22: from app.staging_gui import microphone_semantic_family as _sf_mod 26: # Windows mic diagnosis (Task6-9). Safe soft-import; gates deactivate gracefully if missing. 28: from app.staging_gui import windows_mic_diagnostics as _wmd_mod 85: "COPY_FILE", 352: # to one class copy, runtime dispatch invokes the other → no forensic 792: _MIC_OFF = "OFF" 793: _MIC_PERMISSION_PENDING = "PERMISSION_PENDING" 794: _MIC_LISTENING = "LISTENING" 795: _MIC_STOPPING = "STOPPING" 796: _MIC_STT_PROCESSING = "STT_PROCESSING" 797: _MIC_HARD_CAP_SEC = 55 859: lbl.pack() 873: class DesktopMicState: 874: state: str = _MIC_OFF 881: mic_start_btn: Any = None 882: mic_stop_btn: Any = None 886: mic_lock: threading.Lock = field(default_factory=threading.Lock) 900: COPY_MIC_PHASE_INSTALLED = False 2495: """Staging layer that adds COPY + MIC buttons to the real Tkinter LightweightChatWindow 2503: hook callbacks, and intercept existing Pausar/Kill commands for mic integration. 2505: track self._last_optimus_normal_response for the Copy button. 2506: 4. Implements mic as default-OFF push-to-talk with hard cap 55s, no background listening, 2564: self._mic_state = DesktopMicState() 2566: self._copy_btn_ref: Any = None 2572: # Task9: real MIC readiness flags (only true after real HW) 2573: self._mic_device_available_live: bool = False 2574: self._mic_real_audio_captured_live: bool = False 2575: self._mic_last_diagnostic_report: Any = None 2576: self._mic_last_retry_proposals: List[Any] = [] 2577: self._mic_last_human_diagnostic_text: str = "" 2762: global COPY_MIC_PHASE_INSTALLED 2793: if getattr(window, "_staging_copy_mic_injected__", False): 2796: setattr(window, "_staging_copy_mic_injected__", True) 2801: setattr(window, "_stg_mic", DesktopMicState()) 2867: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=BEFORE_COPY\n") 2870: self._inject_copy_button(window, root) 2873: "[AFTER_WIDGETS_PHASE] PHASE=COPY_SKIP " 2878: AFTER_WIDGETS_LAST_COMPLETED_PHASE = "COPY" 2879: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=AFTER_COPY\n") 2882: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=BEFORE_MIC\n") 2885: self._inject_microphone_buttons(window, root) 2888: "[AFTER_WIDGETS_PHASE] PHASE=MIC_BUTTONS_SKIP " 2893: AFTER_WIDGETS_LAST_COMPLETED_PHASE = "MIC_BUTTONS" 2894: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=AFTER_MIC\n") 2897: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=BEFORE_PAUSE_KILL_MIC\n") 2900: self._hook_pause_kill_for_mic(window, root) 2909: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=AFTER_PAUSE_KILL_MIC\n") 2912: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=BEFORE_MIC_DEFAULT_OFF\n") 2915: self._install_mic_default_off(window) 2918: "[AFTER_WIDGETS_PHASE] PHASE=MIC_DEFAULT_OFF_SKIP " 2923: AFTER_WIDGETS_LAST_COMPLETED_PHASE = "MIC_DEFAULT_OFF" 2924: sys.stdout.write("[AFTER_WIDGETS_PHASE] PHASE=AFTER_MIC_DEFAULT_OFF\n") 3402: # Note: returns filtered list copy (defensive) 4246: COPY_MIC_PHASE_INSTALLED = True 4563: # (T3/T4/T5 gate driver, T9 mic pipeline driver, T10/T11 final regression). 4572: # is first classified by microphone_semantic_family classifier, storing 4803: # CP7_PB03 FIX: Window marker copy MUST ONLY include 4809: # Only copy 6 whitelisted structural keys. All other 5399: r = cf(captured_text) # Use immutable captured copy NOT entry.get 5961: # 2. _inject_copy_button 5966: # transient-status words via _sanitize_for_copy, writes to Windows 5970: def _inject_copy_button(self, window: Any, root: Any) -> None: 5982: copy_btn = tk.Button( 5992: copy_btn = tk.Button(parent, text="Copiar") 5994: def _copy_handler() -> None: 5997: cleaned = injector._sanitize_for_copy(raw) 5998: injector._copy_to_clipboard_windows(root, cleaned) 6007: copy_btn.configure(command=_copy_handler) 6015: copy_btn.pack(side=side, padx=4, pady=2, after=send_btn) 6018: copy_btn.pack(after=send_btn) 6020: copy_btn.pack() 6021: self._copy_btn_ref = copy_btn 6023: "copy_button_injected", 6025: btn_ok=copy_btn is not None, 6031: # 3. _inject_microphone_buttons 6033: # Creates 🎤 Hablar (mic START) and ⏹ Detener (mic STOP) buttons into 6034: # the button row after any existing copy-btn widget, wires their 6035: # command callbacks into _mic_start_capture / _mic_stop_capture_and_run_stt, 6036: # stores widget references into window._stg_mic so later lifecycle 6037: # methods (_install_mic_default_off, stop-event handlers) can reach them. 6039: def _inject_microphone_buttons(self, window: Any, root: Any) -> None: 6043: mic_state = getattr(window, "_stg_mic", None) 6044: if mic_state is None: 6045: mic_state = DesktopMicState() 6046: setattr(window, "_stg_mic", mic_state) 6047: anchor = getattr(window, "_chat_send_button", None) or self._copy_btn_ref 6055: mic_start_btn = tk.Button( 6068: mic_stop_btn = tk.Button( 6082: mic_start_btn = tk.Button(parent, text="🎤 Hablar") 6083: mic_stop_btn = tk.Button(parent, text="⏹ Detener") 6087: injector._mic_start_capture(window, root) 6089: _dbg_instrument("mic_start_error", error=repr(_se)) 6093: injector._mic_stop_capture_and_run_stt( 6097: _dbg_instrument("mic_stop_error", error=repr(_se)) 6100: mic_start_btn.configure(command=_start_cmd) 6104: mic_stop_btn.configure(command=_stop_cmd) 6108: mic_start_btn.pack(after=anchor, side="left", padx=4, pady=2) 6111: mic_start_btn.pack(after=anchor) 6113: mic_start_btn.pack() 6115: mic_stop_btn.pack(after=mic_start_btn, side="left", padx=4, pady=2) 6118: mic_stop_btn.pack(after=mic_start_btn) 6120: mic_stop_btn.pack() 6121: mic_state.mic_start_btn = mic_start_btn 6122: mic_state.mic_stop_btn = mic_stop_btn 6123: mic_state.root_ref = root 6125: mic_state.input_entry = getattr(window, "_chat_input_entry", None) 6127: mic_state.input_entry = None 6129: "mic_buttons_injected", 6130: start_ok=mic_start_btn is not None, 6131: stop_ok=mic_stop_btn is not None, 6137: # 4. _hook_pause_kill_for_mic 6140: # button original commands. If microphone is currently LISTENING or 6147: def _hook_pause_kill_for_mic(self, window: Any, root: Any) -> None: 6171: mic_state = getattr(window, "_stg_mic", None) 6172: if mic_state is not None: 6173: cur_state = getattr(mic_state, "state", _MIC_OFF) 6174: if cur_state in (_MIC_LISTENING, _MIC_STOPPING): 6176: ev = getattr(mic_state, "stop_event", None) 6182: mic_state.state = _MIC_STOPPING 6183: mic_state.last_stop_reason = "PAUSE_BTN_INTERRUPT" 6191: mic_state = getattr(window, "_stg_mic", None) 6192: if mic_state is not None: 6193: cur_state = getattr(mic_state, "state", _MIC_OFF) 6194: if cur_state in (_MIC_LISTENING, _MIC_STOPPING, _MIC_STT_PROCESSING): 6196: ev = getattr(mic_state, "stop_event", None) 6202: mic_state.state = _MIC_STOPPING 6203: mic_state.last_stop_reason = "KILL_SWITCH_INTERRUPT" 6237: # 5. _install_mic_default_off 6239: # DEFAULT-on-start mic posture. Ensures state is OFF, START button is 6243: def _install_mic_default_off(self, window: Any) -> None: 6245: mic_state = getattr(window, "_stg_mic", None) 6246: if mic_state is None: 6249: mic_state.state = _MIC_OFF 6253: ev = getattr(mic_state, "stop_event", None) 6258: start_btn = getattr(mic_state, "mic_start_btn", None) 6259: stop_btn = getattr(mic_state, "mic_stop_btn", None) 6266: start_btn.pack() 6279: "mic_default_off_installed", 6282: state=str(getattr(mic_state, "state", "?")), 6290: # Hardens the mic OFF-default posture against any background/open-mic 6301: setattr(window, "_stg_mic_listen_gate_armed", True) 6306: # If user closes root while LISTENING → force mic teardown cleanly 6308: mic_state = getattr(window, "_stg_mic", None) 6309: if mic_state is not None: 6311: ev = getattr(mic_state, "stop_event", None) 6317: mic_state.state = _MIC_OFF 6321: stream = getattr(mic_state, "stream", None) 6335: mic_state.stream = None 6339: timer = getattr(mic_state, "hard_cap_timer", None) 6377: gate_armed=bool(getattr(window, "_stg_mic_listen_gate_armed", False)), 6387: # -- Copy-button helpers ------------------------------------------------ 6388: def _sanitize_for_copy(self, raw_response: str) -> str: 6507: def _copy_to_clipboard_windows(self, root: Any, text: str) -> bool: 6522: # -- Microphone helpers ------------------------------------------------- 6532: def _mic_start_capture(self, window: Any, root: Any) -> None: 6533: """Open PyAudio stream, begin appending frames to mic_state.frames list, 6534: schedule hard-cap auto-stop after _MIC_HARD_CAP_SEC seconds. 6539: que quieres hablar", "no puedo acceder al micrófono"}; has_details ∈ 6542: def _write_mic_diagnosis(kind: str) -> None: 6548: "Entiendo que quieres hablar con el micrófono, pero no puedo acceder al micrófono " 6550: " · Permisos Windows: entra a Configuración → Privacidad → Micrófono y activa el " 6552: " · Sin dispositivo predeterminado: en Sonido → Grabación marca tu micrófono como " 6562: " · Permisos Windows: valida el acceso al micrófono en Configuración → Privacidad.\n" 6565: "micrófono aparece sin errores.\n" 6570: "Entiendo que quieres hablar, pero no puedo abrir el canal de audio del micrófono.\n\n" 6571: " · Revisa configuración: otra aplicación puede tener el micrófono ocupado (zoom, " 6573: " · Dispositivos encontrados: tu micrófono está desconectado o deshabilitado.\n" 6575: " · Permisos Windows: vuelve a validar acceso al micrófono desde Configuración.\n" 6590: _show_tk_toast(root, "Micrófono no disponible: revisa permisos / dispositivo.") 6597: armed = getattr(window, "_stg_mic_listen_gate_armed", None) 6599: _dbg_instrument("mic_start_blocked_gate", armed=armed) 6601: mic_state = getattr(window, "_stg_mic", None) 6602: if mic_state is None: 6605: lock = getattr(mic_state, "mic_lock", None) 6615: if getattr(mic_state, "state", _MIC_OFF) not in (_MIC_OFF,): 6617: "mic_start_skipped_wrong_state", 6618: state=getattr(mic_state, "state", "?"), 6624: mic_state.state = _MIC_PERMISSION_PENDING 6625: mic_state.last_stop_reason = "PYAUDIO_UNAVAILABLE" 6628: _write_mic_diagnosis("PYAUDIO_UNAVAILABLE") 6630: mic_state.pyaudio_mod = pa 6637: mic_state.state = _MIC_PERMISSION_PENDING 6638: mic_state.last_stop_reason = "PYAUDIO_INIT_FAIL" 6641: _write_mic_diagnosis("PYAUDIO_INIT_FAIL") 6643: mic_state.audio_interface = audio_if 6646: mic_state.frames = frames_list 6650: stop_ev = getattr(mic_state, "stop_event", None) 6665: stop_ev_now = getattr(mic_state, "stop_event", None) 6681: mic_state.state = _MIC_PERMISSION_PENDING 6682: mic_state.last_stop_reason = "OPEN_STREAM_FAIL" 6685: _write_mic_diagnosis("OPEN_STREAM_FAIL") 6687: mic_state.stream = stream 6688: mic_state.state = _MIC_LISTENING 6696: sev = getattr(mic_state, "stop_event", None) 6702: self._mic_stop_capture_and_run_stt( 6711: cap_timer = threading.Timer(float(_MIC_HARD_CAP_SEC), _hard_cap_timeout) 6714: mic_state.hard_cap_timer = cap_timer 6718: sb = getattr(mic_state, "mic_start_btn", None) 6724: spb = getattr(mic_state, "mic_stop_btn", None) 6731: spb.pack(after=sb) if sb is not None else spb.pack() 6737: "mic_capture_started", 6739: hard_cap_sec=_MIC_HARD_CAP_SEC, 6741: self._mic_device_available_live = True 6751: def _mic_stop_capture_and_run_stt( 6760: mic_state = getattr(window, "_stg_mic", None) 6761: if mic_state is None: 6764: lock = getattr(mic_state, "mic_lock", None) 6773: mic_state.last_stop_reason = str(reason or "USER_STOP") 6777: timer = getattr(mic_state, "hard_cap_timer", None) 6786: ev = getattr(mic_state, "stop_event", None) 6792: stream = getattr(mic_state, "stream", None) 6810: mic_state.stream = None 6814: cur_state = getattr(mic_state, "state", _MIC_OFF) 6815: if cur_state != _MIC_OFF: 6816: mic_state.state = _MIC_STT_PROCESSING 6819: frames = list(getattr(mic_state, "frames", []) or []) 6826: reason=mic_state.last_stop_reason or reason, 6832: mic_state.state = _MIC_OFF 6836: self._install_mic_default_off(window) 6839: self._mic_real_audio_captured_live = bool(frames) 6841: "mic_capture_stopped", 6842: reason=mic_state.last_stop_reason, 6861: mic_state = getattr(self, "_mic_state", None) 6865: for winst in [getattr(self, "_copy_btn_ref", None)]: 7025: def get_mic_readiness_flags(self) -> Dict[str, Any]: 7028: T9-TR4 4-case truth table: mic_ready_ok = bool(device_avail_live) AND 7035: device_avail = bool(getattr(self, "_mic_device_available_live", False)) 7036: captured_live = bool(getattr(self, "_mic_real_audio_captured_live", False)) 7038: "mic_device_available_live": device_avail, 7039: "mic_real_audio_captured_live": captured_live, 7040: "mic_ready_ok": bool(device_avail and captured_live), 7041: "last_diagnostic_report": getattr(self, "_mic_last_diagnostic_report", None), 7042: "last_retry_proposals": list(getattr(self, "_mic_last_retry_proposals", []) or []), 7044: getattr(self, "_mic_last_human_diagnostic_text", "") or "" 7557: # no secondary worker, late callback (e.g. mic diagnostic event), retry 9521: # the wrong class copy. DO NOT assume same-file → same-class. 10155: # fire for every resolve() call on this class copy 10816: # Attempt 2: frozen dataclass → build fresh copy with overrides 10825: _old_p_for_copy = _fld_override.get("parser_source", "") 10826: _fld_override["parser_source"] = f"{str(_old_p_for_copy or '') or 'UNKNOWN'}+PROVENANCE_DENIED" 10827: _cls_for_copy = _resolver_ret.__class__ 10828: _resolver_ret = _cls_for_copy(**_fld_override) 12462: # marker copy _stg_s8_marker_latest at wrapper finally. 12638: Historical public symbol imported by run_live_desktop_with_copy_and_mic.py