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}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "src_yt_scraper_webapp_static_index", "label": "SPA shell (index.html)", "file_type": "document", "source_file": "src/yt_scraper/webapp/static/index.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [{"source": "src_yt_scraper_webapp_static_index", "target": "docs_superpowers_specs_2026_07_26_platform_design_dark_command_center", "relation": "implements", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "src/yt_scraper/webapp/static/index.html", "source_location": null, "weight": 1.0}, {"source": "src_yt_scraper_webapp_static_index", "target": "docs_superpowers_specs_2026_07_26_platform_design_cookie_vault_ux", "relation": "implements", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "src/yt_scraper/webapp/static/index.html", "source_location": "Cookies view dropzone", "weight": 1.0}], "hyperedges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "opencode_agent_webapp_builder", "label": "webapp-builder agent definition", "file_type": "document", "source_file": ".opencode/agent/webapp-builder.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opencode_agent_webapp_builder_store_data_layer", "label": "Store API surface (data layer the webapp reuses)", "file_type": "concept", "source_file": ".opencode/agent/webapp-builder.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null, "rationale": "Agent must REUSE not duplicate: Store(db_path), cookies.py, segments.py, analysis.py, export.py, pipeline.process_video, config.Config. Active cookie (cookies.resolve_active_path) MUST be passed as cookies_file to every scrape job."}], "edges": [{"source": "opencode_agent_webapp_builder", "target": "docs_superpowers_specs_2026_07_26_platform_design", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".opencode/agent/webapp-builder.md", "source_location": "last line", "weight": 1.0}, {"source": "opencode_agent_webapp_builder", "target": "docs_superpowers_plans_2026_07_26_platform_build", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".opencode/agent/webapp-builder.md", "source_location": "last line", "weight": 1.0}, {"source": "opencode_agent_webapp_builder", "target": "opencode_agent_webapp_builder_store_data_layer", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".opencode/agent/webapp-builder.md", "source_location": "What already exists section", "weight": 1.0}, {"source": "opencode_agent_webapp_builder", "target": "src_yt_scraper_webapp_static_index", "relation": "references", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": ".opencode/agent/webapp-builder.md", "source_location": null, "weight": 1.0}], "hyperedges": [{"id": "webapp_stack_b", "label": "Workstream B webapp stack (FastAPI + Alpine SPA + subagent)", "nodes": ["opencode_agent_webapp_builder", "opencode_goals_webapp_build", "src_yt_scraper_webapp_static_index", "docs_superpowers_specs_2026_07_26_platform_design_dark_command_center", "docs_superpowers_specs_2026_07_26_platform_design_sse_job_runner"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.95, "source_file": ".opencode/agent/webapp-builder.md"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "opencode_goals_webapp_build", "label": "Sub-goal: Build the local webapp (Workstream B)", "file_type": "document", "source_file": ".opencode/goals/webapp-build.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [{"source": "opencode_goals_webapp_build", "target": "opencode_agent_webapp_builder", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".opencode/goals/webapp-build.md", "source_location": "Owner agent line", "weight": 1.0}, {"source": "opencode_goals_webapp_build", "target": "docs_superpowers_specs_2026_07_26_platform_design", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".opencode/goals/webapp-build.md", "source_location": "Spec line", "weight": 1.0}], "hyperedges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "docs_superpowers_plans_2026_07_26_platform_build", "label": "Platform Implementation Plan", "file_type": "document", "source_file": "docs/superpowers/plans/2026-07-26-platform-build.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [{"source": "docs_superpowers_plans_2026_07_26_platform_build", "target": "docs_superpowers_specs_2026_07_26_platform_design", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "docs/superpowers/plans/2026-07-26-platform-build.md", "source_location": "Spec line", "weight": 1.0}], "hyperedges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "readme", "label": "yt-channel-scraper README", "file_type": "document", "source_file": "README.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "readme_yt_dlp_innertube", "label": "yt-dlp InnerTube mechanism (ANDROID/IOS/WEB clients)", "file_type": "concept", "source_file": "README.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [{"source": "readme", "target": "config_example", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "Configuraci\u00f3n section", "weight": 1.0}, {"source": "readme", "target": "readme_yt_dlp_innertube", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "header", "weight": 1.0}], "hyperedges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "opportunities", "label": "OPPORTUNITIES.md - extension audit", "file_type": "document", "source_file": "OPPORTUNITIES.md", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_search_fts5", "label": "Feature proposal: search (FTS5 transcript search)", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 1 #1", "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_llm_summaries", "label": "Feature proposal: LLM summaries per video", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 3 #12", "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_rag_qa", "label": "Feature proposal: Q&A / RAG over channel", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 3 #13", "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_export_formats", "label": "Feature proposal: multi-format export (json/csv/srt/html)", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 1 #3", "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_analysis_wordcloud", "label": "Feature proposal: content analysis (wordcloud/top-words/timeline)", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 2 #9", "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "opportunities_watch_mode", "label": "Feature proposal: watch mode (periodic discovery)", "file_type": "concept", "source_file": "OPPORTUNITIES.md", "source_location": "Tier 2 #8", "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [{"source": "opportunities_search_fts5", "target": "docs_superpowers_specs_2026_07_26_platform_design_transcript_fts5", "relation": "semantically_similar_to", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "OPPORTUNITIES.md", "source_location": null, "weight": 1.0}, {"source": "opportunities_export_formats", "target": "opencode_agent_webapp_builder_store_data_layer", "relation": "semantically_similar_to", "confidence": "INFERRED", "confidence_score": 0.65, "source_file": "OPPORTUNITIES.md", "source_location": null, "weight": 1.0}, {"source": "opportunities_watch_mode", "target": "docs_superpowers_specs_2026_07_26_platform_design_sse_job_runner", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.65, "source_file": "OPPORTUNITIES.md", "source_location": null, "weight": 1.0}, {"source": "opportunities_analysis_wordcloud", "target": "opencode_agent_webapp_builder_store_data_layer", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.65, "source_file": "OPPORTUNITIES.md", "source_location": null, "weight": 1.0}], "hyperedges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "config_example", "label": "config.example.yaml", "file_type": "document", "source_file": "config.example.yaml", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}], "edges": [], "hyperedges": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long