Skip to content

fix(ui): device salvo resolvido por nome+hostapi, nao pelo indice do PortAudio - #55

Merged
caioross merged 1 commit into
mainfrom
auto/issue-53-device-identity
Jul 30, 2026
Merged

fix(ui): device salvo resolvido por nome+hostapi, nao pelo indice do PortAudio#55
caioross merged 1 commit into
mainfrom
auto/issue-53-device-identity

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

O índice do PortAudio é só a posição na enumeração do host API — ele renumera quando o usuário liga um fone USB, pareia um Bluetooth, o driver reinicia ou o VB-CABLE é reinstalado. A UI salvava e restaurava a seleção de device por índice (restorePanelConfig casando o.value === val, refreshDevices idem), então depois de uma renumeração o Laguna passava a usar outro dispositivo, em silêncio: o <option> mostra o nome novo, não há erro, e a tradução some do Discord (ou pior, a captura vira uma saída e gera realimentação). Com a #43 (/api/devices?refresh=1 re-inicializando o PortAudio) a renumeração passa a acontecer exatamente no clique do 🔄 — o único momento em que a UI reaplica a seleção salva.

O que mudou (só static/)

  • fillSelect() passa name/hostapi para o dataset de cada <option>. O label é decorado com API e marcas (🌊 Laguna, VB-CABLE), então não serve para comparação; /api/devices já devolve os campos crus (laguna_devices.py:33-41) — backend intacto.
  • savePanelConfig() grava _devices: {role: {index, name, hostapi}} ao lado dos campos de hoje. data[role] continua sendo o índice: nenhum formato existente mudou.
  • resolveDeviceSelection(saved, options) — função pura que concentra a regra: casa por name+hostapi e devolve o índice ATUAL do device; empata homônimos pelo índice salvo; config antiga (só índice) segue o caminho de hoje; sem match devolve null.
  • restorePanelConfig() / refreshDevices() viram chamadores finos do helper. Sem match, o select fica no placeholder e o painel mostra o aviso novo warn.device_missing (PT e EN, role="status" + aria-live="polite") — melhor que apontar calado para o dispositivo errado. Quando o device volta a aparecer, a identidade salva ainda está no localStorage e a seleção é recuperada (não sobrescrevemos a config no caminho do "sumiu").
  • warn.device_missing é re-renderizado no toggle PT/EN, no mesmo padrão de updateLagunaBadge()/refreshPlaceholders().

buildConfig() não mudou: o payload de /api/start continua sendo intcontrato REST intacto (AC 5).

Gate (resultado real)

  • T1python -m compileall -q .COMPILE_OK; import fase0_poc, laguna_core, laguna_serverIMPORTS_OK (Python C:\Python313\python.exe).
  • T3node --check static/app.js e node --check static/i18n.js → OK. pytest tests_unit -q48 passed (inclui test_i18n_parity.py, a paridade PT/EN das chaves novas).
  • T2 — não se aplica: o diff não toca laguna_core.py/laguna_pipeline.py/fase0_poc.py nem qualquer constante de VAD/latência ou default de modelo.
  • Além do gate: resolveDeviceSelection foi exercitada fora do browser (Node, função extraída do próprio app.js) em 12 casos — renumeração, hostapi desempatando homônimo, device ausente, formato legado (string e número), índice fora do intervalo, entradas vazias, lista de opções vazia e homônimos exatos. Todos passaram. Não foi versionado como teste: o repo não tem runner de JS e criar um seria dependência nova fora do escopo desta issue (fica a sugestão de issue própria).

Riscos e limites

  • Não houve verificação manual no browser nesta rodada (o preview travou); a validação foi a acima. O caminho crítico é o restore, coberto pelos casos do helper, e o finally garante que o aviso é sempre recalculado — inclusive limpo quando não há config salva.
  • Mudança de comportamento deliberada: antes, um índice salvo inexistente deixava valer a preferência automática (laguna.virtual_out/CABLE); agora, se a identidade salva não existe mais, o select vai para o placeholder + aviso. É o que o AC 2 pede, e é o comportamento previsível — o usuário escolhe conscientemente em vez de o app decidir por ele. Configs antigas (só índice) seguem exatamente como hoje.
  • Observado e fora do escopo (não ampliei a PR): use_loopback é restaurado depois de capture_device em PERSIST_FIELDS.escutar, então recarregar a página com loopback marcado deixa o select de captura populado com inputs em vez de loopbacks. É um bug pré-existente; o efeito desta PR é que ele deixa de ser silencioso (o aviso aparece, corretamente, dizendo que o device salvo não está na lista). Vale issue própria.

Closes #53

…o indice do PortAudio

O indice do PortAudio e apenas a posicao na enumeracao do host API: plugar um
fone USB, parear um Bluetooth ou reinstalar o VB-CABLE renumera tudo. Como a UI
salvava e restaurava a selecao POR INDICE, o Laguna passava a usar outro
dispositivo em silencio — o rotulo do select mostra o nome novo, entao nem a UI
nem o usuario percebem. Com o botao 🔄 re-inicializando o PortAudio, a
renumeracao acontece exatamente no clique que reaplica a selecao salva.

- fillSelect() carrega `name`/`hostapi` no dataset de cada <option> (o `label`
  e decorado com API e marcas, nao serve para comparacao);
- savePanelConfig() grava `_devices: {role: {index, name, hostapi}}` ao lado dos
  campos de hoje — o indice continua em data[role], nada muda de formato;
- resolveDeviceSelection(saved, options) concentra a regra: casa por
  name+hostapi e devolve o indice ATUAL; config antiga (so indice) segue pelo
  caminho de hoje; sem match, devolve null;
- restorePanelConfig()/refreshDevices() viram chamadores finos do helper. Sem
  match o select fica no placeholder e o painel mostra `warn.device_missing`
  (PT/EN) em vez de apontar calado para o device errado.

O payload de /api/start continua sendo o indice int — contrato REST intacto.

Refs #53
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@caioross

Copy link
Copy Markdown
Owner Author

Parecer do PR Doctor — APROVADA (head 2e1c6d5)

Classificação: §7.3 NORMAL. Diff 100% em static/ (app.js, i18n.js, index.html, style.css). Nada de laguna_core.py/laguna_pipeline.py/fase0_poc.py, nenhuma constante de VAD/latência, nenhum default de modelo. buildConfig() intacto → o payload de /api/start continua int: contrato REST preservado (confirmado lendo o diff inteiro, não só o corpo da PR). Fora da área de quórum §7.2.

Gate local (worktree Laguna-wt/i53 no SHA 2e1c6d5, C:\Python313\python.exe):

  • T1 → COMPILE_OK + IMPORTS_OK
  • T3 → node --check em app.js e i18n.js = JS_OK; pytest tests_unit -q = 48 passed (inclui a paridade PT/EN das chaves novas)
  • T2 → não se aplica (diff não toca o pipeline). CI do GitHub verde nos 2 jobs.

AC da #53, um a um: (1) _devices: {role:{index,name,hostapi}} gravado ao lado dos campos atuais ✔; (2) match name+hostapi → índice ATUAL, sem match → placeholder + warn.device_missing PT/EN com role="status"/aria-live ✔; (3) refreshDevices() reaplica o snapshot pela identidade (app.js:271-286) ✔; (4) config legada (só índice) segue o caminho de hoje — inclusive string e número ✔; (5) contrato REST inalterado ✔; (6) T3 verde ✔. Closes #53 está correto — a fatia é a issue inteira.

Achado que NÃO bloqueia (mas o corpo da PR o descreve otimista demais): a afirmação "quando o device volta a aparecer, a identidade salva ainda está no localStorage" não se sustenta no caminho do 🔄. savePanelConfig() reescreve data[LS_DEVICES_FIELD] inteiro a partir do estado atual dos selects (app.js:432), e o laço do refreshDevices() chama savePanelConfig(dir) sempre que qualquer outro campo casa (app.js:280 e app.js:289) — e src_lang/tgt_lang/model_size/device sempre casam. Como o select do device ausente acabou de ir para '' (app.js:283), a identidade dele não é regravada e é apagada do localStorage nesse mesmo clique. Efeito: o dispositivo volta a existir e a escolha não é recuperada (o usuário reseleciona uma vez).

Não é regressão — na main de hoje não existe identidade nenhuma, e o ganho principal da PR (parar de apontar calado para OUTRO device) está entregue e coberto. Também não é fix trivial: preservar cegamente a identidade antiga ressuscitaria um device que o usuário deliberadamente desmarcou; o correto é preservar só para roles marcados como ausentes. Fica registrado para virar issue própria, junto com as duas que a própria PR já levantou (runner de JS versionado para resolveDeviceSelection; ordem use_loopback × capture_device em PERSIST_FIELDS.escutar).

Mergeando por squash.

@caioross
caioross merged commit d460971 into main Jul 30, 2026
2 checks passed
@caioross
caioross deleted the auto/issue-53-device-identity branch July 30, 2026 19:13
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.

audio/ui: seleção de device é salva pelo índice do PortAudio — plugar um fone renumera e o Laguna passa a usar OUTRO dispositivo, calado

1 participant