feat(calls): transcript export (txt/vtt) + archive search (LC-393 Phase 4) #409

Merged
longjacksonle merged 1 commit from feat/lc-395-transcript-export-search into main 2026-06-20 08:41:18 +02:00

LC-393 Phase 4 - the concrete, dependency-free core (browse landed in LC-394). Fixes LC-395.

Export (txt + vtt)

GET /transcripts/{id}/export?format=txt|vtt downloads a saved transcript, gated with the same access check as the transcript page, served as an attachment (Content-Disposition). Download links added to the transcript page.

  • txt: a header (call + date) then HH:MM:SS Speaker: text per line, the timestamp being the offset from the call start.
  • vtt: standard WebVTT (HH:MM:SS.mmm --> ... + <v Speaker>text) so the transcript loads as a caption track / imports elsewhere. The stored segment timestamps are only second-resolution (several can share a second), so cue times are forced monotonic - each cue starts at max(real offset, prev end) and ends at the next cue's start (last gets a short tail) - guaranteeing valid, ordered, non-zero-length cues. Cue text is HTML-escaped.

The archive (/transcripts) gains a ?q= filter: list only transcripts containing a segment matching the query (case-insensitive, LIKE-escaped EXISTS), still access-gated per row. A search box on the archive page echoes the query; a distinct empty state shows when a query matches nothing (vs the "no transcripts yet" state).

Testing

  • tests/transcripts.rs -> 10 green: export_txt_and_vtt (content + WEBVTT/--> shape + non-member 403) and archive_search_filters_by_content (matching lists, non-matching excludes).
  • just check, just test, just test-saas all exit 0; just build-css regenerated.

Out of scope (optional follow-up)

The LLM summary / action-items piece. It needs an operator-configured LLM endpoint - a new [operator-action] config + mockable client mirroring the Phase 3 STT pattern, plus storage + UI - so it's a separate PR rather than crammed in here. Happy to do it next if wanted.

No operator action, no migration.

LC-393 Phase 4 - the concrete, dependency-free core (browse landed in LC-394). Fixes LC-395. ## Export (txt + vtt) `GET /transcripts/{id}/export?format=txt|vtt` downloads a saved transcript, gated with the same access check as the transcript page, served as an attachment (`Content-Disposition`). Download links added to the transcript page. - **txt**: a header (call + date) then `HH:MM:SS Speaker: text` per line, the timestamp being the offset from the call start. - **vtt**: standard WebVTT (`HH:MM:SS.mmm --> ...` + `<v Speaker>text`) so the transcript loads as a caption track / imports elsewhere. The stored segment timestamps are only second-resolution (several can share a second), so cue times are forced monotonic - each cue starts at `max(real offset, prev end)` and ends at the next cue's start (last gets a short tail) - guaranteeing valid, ordered, non-zero-length cues. Cue text is HTML-escaped. ## Search The archive (`/transcripts`) gains a `?q=` filter: list only transcripts containing a segment matching the query (case-insensitive, LIKE-escaped `EXISTS`), still access-gated per row. A search box on the archive page echoes the query; a distinct empty state shows when a query matches nothing (vs the "no transcripts yet" state). ## Testing - `tests/transcripts.rs` -> 10 green: `export_txt_and_vtt` (content + WEBVTT/`-->` shape + non-member 403) and `archive_search_filters_by_content` (matching lists, non-matching excludes). - `just check`, `just test`, `just test-saas` all exit 0; `just build-css` regenerated. ## Out of scope (optional follow-up) The LLM summary / action-items piece. It needs an operator-configured LLM endpoint - a new `[operator-action]` config + mockable client mirroring the Phase 3 STT pattern, plus storage + UI - so it's a separate PR rather than crammed in here. Happy to do it next if wanted. No operator action, no migration.
feat(calls): transcript export (txt/vtt) + archive search (LC-393 Phase 4)
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 2m48s
Create release / Create release from merged PR (pull_request) Has been skipped
f27914da4c
The concrete, dependency-free core of LC-393 Phase 4 (browse landed in LC-394).

Export: GET /transcripts/{id}/export?format=txt|vtt downloads a saved transcript, gated with the same access check as the transcript page, served as an attachment. txt is a header + "HH:MM:SS  Speaker: text" per line (offset from the call start); vtt is standard WebVTT cues (<v Speaker>text) with forced-monotonic timings (the stored timestamps are second-resolution, so cue start = max(real offset, prev end) and end = next start, guaranteeing valid ordered cues). Download links on the transcript page.

Search: the archive (/transcripts) gains a ?q= filter listing only transcripts containing a matching segment (case-insensitive, LIKE-escaped EXISTS), still access-gated per row. A search box on the archive page; a distinct empty state when a query matches nothing.

db::transcripts::list_recent now takes an optional query; new routes::transcripts::export with txt/vtt builders; IndexQuery/ExportQuery; en/es strings; export + search tests.

The optional LLM summary / action-items piece (needs an operator-configured endpoint, mirroring the Phase 3 STT pattern) is intentionally left as a separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-395-transcript-export-search 2026-06-20 08:41:18 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!409
No description provided.