synops/docs/retninger/maskinrommet.md
vegard 0a467066ba Synops v2: arkitektur, retninger og dokumentasjon
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>
2026-03-17 06:43:08 +01:00

322 lines
14 KiB
Markdown

# Maskinrommet — teknisk tjenestelaget
> Én Rust-tjeneste med et fast grensesnitt. Alle tekniske tjenester beveger
> seg gjennom dette laget. Fang, prosesser, lever.
## Observasjoner
I dag er tekniske tjenester spredt:
- **Worker** (Rust) — kjører bakgrunnsjobber
- **Jobbkø** (PG) — koordinerer arbeid
- **AI Gateway** (LiteLLM) — ruter AI-kall
- **Whisper** — transkripsjon
- **LiveKit** — lyd/video-strømmer
Hver har sitt eget grensesnitt. Frontend og primitiv-laget må vite hva
som finnes under panseret. Det er ingen felles abstraksjon, ingen felles
logging, ingen felles kapasitetsstyring.
## Tesen
Alt som krever tunge ressurser eller eksterne tjenester går gjennom
**ett lag** med **ett grensesnitt**. Ikke fordi det er elegant — fordi
det gir et fast punkt som er enkelt å fange, modifisere og forbedre.
Maskinrommet gjør tre ting:
### 1. Fang (input-absorpsjon)
Ta imot råmateriale i alle modaliteter:
- Tekst (melding, URL, dokument)
- Lyd (voice memo, live stream, filopplasting)
- Bilde (foto, skjermbilde, tegning)
- Video (stream, opptak)
- Strukturert data (JSON, metadata, edges)
### 2. Prosesser (transformasjon)
Analyser, transformer, berik og systematiser:
- **STT** — lyd → tekst (Whisper)
- **TTS** — tekst → lyd (ElevenLabs / lokal modell)
- **AI-analyse** — oppsummering, klassifisering, sentimentanalyse,
faktasjekk, edge-forslag
- **Beriking** — URL → metadata, bilde → beskrivelse, lyd → segmenter
- **Søk** — fulltekst, semantisk (pgvector), graftraversering
- **Mediaprosessering** — transcode, thumbnail, waveform
### 3. Lever (output-distribusjon)
Lever resultat i riktig modalitet til riktig mottaker:
- Tekst (melding, notifikasjon, digest)
- Lyd (TTS-opplesning, lydstream)
- Video/bilde (stream, thumbnail, snapshot)
- Strukturert data (noder, edges, metadata tilbake i grafen)
- Push (webhook, SSE, SpacetimeDB-reducer)
## Edge-drevet ressursorkestrering
Nøkkelinnsikten: **maskinrommet leser edges for å vite hva det skal gjøre.**
Noden selv er alltid enkel. Det er edgene som bestemmer hvilke ressurser
som spinnes opp.
### Security by default
Input uten mottaker-edge er automatisk privat. Du trenger ikke "velge
privat" — det er utgangspunktet. Ingen ser det. Ingen ressurser kobles
inn utover det grunnleggende (fang + transkriber). Privat er ikke en
innstilling, det er fravær av deling.
### Ressurser er proporsjonale med edges
Samme nodetype, vilt forskjellig ressursbruk:
```
Dagboknotat (privat voice memo):
node → fang lyd → transkriber (Whisper) → lagre
Ressurser: minimal
Samtale med Trond:
node + mottaker-edge(Trond)
→ fang lyd → transkriber → lever tekst/lyd til Trond
Ressurser: STT + levering til én
Redaksjonsmøte (5 deltakere):
node + mottaker-edges(5) + rolle-edges
→ fang lyd fra alle → transkriber → lever til alle → AI-referent
Ressurser: STT + levering til 5 + LLM
Livesending (1000 lyttere):
node + mottaker-edges(∞) + stream-edge + publiserings-edge
→ fang lyd → transkriber → stream via LiveKit → distribuer
→ generer segmenter → kjør live AI → publiser
Ressurser: STT + LiveKit + LLM + mediaprosessering
```
Maskinrommet gjør ikke mer enn det edges krever. Ingen overhead for
enkle ting. Noden vet ingenting om LiveKit — den har bare edges som
sier "stream til disse mottakerne", og maskinrommet bestemmer at det
betyr LiveKit.
### Naturlig eskalering
Du starter en privat voice-note. Bestemmer deg for å dele den med Trond
→ legg til mottaker-edge, maskinrommet begynner å levere. Trond foreslår
at dere tar det som et møte → legg til flere deltaker-edges, maskinrommet
kobler inn sanntidsstrømming. Møtet blir en innspilling → legg til
publiserings-edge, maskinrommet aktiverer produksjonspipeline.
Hvert steg er bare å legge til edges. Maskinrommet reagerer og kobler
inn flere ressurser etter hvert. Ingen migrering, ingen modebytte.
## Grensesnittet
Maskinrommet eksponerer et konsistent API — sannsynligvis en Rust trait
eller et sett traits:
```
fang(input: RåInput) → NodeId
prosesser(node: NodeId, operasjon: Operasjon) → Resultat
lever(node: NodeId, mottaker: Mottaker, format: Format) → Status
```
Men i praksis er mye av dette *reaktivt*: maskinrommet observerer
edge-endringer og handler automatisk. Legger noen til en mottaker-edge
→ maskinrommet begynner å levere. Legger noen til en stream-edge →
maskinrommet kobler inn LiveKit. Primitivene trenger ikke eksplisitt
kalle `lever()` — de manipulerer edges, og maskinrommet reagerer.
## Hva dette gir
### Isolasjon
Bytt Whisper med noe annet? Endre maskinrommet. Frontend vet ingenting.
Legg til bildegenerering? Ny operasjon i maskinrommet. Primitivene
kaller den uten å vite hva som skjer under.
### Observerbarhet
Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering,
feilhåndtering — alt på ett sted. "Hva bruker vi AI-ressurser på?"
har ett svar.
### Kapasitetsstyring
Prioritering, kø, rate limiting, fallback mellom leverandører — alt
håndtert av maskinrommet. En podcastinnspilling som trenger live
transkripsjon kan prioriteres over en bakgrunns-oppsummering.
### Fast utviklingspunkt
To team (eller to hatter) med klart grensesnitt:
- **Over maskinrommet:** primitiver, noder, edges, UI, brukeropplevelse
- **I maskinrommet:** ytelse, integrasjoner, kapasitet, kostnad
Du kan perfeksjonere det ene uten å røre det andre.
## Content-Addressable Storage og intelligent pruning
Maskinrommet forvalter også lagring. Ikke alt kan lagres for evig — men
ikke alt trenger det heller. Signalene for hva som er viktig finnes
allerede i grafen.
### CAS som lagringsprimitiv
All binærdata (lyd, bilde, video) lagres i et content-addressable store.
Fordeler:
- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi
- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet er
tilgjengelig." Noden peker på en hash, CAS har filen (eller ikke).
- **Enkel opprydning** — slett hashen fra CAS, alle noder som pekte
dit mister binærdataen men beholder metadata og transkripsjon.
### Lagringsregler per modalitet
| Modalitet | Default levetid | Begrunnelse |
|-----------|----------------|-------------|
| Tekst | Evig | Billig, er essensen av innholdet |
| Transkripsjon | Evig | Tekstlig representasjon av lyd/video — tar vare på meningen |
| Lyd | 30 dager | Mellomkostnad, transkripsjon bevarer innholdet |
| Bilde | 30 dager | Mellomkostnad, beskrivelse/metadata bevarer kontekst |
| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste |
### Signaler som forlenger levetid
Default-TTL er bare utgangspunktet. Maskinrommet justerer basert på:
- **Edges.** En lydfil med edge til episoderegisteret = publisert
podcast, beholdes. En privat voice-memo uten edges = 30-dagers TTL.
- **Aksesslog.** Hvis noen har spilt av lydfilen i løpet av TTL-perioden,
forlenges den. Ingen aksess = ingen verdi i å beholde binærdataen.
- **Transkripsjonsstatus.** Lyd som er transkribert har "overlevert sin
essens" til tekst. Lyd som *ikke* er transkribert (f.eks. musikk,
lydeffekter) kan trenge lengre TTL.
- **Edge-type.** Edge til publisert innhold = behold. Edge til arkivert
møte = transkripsjon holder. Edge til ingenting = teksten lever videre,
binærdataen kan dø.
### Eksempler
```
Privat voice-memo, aldri delt:
→ Lyd transkriberes → tekst lagres evig
→ Lydfil: 30 dager, ingen aksess, ingen edges → slettes
→ Noden lever videre med teksten
Podcastepisode:
→ Lyd har edge til episoderegister + publiserings-edge
→ Aksesseres regelmessig via podcastarkivet
→ Lydfil: beholdes så lenge edges og aksess tilsier det
Rutinemøte for et år siden:
→ Video (6 kanaler): ingen har sett den på 6 måneder → slettes
→ Lyd: ingen har spilt av → slettes
→ Transkripsjon: tekst, lagres evig. Søkbar, refererbar.
→ Noden lever med full kontekst minus binærdata
Viktig styremøte:
→ Video aksesseres av styremedlemmer → forlenges
→ Workspace-innstilling: "behold video i 1 år" → overrider default
```
### Generert innhold er en cache
TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan regenereres
fra kildedata er i praksis en cache. Det lagres i CAS med samme TTL-
mekanisme som alt annet:
- Peter ber om lyd-versjon av en tekstmelding → TTS genereres, lagres
- Ingen spiller den av på 30 dager → filen slettes fra CAS
- Peter (eller noen andre) ber om lyd igjen → regenereres on-demand
- Teksten er der alltid. Binærdataen er flyktig.
Maskinrommet trenger ikke skille mellom "original lyd" (voice memo) og
"generert lyd" (TTS) i pruning-logikken. Begge er binærdata i CAS med
en TTL som forlenges ved aksess. Forskjellen er bare at generert
innhold alltid kan gjenskapes fra kilden — så det er tryggere å prune.
### Workspace-styrt aggressivitet
Hvert workspace kan justere sin pruning-profil:
- **Konservativt** — behold alt lenge (f.eks. arkiv-workspace)
- **Aggressivt** — tekst bevares, binærdata prunes raskt (f.eks.
daglig drift-workspace med mye rutineinnhold)
- **Tilpasset** — egne regler per modalitet og edge-type
### Brukerens erfaringsbaserte meny
Over tid bruker du noen edges oftere enn andre, noen noder oftere enn
andre. Maskinrommet observerer dette og tilbyr en erfaringsbasert meny:
dine mest brukte koblinger, dine vanligste input-mønstre, dine
foretrukne modaliteter. Ikke som en rigid konfigurasjon — som en
adaptiv overflate du kan aktivere og deaktivere fortløpende.
Dette er ikke maskinlæring eller kompleks AI — det er frekvenstelling
på edges og aksesslog. Enkelt å implementere, intuitivt for brukeren.
## Pragmatisk vei dit
Ikke bygg dette fra scratch. Formaliser det som allerede finnes:
1. **Worker + jobbkø er allerede kjernen.** De trenger et konsistent
API, ikke en omskriving.
2. **AI Gateway (LiteLLM) absorberes** — i stedet for en separat proxy,
blir LLM-kall en operasjon i maskinrommet som alt annet.
3. **Whisper, TTS, mediaprosessering** — allerede planlagt som
worker-jobber. Gi dem samme grensesnitt.
4. **LiveKit** — den mest spesielle tjenesten (sanntidsstrømmer). Kan
starte som en separat integrasjon og formaliseres inn over tid.
Rekkefølge: definer traits → migrer eksisterende worker-jobber inn →
legg til nye tjenester etter hvert. Fast punkt fra dag én, full
dekning over tid.
## Compute-separasjon
Maskinrommet orkestrerer — men tunge jobber trenger ikke kjøre på
samme maskin. Hetzner CPX42 (8 vCPU, 16 GB RAM) skal håndtere state
(PG, SpacetimeDB) og sanntid (Caddy, LiveKit, SvelteKit). Whisper
(CPU-intensiv, spesielt large-v3) og lokal LLM (kildevern-modus)
vil konkurrere om ressurser under live innspilling.
Maskinrommets abstraksjon gjør dette løsbart:
- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch.
Whisper kjøres med lavere concurrency under live-sesjoner.
- **Senere:** Trekk ut tunge workers til en separat node (billig
ARM/Ampere-instans) som poller jobbkøen over internt nettverk.
Maskinrommet ruter transparent — primitivene merker ingenting.
- **Kildevern-modus:** Lokal LLM (Llama/Gemma) krever GPU eller
dedikert compute. Urealistisk på delt VPS. Egen node for dette.
Poenget: maskinrommet er designet for å rute arbeid, ikke for å
*utføre* alt selv. Compute-separasjon er en konfigurasjon, ikke en
arkitekturendring.
## Spenninger og åpne spørsmål
- **Synkron vs asynkron.** "Fang" og "lever" kan være instant, men
"prosesser" kan ta sekunder (TTS) eller minutter (full episode-
transkripsjon). Grensesnittet må håndtere begge naturlig.
- **Strømmer.** Live lyd/video er fundamentalt annerledes enn
request/response. Men edge-modellen løser mye: maskinrommet ser en
stream-edge og vet at det betyr LiveKit. Utfordringen er *reaktivitet*
— maskinrommet må observere edge-endringer i sanntid og koble inn/ut
ressurser dynamisk.
- **Granularitet.** Hvor mye skal maskinrommet vite om domenet? "Fang
lyd" er generisk, men "transkriber og splitt i segmenter med
taler-identifikasjon" er domenespesifikt. Hvor går grensen?
- **Overhead.** Et ekstra lag betyr et ekstra kall. For tunge
operasjoner (Whisper, LLM) er det neglisjerbart. For lette
operasjoner (slå opp metadata) kan det være unødvendig indirection.
## Plassering i lagmodellen
Maskinrommet (Rust) er det eneste orkestringslaget. Alle tjenester —
inkludert PG, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit — er
likeverdige tjenester *under* maskinrommet. SpacetimeDB er ikke et lag
mellom Rust og GUI, det er en tjeneste maskinrommet skriver til.
Ett unntak: SpacetimeDB har en direkte WebSocket-kobling til frontend
for sanntids lese-strøm. Dette er en bevisst optimering — STDB sitt
klient-SDK gir ~10μs-oppdateringer med automatisk synk og lokal cache.
Å proxy dette gjennom Rust ville vært å bygge en dårligere versjon av
noe STDB gjør optimalt.
Se [datalaget](datalaget.md) for full lagmodell med diagram.
## Forhold til andre retninger
Maskinrommet er infrastrukturen *under* de tre primitivene i
[universell input og mottak](universell_input.md):
- Input-primitiven kaller `fang()` + `prosesser()`
- Mottak-primitiven kaller `lever()`
- Kommunikasjonsnoden bruker alle tre (fang input fra deltakere,
prosesser sanntid, lever til mottakere)
Det er også det som gjør to-lags-modellen fra [rom, ikke forum](rom_ikke_forum.md)
praktisk: maskinrommet ruter til riktig lag (sanntid vs tradisjonelt)
uten at primitivene trenger å vite forskjellen.