"""Mock backends. These let the full pipeline run with no GPU, no mic, no API key — so the skeleton is verifiable and gives every real backend a reference implementation to match. """ from __future__ import annotations import asyncio import itertools import time from typing import AsyncIterator from ..interfaces import ( Frame, Reply, ScreenObservation, Utterance, ) class MockFrameSource: """Emits tiny synthetic frames on a fixed interval.""" def __init__(self, interval: float = 1.0, limit: int | None = None) -> None: self.interval = interval self.limit = limit async def frames(self) -> AsyncIterator[Frame]: for i in itertools.count(): if self.limit is not None and i >= self.limit: return yield Frame( data=b"\x89PNG\r\n\x1a\n", # PNG magic; enough for a stub width=1280, height=720, ts=time.monotonic(), mime="image/png", ) await asyncio.sleep(self.interval) async def aclose(self) -> None: # nothing to release return class MockVision: """Pretends to read the screen. Cycles through a few canned scenes.""" SCENES = [ "VS Code is open with a Python file; a traceback is visible in the terminal.", "A browser shows a GitHub pull request diff.", "A game is running; the player is in a menu screen.", ] def __init__(self) -> None: self._i = 0 async def describe(self, frame: Frame, hint: str | None = None) -> ScreenObservation: scene = self.SCENES[self._i % len(self.SCENES)] self._i += 1 return ScreenObservation(text=scene, ts=frame.ts) class MockSTT: """Feeds a scripted set of user utterances, then goes quiet.""" def __init__( self, script: list[str] | None = None, interval: float = 2.0, loop: bool = False, ) -> None: self.script = script or [ "지금 화면에 뭐 보여?", "저 에러 왜 나는 거야?", "고마워", ] self.interval = interval self.loop = loop async def utterances(self) -> AsyncIterator[Utterance]: while True: for line in self.script: await asyncio.sleep(self.interval) yield Utterance(text=line, ts=time.monotonic(), source="voice") if not self.loop: return async def aclose(self) -> None: return class MockTTS: """'Speaks' by printing. Real TTS swaps in here.""" async def speak(self, reply: Reply) -> None: print(f"[TTS] {reply.text}") class MockBrain: """Echo-style brain that references the current screen, so you can see the screen context actually reaching the conversation loop.""" async def respond(self, user_text, screen, history) -> Reply: seen = screen.text if screen else "아직 화면을 못 읽었어요" return Reply( text=f'(화면: "{seen}") 라고 봤어요. 말씀하신 "{user_text}"에 대해 답하자면… [mock]', ts=time.monotonic(), )