From f87820e1056cc74ff82cdee5dbe1fc989e6f71db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:59:24 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20P0=20fixes=20=E2=80=94=20VAD=20hint=20ro?= =?UTF-8?q?uting,=20session=20leak,=20YAML=20regex,=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hint routing regex to VAD auto-transcribe path (was bypassed) - Clean up audio sessions on WebSocket disconnect (memory leak) - Fix YAML extraction regex (double-escaped backslashes) - Fix corrupted markdown output (literal \\n) - Add UTF-8 encoding to export_md write_text() Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/dev/local_studio_ai_gateway.py | 12 ++++++------ tools/dev/mascarade_voice_bridge.py | 14 ++++++++++++-- tools/scenario/export_md.py | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tools/dev/local_studio_ai_gateway.py b/tools/dev/local_studio_ai_gateway.py index d9f4c1a..5030c26 100644 --- a/tools/dev/local_studio_ai_gateway.py +++ b/tools/dev/local_studio_ai_gateway.py @@ -310,7 +310,7 @@ def to_float(value: Any, fallback: float) -> float: def extract_yaml(payload: str) -> str: - block_match = re.search(r"```ya?ml\\s*\\n([\\s\\S]*?)```", payload, re.IGNORECASE) + block_match = re.search(r"```ya?ml\s*\n([\s\S]*?)```", payload, re.IGNORECASE) if block_match: return block_match.group(1).strip() @@ -420,11 +420,11 @@ def build_printables_plan(scenario_id: str, title: str, selected: list[str] | No yaml_text = safe_dump(manifest_out, sort_keys=False, allow_unicode=True).strip() markdown = ( - f"# Pack imprimables — {manifest_out['title']}\\n\\n" - f"- Scenario: {manifest_out['scenario_id']}\\n" - f"- Items: {len(filtered)}\\n\\n" - + "\\n".join(f"- {entry.get('id')} ({entry.get('category')})" for entry in filtered) - + "\\n\\nGénéré via gateway IA locale.\\n" + f"# Pack imprimables — {manifest_out['title']}\n\n" + f"- Scenario: {manifest_out['scenario_id']}\n" + f"- Items: {len(filtered)}\n\n" + + "\n".join(f"- {entry.get('id')} ({entry.get('category')})" for entry in filtered) + + "\n\nGénéré via gateway IA locale.\n" ) return yaml_text, markdown diff --git a/tools/dev/mascarade_voice_bridge.py b/tools/dev/mascarade_voice_bridge.py index 15c6b4b..e5f56c9 100644 --- a/tools/dev/mascarade_voice_bridge.py +++ b/tools/dev/mascarade_voice_bridge.py @@ -358,6 +358,7 @@ async def voice_websocket(websocket: WebSocket, token: str = ""): await websocket.accept() client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" logger.info("Client connected: %s", client) + device_id = None try: # --- Handshake --- @@ -393,6 +394,8 @@ async def voice_websocket(websocket: WebSocket, token: str = ""): except WebSocketDisconnect: logger.info("Client disconnected: %s", client) + if device_id and device_id in _audio_sessions: + del _audio_sessions[device_id] except asyncio.TimeoutError: logger.warning("Handshake timeout: %s", client) if websocket.client_state == WebSocketState.CONNECTED: @@ -505,8 +508,15 @@ async def _handle_audio(ws: WebSocket, frame: bytes, device_id: str): logger.info("[%s] Auto-STT (%.1fs): %s", device_id, t_stt, transcription[:80]) await ws.send_json({"type": "stt", "text": transcription}) - # Feed into LLM pipeline - response = await _query_llm(transcription) + # Feed into LLM pipeline — detect hint routing + hint_match = re.match(r'^\[HINT:(\w+):(\d)\]\s*(.*)', transcription) + if hint_match: + puzzle_id = hint_match.group(1) + hint_level = int(hint_match.group(2)) + question = hint_match.group(3) or "Give me a hint" + response = await _query_hints(puzzle_id, question, hint_level, device_id) + else: + response = await _query_llm(transcription) await _send_tts_response(ws, response, device_id) else: logger.warning("[%s] Auto-STT returned empty", device_id) diff --git a/tools/scenario/export_md.py b/tools/scenario/export_md.py index 00761f0..b702564 100755 --- a/tools/scenario/export_md.py +++ b/tools/scenario/export_md.py @@ -412,7 +412,7 @@ def build_brief_doc(model: dict) -> str: def write_file(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content) + path.write_text(content, encoding="utf-8") def export_files(scenario_path: Path, out_path: Path | None = None) -> None: