opencode-skills-collection 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +84 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +8 -7
- package/bundled-skills/android-cli/SKILL.md +19 -7
- package/bundled-skills/android-ui-journey-testing/SKILL.md +191 -0
- package/bundled-skills/apple-notes-search/SKILL.md +12 -2
- package/bundled-skills/ask-matt/SKILL.md +92 -0
- package/bundled-skills/atlas-ledger/SKILL.md +8 -0
- package/bundled-skills/bugs-are-annoying/SKILL.md +137 -0
- package/bundled-skills/codebase-design/DEEPENING.md +37 -0
- package/bundled-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/bundled-skills/codebase-design/SKILL.md +145 -0
- package/bundled-skills/codex-fable5/SKILL.md +10 -2
- package/bundled-skills/competitor-analysis/LICENSE.txt +21 -0
- package/bundled-skills/competitor-analysis/SKILL.md +434 -0
- package/bundled-skills/competitor-analysis/references/battle-card-subagent.md +127 -0
- package/bundled-skills/competitor-analysis/references/battle-card.md +91 -0
- package/bundled-skills/competitor-analysis/references/example-research.md +130 -0
- package/bundled-skills/competitor-analysis/references/report-template.html +127 -0
- package/bundled-skills/competitor-analysis/references/research-patterns.md +217 -0
- package/bundled-skills/competitor-analysis/references/workflow.md +434 -0
- package/bundled-skills/competitor-analysis/scripts/capture_screenshots.mjs +142 -0
- package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +929 -0
- package/bundled-skills/competitor-analysis/scripts/extract_vs_names.mjs +140 -0
- package/bundled-skills/competitor-analysis/scripts/gate_candidates.mjs +224 -0
- package/bundled-skills/competitor-analysis/scripts/list_urls.mjs +90 -0
- package/bundled-skills/competitor-analysis/scripts/md_utils.mjs +50 -0
- package/bundled-skills/competitor-analysis/scripts/merge_partials.mjs +291 -0
- package/bundled-skills/competitor-analysis/scripts/package.json +6 -0
- package/bundled-skills/design-it/3d-ui/SKILL.md +259 -0
- package/bundled-skills/design-it/SKILL.md +170 -0
- package/bundled-skills/design-it/ai-native-ui/SKILL.md +295 -0
- package/bundled-skills/design-it/aurora-ui/SKILL.md +307 -0
- package/bundled-skills/design-it/bento-ui/SKILL.md +314 -0
- package/bundled-skills/design-it/brutalism/SKILL.md +270 -0
- package/bundled-skills/design-it/brutalist-typography/SKILL.md +287 -0
- package/bundled-skills/design-it/card-based-design/SKILL.md +262 -0
- package/bundled-skills/design-it/claymorphism/SKILL.md +287 -0
- package/bundled-skills/design-it/color-blocking/SKILL.md +278 -0
- package/bundled-skills/design-it/command-center-ui/SKILL.md +345 -0
- package/bundled-skills/design-it/cyber-y2k/SKILL.md +312 -0
- package/bundled-skills/design-it/cyberpunk-ui/SKILL.md +262 -0
- package/bundled-skills/design-it/dark-mode/SKILL.md +289 -0
- package/bundled-skills/design-it/dashboard-design/SKILL.md +331 -0
- package/bundled-skills/design-it/data-dense-design/SKILL.md +322 -0
- package/bundled-skills/design-it/duotone-design/SKILL.md +248 -0
- package/bundled-skills/design-it/editorial-design/SKILL.md +328 -0
- package/bundled-skills/design-it/flat-design/SKILL.md +221 -0
- package/bundled-skills/design-it/flat-design-2/SKILL.md +240 -0
- package/bundled-skills/design-it/floating-ui/SKILL.md +299 -0
- package/bundled-skills/design-it/frutiger-aero/SKILL.md +274 -0
- package/bundled-skills/design-it/glassmorphism/SKILL.md +272 -0
- package/bundled-skills/design-it/gradient-design/SKILL.md +309 -0
- package/bundled-skills/design-it/high-contrast/SKILL.md +288 -0
- package/bundled-skills/design-it/holographic-ui/SKILL.md +310 -0
- package/bundled-skills/design-it/isometric-design/SKILL.md +228 -0
- package/bundled-skills/design-it/layered-design/SKILL.md +247 -0
- package/bundled-skills/design-it/material-design/SKILL.md +275 -0
- package/bundled-skills/design-it/maximalism/SKILL.md +297 -0
- package/bundled-skills/design-it/minimalism/SKILL.md +267 -0
- package/bundled-skills/design-it/monochromatic-ui/SKILL.md +296 -0
- package/bundled-skills/design-it/neo-brutalism/SKILL.md +270 -0
- package/bundled-skills/design-it/neumorphism/SKILL.md +248 -0
- package/bundled-skills/design-it/retro-design/SKILL.md +283 -0
- package/bundled-skills/design-it/retro-futurism/SKILL.md +259 -0
- package/bundled-skills/design-it/sci-fi-interface/SKILL.md +309 -0
- package/bundled-skills/design-it/skeuomorphism/SKILL.md +280 -0
- package/bundled-skills/design-it/soft-pastel/SKILL.md +307 -0
- package/bundled-skills/design-it/spatial-computing-ui/SKILL.md +300 -0
- package/bundled-skills/design-it/spatial-design/SKILL.md +268 -0
- package/bundled-skills/design-it/swiss-design/SKILL.md +293 -0
- package/bundled-skills/design-it/synthwave/SKILL.md +257 -0
- package/bundled-skills/design-it/tile-design/SKILL.md +297 -0
- package/bundled-skills/design-it/typography-first/SKILL.md +247 -0
- package/bundled-skills/design-it/vaporwave/SKILL.md +331 -0
- package/bundled-skills/design-it/vibrant-maximalism/SKILL.md +291 -0
- package/bundled-skills/design-it/widget-based-design/SKILL.md +274 -0
- package/bundled-skills/design-it/y2k-design/SKILL.md +268 -0
- package/bundled-skills/diagnosing-bugs/SKILL.md +165 -0
- package/bundled-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/bundled-skills/docs/contributors/skill-scoring.md +235 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +145 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/specialized-plugin-roadmap.md +11 -4
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/bundled-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/bundled-skills/domain-modeling/SKILL.md +105 -0
- package/bundled-skills/dos-verify-done-claims/SKILL.md +16 -4
- package/bundled-skills/ecl-harness-engineer/agents/creator-config.md +1 -1
- package/bundled-skills/ecl-harness-engineer/references/environment-config-guide.md +2 -2
- package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +4 -4
- package/bundled-skills/event-staffing-ordering/SKILL.md +4 -0
- package/bundled-skills/grill-me/SKILL.md +36 -0
- package/bundled-skills/grill-with-docs/SKILL.md +36 -0
- package/bundled-skills/grilling/SKILL.md +39 -0
- package/bundled-skills/handoff/SKILL.md +45 -0
- package/bundled-skills/image-generator/.env.example +7 -0
- package/bundled-skills/image-generator/SKILL.md +509 -0
- package/bundled-skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/bundled-skills/improve-codebase-architecture/SKILL.md +97 -0
- package/bundled-skills/learn/SKILL.md +156 -0
- package/bundled-skills/lesson-generator/SKILL.md +90 -0
- package/bundled-skills/llm-council/.env.example +7 -0
- package/bundled-skills/llm-council/SKILL.md +602 -0
- package/bundled-skills/loop-library/SKILL.md +208 -0
- package/bundled-skills/loop-library/agents/openai.yaml +4 -0
- package/bundled-skills/loop-library/references/catalog.md +270 -0
- package/bundled-skills/lovable-cleanup/SKILL.md +9 -7
- package/bundled-skills/macos-screen-recorder/SKILL.md +9 -1
- package/bundled-skills/mailtrap-managing-contacts/SKILL.md +112 -0
- package/bundled-skills/mailtrap-sending-emails/SKILL.md +167 -0
- package/bundled-skills/mailtrap-setting-up-sending-domain/SKILL.md +77 -0
- package/bundled-skills/mailtrap-testing-with-sandbox/SKILL.md +110 -0
- package/bundled-skills/prototype/LOGIC.md +79 -0
- package/bundled-skills/prototype/SKILL.md +62 -0
- package/bundled-skills/prototype/UI.md +112 -0
- package/bundled-skills/screenstudio-alt/SKILL.md +9 -1
- package/bundled-skills/setup-matt-pocock-skills/SKILL.md +158 -0
- package/bundled-skills/setup-matt-pocock-skills/domain.md +51 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-github.md +34 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +35 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
- package/bundled-skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/bundled-skills/survey-generator/LICENSE +21 -0
- package/bundled-skills/survey-generator/SKILL.md +143 -0
- package/bundled-skills/survey-generator/build_artifact.py +208 -0
- package/bundled-skills/survey-generator/examples/agentic-engineering/research_bundle.json +1196 -0
- package/bundled-skills/survey-generator/examples/agentic-engineering/survey.html +706 -0
- package/bundled-skills/survey-generator/style_spec.json +85 -0
- package/bundled-skills/survey-generator/templates/research_bundle_template.json +69 -0
- package/bundled-skills/tdd/SKILL.md +139 -0
- package/bundled-skills/tdd/mocking.md +59 -0
- package/bundled-skills/tdd/refactoring.md +10 -0
- package/bundled-skills/tdd/tests.md +61 -0
- package/bundled-skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/bundled-skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/bundled-skills/teach/MISSION-FORMAT.md +31 -0
- package/bundled-skills/teach/RESOURCES-FORMAT.md +32 -0
- package/bundled-skills/teach/SKILL.md +169 -0
- package/bundled-skills/to-issues/SKILL.md +115 -0
- package/bundled-skills/to-prd/SKILL.md +104 -0
- package/bundled-skills/tools-page-seo-optimizer/SKILL.md +616 -0
- package/bundled-skills/triage/AGENT-BRIEF.md +207 -0
- package/bundled-skills/triage/OUT-OF-SCOPE.md +105 -0
- package/bundled-skills/triage/SKILL.md +143 -0
- package/bundled-skills/vibecode-production-qa-validator/SKILL.md +371 -141
- package/bundled-skills/wiki-builder/SKILL.md +157 -0
- package/bundled-skills/wiki-builder/agents/openai.yaml +5 -0
- package/bundled-skills/wiki-builder/references/wiki-flavors.md +98 -0
- package/bundled-skills/wiki-builder/scripts/init_wiki.sh +105 -0
- package/bundled-skills/wiki-builder/templates/index.md +20 -0
- package/bundled-skills/wiki-builder/templates/maintenance-log.md +7 -0
- package/bundled-skills/wiki-builder/templates/prompts/compile-concept-page.md +12 -0
- package/bundled-skills/wiki-builder/templates/prompts/compile-index.md +11 -0
- package/bundled-skills/wiki-builder/templates/prompts/compile-source-page.md +12 -0
- package/bundled-skills/wiki-builder/templates/prompts/lint-wiki.md +10 -0
- package/bundled-skills/wiki-builder/templates/prompts/query-and-file.md +11 -0
- package/bundled-skills/wiki-builder/templates/sources.md +9 -0
- package/bundled-skills/wiki-builder/templates/wiki.config.md +53 -0
- package/bundled-skills/writing-great-skills/GLOSSARY.md +181 -0
- package/bundled-skills/writing-great-skills/SKILL.md +111 -0
- package/bundled-skills/yao-meta-skill/SKILL.md +86 -0
- package/bundled-skills/yao-meta-skill/agents/interface.yaml +26 -0
- package/bundled-skills/yao-meta-skill/manifest.json +24 -0
- package/bundled-skills/yao-meta-skill/references/artifact-design-doctrine.md +49 -0
- package/bundled-skills/yao-meta-skill/references/authoring-discipline.md +78 -0
- package/bundled-skills/yao-meta-skill/references/autonomous-adaptation.md +65 -0
- package/bundled-skills/yao-meta-skill/references/distribution-registry-method.md +60 -0
- package/bundled-skills/yao-meta-skill/references/eval-playbook.md +69 -0
- package/bundled-skills/yao-meta-skill/references/gate-selection.md +68 -0
- package/bundled-skills/yao-meta-skill/references/governance.md +134 -0
- package/bundled-skills/yao-meta-skill/references/human-review-template.md +54 -0
- package/bundled-skills/yao-meta-skill/references/intent-dialogue.md +138 -0
- package/bundled-skills/yao-meta-skill/references/iteration-philosophy.md +30 -0
- package/bundled-skills/yao-meta-skill/references/non-skill-decision-tree.md +39 -0
- package/bundled-skills/yao-meta-skill/references/operating-modes.md +107 -0
- package/bundled-skills/yao-meta-skill/references/output-eval-method.md +113 -0
- package/bundled-skills/yao-meta-skill/references/output-quality-risk.md +41 -0
- package/bundled-skills/yao-meta-skill/references/output-visual-quality.md +53 -0
- package/bundled-skills/yao-meta-skill/references/packaging-contracts.md +70 -0
- package/bundled-skills/yao-meta-skill/references/pattern-extraction-doctrine.md +76 -0
- package/bundled-skills/yao-meta-skill/references/platform-capability-matrix.md +49 -0
- package/bundled-skills/yao-meta-skill/references/prompt-engineering-doctrine.md +76 -0
- package/bundled-skills/yao-meta-skill/references/qa-ladder.md +57 -0
- package/bundled-skills/yao-meta-skill/references/reference-scan.md +126 -0
- package/bundled-skills/yao-meta-skill/references/regression-cause-taxonomy.md +80 -0
- package/bundled-skills/yao-meta-skill/references/resource-boundaries.md +120 -0
- package/bundled-skills/yao-meta-skill/references/review-studio-method.md +87 -0
- package/bundled-skills/yao-meta-skill/references/review-waiver-method.md +76 -0
- package/bundled-skills/yao-meta-skill/references/runtime-conformance-method.md +21 -0
- package/bundled-skills/yao-meta-skill/references/skill-archetypes.md +86 -0
- package/bundled-skills/yao-meta-skill/references/skill-atlas-method.md +35 -0
- package/bundled-skills/yao-meta-skill/references/skill-engineering-method.md +210 -0
- package/bundled-skills/yao-meta-skill/references/skill-ir-method.md +41 -0
- package/bundled-skills/yao-meta-skill/references/skillops-decision-policy.md +53 -0
- package/bundled-skills/yao-meta-skill/references/systems-thinking-doctrine.md +75 -0
- package/bundled-skills/yao-meta-skill/references/telemetry-drift-method.md +182 -0
- package/bundled-skills/yao-meta-skill/references/trust-security-method.md +79 -0
- package/bundled-skills/yao-meta-skill/references/user-memory-policy.md +35 -0
- package/bundled-skills/youtube-notetaker/SKILL.md +209 -0
- package/bundled-skills/youtube-notetaker/reference/artifact.html +269 -0
- package/bundled-skills/youtube-notetaker/scripts/contact_sheet.py +53 -0
- package/bundled-skills/youtube-notetaker/scripts/detect_slides.sh +19 -0
- package/bundled-skills/youtube-notetaker/scripts/download.sh +24 -0
- package/bundled-skills/youtube-notetaker/scripts/extract_slides.py +43 -0
- package/bundled-skills/youtube-notetaker/scripts/serve.py +222 -0
- package/bundled-skills/youtube-notetaker/scripts/setup.sh +27 -0
- package/bundled-skills/youtube-notetaker/scripts/verify.sh +31 -0
- package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +59 -0
- package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +69 -0
- package/package.json +1 -1
- package/skills_index.json +2013 -330
- package/bundled-skills/ai-md/SKILL.md +0 -523
- package/bundled-skills/atlas-contract/SKILL.md +0 -650
- package/bundled-skills/busybox-on-windows/SKILL.md +0 -40
- package/bundled-skills/monte-carlo-prevent/SKILL.md +0 -257
- package/bundled-skills/monte-carlo-prevent/references/TROUBLESHOOTING.md +0 -23
- package/bundled-skills/monte-carlo-prevent/references/parameters.md +0 -32
- package/bundled-skills/monte-carlo-prevent/references/workflows.md +0 -478
- package/bundled-skills/monte-carlo-push-ingestion/SKILL.md +0 -372
- package/bundled-skills/monte-carlo-push-ingestion/references/anomaly-detection.md +0 -87
- package/bundled-skills/monte-carlo-push-ingestion/references/custom-lineage.md +0 -203
- package/bundled-skills/monte-carlo-push-ingestion/references/direct-http-api.md +0 -207
- package/bundled-skills/monte-carlo-push-ingestion/references/prerequisites.md +0 -150
- package/bundled-skills/monte-carlo-push-ingestion/references/push-lineage.md +0 -160
- package/bundled-skills/monte-carlo-push-ingestion/references/push-metadata.md +0 -158
- package/bundled-skills/monte-carlo-push-ingestion/references/push-query-logs.md +0 -219
- package/bundled-skills/monte-carlo-push-ingestion/references/validation.md +0 -257
- package/bundled-skills/monte-carlo-push-ingestion/scripts/sample_verify.py +0 -357
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_lineage.py +0 -70
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_metadata.py +0 -65
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_query_logs.py +0 -70
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_lineage.py +0 -214
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_metadata.py +0 -160
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_query_logs.py +0 -164
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_lineage.py +0 -198
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_metadata.py +0 -193
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_query_logs.py +0 -207
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_metadata.py +0 -71
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_query_logs.py +0 -64
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_metadata.py +0 -253
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_query_logs.py +0 -149
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_metadata.py +0 -190
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_query_logs.py +0 -208
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_lineage.py +0 -83
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_metadata.py +0 -77
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_query_logs.py +0 -83
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_lineage.py +0 -240
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_metadata.py +0 -212
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_query_logs.py +0 -204
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_lineage.py +0 -192
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_metadata.py +0 -178
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_query_logs.py +0 -200
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_lineage.py +0 -119
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_metadata.py +0 -119
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_query_logs.py +0 -117
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_lineage.py +0 -265
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_metadata.py +0 -313
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_query_logs.py +0 -284
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_lineage.py +0 -309
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_metadata.py +0 -245
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_query_logs.py +0 -255
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_lineage.py +0 -78
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_metadata.py +0 -80
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_query_logs.py +0 -88
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_lineage.py +0 -235
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_metadata.py +0 -219
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_query_logs.py +0 -239
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_lineage.py +0 -178
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_metadata.py +0 -178
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_query_logs.py +0 -196
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_lineage.py +0 -154
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_metadata.py +0 -137
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_query_logs.py +0 -137
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_lineage.py +0 -349
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_metadata.py +0 -329
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_query_logs.py +0 -254
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_lineage.py +0 -307
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_metadata.py +0 -228
- package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_query_logs.py +0 -248
- package/bundled-skills/monte-carlo-push-ingestion/scripts/test_template_sdk_usage.py +0 -340
- package/bundled-skills/skill-optimizer/SKILL.md +0 -271
- package/bundled-skills/using-superpowers/SKILL.md +0 -98
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube-notetaker
|
|
3
|
+
description: "Turn YouTube talks into local study notes with slides, transcripts, editable annotations, and a markdown-backed viewer."
|
|
4
|
+
category: "video"
|
|
5
|
+
risk: "safe"
|
|
6
|
+
source: "official"
|
|
7
|
+
source_repo: "dair-ai/dair-academy-plugins"
|
|
8
|
+
source_type: "official"
|
|
9
|
+
date_added: "2026-06-19"
|
|
10
|
+
author: "DAIR.AI"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
license_source: "https://github.com/dair-ai/dair-academy-plugins/blob/main/README.md#license"
|
|
13
|
+
tags:
|
|
14
|
+
- dair-academy
|
|
15
|
+
- ai
|
|
16
|
+
- workflow
|
|
17
|
+
tools:
|
|
18
|
+
- claude-code
|
|
19
|
+
- codex-cli
|
|
20
|
+
- cursor
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# YouTube Notetaker
|
|
24
|
+
|
|
25
|
+
## When to Use
|
|
26
|
+
|
|
27
|
+
Use when this workflow matches the user request: >
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_Source: [dair-ai/dair-academy-plugins](https://github.com/dair-ai/dair-academy-plugins) (MIT)._
|
|
31
|
+
|
|
32
|
+
Build a personal library of YouTube talks you study with. Each video becomes one **plain
|
|
33
|
+
markdown file**: slide snapshots at their timestamps, a full timestamped transcript, and
|
|
34
|
+
editable notes. A small bundled server renders the library as an interactive deep-dive in the
|
|
35
|
+
browser. No database, no cloud service. Everything is files on disk you fully own.
|
|
36
|
+
|
|
37
|
+
## Architecture (read this first)
|
|
38
|
+
|
|
39
|
+
The **markdown library is the single source of truth**. The artifact is a thin HTML shell that
|
|
40
|
+
fetches from the server and writes notes back. Never hardcode video data into the HTML.
|
|
41
|
+
|
|
42
|
+
- **Library:** a plain folder, set by `VIDEO_LIBRARY_DIR` (default `~/video-deepdives/`).
|
|
43
|
+
- One markdown file per video, **filename slug = YouTube id** (e.g. `RtywqDFBYnQ.md`).
|
|
44
|
+
- Frontmatter holds video metadata + a `slides` array.
|
|
45
|
+
- Body holds the full transcript as `[HH:MM:SS] text` lines.
|
|
46
|
+
- `_media/` holds slide images, **namespaced per video** as `<youtube_id>-slide-NN.jpg`
|
|
47
|
+
to avoid collisions between videos.
|
|
48
|
+
- **Server:** `scripts/serve.py`, a single stdlib + PyYAML file. Start it with:
|
|
49
|
+
```
|
|
50
|
+
python3 scripts/serve.py --dir ~/video-deepdives --port 8000
|
|
51
|
+
```
|
|
52
|
+
It serves the artifact at `/` and a small API the artifact talks to:
|
|
53
|
+
- `GET /api/video-deepdives` (front page fetches this) lists every video.
|
|
54
|
+
- `GET /api/video-deepdives/<id>` returns one video `{meta, body}`.
|
|
55
|
+
- `GET /api/video-deepdives/_media/<file>` serves a slide image.
|
|
56
|
+
- `PATCH /api/video-deepdives/<id>` with `{fields:{slides:[...]}}` writes notes back.
|
|
57
|
+
- **It picks up new videos automatically** the moment a markdown file exists. Adding a video
|
|
58
|
+
means writing a markdown file + media; you almost never touch the HTML.
|
|
59
|
+
- The `/api/video-deepdives` URL namespace is local to the bundled server.
|
|
60
|
+
- **Artifact:** `reference/artifact.html`, served by `serve.py` at `/`. A clean reference copy;
|
|
61
|
+
only rewrite it if the user wants a UI change. For new videos, leave it alone.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- `yt-dlp` and `ffmpeg` on PATH (download + frame/scene extraction).
|
|
66
|
+
- Python 3 with `Pillow` (contact sheet) and `PyYAML` (markdown file + server).
|
|
67
|
+
```
|
|
68
|
+
pip install yt-dlp pillow pyyaml # ffmpeg via your package manager
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Adding a video — the pipeline
|
|
72
|
+
|
|
73
|
+
All helper scripts are in `scripts/`. Work in a scratch dir (e.g. `/tmp/ytnote-<id>/`), then
|
|
74
|
+
copy final assets into the library. Set `VIDEO_LIBRARY_DIR` once per shell if you don't want the
|
|
75
|
+
default. **Do not use em dashes (—) or arrows (→) in notes/titles.**
|
|
76
|
+
|
|
77
|
+
### 1. Resolve the id and check embeddability
|
|
78
|
+
```
|
|
79
|
+
scripts/setup.sh "<youtube_url_or_id>"
|
|
80
|
+
```
|
|
81
|
+
Prints the 11-char `YTID`, the scratch dir, the target library path, and whether YouTube
|
|
82
|
+
**embedding is allowed** (oembed 200) or **blocked** (oembed 401, e.g. some university talks).
|
|
83
|
+
If blocked, inline playback won't work but the artifact degrades gracefully to an "open at this
|
|
84
|
+
moment on YouTube" link, so proceed normally.
|
|
85
|
+
|
|
86
|
+
### 2. Download video + subtitles
|
|
87
|
+
```
|
|
88
|
+
scripts/download.sh "<YTID>" /tmp/ytnote-<YTID>
|
|
89
|
+
```
|
|
90
|
+
Uses `yt-dlp` to grab the video (≤720p is plenty for slide frames) and the best available
|
|
91
|
+
subtitles (manual if present, else auto-captions) as `.vtt`. Also fetches title/uploader.
|
|
92
|
+
|
|
93
|
+
### 3. Detect candidate slide timestamps
|
|
94
|
+
```
|
|
95
|
+
scripts/detect_slides.sh /tmp/ytnote-<YTID>/video.mp4 /tmp/ytnote-<YTID>
|
|
96
|
+
```
|
|
97
|
+
Runs ffmpeg scene detection (`select='gt(scene,0.3)'`) and writes `scene_times.txt` (seconds).
|
|
98
|
+
0.3 is a good default; lower it (0.2) for subtle slide decks, raise it (0.4) for busy video.
|
|
99
|
+
|
|
100
|
+
### 4. Build a contact sheet and CURATE
|
|
101
|
+
```
|
|
102
|
+
python3 scripts/contact_sheet.py /tmp/ytnote-<YTID>/video.mp4 /tmp/ytnote-<YTID>/scene_times.txt /tmp/ytnote-<YTID>/contact.jpg
|
|
103
|
+
```
|
|
104
|
+
Read `contact.jpg` (labeled with index + timestamp). **This is the human-judgment step:** keep
|
|
105
|
+
frames that are real content slides; **drop talking-head shots, transitions, duplicates, and
|
|
106
|
+
blurry mid-animation frames.** Save the kept timestamps (seconds) to `/tmp/ytnote-<YTID>/keep.txt`,
|
|
107
|
+
one per line. Typical talk yields 15-25 slides.
|
|
108
|
+
|
|
109
|
+
### 5. Extract the curated slides at full quality and install to _media
|
|
110
|
+
```
|
|
111
|
+
python3 scripts/extract_slides.py <YTID> /tmp/ytnote-<YTID>/video.mp4 /tmp/ytnote-<YTID>/keep.txt > /tmp/ytnote-<YTID>/slides.json
|
|
112
|
+
```
|
|
113
|
+
Extracts each kept timestamp at 1280px wide, JPEG, and copies them into
|
|
114
|
+
`$VIDEO_LIBRARY_DIR/_media/` as `<YTID>-slide-01.jpg`, `-02.jpg`, … (numbered in time order).
|
|
115
|
+
Progress goes to stderr; a clean `slides.json` scaffold prints to **stdout**, so redirect it to a
|
|
116
|
+
file as shown, then fill in `title` and `note`.
|
|
117
|
+
|
|
118
|
+
Tip: talks are often a slide + speaker-cam composite, and speakers flip back and forth, so the
|
|
119
|
+
same slide appears at several timestamps. Keep the cleanest instance of each, and re-anchor each
|
|
120
|
+
slide's `t` to where it is actually discussed in the transcript (better "play from here" UX).
|
|
121
|
+
|
|
122
|
+
### 6. Build the transcript
|
|
123
|
+
```
|
|
124
|
+
python3 scripts/vtt_to_transcript.py /tmp/ytnote-<YTID>/*.vtt /tmp/ytnote-<YTID>/transcript.txt
|
|
125
|
+
```
|
|
126
|
+
Parses the VTT into clean, de-duplicated `[HH:MM:SS] text` lines (YouTube auto-captions repeat
|
|
127
|
+
rolling text; the script collapses it). This becomes the markdown body.
|
|
128
|
+
|
|
129
|
+
### 7. Write notes and assemble the markdown file
|
|
130
|
+
For each kept slide, write a 1-3 sentence `note` grounded in the transcript around that timestamp
|
|
131
|
+
(don't invent claims). Then assemble:
|
|
132
|
+
```
|
|
133
|
+
python3 scripts/write_library_item.py \
|
|
134
|
+
--id <YTID> \
|
|
135
|
+
--title "Talk title" \
|
|
136
|
+
--speaker "Name, Role, Org" \
|
|
137
|
+
--tags tag1,tag2,tag3 \
|
|
138
|
+
--slides /tmp/ytnote-<YTID>/slides.json \
|
|
139
|
+
--transcript /tmp/ytnote-<YTID>/transcript.txt
|
|
140
|
+
```
|
|
141
|
+
Writes `$VIDEO_LIBRARY_DIR/<YTID>.md` with correct frontmatter + body.
|
|
142
|
+
|
|
143
|
+
### 8. Serve and verify (always do this)
|
|
144
|
+
```
|
|
145
|
+
python3 scripts/serve.py --dir "$VIDEO_LIBRARY_DIR" --port 8000 &
|
|
146
|
+
scripts/verify.sh <YTID> # defaults to http://127.0.0.1:8000
|
|
147
|
+
```
|
|
148
|
+
`verify.sh` curls the collection list, the item, the first slide image, and the artifact,
|
|
149
|
+
asserting HTTP 200 and that the new id appears in the index. Then open
|
|
150
|
+
`http://127.0.0.1:8000/#/<YTID>` in a browser to confirm slides + transcript + notes render.
|
|
151
|
+
|
|
152
|
+
## Markdown file shape (reference)
|
|
153
|
+
|
|
154
|
+
```markdown
|
|
155
|
+
---
|
|
156
|
+
id: RtywqDFBYnQ
|
|
157
|
+
title: Memory and dreaming for self-learning agents
|
|
158
|
+
youtube_id: RtywqDFBYnQ
|
|
159
|
+
speaker: Mahesh, Product Manager, Platform team at Anthropic
|
|
160
|
+
source_url: https://www.youtube.com/watch?v=RtywqDFBYnQ
|
|
161
|
+
slide_count: 19
|
|
162
|
+
created: '2026-05-25'
|
|
163
|
+
tags: [anthropic, memory, agents]
|
|
164
|
+
slides:
|
|
165
|
+
- idx: 1
|
|
166
|
+
t: 55.7 # seconds (float ok), used for seeking
|
|
167
|
+
mmss: 00:55 # display label
|
|
168
|
+
title: Agent primitives have evolved
|
|
169
|
+
note: One to three sentences grounded in the transcript at this timestamp.
|
|
170
|
+
img: /api/video-deepdives/_media/RtywqDFBYnQ-slide-01.jpg
|
|
171
|
+
# ... more slides
|
|
172
|
+
---
|
|
173
|
+
## Transcript
|
|
174
|
+
[00:00:08] Hello, everyone...
|
|
175
|
+
[00:00:11] ...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
- `idx` can be sparse/non-contiguous; the artifact sorts slides by `t`, so ordering is by
|
|
180
|
+
timestamp, not idx.
|
|
181
|
+
- `img` is always a `/api/video-deepdives/_media/<file>` URL (served by serve.py),
|
|
182
|
+
never base64.
|
|
183
|
+
- Slide `note` is what the user edits in the UI; PATCH writes the whole `slides` array back.
|
|
184
|
+
|
|
185
|
+
## Gotchas
|
|
186
|
+
- **Embedding disabled** (oembed 401): inline player is blocked by the video owner. Not a bug;
|
|
187
|
+
the artifact shows an "open at this moment on YouTube" link instead. Mention it to the user.
|
|
188
|
+
- **Image collisions:** always namespace media `<YTID>-slide-NN.jpg`. Never reuse bare
|
|
189
|
+
`slide-NN.jpg` for a new video.
|
|
190
|
+
- **Auto-caption noise:** rolling YouTube captions duplicate text across cues; use the provided
|
|
191
|
+
VTT parser, don't dump raw VTT into the body.
|
|
192
|
+
- **Don't touch existing videos** when adding a new one. Each video is an independent file.
|
|
193
|
+
- **Server not picking up a video:** confirm the `.md` file is directly inside `--dir` (not a
|
|
194
|
+
subfolder) and the filename is `<YTID>.md`.
|
|
195
|
+
|
|
196
|
+
## What makes this portable
|
|
197
|
+
- **No orchestrator / no database.** Storage is a plain folder of markdown + images.
|
|
198
|
+
- **One env var** (`VIDEO_LIBRARY_DIR`) controls where the library lives.
|
|
199
|
+
- **One small server file** (`serve.py`, stdlib + PyYAML) renders everything and handles
|
|
200
|
+
note write-back. Drop it anywhere Python runs.
|
|
201
|
+
- The markdown files are portable: readable in Obsidian or any editor, and the frontmatter is
|
|
202
|
+
standard YAML.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
## Limitations
|
|
206
|
+
|
|
207
|
+
- Requires the upstream tool, account, API key, or local setup when the workflow names one.
|
|
208
|
+
- Does not authorize destructive, production, paid, or external-message actions without explicit user approval.
|
|
209
|
+
- Validate generated artifacts or recommendations against the user's real sources before treating them as final.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2
|
+
<title>Video Deep-Dives</title>
|
|
3
|
+
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
4
|
+
<style>
|
|
5
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
6
|
+
html,body{height:100%}
|
|
7
|
+
body{font-family:'DM Sans',system-ui,sans-serif;background:#f4f1eb;color:#1c1a17;line-height:1.55;display:flex;flex-direction:column;height:100vh;overflow:hidden}
|
|
8
|
+
:root{--slide-max:560px;--vid-max:100%}
|
|
9
|
+
header{padding:11px 18px;border-bottom:1px solid #e2dccf;background:#fbf9f4;flex:0 0 auto}
|
|
10
|
+
.eyebrow{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#8a7e6e;font-weight:600}
|
|
11
|
+
.backlink{display:none;font-size:11.5px;color:#2a5cbf;text-decoration:none;font-weight:600;cursor:pointer}
|
|
12
|
+
.backlink:hover{text-decoration:underline}
|
|
13
|
+
h1{font-family:'Playfair Display',serif;font-size:clamp(17px,2.3vw,25px);font-weight:900;color:#111;line-height:1.05;margin:2px 0}
|
|
14
|
+
.speaker{color:#4a4236;font-size:12.5px}.speaker a{color:#2a5cbf;text-decoration:none}
|
|
15
|
+
/* ---------- HOME (index) ---------- */
|
|
16
|
+
#home{flex:1 1 auto;overflow:auto;padding:22px 26px}
|
|
17
|
+
.home-intro{max-width:760px;margin:0 auto 22px}
|
|
18
|
+
.home-intro .lede{font-family:'Playfair Display',serif;font-size:clamp(22px,3vw,34px);font-weight:900;color:#111;line-height:1.08}
|
|
19
|
+
.home-intro p{color:#5a5145;font-size:13.5px;margin-top:8px;max-width:640px}
|
|
20
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;max-width:1100px;margin:0 auto}
|
|
21
|
+
.card{background:#fff;border:1px solid #e2dccf;border-radius:14px;overflow:hidden;cursor:pointer;text-decoration:none;color:inherit;display:flex;flex-direction:column;transition:transform .14s,box-shadow .14s,border-color .14s}
|
|
22
|
+
.card:hover{transform:translateY(-3px);box-shadow:0 10px 26px rgba(0,0,0,.10);border-color:#c8920a}
|
|
23
|
+
.card .thumb{position:relative;aspect-ratio:16/9;background:#000;overflow:hidden}
|
|
24
|
+
.card .thumb img{width:100%;height:100%;object-fit:cover;display:block}
|
|
25
|
+
.card .badge{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,.74);color:#fff;font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px}
|
|
26
|
+
.card .play{position:absolute;inset:0;margin:auto;width:54px;height:54px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.5);color:#fff;border-radius:50%;font-size:20px;opacity:0;transition:.2s}
|
|
27
|
+
.card:hover .play{opacity:1}
|
|
28
|
+
.card .body{padding:13px 15px 15px}
|
|
29
|
+
.card .ct{font-family:'Playfair Display',serif;font-size:16.5px;font-weight:700;color:#111;line-height:1.22}
|
|
30
|
+
.card .cs{color:#7a6f5d;font-size:12px;margin-top:6px}
|
|
31
|
+
.card .tags{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px}
|
|
32
|
+
.card .tag{font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:#7a5010;background:rgba(180,130,20,.14);border-radius:5px;padding:2px 7px;font-weight:600}
|
|
33
|
+
#homeErr{display:none;max-width:760px;margin:0 auto;padding:10px 14px;background:#fff0ec;border:1px solid #c87060;border-radius:8px;color:#8a1a1a;font-size:13px}
|
|
34
|
+
/* ---------- DEEP-DIVE ---------- */
|
|
35
|
+
#deepdive{flex:1 1 auto;display:none;min-height:0;overflow:hidden}
|
|
36
|
+
#split{flex:1 1 auto;display:flex;min-height:0;overflow:hidden;width:100%}
|
|
37
|
+
#left{flex:0 0 58%;min-width:240px;overflow:auto;padding:14px 18px;display:flex;flex-direction:column}
|
|
38
|
+
#divider{flex:0 0 8px;cursor:col-resize;background:linear-gradient(#ddd6c8,#cfc7b6);position:relative}
|
|
39
|
+
#divider::after{content:"⋮⋮";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) rotate(90deg);color:#8a7e6e;font-size:11px;letter-spacing:-2px}
|
|
40
|
+
#divider:hover,#divider.drag{background:#c8920a}
|
|
41
|
+
/* RIGHT pane: fixed video region + independently scrolling transcript */
|
|
42
|
+
#right{flex:1 1 0;min-width:280px;display:flex;flex-direction:column;overflow:hidden;padding:14px 16px;background:#fbf9f4;border-left:1px solid #e2dccf}
|
|
43
|
+
.rtop{flex:0 0 auto}
|
|
44
|
+
.toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;background:#fff;border:1px solid #ddd8d0;border-radius:10px;padding:8px 12px;margin-bottom:12px;font-size:12px;color:#4a4236}
|
|
45
|
+
.toolbar label{display:flex;align-items:center;gap:7px}.toolbar input[type=range]{accent-color:#c8920a}
|
|
46
|
+
.deck-h{font-family:'Playfair Display',serif;font-size:18px;margin-bottom:3px}
|
|
47
|
+
.deck-sub{color:#8a7e6e;font-size:12px;margin-bottom:12px}
|
|
48
|
+
.slide{background:#fff;border:1px solid #ddd8d0;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.05);padding:11px;margin:0 auto 16px;width:100%;max-width:var(--slide-max);transition:border-color .15s,box-shadow .15s}
|
|
49
|
+
.slide.active{border-color:#b0357a;box-shadow:0 0 0 3px rgba(176,53,122,.16)}
|
|
50
|
+
.slide-img{position:relative;cursor:pointer;border-radius:9px;overflow:hidden;background:#000}
|
|
51
|
+
.slide-img img{width:100%;display:block;aspect-ratio:16/9;object-fit:contain;background:#000}
|
|
52
|
+
.slide-img:hover img{opacity:.85}
|
|
53
|
+
.play-badge{position:absolute;inset:0;margin:auto;width:54px;height:54px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);color:#fff;border-radius:50%;font-size:20px;opacity:0;transition:.2s}
|
|
54
|
+
.slide-img:hover .play-badge{opacity:1}
|
|
55
|
+
.slide-t{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,.72);color:#fff;font-size:11px;font-weight:600;padding:2px 7px;border-radius:5px}
|
|
56
|
+
.slide-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 2px 8px}
|
|
57
|
+
.slide-meta h3{font-family:'Playfair Display',serif;font-size:15.5px;color:#111;font-weight:700;line-height:1.25}
|
|
58
|
+
.btn{cursor:pointer;border:0;background:#2a5cbf;color:#fff;font-size:11.5px;font-weight:600;padding:5px 11px;border-radius:6px;white-space:nowrap;font-family:'DM Sans'}
|
|
59
|
+
.btn:hover{background:#1d4699}
|
|
60
|
+
.note-lbl{display:block;font-size:10px;letter-spacing:1.4px;text-transform:uppercase;color:#8a7e6e;font-weight:600;margin:2px 2px 4px}
|
|
61
|
+
.saved{color:#2e8b57;letter-spacing:0;text-transform:none;font-weight:500;margin-left:6px}
|
|
62
|
+
.note-area{width:100%;min-height:70px;resize:vertical;border:1px solid #e3dccb;border-radius:8px;background:#fdfcf8;padding:9px 11px;font-family:'DM Sans';font-size:13px;color:#3a342b;line-height:1.55}
|
|
63
|
+
.note-area:focus{outline:none;border-color:#c8920a;background:#fffdf6}
|
|
64
|
+
.vidwrap{max-width:var(--vid-max);margin:0 auto}
|
|
65
|
+
.vid{position:relative;width:100%;aspect-ratio:16/9;border-radius:10px;overflow:hidden;background:#000}
|
|
66
|
+
.vid iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
|
|
67
|
+
.now{margin-top:10px;background:#fdf9f0;border:1px solid #e6dcc4;border-radius:9px;padding:10px 13px}
|
|
68
|
+
.now .lbl{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:#7a5010;font-weight:600;display:flex;gap:8px;align-items:center}
|
|
69
|
+
.now .lbl b{background:rgba(180,130,20,.14);color:#7a5010;border-radius:5px;padding:2px 7px;font-size:11px}
|
|
70
|
+
.now p{margin-top:6px;font-size:13px;color:#3a342b;max-height:96px;overflow:auto}
|
|
71
|
+
.tr-h{font-family:'Playfair Display',serif;font-size:15px;margin:12px 2px 6px}
|
|
72
|
+
.tsearch{width:100%;padding:8px 11px;border:1px solid #ddd8d0;border-radius:8px;font-size:12.5px;margin-bottom:8px;font-family:'DM Sans'}
|
|
73
|
+
#transcript{flex:1 1 0;overflow:auto;border:1px solid #eee4d4;border-radius:9px;background:#fffdf8}
|
|
74
|
+
.trow{display:flex;gap:9px;padding:6px 11px;cursor:pointer;border-bottom:1px solid #f1ead9;font-size:12.5px}
|
|
75
|
+
.trow:hover{background:#fdf3df}.trow.hl{background:#fce9bd}
|
|
76
|
+
.tt{color:#2a5cbf;font-weight:600;font-size:11.5px;min-width:54px;flex-shrink:0}
|
|
77
|
+
#err{display:none;padding:10px 14px;background:#fff0ec;border:1px solid #c87060;border-radius:8px;color:#8a1a1a;font-size:13px;margin:10px 0}
|
|
78
|
+
@media(max-width:760px){#split{flex-direction:column}#left,#right{flex:1 1 auto}#divider{display:none}}
|
|
79
|
+
</style></head><body>
|
|
80
|
+
<header>
|
|
81
|
+
<a class="backlink" id="backlink" href="#/">← All videos</a>
|
|
82
|
+
<div class="eyebrow">Video deep-dives · markdown-backed</div>
|
|
83
|
+
<h1 id="title">Video Deep-Dives</h1>
|
|
84
|
+
<div class="speaker" id="speaker">A growing library of talks I'm studying. Slides, transcripts, and editable notes, all backed by markdown files.</div>
|
|
85
|
+
</header>
|
|
86
|
+
|
|
87
|
+
<!-- ===================== HOME / INDEX ===================== -->
|
|
88
|
+
<div id="home">
|
|
89
|
+
<div id="homeErr"></div>
|
|
90
|
+
<div class="grid" id="grid"></div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- ===================== DEEP-DIVE ===================== -->
|
|
94
|
+
<div id="deepdive">
|
|
95
|
+
<div id="split">
|
|
96
|
+
<div id="left">
|
|
97
|
+
<div class="toolbar">
|
|
98
|
+
<label>Slide size <input type="range" id="slideSize" min="320" max="900" value="560" oninput="document.documentElement.style.setProperty('--slide-max',this.value+'px')"></label>
|
|
99
|
+
<span style="color:#cfc7b6">|</span><span class="deck-sub" style="margin:0" id="deckcount"></span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="deck-h">Slide deck</div>
|
|
102
|
+
<div class="deck-sub">Click a slide (or ▶) to play the video from that moment. Notes are editable and saved to markdown.</div>
|
|
103
|
+
<div id="err"></div>
|
|
104
|
+
<div id="deck"></div>
|
|
105
|
+
</div>
|
|
106
|
+
<div id="divider" title="Drag to resize"></div>
|
|
107
|
+
<div id="right">
|
|
108
|
+
<div class="rtop">
|
|
109
|
+
<div class="toolbar"><label>Video size <input type="range" id="vidSize" min="40" max="100" value="100" oninput="document.documentElement.style.setProperty('--vid-max',this.value+'%')"></label></div>
|
|
110
|
+
<div class="vidwrap"><div class="vid"><iframe id="ytplayer" src="" allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe></div></div>
|
|
111
|
+
<div class="now"><div class="lbl">Now playing <b id="now-t">--:--</b> <a id="yt-jump" target="_blank" rel="noopener" style="margin-left:auto;color:#2a5cbf;text-decoration:none;font-weight:600;letter-spacing:0;text-transform:none;display:none">open at this moment on YouTube ↗</a></div><p id="now-tx">Click any slide to play the video from that point.</p></div>
|
|
112
|
+
<div class="tr-h">Full transcript</div>
|
|
113
|
+
<input class="tsearch" id="tsearch" placeholder="Search transcript…" oninput="filt(this.value)">
|
|
114
|
+
</div>
|
|
115
|
+
<div id="transcript"></div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<script src="https://www.youtube.com/iframe_api"></script>
|
|
121
|
+
<script>
|
|
122
|
+
var API_URL='/api/video-deepdives';
|
|
123
|
+
var player,ready=false,pending=null,DATA=null,SLIDES=[],SEGS=[];
|
|
124
|
+
var CURRENT_ID=null, YTID=null, INDEX=null;
|
|
125
|
+
|
|
126
|
+
function onYouTubeIframeAPIReady(){player=new YT.Player('ytplayer',{events:{'onReady':function(){ready=true;if(pending!=null){doPlay(pending);pending=null;}}}});}
|
|
127
|
+
function fmt(t){t=Math.floor(t);return String(Math.floor(t/60)).padStart(2,'0')+':'+String(t%60).padStart(2,'0');}
|
|
128
|
+
function esc(s){var d=document.createElement('div');d.textContent=s==null?'':s;return d.innerHTML;}
|
|
129
|
+
|
|
130
|
+
/* ---------------- Router ---------------- */
|
|
131
|
+
function route(){
|
|
132
|
+
var h=(location.hash||'').replace(/^#\/?/,'').trim();
|
|
133
|
+
if(h){showVideo(h);} else {showHome();}
|
|
134
|
+
}
|
|
135
|
+
function showHome(){
|
|
136
|
+
document.getElementById('deepdive').style.display='none';
|
|
137
|
+
document.getElementById('home').style.display='block';
|
|
138
|
+
document.getElementById('backlink').style.display='none';
|
|
139
|
+
document.getElementById('title').textContent='Video Deep-Dives';
|
|
140
|
+
document.getElementById('speaker').textContent="A growing library of talks I'm studying. Slides, transcripts, and editable notes, all backed by markdown files.";
|
|
141
|
+
document.title='Video Deep-Dives';
|
|
142
|
+
if(INDEX===null) loadIndex();
|
|
143
|
+
}
|
|
144
|
+
function showVideo(id){
|
|
145
|
+
document.getElementById('home').style.display='none';
|
|
146
|
+
document.getElementById('deepdive').style.display='flex';
|
|
147
|
+
document.getElementById('backlink').style.display='inline-block';
|
|
148
|
+
loadVideo(id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ---------------- Home / index ---------------- */
|
|
152
|
+
async function loadIndex(){
|
|
153
|
+
try{
|
|
154
|
+
var r=await fetch(API_URL); if(!r.ok) throw new Error('HTTP '+r.status);
|
|
155
|
+
var d=await r.json();
|
|
156
|
+
INDEX=(d.items||[]).filter(function(it){return it.youtube_id;});
|
|
157
|
+
var g=document.getElementById('grid'); g.innerHTML='';
|
|
158
|
+
if(!INDEX.length){g.innerHTML='<p style="color:#7a6f5d">No videos in the library yet.</p>';return;}
|
|
159
|
+
INDEX.forEach(function(it){
|
|
160
|
+
var slides=it.slides||[]; var thumb=(slides[0]&&slides[0].img)||'';
|
|
161
|
+
var tags=(it.tags||[]).slice(0,3).map(function(t){return '<span class="tag">'+esc(t)+'</span>';}).join('');
|
|
162
|
+
var a=document.createElement('a'); a.className='card'; a.href='#/'+encodeURIComponent(it.id);
|
|
163
|
+
a.innerHTML='<div class="thumb">'+(thumb?'<img src="'+esc(thumb)+'" alt="">':'')+'<span class="play">▶</span><span class="badge">'+(it.slide_count||slides.length)+' slides</span></div>'
|
|
164
|
+
+'<div class="body"><div class="ct">'+esc(it.title||it.id)+'</div>'
|
|
165
|
+
+'<div class="cs">'+esc(it.speaker||'')+'</div>'
|
|
166
|
+
+(tags?'<div class="tags">'+tags+'</div>':'')+'</div>';
|
|
167
|
+
g.appendChild(a);
|
|
168
|
+
});
|
|
169
|
+
}catch(e){var el=document.getElementById('homeErr');el.style.display='block';el.textContent='Could not load the video library: '+e.message+'. Is the backend running?';}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ---------------- Deep-dive ---------------- */
|
|
173
|
+
async function loadVideo(id){
|
|
174
|
+
if(CURRENT_ID===id && DATA){return;} // already loaded
|
|
175
|
+
CURRENT_ID=id;
|
|
176
|
+
document.getElementById('err').style.display='none';
|
|
177
|
+
document.getElementById('deck').innerHTML='';
|
|
178
|
+
document.getElementById('transcript').innerHTML='';
|
|
179
|
+
try{
|
|
180
|
+
var r=await fetch(API_URL+'/'+encodeURIComponent(id)); if(!r.ok) throw new Error('HTTP '+r.status);
|
|
181
|
+
DATA=await r.json(); var m=DATA.meta||{};
|
|
182
|
+
YTID=m.youtube_id||id;
|
|
183
|
+
SLIDES=(m.slides||[]).slice().sort(function(a,b){return a.t-b.t;});
|
|
184
|
+
document.title=m.title||'Video deep-dive';
|
|
185
|
+
document.getElementById('title').textContent=m.title||'';
|
|
186
|
+
document.getElementById('speaker').innerHTML=esc(m.speaker||'')+' · <a target="_blank" href="'+(m.source_url||'#')+'">watch on YouTube ↗</a>';
|
|
187
|
+
document.getElementById('deckcount').textContent=SLIDES.length+' slides · drag the divider ⋮⋮ to resize';
|
|
188
|
+
document.getElementById('now-t').textContent='--:--';
|
|
189
|
+
document.getElementById('now-tx').textContent='Click any slide to play the video from that point.';
|
|
190
|
+
document.getElementById('ytplayer').src='https://www.youtube.com/embed/'+YTID+'?enablejsapi=1&rel=0&playsinline=1';
|
|
191
|
+
SEGS=parseTranscript(DATA.body||'');
|
|
192
|
+
renderDeck(); renderTranscript();
|
|
193
|
+
}catch(e){var el=document.getElementById('err');el.style.display='block';el.textContent='Could not load library data: '+e.message+'. Is the server running?';}
|
|
194
|
+
}
|
|
195
|
+
function parseTranscript(body){
|
|
196
|
+
var out=[],re=/^\[(\d{2}):(\d{2}):(\d{2})\]\s*(.*)$/;
|
|
197
|
+
body.split('\n').forEach(function(line){var mm=line.match(re);if(mm){var sec=(+mm[1])*3600+(+mm[2])*60+(+mm[3]);out.push({t:sec,text:mm[4]});}});
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function renderDeck(){
|
|
201
|
+
var deck=document.getElementById('deck');deck.innerHTML='';
|
|
202
|
+
SLIDES.forEach(function(s,i){
|
|
203
|
+
var d=document.createElement('div');d.className='slide';d.id='slide-'+i;d.dataset.t=s.t;
|
|
204
|
+
d.innerHTML='<div class="slide-img"><img src="'+esc(s.img)+'" alt="'+esc(s.title)+'"><span class="play-badge">▶</span><span class="slide-t">'+esc(s.mmss||fmt(s.t))+'</span></div>'
|
|
205
|
+
+'<div class="slide-meta"><h3>'+esc(s.title)+'</h3><button class="btn">▶ Play '+esc(s.mmss||fmt(s.t))+'</button></div>'
|
|
206
|
+
+'<label class="note-lbl">Notes <span class="saved" id="saved-'+i+'"></span></label>'
|
|
207
|
+
+'<textarea class="note-area" id="note-'+i+'"></textarea>';
|
|
208
|
+
deck.appendChild(d);
|
|
209
|
+
d.querySelector('textarea').value=s.note||'';
|
|
210
|
+
d.querySelector('.slide-img').onclick=function(){play(i);};
|
|
211
|
+
d.querySelector('.btn').onclick=function(){play(i);};
|
|
212
|
+
d.querySelector('textarea').addEventListener('input',function(){onNote(i,this.value);});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function renderTranscript(){
|
|
216
|
+
var c=document.getElementById('transcript');c.innerHTML='';
|
|
217
|
+
SEGS.forEach(function(seg){
|
|
218
|
+
var r=document.createElement('div');r.className='trow';r.dataset.t=seg.t;r.dataset.text=seg.text.toLowerCase();
|
|
219
|
+
r.innerHTML='<span class="tt">'+fmt(seg.t)+'</span><span class="tx">'+esc(seg.text)+'</span>';
|
|
220
|
+
r.onclick=function(){seekOnly(seg.t);};c.appendChild(r);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function loadAt(t){
|
|
224
|
+
// Robust across video switches: use the JS API to load the right video at t.
|
|
225
|
+
var vd=player.getVideoData?player.getVideoData():null;
|
|
226
|
+
if(vd && vd.video_id===YTID){player.seekTo(t,true);player.playVideo();}
|
|
227
|
+
else{player.loadVideoById({videoId:YTID,startSeconds:Math.floor(t)});}
|
|
228
|
+
}
|
|
229
|
+
function doPlay(t){loadAt(t);}
|
|
230
|
+
function srcFallback(t){document.getElementById('ytplayer').src='https://www.youtube.com/embed/'+YTID+'?enablejsapi=1&rel=0&playsinline=1&autoplay=1&start='+Math.floor(t);}
|
|
231
|
+
function setJump(t){var a=document.getElementById('yt-jump');if(a){a.href='https://www.youtube.com/watch?v='+YTID+'&t='+Math.floor(t)+'s';a.style.display='inline';}}
|
|
232
|
+
function play(i){
|
|
233
|
+
var s=SLIDES[i];
|
|
234
|
+
document.querySelectorAll('.slide.active').forEach(function(x){x.classList.remove('active')});
|
|
235
|
+
var card=document.getElementById('slide-'+i);if(card)card.classList.add('active');
|
|
236
|
+
document.getElementById('now-t').textContent=s.mmss||fmt(s.t);
|
|
237
|
+
document.getElementById('now-tx').textContent=transcriptAt(s.t)||'(no transcript here)';
|
|
238
|
+
setJump(s.t);
|
|
239
|
+
if(ready&&player&&player.loadVideoById)doPlay(s.t);else{pending=s.t;srcFallback(s.t);}
|
|
240
|
+
hlRow(s.t);
|
|
241
|
+
}
|
|
242
|
+
function seekOnly(t){document.getElementById('now-t').textContent=fmt(t);document.getElementById('now-tx').textContent=transcriptAt(t);setJump(t);if(ready&&player&&player.loadVideoById)doPlay(t);else{pending=t;srcFallback(t);}hlRow(t);}
|
|
243
|
+
function transcriptAt(t){var out=[];SEGS.forEach(function(s){if(s.t>=t-1&&s.t<=t+10)out.push(s.text);});return out.join(' ');}
|
|
244
|
+
function hlRow(t){var rows=document.querySelectorAll('.trow'),best=null;rows.forEach(function(r){if(parseFloat(r.dataset.t)<=t+0.5)best=r;});document.querySelectorAll('.trow.hl').forEach(function(r){r.classList.remove('hl')});if(best){best.classList.add('hl');best.scrollIntoView({block:'nearest'});}}
|
|
245
|
+
function filt(q){q=q.toLowerCase().trim();document.querySelectorAll('.trow').forEach(function(r){r.style.display=(!q||r.dataset.text.indexOf(q)>-1)?'flex':'none';});}
|
|
246
|
+
// note write-back to markdown via PATCH
|
|
247
|
+
var timers={};
|
|
248
|
+
function onNote(i,val){
|
|
249
|
+
SLIDES[i].note=val;
|
|
250
|
+
var s=document.getElementById('saved-'+i);s.textContent='saving…';
|
|
251
|
+
clearTimeout(timers[i]);
|
|
252
|
+
timers[i]=setTimeout(function(){saveNotes(i,s);},700);
|
|
253
|
+
}
|
|
254
|
+
async function saveNotes(i,badge){
|
|
255
|
+
try{
|
|
256
|
+
var payload={fields:{slides:SLIDES.map(function(s){return {idx:s.idx,t:s.t,mmss:s.mmss,title:s.title,note:s.note,img:s.img};})}};
|
|
257
|
+
var r=await fetch(API_URL+'/'+encodeURIComponent(CURRENT_ID),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
258
|
+
badge.textContent=r.ok?'✓ saved':'save failed';
|
|
259
|
+
}catch(e){badge.textContent='save failed';}
|
|
260
|
+
setTimeout(function(){badge.textContent='';},1500);
|
|
261
|
+
}
|
|
262
|
+
(function(){var dv=document.getElementById('divider'),sp=document.getElementById('split'),lf=document.getElementById('left'),drag=false;
|
|
263
|
+
dv.addEventListener('mousedown',function(e){drag=true;dv.classList.add('drag');e.preventDefault();});
|
|
264
|
+
window.addEventListener('mousemove',function(e){if(!drag)return;var r=sp.getBoundingClientRect();var pct=(e.clientX-r.left)/r.width*100;pct=Math.max(25,Math.min(80,pct));lf.style.flexBasis=pct+'%';});
|
|
265
|
+
window.addEventListener('mouseup',function(){drag=false;dv.classList.remove('drag');});})();
|
|
266
|
+
|
|
267
|
+
window.addEventListener('hashchange',route);
|
|
268
|
+
route();
|
|
269
|
+
</script></body></html>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a labeled contact sheet of candidate slide frames for human curation.
|
|
3
|
+
|
|
4
|
+
Usage: contact_sheet.py <video.mp4> <scene_times.txt> <out.jpg> [--cols 5] [--thumb 360]
|
|
5
|
+
|
|
6
|
+
Reads timestamps (seconds, one per line), grabs a frame at each, lays them out in a
|
|
7
|
+
grid labeled "<index> | <mm:ss>". Read the output image, then write the timestamps you
|
|
8
|
+
want to KEEP (real content slides, not talking-head/transition frames) to a keep.txt,
|
|
9
|
+
one per line. The index labels make it easy to call out which to drop.
|
|
10
|
+
"""
|
|
11
|
+
import subprocess, sys, tempfile, os, argparse
|
|
12
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
13
|
+
|
|
14
|
+
def grab(video, t, path, w=360):
|
|
15
|
+
subprocess.run(["ffmpeg","-hide_banner","-loglevel","error","-ss",str(t),
|
|
16
|
+
"-i",video,"-frames:v","1","-vf",f"scale={w}:-1","-y",path], check=True)
|
|
17
|
+
|
|
18
|
+
def mmss(t):
|
|
19
|
+
t=int(float(t)); return f"{t//60:02d}:{t%60:02d}"
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
ap=argparse.ArgumentParser()
|
|
23
|
+
ap.add_argument("video"); ap.add_argument("times"); ap.add_argument("out")
|
|
24
|
+
ap.add_argument("--cols",type=int,default=5); ap.add_argument("--thumb",type=int,default=360)
|
|
25
|
+
a=ap.parse_args()
|
|
26
|
+
times=[l.strip() for l in open(a.times) if l.strip()]
|
|
27
|
+
if not times: sys.exit("no timestamps")
|
|
28
|
+
tmp=tempfile.mkdtemp()
|
|
29
|
+
thumbs=[]
|
|
30
|
+
for i,t in enumerate(times):
|
|
31
|
+
p=os.path.join(tmp,f"f{i:03d}.jpg")
|
|
32
|
+
try:
|
|
33
|
+
grab(a.video,t,p,a.thumb); thumbs.append((i,t,p))
|
|
34
|
+
except subprocess.CalledProcessError:
|
|
35
|
+
pass
|
|
36
|
+
if not thumbs: sys.exit("could not grab any frames")
|
|
37
|
+
tw=a.thumb; th=int(tw*9/16); lab=22; pad=6
|
|
38
|
+
cols=a.cols; rows=(len(thumbs)+cols-1)//cols
|
|
39
|
+
cw=tw+pad*2; ch=th+lab+pad*2
|
|
40
|
+
sheet=Image.new("RGB",(cols*cw,rows*ch),(20,20,20))
|
|
41
|
+
d=ImageDraw.Draw(sheet)
|
|
42
|
+
try: font=ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial Bold.ttf",15)
|
|
43
|
+
except Exception: font=ImageFont.load_default()
|
|
44
|
+
for n,(idx,t,p) in enumerate(thumbs):
|
|
45
|
+
r,c=divmod(n,cols); x=c*cw+pad; y=r*ch+pad
|
|
46
|
+
im=Image.open(p).convert("RGB").resize((tw,th))
|
|
47
|
+
sheet.paste(im,(x,y+lab))
|
|
48
|
+
d.text((x+2,y+2),f"{idx} | {mmss(t)} ({float(t):.1f}s)",fill=(255,210,90),font=font)
|
|
49
|
+
sheet.save(a.out,quality=85)
|
|
50
|
+
print(f"contact sheet: {a.out} ({len(thumbs)} frames, {cols}x{rows})")
|
|
51
|
+
print("Read it, then write the timestamps (seconds) to keep -> keep.txt (one per line).")
|
|
52
|
+
|
|
53
|
+
if __name__=="__main__": main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scene-detect candidate slide-change timestamps with ffmpeg.
|
|
3
|
+
# Usage: detect_slides.sh <video.mp4> <out_dir> [threshold]
|
|
4
|
+
# threshold default 0.3 (lower=more frames for subtle decks, higher=fewer for busy video).
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
VIDEO="${1:?usage: detect_slides.sh <video.mp4> <out_dir> [threshold]}"
|
|
7
|
+
OUT="${2:?usage: detect_slides.sh <video.mp4> <out_dir> [threshold]}"
|
|
8
|
+
THRESH="${3:-0.3}"
|
|
9
|
+
mkdir -p "$OUT"
|
|
10
|
+
|
|
11
|
+
# showinfo on the scene-selected frames prints pts_time per cut.
|
|
12
|
+
ffmpeg -hide_banner -i "$VIDEO" \
|
|
13
|
+
-vf "select='gt(scene,$THRESH)',showinfo" -vsync vfr -f null - 2>"$OUT/ffinfo.log" || true
|
|
14
|
+
|
|
15
|
+
grep -oE 'pts_time:[0-9.]+' "$OUT/ffinfo.log" | sed 's/pts_time://' | sort -n -u > "$OUT/scene_times.txt"
|
|
16
|
+
|
|
17
|
+
N=$(wc -l < "$OUT/scene_times.txt" | tr -d ' ')
|
|
18
|
+
echo "Detected $N candidate scene changes (threshold=$THRESH) -> $OUT/scene_times.txt"
|
|
19
|
+
echo "Next: build a contact sheet and curate which are real content slides."
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Download video (<=720p) + best subtitles for slide/transcript extraction.
|
|
3
|
+
# Usage: download.sh "<YTID>" "<scratch_dir>"
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
YTID="${1:?usage: download.sh <YTID> <scratch_dir>}"
|
|
6
|
+
OUT="${2:?usage: download.sh <YTID> <scratch_dir>}"
|
|
7
|
+
mkdir -p "$OUT"
|
|
8
|
+
URL="https://www.youtube.com/watch?v=$YTID"
|
|
9
|
+
|
|
10
|
+
# Video: 720p mp4 is plenty for 1280px slide frames; merge to a single file.
|
|
11
|
+
yt-dlp -f "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]" \
|
|
12
|
+
--merge-output-format mp4 -o "$OUT/video.%(ext)s" "$URL"
|
|
13
|
+
|
|
14
|
+
# Subtitles: prefer human captions, fall back to auto. English variants.
|
|
15
|
+
yt-dlp --skip-download --write-subs --write-auto-subs \
|
|
16
|
+
--sub-langs "en.*,en" --sub-format vtt -o "$OUT/subs.%(ext)s" "$URL" || true
|
|
17
|
+
|
|
18
|
+
# Metadata for title/uploader.
|
|
19
|
+
yt-dlp --skip-download --print "%(title)s\n%(uploader)s\n%(duration)s" "$URL" \
|
|
20
|
+
> "$OUT/meta.txt" 2>/dev/null || true
|
|
21
|
+
|
|
22
|
+
echo "--- downloaded to $OUT ---"
|
|
23
|
+
ls -la "$OUT"
|
|
24
|
+
echo "title/uploader/duration:"; cat "$OUT/meta.txt" 2>/dev/null || true
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract curated slide frames at full quality and install them into the library _media dir.
|
|
3
|
+
|
|
4
|
+
Usage: extract_slides.py <YTID> <video.mp4> <keep.txt>
|
|
5
|
+
|
|
6
|
+
keep.txt: one timestamp (seconds) per line, the frames you chose from the contact sheet.
|
|
7
|
+
Frames are extracted at 1280px wide, JPEG, numbered in time order, and copied to
|
|
8
|
+
$VIDEO_LIBRARY_DIR/_media/<YTID>-slide-NN.jpg (default ~/video-deepdives/_media)
|
|
9
|
+
|
|
10
|
+
Prints a slides scaffold (idx,t,mmss,img) you can paste into slides.json and then fill
|
|
11
|
+
in title + note for each. idx here is just the sequence number; ordering is by time.
|
|
12
|
+
|
|
13
|
+
The img URL is served by serve.py at /api/video-deepdives/_media/<file>.
|
|
14
|
+
"""
|
|
15
|
+
import subprocess, sys, os, json
|
|
16
|
+
|
|
17
|
+
LIB = os.path.expanduser(os.environ.get("VIDEO_LIBRARY_DIR", "~/video-deepdives"))
|
|
18
|
+
MEDIA = os.path.join(LIB, "_media")
|
|
19
|
+
IMG_PREFIX = "/api/video-deepdives/_media" # served by serve.py
|
|
20
|
+
|
|
21
|
+
def mmss(t):
|
|
22
|
+
t=int(round(float(t))); return f"{t//60:02d}:{t%60:02d}"
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
if len(sys.argv)!=4: sys.exit("usage: extract_slides.py <YTID> <video.mp4> <keep.txt>")
|
|
26
|
+
ytid,video,keep=sys.argv[1],sys.argv[2],sys.argv[3]
|
|
27
|
+
times=sorted({float(l.strip()) for l in open(keep) if l.strip()})
|
|
28
|
+
if not times: sys.exit("keep.txt is empty")
|
|
29
|
+
os.makedirs(MEDIA,exist_ok=True)
|
|
30
|
+
scaffold=[]
|
|
31
|
+
for i,t in enumerate(times,1):
|
|
32
|
+
fn=f"{ytid}-slide-{i:02d}.jpg"
|
|
33
|
+
out=os.path.join(MEDIA,fn)
|
|
34
|
+
subprocess.run(["ffmpeg","-hide_banner","-loglevel","error","-ss",f"{t}",
|
|
35
|
+
"-i",video,"-frames:v","1","-vf","scale=1280:-1","-q:v","3","-y",out],check=True)
|
|
36
|
+
scaffold.append({"idx":i,"t":round(t,1),"mmss":mmss(t),"title":"","note":"",
|
|
37
|
+
"img":f"{IMG_PREFIX}/{fn}"})
|
|
38
|
+
print(f" wrote {fn} @ {mmss(t)}",file=sys.stderr)
|
|
39
|
+
print(f"\nInstalled {len(scaffold)} slides to {MEDIA}",file=sys.stderr)
|
|
40
|
+
print("--- slides.json scaffold on stdout; redirect to a file, then fill in title + note ---",file=sys.stderr)
|
|
41
|
+
print(json.dumps(scaffold,indent=2))
|
|
42
|
+
|
|
43
|
+
if __name__=="__main__": main()
|