Skip to content

feat(stt): fetch the pinned ffmpeg decoder for source installs - #8427

Merged
bolichen97 merged 1 commit into
mainfrom
feat/stt-ffmpeg-auto-provision
Sep 5, 2026
Merged

feat(stt): fetch the pinned ffmpeg decoder for source installs#8427
bolichen97 merged 1 commit into
mainfrom
feat/stt-ffmpeg-auto-provision

Conversation

@bolichen97

@bolichen97 bolichen97 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

On a source / Toolbox install there is no imageio_ffmpeg package, so batch voice input (browser WebM uploads from MediaRecorder, Slack/Telegram voice notes via POST /api/stt/transcribe) fails with "the audio decoder is unavailable" until the user installs ffmpeg themselves. On a distribution without an ffmpeg package (Amazon Linux) the Settings > Speech page could only show echo 'Build ffmpeg from source: https://ffmpeg.org/releases/' — a line a user pasted into a terminal verbatim, which printed a URL and fixed nothing.

Why it matters

Voice is advertised as working out of the box, and the whisper model already downloads itself. Leaving the decoder as a manual, distro-specific chore means every source install on a host without a package-manager ffmpeg has a half-working voice feature, and the "fix" the UI suggests is not a command at all.

What changed (motivation → approach → change)

  • Goal: a source install ends up with a working decoder without the user hand-installing anything, without widening the trust model that the desktop build relies on.
  • Approach: the repo already pins the imageio-ffmpeg 0.6.0 executables by size + SHA-256 (_PACKAGED_FFMPEG_ARTIFACTS) and re-verifies them on every open, fd-bound through spawn. The trust anchor is the digest, not the path (the spec says so explicitly). So the same upstream bytes can be fetched into the data home and accepted through exactly the same check. Rejected alternatives: adding ~/.local/bin or a venv dir to the candidate list (name-based trust of an agent-writable dir — the thing the existing docstring refuses), and shelling out to apt/brew/dnf from the gateway (needs sudo, distro-specific, and runs a package manager as the gateway).
  • Change:
    • stt/decoder.py (new): owns the pinned-artifact table (imported by transcribe.py, one table so the store can never install bytes the resolver refuses) plus per-platform pins of the imageio-ffmpeg 0.6.0 wheel (filename, URL, sha256). DecoderStore.ensure() is idempotent and single-flight: stream the wheel to a .part under the store dir, verify the wheel digest, extract only imageio_ffmpeg/binaries/<artifact> (exact member match; anything else is rejected), verify the payload's size + digest, chmod_safe 0o755, atomic rename into <models_dir()>/ffmpeg/. Platforms with no pin report unsupported. maybe_autofetch() runs after a successful whisper model download when the interpreter is not bundled and no decoder resolves.
    • transcribe.py: _open_authenticated_in is the one implementation of the path-safety + digest check for both the bundled imageio_ffmpeg/binaries and the store; _open_store_ffmpeg_resource / _store_ffmpeg add the store as the LAST source (bundled → trusted system dirs → store). The macOS signature anchor does not apply to the store. No directory is added to _ffmpeg_candidate_dirs. ffmpeg_source() reports which source would run.
    • dashboard/handlers/core.py: GET /api/stt/status gains ffmpeg: {present, source, auto_fetch, os, arch, download{stage, artifact, downloaded_bytes, total_bytes, error_code, error_detail}}; new POST /api/stt/ffmpeg/download (202; 403 on an app token; 409 decoder_unsupported_platform / stt_decoder_bundled). _ffmpeg_install_commands no longer returns the echo.
    • website/.../SttSettings.tsx: the "paste these commands" block becomes state-aware — download progress, a Download decoder button, the manual commands only where auto-fetch is unsupported, and on failure the error detail plus Let Kiro Crew fix it, which prefills the chat composer through the existing setPendingInput + mergeIntoDraft hand-off with a localised built-in prompt (failure code + detail, host OS/arch, the exact trusted decoder locations, and that ~/.local/bin is not one of them).
    • Docs: docs/system-specs/features/stt-streaming.md decoder section (third source and why it does not weaken the anchor), docs/system-specs/modules/learn-cron-dashboard.md (endpoint), src/kiro_crew/docs/configuration.md.

Tests

  • test/test_stt_decoder.py (41): artifact selection per platform, wheel digest mismatch leaves no file, zip-slip / wrong member rejected, payload digest mismatch rejected, success lands 0o755 and the resolver finds it, tampered store file is ignored, status shape, single-flight ensure(), maybe_autofetch gating.
  • test/test_dashboard_handlers_core_coverage.py: the ffmpeg status object, the download endpoint's 202/403/409 paths, _ffmpeg_install_commands no longer emits the echo.
  • test/test_transcribe.py: resolver order and store authentication.
  • website/src/test/SttSettings.decoder.test.tsx (10) + SttSettings.transcribe.test.tsx: each UI state and the exact prefilled prompt.

Manual verification

Real end-to-end on Linux x86_64 with a throwaway KIROCREW_HOME and the host's own /usr/local/bin/ffmpeg stubbed out of the resolver: ensure() fetched imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl from PyPI, extracted ffmpeg-linux-x86_64-v7.0.2 (79,826,272 bytes, sha256 e7e7fb30…3eb99 = pinned), mode 0o755, ffmpeg_source() == "store", ffmpeg -versionffmpeg version 7.0.2-static; after tampering the file the resolver returned None and logged the digest mismatch. All five wheel pins were verified by downloading each wheel and hashing its member against the artifact table.

Gates on the rebased tree: isort/flake8 clean; mypy --platform linux no issues found in 1289 source files; black-formatting, subprocess-encoding, brand, harness-parity, changelog-history, docs-lint and scrub-lint gates passed; targeted pytest 345 passed (test_stt_decoder.py 41, test_transcribe.py 141, test_dashboard_handlers_core_coverage.py 163); tsc -b clean via npm run build; vitest 21 passed on SttSettings.decoder + SttSettings.transcribe.

Screenshots / video

Captured with the repo's own harness, website/scripts/capture-stt-decoder.mjs
(scripted Playwright against the real built SPA with /api/** stubbed, the same
shape as the other capture-*.mjs scripts). Each scene asserts its strings before
it shoots and the harness fails if two frames come out byte-identical.

1. No decoder, and this platform has a pinned one — the fetch sentence and the
Download decoder button, where the panel previously printed a shell command.

Settings > Voice: ffmpeg is missing, with the Download decoder button

2. Fetch in flight — the decoder's own progress bar and its own caption, which
names the decoder rather than the speech model whose bar sits beside it.

Settings > Voice: Downloading the audio decoder, 42% of 79.8MB

3. Fetch failed — the backend's own failure detail, plus Let Kiro Crew fix
it
, which pre-fills the chat composer with the repair prompt and sends nothing.

Settings > Voice: the decoder download failed on a digest mismatch, with the hand-off button

4. A platform with no pinned executable (32-bit ARM Linux) — the manual
system-decoder command and no button, because a fetch there cannot succeed.

Settings > Voice: the manual apt-get command on an unsupported platform

Related Issues

N/A — found while debugging a live source install.

CI notes

  • Rebased onto current main. The branch was conflicting on
    docs/feature-map/README.md: main had added "plain vs highlighted diffs" to
    the display row while this branch added the two new endpoints to the voice
    row, so both edits are kept. The earlier run's red Backend Tests (3.12, 4) and
    Backend Tests (Windows) (4) named only test/test_slot_close_recreation_race.py
    (11 x Timeout >120.0s plus AttributeError: '_Req' object has no attribute 'can_read_body'), which is a main bug fixed by test(dashboard): give the slot-close race double the body surface read_bounded_json reads #8536/fix: drop the shadowed can_read_body double on the slot-race request #8583 and is in the new
    base; the red Bundle Size Gate was main's drifted App-chunk ceiling, over
    by 1.5 KB on a chunk this diff does not touch, re-measured on main by fix(ci): re-measure the drifted App-chunk bundle ceiling #8519.

  • CodeQL py/clear-text-logging-sensitive-data. The alert was on the warning
    this PR added when a pinned-name file fails its digest. Its SARIF flow makes the
    source _trusted_site_package_roots() — a pre-existing helper returning
    sys.prefix-derived site-packages directories, classified "secret" by a name
    heuristic and carrying no secret — reaching the log through the resolved path.
    Rather than argue the heuristic, the shared open loop is now driven by the pin
    table instead of a directory listing (the table is already the sole authority on
    which filenames may be opened there), so the warning names a module constant and
    no environment-derived path is logged at all. Behaviour is unchanged: the same
    filenames are probed, and the same path-safety, symlink and digest checks decide
    the outcome.

  • Review findings from the previous round, both addressed in code. The Opus 4.8
    lane's advisory finding was a real bug: _open_store_ffmpeg_resource handed
    decoder.store_dir() to the scan without realpath, so on a host whose data
    home is reached through a symlinked ancestor (/home/var/home on
    rpm-ostree — a distribution that also ships no ffmpeg package) the per-file
    guard os.path.dirname(candidate) != binaries_root rejected every file and a
    decoder the store had just installed and verified read as permanently absent.
    Fixed, with test_a_symlinked_ancestor_of_the_store_still_resolves pinning it
    (it fails without the fix). GPT 5.6's blocking finding was that the hand-off
    beside the decoder error carried no written askAgent decision; the decision is
    now spelled out at the call site — why the hand-off replaces
    AskAgentButton rather than sitting beside it, and that the only editable
    values in the subtree commit onBlur, so moving focus to the button is what
    saves them. Reasoning in a PR comment.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@bolichen97
bolichen97 requested a review from a team September 4, 2026 09:39
@bolichen97
bolichen97 requested a review from a team as a code owner September 4, 2026 09:39
@bolichen97
bolichen97 requested a review from cixuuz September 4, 2026 09:39
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of a1221c13752c2e5a0c9d88146f1d1225455fd6c6 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Same digest anchor, same pin table, no new trusted directory — the store adds a location without widening the trust model, exactly as claimed.

Suggestions

  • api_stt_status's probe reaches ffmpeg_source()_store_ffmpeg(), which digest-hashes the ~50–90 MB store binary on every poll — during a later model download that is an 80 MB SHA-256 every DOWNLOAD_POLL_MS (1 s) on exactly the hosts this feature targets. Your own is_present docstring states the rule ("size only… on the path of a UI poll; NOT a trust check"); apply it here and let the per-open resolver keep owning trust.

[DESIGN-REVIEWED] a1221c1

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of a1221c13752c2e5a0c9d88146f1d1225455fd6c6 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence gathered. The flow is state-complete (missing → downloading with live progress → failed with retry + agent hand-off → unsupported with manual command), the screenshots match the claims, progress has aria-live, and the hand-off deliberately pre-fills rather than auto-sends. The only real signals are vocabulary-level.

UX-Verdict: PASS

A dead-end panel becomes a complete fetch flow — every state renders, recovers, and matches the pixels; only the hand-off button's vocabulary drifts from the learned pattern.

Suggestions

  • let_the_agent_fix_it ("Let Kiro Crew fix it") uses the same Sparkles icon and same sendErrorToChat prefill as the product-wide "Ask the agent" button but a different verb — a user who learned "Ask the agent" across ~80 error surfaces can't tell whether this one auto-fixes unattended; align it ("Ask Kiro Crew to fix this").
  • decoder_fetch_prompt "fetch a digest-verified copy" — "digest-verified" is implementation vocabulary; "a verified copy" carries the reassurance without asking the reader to know what a digest is.

[UX-REVIEWED] a1221c1

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of a1221c13752c2e5a0c9d88146f1d1225455fd6c6 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All checks are done: the screenshots follow an established repo convention (60+ sibling capture-*.mjs scripts with committed frames), the chat hand-off reuses the existing sendErrorToChat seam, the digest table and download streamer are unified rather than duplicated, and I counted consumers of the new surface. Final review:

First-Principles-Verdict: PASS

Every item traces to one live defect — a source install with no reachable decoder — and the new trust surface reuses the existing digest pin rather than adding a rule.

What this change ships

Intent: voice input works out of the box on a source install whose distro packages no ffmpeg — an ADDITION, honestly framed as feat.

  1. Voice settings offers a Download-decoder button fetching pinned upstream bytes — justified (reported live defect)
  2. The echo 'Build ffmpeg…' dead-end command is removed — justified (the observed symptom)
  3. Decoder auto-fetches in the background after a speech-model download — declared, named harm (first-upload hang)
  4. GET /api/stt/status gains an ffmpeg object (present/source/auto_fetch/progress) — justified, drives the panel
  5. New POST /api/stt/ffmpeg/download, dashboard-only — justified, mirrors prepare's contract and gate
  6. Transcode path gains the store as last source, digest re-verified per open — justified (the fix itself)
  7. Failed fetch pre-fills a chat repair prompt — justified, reuses existing sendErrorToChat
  8. Model and wheel downloads unified into one stream_pinned_payload — justified (deletes a copy)
  9. Decoder strings in 12 locales — mandated i18n invariant
  10. Screenshot harness + committed frames — matches the repo's 60+ capture-*.mjs convention

Watch

embeddings._download_via_https (embeddings.py:2185) remains a second streaming-sha256 download after the dedup (count: 2 implementations); it has no pinned size so conversion isn't drop-in — accepted-and-deferred, but the shared streamer sitting in stt/models.py is what keeps it out of reach.

Subtractions

  • Drop installed_path's artifact=None default (decoder.py:634) — all 3 callers pass an artifact (grepped installed_path(); require the parameter.
  • Retire config's ffmpeg_missing: it is now a second spelling of status.ffmpeg.present, with one remaining consumer (SttSettings.tsx:796) the new object already covers.

[FIRST-PRINCIPLES-REVIEWED] a1221c1

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of a1221c13752c2e5a0c9d88146f1d1225455fd6c6 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] a1221c1

False positive or not applicable? A repository writer can comment:
/ai-review override gpt a1221c13752c2e5a0c9d88146f1d1225455fd6c6: <one-sentence reason>

@github-actions github-actions Bot removed the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed a1221c13752c2e5a0c9d88146f1d1225455fd6c6 — this comment is updated in place on each push.

Review details

No findings.

The sole candidate — GET /api/stt/status re-hashing the store decoder per poll — runs off the event loop via asyncio.to_thread, produces no wrong output, and its cost is an explicitly documented tradeoff; it also only triggers on the narrow host (source install, no system ffmpeg, a store decoder already present) and the code comment acknowledges the expense. That is an accepted efficiency trade, not a reachable defect at the required bar.

[OPUS-REVIEWED] a1221c1

Verdict parsed from the review's SHA-scoped output markers for commit a1221c13752c2e5a0c9d88146f1d1225455fd6c6.

False positive or not applicable? A repository writer can comment:
/ai-review override fable a1221c13752c2e5a0c9d88146f1d1225455fd6c6: <one-sentence reason>

@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 4, 2026
Comment thread src/kiro_crew/transcribe.py Fixed
@bolichen97
bolichen97 force-pushed the feat/stt-ffmpeg-auto-provision branch from c9dc6df to b4c4bc6 Compare September 4, 2026 10:49
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

False positive. candidate here is a public filesystem path (e.g. /usr/local/bin/ffmpeg) that the digest-pinned resolver tried and rejected — not a secret, credential, or key material. Logging the rejected path is intentional audit information so operators can see which candidate was skipped and why. CodeQL's sensitive-data heuristic over-matches on filesystem paths here.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Watch 1 — "Let Kiro Crew fix it" label drift: rebutted as disproportionate to fix in this PR.

The label is intentionally distinct from "Ask the agent" because this button prefills a specific repair prompt (failure code, host OS/arch, trusted paths, what ~/.local/bin is excluded) rather than the generic hand-off. The existing call site uses sendErrorToChat / setPendingInput directly — the same mechanism AskAgentButton wraps. Renaming to an ask-verb is a valid improvement but out of scope here; filed as a follow-up to unify the three hand-off labels.

Watch 2 — unsupported platform with empty prereqs shows warning with no remedy: accepted-and-deferred.

This is a real gap: a host with no package manager and no pinned artifact gets a dead-end. Adding the agent hand-off in the auto_fetch !== 'available' + empty-prereqs branch is the right fix and is filed as a follow-up. Out of scope for this PR which adds the download path for supported platforms.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

ffmpeg.source field has zero UI consumers: accepted-and-deferred.

The field is correct per the spec — it names which of three sources (bundled, system, store) provided the decoder, and the spec says each is repaired differently. The reviewer is right that no UI branch currently reads it: the panel branches on auto_fetch and present only. The source field is useful for debugging and will be consumed when the repair UI distinguishes "your system ffmpeg is misconfigured" from "store fetch failed" — but that branching is follow-up work. Dropping the field now and reintroducing it later would be a wire-format churn with no consumer benefit; keeping it costs nothing and preserves the spec. Filed as a tracked follow-up to add the source-based repair branches.

The three shared-fetch siblings (embeddings.py, wheel_engine.py, papyrus/tectonic.py) are noted as genuinely out of scope; consolidation into a shared stream_pinned_payload helper is a separate refactor.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) September 4, 2026 17:05
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 4, 2026
@bolichen97
bolichen97 force-pushed the feat/stt-ffmpeg-auto-provision branch from b4c4bc6 to aaa96d5 Compare September 4, 2026 17:36
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Sep 4, 2026
@bolichen97
bolichen97 force-pushed the feat/stt-ffmpeg-auto-provision branch from aaa96d5 to 8c939bd Compare September 5, 2026 03:58
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Sep 5, 2026
On a source / Toolbox install there is no imageio_ffmpeg package, so batch
voice input fails with "the audio decoder is unavailable" until the user
installs ffmpeg themselves. This adds an idempotent, digest-verified fetch
into the data home so voice works out of the box on source installs.

Changes:
- stt/decoder.py: DecoderStore.ensure() fetches the pinned imageio-ffmpeg
  0.6.0 wheel from PyPI, verifies the wheel SHA-256, extracts only the
  matched binary member, verifies its size+digest, and atomically installs
  it with chmod 0o755. maybe_autofetch() runs after a whisper model download
  on non-bundled interpreters. Platform support: Linux x86_64/arm64, macOS
  x86_64/arm64, Windows x86_64.
- transcribe.py: adds the store as the last candidate source; the macOS
  signature anchor does not apply to the store path. The shared open loop is
  driven by the pin table rather than by a directory listing, because that
  table is already the sole authority on which filenames may be opened
  there, and it keeps the name the refusal warning reports a module constant
  instead of a path composed from the interpreter's install prefix.
- dashboard/handlers/core.py: GET /api/stt/status gains ffmpeg status
  (present, source, auto_fetch, os, arch, download progress); new POST
  /api/stt/ffmpeg/download (202; 403 on app token; 409 for unsupported
  platform or bundled decoder). _ffmpeg_install_commands no longer returns
  the echo.
- SttSettings.tsx: decoder block is now state-aware — download progress,
  Download decoder button, manual commands only where auto-fetch is
  unsupported, and on failure ErrorNotice + Let Kiro Crew fix it to prefill
  the chat composer with a localised repair prompt.
- website/scripts/capture-stt-decoder.mjs: screenshot harness for the four
  decoder states, asserting each scene's strings and that no two frames are
  byte-identical, with the frames under temp-screenshots/.
- docs/feature-map/README.md: voice row updated with new endpoints.
- Docs: stt-streaming.md, learn-cron-dashboard.md, configuration.md.

Build: isort/flake8/mypy --platform linux clean; brand/harness-parity/
docs-lint/scrub-lint passed; targeted pytest 345 passed; tsc -b clean;
vitest 21 passed on affected files.

Co-authored-by: Kiro Crew <kirocrew@users.noreply.github.com>
@bolichen97
bolichen97 force-pushed the feat/stt-ffmpeg-auto-provision branch from 8c939bd to a1221c1 Compare September 5, 2026 04:21
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 5, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

F1 — "Agent handoff can discard unsaved Transcribe settings" (SttSettings.tsx:858): addressed as a documented decision, not by removing the hand-off.

The rule the finding anchors to says what a reviewer may block on:

On the askAgent choice, the REVIEWER blocks on the missing decision, never on
the direction of it: an added or touched ErrorNotice carrying neither
askAgent nor a No hand-off comment, or a comment that names no concrete
draft. […] Whether a subtree holds unsaved state is not visible in a hunk, so a
reviewer never demands askAgent be turned on — a wrong demand causes exactly
the data loss the default prevents.
website/AUTOSDE.yaml:571-578

The decision was implicit, which is the real defect here, so a1221c13 writes it
out at the call site. Two things it records:

  1. Why askAgent is off. It is replaced, not withheld. AskAgentButton
    hands over the error journal's context (route, endpoint, HTTP status), which
    cannot express the four facts this repair needs — the failure code, the
    gateway's OS/arch (not the browser's), the trusted decoder locations, and
    that ~/.local/bin is deliberately not one of them. That prompt is a
    translated catalog value (decoder_agent_prompt); AskAgentButton takes no
    prompt. Two buttons offering the agent different payloads is worse than one
    carrying the right one.
  2. What the navigation can cost, named concretely. The only editable values in
    this subtree are the AWS profile and region inputs
    (SttSettings.tsx:735-736), and both commit onBlur — so moving focus to this
    button is itself what saves them. That makes this a safer instance of the
    trade BrowserPanel.tsx:521-532 already takes with askAgent on next to its
    token field ("It costs an unsaved token draft if one is mid-typing … and that
    trade is worth it -- being stranded on an npm error is not recoverable from
    this screen"). A digest mismatch or an unreachable index is likewise not
    recoverable from the Settings page, which is the whole reason the hand-off
    exists.

The suggested remedy — "remove the handoff beside editable settings" — would delete
the feature's recovery path for the one failure a user cannot act on, so it is not
taken. If the lane still reads this as a violation after the decision is written
out, that is a maintainer call rather than something to resolve by dropping the
affordance.


Also fixed in a1221c13, from the Opus 4.8 lane's advisory finding — a real
bug:
_open_store_ffmpeg_resource handed decoder.store_dir() to the scan
without realpath, so on a host whose data home is reached through a symlinked
ancestor (/home/var/home on rpm-ostree, which is also a distribution that
ships no ffmpeg package) the per-file guard os.path.dirname(candidate) != binaries_root rejected every file and a decoder the store had just installed and
verified read as permanently absent. Now resolved before the scan, exactly as the
packaged roots are, with
test_a_symlinked_ancestor_of_the_store_still_resolves pinning it — it fails
without the fix and is the deliberate complement of
test_a_symlink_out_of_the_store_is_refused (a symlink at the pinned filename
is refused; a symlink above the store is just how the host spells that
directory).

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 5, 2026
@bolichen97
bolichen97 merged commit 8a368bb into main Sep 5, 2026
69 of 75 checks passed
@bolichen97
bolichen97 deleted the feat/stt-ffmpeg-auto-provision branch September 5, 2026 05:35
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 2026
bolichen97 pushed a commit that referenced this pull request Sep 6, 2026
…ocal/bin

The Linux ffmpeg fix hint pointed users at ~/.local/bin, which
transcribe._find_ffmpeg deliberately never searches, so following it left
the doctor still reporting ffmpeg as not found. Point at /usr/local/bin (a
real _FFMPEG_CANDIDATE_DIRS entry) and the supported dashboard decoder
download (POST /api/stt/ffmpeg/download, added in #8427). Correct the same
claim in the EC2 guide, update the pinned test assertion, and add a
regression guard that holds the doctor hint against the resolver's actual
candidate directory list so the two surfaces can no longer contradict.
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.

3 participants