feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) (#39)
CI / Deploy via GitOps (push) Successful in 7s
CI / Lint / Test / Vet (push) Successful in 33s
CI / Build & Import (push) Successful in 23s

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>
This commit was merged in pull request #39.
This commit is contained in:
2026-10-03 14:13:37 +00:00
committed by mathias
parent 4bafd5f082
commit 4a2067063d
104 changed files with 14214 additions and 311 deletions
+4 -4
View File
@@ -583,7 +583,7 @@ templ chatPanel(v chatView) {
</div>
}
if v.Truncated {
<p class="chat-note muted">Working from a bounded portion of a long transcript — answers about the end of the video may be incomplete.</p>
<p class="chat-note muted">Working from a bounded portion of a long transcript — answers about the end of the { v.subject() } may be incomplete.</p>
}
if v.Error != "" {
<p class="chat-error" role="alert">{ v.Error }</p>
@@ -591,8 +591,8 @@ templ chatPanel(v chatView) {
<form
class="chat-form"
method="post"
action={ chatURL(v.VideoID) }
hx-post={ string(chatURL(v.VideoID)) }
action={ v.postURL() }
hx-post={ string(v.postURL()) }
hx-target="#chat-panel"
hx-swap="outerHTML"
>
@@ -609,7 +609,7 @@ templ chatPanel(v chatView) {
</select>
<span class="chat-model-hint muted">Switch models to compare answers on the same transcript.</span>
</label>
<textarea name="question" rows="3" placeholder="Ask a question about this video…" required aria-label="Your question"></textarea>
<textarea name="question" rows="3" placeholder={ "Ask a question about this " + v.subject() + "…" } required aria-label="Your question"></textarea>
<button type="submit" class="btn">Ask</button>
<span class="htmx-indicator chat-thinking">thinking…</span>
</form>