Commit Graph
509 Commits
Author SHA1 Message Date
Palash Debnath 764fed09bc Merge pull request #2018 from debpalash/fix/license-reason-passthrough
fix(engines): say when a license or the platform is what blocks an engine
2026-09-10 09:04:07 -07:00
Palash Debnath 4fdde15079 Merge pull request #2023 from debpalash/fix/translation-uninstall-guard
fix(translation): uninstalling an engine never removes what others need
2026-09-10 09:00:07 -07:00
Palash Debnath e0879027d5 Merge remote-tracking branch 'origin/main' into fix/license-reason-passthrough
# Conflicts:
#	CHANGELOG.md
#	tests/test_engine_unavailable_reason_1866.py
2026-09-10 08:41:45 -07:00
Palash Debnath c5f32d020a fix(translation): uninstalling an engine never removes what others need
The translation uninstall route runs `pip uninstall -y <package>` on the
app's own environment. Two entries made that break something else:

- The LLM engine's package is openai, a core dependency that Settings →
  LLM Providers also uses. Unlike Argos, it was not marked builtin, so
  uninstalling that engine removed it from the app.
- Google, DeepL, Microsoft and MyMemory share deep_translator. Uninstalling
  any one removed it for all four.

The route now asks uninstall_blocker() first. It refuses (400) a package
VoiceStudio itself depends on, read from the installed package metadata so
there is no second list to keep in step, and refuses (409) a package
another engine shares, naming the engines that would stop working. The
openai entry is marked builtin as well, and a test requires every entry
backed by an app dependency to be.
2026-09-10 08:26:53 -07:00
Palash Debnath f5ea363724 fix(engines): name the missing MPS on Apple Silicon, and hold the MLX test on every host
mlx_supported() fails three ways. Only the non-Apple branch is a platform
gap; Apple Silicon whose PyTorch cannot use MPS was left on the generic
line, and the live-host test asserted a platform reason even there, so it
would fail on such a Mac. That case now has its own owned sentence, and
the live test asserts only the branch its host can produce, with literal
cases covering the rest on every machine.
2026-09-10 08:22:20 -07:00
Palash Debnath e532c9d8dd fix(engines): say when an engine can't run on this platform
The public reason sanitizer had no category for a host the engine cannot
run on at all, the same gap that hid the license button. "MLX requires
Apple Silicon" and PocketTTS's Intel-Mac reason became the generic
"check installation" line, and "not supported on this platform" became
"isn't installed yet", which an existing test asserted. Each sent people
after an install that could never work.

A platform category, matched after the license and before the install
and file checks, now says the engine doesn't run on this platform and
points at its guide. mlx-audio's "Apple Silicon only" wording is left
out of the markers: it also appears on an M-series Mac when the package
is simply missing, where installing does help.
2026-09-10 07:52:46 -07:00
Palash Debnath 664c9ea4b5 fix(engines): keep the license reason so Supertonic-3 and PocketTTS can be enabled
The Model Catalogue shows an engine's license Accept button only when its
reason matches /license not accepted/i. public_backends() replaces probe
text with owned sentences, and no category covered a license gate, so the
reason arrived as the generic "Engine unavailable" line and the only way
to enable Supertonic-3 or PocketTTS never rendered (#2017).

A license category, matched first, keeps those words. The test reads the
regex out of EngineCompatibilityMatrix.jsx, so a wording change on either
side fails CI instead of silently hiding the button.
2026-09-10 07:34:06 -07:00
Palash Debnath faa3d39836 feat(engines): isolated one-click installs for MOSS-TTS-v1.5, Confucius4 and dots.tts
Three engines that shipped as terminal-only setups now install from Model
Catalogue → Engines with the existing sidecar installer, which is
generalised to take a per-engine venv interpreter, install target, import
probe and host gate.

Each engine gets DATA_DIR/engines/<id>/ with its own checkout and .venv;
every uv pip install passes --python for that venv, never the app's
interpreter. Switching the active engine only changes a pref, so moving
between engines and back cannot corrupt a working one, and uninstalling one
removes only its own folder. Tests pin both invariants for every spec.

MOSS-TTS-v1.5's [torch-runtime] extra pins torch==2.9.1+cu128, which exists
only on PyTorch's index, so its manual install and its bootstrap could never
resolve (#2015). core.torch_indexes defines the index once for the
installer and the bootstrap, and a test ties it to the app's own
pytorch-cuda index.

Install buttons appear only where the install can work: MOSS on CUDA hosts,
dots.tts off Windows (upstream publishes no Windows install). A direct POST
on an unsupported host gets a 409 with the reason. An engine with no
one-click install now points at its guide, not at a page with no Install
button.
2026-09-10 07:28:42 -07:00
Palash Debnath 744a0550d3 Merge remote-tracking branch 'origin/main' into tmp/1986
# Conflicts:
#	CHANGELOG.md
2026-09-09 23:25:50 -07:00
Palash DebnathandClaude Opus 5 d1e4c847cb fix(dub): name the source language code that was rejected
Closes #1960.

The report was "400 Bad Request: Invalid source language code" and nothing
else. That cannot be acted on or triaged: it does not say which of the ninety
or so codes was wrong, so neither the user nor a maintainer reading the
auto-filed issue can tell whether the picker offered something the backend does
not accept, or a stale preference from an older build is still being sent.

I could not determine the cause from the report, which is exactly the problem.
Naming the code makes the next one answerable instead of guessing at this one.

The value is a language code chosen from a menu, not private data, and the
engine validator a few lines away already echoes its input the same way.

Also adds the check I actually wanted while investigating: a test that reads
the picker's own LANG_CODES and asserts the backend accepts every one of them,
so a code added to the menu cannot silently become a 400. It passes today —
the menu and the allowlist do agree — which is how I ruled that out as the
cause rather than assuming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 22:42:27 -07:00
Palash DebnathandClaude Opus 5 de962d164a fix(pronunciation): say when an IPA/CMU entry is stored but not applied
Closes #1949.

Settings offers three notations. Only Respelling substitutes text today; IPA
and CMU rows save cleanly, are validated, get a badge and can be toggled on,
then get dropped before term matching and are never read again.

That much is Phase 1 behaving as designed. The defect is that it was INVISIBLE:
"Test a sentence" answered "No entries match — spoken as written" for a term
that does match. Not a degraded answer, a wrong one — and it sent the user off
to re-type an entry that was already correct, or to convert it to Respelling,
where a phoneme string is then read as graphemes.

docs/specs/01-expressive-tts.md asked for exactly the opposite: such entries
"passed through and flagged 'phoneme not honored on this engine' (parity-rule:
visible degradation)". That flag was never implemented. This is it.

The dry run reports inert entries separately, and the panel names them. The
substitution path is deliberately untouched — this does NOT start feeding raw
phoneme strings into the grapheme stream, which is the thing Phase 1 refuses on
purpose, and a test pins that it still refuses.

Not Phase 2. Lowering IPA/CMU to engine markup is a real feature per engine and
stays open; what changes here is that the gap is now honest rather than silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 22:11:05 -07:00
Palash DebnathandClaude Opus 5 0a019b5d7c fix(errors): say 'no reference clip' instead of naming library parameters
Closes #1879.

mlx-audio raises a bare ValueError in its own vocabulary — "No conditionals
available. Either provide audio_prompt/audio_prompt_sr for voice cloning, or
ensure conds.safetensors is in the model directory." — and the generate route
passed it straight through as the 400 detail. The user was told to supply an
argument they have no way to name and to check for a file they have never heard
of, when what happened is simply that they asked to clone with nothing to clone
from.

Classified now, with a remedy in the user's terms: pick a profile that has a
saved reference clip, or record one. It also notes that a designed voice with
no saved reference cannot be cloned from, which is the case that produces this.

The route still passes through every ValueError it cannot classify. Most are
VoiceStudio's own validation messages and are exactly what the user should
read, so replacing them wholesale would have been a regression — tests pin four
of them as untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 15:24:12 -07:00
Palash DebnathandClaude Opus 5 075ab7b68b fix(engines): say why an engine is unavailable instead of reporting a failed check
Closes #1866.

Model Catalogue → Engines showed "Engine unavailable. Check installation and
configuration." and "Last error: A previous engine check failed." for engines
the user had simply never installed. Neither names a missing package, a missing
step, or a next action, and the second reads like a crash or a poisoned cache
rather than "you have not installed this yet" — so a normal, expected state
looked like a fault.

The probe's own sentence still cannot cross the boundary: it carries exception
text, local paths and sometimes credentials, which is why it was replaced in
the first place. What changed is that the private diagnostic is now CLASSIFIED
into a VoiceStudio-owned category — package not installed, needs configuring,
file missing or unreadable — exactly the shape _public_routing_reason already
uses for routing. Anything unrecognised keeps the old generic sentence rather
than asserting a cause the probe never gave.

test_docs_url_survives_the_public_metadata_scrub pinned the generic wording
while testing something else; it now asserts what it is actually about, that no
private text survives.

Also skips the exec-bit placeholder test on Windows, where os.access(X_OK) is
true for any existing file so the assertion cannot fail — it errored the whole
module on a Windows checkout. Pre-existing, unrelated to this change, and in
the way of running these tests at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 14:37:27 -07:00
Palash Debnath 6a44733297 Merge pull request #1962 from debpalash/land/queue2
Land the reviewed PR queue, part two: log panel, bootstrap mirror, setup diagnostics
2026-09-09 13:58:18 -07:00
Palash Debnath f82f35ee97 Merge pull request #1963 from debpalash/land/queue3
Land the reviewed PR queue, part three: zh-CN locale and null capture timings
2026-09-09 13:43:45 -07:00
Palash Debnath 1c37c479be Merge remote-tracking branch 'origin/main' into land/queue2
# Conflicts:
#	CHANGELOG.md
2026-09-09 13:43:21 -07:00
Palash Debnath b6143cce08 Merge remote-tracking branch 'origin/pr/1959' into land/queue3 2026-09-09 13:18:49 -07:00
Palash Debnath 512aad5c48 Merge remote-tracking branch 'origin/main' into land/1952
# Conflicts:
#	frontend/src/pages/AudiobookTab.jsx
2026-09-09 13:17:13 -07:00
Palash Debnath 8526ff56f7 Merge remote-tracking branch 'origin/main' into land/queue2
# Conflicts:
#	CHANGELOG.md
2026-09-09 13:15:23 -07:00
Palash DebnathandClaude Opus 5 f48a33edcf fix: address the review findings on the PRs landed here
Bot review raised one blocking and several real findings across these PRs.
Each is fixed here rather than merged and followed up.

#1892 — apply_pypi_index_env() now runs for EVERY uv invocation, and with the
default region "auto" it called the UNCACHED auto_detect_region(), racing two
live network probes with a 4s timeout per uv call. On a blocked or offline
network that is a repeated multi-second stall, and it multiplies the outbound
calls a local-first app makes unasked. The probe is memoised for the life of
the process. It also now clears UV_INDEX_URL before setting it, so a stale
ambient value cannot outrank the region the user picked.

#1925 — backend.rs trimmed OMNIVOICE_LOG_DIR for its emptiness guard but built
the path from the RAW value, while the Python reader strips it. A padded value
therefore had the writer and the reader looking at different directories, which
is the divergence the PR exists to close.

#1920 — the rotation walk caught bare OSError, so a PermissionError or a real
I/O failure was swallowed and the panel silently rendered less. Only the race
the guard exists for (a file that rolled away, and on Windows the handler's own
sharing violation) is skipped now; anything else surfaces.

#1951 — the fix was right but shipped no tests and no changelog entry. Both
added, including a case pinning that the wizard preflight and the diagnostic
route the same host the same way, since they carry separate copies of the
branch.

#1923 — the cell hardcodes the CTranslate2 model, but if the cuDNN 8 step
failed the backend falls back to PyTorch Whisper and downloads a second
multi-gigabyte model. The cell now says so while the download it just spent is
still on screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 13:09:12 -07:00
Palash Debnath 61eb27dbba Merge remote-tracking branch 'origin/pr/1892' into land/queue2 2026-09-09 13:02:15 -07:00
Palash Debnath 19d371f9d6 Merge remote-tracking branch 'origin/pr/1951' into land/queue2 2026-09-09 13:02:15 -07:00
Palash Debnath c760347339 Merge remote-tracking branch 'origin/pr/1925' into land/queue2
# Conflicts:
#	CHANGELOG.md
#	backend/api/routers/system.py
2026-09-09 13:02:15 -07:00
Chang-Jin-LeeandClaude Opus 5 2e4465a3f4 fix(capture): pass a null segment end through instead of rounding it
`round(s.get("end", 0), 2)` does not defend against a stored None: the key is
present, so `.get` returns the None rather than the default, and `round` raises

    TypeError: type NoneType doesn't define __round__ method

`max(s.get("end", 0) for s in segments)` on the line above raises first when any
other segment is timed:

    TypeError: '>' not supported between instances of 'NoneType' and 'float'

Two engines reach these builders with end=None. `_sherpa_result` sets
duration=None when it cannot derive one from the sample rate, and sherpa is the
first capture engine. `OpenAICompatASRBackend._adapt_response` emits end=None for
every plain-text response, which is what a server that rejects verbose_json
returns, and that backend is selectable as the active one used by accurate mode.

Measure the duration from the segments that carry a number, and pass the nulls
through. That is the shape the segment list already renders since #1904 — it
shows whichever half of the range is known — and it keeps the honest null the
producers deliberately write instead of inventing a zero.

capture_ws.py has the same two lines and gets the same treatment; it also emits
end=None itself in five of its own streaming payloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:33:18 +09:00
Palash Debnath 67817b89bf Merge remote-tracking branch 'origin/pr/1942' into land/queue1
# Conflicts:
#	CHANGELOG.md
2026-09-09 12:25:35 -07:00
Palash Debnath 2e1e52f26e Merge remote-tracking branch 'origin/pr/1917' into land/queue1
# Conflicts:
#	CHANGELOG.md
2026-09-09 12:24:12 -07:00
Palash Debnath 6b68c4d9fe Merge remote-tracking branch 'origin/pr/1914' into land/queue1
# Conflicts:
#	CHANGELOG.md
2026-09-09 12:24:11 -07:00
Palash Debnath 5f5bb04f41 Merge remote-tracking branch 'origin/pr/1912' into land/queue1
# Conflicts:
#	CHANGELOG.md
2026-09-09 12:24:10 -07:00
Palash Debnath 8fc003ad7c Merge remote-tracking branch 'origin/main' into feat/local-workspace-dictation-polish 2026-09-09 18:55:28 +05:30
Palash Debnath 99a534882c feat: improve dictation controls and creative workspaces 2026-09-09 18:55:19 +05:30
michaelhuamanfloresandClaude Sonnet 5 693a43b26c fix(setup): stop misreporting low-VRAM caveat as kernel-launch risk
The GPU routing "accelerated" caveat branch in preflight and diagnose
always showed the driver/arch "may fail at kernel launch" fix hint,
even when the actual reason was a low-VRAM advisory unrelated to
drivers or torch. Gate that message on KERNEL_RISK_MARKER and show an
accurate VRAM-appropriate hint otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 07:47:18 -05:00
Chang-Jin-Lee d5d28ddabf Merge remote-tracking branch 'upstream/main' into fix/1866-engine-unavailability-reason 2026-09-09 12:55:59 +09:00
Chang-Jin-Lee 67ef5e22eb Merge remote-tracking branch 'upstream/main' into fix/tauri-clear-preserves-backend-stderr 2026-09-09 12:55:06 +09:00
Chang-Jin-Lee a84dc9e8ef Merge remote-tracking branch 'upstream/main' into fix/system-logs-rotation 2026-09-09 12:54:56 +09:00
marreiradigital b15fa1209a fix(download): log nao promete um fallback que ainda nao aconteceu
Achado do CodeRabbit (fora do diff) no PR #1942. Depois de desacoplar os dois
sinais, a mensagem passou a ser escolhida so por `_segmented_off` — mas na
penultima tentativa o plano devolve (True, True): o acelerador esta esgotado E
a tentativa re-lanca, entao o `snapshot_download` so entra na PROXIMA. O log
dizia "falling back to snapshot_download" enquanto na verdade ia retentar.

Sao tres estados distintos e ler so um sinal funde dois deles. A frase virou o
helper puro `_segmented_retry_note(disable, reraise)`, testado nos tres ramos —
inclusive o (True, True), que e o que a revisao pediu para cobrir.
2026-09-08 23:47:42 -04:00
marreiradigital 8a2c9a4828 Merge branch 'main' into fix/windows-dev-stack-and-download-resume
Conflito unico em `install_model`: a `main` (#1926) acrescentou
`and not allow_patterns` a condicao do acelerador, e este branch trocou
`_attempt == 1` por `not _segmented_off`. As duas guardas valem e foram
mantidas juntas.

O comentario acima da condicao ainda dizia que qualquer falha cai no
snapshot_download; atualizado para a regra atual (falha nao-transitoria, ou a
ultima tentativa), com ponteiro para `_segmented_retry_plan`.
2026-09-08 23:28:28 -04:00
marreiradigital 97e0e70bf2 fix(download): entrega o caminho simples so na ultima tentativa
Achado P1 do Greptile no PR #1942: com `disable` e `reraise` amarrados um ao
outro, a tentativa 4 de 5 desligava o acelerador E ja caia no
`snapshot_download` na mesma iteracao. Resultado: o acelerador ficava com 3
tentativas em vez de 4, e uma nova queda abandonava o manifesto reaproveitavel
uma tentativa antes do necessario, recomecando por um arquivo separado — que e
exatamente o que este helper existe para evitar.

Os dois sinais agora sao independentes: a tentativa que esgota o acelerador
ainda re-lanca, entao o caminho simples comeca na ULTIMA tentativa. Acelerador
fica com 1-4, `snapshot_download` com a 5.

Tambem blindei o caso de o acelerador falhar ja na ultima tentativa: ali nao ha
para onde re-lancar, entao a decisao vira "caminho simples agora" em vez de
estourar o laco sem nunca ter tentado o fallback.

Os testes do helper passaram a resolver o modulo da app em tempo de execucao,
como pede a instrucao de caminho para tests/**/*.py (import de modulo da app no
topo fica velho se um teste anterior sujar o sys.modules) — apontado pelo
CodeRabbit no mesmo round.
2026-09-08 23:25:30 -04:00
marreiradigital 66f7ef8cfe fix(download): reentra no acelerador na proxima tentativa apos queda
Achados do CodeRabbit no PR #1942.

O mais grave: com o erro classificado como transitorio, o codigo mantinha o
acelerador ligado mas caia direto no `snapshot_download` na MESMA tentativa. Se
esse download desse certo, o laco terminava e o manifesto do `.part` nunca era
reusado — exatamente o recomeco-do-zero que a correcao existe para impedir.

Agora o erro transitorio e propagado para o retry externo, cuja proxima
tentativa reentra no `_segmented_snapshot` e retoma do manifesto. A decisao
virou o helper puro `_segmented_retry_plan`, testavel direto (o laco mora dentro
de `install_model`, uma rota de ~200 linhas). A ultima tentativa fica reservada
para o caminho simples, entao o acelerador continua sem poder ser o motivo de um
install falhar de vez.

Tambem deste round de revisao:

- `Invoke-CimMethod ... Terminate` tinha o retorno descartado com `$null =`. O
  Win32_Process.Terminate reporta falha pelo ReturnValue, nao lancando: um kill
  negado por permissao era reportado como sucesso e a porta seguia presa. Agora
  o ReturnValue e validado, com exit 4 proprio e a mensagem carregando o codigo.
- O teste de concorrencia era vazio: o handler sincrono do MockTransport retorna
  antes de qualquer outra task rodar, entao `peak` nunca passava de 1 e a
  asserção `peak <= 4` passava sem exercitar o semaforo. Passou a segurar as
  requisicoes abertas com um asyncio.Event e a exigir `peak == 4` (verificado:
  com o semaforo afrouxado para 1000, o teste acusa 31).
- A doc dizia que OMNIVOICE_DOWNLOAD_MAX_WORKERS limita as faixas e que origem
  sem Range cai no snapshot_download. Nenhum dos dois: `_segmented_snapshot` nao
  passa `num_connections` (usa as 8 padrao) e origem sem Range vira stream unico
  dentro do proprio acelerador.
- Entradas de Highlights do CHANGELOG sem o `(#NNNN)` exigido.
2026-09-08 23:12:14 -04:00
marreiradigital 0d3fb07c1f fix(download): segmenta em blocos limitados e retoma o acelerador
O downloader segmentado gravava progresso no manifesto apenas quando um
segmento INTEIRO terminava, e dimensionava os segmentos como
tamanho/num_connections. Num blob de 806 MB isso dava 8 segmentos de ~100 MB:
numa conexão que cai a cada ~50 MB nenhum segmento jamais completava, o
manifesto nunca era escrito e cada tentativa recomeçava do zero.

Pior, o acelerador só rodava na PRIMEIRA tentativa (`_attempt == 1`), então
depois da primeira queda todas as retentativas iam para o `snapshot_download`
e o `.part` acumulado ficava órfão para sempre.

Agora os segmentos são limitados a 16 MB e a concorrência passa a ser
controlada por semáforo (antes vinha da própria contagem de segmentos), e o
acelerador é preservado entre tentativas quando o erro é de rede — reusando
`_is_retryable_download_error`, que já é a fonte única dessa classificação.
Ele só é desligado de vez quando a falha NÃO é transitória, ou seja, quando o
acelerador de fato não serve naquele host.

Reproduzido em rede real: `peer closed connection without sending complete
message body (received 54260979, expected 100708200)`.
2026-09-08 22:45:31 -04:00
Palash Debnath 870b6f93ba fix(audiocpp): harden GPU runtime routing 2026-09-08 23:33:40 +05:30
Palash Debnath fe53e02693 fix(audiocpp): harden async and worker routing 2026-09-08 19:45:48 +05:30
Palash Debnath 23dd8728d1 fix(audiocpp): preserve GPU routing across workers 2026-09-08 18:30:12 +05:30
Chang-Jin-LeeandClaude Opus 5 22ab31e0c7 fix(system): correct the Linux log path in the docstring, and follow the writer's override
CodeRabbit: the docstring said $XDG_STATE_HOME/VoiceStudio where the code says
OmniVoice. Checked against the writer rather than guessing which side was
wrong -- backend.rs::backend_log_path() joins "OmniVoice" on Linux, so the
code was right and the docstring was a pre-existing error. It matters because
that docstring is what gets read when telling a Linux user where the file is.

Reading backend_log_path() to settle it turned up something worth fixing in
the resolver this PR introduced: Rust checks OMNIVOICE_LOG_DIR before any
per-OS default, and nothing on the Python side knew about it. The backend is a
child of the shell, so an ambient override reaches both processes -- a
resolver that ignored it would look in the per-OS default while the writer
wrote somewhere else. That is the same divergence class as the desktop Logs
panel in #1782, and leaving a newly added resolver knowingly wrong was not an
option.

Two tests: the override moves both candidates, and a whitespace-only value
falls back to the default, matching the writer's !dir.trim().is_empty() guard.
The platform parity test now also clears OMNIVOICE_LOG_DIR so it stays a
statement about the defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
2026-09-08 21:46:35 +09:00
Palash Debnath 260519d4ee Merge branch 'main' into feat/audiocpp-gpu-routing
# Conflicts:
#	CHANGELOG.md
#	backend/engines/audiocpp/__init__.py
#	backend/engines/audiocpp/bootstrap.py
2026-09-08 17:29:25 +05:30
Palash Debnath ffc5c07949 fix(audiocpp): require explicit model install 2026-09-08 16:21:08 +05:30
Palash Debnath 503407272e feat(audiocpp): route native GPU backends 2026-09-08 15:05:23 +05:30
Chang-Jin-LeeandClaude Opus 5 4999bac7a8 fix(system): stop the Tauri tab's Clear from wiping the backend's stderr
/system/logs/tauri/clear iterated every entry in _tauri_log_candidates() and
truncated each one, including backend_err.log -- the spawned backend's stderr.
Three things make that data loss rather than a tidy-up.

The tab that owns the button does not show it. On desktop the Frontend/Tauri
panel goes through the Rust read_log_tail command, whose tauri_log_path()
resolves tauri.log and nothing else, so the user truncates a file they were
never shown.

backend.rs::open_err_log_for_run() opens it APPEND-ONLY so "a respawn must not
destroy the previous run's evidence" (#1510) and rotates it to .1 rather than
truncating. It manages its own size; clearing it from here only undoes that
design. The same file's spawn diagnostics are described there as "retained in
backend_err.log across runs and lands verbatim in bug reports".

A native death -- a Windows access violation, a SIGSEGV -- writes nothing to
the Python log by construction, so this file is the only record it happened.
#1777 and #1782 are both threads where the maintainer had to ask a reporter
for it by hand.

Clear is narrowed to the shell's own log. The READ path is unchanged: the
candidate list was split into two halves and recomposed, and a parametrized
test pins that /system/logs/tauri still reaches all four files in the same
order on darwin, linux and win32 -- the recompose is where a slip would
silently hide a log.

Refs #1510

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
2026-09-08 18:04:05 +09:00
Chang-Jin-LeeandClaude Opus 5 17fda0d79f fix(system): survive a rollover racing the read, and stop trusting the scan
Three review findings, all applied.

Greptile P1, read race: a rollover can rename a candidate between the
existence check and the open, and the handler exposes no lock a route can
take. Per-file OSError now skips that file instead of 500ing the whole panel
-- which is what the single-file version did in the same situation, so this is
strictly better than before rather than a new guarantee. A roll landing
mid-walk can still shift which chunk a file holds, so a tail taken at that
instant may repeat or miss a block; the panel re-polls every 5s and the next
read is clean. Buying strict consistency would mean reaching into logging's
internals from a route.

Greptile P1, clear race: enumerating first left a window where a rollover
created a backup after the scan and its history survived a Clear that
reported success. Clear now works off the fixed name set -- every name the
handler can write is known up front, so there is nothing to enumerate and no
snapshot to go stale.

CodeRabbit: the CHANGELOG lines ended in (#1782), which reads as "this fixes
#1782" when the desktop path defect that thread is about is untouched. Now
(#1920).

Two tests added, both red before: a candidate vanishing mid-walk still fills
the request from the next file, and a Clear whose scan reported nothing still
empties the backups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
2026-09-08 16:46:18 +09:00
Chang-Jin-LeeandClaude Opus 5 3beebc6d57 fix(system): tail the backend log across a rollover, and clear the backups
main.py rolls omnivoice.log at 2 MB into .1/.2/.3, and /system/logs read only
the current file. For the minutes after a rollover the Backend tab showed a
handful of lines while up to 6 MB of history sat in omnivoice.log.1. Measured
with 3 lines in the current file and 500 in each of two backups: tail=200
returned 3 lines and reported total_lines: 3.

That is the panel CONTRIBUTING and the engine guides tell a reporter to paste
from, so the gap costs a round trip on every bug report that lands near a
roll.

The tail now reaches into the rotated siblings, but only when the current file
cannot satisfy the request -- the panel polls every 5s and opening 6 MB of
backups on each call would be a bad trade for a case that only matters right
after a roll. The response gains a `paths` list so a report can say whether
its tail crossed a boundary.

Clear is in the same commit because the two are coupled: it truncated only
omnivoice.log, so it freed almost nothing, and once the tail can see the
backups a Clear that leaves them looks like it did nothing at all.

Found while reading #1782, and it does NOT close it. That thread's blank panel
is the desktop path, which never reaches this route -- details in a comment
there.

Refs #1782

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
2026-09-08 14:39:49 +09:00
Chang-Jin-LeeandClaude Opus 5 09a582c65a fix(engines): narrow the health log field to what the probe did
Greptile P1: `failure=unavailable` reads as a classification of the cause, but
SubprocessBackend.health_check() swallows its own exceptions by contract, so a
dead sidecar and a package that was never installed both return (False, msg)
and land in the same bucket. Rename to `probe=`, with `raised:<Class>` and
`returned-unavailable` as the two values, so the field states what the probe
did and claims nothing about why. The limitation and what it would take to fix
it properly (structured failure metadata from the probes) are named in the
comment and the test docstring.

Greptile P2: drop the trailing arrow glyph from the Learn more button. It sat
outside t(), and a bare "→" points the wrong way once the app switches to an
RTL locale. InfoHint hardcodes the same glyph and would want the same
treatment, but that is not this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
2026-09-08 13:43:36 +09:00