Nystart basert på arkitektonisk innsikt fra Sidelinja v1. Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid. Inneholder: - Komplett arkitekturdokumentasjon (docs/arkitektur.md) - 6 vedtatte retninger (docs/retninger/) - Alle concepts, features, proposals og erfaringer fra v1 - Server-oppsett og drift (docs/setup/) - LiteLLM-konfigurasjon (API-nøkler via env) - Editor.svelte referanse fra v1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
Storyboard — Fritt canvas for innspillingsplanlegging og live-produksjon
1. Konsept
Storyboardet er et fritt canvas der meldingsboks-kort plasseres, flyttes og grupperes visuelt. Det brukes før, under og etter innspilling — fra idémyldring til ferdig episodestruktur. Ikke et erstatningsverktøy for kanban (som er for langsiktig planlegging), men et taktilt arbeidsflate for å se en episode ta form.
Storyboardet er en consumer av Canvas-primitivet (docs/features/canvas_primitiv.md) og deltar i universell overføring (docs/features/universell_overfoering.md).
2. Kjernemodell
2.1 Kort = Meldingsboks med storyboard-plassering
Alle kort på storyboardet er vanlige messages-noder. Plasseringen på canvaset styres av message_placements-tabellen:
message_placements:
message_id → messages.id
context_type = 'storyboard'
context_id → episode_id (eller standalone storyboard-id)
entered_at = når kortet ble lagt på boardet
position = { "x": 340, "y": 120, "status": "klar" }
Kortet kan samtidig leve i en chat, på et kanban-brett, eller i en kalender. Endringer i chatten (nye svar, redigeringer) propagerer til storyboard-visningen fordi det er samme melding.
2.2 Episode-objekt
Et storyboard er knyttet til en episode (node av type episode) eller eksisterer som frittstående canvas (for idémyldring uten episode-tilknytning).
Episode-noden eier:
- Sekvensliste: En ordnet liste over kort som er "Tatt opp", med tidsstempel for når de ble markert. Lagres som
episode_sequencei SpacetimeDB. - Målvarighet: Konfigurerbar per workspace (f.eks. 45 min) — brukes av Flow Meter.
2.3 Statuser
Kort har status (lagret i position-JSONB på plasseringen):
| Status | Visuelt | Betydning |
|---|---|---|
| Klar | Solid, hvit/lys border | Planlagt, venter på tur |
| Tatt opp | Grønn border, tidsstempel-badge | Snakket om under innspilling |
| Droppet | Dimmet (opacity 0.4), rød stripe | Ikke brukt denne gang |
| Arkivert | Skjult (filter) | Ferdig behandlet |
Status-endring skjer via:
- Drag til en status-sone (valgfri — konfigurer per workspace)
- Hurtigtast:
R= Tatt opp,D= Droppet (når kort er valgt) - Kontekstmeny
- Flytende toolbar ved seleksjon
2.4 Flere storyboards
Et workspace kan ha mange storyboards — ett per episode, pluss frittstående for idémyldring. Hver er en StoryboardBlock med props.episodeId (eller props.boardId for frittstående).
Tre episoder under planlegging = tre storyboard-blokker, enten:
- På samme side (splittet grid-layout)
- På ulike sider i workspace-navigasjonen
3. Fritt canvas
Storyboardet bruker Canvas-primitivet for all canvas-interaksjon:
- Pan/zoom: Se
canvas_primitiv.md§2 - Objekt-plassering: Kort har
(x, y)i world-space, ingen kolonner eller rader - Snap-to-grid: Av som default, toggle med
G - Ingen akse-begrensning: Brukeren plasserer kort fritt. Ingen antatt retning eller tidslinje
3.1 Kort-rendering
Hvert kort rendres som en <StoryboardCard> inne i canvas-primitivets objekt-slot:
┌─────────────────────────┐
│ 🔵 Tittel │ ← Status-farge som border
│ │
│ Kort sammendrag av body │ ← Avkortet tekst
│ │
│ 💬 3 ⏱ 04:32 │ ← Svar-count + varighet (hvis tatt opp)
└─────────────────────────┘
Kort-størrelse er fast bredde (variabel ved zoom), høyde tilpasser seg innholdet opp til en max.
3.2 Kort-interaksjon
- Klikk: Velg kort, vis flytende toolbar
- Dobbeltklikk: Åpne meldingsboksen i utvidet modus (full diskusjonstråd)
- Drag: Flytt kort på canvaset
- Høyreklikk: Kontekstmeny (status, send til, fjern, slett)
4. Overføring mellom blokker
Storyboardet deltar fullt i universell overføring (universell_overfoering.md):
4.1 Som sender
Dra et kort ut av storyboard-blokken → ghost følger musepekeren → slipp på annen blokk:
- → Chat: Melding ankommer som ny chatmelding med
entered_at = now() - → Kanban: Melding blir kort i første kolonne
- → Kalender: Melding blir hendelse på dagens dato
- → Annet storyboard: Melding får ny canvas-posisjon i mål-boardet
4.2 Som mottaker
Storyboardet kan motta fra alle blokk-typer:
- Default-plassering: Senter av viewport
- Drag-plassering: Der brukeren slipper objektet på canvaset
- Status: Nye kort ankommer som "Klar"
4.3 Inter-storyboard overføring
For å flytte kort mellom episoder (f.eks. "dette passer bedre i episode 48"):
- Dra kortet til en annen storyboard-blokk
- Eller bruk "Send til..." → velg annet storyboard
- Plasseringen i kilde-boardet fjernes, ny plassering opprettes i mål-boardet
5. Kort som oppstår under innspilling
Under live innspilling dukker nye idéer opp. Flere veier inn:
| Metode | Flyt |
|---|---|
Hurtigtast (N) |
Popup for tittel + body → kort plasseres nær senter |
| Fra chat | Skriv melding i studio-chat → "Send til Storyboard" |
| AI-forslag | Live AI foreslår kort basert på transkripsjon (se §8) |
Alle veier oppretter en meldingsboks + plassering med status "Klar".
6. Kobling til LiveKit / Studioet
6.1 Tidsstempel ved "Tatt opp"
Når et kort settes til "Tatt opp" under en aktiv innspilling:
- Klient spør LiveKit om nåværende innspillings-tidspunkt (offset fra oppstart)
- Tidspunktet lagres i plasserings-metadata:
position.recorded_at_offset = 1823(sekunder) - Episode-sekvensen oppdateres med kortet i riktig posisjon
Etter innspilling, når Whisper har prosessert lyden, kan recorded_at_offset matches mot Whisper-segmenter for å koble kort til eksakte transkripsjonsavsnitt.
6.2 Episode-sekvens
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32, // REAL for midpoint-innsetting
pub recorded_at_offset: Option<i64>, // sekunder fra innspillingsstart
pub workspace_id: String,
}
Sekvensen er den ordnede listen over "Tatt opp"-kort og blir grunnlaget for episode-strukturen i Podcastfabrikken.
7. Etter innspilling
7.1 Episode-oppsummering
Automatisk generert visning etter innspilling:
Episode 47 — Oppsummering
━━━━━━━━━━━━━━━━━━━━━━━━
1. [00:00] Intro — Kommunevalg 2027 💬 2 svar
2. [04:32] Listekandidatene i Oslo 💬 5 svar
3. [12:15] Valgomaten — første resultater 💬 1 svar
4. [18:40] Debatten om bompenger 💬 3 svar
━━━━━━━━━━━━━━━━━━━━━━━━
Totalt: 23:10 av 45:00 mål (51%)
Droppet: 3 kort (bevart for neste episode)
7.2 Arkivering
Ett-klikks arkivering:
- Alle "Tatt opp"-kort settes til "Arkivert"
- "Droppet"-kort beholder status (synlige for neste episode som Ghost Cards)
- "Klar"-kort beholder status (ubrukte idéer)
- Episode-sekvensen fryses (immutable etter arkivering)
8. AI-integrasjon (fremtidig)
Under innspilling kan Live AI foreslå nye kort:
- Whisper transkriberer i sanntid
- AI analyserer transkripsjon: "det ble nevnt et nytt tema som ikke er på boardet"
- AI oppretter et foreslått kort (status: "Foreslått", visuelt distinkt — stiplet border)
- Bruker aksepterer → status endres til "Klar"
Dette bygger på Live AI (docs/features/live_ai.md) og er ikke del av MVP.
9. SpacetimeDB-modell
9.1 Tabeller
// Kort-posisjon og status (via universell overføring)
// Bruker message_placement-tabellen — se universell_overfoering.md
// Episode-sekvens (ordnet liste over "Tatt opp"-kort)
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32,
pub recorded_at_offset: Option<i64>,
pub workspace_id: String,
}
9.2 Reducers
#[reducer]
pub fn set_card_status(ctx: &ReducerContext, placement_id: String, status: String) { ... }
#[reducer]
pub fn record_card(ctx: &ReducerContext, episode_id: String, message_id: String, offset: Option<i64>) {
// Sett status til "Tatt opp" + legg til i episode-sekvens
}
#[reducer]
pub fn reorder_sequence(ctx: &ReducerContext, episode_id: String, entries: Vec<(String, f32)>) { ... }
#[reducer]
pub fn archive_episode(ctx: &ReducerContext, episode_id: String) { ... }
10. Responsivt design
| Skjerm | Tilpasning |
|---|---|
| Desktop | Full canvas med zoom/pan, hurtigtaster, drag-and-drop |
| Tablet | Touch-gester, flytende toolbar i bunn, "Send til"-meny |
| Mobil | Listevisning av kort (gruppert etter status), "Send til"-meny, ingen canvas |
Mobil får en alternativ listevisning fordi fritt canvas på liten skjerm er upraktisk. Listen grupperer kort etter status og lar brukeren endre status via swipe.
11. Bygger på
| Feature / Infra | Rolle |
|---|---|
| Canvas-primitiv | Fritt canvas med zoom/pan/drag |
| Meldingsboks | Kort = meldinger med view-config |
| Universell overføring | Portal-mekanikk mellom blokker |
| Kunnskapsgraf | Kort er noder, relasjoner mellom kort |
| SpacetimeDB | Sanntidssynk av posisjon og status |
| Studioet / LiveKit | Tidsstempel ved "Tatt opp" under innspilling |
| Podcastfabrikken | Episode-sekvens → redigeringsgrunnlag |
12. Innsats
Middels–Stor — canvas-primitivet er det tyngste løftet, men gjenbrukes av whiteboard. Selve storyboard-logikken er middels (status, sekvens, arkivering).
13. Wow-faktor
Høy — dette er "limen" mellom redaksjonelt arbeid og faktisk innspilling. Visuelt imponerende og genuint nyttig.
14. Implementeringsfaser
Fase 1: Fundament
- Canvas-primitiv (
<Canvas>) med pan, zoom, drag, viewport culling - BlockShell fullskjerm-toggle
message_placements-tabell (PG + SpacetimeDB)StoryboardBlockregistrert i block registry
Fase 2: Kjerne-storyboard
<StoryboardCard>med status-visning og hurtigtaster- Episode-sekvens med SpacetimeDB-synk
- Universell overføring: "Send til..." kontekstmeny
- Drag-and-drop mellom blokker
Fase 3: Innspillingsintegrasjon
- LiveKit-kobling for tidsstempler ved "Tatt opp"
- Episode-oppsummering og arkivering
- Kort opprettet under innspilling (hurtigtast + fra chat)
Fase 4: Polish og utvidelser
- Ghost Cards (forrige episodes kort)
- Pinboard Mode (zoom-ut til fugleperspektiv)
- Flow Meter (visuell progresjon)
- Mobil listevisning
- AI-foreslåtte kort under innspilling
15. Åpne spørsmål (parkert)
- Skal frittstående storyboards (uten episode) ha en egen node-type, eller er de bare episoder uten publiseringsdato?
- Bør canvaset ha et bakgrunnsmønster (dots/grid) for orientering, eller blank?
- Hvor mange kort tåler canvaset før ytelsen lider? Sannsynlig grense: 200–500 DOM-noder med viewport culling.
16. Instruks for Claude Code
- Storyboard er en blokk-type (
StoryboardBlock.svelte) i block registry - Kort-posisjon eies av SpacetimeDB via
message_placements— aldri direkte PG fra frontend - Episode-sekvens er en egen SpacetimeDB-tabell, ikke metadata på episode-noden
- Bruk Canvas-primitivet for all canvas-logikk — ikke reimplementer pan/zoom
- Mobil-fallback (listevisning) er en egen komponent, ikke en "responsiv" versjon av canvaset
- Status-endring skal alltid gå via SpacetimeDB reducer, aldri direkte state-mutasjon