feat: integrated audio player + animated transcript follower

- Video detail (done videos): added 'Download audio' button; removed the
  manual 'Thumbnail' button (thumbnails auto-download now, so it was redundant).
- New 'GET /api/videos/{id}/audio' endpoint (also serves HEAD for probing),
  serving data/audio/<video_id>.mp3. Audio job outtmpl switched to %(id)s so
  files are addressable per video.
- Integrated <audio> player appears in the detail view once the MP3 is present
  (probed via HEAD on openVideo; polled after a download job until ready).
- Synced transcript follower: as audio plays, the matching segment is
  highlighted (seg-active) and auto-scrolled into view like a karaoke/lyrics
  tracker. Clicking any segment timestamp or chapter seeks the audio to that
  point (falls back to scroll when no audio). Playback indicator pulses while
  playing.
This commit is contained in:
urieljareth
2026-07-27 00:00:28 -06:00
parent 0866c92650
commit 06497299ab
51 changed files with 16013 additions and 17 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "d_yt_channel_scraper_scripts_start_server_ps1", "label": "start-server.ps1", "file_type": "code", "source_file": "scripts/start-server.ps1", "source_location": "L1"}, {"id": "d_yt_channel_scraper_scripts_start_server_open_browser", "label": "Open-Browser()", "file_type": "code", "source_file": "scripts/start-server.ps1", "source_location": "L6"}], "edges": [{"source": "d_yt_channel_scraper_scripts_start_server_ps1", "target": "d_yt_channel_scraper_scripts_start_server_open_browser", "relation": "contains", "confidence": "EXTRACTED", "source_file": "scripts/start-server.ps1", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "d_yt_channel_scraper_scripts_start_server_open_browser", "callee": "Start-Process", "is_member_call": false, "source_file": "scripts/start-server.ps1", "source_location": "L9"}, {"caller_nid": "d_yt_channel_scraper_scripts_start_server_open_browser", "callee": "Write-Host", "is_member_call": false, "source_file": "scripts/start-server.ps1", "source_location": "L18"}, {"caller_nid": "d_yt_channel_scraper_scripts_start_server_open_browser", "callee": "Write-Host", "is_member_call": false, "source_file": "scripts/start-server.ps1", "source_location": "L19"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "d_yt_channel_scraper_src_yt_scraper_ratelimit_py", "label": "ratelimit.py", "file_type": "code", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L1"}, {"id": "d_yt_channel_scraper_src_yt_scraper_ratelimit_polite_sleep", "label": "polite_sleep()", "file_type": "code", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L7", "_callable": true}, {"id": "d_yt_channel_scraper_src_yt_scraper_ratelimit_backoff_sleep", "label": "backoff_sleep()", "file_type": "code", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L12", "_callable": true}], "edges": [{"source": "d_yt_channel_scraper_src_yt_scraper_ratelimit_py", "target": "random", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L3", "weight": 1.0}, {"source": "d_yt_channel_scraper_src_yt_scraper_ratelimit_py", "target": "time", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L4", "weight": 1.0}, {"source": "d_yt_channel_scraper_src_yt_scraper_ratelimit_py", "target": "d_yt_channel_scraper_src_yt_scraper_ratelimit_polite_sleep", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L7", "weight": 1.0}, {"source": "d_yt_channel_scraper_src_yt_scraper_ratelimit_py", "target": "d_yt_channel_scraper_src_yt_scraper_ratelimit_backoff_sleep", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L12", "weight": 1.0}], "raw_calls": [{"caller_nid": "d_yt_channel_scraper_src_yt_scraper_ratelimit_polite_sleep", "callee": "uniform", "is_member_call": true, "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L8", "receiver": "random"}, {"caller_nid": "d_yt_channel_scraper_src_yt_scraper_ratelimit_polite_sleep", "callee": "sleep", "is_member_call": true, "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L9", "receiver": "time"}, {"caller_nid": "d_yt_channel_scraper_src_yt_scraper_ratelimit_backoff_sleep", "callee": "uniform", "is_member_call": true, "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L13", "receiver": "random"}, {"caller_nid": "d_yt_channel_scraper_src_yt_scraper_ratelimit_backoff_sleep", "callee": "sleep", "is_member_call": true, "source_file": "src/yt_scraper/ratelimit.py", "source_location": "L14", "receiver": "time"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "d_yt_channel_scraper_src_yt_scraper_webapp_init_py", "label": "__init__.py", "file_type": "code", "source_file": "src/yt_scraper/webapp/__init__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "d_yt_channel_scraper_src_yt_scraper_init_py", "label": "__init__.py", "file_type": "code", "source_file": "src/yt_scraper/__init__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "d_yt_channel_scraper_scripts_stop_server_ps1", "label": "stop-server.ps1", "file_type": "code", "source_file": "scripts/stop-server.ps1", "source_location": "L1"}], "edges": [], "raw_calls": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "pkg_yt_channel_scraper", "label": "yt-channel-scraper", "file_type": "code", "type": "package", "ecosystem": "python", "source_file": "pyproject.toml", "source_location": "L1", "version": "0.1.0"}], "edges": [{"source": "pkg_yt_channel_scraper", "target": "pkg_yt_dlp", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_click", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_rich", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_jinja2", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_pyyaml", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_python_slugify", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}, {"source": "pkg_yt_channel_scraper", "target": "pkg_requests", "relation": "depends_on", "context": "dependency", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "pyproject.toml", "source_location": "L1", "weight": 1.0}]}