Migrering 0005 samler kanban-kort, kalenderhendelser, faktoider og notater til én felles messages-tabell med view-config-tabeller. Actors og topics erstattes av unified entities-tabell. - 0005_meldingsboks.sql: messages utvides med title/pinned/visibility, kanban_card_view + calendar_event_view + message_reactions opprettes, entities erstatter actors+topics, gamle tabeller droppes - seed_dev.sql: oppdatert til meldingsboks-modell + 5 test-entiteter med graf-relasjoner - API-ruter: kanban/kalender/notater bruker messages + view-config - Dokumentasjon: meldingsboks feature-spec, oppdatert arkitektur, kunnskapsgraf, jobbkø, konseptdokumenter og proposals Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
No EOL
10 KiB
Markdown
130 lines
No EOL
10 KiB
Markdown
# Konsept: Podcastfabrikken (Lyd & Publiserings-Pipeline)
|
|
**Filsti:** `docs/concepts/podcastfabrikken.md`
|
|
|
|
## 1. Konsept
|
|
Den automatiserte "samlebåndet" som tar over når en ferdigklippet episode er klar, samt verktøyet for å **oppdatere eksisterende episoder** (f.eks. en rullerende intro-episode). Målet er at maskinen gjør 90 % av grovarbeidet (transkripsjon, metadata, kapittelinndeling), men at redaksjonen alltid kan overstyre resultatet manuelt før publisering.
|
|
|
|
## 2. Arkitektur & Dataflyt
|
|
Dette er en asynkron arbeidsflyt som kombinerer filsystem, AI, databaser og CI/CD.
|
|
|
|
1. **Trigger (Opplasting/Oppdatering):** Brukeren laster opp en `.mp3`-fil via SvelteKit-grensesnittet. Dette rutes enten som en *ny* episode (`INSERT`), eller en *oppdatering* av en eksisterende (`UPDATE`).
|
|
2. **Kø-system (PostgreSQL jobbkø):** Siden lydprosessering tar tid (CPU-intensivt), legges oppgaven i den felles jobbkøen (se `docs/infra/jobbkø.md`). Opplastingen oppretter to jobber i sekvens: først `whisper_transcribe`, deretter `openrouter_analyze` (som trigges automatisk ved fullført transkripsjon).
|
|
3. **Transkripsjon (faster-whisper):** Rust-worker kaller faster-whisper-server (OpenAI-kompatibelt API, `POST /v1/audio/transcriptions`) med `response_format=srt` og mottar SRT direkte. Modell: `Systran/faster-whisper-medium` med `initial_prompt` (navneliste).
|
|
4. **Lagring av transkripsjon (Git):** Rust-worker committer SRT-filen til Forgejo. SRT er master-formatet — redigerbart, tidsstemplet, og et etablert standardformat. Git gir diff, historikk og sporbarhet. Redaksjonen kan redigere SRT direkte.
|
|
5. **Avledede formater (PostgreSQL):** Ved commit (via Forgejo webhook) parser en Rust-worker SRT-filen og genererer:
|
|
* **Ren tekst** — strippes fra SRT (fjern tidsstempler/sekvensnummer) for lesbart publiseringsdokument
|
|
* **Segmenter** — tidsstemplede utdrag koblet til Aktører/Temaer i kunnskapsgrafen
|
|
* **Full-text søkeindeks** — for oppslag på tvers av episoder
|
|
6. **AI-Analyse (OpenRouter):** Transkripsjonen sendes til OpenRouter (Claude-modell) for uttrekk av forslag til tittel, sammendrag, show notes og kapittler.
|
|
7. **Manuell Godkjenning & Fletting (SvelteKit):**
|
|
* *For nye episoder:* Presenteres som et ferskt utkast.
|
|
* *For oppdateringer:* Viser AI-ens nye forslag side-om-side med eksisterende metadata. Redaksjonen kan da velge hva som skal beholdes eller flettes (merge).
|
|
8. **Publisering (PostgreSQL):** Ved "Godkjenn" lagres metadataene permanent i databasen.
|
|
9. **RSS-Generering:** SvelteKit-appen genererer en oppdatert `/feed.xml`.
|
|
|
|
### 2.1 Episodeside (publisert visning)
|
|
Hver publisert episode får en side med:
|
|
* Lydavspiller + sammendrag + kapitler + stikkord
|
|
* Personreferanser og artikler (koblet via kunnskapsgrafen)
|
|
* Fane: **SRT** (nedlastbar undertekstfil — master-kopi fra Git)
|
|
* Fane: **Ren tekst** (lesbart transkripsjonsdokument — avledet fra SRT, lagret i PG)
|
|
|
|
## 3. Spesialhåndtering: Oppdatering av eksisterende episoder (Cache-busting)
|
|
Podcast-apper (Apple, Spotify) og CDN-er cacher innhold aggressivt. For at en endring i f.eks. "Introepisoden" skal slå gjennom hos lytterne, MÅ følgende tekniske regler følges:
|
|
|
|
* **Filnavn-versjonering (Viktigst!):** Den nye lydfilen skal *aldri* overskrive det gamle filnavnet på disken. Systemet må legge til en hash, UUID eller et tidsstempel (f.eks. `intro_v2_1710289000.mp3`). Dette tvinger appene til å laste ned filen på nytt.
|
|
* **RSS `<guid>` (Global Unique Identifier):** Denne taggen MÅ forbli 100% statisk/uendret fra originalepisoden. Den forteller appene at "Dette er fortsatt samme episode, ikke lag en duplikat".
|
|
* **RSS `<enclosure>`:** URL-en i `enclosure`-taggen (som peker på `.mp3`-filen) oppdateres i databasen til å reflektere det *nye* filnavnet.
|
|
* **RSS `<pubDate>`:** SvelteKit-grensesnittet skal gi redaksjonen en toggle-knapp ved oppdatering:
|
|
* Alternativ A: "Behold opprinnelig dato" (Episoden oppdateres i det stille for nye lyttere).
|
|
* Alternativ B: "Sett dato til NÅ" (Episoden spretter til toppen av feeden som en ny utgivelse).
|
|
|
|
## 4. Whisper-konfigurasjon
|
|
* **Tjeneste:** `fedirz/faster-whisper-server` (Docker, OpenAI-kompatibelt API)
|
|
* **Endepunkt:** `POST /v1/audio/transcriptions` med `response_format=srt`
|
|
* **Beslutning:** SRT direkte fra Whisper, ikke verbose JSON. Verbose JSON inneholder diagnostikk (tokens, logprob, temperatur) som ikke har verdi for oss. SRT gir tidsstempler + tekst i et etablert format som er redigerbart, diffbart i Git, og trivielt å parse til ren tekst og segmenter.
|
|
* **Modeller (benchmarket med E277.mp3, 32:45 norsk tale, CPU i7-13900K):**
|
|
|
|
| Konfigurasjon | Tid (CPU) | Seg | Tegn | Kommentar |
|
|
|---|---|---|---|---|
|
|
| `small` | ~6 min | 777 | 25851 | Rask, men hyppige feil i egennavn |
|
|
| `medium` | ~18 min | 442 | 26938 | God balanse, noen navnefeil |
|
|
| `medium` + prompt | ~17 min | 455 | 26957 | Riktige egennavn, anbefalt standard |
|
|
| `large-v3` | ~24 min | 520 | 14559 | Hallusinerer uten VAD — IKKE bruk uten VAD |
|
|
| `large-v3` + VAD | ~31 min | 964 | 28291 | God kvalitet, men noen navnefeil |
|
|
| `large-v3` + VAD + prompt | ~31 min | 964 | 28295 | Best kvalitet, riktige egennavn |
|
|
|
|
* **Anbefaling:** `medium` + `initial_prompt` som standard. `large-v3` + VAD + prompt for best mulig kvalitet der det er verdt ventetiden.
|
|
* **Viktig:** `large-v3` KREVER `vad_filter=true` — uten hallusinerer modellen repeterende tekst.
|
|
* **Språk:** Sett `language=no` eksplisitt for norsk — unngå auto-detect som kan velge dansk/svensk.
|
|
|
|
### 4.1 initial_prompt (navneliste)
|
|
`initial_prompt` primes Whisper med ordforråd som forbedrer gjenkjenning av egennavn. Effekten er tydelig:
|
|
* Uten prompt: "Vegard Nøgnes", "SideLinja", "Sidlinja"
|
|
* Med prompt: "Vegard Nøtnæs", "Sidelinja" (riktig)
|
|
|
|
Prompten bygges automatisk av Rust-worker fra en statisk navneliste + aktører i kunnskapsgrafen:
|
|
```
|
|
Sidelinja podcast med Vegard Nøtnæs, Trond Sørensen, Arne Eidshagen,
|
|
Peter Hagen, Nicolai Buzatu, Bjørn Einar Drag, Øystein Sjølie
|
|
```
|
|
|
|
## 5. Workspace-spesifikk konfigurasjon
|
|
Hver workspace har sin egen podcast-konfigurasjon, lagret i `workspaces.settings` (JSONB):
|
|
|
|
### 5.1 Mediefiler
|
|
Lydfiler lagres i undermapper per workspace: `/srv/sidelinja/media/{workspace_slug}/`. Caddy ruter trafikk basert på domene (fra `workspaces.domain`) til riktig undermappe.
|
|
|
|
### 5.2 Transkripsjoner (Git-repostruktur)
|
|
Det opprettes **ett Forgejo-repo per workspace** for SRT-filer, slik at historikk og redigering ikke blandes på tvers av podcaster.
|
|
|
|
#### Repo-oppretting
|
|
Repoet opprettes **on-demand** ved første transkripsjonsjobb for en workspace, via Forgejo API. Ikke alle workspaces trenger transkripsjonsrepo.
|
|
|
|
#### Filnavnkonvensjon
|
|
Flat struktur med prosesseringstidspunkt som filnavn:
|
|
```
|
|
20260315_143022.srt
|
|
20260401_091500.srt
|
|
```
|
|
* **Format:** `YYYYMMDD_HHMMSS.srt` — settes automatisk av Rust-worker ved prosessering
|
|
* **Sortering:** Kronologisk i enhver filvisning
|
|
* **Unikhet:** Tidsstempel garanterer unikhet uten suffiks-logikk
|
|
* **Ingen metadata i filnavn:** Episodenummer, tittel, slug og annen metadata lever i PostgreSQL, ikke i filnavnet. Filnavnet er en stabil identifikator som aldri endres.
|
|
|
|
#### Mediefiler matcher Git
|
|
Lydfilen i `/srv/sidelinja/media/{workspace_slug}/` bruker **samme navnekonvensjon** som SRT-filen: `20260315_143022.mp3` matcher `20260315_143022.srt`. Dette kobler mediefil og transkripsjon uten databaseoppslag.
|
|
|
|
#### Reprosessering (redigert lyd)
|
|
Når en lydfil redigeres og transkriberes på nytt, **beholdes det opprinnelige filnavnet**. Rust-worker overskriver SRT-filen i Git — historikken viser endringene via `git log`/`git diff`. Mediefilen i arkivet døpes om til å matche Git-filnavnet dersom den opprinnelig hadde et annet navn.
|
|
|
|
#### Forgejo-bruker
|
|
En dedikert servicebruker **"serverassistent"** opprettes i Forgejo med push-tilgang til transkripsjonsrepoer. Ingen admin-rettigheter.
|
|
|
|
#### Webhook-flyt
|
|
```
|
|
Forgejo push-webhook → SvelteKit POST /api/webhooks/forgejo
|
|
→ INSERT INTO job_queue (type: 'srt_parse', payload: {repo, commit, workspace_id})
|
|
→ Rust-worker plukker opp jobben og parser SRT → avledede formater i PG
|
|
```
|
|
SvelteKit validerer webhook-signatur og legger jobb i køen. Rust-worker forblir en ren kø-consumer uten eget HTTP-endepunkt.
|
|
|
|
#### SRT-editor
|
|
En enkel SRT-editor bygges i SvelteKit (Lag 3, sammen med Podcastfabrikken): segmenter som redigerbare tekstfelt med tidsstempler, "Lagre" committer tilbake til Git via Forgejo API. Forgejo web-UI fungerer som fallback for power users.
|
|
|
|
### 5.3 AI-prompts
|
|
* **Whisper `initial_prompt`:** Navnelister og kontekst lagres per workspace i `settings.whisper_prompt`. Rust-worker bygger prompten fra statisk liste + aktører i workspace-ets kunnskapsgraf.
|
|
* **LLM system-prompts:** OpenRouter-prompts for metadata-uttrekk lagres i `settings.llm_prompts` slik at AI-en kjenner konteksten og vertene for akkurat den podcasten.
|
|
|
|
### 5.4 RSS-feed
|
|
SvelteKit genererer `/feed.xml` dynamisk basert på domenet forespørselen kommer fra (matcher `workspaces.domain`), eller workspace-slug som fallback.
|
|
|
|
### 5.5 Statistikk
|
|
Rust-workeren `stats_parse` knytter nedlastingstall fra Caddy-logger til riktig `workspace_id` basert på filsti i loggen.
|
|
|
|
## 6. Instruks for Claude Code
|
|
* **Lydfiler:** Håndter filopplasting i SvelteKit strømmende (streaming) for filer >100MB for å unngå minne-lekkasjer.
|
|
* **Feilhåndtering:** Hvis OpenRouter timer ut eller Whisper feiler, må oppgaven flagges med status `error` i databasen slik at brukeren kan trigge jobben på nytt manuelt via UI.
|
|
* **Opprydding (Disk):** Når en fil oppdateres vellykket, skal den gamle/foreldede `.mp3`-filen enten slettes fra Hetzner-serveren automatisk, eller flyttes til en `/archive/`-mappe basert på en miljøvariabel.
|
|
* **Transkripsjoner:** Master-kopi alltid i Git. Aldri rediger avledede formater direkte i PG — de regenereres fra Git-kilden.
|
|
* **Workspace:** Alle jobber, mediefiler og metadata opprettes med riktig `workspace_id`. Hent workspace-config (prompts, domene) fra `workspaces.settings`. |