fix(core): laco de traducao sobrevive a falha de UMA frase — erro recuperavel vs. fatal (#45) - #60
Conversation
O laco de traducao rodava inteiro dentro do mesmo `try` que cobre o setup: uma excecao em qualquer engine (Piper engasgando num texto do Argos, hiccup de CUDA) encerrava a direcao pelo resto da sessao. Pior, sem setar `_stop`: `_capture`/`_segmenter` seguiam rodando, o medidor de entrada continuava se mexendo, `seg_q` enchia e emitia `overload` — o app parecia vivo, segurando o device de captura, e nao traduzia mais nada. A UI recebia `error` e desabilitava o Parar, entao a unica saida era Iniciar (recarga de modelos no meio da conversa). Era a unica etapa do pipeline sem resiliencia: a captura ja tem laco de reconexao com budget de retries (#12) e `_play` ja isola device a device (#38). - `_process_segment(seg, stt, mt, tts)`: corpo do laco extraido sem mudanca de logica — a falha de uma frase fica isolavel num `try` proprio e o caminho quente vira testavel com dubles, sem sounddevice/modelo/GPU. - `_translation_loop(seg_q, stt, mt, tts)`: falha de segmento emite `error.segment_failed` (`recoverable=True`) e SEGUE para o proximo. Contador de falhas CONSECUTIVAS; frase boa zera (mesma doutrina de CAPTURE_HEALTHY_S). - `SEGMENT_MAX_FAILURES = 3`: no limiar, emite `error.direction_lost` e encerra de verdade — setando `_stop` ANTES de sair, para nao deixar thread zumbi. Numero de robustez, nao de latencia: o caminho feliz nunca toca o contador. - Excecao com `_stop` ja setado nao vira erro: parar no meio de uma frase e Stop, nao falha (mesmo guarda de `_OutputSink._report`). - Falha de SETUP continua matando a direcao na hora — "sem modelo" nao vira retry infinito. - `_emit_key` aceita `**data` para campos que a UI le sem traduzir. - `static/app.js`: `error` com `recoverable` mostra o aviso e NAO derruba os botoes (derrubar era o bug: Parar desabilitado com o worker vivo). - `static/i18n.js`: `error.segment_failed` e `error.direction_lost` em PT/EN. Metrica intacta: as latencias so entram em `_lat_*` no fim de `_process_segment`, entao segmento que falhou nao entra na conta — e o erro sempre aparece, o laco so passa a sobreviver a ele. Refs #45 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6 casos sobre `_translation_loop` com dubles de STT/MT/TTS (o `conftest.py` ja stuba sounddevice; `_play` e neutralizado porque a reproducao tem cobertura propria em test_output_sink.py): - falha em 1 segmento nao impede o proximo (o evento vem com `recoverable`); - a guarda cobre MT e TTS, nao so o STT; - frase boa zera o contador: 3 falhas alternadas nunca viram fatal; - N consecutivas emitem `error.direction_lost`, param de consumir a fila e setam `_stop` (o anti-zumbi da issue); - excecao com `_stop` ja setado nao vira erro (Stop nao e falha); - segmento sem fala nao gasta orcamento de falhas. A fila de teste sinaliza o stop no `get` vazio — mesmo ponto onde o laco real espera — para o teste encerrar sem timer nem sleep e sem tocar o caminho de excecao sob teste. Refs #45 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
O erro recuperavel introduzido nesta PR pintava o painel de vermelho e ficava la: nenhum evento do caminho feliz (stt/mt/latency) repinta status, entao uma frase perdida no minuto 2 deixava "Frase perdida (2/3)" na tela pelo resto da sessao — com o contador ja zerado no backend na frase boa seguinte. Era o erro simetrico ao que a issue corrigiu: antes o painel mentia dizendo que morreu; agora mentiria dizendo que esta quase morrendo. - aviso vai para a classe `warn` (ambar, transitorio) e volta a `status.running` depois de RECOVERABLE_STATUS_MS; - toda pintura de status cancela a volta pendente, para o timer nunca repintar "rodando" por cima de um erro fatal, de um Parar ou de um novo Iniciar que cheguem dentro da janela; - `.status.warn` no style.css reusa a cor de aviso ja existente, em vez de emprestar a classe `loading` (que significa outra coisa). Achado do quorum adversarial (3/3 lentes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🩺 Parecer do PR Doctor — quórum §7.2 concluído, 3× APROVABalde B (área de quórum): diff em Rodada 1 — 1 APROVA / 2 VETAAs três lentes convergiram num achado que esta PR introduziu: o erro recuperável pintava o painel de vermelho e nunca voltava. Nenhum evento do caminho feliz ( Reparo aplicado — 48b85e2 (só frontend)
Rodada 2 — veredito das 3 lentes sobre
|
Contexto
O laço de tradução rodava inteiro dentro do mesmo
tryque cobre o setup: uma exceção em qualquer engine (Piper engasgando num texto vindo do Argos, hiccup de CUDA) encerrava a direção pelo resto da sessão.Pior que parar: parava sem setar
_stop._capture/_segmenterseguiam rodando, o medidor de entrada continuava se mexendo,seg_qenchia e passava a emitiroverload— o app parecia vivo, segurando o device de captura e queimando CPU, sem traduzir nada. A UI recebiaerror, desabilitava o Parar e a única saída era Iniciar: recarga de modelos no meio da conversa.Era a única etapa do pipeline sem resiliência. A captura já tem laço de reconexão com orçamento de retries (#12) e
_playjá isola device a device (#38).O que mudou
laguna_core.py_process_segment(seg, stt, mt, tts)— corpo do laço extraído sem mudança de lógica (o diff é literalmente o mesmo bloco, comcontinue→return). Isso torna a falha de uma frase isolável numtrypróprio e o caminho quente testável com dublês._translation_loop(seg_q, stt, mt, tts)— falha de segmento emiteerror.segment_failed(comrecoverable=True) e segue para o próximo. Contador de falhas consecutivas; uma frase boa zera (mesma doutrina deCAPTURE_HEALTHY_S).SEGMENT_MAX_FAILURES = 3— no limiar emiteerror.direction_loste encerra de verdade, setando_stopANTES de sair de_run, para captura e segmentador saírem junto (o anti-zumbi da issue)._stopjá setado não vira erro: parar no meio de uma frase é Stop, não falha — mesmo guarda de_OutputSink._report._emit_keypassa a aceitar**datapara campos que a UI lê sem traduzir.static/app.js:errorcomrecoverablemostra o aviso e não derruba os botões (derrubar era o bug — Parar desabilitado com o worker vivo). Erro fatal segue igual.i18n.js:error.segment_failedeerror.direction_lostem PT e EN.tests_unit/test_translation_loop.py(novo, 6 casos, sem áudio/modelo/GPU/rede): falha isolada → próxima frase processa; a guarda cobre MT e TTS além do STT; 3 falhas alternadas nunca viram fatal; N consecutivas → fatal + para de consumir a fila +_stopsetado; exceção durante o Stop não vira erro; segmento sem fala não gasta orçamento de falhas.A métrica não passa a mentir por omissão (família da #21): as latências só entram em
_lat_*no fim de_process_segment, então o segmento que falhou não entra na conta — e o evento de erro sempre aparece. A diferença é o laço sobreviver a ele.Gate (HANDBOOK §6) — resultado real
compileall -q .→COMPILE_OK; import-smoke defase0_poc, laguna_core, laguna_server→IMPORTS_OK(C:\Python313\python.exe).pytest tests_unit -q→ 54 passed (48 antes + 6 novos), inclusivetest_i18n_parity.py.laguna_core.py) —test_offline.py "…/dry_pt2en.wav" --direction pt2en --model small --device auto→ verde,out_gate.wavgerado:static/) —node --check app.js+node --check i18n.js→JS_OK; paridade PT/EN pelo teste versionado.Riscos
errorganha um campo opcionalrecoverable; cliente antigo ignora e cai no comportamento de hoje.CAPTURE_MAX_RETRIES = 5, mas menor porque aqui cada tentativa custa uma frase da conversa.Fora de escopo (como a issue pediu)
Não ampliei a PR para: (a)
laguna_serverremover o worker de_workersno erro fatal — hoje o worker fatal fica registrado com_stopsetado, e o próximo Iniciar o recria; (b) tratamento visual dedicado do erro recuperável (hoje ele reusa o statuserrordo painel, com os botões preservados). Ambos merecem issue própria.Solicito quórum (HANDBOOK §7)
Closes #45