[Backend] Persist transcript segment rows and render the text format as a view #335
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: HIGH. Found in the August 2026 session lifecycle review (#319).
The defect
The transcript is persisted as a single flat
Textcolumn (webapp/backend/app/models/session.py:152) with no segment rows. Four places then re-parse that text format, which makes the rendering format a load-bearing data contract:reminder_tasks.py:2786— member erasure regex-parses the blob (_TRANSCRIPT_LINE_RE)audio_service.py:1052-1067— highlight quotes validated by substring against the blob, while the storedspeakerandtimestamp_refare LLM-asserted and never checkedlore_service.py:236-285—chunk_texthard-cuts by character count and can split mid-line, so a chunk can open with unattributed textSessionDetail.jsx:373-408— the GM free-edits the raw blob, and a hand edit can break the erasure regexProposed fix
Add a segment table:
session_id,track_owner_id,speaker_label,start,end,text,span_idx. Write it inprocess_audio; makemerge_attributed_transcripta renderer over those rows.Note the justification has shifted since the capture fix: this is no longer about timeline fidelity, it is about structural robustness. Erasure operates on rows instead of a regex. Highlights become validatable. Per-speaker QA ("this track had 40 minutes of speech and zero segments") becomes a query.
track_owner_idmakes a server-side speaker-label override detectable. And the text format can be re-rendered — ranges, scene blocks, anything — without re-transcribing.Decide explicitly how GM free-editing interacts: either edits write back to rows, or the blob overrides rows with rows frozen as provenance. Do not leave it implicit.
Acceptance criteria
process_audioand backfilled where a transcript existsmerge_attributed_transcriptrenders from rows_TRANSCRIPT_LINE_REis deletedVerified; remaining gaps closed in
9dd7122. Two criteria are recorded as deliberately-not-done rather than met.Criteria
process_audioand backfilled where a transcript exists — writing yes; backfill deliberately refused, see below.merge_attributed_transcriptrenders from rows — not as written, see below._TRANSCRIPT_LINE_REis deleted — first half met; the regex stays, deliberately.The single-pass path never chunked on boundaries at all
Multi-pass packs whole
TranscriptSegmentrows, so a chunk can only begin and end between utterances. Single-pass — admin-selectable viaLORE_PIPELINE_SINGLE_PASS— ransample_evenlystraight over the raw transcript characters, cutting mid-utterance freely. Exactly the defect this issue exists to prevent, on the path nobody looked at.sample_evenly_over_segmentskeeps the property that matters for single-pass — coverage across the whole session, not the first twenty minutes — while only ever cutting between whole lines. Sessions with no rows keep the old behaviour.The test first asserts the character sampler does produce a mid-line cut on that fixture, so it cannot pass vacuously.
chunk_segments had silently dropped chunk_text's overlap
chunk_texttakesoverlap_chars, documented as existing "so entities mentioned at chunk edges are not missed". The row-based replacement took no overlap parameter, so every post-#335 session lost that property with nothing saying so. Now carried as whole segments, and dropped rather than padded when it would push a chunk past its budget — an overlap that overflows the prompt would reintroduce the risk the chunker exists to remove.Chunk text did not match the transcript for simultaneous speech
_render_segment_linealways emitted a single[HH:MM:SS], while the stored transcript renders an overlapping line as[start-end](#345). The extractor read a different rendering than the GM sees, for precisely the lines where two people talked at once. It now goes throughaudio_service.mark_overlapping— the same function the transcript renderer uses, not a second copy of the rule — and the test asserts byte equality withrender_transcript_from_rows.Two criteria that will not be met, and should be closed as such
"Backfilled where a transcript exists" was refused by migration
a2b3c4d5e6f8, and the reasoning is right: the structure old blobs would need was never captured, and a best-effort re-parse produces rows that look authoritative and are not. Same call as #328'spre_timeline_fixmarker. The criterion should be struck rather than left open."
_TRANSCRIPT_LINE_REis deleted" cannot follow while that holds. It still runs for sessions recorded before rows existed, and for a transcript a GM has hand-edited away from its rows. Rows are primary —_scrub_session_transcriptscrubs bytrack_owner_id— but the regex is the fallback for a corpus that will never have rows."
merge_attributed_transcriptrenders from rows" is likewise not literally true: there are two renderers,merge_attributed_transcriptover in-memory dicts andrender_transcript_from_rowsover rows, kept byte-identical by a test. The "text is a view of the rows, one renderer" refactor did not happen. Recording it plainly rather than ticking the box — the property holds, the structure the criterion asked for does not.GM edit semantics
Implemented and tested, documented only in code comments;
docs/mentioned none of this. Now indocs/API.md, and flagged as what it actually is: a third option, neither of the two the issue proposed. On erasure of a hand-edited transcript the code writes to both — redacting the rows and separately scrubbing the text — so rows are not "frozen as provenance", they are mutated. Defensible for a legal erasure, and now written down.Per-speaker QA
transcript_qa_serviceplus a GM-only endpoint, grouped by(track_owner_id, speaker_label)so a mid-session rename cannot merge two people.It states its own limitation rather than overselling: audio duration per track is not stored beside the rows, so it answers "how much did each speaker's transcript contain" and can show a track that looks thin — but it cannot alone confirm the motivating case, "this track had 40 minutes of speech and zero segments". It raises the suspicion; resolving it needs the source audio.
Also added the two
summarisation-runsendpoints from #333/#424, whichdocs/API.mdhad never mentioned.Closing, on the understanding that the three struck criteria are closed as won't-do rather than done.