Store waveform peaks inline in audio_versions (JSONB columns) so WaveSurfer
can render the waveform immediately on page load without waiting for audio
decode. Adds a 100-point mini-waveform for version selector thumbnails.
Backend:
- Migration 0006: adds waveform_peaks and waveform_peaks_mini JSONB columns
- Worker generates both resolutions (500-pt full, 100-pt mini) during transcode
and stores them directly in DB — replaces file-based waveform_url approach
- AudioVersionRead schema exposes both fields inline (no extra HTTP round-trip)
- GET /versions/{id}/waveform reads from DB; adds ?resolution=mini support
Frontend:
- audioService.initialize() accepts peaks and calls ws.load(url, Float32Array)
so waveform renders instantly without audio decode
- useWaveform hook threads peaks option through to audioService
- PlayerPanel passes waveform_peaks from the active version to the hook
- New MiniWaveform SVG component (no WaveSurfer) renders mini peaks in the
version selector buttons
Fix: docker-compose.dev.yml now runs alembic upgrade head before starting
the API server, so a fresh volume gets the full schema automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.7 KiB
5.7 KiB
Plan: Waveform Pre-computation
Branch: feature/waveform-precompute
Goal: Store waveform peaks in the database during indexing so WaveSurfer renders
the waveform instantly (no waiting for audio decode), and show a mini-waveform in
the library/overview song list.
Background
WaveSurfer v7 supports ws.load(url, channelData) — when pre-computed peaks are
passed as a Float32Array[], the waveform renders immediately and audio streams in
the background. Currently the frontend calls ws.load(url) which blocks until the
full audio is decoded.
The worker already generates a 500-point peaks JSON file (waveform_url), but:
- It is stored as a file on disk, not inline in the DB
- The frontend never reads it (the
peaksUrloption inuseWaveformis wired to nothing)
Architecture Decision
Add two JSONB columns to audio_versions:
waveform_peaks— 500 points, returned inline with version data, passed to WaveSurferwaveform_peaks_mini— 100 points, returned inline, used for SVG mini-waveform in library/song list
This eliminates a separate HTTP round-trip and lets the UI render the waveform the moment the page loads.
Checklist
Backend
B1 — DB: Peaks columns + Alembic migration
- Write migration test: after upgrade,
audio_versionstable haswaveform_peaksandwaveform_peaks_miniJSONB columns - Create
api/alembic/versions/0006_waveform_peaks_in_db.py - Add
waveform_peaksandwaveform_peaks_miniJSONB columns toAudioVersionmodel inapi/src/rehearsalhub/db/models.py
B2 — Worker: Generate and store both peak resolutions
- Write unit tests for
extract_peaks()inworker/tests/test_waveform.py:- Returns exactly
num_pointsvalues - All values in [0.0, 1.0]
- Empty audio returns list of zeros (no crash)
- 100-point and 500-point both work
- Returns exactly
- Update
handle_transcodeinworker/src/worker/main.py:- Generate
peaks_500 = extract_peaks(audio, 500) - Generate
peaks_100 = extract_peaks(audio, 100) - Store both on
AudioVersionDB row
- Generate
- Write integration test: after
handle_transcode, row has non-nullwaveform_peaks(len 500) andwaveform_peaks_mini(len 100)
B3 — API Schema: Expose peaks in AudioVersionRead
- Write serialization test:
AudioVersionRead.model_validate(orm_obj)includeswaveform_peaks: list[float] | Noneandwaveform_peaks_mini: list[float] | None - Update
api/src/rehearsalhub/schemas/audio_version.py— add both fields
B4 — API Router: /waveform endpoint reads from DB
- Write endpoint tests:
GET /versions/{id}/waveformreturns{"data": [...500 floats...]}from DBGET /versions/{id}/waveform?resolution=minireturns 100-point peaks- 404 when version has no peaks yet
- Update
api/src/rehearsalhub/routers/versions.py— read fromversion.waveform_peaks/version.waveform_peaks_miniinstead of file I/O
B5 — API: Peaks inline on versions list (verify, no change expected)
- Write test:
GET /songs/{id}/versionsresponse includeswaveform_peaksandwaveform_peaks_minion each version object - Confirm no router change needed (schema update in B3 is sufficient)
Frontend
F1 — Types: Update AudioVersionRead TS type
- Add
waveform_peaks: number[] | nullandwaveform_peaks_mini: number[] | nullto the TypeScript version type (wherever API types live)
F2 — audioService: Accept and use pre-computed peaks
- Write unit tests for
AudioService.initialize():- With peaks: calls
ws.load(url, [Float32Array])→ waveform renders immediately - Without peaks: calls
ws.load(url)→ falls back to audio decode - Same URL + same peaks → no re-initialization
- With peaks: calls
- Update
AudioService.initialize(container, url, peaks?: number[])inweb/src/services/audioService.ts:- Call
ws.load(url, peaks ? [new Float32Array(peaks)] : undefined)
- Call
F3 — useWaveform hook: Thread peaks through
- Write hook test: when
peaksoption is provided, it is forwarded toaudioService.initialize - Add
peaks?: number[] | nulltoUseWaveformOptionsinweb/src/hooks/useWaveform.ts - Forward
options.peakstoaudioService.initialize()in the effect
F4 — PlayerPanel: Pass peaks to hook
- Write component test:
PlayerPanelpassesversion.waveform_peakstouseWaveform - Update
web/src/components/PlayerPanel.tsxto extract and forwardwaveform_peaks
F5 — MiniWaveform: New SVG component for library overview
- Write unit tests:
- Renders SVG with correct number of bars matching peaks length
- Null/empty peaks renders a grey placeholder (no crash)
- Accepts
peaks,width,height,colorprops
- Create
web/src/components/MiniWaveform.tsx— pure SVG, no WaveSurfer - Integrate into song list / library view using
waveform_peaks_mini
Testing Strategy
| Layer | Tool |
|---|---|
| Backend unit | pytest, synthetic numpy arrays |
| Backend integration | Real Postgres via docker-compose test profile |
| Frontend unit | Vitest + Testing Library |
| E2E | Playwright — assert waveform visible before audio canplay fires |
Implementation Order
- B1 — migration + model
- B2 — worker (TDD: unit tests → implementation → integration test)
- B3 — schema
- B4 — router
- B5 — verify versions list
- F1 — TS types
- F2 — audioService
- F3 — useWaveform
- F4 — PlayerPanel
- F5 — MiniWaveform