{"openapi":"3.0.3","info":{"title":"Meeting Scribe API","version":"3.0.0","description":"Automated meeting transcription and note-generation pipeline with task extraction.\n\nMeeting Scribe provides a complete end-to-end workflow for processing meeting recordings:\n- Audio transcription via haiven-transcribe (tri-engine: Canary + Parakeet + Whisper Turbo)\n- Transcript cleaning (filler word removal, formatting)\n- Metadata inference (title, speakers, mentioned people)\n- Meeting notes generation (template-based LLM prompts)\n- Hallucination validation with automatic retry\n- Structured data extraction (tasks, decisions, follow-ups)\n- Email delivery with styled HTML + local file storage\n\nVersion 3.0 adds per-item candidate triage, embedding-based Vikunja dedupe, project picker, and title generation.\n\nThe service supports 9 meeting templates and provides real-time progress tracking via Server-Sent Events (SSE).\n","contact":{"name":"Haiven Infrastructure","url":"https://scribe.haiven.site"},"license":{"name":"Internal Use Only"}},"servers":[{"url":"https://scribe.haiven.site","description":"Production server"},{"url":"http://localhost:5010","description":"Development server"}],"tags":[{"name":"Health","description":"Service health check"},{"name":"Upload","description":"Audio file upload and job creation"},{"name":"Jobs","description":"Job management and retrieval"},{"name":"Progress","description":"Real-time job progress streaming"},{"name":"Templates","description":"Meeting template management"},{"name":"Edit","description":"Job editing and partial re-runs (v2)"},{"name":"Triage","description":"Per-item candidate triage and Vikunja push (v3)"},{"name":"Projects","description":"Vikunja project listing (v3)"}],"paths":{"/health":{"get":{"tags":["Health"],"summary":"Health check","description":"Returns service health status","operationId":"getHealth","responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"service":{"type":"string","example":"meeting-scribe"}}}}}}}}},"/api/upload":{"post":{"tags":["Upload"],"summary":"Upload audio file","description":"Upload an audio file to start the processing pipeline. Supported formats: mp3, m4a, wav, ogg, webm, mp4, flac.\n\nThe pipeline stages are:\n1. Transcribe (haiven-transcribe)\n2. Clean transcript (filler word removal)\n3. Infer metadata (title, speakers, mentioned people)\n4. Generate notes (LLM with template)\n5. Validate notes (hallucination detection)\n6. Extract data (tasks/decisions/follow-ups)\n7. Deliver (email + file save)\n","operationId":"uploadAudio","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Audio file to process"},"title":{"type":"string","description":"Meeting title (optional, defaults to filename)","example":"Weekly standup 2026-02-09"},"attendees":{"type":"string","description":"Comma-separated list of attendees (optional)","example":"Alice, Bob, Charlie"},"template_id":{"type":"string","description":"Template ID to use for notes generation (optional, defaults to 'meeting-notes')","example":"standup","enum":["meeting-notes","one-on-one","standup","retro","brainstorm","discovery","planning","interview","client-call"]}}}}}},"responses":{"200":{"description":"File uploaded successfully, pipeline started","content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid","description":"Unique job identifier","example":"550e8400-e29b-41d4-a716-446655440000"},"status":{"type":"string","description":"Initial job status","example":"ingested"},"redirect":{"type":"string","description":"URL to job detail page","example":"/jobs/550e8400-e29b-41d4-a716-446655440000"}}}}}},"400":{"description":"Invalid file type or request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs":{"get":{"tags":["Jobs"],"summary":"List all jobs","description":"Returns an array of all jobs with their current status and metadata","operationId":"listJobs","responses":{"200":{"description":"List of jobs","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Job"}}}}}}}},"/api/jobs/{job_id}":{"get":{"tags":["Jobs"],"summary":"Get job details","description":"Returns complete job details including file paths, timestamps, and current status","operationId":"getJob","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Job UUID"}],"responses":{"200":{"description":"Job details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs/{job_id}/progress":{"get":{"tags":["Progress"],"summary":"Real-time job progress (SSE)","description":"Server-Sent Events (SSE) stream for real-time pipeline progress updates.\n\nEvent types:\n- `progress`: Status change with message\n- `heartbeat`: Keep-alive ping every 15 seconds\n- `complete`: Final event when job finishes (success or failure)\n\nThe stream automatically closes when the job reaches `delivered` or `failed` status.\n","operationId":"jobProgress","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Job UUID"}],"responses":{"200":{"description":"SSE stream of progress events","content":{"text/event-stream":{"schema":{"type":"string"},"examples":{"progress":{"summary":"Progress event","value":"event: progress\ndata: {\"status\": \"transcribing\", \"message\": \"Transcribing audio...\"}\n"},"heartbeat":{"summary":"Heartbeat event","value":"event: heartbeat\ndata: {\"status\": \"transcribing\"}\n"},"complete":{"summary":"Complete event","value":"event: complete\ndata: {\"status\": \"delivered\", \"message\": \"Complete! Notes delivered.\"}\n"}}}}}}}},"/api/jobs/{job_id}/transcript":{"get":{"tags":["Jobs"],"summary":"Get raw transcript","description":"Returns the raw transcript text as produced by the STT engine","operationId":"getTranscript","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Raw transcript text","content":{"application/json":{"schema":{"type":"object","properties":{"transcript":{"type":"string","description":"Raw transcript text"}}}}}},"404":{"description":"Transcript not yet available or job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs/{job_id}/clean-transcript":{"get":{"tags":["Jobs"],"summary":"Get cleaned transcript","description":"Returns the cleaned transcript with filler words removed and formatting normalized","operationId":"getCleanTranscript","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cleaned transcript text","content":{"application/json":{"schema":{"type":"object","properties":{"transcript":{"type":"string","description":"Cleaned transcript text"}}}}}},"404":{"description":"Clean transcript not yet available or job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs/{job_id}/notes":{"get":{"tags":["Jobs"],"summary":"Get meeting notes","description":"Returns generated meeting notes (validated if retry occurred, otherwise raw). Format is markdown.","operationId":"getNotes","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Meeting notes markdown","content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","description":"Meeting notes in markdown format"}}}}}},"404":{"description":"Notes not yet available or job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Edit"],"summary":"Update notes markdown (v2)","description":"Save edited notes markdown to disk (both validated and raw files).\n\nJob must be in `delivered` or `failed` status.\n\nUse this endpoint to save manual edits before triggering a partial re-run.\n","operationId":"updateNotes","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["notes"],"properties":{"notes":{"type":"string","description":"Edited notes markdown","example":"# Meeting Notes\n\n## Summary\n\nDiscussed Q1 goals..."}}}}}},"responses":{"200":{"description":"Notes saved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"saved"},"job_id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Missing 'notes' field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job is still processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs/{job_id}/rerun-from/notes":{"post":{"tags":["Edit"],"summary":"Re-run from notes stage (v2)","description":"Re-run pipeline from Stage 4 (notes generation) through completion.\n\nStages executed:\n1. Generate notes (reads clean transcript from disk)\n2. Validate notes (hallucination detection)\n3. Extract data (structured JSON)\n4. Deliver (email + file save)\n\nJob must be in `delivered` or `failed` status. Clean transcript file must exist.\n\nUse this after changing metadata or template to regenerate notes without re-transcribing.\n","operationId":"rerunFromNotes","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Pipeline restarted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"restarted"},"job_id":{"type":"string","format":"uuid"},"from_stage":{"type":"string","example":"notes"}}}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job is still processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Clean transcript no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/jobs/{job_id}/rerun-from/extract":{"post":{"tags":["Edit"],"summary":"Re-run from extract stage (v2)","description":"Re-run pipeline from Stage 6 (extraction) through completion.\n\nStages executed:\n1. Extract data (reads notes from disk)\n2. Deliver (email + file save)\n\nJob must be in `delivered` or `failed` status. Notes file must exist.\n\nUse this after editing notes markdown to re-extract structured data without regenerating notes.\n","operationId":"rerunFromExtract","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Pipeline restarted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"restarted"},"job_id":{"type":"string","format":"uuid"},"from_stage":{"type":"string","example":"extract"}}}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job is still processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Notes no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/templates":{"get":{"tags":["Templates"],"summary":"List templates (HTML page)","description":"Returns HTML page listing all available meeting templates","operationId":"listTemplatesPage","responses":{"200":{"description":"Templates page","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/templates":{"get":{"tags":["Templates"],"summary":"List templates (JSON)","description":"Returns JSON array of all available meeting templates with metadata","operationId":"listTemplates","responses":{"200":{"description":"Template list","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Template"}}}}}}}},"/api/projects":{"get":{"tags":["Projects"],"summary":"List Vikunja projects","description":"Proxies to vikunja-proxy and returns the list of Vikunja projects for use in the job header project picker.","operationId":"listProjects","responses":{"200":{"description":"Project list","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"}}}}}}}}}},"/api/jobs/{job_id}/candidates":{"get":{"tags":["Triage"],"summary":"List candidates for a job","description":"Returns all per-item triage candidates for the given job, populated from the extract_v3 pipeline.","operationId":"listCandidates","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Candidates list","content":{"application/json":{"schema":{"type":"object","properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}}}}}}},"404":{"description":"Job not found"}}}},"/api/jobs/{job_id}/dedupe-suggestions":{"get":{"tags":["Triage"],"summary":"Embedding-based dedupe suggestions","description":"Computes cosine similarity between candidate titles and open Vikunja tasks in the selected project.\nResults cached in-memory for 30 minutes per (job_id, project_id) pair.\n\nRequires `encoding_format: float` on the embedding request to qwen3-embedding-4b.\nThreshold configurable via settings DB key `dedupe.similarity_threshold` (default: 0.75).\n","operationId":"dedupeSuggestions","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"project_id","in":"query","required":true,"schema":{"type":"integer"},"description":"Vikunja project ID to compare against"},{"name":"force_refresh","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Bypass the 30-minute in-memory cache"}],"responses":{"200":{"description":"Dedupe suggestions per candidate","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"vikunja_id":{"type":"integer"},"title":{"type":"string"},"similarity":{"type":"number","format":"float"}}}}}}}}}}}}},"/api/jobs/{job_id}/approve-all":{"post":{"tags":["Triage"],"summary":"Approve all triaged candidates","description":"Pushes all non-dropped candidates to Vikunja and marks the job as delivered.\n\n- `kept` / `reassigned_to_me` → creates new Vikunja task in `associated_project_id`\n- `merged` → posts comment to `merge_target_vikunja_id`\n- `dropped` → no Vikunja action\n\nPrerequisites: all candidates must have `triage_state != pending`; job must have `associated_project_id` set.\n","operationId":"approveAll","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Approval completed","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"approved"},"pushed":{"type":"integer"},"merged":{"type":"integer"},"dropped":{"type":"integer"}}}}}},"400":{"description":"Not all candidates triaged, or project_id not set"}}}},"/api/candidates":{"post":{"tags":["Triage"],"summary":"Add a candidate manually","description":"Add a candidate item the LLM missed. Inserted with `triage_state=kept`.","operationId":"addCandidate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["job_id","item_type","title"],"properties":{"job_id":{"type":"string","format":"uuid"},"item_type":{"type":"string","enum":["commitment","requirement","follow_up","open_question","advisory"]},"title":{"type":"string"},"owner":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Candidate created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Candidate"}}}},"422":{"description":"Missing required fields"}}}},"/api/candidates/{candidate_id}":{"patch":{"tags":["Triage"],"summary":"Update candidate triage state","description":"Set the triage decision for a single candidate.","operationId":"updateCandidate","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["triage_state"],"properties":{"triage_state":{"type":"string","enum":["pending","kept","reassigned_to_me","dropped","merged"]},"merge_target_vikunja_id":{"type":"integer","nullable":true,"description":"Required when triage_state=merged"},"reassigned_owner":{"type":"string","nullable":true,"description":"Set when triage_state=reassigned_to_me (always 'Elijah')"}}}}}},"responses":{"200":{"description":"Candidate updated"},"422":{"description":"Missing required fields"},"404":{"description":"Candidate not found"}}}},"/api/jobs/{job_id}/generate-title":{"post":{"tags":["Edit"],"summary":"Generate LLM title for a job (v3)","description":"Reads the first 50% of the transcript and calls `glm-4-7-flash` (thinking disabled)\nto generate a concise meeting title. Persists and returns the new title.\n","operationId":"generateTitle","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"New title generated","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"}}}}}},"404":{"description":"Job not found"}}}}},"components":{"schemas":{"Job":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid","description":"Unique job identifier"},"title":{"type":"string","description":"Meeting title"},"original_filename":{"type":"string","description":"Original uploaded filename"},"status":{"type":"string","description":"Current pipeline status","enum":["ingested","transcribing","transcribed","cleaning_transcript","inferring_metadata","generating_notes","validating_notes","extracting_data","sending_email","delivered","failed"]},"template_id":{"type":"string","description":"Template used for notes generation"},"attendees":{"type":"string","description":"Comma-separated list of speakers (v2)"},"mentioned_people":{"type":"string","description":"Comma-separated list of mentioned people (v2)"},"audio_path":{"type":"string","description":"Path to audio file"},"transcript_path":{"type":"string","description":"Path to raw transcript"},"clean_transcript_path":{"type":"string","description":"Path to cleaned transcript"},"speaker_transcript_path":{"type":"string","description":"Path to speaker-attributed transcript (v2)"},"notes_path":{"type":"string","description":"Path to generated notes"},"tasks_path":{"type":"string","description":"Path to extracted tasks JSON"},"validation_path":{"type":"string","description":"Path to validation report"},"error_message":{"type":"string","nullable":true,"description":"Error message if status is failed"},"created_at":{"type":"string","format":"date-time","description":"Job creation timestamp"},"transcribed_at":{"type":"string","format":"date-time","nullable":true,"description":"Transcription completion timestamp"},"notes_generated_at":{"type":"string","format":"date-time","nullable":true,"description":"Notes generation timestamp"},"delivered_at":{"type":"string","format":"date-time","nullable":true,"description":"Email delivery timestamp"}}},"ExtractedData":{"type":"object","description":"Template-specific extracted data. Schema varies by template type.\n\nFor meeting-notes template, structure is tasks/decisions/follow_ups.\nFor standup template, structure is blockers/progress/plans.\nOther templates have their own schemas.\n","additionalProperties":true,"example":{"tasks":[{"title":"Review Q1 metrics","assignee":"Alice","priority":"high","deadline":"2026-02-15","context":"Review analytics dashboard before Friday's presentation"}],"decisions":[{"decision":"Adopt new deployment pipeline","rationale":"Reduces deployment time from 2h to 15min","stakeholders":["DevOps","Engineering"]}],"follow_ups":["Schedule follow-up meeting for next week","Share notes with stakeholders"]}},"ValidationReport":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"hallucination_score":{"type":"number","format":"float","minimum":0.0,"maximum":1.0,"description":"Hallucination score (0.0 = perfect, 1.0 = high hallucination)"},"threshold":{"type":"number","format":"float","description":"Configured threshold for retries"},"passed":{"type":"boolean","description":"Whether validation passed"},"attempt":{"type":"integer","description":"Validation attempt number"},"max_retries":{"type":"integer","description":"Maximum retry attempts configured"},"flagged_claims":{"type":"array","items":{"type":"string"},"description":"Claims flagged as unsupported"},"timestamp":{"type":"string","format":"date-time"}}},"Template":{"type":"object","properties":{"id":{"type":"string","description":"Template identifier"},"name":{"type":"string","description":"Template display name"},"description":{"type":"string","description":"Template description"},"prompt_path":{"type":"string","description":"Path to main template prompt file"},"extract_path":{"type":"string","description":"Path to extraction template file"}}},"Candidate":{"type":"object","properties":{"id":{"type":"string","description":"UUIDv4 candidate identifier"},"job_id":{"type":"string","format":"uuid"},"item_type":{"type":"string","enum":["commitment","requirement","follow_up","open_question","advisory"]},"title":{"type":"string"},"owner":{"type":"string","nullable":true},"deadline_iso":{"type":"string","nullable":true},"context_paragraph":{"type":"string","nullable":true},"source_quote":{"type":"string","nullable":true},"confidence_score":{"type":"integer","nullable":true},"suggested_project":{"type":"string","nullable":true},"triage_state":{"type":"string","enum":["pending","kept","reassigned_to_me","dropped","merged"]},"merge_target_vikunja_id":{"type":"integer","nullable":true},"vikunja_id":{"type":"integer","nullable":true,"description":"Set after successful Vikunja push"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Error":{"type":"object","properties":{"detail":{"type":"string","description":"Error message","example":"Job not found"}}}}}}