fix: P0 fixes — VAD hint routing, session leak, YAML regex, encoding

- 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) <noreply@anthropic.com>
This commit is contained in:
L'électron rare
2026-03-27 05:59:24 +01:00
parent f673822755
commit f87820e105
3 changed files with 19 additions and 9 deletions
+6 -6
View File
@@ -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
+12 -2
View File
@@ -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)
+1 -1
View File
@@ -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: