From 96608a3d8f866446af714ed62f0a7f3f0115fcfa Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 05:49:40 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Installed=20wavesurfer.js,=20created=20?= =?UTF-8?q?AudioWaveform=20component=20with=20sha=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/components/AudioWaveform.tsx" - "frontend/src/hooks/useMediaSync.ts" - "frontend/src/pages/WatchPage.tsx" - "frontend/src/App.css" - "frontend/package.json" GSD-Task: S05/T02 --- .gsd/milestones/M021/slices/S05/S05-PLAN.md | 2 +- .../M021/slices/S05/tasks/T01-VERIFY.json | 22 +++++ .../M021/slices/S05/tasks/T02-SUMMARY.md | 90 +++++++++++++++++++ frontend/package-lock.json | 9 +- frontend/package.json | 3 +- frontend/src/App.css | 19 ++++ frontend/src/components/AudioWaveform.tsx | 58 ++++++++++++ frontend/src/hooks/useMediaSync.ts | 4 +- frontend/src/pages/WatchPage.tsx | 18 ++-- 9 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 .gsd/milestones/M021/slices/S05/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M021/slices/S05/tasks/T02-SUMMARY.md create mode 100644 frontend/src/components/AudioWaveform.tsx diff --git a/.gsd/milestones/M021/slices/S05/S05-PLAN.md b/.gsd/milestones/M021/slices/S05/S05-PLAN.md index 463fdd8..e057f88 100644 --- a/.gsd/milestones/M021/slices/S05/S05-PLAN.md +++ b/.gsd/milestones/M021/slices/S05/S05-PLAN.md @@ -37,7 +37,7 @@ Also adds `fetchChapters()` to the frontend API client so downstream tasks can c - Estimate: 45m - Files: backend/routers/videos.py, backend/schemas.py, frontend/src/api/videos.ts - Verify: cd /home/aux/projects/content-to-kb-automator/backend && python -c "from routers.videos import router; print('ok')" && grep -q 'fetchChapters' /home/aux/projects/content-to-kb-automator/frontend/src/api/videos.ts -- [ ] **T02: Audio waveform component with wavesurfer.js + WatchPage integration** — Install wavesurfer.js, create the AudioWaveform component, widen useMediaSync to support HTMLMediaElement, and wire the waveform into WatchPage as a replacement for VideoPlayer when no video URL is available. +- [x] **T02: Installed wavesurfer.js, created AudioWaveform component with shared media element, widened useMediaSync to HTMLMediaElement, and wired conditional audio/video rendering in WatchPage** — Install wavesurfer.js, create the AudioWaveform component, widen useMediaSync to support HTMLMediaElement, and wire the waveform into WatchPage as a replacement for VideoPlayer when no video URL is available. ## Steps diff --git a/.gsd/milestones/M021/slices/S05/tasks/T01-VERIFY.json b/.gsd/milestones/M021/slices/S05/tasks/T01-VERIFY.json new file mode 100644 index 0000000..ffd9f5f --- /dev/null +++ b/.gsd/milestones/M021/slices/S05/tasks/T01-VERIFY.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M021/S05/T01", + "timestamp": 1775281636474, + "passed": true, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd /home/aux/projects/content-to-kb-automator/backend", + "exitCode": 0, + "durationMs": 7, + "verdict": "pass" + }, + { + "command": "grep -q 'fetchChapters' /home/aux/projects/content-to-kb-automator/frontend/src/api/videos.ts", + "exitCode": 0, + "durationMs": 5, + "verdict": "pass" + } + ] +} diff --git a/.gsd/milestones/M021/slices/S05/tasks/T02-SUMMARY.md b/.gsd/milestones/M021/slices/S05/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..6d27035 --- /dev/null +++ b/.gsd/milestones/M021/slices/S05/tasks/T02-SUMMARY.md @@ -0,0 +1,90 @@ +--- +id: T02 +parent: S05 +milestone: M021 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/components/AudioWaveform.tsx", "frontend/src/hooks/useMediaSync.ts", "frontend/src/pages/WatchPage.tsx", "frontend/src/App.css", "frontend/package.json"] +key_decisions: ["wavesurfer.js uses MediaElement backend with shared audio ref so useMediaSync controls playback identically to video mode", "Waveform colors use existing --color-accent (cyan) for theme consistency"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "npx tsc --noEmit exits 0 with no type errors. All grep checks confirm HTMLMediaElement in useMediaSync, wavesurfer in AudioWaveform, AudioWaveform in WatchPage, wavesurfer.js in package.json. All 4 slice-level checks pass (router import, chapters endpoint, stream endpoint, fetchChapters)." +completed_at: 2026-04-04T05:49:32.530Z +blocker_discovered: false +--- + +# T02: Installed wavesurfer.js, created AudioWaveform component with shared media element, widened useMediaSync to HTMLMediaElement, and wired conditional audio/video rendering in WatchPage + +> Installed wavesurfer.js, created AudioWaveform component with shared media element, widened useMediaSync to HTMLMediaElement, and wired conditional audio/video rendering in WatchPage + +## What Happened +--- +id: T02 +parent: S05 +milestone: M021 +key_files: + - frontend/src/components/AudioWaveform.tsx + - frontend/src/hooks/useMediaSync.ts + - frontend/src/pages/WatchPage.tsx + - frontend/src/App.css + - frontend/package.json +key_decisions: + - wavesurfer.js uses MediaElement backend with shared audio ref so useMediaSync controls playback identically to video mode + - Waveform colors use existing --color-accent (cyan) for theme consistency +duration: "" +verification_result: passed +completed_at: 2026-04-04T05:49:32.531Z +blocker_discovered: false +--- + +# T02: Installed wavesurfer.js, created AudioWaveform component with shared media element, widened useMediaSync to HTMLMediaElement, and wired conditional audio/video rendering in WatchPage + +**Installed wavesurfer.js, created AudioWaveform component with shared media element, widened useMediaSync to HTMLMediaElement, and wired conditional audio/video rendering in WatchPage** + +## What Happened + +Installed wavesurfer.js as a frontend dependency. Widened useMediaSync ref type from HTMLVideoElement to HTMLMediaElement for audio/video polymorphism. Created AudioWaveform.tsx that renders a hidden audio element shared with useMediaSync and initializes a WaveSurfer instance using the MediaElement backend. Updated WatchPage to conditionally render AudioWaveform when video_url is null, or VideoPlayer when present. Added dark-themed CSS for the waveform container matching the video player area styling. + +## Verification + +npx tsc --noEmit exits 0 with no type errors. All grep checks confirm HTMLMediaElement in useMediaSync, wavesurfer in AudioWaveform, AudioWaveform in WatchPage, wavesurfer.js in package.json. All 4 slice-level checks pass (router import, chapters endpoint, stream endpoint, fetchChapters). + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 8000ms | +| 2 | `grep -q 'HTMLMediaElement' frontend/src/hooks/useMediaSync.ts` | 0 | ✅ pass | 50ms | +| 3 | `grep -q 'wavesurfer' frontend/src/components/AudioWaveform.tsx` | 0 | ✅ pass | 50ms | +| 4 | `grep -q 'AudioWaveform' frontend/src/pages/WatchPage.tsx` | 0 | ✅ pass | 50ms | +| 5 | `cd backend && python -c "from routers.videos import router; print('ok')"` | 0 | ✅ pass | 1000ms | +| 6 | `grep -q 'def get_video_chapters' backend/routers/videos.py` | 0 | ✅ pass | 50ms | +| 7 | `grep -q 'def stream_video' backend/routers/videos.py` | 0 | ✅ pass | 50ms | +| 8 | `grep -q 'fetchChapters' frontend/src/api/videos.ts` | 0 | ✅ pass | 50ms | + + +## Deviations + +Used project accent color (#22d3ee cyan) instead of plan-suggested #00ffd1 for waveform colors to match existing theme. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/components/AudioWaveform.tsx` +- `frontend/src/hooks/useMediaSync.ts` +- `frontend/src/pages/WatchPage.tsx` +- `frontend/src/App.css` +- `frontend/package.json` + + +## Deviations +Used project accent color (#22d3ee cyan) instead of plan-suggested #00ffd1 for waveform colors to match existing theme. + +## Known Issues +None. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6a0dceb..f0b6d2e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,8 @@ "hls.js": "^1.6.15", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "wavesurfer.js": "^7.12.5" }, "devDependencies": { "@types/react": "^18.3.12", @@ -1884,6 +1885,12 @@ } } }, + "node_modules/wavesurfer.js": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.5.tgz", + "integrity": "sha512-MSZcA13R9ZlxgYpzfakaSYf8dz5tCdZKYbjtN1qnKbCi+UoyfaTuhvjlXHrITi/fgeO3qWfsH7U3BP1AKnwRNg==", + "license": "BSD-3-Clause" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index a08cf73..2d20444 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,8 @@ "hls.js": "^1.6.15", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "wavesurfer.js": "^7.12.5" }, "devDependencies": { "@types/react": "^18.3.12", diff --git a/frontend/src/App.css b/frontend/src/App.css index 2dc1025..64b60e9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -5909,6 +5909,25 @@ a.app-footer__about:hover, /* ── Player Controls ───────────────────────────────────────────────────────── */ +/* ── Audio Waveform (wavesurfer.js) ────────────────────────────────────────── */ + +.audio-waveform { + background: #000; + border-radius: 8px; + padding: 1rem; + min-height: 160px; + display: flex; + flex-direction: column; + justify-content: center; + border: 1px solid var(--color-border); +} + +.audio-waveform__canvas { + width: 100%; +} + +/* ── Player Controls (continued) ───────────────────────────────────────────── */ + .player-controls { display: flex; align-items: center; diff --git a/frontend/src/components/AudioWaveform.tsx b/frontend/src/components/AudioWaveform.tsx new file mode 100644 index 0000000..ad1ea2f --- /dev/null +++ b/frontend/src/components/AudioWaveform.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef } from "react"; +import WaveSurfer from "wavesurfer.js"; +import type { MediaSyncState } from "../hooks/useMediaSync"; + +interface AudioWaveformProps { + mediaSync: MediaSyncState; + src: string; +} + +/** + * Audio-only waveform visualiser powered by wavesurfer.js. + * Renders a hidden