Spike: audio/video upload → whisperX SRT → Cardigan job (no caption file)#271
Spike: audio/video upload → whisperX SRT → Cardigan job (no caption file)#271mriechers wants to merge 4 commits into
Conversation
Thin vertical slice proving the "no caption file required" path: - diarization service: new POST /transcribe (whisperX transcribe + align, no diarize) returning segments with text + duration + language - diarization_client: transcribe() mirroring diarize() - utils: segments_to_srt() to build SRT from whisperX segments (unit-tested) - upload router: POST /api/upload/media — accepts audio/video, calls the service, builds an SRT, writes it to transcripts/, and creates a job via the existing JobCreate path (dedup + Airtable SST auto-link) Verified end-to-end locally: raw MP4 → 16 segments → SRT → job → 4 phases completed ($0.06). See SPIKE-NOTES.md for the open build items (anti- hallucination thresholds, glossary wiring, SRT download, QC pass). [Agent: Claude Code] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ULSNyeewocgPt6sAPw7ej
- SPIKE-NOTES.md: full spike + vertical-slice findings, deployment shape (whisperX already containerized as cardigan-diarization), and the build backlog (glossary wiring, anti-hallucination, SRT download, QC pass) - knowledge/glossary.md: header documenting this as the Cardigan project layer that extends the workspace base glossary (project wins on conflict); add reporter correction Erica Ayisi (whisperX heard "Erika Aisi") [Agent: Claude Code] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ULSNyeewocgPt6sAPw7ej
Code reviewThree issues found in 1. 2 GB upload buffered into RAM before the size check takes effectcardigan/api/routers/upload.py Lines 263 to 270 in b676636
The fix is to stream the body to the temp file in bounded chunks, enforcing a running total: MAX_CHUNK = 1 * 1024 * 1024 # 1 MB
with tempfile.NamedTemporaryFile(suffix=file_ext, delete=False) as tmp:
tmp_path = tmp.name
total = 0
while chunk := await file.read(MAX_CHUNK):
total += len(chunk)
if total > MAX_MEDIA_FILE_SIZE:
Path(tmp_path).unlink(missing_ok=True)
raise HTTPException(status_code=400, detail="File too large. Maximum size: 2 GB")
tmp.write(chunk)This also eliminates the separate 2. SRT silently overwrites an existing transcript when the media ID is absentcardigan/api/routers/upload.py Lines 274 to 308 in b676636 When existing = await database.find_jobs_by_media_id(media_id) if media_id else []
A simple guard before writing: if srt_path.exists():
raise HTTPException(status_code=409, detail=f"Transcript already exists: {srt_name}")Or use try:
srt_path.write_text(srt_content, encoding="utf-8") # replace with open(..., 'x')
except FileExistsError:
raise HTTPException(status_code=409, detail=f"Transcript already exists: {srt_name}")3. Temp file leaked when
|
|
@claude please review |
|
Claude finished @mriechers's task in 2m 28s —— View job Code Review
Verdict: solid spike. The path from raw media to publishable metadata is proven and the implementation is clean. Three issues were already flagged in the prior review against
|
| # | Issue | Status |
|---|---|---|
| 1 | 2 GB body buffered into RAM before size check | Still present, line 264–269 |
| 2 | SRT silently overwrites existing file when media_id is None |
Still present, line 307 |
| 3 | Temp file leaked if tmp.write() raises |
Still present, line 291–296 |
SPIKE-NOTES.md
Excellent documentation. The two ⚠️ findings (anti-hallucination thresholds, glossary not wired) and the layered glossary architecture decision are exactly the right things to capture before the real build. The repro steps are detailed enough to reproduce from cold.
One note: the SPIKE-NOTES.md hardcodes 6HNS20260619HoChunkLand.mp4 in the repro steps — fine for a spike doc, but that filename also surfaces the media-ID truncation bug you noted (6HNS20260619… → 6HNS2026). Worth a unit test in test_extract_media_id.py (if one exists) so the fix has coverage before the feature ships.
Summary
| File | New Issues | Notes |
|---|---|---|
diarization/app.py |
3 (temp-file leak ×2, no size guard) | Same patterns as upload.py |
diarization_client.py |
3 (log message, except redundancy, no CM support) | Minor / naming |
utils.py |
1 (in-place mutation) | Low risk today, higher risk at SRT download |
test_segments_to_srt.py |
2 (missing edge cases) | Easy adds |
upload.py |
— | Covered in prior review |
SPIKE-NOTES.md |
— | Well-written |
The spike is ready to inform the real build. I'd prioritize the three upload.py issues (#1 streaming, #2 overwrite guard, #3 temp file), then the diarization service temp-file leak (#1 above), before shipping to users.
What this is
A prototype / spike proving the "upload media, no caption file required" workflow end-to-end. Not for merge as-is — it's a reviewable vertical slice with an explicit build backlog (see
SPIKE-NOTES.md).The path it proves
raw MP4 → whisperX service → SRT → existing job pipeline → 4-phase metadatadiarization/app.py): newPOST /transcribe— whisperX transcribe + align (no diarize), returns segments with text + duration + language. (The service already ran whisperX for/diarizebut discarded the text; this surfaces it.)api/services/diarization_client.py):transcribe()mirroringdiarize().api/services/utils.py):segments_to_srt()— builds SRT from whisperX segments. Unit-tested (tests/api/test_segments_to_srt.py).api/routers/upload.py):POST /api/upload/media— accepts audio/video, calls the service, builds the SRT, writes it totranscripts/, and creates a job via the existingJobCreatepath (dedup + Airtable SST auto-link).Verified
Ran locally, prod-shaped (whisperX service in one venv, API/worker in another, over HTTP): raw MP4 → 16 segments → SRT → job → all 4 phases
completed($0.06).ruffclean; unit test passing. Full write-up inSPIKE-NOTES.md.Key deployment finding
whisperX is already containerized in prod as
cardigan-diarization(behind thediarizationcompose profile) — so "run whisperX in a container" is already solved; this extends that service.Open build items (tracked in SPIKE-NOTES.md)
/transcribe(raw whisperX hallucinated cues over a silent b-roll gap; the validator caught it)initial_prompt) — see Secrets resolver hardcodes macOS Keychain for local dev; team standard is 1Password #267-adjacent secrets note and glossary headers6HNS20260619…→ truncates to6HNS2026)🤖 Generated with Claude Code