feat(stt): fetch the pinned ffmpeg decoder for source installs - #8427
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of 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
[DESIGN-REVIEWED] a1221c1 |
UX Review (Fable 5) — ✅ PASSUX-level review of 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 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
[UX-REVIEWED] a1221c1 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks are done: the screenshots follow an established repo convention (60+ sibling 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 shipsIntent: voice input works out of the box on a source install whose distro packages no ffmpeg — an ADDITION, honestly framed as
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] a1221c1 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings. The sole candidate — [OPUS-REVIEWED] a1221c1 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
c9dc6df to
b4c4bc6
Compare
|
False positive. |
|
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 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 |
|
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 The three shared-fetch siblings ( |
b4c4bc6 to
aaa96d5
Compare
aaa96d5 to
8c939bd
Compare
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>
8c939bd to
a1221c1
Compare
|
F1 — "Agent handoff can discard unsaved Transcribe settings" ( The rule the finding anchors to says what a reviewer may block on:
The decision was implicit, which is the real defect here, so
The suggested remedy — "remove the handoff beside editable settings" — would delete Also fixed in |
…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.
Problem / Motivation
On a source / Toolbox install there is no
imageio_ffmpegpackage, so batch voice input (browser WebM uploads fromMediaRecorder, Slack/Telegram voice notes viaPOST /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 showecho '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)
_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/binor a venv dir to the candidate list (name-based trust of an agent-writable dir — the thing the existing docstring refuses), and shelling out toapt/brew/dnffrom the gateway (needs sudo, distro-specific, and runs a package manager as the gateway).stt/decoder.py(new): owns the pinned-artifact table (imported bytranscribe.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.partunder the store dir, verify the wheel digest, extract onlyimageio_ffmpeg/binaries/<artifact>(exact member match; anything else is rejected), verify the payload's size + digest,chmod_safe0o755, atomic rename into<models_dir()>/ffmpeg/. Platforms with no pin reportunsupported.maybe_autofetch()runs after a successful whisper model download when the interpreter is not bundled and no decoder resolves.transcribe.py:_open_authenticated_inis the one implementation of the path-safety + digest check for both the bundledimageio_ffmpeg/binariesand the store;_open_store_ffmpeg_resource/_store_ffmpegadd 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/statusgainsffmpeg: {present, source, auto_fetch, os, arch, download{stage, artifact, downloaded_bytes, total_bytes, error_code, error_detail}}; newPOST /api/stt/ffmpeg/download(202; 403 on an app token; 409decoder_unsupported_platform/stt_decoder_bundled)._ffmpeg_install_commandsno 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 existingsetPendingInput+mergeIntoDrafthand-off with a localised built-in prompt (failure code + detail, host OS/arch, the exact trusted decoder locations, and that~/.local/binis not one of them).docs/system-specs/features/stt-streaming.mddecoder 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-flightensure(),maybe_autofetchgating.test/test_dashboard_handlers_core_coverage.py: theffmpegstatus object, the download endpoint's 202/403/409 paths,_ffmpeg_install_commandsno 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_HOMEand the host's own/usr/local/bin/ffmpegstubbed out of the resolver:ensure()fetchedimageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whlfrom PyPI, extractedffmpeg-linux-x86_64-v7.0.2(79,826,272 bytes, sha256e7e7fb30…3eb99= pinned), mode 0o755,ffmpeg_source() == "store",ffmpeg -version→ffmpeg version 7.0.2-static; after tampering the file the resolver returnedNoneand 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 linuxno issues found in 1289 source files; black-formatting, subprocess-encoding, brand, harness-parity, changelog-history, docs-lint and scrub-lint gates passed; targeted pytest345 passed(test_stt_decoder.py41,test_transcribe.py141,test_dashboard_handlers_core_coverage.py163);tsc -bclean vianpm run build; vitest 21 passed onSttSettings.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 sameshape as the other
capture-*.mjsscripts). Each scene asserts its strings beforeit 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.
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.
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.
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.
Related Issues
N/A — found while debugging a live source install.
CI notes
Rebased onto current
main. The branch was conflicting ondocs/feature-map/README.md:mainhad added "plain vs highlighted diffs" tothe
displayrow while this branch added the two new endpoints to thevoicerow, so both edits are kept. The earlier run's red
Backend Tests (3.12, 4)andBackend Tests (Windows) (4)named onlytest/test_slot_close_recreation_race.py(11 x
Timeout >120.0splusAttributeError: '_Req' object has no attribute 'can_read_body'), which is amainbug 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 newbase; the red
Bundle Size Gatewasmain's driftedApp-chunk ceiling, overby 1.5 KB on a chunk this diff does not touch, re-measured on
mainby fix(ci): re-measure the drifted App-chunk bundle ceiling #8519.CodeQL
py/clear-text-logging-sensitive-data. The alert was on the warningthis PR added when a pinned-name file fails its digest. Its SARIF flow makes the
source
_trusted_site_package_roots()— a pre-existing helper returningsys.prefix-derivedsite-packagesdirectories, classified "secret" by a nameheuristic 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_resourcehandeddecoder.store_dir()to the scan withoutrealpath, so on a host whose datahome is reached through a symlinked ancestor (
/home→/var/homeonrpm-ostree — a distribution that also ships no ffmpeg package) the per-file
guard
os.path.dirname(candidate) != binaries_rootrejected every file and adecoder the store had just installed and verified read as permanently absent.
Fixed, with
test_a_symlinked_ancestor_of_the_store_still_resolvespinning it(it fails without the fix). GPT 5.6's blocking finding was that the hand-off
beside the decoder error carried no written
askAgentdecision; the decision isnow spelled out at the call site — why the hand-off replaces
AskAgentButtonrather than sitting beside it, and that the only editablevalues in the subtree commit
onBlur, so moving focus to the button is whatsaves them. Reasoning in a PR comment.
Checklist