Documentação de auditoria: o que o score mede, por que mede assim, e como cada número é calculado. Parte I é para decisão de negócio; Parte II é o detalhamento técnico completo.
Política: priority_gated v1 · Projeto: vad_interval_project_v2 · Última revisão: 2026-07-16
O agente de voz atende ligações telefônicas. O áudio nasce em G729 a 8 kHz e é convertido para G711 a 16 kHz no SBC antes de chegar ao LiveKit — uma cadeia fixa, que não podemos alterar. Dentro do LiveKit, o VAD (detector de atividade de voz) toma três decisões que definem a experiência da ligação:
Todo erro do VAD cai em uma de seis categorias. A ordem abaixo é uma decisão de negócio (o que dói mais na operação), e é a espinha dorsal de toda a metodologia:
| # | Erro | O que acontece na ligação | Peso |
|---|---|---|---|
| 1 | Perder fala principal | O cliente falou e o sistema não ouviu. O whisper nunca vê o áudio; não há transcrição, não há correção possível. O agente responde como se o cliente estivesse em silêncio. Irrecuperável. | eliminatório (gate) |
| 2 | Segmento órfão (ativar no nada) | O VAD "ouviu" fala onde não há nenhuma (nem de fundo). Um chunk de ruído/silêncio vai ao whisper, que é conhecido por alucinar texto plausível nesses casos — o agente responde a algo que ninguém disse. Também dispara barge-in falso. | 0.80 |
| 3 | Ponte entre falas (emendar por cima de silêncio longo) | Duas falas separadas viram um segmento só. A resposta do agente atrasa (espera o segmento fechar) e turnos distintos do cliente se misturam numa transcrição só. Grave entre duas falas principais; menos grave quando envolve fala de fundo (ver #5). | 0.60 |
| 4 | Spill (sobra fora da margem perdoada) | O segmento invade áudio não-rotulado além da tolerância — ruído/silêncio anexado à fala real. Infla o payload do STT e aumenta o risco de lixo na transcrição. | 0.40 |
| 5 | Fala de fundo (e pontes que a envolvem) | Vozes de terceiros (TV, pessoas ao lado) entram no payload. O whisper transcreve texto real que não é do cliente. Peso menor porque geralmente vem anexada à fala legítima — mas é o erro mais insidioso quando aparece sozinho (ver §II-5). | 0.30 / 0.20 |
| 6 | Fragmentação | Uma fala vira vários segmentos. Hoje é amortecida pelo min_endpointing_delay do
LiveKit (o agente não responde imediatamente a cada fim de segmento), por isso é a menos grave.
Se o endpointing mudar, esta posição deve ser revista. |
0.08 |
Um único número nunca conta a história toda. A política priority_gated separa a decisão
em três camadas, cada uma respondendo a uma pergunta diferente:
Analogia: uma licitação. Os gates são os requisitos de habilitação (quem não cumpre está fora, não importa o preço); o score é a nota técnica que ordena os habilitados; o vetor é o parecer detalhado que justifica a nota e orienta a próxima rodada.
Score escalar é uma taxa de câmbio entre erros, e taxa de câmbio sempre permite trocas. Na
comparação empírica de 2026-07-15, a configuração campeã antiga (act 0.34) perdia
6 das 111 falas do dataset — clientes literalmente ignorados — e ainda pontuava
0.837, a três centésimos do topo. A trava de nível de locutor perdia 5 e pontuava 0.839.
Num grid search automatizado, isso é uma armadilha: qualquer mecanismo que compre "limpeza" suficiente compensaria numericamente a perda de fala, e o otimizador escolheria um VAD que ignora 5% dos clientes. Ajustar pesos não resolve o dilema (peso alto demais → o score vira ruído; baixo demais → a troca continua permitida). A perda de fala não é mais cara — é inaceitável. Coisas inaceitáveis são restrições, não preços. Daí os gates.
streaming final.O detector roda exatamente como em produção (mesma classe, mesmos frames, via
livekit-agents VADStream). As tolerâncias de avaliação nunca são repassadas ao detector —
são régua, não instrumento.
| Classe | O que marca | Papel na métrica |
|---|---|---|
speech | Fala do locutor principal (o cliente), a fala audível — não o chunk com contexto | Base de captura, pontes, fragmentação, perdas |
background_speech | Vozes de terceiros (TV, ambiente) | Intrusão de fundo (peso 0.20) e pontes mistas (0.30); nunca conta como captura |
non_speech_event | Música de espera, URA, bipes, DTMF | Diagnóstico: divide os órfãos em "ativou em evento" vs "ativou no nada" (não altera o score) |
Convenções em vigor (deduzidas dos dados e agora normativas):
bridge_grace_s < 0,8 é
consistente com o labeling (usamos 0,30).com_trechos (rotulado), sem_fala (áudio sem nenhuma
fala — categoria hoje vazia e prioritária para expansão), pendente, descartado.| Parâmetro | Default | O que perdoa | Racional |
|---|---|---|---|
left_edge_tolerance_s | 0.100 | Não cobrir até 100 ms do início de cada rótulo | Imprecisão humana de borda + prefix padding cobre o contexto no payload real |
right_edge_tolerance_s | 0.100 | Não cobrir até 100 ms do fim de cada rótulo | Idem; caudas de fonemas são ambíguas até para humanos |
outer_edge_tolerance_s | 0.150 | Sobrar até 150 ms além de cada rótulo, de cada lado | Hangover natural do detector; não é spill |
bridge_grace_s | 0.300 | Até 300 ms de silêncio interno entre duas falas no mesmo segmento | Abaixo da convenção de labeling (0,8 s); só o excesso vira ponte penalizada |
Núcleo mensurável: ao aplicar as tolerâncias de borda num rótulo, sobra o "núcleo" que o VAD é obrigado a cobrir. Para rótulos curtíssimos, um piso interno de 32 ms garante que sempre exista núcleo (senão um rótulo de 150 ms seria 100% perdoável). Rótulo de 2 s com l=r=0,1 → núcleo de 1,8 s.
Cada segmento predito é decomposto contra os rótulos. As definições são mutuamente exclusivas
(sem dupla contagem) e todas as taxas usam o mesmo denominador: duração predita total.
O max garante que nem muitos órfãos curtinhos (count) nem um órfão longo (duração)
escapem. O overlap com rótulos non_speech_event é reportado à parte
(orphan_on_labeled_event_s) para separar "ativou na música de espera" de "ativou no nada".
Mesma construção, mas para pares de vizinhos onde pelo menos um é fala de fundo (fala↔fundo ou fundo↔fundo). Menos grave que emendar duas falas do cliente (0.30 vs 0.60) — mistura turnos, mas não turnos legítimos entre si.
A subtração evita contar o mesmo segundo duas vezes: um órfão é 100% spill por definição, e o silêncio de uma ponte também é spill — cada segundo é punido apenas na sua categoria mais específica (que é também a mais grave).
Overlap do segmento com rótulos de fundo fora das zonas de tolerância externa da fala principal (fundo colado na fala do cliente é perdoado — inseparável na prática).
priority_v4
do projeto v1 — a política vencedora do diagnóstico de 2026-07-15 — apenas embrulhado pelos gates.| Gate | Default | Métrica exata | Flag |
|---|---|---|---|
| Falas perdidas | ≤ 1 | nº de rótulos de fala com overlap ZERO com qualquer segmento (dataset inteiro) | --gate-max-missed |
| Macro core recall | ≥ 0.95 | média por rótulo da cobertura do núcleo (§4) — pega mutilação parcial sistemática | --gate-min-macro-core |
| Latência de onset p90 | ≤ 0.25 s | p90 do atraso do START_OF_SPEECH vs início do rótulo (§8) | --gate-max-onset-p90 |
| Pior áudio | ≥ 0.50 | menor priority_score per-audio — anti-catástrofe localizada que a agregação global diluiria |
--gate-min-worst-audio |
Semântica: reprovar não esconde nada — o score e os valores continuam calculados e
visíveis (permite ver quão longe da régua a config ficou). No ranking do grid, ordena-se por
(passou_gates, score). Valor -1 desliga um gate. A definição completa dos
gates usados fica gravada no score_definition de cada artefato.
Por que "≤ 1" e não zero: existe um caso limítrofe conhecido (fala curta e baixa) que oscila com o ruído de pipeline (§12); gate em 0 reprovaria por azar, não por mérito. O limiar é requisito de produto: ~1 perda tolerada por ~30 min de áudio.
É o tempo entre o cliente começar a falar e o sistema saber que ele falou — o que o barge-in sente. Não é derivável da geometria dos segmentos (um chunk com 1 s de padding pode cobrir o rótulo perfeitamente tendo disparado tarde). O avaliador v1 descartava esses eventos; aqui são medidos sempre. Referência atual (config ultra): mediana 0,127 s, p90 0,176 s. Usa-se p90 no gate porque a cauda é o que destrói a experiência — a mediana é ~130 ms em quase qualquer config.
Gravado em todo summary.json, em todo manifest de trace, e verificado pela UI
(⚠ quando um run foi medido contra labels que não são os atuais). Motivação histórica: no projeto v1,
runs antigos re-pontuados contra um dataset que tinha crescido (19→20 áudios, 70→111 rótulos)
produziram scores absurdos sem nenhum aviso. Regra: scores só se comparam com fingerprints
iguais. Mudou rótulo → re-roda (o replay torna isso barato).
Insight: a parte cara do VAD (preprocessamento + inferência do Silero) não depende de nenhum parâmetro da máquina de estados. Thresholds, min_speech/min_silence, padding e os mecanismos do VAD2 consomem apenas três escalares por janela de 32 ms:
trace roda o pipeline real uma vez por configuração de preprocessamento
(~2 min) e cacheia. replay reexecuta a máquina de estados inteira sobre o cache — incluindo
o ExpFilter (α=0.35), a histerese, o buffer de payload com prefix padding e os 4 mecanismos adaptativos
do VAD2 — em ~60 ms para o dataset todo. Um grid de 88 configs roda em 4,9 s.
evaluate.py verify: no caminho determinístico (sem
preprocessamento), streaming e replay produzem segmentos bit-idênticos — verificado
inclusive com a trava de locutor e re-ancoragem ligadas. A acumulação de tempo do replay usa as mesmas
operações de ponto flutuante do streaming (+= 0.032, nunca i × 0.032) para
reproduzir até o drift numérico das comparações de duração.
Limitações declaradas do replay: (a) o alpha da EMA do RNNoise é fixado na captura do trace
(o min_prob da fusão pode variar; o alpha não); (b) mudar o preprocessamento exige novo trace.
Ao provar a fidelidade do replay, isolamos por camada uma divergência conhecida do v1 (o falso
"warmup" de ±0.002): o rtc.AudioResampler do LiveKit aplica dither aleatório
não-semeado (Δ≤4 LSB por amostra, medido comparando duas execuções sobre entrada idêntica). O RNNoise
amplifica para Δ≈950 LSB, e o Silero converte isso em até Δ0.05 de probabilidade em janelas marginais.
ONNX puro e APM-HPF são determinísticos; o dither é o único ofensor.
| Consequência | Política adotada |
|---|---|
| Dois runs idênticos diferem ±0.005 no score | Diferenças < 0.005 = empate técnico |
| Blips de 0,1–0,3 s aparecem/somem entre runs (órfãos oscilam 2–7) | Órfão usa max(count, duração); min_speech maior é eixo de grid anti-blip |
| Streaming ≠ replay bit a bit (com preproc) | verify em 2 níveis: bit-exato sem preproc; match estrutural com tolerância com preproc
(blips não-pareados < 0,35 s = dither perdoado) |
| Comparações entre configs contaminadas por sorteio | Ranking sempre no replay (mesmo trace = mesmo sorteio para todas) ; número oficial no streaming |
Cada run salvo em evals/<nome>/ contém tudo o que é preciso para re-auditar sem
re-executar:
| Arquivo | Conteúdo |
|---|---|
summary.json |
KPIs globais, resultado dos gates (com valores e motivos), vetor de severidade,
score_definition completo (pesos, gates, tolerâncias, fingerprint), config integral
do detector, resumo do dataset, timestamp |
per_audio.json / .csv |
Todas as métricas por áudio — incluindo listas de falas perdidas e órfãos com timestamps (campos declarados; nada se perde na serialização) |
segments.jsonl |
Cada segmento predito com os DOIS recortes (decisão e payload emitido), timestamps de evento e durações — permite re-pontuar com outra régua no futuro |
grid_results.json grids |
Vetor de severidade + gates + score de cada config varrida, eixos usados, config base |
Fluxo de auditoria de um número: summary.json → score_definition (que régua?) →
severity_vector (de onde veio o desconto?) → per_audio (em que áudio?) →
/eval (ver e ouvir o trecho exato — os problemas viram chips clicáveis que dão seek no
waveform). O caminho inverso (do áudio ao score) também fecha, porque o escalar é reconstruível à mão
a partir do vetor (§6).
sem_fala rotulados — a
gravidade nº 2 é medida só em trechos silenciosos de áudios com fala. Prioridade de labeling.| Núcleo (core) | Parte do rótulo que sobra após o perdão de borda; o que o VAD é obrigado a cobrir |
| Órfão | Segmento predito sem nenhuma sobreposição com fala (nem margens, nem fundo) |
| Ponte | Silêncio entre dois rótulos mantido dentro de um mesmo segmento, além da graça |
| Spill | Duração predita fora de rótulos e margens (excluindo órfãos e pontes, já contados) |
| vad_decision / stt_emitted | Recorte "o que o detector decidiu" vs "o que foi enviado ao STT (com padding)" |
| Trace / replay | Cache dos escalares por janela / reexecução offline da máquina de estados |
| Fingerprint | Hash dos rótulos; define o universo em que scores são comparáveis |
| Gate | Critério eliminatório avaliado antes do ranking |
Fontes: vadlab/metrics.py (definições),
vadlab/scoring.py (pesos e gates), vadlab/replay.py (FSM),
RELATORIO_V2.md (validações e descobertas), RELATORIO_SCORING_POLITICAS.md
no projeto v1 (comparação empírica que originou esta política).