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>
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.
- 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). - 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ørstwhisper_transcribe, deretteropenrouter_analyze(som trigges automatisk ved fullført transkripsjon). - Transkripsjon (faster-whisper): Rust-worker kaller faster-whisper-server (OpenAI-kompatibelt API,
POST /v1/audio/transcriptions) medresponse_format=srtog mottar SRT direkte. Modell:Systran/faster-whisper-mediummedinitial_prompt(navneliste). - 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.
- 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
- AI-Analyse (OpenRouter): Transkripsjonen sendes til OpenRouter (Claude-modell) for uttrekk av forslag til tittel, sammendrag, show notes og kapittler.
- 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).
- Publisering (PostgreSQL): Ved "Godkjenn" lagres metadataene permanent i databasen.
- 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:
- Under innspilling i LiveKit produserer Whisper chunks i sanntid
- 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)
- Lydfilen lagres i CAS med edge til episode-noden
- 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 ienclosure-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/transcriptionsmedresponse_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_promptsom standard.large-v3+ VAD + prompt for best mulig kvalitet der det er verdt ventetiden. - Viktig:
large-v3KREVERvad_filter=true— uten hallusinerer modellen repeterende tekst. - Språk: Sett
language=noeksplisitt 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
errori 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.