Repository navigation
fix(meeting-recorder): long meetings can no longer fall into a transcription loop; chunks cut at pauses, a temperature ladder, and a safe re-transcribe (1.0.2) - #526
Merged
Conversation
… ladder, so a long meeting cannot loop faster-whisper-server keeps condition_on_previous_text on and takes one float temperature, so Whisper's own fallback never runs and a long slice could decode into a repetition loop or never return. The worker now cuts audio at the quietest 100 ms frame between 20 and 30 s (one decoder window), sends each chunk on its own at temperature 0 with no prompt, and re-sends any chunk whose segments compress past 2.4 or that an engine-agnostic loop guard flags (words per second, repeated 5-grams) at 0.2 ... 1.0. A timeout drains the server before the next rung. A chunk nothing can read becomes a gap; finished chunks are cached with a settings fingerprint so a re-run resumes. An optional fallback endpoint (unset by default) takes chunks the primary cannot read cleanly. --retranscribe re-runs a finished session, commits only a gap-free run, and archives the old transcript, chunk records and meta as *.v1-loop.* (later *.prev-<time>.*) before swapping in, deleting nothing. Folds in the per-attempt timeout, VAD and gap handling that installed 1.0.0 copies carried as local patches (old env names still work). writeMeta is now atomic. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…ed transcription Documents the chunking, the loop defences, every env var, re-transcription and measured throughput; drops the "nothing leaves the machine" claim, which holds only for a loopback endpoint, and the wrong 600 s slice default. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
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.
Why
faster-whisper-server keeps
condition_on_previous_texton and accepts one floattemperature, so Whisper's own temperature fallback never runs server-side. On the meeting recorder's four-minute slices that meant a decoder could fall into a repetition loop (one sentence, or "thank you", over and over) and either return pages of it or never return at all. A newer server image does not help: the current upstream router still exposes neither knob.What changes
The worker (
bundles/meeting-recorder/server/)audio.js(new): reads the 16 kHz mono WAV in fixed-size blocks, computes 100 ms frame energy, and cuts each chunk at the quietest frame between 20 and 30 s (ties go to the later frame, so flat audio cuts at 30 s). A chunk is at most one decoder window, so there is no previous text to condition on. Chunks are sliced straight from the WAV; ffmpeg now runs once per session, not once per slice.loop-guard.js(new, engine-agnostic): Whisper's compression ratio (computed client-side when an engine does not report it), words per second, and the most-repeated 5-gram.stt-client.js(new): one request per chunk at temperature 0, no prompt carried over,vad_filteron by default. If any segment's compression ratio is above 2.4 or the loop guard flags the text (> 4 words/s, or a 5-gram repeated > 8x), the chunk is re-sent at 0.2, 0.4, 0.6, 0.8 and 1.0, and the first clean result is kept. If none is clean, the least bad one is kept and the chunk is marked suspect. A timeout moves up the ladder, but only after a one-second "drain" request comes back, because the server keeps decoding a request the client abandoned and runs one at a time. Two timeouts end a chunk's ladder. A 4xx is a configuration error and stops the run.WHISPER_FALLBACK_URL/WHISPER_FALLBACK_MODEL(unset by default, so behaviour is unchanged). It takes any chunk the primary cannot answer or answers only suspectly. After two chunks in a row with the primary down, chunks go straight to the fallback and the primary is re-probed every ten chunks. This is groundwork for routing meetings to a second engine; per-engine chunk sizes come later.[transcription gap: a to b]with its real span, and the run goes on. The run is marked failed if more than half the chunks are gaps, or if no endpoint answers five chunks in a row.chunks/, keyed by index and boundaries, with a settings fingerprint (version, models, VAD, language, thresholds, window, audio hash). A cache made under other settings is moved aside tochunks.stale-<time>/, never deleted.transcript.jsonstays an array of segments (now withchunkandsuspect/gapflags), clamped to their chunk. A newchunks.jsonholds one record per chunk: engine, model, temperature, ratio, words/s, language, wall time, and every attempt. The markdown header names the model(s) that actually ran, adds a counts line when anything was retried, suspect or missing, and drops "transcribed locally".meta.jsongainstranscriber,gapsandsuspect_chunks, all replaced in full on each run so nothing stale survives.--retranscribe [--allow-gaps] <id>: re-runs a finished session. It refuses if the session is recording or has a live worker, if another session has a live worker, or if another retranscribe holds the lock. Until it commits, it writes only its own files (retranscribe.json, the lock,chunks/). It commits only a gap-free run. On commit it copiestranscript.md/.json,chunks.jsonand the originalmeta.jsonto*.v1-loop.*(later runs use*.prev-<UTC time>.*, and no archive is ever overwritten). It does the same in the export folder the session already points at, which is checked to belong to that session. Only then does it swap the new files in. Nothing that existed before is deleted.store.js:writeMetawrites a temp file and renames it, so the panel's status poll never reads half a file.WHISPER_SLICE_TIMEOUT_MSandWHISPER_SLICE_ATTEMPTSstill work as aliases), VAD on by default, and gap segments plusmeta.gapsin the same{start, seconds, error}shape. The oldslices/folder is never touched.Docs / manifest: README rewritten for chunks, the defences, every env var and re-transcription, with measured throughput. Its old 600 s slice default was wrong; the code said 240 s. The manifest no longer claims "nothing leaves the machine", since that holds only for a loopback endpoint. Version 1.0.2; registry rebuilt.
Tests
tests/meeting-recorder-transcribe.test.js: 40 tests against fake endpoints on loopback.meta.jsonbyte-identical and a re-run commits; every refusal; dead-lock takeover.Full suite: 10,123 tests, 10,101 pass, 0 fail, 22 skipped.
Live check
Four 3-minute clips of a real meeting (two of which made large-v3 loop when sent whole: 22 and 3.4 words/s, one sentence repeated 211x) were run through the new worker against a CPU faster-whisper-server large-v3 int8, compared with two independently built loop-free references (agreement between the references: WER 0.017):
No loops, no gaps, no suspect chunks. Per-chunk speed was 1.9–3.2 words/s, so the 4 words/s guard has headroom. Each chunk took at most 7.9 s.
https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp