Ikke-destruktiv redigering via EDL (Edit Decision List): - Backend: audio.rs med FFmpeg-subprocess for klipp, normalisering, silence trim, fades, noise reduction, EQ, kompressor - Frontend: /studio/[id] med wavesurfer.js RegionsPlugin, verktøypanel, sesjonslagring, og render-dialog - Studio-trait for samlinger, versjonshistorikk via derived_from-edges - API: audio_analyze (synkron), audio_process (jobbkø), audio_info Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
4.4 KiB
Markdown
130 lines
4.4 KiB
Markdown
# Lydstudio — Lydredigering i nettleseren
|
|
|
|
**Status:** Under utvikling (v1)
|
|
|
|
## Konsept
|
|
|
|
Lydstudioet er en visning (`/studio/[id]`) av en medienode som gir
|
|
brukeren verktøy for enkel lydredigering direkte i nettleseren.
|
|
Tenk "Audacity-light" integrert i Synops-plattformen.
|
|
|
|
**Prinsipp:** Ikke-destruktiv redigering. Originalen i CAS røres aldri.
|
|
Operasjoner lagres som en EDL (Edit Decision List), og rendres til ny
|
|
fil via maskinrommet + ffmpeg.
|
|
|
|
## Arkitektur
|
|
|
|
### Node/edge-modell
|
|
```
|
|
Original medienode (media, cas_hash: "abc...")
|
|
←derived_from── Prosessert medienode (media, cas_hash: "def...", metadata.edl)
|
|
←has_studio──── Studio-sesjon (content, metadata.edl = {...})
|
|
```
|
|
|
|
- **Studioet** er en *visning*, ikke en ny node_kind
|
|
- **Studio-sesjon** er en content-node som lagrer EDL-en (gjenopptagbart)
|
|
- **Prosessert fil** er en ny medienode med `derived_from`-edge
|
|
|
|
### EDL-format (Edit Decision List)
|
|
|
|
```json
|
|
{
|
|
"source_hash": "abc123...",
|
|
"operations": [
|
|
{ "type": "cut", "start_ms": 15200, "end_ms": 17800 },
|
|
{ "type": "normalize", "target_lufs": -16.0 },
|
|
{ "type": "trim_silence", "threshold_db": -30.0, "min_duration_ms": 500 },
|
|
{ "type": "fade_in", "duration_ms": 1000 },
|
|
{ "type": "fade_out", "duration_ms": 2000 },
|
|
{ "type": "noise_reduction", "strength_db": -25.0 },
|
|
{ "type": "equalizer", "low_gain": 2.0, "mid_gain": 0.0, "high_gain": -1.0 },
|
|
{ "type": "compressor", "threshold_db": -20.0, "ratio": 4.0 }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Prosesseringsflyt
|
|
```
|
|
Frontend (EDL)
|
|
→ POST /intentions/audio_process
|
|
→ Jobbkø (audio_process, prioritet 5)
|
|
→ maskinrommet: edl → ffmpeg filtergraf → subprocess
|
|
→ Resultat lagres i CAS → ny medienode + derived_from edge
|
|
```
|
|
|
|
## Operasjoner
|
|
|
|
| Operasjon | FFmpeg-filter | Beskrivelse |
|
|
|-----------|---------------|-------------|
|
|
| Klipp (cut) | `aselect` + `asetpts` | Fjern region (nysing, telefon, etc.) |
|
|
| Normaliser | `loudnorm` (to-pass) | EBU R128 loudness-normalisering, typisk -16 LUFS |
|
|
| Trim stillhet | `silencedetect` → cuts | Forkort/fjern stille regioner |
|
|
| Fade in | `afade=t=in` | Gradvis inngang |
|
|
| Fade out | `afade=t=out` | Gradvis utgang |
|
|
| Noise reduction | `afftdn` | FFT-basert støyreduksjon |
|
|
| EQ | `equalizer` | Tre-bånds parametrisk (lav/mid/høy) |
|
|
| Kompressor | `acompressor` | Dynamisk kompresjon ("radio-lyd") |
|
|
|
|
### Operasjonsrekkefølge (ved render)
|
|
1. Cuts (aselect) — fjerner regioner
|
|
2. Noise reduction (afftdn)
|
|
3. EQ (equalizer)
|
|
4. Compressor (acompressor)
|
|
5. Normalize (loudnorm) — alltid nest sist
|
|
6. Fades (afade) — helt sist
|
|
|
|
## API-endepunkter
|
|
|
|
### `POST /intentions/audio_analyze`
|
|
Synkron analyse av lydfil: loudness, silence-regioner, metadata.
|
|
|
|
```json
|
|
// Request
|
|
{ "cas_hash": "abc...", "silence_threshold_db": -30.0, "silence_min_duration_ms": 500 }
|
|
|
|
// Response
|
|
{
|
|
"loudness": { "input_i": -23.1, "input_tp": -5.2, "input_lra": 14.0, "input_thresh": -34.0 },
|
|
"silence_regions": [{ "start_ms": 1200, "end_ms": 2800, "duration_ms": 1600 }],
|
|
"info": { "duration_ms": 180000, "sample_rate": 44100, "channels": 2, "codec": "mp3", "format": "mp3" }
|
|
}
|
|
```
|
|
|
|
### `POST /intentions/audio_process`
|
|
Køer render-jobb med EDL. Returnerer job_id for polling.
|
|
|
|
```json
|
|
// Request
|
|
{ "media_node_id": "uuid", "edl": { "source_hash": "...", "operations": [...] }, "output_format": "mp3" }
|
|
|
|
// Response
|
|
{ "job_id": "uuid" }
|
|
```
|
|
|
|
### `GET /query/audio_info?hash=...`
|
|
Hurtig metadata om lydfil (ffprobe).
|
|
|
|
## Frontend
|
|
|
|
- **Rute:** `/studio/[id]` — waveform-visning av medienode
|
|
- **Waveform:** wavesurfer.js med RegionsPlugin for visuell region-markering
|
|
- **Verktøypanel:** Alle operasjoner tilgjengelig som knapper/slidere
|
|
- **Tastatur:** Space (play/pause), Delete (klipp), Ctrl+Z (angre)
|
|
- **Transkripsjon:** Segmenter synkronisert med waveform (klikk → seek)
|
|
- **Render:** Dialog med format-valg, deretter jobb-polling
|
|
|
|
## Avhengigheter
|
|
|
|
- **ffmpeg 6.1.1** — installert native på serveren
|
|
- **wavesurfer.js** — allerede i bruk (AudioPlayer.svelte)
|
|
- **Trait:** `studio` — aktiverer "Rediger i studioet"-knapp på medienoder
|
|
|
|
## Filer
|
|
|
|
| Fil | Rolle |
|
|
|-----|-------|
|
|
| `maskinrommet/src/audio.rs` | EDL-parser, ffmpeg-kommandoer, jobbhåndterer |
|
|
| `maskinrommet/src/jobs.rs` | `audio_process` dispatch |
|
|
| `maskinrommet/src/intentions.rs` | API-endepunkter for analyze/process/info |
|
|
| `frontend/src/routes/studio/[id]/+page.svelte` | Hovedside |
|
|
| `frontend/src/lib/components/studio/` | Waveform, panel, render-dialog |
|