Repository navigation
feat(video): orchestrate Audio Bridge STT with one-download budgets (FU-06, #11654) - #12012
Merged
diegosouzapw merged 27 commits intoAug 29, 2026
Merged
Conversation
…11654) Add server-side orchestration composing three existing seams: the same already-downloaded video bytes, a new loopback-only broker mode=audio operation (mono 16kHz PCM WAV) that shares the frame path's exact process queue/deadline/AbortSignal/byte budgets, and the existing Audio Bridge transcription boundary (callAudioTranscriptionTimed, extended to preserve provider segment timing when supplied and mark whole-clip fallback timing as coarse). Transcription only ever runs when BOTH the operator setting (modalityBridgeVideoAudioTranscriptionEnabled, default false) AND a per-request opt-in are present; every other permutation and every failure mode (unavailable provider, timeout, abort, extraction failure) returns a well-defined partial with track: null so the visual-only fallback stays available, and a result cache avoids repeat paid STT calls for the same video+model. Full request-pipeline wiring into videoBridgeHelpers.describeVideoPart is deferred: that exact fusion call site is also touched by the in-flight #12009 (FU-05 transcript contract) PR, so this orchestration ships as a composable, fully unit-tested primitive rather than risking a conflicting rewrite of shared lines.
…t createTask flow (#11296) (#11985) flux/kontext is catalogued with isMarket: true, so handleKieImageGeneration routed it through KIE's unified Market createTask endpoint with model: "flux/kontext". KIE does not expose Flux Kontext through the Market catalog at all -- it lives under a dedicated API tree (POST /api/v1/flux/kontext/generate, poll GET /api/v1/flux/kontext/record-info, models flux-kontext-pro/flux-kontext-max) -- so the Market endpoint rejected it with "model name not supported", matching the reporter's exact error text. Special-case flux/kontext ahead of the isMarket branch so it hits the dedicated endpoint/payload shape instead of being treated as a Market entry. z-image/4.0-*/4.5-* remains intentionally untouched (still blocked on reporter/live confirmation per the existing in-code comment). Co-authored-by: Markus Hartung <mail@hartmark.se>
…outs (#11500) (#11989) * fix(db): rate-limit Arena ELO fetch-failure warnings on repeated timeouts (#11500) * test(quality): split the #11500 fetch-failure-dedup tests into their own file tests/unit/arena-elo-sync.test.ts crossed the 1000-line new-test-file cap (file-size gate, PR mode). The two new tests don't need the file's DB fixture (fetchArenaLeaderboards() never touches the DB), so they move to a self-contained sibling file instead of growing the frozen suite. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…te path (#11707) (#11984) enforceCodexResponsesLiteParallelToolCalls() forces parallel_tool_calls:false at the top of CodexExecutor.execute(), but transformRequest() early-returns the body before its RESPONSES_API_ALLOWLIST field filter only when _nativeCodexPassthrough is set. Any request that reaches the codex executor via the translated (non-native-passthrough) path never gets that flag, so the allowlist filter silently deleted parallel_tool_calls right before the fetch body was sent, reproducing the reported upstream rejection ('X-OpenAI-Internal-Codex-Responses-Lite requires parallel_tool_calls to be false') for every model. Add parallel_tool_calls to RESPONSES_API_ALLOWLIST so the value survives the translated path too. Update the sibling #2608 allowlist test that previously asserted parallel_tool_calls gets stripped like other Chat Completions-only fields -- it is a legitimate Responses API field that must now survive. Co-authored-by: Markus Hartung <mail@hartmark.se>
…publish's opencode-plugin skip check (#11787) (#11990) * fix(cli): drop the never-produced dist/index.cjs requirement from prepublish's opencode-plugin skip check (#11787) * test(build): resolve tsup/npm portably in the #11787 regression test instead of a hardcoded .bin path The old test assumed @omniroute/opencode-plugin/node_modules/.bin/tsup already existed. A fresh checkout (CI's npm ci never installs this standalone package's own deps) has no such node_modules at all, so the test failed with MODULE_NOT_FOUND in CI while passing locally on a devbox that had installed it before. Mirror scripts/build/prepublish.ts's own install-then-resolveLocalBinEntry approach. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…ked rows (#9133) (#11994) * fix(sse): stop the auto-combo candidates inspector from dropping blocked rows (#9133) prepareVirtualAutoComboInputs applied filterResilienceBlockedCandidates before the #7819 read-only candidate inspector ever saw the pool, so a model-locked or cooled-down candidate silently disappeared from /auto-combo/*/candidates instead of showing up as reachable:false with a reason (modelLocked/connectionCooldown/breakerState were dead fields by construction). Add an opt-in `skip` parameter so the inspector builds its own unfiltered pool; routing (createVirtualAutoCombo/createBuiltinAutoCombo called without a prepared override) is unchanged. Also aligns isModelLocked's model argument to the bare model id, matching every lock writer and the routing-side filter, instead of the "provider/model" string. Regression test: tests/unit/auto-combo-candidates-locked-model-visible.test.ts (red before the fix — locked account's row silently missing; green after). * chore(quality): register the #9133 regression test in stryker tap.testFiles tests/unit/auto-combo-candidates-locked-model-visible.test.ts covers open-sse/services/accountFallback.ts (via isModelLocked) but wasn't listed, so its mutant kills wouldn't count toward mutation coverage. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…er> so it satisfies BodyInit (#11654)
…SBOM on dispatch (twin of #11982 + #12020) (#12022) * fix(release): resync the electron lockfile, build a dispatch from a repaired ref, keep curated notes, attach the SBOM on dispatch (release/v3.8.51 twin of #11982 + #12020) Same four changes as #11982 and #12020 on main, applied to this branch's own copies: - electron/package-lock.json regenerated (271 -> 284 entries): the optional electron-builder-squirrel-windows subtree was missing and `npm ci` refused the lock (EUSAGE) on the Linux and macOS legs; a clean `npm ci --ignore-scripts` on the result exits 0. - electron-release.yml: `build_ref` dispatch input (default: the version tag) and `generate_release_notes` only on the tag push (a re-attach dispatch appended GitHub's auto notes to the curated body on v3.8.50). - npm-publish.yml: the SBOM attaches to the GitHub Release on workflow_dispatch publishes too, whenever a release for the tag exists. actionlint and prettier clean; electron-release-desktop-channel-8949, electron-release-efficiency, electron-release-latest-yml.repro, check-workflows and npm-publish-artifact-provenance suites pass. * fix(release): validate build_ref in the validate job before any checkout uses it CodeQL (actions/cache-poisoning/poisonable-step, high) on release/v3.8.51 — the default branch: a raw dispatch input checked out next to setup-node's npm cache is a cache-poisoning vector. The input now goes through the validate job's regex allowlist (main or release/vX.Y.Z, empty = the version tag) and every build job checks out needs.validate.outputs.build_ref, never the input itself. * fix(release): drop the build_ref input — a dispatch builds the ref it is dispatched on CodeQL (actions/cache-poisoning/poisonable-step) tracks the input through the validate job's output regardless of the regex allowlist: an input-controlled checkout next to setup-node's npm cache on the default branch is a cache-poisoning vector. The ref is not an input any more; the checkouts use github.ref, so `gh workflow run electron-release.yml --ref v3.8.50 -f version=v3.8.50` rebuilds the tag and `--ref main` builds the repaired line. The tag-push path is unchanged.
…12021) * fix(ci): stop hosted docker-publish OOM and unpaint Build (advisory) docker-publish was firing 8 concurrent hosted builds on every merge storm; each died ResourceExhausted in npm run build (#11976). One publish per ref, webpack instead of Turbopack so native RSS stays inside the V8 heap we can cap. Build (advisory) is skipped: continue-on-error still reports FAILURE and was painting every fork PR red. Closes #11976 * fix(ci): run docker-publish amd64 on omni-build and share the heavy lane The .113 box is 31 GB / 32 cores — enough for one next-build. Hosted ubuntu-24.04 is ~7 GB and ResourceExhausted every publish (#11976). amd64 now targets [self-hosted, omni-build] (Turbopack) when USE_VPS_RUNNER is on, joins the existing heavy-build-main group so it queues beside ci.yml Build instead of becoming a third heavy, and falls back to hosted + webpack if the VPS is off. arm64 stays on ubuntu-24.04-arm with webpack (no ARM box). * test(ci): align the advisory-build contract with the hosted-OOM skip if: ${{ false }} tripped zizmor obfuscation (194→195). Bare if: false skips the job without a new finding. The #7307 test now pins the skip and keeps the job body as the restore recipe.
…t a driver line the primary path never prints (twin of #12032) (#12047) * fix(release): the packaged-app smoke verifies the database opened, not a driver line the primary path never prints (release/v3.8.51 twin of #12032) Same change as #12032 on main: the packaged app opens SQLite during the smoke but its primary open path prints no "[DB] Driver: …" line (only the recovery path and the sql.js fallback do), so the #7592 assertion failed every Linux release leg. The guard rejects the sql.js fallback line, accepts a native driver line, and otherwise accepts demonstrable database activity; after readiness the smoke requests /api/monitoring/health and waits for that activity outside the readiness loop. electron-smoke-script suite 10/10. * fix(release): reapply the smoke rework on top of release/v3.8.51's own copy of the script The previous commit copied main's file wholesale and dropped this branch's ensureSmokeEnvDirs(currentPlatform) fix and its tests; this reapplies only the DB-open evidence change as a patch. electron-smoke-script suite green.
* docs(ops): the .113 heavy-build ceiling is one runner, not two Two concurrent next-builds (15.4 GB + 17.2 GB RSS) OOM-killed one on 2026-08-29 17:26 UTC; systemd booked the kill on the other runner's unit and its job died with the same "shutdown signal" text a hosted-runner OOM shows. omni-build now lives on omniroute-113-5 only; 113-6 keeps omni-release. The janitor ceiling counts every listener on the box (4 OmniRoute + OmniHeuris + OmniMind = 6). The second heavy slot returns when the Proxmox VM gets more RAM; the exact command is in the doc. * docs(ops): apply the single-heavy-slot text (previous commit only carried formatting)
Turbopack had 31 GB on omniroute-113-6 and still panicked (TurbopackInternalError: there must be a path to a root, run 33253576569). The same tree's arm64 webpack build on hosted ARM succeeded. Dockerfile already documents webpack as the Docker escape hatch. Keep amd64 on the one omni-build slot (#12048).
#11600) (#11996) Fixes the blocking Lint job's own ci.yml cache: PR #11963 removed the stale restore-keys fallback from quality.yml but left ci.yml's two "Restore ESLint file cache" steps carrying the same prefix-match fallback that lets a cache from a different lint config report stale per-file verdicts. Byte-level parity with #11963's already-merged fix. Deliberately half of #11600 — the other half (run-eslint-json.mjs) is covered by PR #11983 from a parallel session, so the two don't collide on the same file.
… list (#11949) (#11995) Local no-API-key providers (ollama-local, lm-studio, vllm, etc.) were invisible in the Qdrant embedding-model dropdown because configuredProviders required a real apiKey or OAuth. Extended the filter to also include providerAllowsOptionalApiKey(connection.provider) — the same canonical helper already used for the identical check elsewhere. TDD: 21/21 integration tests pass (was 20/21 before the fix).
…start (#11885) (#12005) Fixes three defects in the "update doesn't restart the running process" bug class: CLI update guidance now detects a live server and tells the operator to restart instead of implying the update is already live; the dashboard's Update button tries OmniRoute's own PID-file supervisor before falling back to pm2 instead of hardcoding pm2 and silently skipping; getLatestVersionFromNpmCli now uses --prefer-online (same fix pattern as #4376). TDD throughout, 63/63 targeted regression tests pass.
…1481) (#11997) Adds opt-in modelVisibilityAllowlist/modelVisibilityDenylist settings so an operator can curate exactly which models GET /v1/models advertises, mirrored into auto/* combo candidate pools (the same trap #6512 fixed for hidePaidModels). Default off, no behavior change for anyone who doesn't opt in. TDD: 4 new test files, 22/22 passing (16 node:test + 6 vitest) + regression sweep across virtual-auto-combo/hide-paid/hide-auto-no-think suites (21/21). Rebased onto the updated tip (a sibling #9133 landed first, same file) — kept both rebaseline annotations in file-size-baseline.json and set the value to the real measured line count after both merged.
…conciliation (#11652) (#12009) FU-05 core (closes #11652): caller-supplied Video Bridge transcripts had no bounded, deterministic contract — a client could self-assert source: "embedded"/"audio-bridge" and it was accepted verbatim. normalizeVideoTranscript gained a code-only trustedSource seam unreachable from request-body JSON; without it, any cue declaring embedded/audio-bridge is reclassified to client. Added budgets (256 cues, 4096 code units/cue, 4KiB/cue, 64KiB total), malformed-surrogate rejection, focus-window scoping, deterministic cross-source reconciliation, and bumped the result-cache version v4→v5 so old-contract cache entries can never serve new-contract requests. All 187 videoBridge* tests pass (185 pass, 2 unrelated pre-existing skips).
…ker (#11659) (#12011) FU-05 subtitle adapter (Refs #11659 — deliberately not Closes: the adapter is not yet wired into the live describeVideoPart path, that composition point is sibling #12009 which just landed): server-owned, loopback-only ffprobe/ffmpeg subtitle extraction that legitimately earns the "embedded" provenance label, mirroring the existing frame-extraction lifecycle. Broker route now also serves ?subtitles=1, stamped with the shared broker fingerprint so the client-side adapter can verify the payload actually came from the trusted process. Bounded, ReDoS-safe WebVTT parser.
…or, evaluator and allowlist scaffold (#11656) (#12008) FU-07/FU-09 promotion-evidence harness (Refs #11656): delivers the manifest schema, deterministic fixture recipes, metrics aggregator, and promotion-verdict evaluator #11656 asks for — deliberately does NOT deliver the promotion verdicts themselves (they require real models against real fixtures on a live host, HOLD with explicit reason instead of any fabricated result). New files only, no collision with sibling PRs.
…ution variants (FU-08) (#12006) FU-08 (Refs #11655): drill-down producer/consumer lifecycle on top of the existing cache substrate, without modifying it — new VideoDrilldownLifecycle (opaque sha256 handles, principal-bound resolve/delete with no existence oracle, preview/standard/detail multiresolution variants, 8-frame/32MiB page budget) plus a new authenticated remote-consumer route, both opt-in (default false).
…lared (#11661) (#12001) Reconciles the Video Bridge FU-01..09 backlog docs against verified code and GitHub state (ground truth established first, per this repo's Documentation accuracy rule), correcting a real gap in GUARDRAILS.md: the transcript source field was documented as validated without noting OmniRoute didn't yet verify server-side extraction — exactly the gap #11652 (now merged as #12009) closes. Refs #11661, not Closes — truthfully closing it needs the sibling PRs' actual landed state folded back in, left as an explicit follow-up.
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…FU-06, cheaper-inference#11654) (cheaper-inference#12012) FU-06 (Audio Bridge STT orchestration): one download, two extractions — takes already-downloaded video bytes and extracts bounded mono 16kHz PCM WAV via the loopback broker's new mode=audio operation, sharing the exact same queue/deadline/byte budgets as the frame path. Dual opt-in (operator setting default false + per-request), only reaches the STT call when both are on. Rebased onto the tip after sibling cheaper-inference#12011 (subtitle mode) landed first, both touching the same broker route/client — combined additively so frames/audio/subtitles all share the one extractionQueue singleton. Re-validated: 59/59 focused tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
release/v3.8.51, not touched by this branch.Summary
FU-06 (Video Bridge series, parent #9760): server-side orchestration that
lets Video Bridge extract audio from a video and run it through Audio
Bridge STT — the original acceptance the fusion primitive (
fuseVideoAndAudio)never covered, since today it only fuses an already-materialized
audioTranscripta caller supplies.Scope delivered:
(
src/lib/guardrails/videoBridgeAudioOrchestration.ts) takes the already-downloaded video bytes as input — it has no download path of its own, so
it structurally cannot re-fetch the video.
(
src/app/api/modality-bridge/video/extract/route.ts,mode=audioqueryflag) extracts bounded mono 16 kHz PCM WAV via array-argument
ffmpeg(
src/lib/guardrails/videoBridgeAudioExtraction.ts, reusingprobeLocalVideo's existing safe-container/format/dimension checks —no parallel validation). Never a shell string (Hard Rule deps: bump qs from 6.14.1 to 6.14.2 #13).
mode=audiorunsthrough the exact same route file, the same module-level
extractionQueuesingleton, the same
MAX_INPUT_BYTES/BROKER_TIMEOUT_MS, and the samedeadline/AbortSignal composition the frame path already uses.
boundary is ever touched:
modalityBridgeVideoAudioTranscriptionEnabled(new settingskey, default
false— Hard Rule feat: v0.2.0 — advanced routing services, cost analytics, pricing overhaul #20 spirit).operatorOptIn && requestOptInreaches the STT call; all threeother permutations return
attempted: falseand never invokeextractAudio/transcribe(asserted directly in tests).callAudioTranscriptionTimed, a new sibling ofcallAudioTranscriptionin
audioBridgeHelpers.tssharing one refactored-out HTTP/multiparthelper (
sendAudioTranscriptionRequest) — no new STT client. It requestsresponse_format=verbose_jsonand preserves provider-supplied per-segmenttimestamps (
timingPrecision: "exact") when present, falling back to onewhole-clip observation spanning the extracted audio's duration
(
timingPrecision: "coarse") when the provider ignores it.FusionObservations (source: "audio") directly from the STT response —it never calls
normalizeVideoTranscripton untrusted client JSON, so itdoes not depend on (or duplicate) the source-trust work feat(guardrails): enforce video transcript provenance, budgets and reconciliation (FU-05 core) #12009 is doing
on that function.
PROVIDER_UNAVAILABLE,EXTRACTION_FAILED,TRANSCRIPTION_FAILED,TIMEOUT,ABORTED) returnstrack: nullinstead of throwing — a caller's visual-only fallback alwayshas a well-defined result to check.
BridgeCache, same primitive Vision/Audio Bridgealready use) keyed on the video part's ref + STT model — a repeated
request for the same video does not re-extract or re-transcribe (no
repeat paid STT call).
Deliberately deferred — composes with #12009 (FU-05, not yet merged):
wiring this orchestration into
videoBridgeHelpers.ts::describeVideoPart'sexisting
fuseVideoAndAudiocall site is not done in this PR. Thatexact block is also where #12009 is landing the transcript-contract/
provenance-trust rewrite (
normalizeVideoTranscript'strustedSourceoption, focus scoping,
contributingSourcesreconciliation, cache v4→v5).Rather than risk a conflicting rewrite of the same lines from two
directions, this PR ships the orchestration as a complete, independently
tested, composable primitive. The follow-up wiring PR (feeding
orchestrateVideoAudioTranscription'sFusionTrackintodescribeVideoPart's existing audio branch, taggedsource: "audio-bridge"— the transcript-source value already declared inVideoTranscriptSourcebut never produced today) is small once #12009'sseam lands.
Test evidence (RED→GREEN, all mocked at the process/provider boundary)
tests/unit/guardrails/videoBridgeAudioOrchestration.test.ts(17 tests):all 4 opt-in permutations (only "both" transcribes; the other 3 assert
extractAudio/transcribewere never called), extraction success →coarse observation, STT segments → exact timing, unavailable provider
(extraction never runs), extraction timeout vs. generic extraction
failure, transcription timeout vs. generic transcription failure, caller
abort during either step, cache hit skips both the broker and the STT
call, a different video's cache key stays isolated, and the same
timeoutMs/signalthreading through both steps.tests/unit/guardrails/videoBridgeAudioExtraction.test.ts(4 tests):bounded WAV extraction reusing the probed duration, propagating the
shared duration cap, rejecting output over the byte cap, and surfacing
ffmpeg's own "no audio stream" failure.
tests/unit/guardrails/audioBridgeHelpers.test.ts(+3 tests): the timedboundary requests
verbose_json, falls back to no-segments when theprovider ignores it, and drops malformed provider segments instead of
trusting them — plus all 7 pre-existing tests still pass unmodified
(
callAudioTranscription's behavior is byte-for-byte unchanged).tests/unit/video-bridge-broker.test.ts(+6 tests) /tests/unit/video-bridge-route-security.test.ts(all 9 pre-existingstill pass): audio broker client round-trip, malformed/non-WAV response
rejection, the audio operation observes the identical
VideoExtractionQueueError/503+Retry-After contract as frames, themodule-level singleton queue is referenced by both branches, frame-only
query params are rejected under
mode=audio, and the injected extractorreceives the shared deadline/byte budget.
tests/unit/video-bridge-settings.test.ts(+1 test): the operator opt-indefaults
falseand the new Zod schema field accepts/rejects correctly.173/173 tests green across all touched files (command run):
No existing assertion was weakened or removed.
Gates run locally (green)
npm run typecheck:core— exit 0npm run typecheck:noimplicit:core— exit 0npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>— 0 problemsnpm run check:complexity-ratchets— OK (2673/2774 cyclomatic, 1193/1223 cognitive — both improved vs. baseline, no new violations)node scripts/check/check-file-size.mjs— OK (both new files well under the 1000-line cap; no frozen file touched)node scripts/check/check-test-discovery.mjs— OKnpm run check:cycles— OK, no cyclesDid not run
npm run test:coverageper instructions (heaviest gate; CI runs it).Pending live validation (cannot be done from this environment)
from this environment; the shared queue/byte/deadline budgets are proven
structurally and via unit tests (identical singleton, identical
VideoExtractionQueueErrorcapacity contract) but not load-measured live.ffmpeg/ffprobeinvocations areexercised through the injectable
VideoCommandRunnerin tests (no realbinary invoked); the array-argument construction is asserted directly,
but a real multi-format video → WAV round-trip was not run here.
verbose_json—callAudioTranscriptionTimedis proven against mocked
fetchresponses shaped like OpenAI-compatibleverbose_jsonoutput; no live call to a real STT provider was made toconfirm which providers actually return
segments.preserved, no transcript-text retention) — not wired into the live
request pipeline in this PR (see "Deliberately deferred" above), so there
is no live streaming/non-streaming request to check yet; the module
itself never logs or persists transcript text.
test:unit+test:vitest) and the full release gateset on the exact candidate SHA — not run here; only the focused suites
above were run locally.
Given the above, this uses
Refs #11654, notCloses— VPS-side budgetmeasurement and live-pipeline wiring remain open follow-up work once
#12009 lands.
Refs #11654