feat(devices): 🔄 re-inicializa o PortAudio e passa a ver hardware novo (/api/devices?refresh=1) - #43
feat(devices): 🔄 re-inicializa o PortAudio e passa a ver hardware novo (/api/devices?refresh=1)#43caioross wants to merge 2 commits into
Conversation
#37) O PortAudio enumera os devices uma unica vez por processo (no Pa_Initialize que o sounddevice dispara no primeiro uso) e serve a lista de um cache estatico: `sd.query_devices()` de novo devolve exatamente a mesma lista. Por isso o botao 🔄 girava o spinner, repopulava os selects com os mesmos dados e nunca mostrava o fone plugado depois, o VB-CABLE instalado depois nem o CABLE renomeado para "Laguna" — justamente o passo 5 do proprio passo a passo de setup. - `laguna_devices.reinit_portaudio()`: ciclo `_terminate()/_initialize()` com acesso defensivo (API privada do sounddevice) — versao sem os simbolos, ou ciclo que falha, degrada para a enumeracao normal em vez de quebrar. - `GET /api/devices?refresh=1`: so o clique no 🔄 pede o re-init (caro no WASAPI); o boot da UI continua na versao barata. Guarda obrigatoria: com qualquer worker vivo o re-init e PULADO — `_terminate()` derruba streams e reatribui os indices que o worker guarda como int. O campo `refresh` ({requested, applied, reason}) explica o resultado para a UI. - UI: aviso inline (aria-live, PT+EN) quando o refresh nao pode ser aplicado — "pare as direcoes" ou "reinicie o Laguna" — em vez de deixar o usuario concluir que o hardware dele e que esta errado. Tooltip do 🔄 atualizado. - `tests_unit/test_devices_refresh.py`: sem worker re-inicializa e ve o device novo; com worker registrado nao re-inicializa; sem `_terminate` degrada sem excecao; sem `?refresh` nao paga o ciclo.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Parecer do PR Doctor — quórum adversarial (HANDBOOK §7.2)Head analisado: Veredito das 3 lentes: Registro que o gate e o CI passam: nenhum destes defeitos é capturado pelo gate atual. Os quatro vetores abaixo eu confirmei pessoalmente no código/no fonte da lib, além do parecer das lentes. 🔴 Bloqueador 1 —
|
O merge de #69 moveu os imports pesados de laguna_server.py para dentro do guarda de import; o `reinit_portaudio` desta PR foi para junto deles. Sem mudanca de comportamento dos dois lados. Gate na uniao: compileall OK, ruff (E9,F63,F7,F82) OK, imports OK, pytest tests_unit/ 111 passed, node --check em static/app.js e i18n.js.
|
Reparo de conflito, sem tocar no mérito: o merge da #69 moveu os imports pesados de Gate na união: Segue DRAFT + |
Contexto
O botão 🔄 prometia no tooltip re-detectar dispositivos "sem recarregar a página", mas o backend só reenumerava o cache estático do PortAudio — que é montado uma vez por processo, no
Pa_Initialize. Fone plugado depois, VB-CABLE instalado depois ou "CABLE Output" renomeado paraLaguna Translator Mic(o passo 5 do próprio passo a passo de setup) nunca apareciam; a única saída era reiniciar o servidor. O usuário concluía que o hardware dele é que estava errado.O que mudou e por quê
laguna_devices.reinit_portaudio()— ciclo_terminate()/_initialize(). São API privada do sounddevice, então o acesso é defensivo (getattr+try/except): versão sem os símbolos, ou ciclo que falha, retornaFalsee o chamador segue com a enumeração normal. Seinitialize()falhar, há uma segunda tentativa (melhor esforço) para não deixar o processo sem enumeração. Nunca levanta.GET /api/devices?refresh=1— só o clique no 🔄 pede o re-init; o boot da UI continua na versão barata (o ciclo custa centenas de ms no WASAPI e não faz sentido em todo carregamento de página)._terminate()derruba streams abertos e reatribui os índices dos devices, e o worker guardacapture_device/output_devicescomoint; ele passaria a apontar para outro hardware silenciosamente. A leitura de_workersusa o_lockjá existente, como as demais rotas.refresh: {requested, applied, reason}(reason∈null | "workers_running" | "unsupported"). Campo aditivo — cliente antigo ignora.#devices-hint(aria-live="polite", reusa.badge warn, sem CSS novo) quando o refresh não pôde ser aplicado: "pare as direções" ou "reinicie o Laguna". Sem isso, "lista igual" continuaria sendo lida como defeito do hardware. Duas chaves i18n novas em PT e EN + tooltip do 🔄 atualizado (a promessa do tooltip agora se sustenta, inclusive na ressalva sobre direções rodando).Gate (resultado real)
compileall -q .→COMPILE_OK; import-smokefase0_poc, laguna_core, laguna_server→IMPORTS_OK(PythonC:\Python313\python.exe). ✅pytest tests_unit/— 44 passed (38 anteriores + 6 novos emtests_unit/test_devices_refresh.py): sem worker → re-inicializa e vê o device novo (incluindohas_laguna_namepara o badge 🌊); com worker registrado → não re-inicializa e a lista fica intacta;sdsem_terminate→unsupported, sem exceção;_initializeque explode →unsupported, sem exceção; sem?refresh→ zero_terminate/_initialize(boot barato). ✅node --check static/app.js+static/i18n.js→JS_OK; paridade PT/EN pelo teste versionadotests_unit/test_i18n_parity.py(dentro dos 44). ✅laguna_core.py,fase0_poc.py,laguna_pipeline.py, VAD, defaults de modelo nem o caminho quente de áudio (o endpoint só é chamado fora do runtime de tradução).sounddevice 0.5.5expõe os dois símbolos,reinit_portaudio()→Trueelist_devices()depois do ciclo devolve a enumeração completa e coerente (36 inputs / 41 outputs, rótulos intactos) — ou seja, o ciclo não deixa o PortAudio num estado quebrado.Riscos e limites
Pa_Initialize, que é o contrato documentado dele._terminate/_initializesão privados: podem sumir numa versão futura do sounddevice — daí o acesso defensivo e o caminhounsupported, que degrada em vez de quebrar/api/devices(a ui: falha do /api/devices no boot deixa a UI morta e muda (WebSocket nunca conecta, nenhum erro visível) #33 mostrou que esse endpoint quebrado mata o boot da UI).POST /api/startque chegue exatamente durante o ciclo espera o_lock; ele valida os devices antes de pegar o lock, então uma config validada milissegundos antes poderia usar índice reatribuído. Fora isso os índices só mudam com nenhum worker vivo.Solicito quórum (HANDBOOK §7)
Closes #37