synops/docs/concepts/podcastfabrikken.md
vegard 00bf5d27ce Arkitekturbeslutninger: noder er sentrum, edges definerer alt
Grunnleggende arkitekturbeslutninger tatt og dokumentert:

- Alt er noder (brukere, team, innhold, mediefiler, samlings-noder)
- Edges definerer hva en node er (freeform typer, metadata i JSONB)
- Materialisert tilgangsmatrise (node_access) erstatter workspace-RLS
- Visibility (hidden/discoverable/readable/open) på noder
- Aliaser via usynlige system-edges
- Maskinrommet eier all skriving (SpacetimeDB først, PG asynk)
- SpacetimeDB holder hele grafen, PG er persistent backup
- Node- og edge-skjema spesifisert (docs/primitiver/)

Fjernet workspace-konseptet fra hele dokumentasjonen (~40 filer).
Fem retninger besluttet, én åpen (rom, ikke forum).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:29:54 +01:00

11 KiB

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)

2.2 Live-to-Archive (Studio/Møterom → Episode)

Mye av innholdet som ender i podcastfabrikken starter som live-innspilling i Studioet eller Møterommet. Denne flyten unngår manuell opplasting:

  1. Under innspilling i LiveKit produserer Whisper chunks i sanntid
  2. Når innspillingen stoppes → automatisk jobb studio_to_episode:
    • Konsoliderer live-chunks til én SRT-fil
    • Oppretter episode-node med storyboard basert på markører satt under innspilling
    • Trigger AI-analyse (samme pipeline som ved vanlig opplasting)
  3. Lydfilen lagres i CAS med edge til episode-noden
  4. Episoden dukker opp i podcastfabrikken som et utkast — klar for godkjenning, redigering og publisering

Fordel: aldri behov for «last opp MP3 etter innspilling» — flyten er live → arkiv → publisering.

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. Per samlings-node konfigurasjon

Hver samlings-node (f.eks. Sidelinja) har sin egen podcast-konfigurasjon, lagret som JSONB-metadata på noden:

5.1 Mediefiler

Lydfiler lagres i CAS (content-addressable store) med edges til episode-noder. Caddy ruter trafikk basert på domene (fra samlings-nodens metadata) til riktig innhold.

5.2 Transkripsjoner (Git-repostruktur)

Det opprettes ett Forgejo-repo per samlings-node 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 samlings-node, via Forgejo API. Ikke alle samlings-noder 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 CAS 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, node_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 samlings-node i metadata (whisper_prompt). Rust-worker bygger prompten fra statisk liste + aktører knyttet til samlings-noden via edges.
  • LLM system-prompts: OpenRouter-prompts for metadata-uttrekk lagres i metadata (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 samlings-nodens domene-metadata), eller node-slug som fallback.

5.5 Statistikk

Rust-workeren stats_parse knytter nedlastingstall fra Caddy-logger til riktig samlings-node basert på domene 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.
  • Tilhørighet: Alle jobber, mediefiler og metadata knyttes til riktig samlings-node via edges. Hent config (prompts, domene) fra samlings-nodens JSONB-metadata.