Upload a recording (audio, or video with the camera off), get a private Swedish transcript, and chat about it. - ADR-033: RLS-scoped private transcript store; ffprobe/ffmpeg intake; gateway transcription (iguana kb-whisper, berget fallback); in-process single-replica worker; chat over the transcript (local models only). - ADR-034: optional two-engine mode (pianissimo + kb-whisper) reconciled by a LOCAL model; off unless TAPIR_RECONCILE_BASE_MODEL is set. - ADR-035: real progress (phases, running/quiet/lost), phase logging, every judged spot stored, merged segments. - Hardening from four adversarial reviews: errors logged and stored as kinds never text; per-engine deadlines; converted-length check; bounded queue; client hang-ups are not errors; strict local-alias check. - Dockerfile: static ffmpeg/ffprobe 7.0.2 pinned by sha256 (#34). Built and inspected by hand; CI does not build the image on a PR, so its first CI build is this merge. Uploads stay OFF until TAPIR_UPLOADS_ENABLED is set in the deployment. Closes #33, #34, #42, #43, #44. Co-authored-by: Mathias <mthbqv@gmail.com>
49 lines
2.6 KiB
SQL
49 lines
2.6 KiB
SQL
-- Migration 017: uploaded_transcripts — PRIVATE transcripts of audio a user
|
|
-- uploaded themselves (ADR-033, tapir#33).
|
|
--
|
|
-- This is deliberately NOT the shared `transcripts` table (migration 015,
|
|
-- ADR-021). That table is non-RLS on the explicit grounds that it holds only
|
|
-- public caption content and nothing user-identifying. A recorded meeting is the
|
|
-- opposite: private third-party content owned by exactly one user. Reusing
|
|
-- `transcripts` — or adding a user_id to it with a partial policy — would put a
|
|
-- private row in the one table the isolation test asserts is shared, and would
|
|
-- pass CI green. A separate table keeps ADR-021's exception singular.
|
|
--
|
|
-- Row-level security follows migration 003 exactly: ENABLE + FORCE (the app
|
|
-- connects as the table owner, which bypasses RLS unless forced), and a policy
|
|
-- keyed off the per-request GUC tapir.current_user_id. With no GUC set the
|
|
-- predicate is NULL and the table is deny-all.
|
|
--
|
|
-- The original audio is never stored (ADR-033): this row is all that is kept.
|
|
-- segments holds the timestamped cues; re-transcribing 90 minutes costs ~7
|
|
-- minutes, so discarding the timestamps would be irreversible.
|
|
CREATE TABLE uploaded_transcripts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'queued'
|
|
CHECK (status IN ('queued', 'transcribing', 'done', 'failed')),
|
|
content TEXT,
|
|
segments JSONB,
|
|
endpoint TEXT, -- which backend served it (iguana | berget); a quality fact
|
|
audio_seconds DOUBLE PRECISION,
|
|
error_kind TEXT, -- typed, never a provider's error string (ADR-033)
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
finished_at TIMESTAMPTZ,
|
|
CONSTRAINT uploaded_transcripts_done_has_content
|
|
CHECK (status <> 'done' OR content IS NOT NULL),
|
|
CONSTRAINT uploaded_transcripts_failed_has_error_kind
|
|
CHECK (status <> 'failed' OR error_kind IS NOT NULL)
|
|
);
|
|
|
|
CREATE INDEX idx_uploaded_transcripts_user ON uploaded_transcripts (user_id, created_at DESC);
|
|
|
|
ALTER TABLE uploaded_transcripts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE uploaded_transcripts FORCE ROW LEVEL SECURITY;
|
|
CREATE POLICY uploaded_transcripts_isolation ON uploaded_transcripts
|
|
FOR ALL
|
|
USING (user_id = current_setting('tapir.current_user_id', true)::uuid);
|
|
|
|
COMMENT ON TABLE uploaded_transcripts IS
|
|
'PRIVATE per-user transcripts of user-supplied audio. RLS-scoped (ADR-012). Never merge into the shared transcripts table (ADR-021, ADR-033).';
|