Skip to content

fix(pipeline): alucinacao do STT nao vira mais voz no cabo virtual (#47) - #73

Open
caioross wants to merge 1 commit into
mainfrom
auto/issue-47-filtro-alucinacao
Open

fix(pipeline): alucinacao do STT nao vira mais voz no cabo virtual (#47)#73
caioross wants to merge 1 commit into
mainfrom
auto/issue-47-filtro-alucinacao

Conversation

@caioross

@caioross caioross commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Contexto

Quando o VAD fecha um segmento sem fala de verdade (respiração, clique de teclado, música ao fundo), o faster-whisper não devolve vazio: devolve uma frase plausível — "Legendas pela comunidade Amara.org", "Thank you.", "...", repetições. O pipeline não tinha defesa: o único filtro era if not text.strip(), e os metadados que o próprio Whisper já calcula (no_speech_prob, avg_logprob) morriam no " ".join(...) de STT.transcribe*.

Como o Laguna fala a tradução no cabo virtual, isso significava o app conversando sozinho com a call de Discord enquanto o usuário está calado — e queimando uma frase inteira de STT+MT+TTS de fila (seg_q tem maxsize=4) por ruído.

O que mudou

laguna_pipeline.py

  • is_hallucination(text, no_speech_prob=None, avg_logprob=None) — decisão pura (só texto e números; sem áudio, modelo, GPU ou rede), em três camadas:
    1. blocklist normalizada (minúsculas, sem acento/pontuação) PT+EN, curta e comentada — só entra o que ninguém diz numa call. "obrigado" sozinho não está lá; "obrigado por assistir" está;
    2. repetição patológica (uma palavra ≥80% do segmento, ou loop curto alternado);
    3. metadados, só em conjunção (no_speech_prob > 0.6 E avg_logprob < -1.0).
  • STT.transcribe_detailed(pcm, detect_lang=False) -> STTResult agrega os metadados por duração dos segmentos. transcribe() e transcribe_with_lang() passaram a delegar e mantiveram as assinaturasfase0_poc.py, bench_fase0.py, stress_*.py, test_offline.py e test_lang_detect.py não precisaram de uma linha.

laguna_core.py_process_segment barra o segmento reprovado antes do MT/TTS: nada de tradução, nada de síntese, nada no sink. O texto barrado ainda é emitido como stt (o usuário vê o que foi descartado) e o descarte vira skipped com chave própria mt.hallucination. A latência de STT continua contando — o trabalho existiu, e a métrica não pode mentir por omissão (#21).

static/ — o handler de skipped passa a resolver ev.key; evento antigo (mesmo idioma) não manda chave e continua no fallback mt.skipped. Chave nova nos dois dicionários.

Por que a conjunção nos metadados, e não avg_logprob sozinho

Medido agora, com o modelo real, no dry_pt2en.wav do dono:

áudio texto no_speech_prob avg_logprob barrado?
dry_pt2en.wav (fala real PT) "Ei, galera, eu estou testando o translator…" 0,0031 -0,7512 não
dry_en2pt.wav (fala real EN) "There, everyone, this is a real-time translator test…" 0,0011 -0,4937 não
silêncio 1s '' sim

A fala legítima em PT já vem com avg_logprob -0,75, a 0,25 do limiar. Um filtro que reprovasse por esse eixo isoladamente estaria a um sotaque de distância de emudecer fala real — daí a conjunção e o viés declarado: falso negativo é preferível a falso positivo.

Durante o desenvolvimento o limiar de repetição estava em 0,75 e barrava "vai vai vai time" (grito de torcida, 3/4 palavras). Subiu para 0,8 com o caso registrado em comentário no código e em teste.

Gate (HANDBOOK §6) — resultado real

  • T1compileallCOMPILE_OK; import-smoke de fase0_poc, laguna_core, laguna_serverIMPORTS_OK.
  • T2test_offline.py "dry_pt2en.wav" --direction pt2en --model small --device auto — exit 0, out_gate.wav gerado, fala real traduzida ponta a ponta: STT 629ms · MT 500ms · TTS 2592ms (cuda/float16, primeira execução da worktree, modelos recém-baixados). Zero regressão de recall.
  • T3node --check static/app.js e static/i18n.jsJS_OK; tests_unit/test_i18n_parity.py → 2 passed (paridade PT/EN mantida).
  • pytesttests_unit/149 passed (18 novos em test_hallucination_filter.py).

Nenhuma constante de VAD ou default de modelo foi tocada — o caminho quente do áudio não mudou. O filtro roda sobre texto, depois do STT, e custa uma normalização de string por frase.

Riscos

  • Falso positivo em fala real é o risco de verdade. Mitigado pelo viés conservador, pelos limiares medidos acima e por testes que fixam "sim", "ok", "yes", "Obrigado" e ênfase curta como não-barráveis. Se aparecer um caso real barrado, o conserto é remover a entrada da blocklist ou afrouxar um limiar — tudo em constantes nomeadas no topo do bloco.
  • "tchau" e "thank you" na blocklist são um trade-off explícito: são despedidas legítimas, mas estão entre as alucinações mais frequentes do Whisper em silêncio. Perder um "tchau" no fim de uma call custa menos que o app falar sozinho no meio dela. Fácil de reverter se o dono discordar.
  • transcribe_detailed é o único ponto que fala com o modelo agora; os dois métodos históricos são casca. Comportamento idêntico ao anterior (mesmos parâmetros de transcribe), mas o T2 acima exercitou justamente esse caminho.

Solicito quórum (HANDBOOK §7)

Closes #47

Segmento sem fala real (respiracao, clique, musica ao fundo) nao volta vazio
do faster-whisper: volta uma frase plausivel ("Legendas pela comunidade
Amara.org", "Thank you."). Como o Laguna FALA a traducao no VB-CABLE, isso
virava o app conversando sozinho na call de Discord — e ainda queimava uma
frase inteira de STT+MT+TTS de fila por ruido.

O unico filtro existente era `if not text.strip()`, e os metadados que o
proprio Whisper calcula (`no_speech_prob`, `avg_logprob`) eram descartados no
`" ".join(...)` de `STT.transcribe*`.

- `is_hallucination()` em laguna_pipeline.py: funcao pura (so texto e numeros,
  sem audio/modelo/GPU) com blocklist normalizada PT+EN, detector de repeticao
  patologica e o par de metadados em CONJUNCAO. Todos os limiares sao
  conservadores: barrar fala real e pior que deixar passar um ruido.
- `STT.transcribe_detailed()` agrega os metadados por duracao e devolve
  `STTResult`; `transcribe()`/`transcribe_with_lang()` delegam e mantem as
  assinaturas — nenhum chamador (fase0_poc, bench, stress, test_offline,
  test_lang_detect) precisou mudar.
- `_process_segment` barra o segmento antes do MT/TTS e emite `skipped` com a
  chave propria `mt.hallucination`; a UI resolve `ev.key` (evento antigo de
  mesmo idioma segue no fallback). Painel nunca fica mudo sem explicacao.
- tests_unit/test_hallucination_filter.py: frases-lixo PT/EN, repeticao,
  fala curta legitima que NAO pode ser barrada, limiares nas duas bordas,
  agregacao ponderada e o laco de traducao com dubles.

Closes #47
@caioross

caioross commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Estado da CI: falha de infraestrutura do GitHub, não do código

O job pytest (lógica pura) passou nas duas execuções (3m57s). O job compile + lint mínimo falhou duas vezes sem executar um único passo do repositório:

  1. primeira execução — morreu em Set up job: Failed to resolve action download info. Error: Service Unavailable (3 tentativas do próprio runner);
  2. re-execuçãoThe job was not acquired by Runner of type hosted even after multiple attempts após 15m01s.

Nenhuma das duas chegou a rodar compileall. São modos de falha da plataforma (resolução de action e alocação de runner), não do diff.

O gate local do HANDBOOK §6 — que é o que vale, já que o CI é subconjunto do T1 — está verde e foi rodado com C:\Python313\python.exe na worktree:

  • T1 compileallCOMPILE_OK; import fase0_poc, laguna_core, laguna_serverIMPORTS_OK
  • T2 test_offline.py dry_pt2en.wav --device auto → exit 0, out_gate.wav gerado
  • T3 node --checkJS_OK; test_i18n_parity.py → 2 passed
  • tests_unit/ → 149 passed

Para o PR Doctor: re-executar o job compile + lint mínimo antes do quórum — o pré-requisito de CI verde não foi satisfeito por indisponibilidade da plataforma, e essa evidência ainda precisa ser obtida. Não estou pedindo dispensa do requisito.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pipeline: alucinação do Whisper em segmento sem fala vira voz no Discord — nenhum filtro, e no_speech_prob/avg_logprob são descartados

1 participant