openvideokit 0.1.0__tar.gz
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.
- openvideokit-0.1.0/.env.example +97 -0
- openvideokit-0.1.0/.gitignore +55 -0
- openvideokit-0.1.0/AGENTS.md +377 -0
- openvideokit-0.1.0/PKG-INFO +26 -0
- openvideokit-0.1.0/README.md +349 -0
- openvideokit-0.1.0/conftest.py +44 -0
- openvideokit-0.1.0/docs/ai-monitoring.md +362 -0
- openvideokit-0.1.0/docs/ai.md +558 -0
- openvideokit-0.1.0/docs/animation.md +232 -0
- openvideokit-0.1.0/docs/assets.md +171 -0
- openvideokit-0.1.0/docs/chat.md +240 -0
- openvideokit-0.1.0/docs/config.md +148 -0
- openvideokit-0.1.0/docs/legacy/api.md +224 -0
- openvideokit-0.1.0/docs/legacy/architecture.md +95 -0
- openvideokit-0.1.0/docs/legacy/compositions.md +134 -0
- openvideokit-0.1.0/docs/legacy/deployment.md +200 -0
- openvideokit-0.1.0/docs/legacy/development.md +170 -0
- openvideokit-0.1.0/docs/legacy/templates.md +190 -0
- openvideokit-0.1.0/docs/mcp.md +329 -0
- openvideokit-0.1.0/docs/mermaid-check.sh +91 -0
- openvideokit-0.1.0/docs/preview-proxy.md +137 -0
- openvideokit-0.1.0/docs/rfc/0001-product-architecture.md +599 -0
- openvideokit-0.1.0/docs/rfc/0002-ai-subsystem.md +388 -0
- openvideokit-0.1.0/docs/rfc/0003-asset-intelligence.md +570 -0
- openvideokit-0.1.0/docs/rfc/0004-credits-and-billing.md +565 -0
- openvideokit-0.1.0/docs/sandbox.md +174 -0
- openvideokit-0.1.0/docs/security/CONTRIBUTING.md +160 -0
- openvideokit-0.1.0/docs/security/README.md +76 -0
- openvideokit-0.1.0/docs/security/audit-log.md +74 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-001-sandbox-escape.md +134 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-002-caption-js-injection.md +138 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-003-web-fetch-ssrf.md +123 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-004-per-slide-composition-xss.md +101 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-005-script-regex-bypass.md +119 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-006-put-slidehtml-unsanitized.md +131 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-007-agent-self-ssrf.md +88 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-008-svg-stored-xss.md +100 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-009-job-id-traversal.md +97 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-010-chat-record-forgery.md +115 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-011-mcp-token-deadconfig.md +105 -0
- openvideokit-0.1.0/docs/security/findings/OVK-SEC-012-audio-json-texthash.md +87 -0
- openvideokit-0.1.0/docs/security/hardening-notes.md +123 -0
- openvideokit-0.1.0/docs/templates.md +295 -0
- openvideokit-0.1.0/docs/tts.md +340 -0
- openvideokit-0.1.0/docs/web/ai-integration.md +101 -0
- openvideokit-0.1.0/docs/web/api.md +326 -0
- openvideokit-0.1.0/docs/web/architecture.md +190 -0
- openvideokit-0.1.0/docs/web/concurrency.md +143 -0
- openvideokit-0.1.0/docs/web/editor-components.md +88 -0
- openvideokit-0.1.0/docs/web/export.md +307 -0
- openvideokit-0.1.0/docs/web/ssr.md +221 -0
- openvideokit-0.1.0/docs/web/state.md +121 -0
- openvideokit-0.1.0/docs/web/templates.md +258 -0
- openvideokit-0.1.0/pyproject.toml +84 -0
- openvideokit-0.1.0/scripts/dev-proxy.sh +105 -0
- openvideokit-0.1.0/scripts/dev.sh +99 -0
- openvideokit-0.1.0/scripts/gc_assets.py +87 -0
- openvideokit-0.1.0/scripts/generate_templates_mp4.py +88 -0
- openvideokit-0.1.0/scripts/generate_voiceover.py +183 -0
- openvideokit-0.1.0/scripts/migrate_assets.py +79 -0
- openvideokit-0.1.0/scripts/test-e2e.py +284 -0
- openvideokit-0.1.0/scripts/test-mcp.py +429 -0
- openvideokit-0.1.0/scripts/test-release.sh +86 -0
- openvideokit-0.1.0/src/openvideokit/__init__.py +7 -0
- openvideokit-0.1.0/src/openvideokit/__main__.py +6 -0
- openvideokit-0.1.0/src/openvideokit/ai/__init__.py +9 -0
- openvideokit-0.1.0/src/openvideokit/ai/config.py +121 -0
- openvideokit-0.1.0/src/openvideokit/ai/context.py +64 -0
- openvideokit-0.1.0/src/openvideokit/ai/events.py +78 -0
- openvideokit-0.1.0/src/openvideokit/ai/graph.py +35 -0
- openvideokit-0.1.0/src/openvideokit/ai/llm.py +87 -0
- openvideokit-0.1.0/src/openvideokit/ai/monitor/__init__.py +18 -0
- openvideokit-0.1.0/src/openvideokit/ai/monitor/config.py +80 -0
- openvideokit-0.1.0/src/openvideokit/ai/monitor/observer.py +462 -0
- openvideokit-0.1.0/src/openvideokit/ai/monitor/sinks.py +605 -0
- openvideokit-0.1.0/src/openvideokit/ai/monitor/types.py +142 -0
- openvideokit-0.1.0/src/openvideokit/ai/ops.py +205 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/__init__.py +97 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/caption_rules.py +21 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/html_contract.py +30 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/model.py +42 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/project_context.py +58 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/role.py +107 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/safety.py +20 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/templates.py +68 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/tools.py +75 -0
- openvideokit-0.1.0/src/openvideokit/ai/prompts/voice_rules.py +44 -0
- openvideokit-0.1.0/src/openvideokit/ai/providers.py +103 -0
- openvideokit-0.1.0/src/openvideokit/ai/sandbox.py +272 -0
- openvideokit-0.1.0/src/openvideokit/ai/server.py +341 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/__init__.py +88 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/_lint.py +159 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/_registry.py +95 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/_template.py +108 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/_voicelist.py +33 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/_web_async.py +53 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/add_many_slides.py +145 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/add_slide.py +151 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/apply_template.py +83 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/copy_slide_html.py +96 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/duplicate_slide.py +32 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/grep_slides.py +48 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/list_files.py +39 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/list_slides.py +44 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/list_templates.py +57 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/list_voices.py +84 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/read_file.py +61 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/read_many_files.py +58 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/read_templates.py +115 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/remove_many_slides.py +72 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/reorder_slides.py +46 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/run_python_sandbox.py +48 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_caption_settings.py +63 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_caption_style.py +35 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_field.py +42 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_many_durations.py +74 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_many_fields.py +77 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_many_slides_html.py +80 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_many_voiceovers.py +92 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/set_voiceover.py +107 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/tree.py +155 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/web_fetch.py +126 -0
- openvideokit-0.1.0/src/openvideokit/ai/tools/web_search.py +68 -0
- openvideokit-0.1.0/src/openvideokit/app.py +207 -0
- openvideokit-0.1.0/src/openvideokit/assets.py +254 -0
- openvideokit-0.1.0/src/openvideokit/captions.py +425 -0
- openvideokit-0.1.0/src/openvideokit/chats.py +160 -0
- openvideokit-0.1.0/src/openvideokit/cli.py +784 -0
- openvideokit-0.1.0/src/openvideokit/composition.py +177 -0
- openvideokit-0.1.0/src/openvideokit/config.py +153 -0
- openvideokit-0.1.0/src/openvideokit/events.py +61 -0
- openvideokit-0.1.0/src/openvideokit/gsap_validator.py +290 -0
- openvideokit-0.1.0/src/openvideokit/mcp/__init__.py +26 -0
- openvideokit-0.1.0/src/openvideokit/mcp/apply_op.py +318 -0
- openvideokit-0.1.0/src/openvideokit/mcp/persist.py +247 -0
- openvideokit-0.1.0/src/openvideokit/mcp/server.py +262 -0
- openvideokit-0.1.0/src/openvideokit/rendering.py +649 -0
- openvideokit-0.1.0/src/openvideokit/routes.py +745 -0
- openvideokit-0.1.0/src/openvideokit/seed.py +182 -0
- openvideokit-0.1.0/src/openvideokit/stamp.py +45 -0
- openvideokit-0.1.0/src/openvideokit/store.py +579 -0
- openvideokit-0.1.0/src/openvideokit/templates.py +216 -0
- openvideokit-0.1.0/src/openvideokit/templates_preview.py +272 -0
- openvideokit-0.1.0/src/openvideokit/tts_queue.py +193 -0
- openvideokit-0.1.0/src/openvideokit/user_config.py +363 -0
- openvideokit-0.1.0/src/openvideokit/validation.py +104 -0
- openvideokit-0.1.0/src/openvideokit/voiceover.py +362 -0
- openvideokit-0.1.0/src/openvideokit/voices.py +180 -0
- openvideokit-0.1.0/src/openvideokit/watcher.py +97 -0
- openvideokit-0.1.0/templates/slides/ovk/avatar-card/defaults.json +6 -0
- openvideokit-0.1.0/templates/slides/ovk/avatar-card/index.html +56 -0
- openvideokit-0.1.0/templates/slides/ovk/avatar-card/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/bg-overlay/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk/bg-overlay/index.html +48 -0
- openvideokit-0.1.0/templates/slides/ovk/bg-overlay/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/big-stat/defaults.json +4 -0
- openvideokit-0.1.0/templates/slides/ovk/big-stat/index.html +42 -0
- openvideokit-0.1.0/templates/slides/ovk/big-stat/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/center-image/defaults.json +4 -0
- openvideokit-0.1.0/templates/slides/ovk/center-image/index.html +47 -0
- openvideokit-0.1.0/templates/slides/ovk/center-image/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/cooking-step/defaults.json +6 -0
- openvideokit-0.1.0/templates/slides/ovk/cooking-step/index.html +67 -0
- openvideokit-0.1.0/templates/slides/ovk/cooking-step/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/cta/defaults.json +6 -0
- openvideokit-0.1.0/templates/slides/ovk/cta/index.html +52 -0
- openvideokit-0.1.0/templates/slides/ovk/cta/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/default/defaults.json +4 -0
- openvideokit-0.1.0/templates/slides/ovk/default/index.html +38 -0
- openvideokit-0.1.0/templates/slides/ovk/default/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-diamond/defaults.json +10 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-diamond/index.html +66 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-diamond/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-trio/defaults.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-trio/index.html +48 -0
- openvideokit-0.1.0/templates/slides/ovk/gallery-trio/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/grid-quadrant/defaults.json +6 -0
- openvideokit-0.1.0/templates/slides/ovk/grid-quadrant/index.html +72 -0
- openvideokit-0.1.0/templates/slides/ovk/grid-quadrant/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/image-left/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk/image-left/index.html +61 -0
- openvideokit-0.1.0/templates/slides/ovk/image-left/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/image-right/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk/image-right/index.html +61 -0
- openvideokit-0.1.0/templates/slides/ovk/image-right/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/logo-wall/defaults.json +8 -0
- openvideokit-0.1.0/templates/slides/ovk/logo-wall/index.html +48 -0
- openvideokit-0.1.0/templates/slides/ovk/logo-wall/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/product-hero/defaults.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/product-hero/index.html +66 -0
- openvideokit-0.1.0/templates/slides/ovk/product-hero/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/quote/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk/quote/index.html +52 -0
- openvideokit-0.1.0/templates/slides/ovk/quote/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk/side-by-side/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk/side-by-side/index.html +62 -0
- openvideokit-0.1.0/templates/slides/ovk/side-by-side/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-data/bar-chart/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-data/bar-chart/index.html +89 -0
- openvideokit-0.1.0/templates/slides/ovk-data/bar-chart/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-data/line-chart/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-data/line-chart/index.html +77 -0
- openvideokit-0.1.0/templates/slides/ovk-data/line-chart/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-data/progress-list/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-data/progress-list/index.html +95 -0
- openvideokit-0.1.0/templates/slides/ovk-data/progress-list/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-data/stat-counter/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-data/stat-counter/index.html +65 -0
- openvideokit-0.1.0/templates/slides/ovk-data/stat-counter/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/flow-diagram/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/flow-diagram/index.html +95 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/flow-diagram/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/svg-draw/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/svg-draw/index.html +58 -0
- openvideokit-0.1.0/templates/slides/ovk-draw/svg-draw/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-type/kinetic-title/defaults.json +6 -0
- openvideokit-0.1.0/templates/slides/ovk-type/kinetic-title/index.html +78 -0
- openvideokit-0.1.0/templates/slides/ovk-type/kinetic-title/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-type/typewriter/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-type/typewriter/index.html +85 -0
- openvideokit-0.1.0/templates/slides/ovk-type/typewriter/index.json +7 -0
- openvideokit-0.1.0/templates/slides/ovk-type/word-swap/defaults.json +5 -0
- openvideokit-0.1.0/templates/slides/ovk-type/word-swap/index.html +80 -0
- openvideokit-0.1.0/templates/slides/ovk-type/word-swap/index.json +7 -0
- openvideokit-0.1.0/tests/ai/__init__.py +0 -0
- openvideokit-0.1.0/tests/ai/conftest.py +94 -0
- openvideokit-0.1.0/tests/ai/test_events.py +68 -0
- openvideokit-0.1.0/tests/ai/test_graph.py +51 -0
- openvideokit-0.1.0/tests/ai/test_lint.py +135 -0
- openvideokit-0.1.0/tests/ai/test_monitor.py +428 -0
- openvideokit-0.1.0/tests/ai/test_ops.py +164 -0
- openvideokit-0.1.0/tests/ai/test_prompts.py +70 -0
- openvideokit-0.1.0/tests/ai/test_route.py +89 -0
- openvideokit-0.1.0/tests/ai/test_sandbox.py +337 -0
- openvideokit-0.1.0/tests/ai/test_server.py +193 -0
- openvideokit-0.1.0/tests/ai/test_tools_ovk.py +330 -0
- openvideokit-0.1.0/tests/ai/test_tools_read.py +124 -0
- openvideokit-0.1.0/tests/ai/test_tools_templates.py +239 -0
- openvideokit-0.1.0/tests/ai/test_tools_voiceover.py +100 -0
- openvideokit-0.1.0/tests/ai/test_tools_web.py +283 -0
- openvideokit-0.1.0/tests/ai/test_voicelist.py +24 -0
- openvideokit-0.1.0/tests/conftest.py +58 -0
- openvideokit-0.1.0/tests/mcp/test_apply_op.py +256 -0
- openvideokit-0.1.0/tests/mcp/test_persist.py +237 -0
- openvideokit-0.1.0/tests/mcp/test_server.py +181 -0
- openvideokit-0.1.0/tests/security/test_OVK_SEC_001.py +103 -0
- openvideokit-0.1.0/tests/security/test_OVK_SEC_002.py +114 -0
- openvideokit-0.1.0/tests/security/test_OVK_SEC_003.py +55 -0
- openvideokit-0.1.0/tests/security/test_OVK_SEC_006.py +122 -0
- openvideokit-0.1.0/tests/test_assets_routes.py +149 -0
- openvideokit-0.1.0/tests/test_captions.py +264 -0
- openvideokit-0.1.0/tests/test_chats.py +122 -0
- openvideokit-0.1.0/tests/test_cli_config_masking.py +133 -0
- openvideokit-0.1.0/tests/test_composition.py +76 -0
- openvideokit-0.1.0/tests/test_events.py +36 -0
- openvideokit-0.1.0/tests/test_export_routes.py +198 -0
- openvideokit-0.1.0/tests/test_hardening_fixes.py +210 -0
- openvideokit-0.1.0/tests/test_projects_routes.py +191 -0
- openvideokit-0.1.0/tests/test_rendering.py +382 -0
- openvideokit-0.1.0/tests/test_settings_route.py +130 -0
- openvideokit-0.1.0/tests/test_stamp.py +61 -0
- openvideokit-0.1.0/tests/test_store.py +268 -0
- openvideokit-0.1.0/tests/test_templates_routes.py +123 -0
- openvideokit-0.1.0/tests/test_user_config.py +133 -0
- openvideokit-0.1.0/tests/test_validation.py +279 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# OpenVideoKit environment variables — copy to `.env` and fill in.
|
|
3
|
+
# `.env` is gitignored; never commit real keys.
|
|
4
|
+
# All vars are optional unless marked (required).
|
|
5
|
+
#
|
|
6
|
+
# Most users only edit the AI section below (OPENAI_*). Server + Frontend
|
|
7
|
+
# defaults work out of the box.
|
|
8
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
# ── AI (LangGraph agent — see docs/ai.md) ─────────────────────────────────
|
|
11
|
+
# Any OpenAI-compatible endpoint works: OpenAI, OpenRouter, Ollama, vLLM, LM Studio.
|
|
12
|
+
# OpenAI: https://api.openai.com/v1
|
|
13
|
+
# OpenRouter: https://openrouter.ai/api/v1
|
|
14
|
+
# Requesty: https://router.requesty.ai/v1
|
|
15
|
+
# Ollama: http://localhost:11434/v1
|
|
16
|
+
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
17
|
+
# (required to use AI) Your API key for the endpoint above.
|
|
18
|
+
OPENAI_API_KEY=
|
|
19
|
+
# Default chat model id (whatever the endpoint above accepts).
|
|
20
|
+
# OVK_AI_MODEL=gpt-5.4-nano
|
|
21
|
+
# OVK_AI_MODEL=openai/gpt-oss-120b:free
|
|
22
|
+
# OVK_AI_MODEL=google/gemma-4-26b-a4b-it:free
|
|
23
|
+
# OVK_AI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free
|
|
24
|
+
OVK_AI_MODEL=poolside/laguna-xs-2.1:free
|
|
25
|
+
# Reserved for Tier-2 (set_slide_html) coding model; v1 falls back to OVK_AI_MODEL.
|
|
26
|
+
# OVK_AI_TIER2_MODEL=gpt-5.4-nano
|
|
27
|
+
OVK_AI_TIER2_MODEL=openrouter/free
|
|
28
|
+
# Sampling temperature for the agent.
|
|
29
|
+
OVK_AI_TEMPERATURE=0.3
|
|
30
|
+
# Cap on agent tool-calling steps per turn (bounds cost + latency).
|
|
31
|
+
OVK_AI_MAX_STEPS=50
|
|
32
|
+
# Reasoning effort for reasoning-capable models: low / medium / high.
|
|
33
|
+
# Leave empty for non-reasoning models (gpt-4o-mini, etc.) — passing it errors.
|
|
34
|
+
# Set when OVK_AI_MODEL is a reasoning model (gpt-5, o1, o3, gpt-oss, …).
|
|
35
|
+
OVK_AI_REASONING_EFFORT=medium
|
|
36
|
+
|
|
37
|
+
# ── AI Web tools (see docs/ai.md §Web Tools) ──────────────────────────────
|
|
38
|
+
# web_search (DuckDuckGo via ddgs) + web_fetch (crawl4ai/Playwright).
|
|
39
|
+
# The tools are always registered so the agent knows they exist; this flag
|
|
40
|
+
# controls whether a call actually hits the network or returns a "disabled"
|
|
41
|
+
# notice the agent relays to the user.
|
|
42
|
+
# On = each web_fetch launches a headless Chromium. First run also needs:
|
|
43
|
+
# uv run playwright install chromium
|
|
44
|
+
OVK_ENABLE_WEB=true
|
|
45
|
+
|
|
46
|
+
# ── AI monitoring (see docs/ai-monitoring.md) ─────────────────────────────
|
|
47
|
+
# Dump the full LLM context (system prompt + tools + messages + project
|
|
48
|
+
# snapshot) + a live execution trace per run_agent turn to
|
|
49
|
+
# {OVK_DATA_DIR}/{project_id}/monitor/. Defaults ON during initial
|
|
50
|
+
# development; set false in production.
|
|
51
|
+
OVK_AI_MONITORING=true
|
|
52
|
+
# Comma-separated sink list. Only "file" is implemented today; reserved for
|
|
53
|
+
# future sinks (s3, otlp, http, langsmith).
|
|
54
|
+
# OVK_AI_MONITORING_SINKS=file
|
|
55
|
+
# Per-tool-result truncation cap (chars) in trace files. 0 = no truncation.
|
|
56
|
+
# OVK_AI_MONITORING_TOOL_LIMIT=8000
|
|
57
|
+
|
|
58
|
+
# ── Server ────────────────────────────────────────────────────────────────
|
|
59
|
+
OVK_HOST=0.0.0.0
|
|
60
|
+
OVK_PORT=8000
|
|
61
|
+
# Where generated audio + assets + project JSON are written (relative to cwd).
|
|
62
|
+
OVK_DATA_DIR=data
|
|
63
|
+
# Where render job dirs + output MP4s are written. Defaults to {OVK_DATA_DIR}/jobs.
|
|
64
|
+
OVK_JOBS_DIR=data/jobs
|
|
65
|
+
# Where the committed slide-layout templates live (read-only catalog).
|
|
66
|
+
# Defaults to <repo_root>/templates/slides.
|
|
67
|
+
# OVK_TEMPLATES_SLIDES_DIR=templates/slides
|
|
68
|
+
# Max concurrent render subprocesses (1–2 for a laptop).
|
|
69
|
+
OVK_MAX_CONCURRENT_RENDERS=1
|
|
70
|
+
# Chrome workers PER render subprocess (passed to `hyperframes render --workers`).
|
|
71
|
+
OVK_RENDER_HF_WORKERS=3
|
|
72
|
+
# Auto-render preview MP4s for every slide-layout template on `ovk serve`
|
|
73
|
+
# startup. Renders happen sequentially before the server accepts requests.
|
|
74
|
+
# Output is gitignored (templates/slides/**/output.mp4). Set false to skip.
|
|
75
|
+
OVK_TEMPLATES_AUTO_RENDER=true
|
|
76
|
+
# Base URL prepended to the content-key when building media asset URLs
|
|
77
|
+
# (images/video/audio). Default: local serve path (/api/assets/<key>.<ext>).
|
|
78
|
+
# Set absolute (https://bucket.s3.amazonaws.com/assets/) to serve from S3/CDN.
|
|
79
|
+
OVK_ASSETS_BASE_URL=/api/assets/
|
|
80
|
+
|
|
81
|
+
# ── MCP server (see docs/mcp.md) ──────────────────────────────────────────
|
|
82
|
+
# Expose the OVK toolset to external AI agents (Claude Code, Cursor, Gemini
|
|
83
|
+
# CLI) via the Model Context Protocol at /mcp/. Enabled by default.
|
|
84
|
+
# Set false to disable the /mcp endpoint.
|
|
85
|
+
OVK_MCP_ENABLED=true
|
|
86
|
+
# MCP token — NOT YET ENFORCED (OVK-SEC-011). The /mcp endpoint is
|
|
87
|
+
# unauthenticated regardless of this value. Do not rely on it for security.
|
|
88
|
+
# OVK_MCP_TOKEN=
|
|
89
|
+
|
|
90
|
+
# ── Frontend (Vite, ovk-web/) ─────────────────────────────────────────────
|
|
91
|
+
# The browser SPA reads these at build/dev time, not the Python server.
|
|
92
|
+
# Point the SPA at a remote backend instead of the Vite dev proxy.
|
|
93
|
+
VITE_API_BASE_URL=/api
|
|
94
|
+
# Auto-continue the AI agent after you Accept a proposal (sends a receipt
|
|
95
|
+
# with the created/edited slide ids so the AI knows the real state, then
|
|
96
|
+
# auto-fires the next turn). Default on; set false to disable.
|
|
97
|
+
VITE_AI_AUTOCONTINUE=true
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
|
|
10
|
+
# Virtual envs
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
|
|
15
|
+
# Runtime data (created on demand, never commit)
|
|
16
|
+
sessions/
|
|
17
|
+
jobs/
|
|
18
|
+
data/
|
|
19
|
+
*.log
|
|
20
|
+
*.mp4
|
|
21
|
+
*.webm
|
|
22
|
+
*.mov
|
|
23
|
+
|
|
24
|
+
# Generated TTS artifacts (not source)
|
|
25
|
+
templates/*/assets/voiceover.mp3
|
|
26
|
+
templates/*/assets/voiceover_timings.json
|
|
27
|
+
|
|
28
|
+
# HyperFrames render work directories (created by npx hyperframes render)
|
|
29
|
+
templates/**/work-*
|
|
30
|
+
|
|
31
|
+
# OS / editor
|
|
32
|
+
.DS_Store
|
|
33
|
+
.idea/
|
|
34
|
+
.vscode/
|
|
35
|
+
*.swp
|
|
36
|
+
*~
|
|
37
|
+
|
|
38
|
+
# Local secrets / overrides
|
|
39
|
+
.env
|
|
40
|
+
.env.local
|
|
41
|
+
*.local
|
|
42
|
+
|
|
43
|
+
# Build artifacts
|
|
44
|
+
build/
|
|
45
|
+
dist/
|
|
46
|
+
*.egg-info/
|
|
47
|
+
|
|
48
|
+
# Built SPA (produced by `pnpm --filter ovk-web build`, never commit).
|
|
49
|
+
# Vite outputs into static/ (see ovk-web/vite.config.ts → build.outDir).
|
|
50
|
+
static/
|
|
51
|
+
|
|
52
|
+
tmp/
|
|
53
|
+
|
|
54
|
+
# Local act test secrets (never commit)
|
|
55
|
+
scripts/.act.secrets
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
> Guide for AI agents working on OpenVideoKit.
|
|
4
|
+
|
|
5
|
+
## What is this repo?
|
|
6
|
+
|
|
7
|
+
OpenVideoKit is a **scene-based, AI-first video editor**. Users edit slides in a React SPA → a Python SSR server stamps values into self-contained GSAP compositions → `<hyperframes-player>` renders them live → edge-tts generates voiceover → export to MP4 via `npx hyperframes render`. A **LangGraph AI copilot** (BYO key) proposes edits the user accepts or rejects (AI flow == human flow), and an **MCP server** lets any external AI agent (Claude Code, Cursor, Gemini CLI, Goose) drive the same 28-tool API. Inference runs on the user's own machine — the cloud control plane never proxies and never charges.
|
|
8
|
+
|
|
9
|
+
**Tech stack:** Python 3.13, FastAPI, uv, edge-tts, HyperFrames (Node CLI), GSAP, FFmpeg, LangGraph, FastMCP; React 19 + Vite frontend.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv sync --extra dev # install all deps
|
|
15
|
+
cp .env.example .env # then edit .env (set OPENAI_API_KEY for AI)
|
|
16
|
+
uv run openvideokit # serve on http://0.0.0.0:8765
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Environment variables
|
|
20
|
+
|
|
21
|
+
All runtime config is env-driven. A root `.env` (gitignored) is **auto-loaded**
|
|
22
|
+
by `src/openvideokit/config.py` via `python-dotenv` at import time —
|
|
23
|
+
`load_dotenv()` is a silent no-op if `.env` is absent, so the server boots fine
|
|
24
|
+
either way; real env vars always override `.env`. See `.env.example` for the
|
|
25
|
+
full documented list. Groups:
|
|
26
|
+
|
|
27
|
+
| Group | Vars | Where |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Server | `OVK_HOST`, `OVK_PORT`, `OVK_DATA_DIR`, `OVK_JOBS_DIR`, `OVK_TEMPLATES_SLIDES_DIR`, `OVK_STATIC_DIR`, `OVK_ASSETS_BASE_URL`, `OVK_MAX_CONCURRENT_RENDERS`, `OVK_RENDER_HF_WORKERS`, `OVK_TTS_WORKERS`, `OVK_TEMPLATES_AUTO_RENDER` | `config.py` |
|
|
30
|
+
| AI (LangGraph agent) | `OPENAI_BASE_URL`, `OPENAI_API_KEY` (required for AI), `OVK_AI_MODEL` (default `gpt-5.4-nano`), `OVK_AI_TIER2_MODEL`, `OVK_AI_TEMPERATURE`, `OVK_AI_MAX_STEPS`, `OVK_AI_REASONING_EFFORT` (low/medium/high; reasoning models only), `OVK_ENABLE_WEB` | `ai/config.py` (see `docs/ai.md`) |
|
|
31
|
+
| AI monitoring | `OVK_AI_MONITORING` (default `true` during dev), `OVK_AI_MONITORING_SINKS` (default `file`), `OVK_AI_MONITORING_TOOL_LIMIT` (default `8000`) | `ai/monitor/config.py` (see `docs/ai-monitoring.md`) |
|
|
32
|
+
| MCP | `OVK_MCP_ENABLED` (default `true`), `OVK_MCP_TOKEN` (reserved for future auth) | `config.py` (see `docs/mcp.md`) |
|
|
33
|
+
| Frontend (Vite) | `VITE_API_BASE_URL` | `ovk-web/src/shared/config.ts` |
|
|
34
|
+
|
|
35
|
+
Config sources resolve in priority order: **real env vars > `.env` at repo root > `~/.openvideokit/config.toml` > hardcoded defaults**. The TOML file is auto-created on first `ovk serve` (chmod 600, secrets safe) and pre-fills values from existing `.env` so users migrating don't lose settings. See [`docs/config.md`](docs/config.md) for the full philosophy and `ovk config init/get/set/edit/path` CLI commands.
|
|
36
|
+
|
|
37
|
+
`OPENAI_BASE_URL`/`OPENAI_API_KEY` accept any OpenAI-compatible endpoint
|
|
38
|
+
(OpenAI, OpenRouter `https://openrouter.ai/api/v1`, Ollama
|
|
39
|
+
`http://localhost:11434/v1`, vLLM, LM Studio).
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Task | Command |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Run server | `uv run openvideokit` (alias: `uv run ovk serve`) |
|
|
46
|
+
| Init user config | `uv run ovk config init` (writes `~/.openvideokit/config.toml`, chmod 600) |
|
|
47
|
+
| Edit user config | `uv run ovk config edit` (opens `$EDITOR`) |
|
|
48
|
+
| Read a config value | `uv run ovk config get server.port` |
|
|
49
|
+
| Set a config value | `uv run ovk config set server.port 9000` |
|
|
50
|
+
| Lint | `uv run ruff check src scripts` |
|
|
51
|
+
| Lint + fix | `uv run ruff check --fix src scripts` |
|
|
52
|
+
| Unit tests (fast, no render) | `uv run pytest tests/` |
|
|
53
|
+
| Unit tests (all incl. render) | `uv run pytest tests/ --run-slow` |
|
|
54
|
+
| E2E smoke test | `uv run --extra dev python scripts/test-e2e.py` |
|
|
55
|
+
| Full pipeline test | `uv run --extra dev python scripts/test-e2e.py --render` |
|
|
56
|
+
| Standalone voiceover gen | `uv run python scripts/generate_voiceover.py --template eco-bottle --bake` |
|
|
57
|
+
| Render a template | `npx hyperframes render templates/eco-bottle --output out.mp4` |
|
|
58
|
+
| Validate a template | `npx hyperframes lint templates/eco-bottle` |
|
|
59
|
+
|
|
60
|
+
## Code style (indentation — critical)
|
|
61
|
+
|
|
62
|
+
**Always use spaces, never tabs, in every file in this repo.**
|
|
63
|
+
|
|
64
|
+
| Area | Indent | Enforced by |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Python (`src/`, `scripts/`) | **4 spaces** | `ruff format` (`W191` rejects tabs; mixed tabs/spaces → `SyntaxError`) |
|
|
67
|
+
| ovk-web (TS/TSX) | **2 spaces** | Biome (`indentStyle: "space"`, `indentWidth: 2`) |
|
|
68
|
+
|
|
69
|
+
Tabs in Python break the build immediately (`W191` / `unindent does not match any outer indentation level`). When creating or editing a file, write every indented line with the correct number of spaces. Run the formatter before committing (`uv run ruff format` / `pnpm exec biome format --write`).
|
|
70
|
+
|
|
71
|
+
### File writes — ALWAYS atomic
|
|
72
|
+
|
|
73
|
+
**Never use `Path.write_text()` or `open().write()` directly for data files.** Use `store._atomic_write(path, data)` instead:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from .store import _atomic_write
|
|
77
|
+
|
|
78
|
+
# CORRECT — temp file + rename, crash-safe
|
|
79
|
+
_atomic_write(path, json.dumps(data))
|
|
80
|
+
|
|
81
|
+
# WRONG — half-written file on crash
|
|
82
|
+
path.write_text(data)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`_atomic_write` writes to a `.tmp` file first, then `rename()`s it over the target. On POSIX, `rename` is atomic — readers either see the old or new file, never a partial write. This prevents corruption when the server crashes mid-write or when the file watcher reads a half-written file.
|
|
86
|
+
|
|
87
|
+
## Module responsibilities
|
|
88
|
+
|
|
89
|
+
| Module | Owns | Key functions |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `config.py` | Paths, env vars, `JOBS` registry | `ensure_data_dirs()` |
|
|
92
|
+
| `app.py` | FastAPI routes (thin) + MCP mount | `create_app()` |
|
|
93
|
+
| `validation.py` | Identifier regex gates (path-safety) | `validate_project_id()`, `validate_slide_id()` |
|
|
94
|
+
| `templating.py` | Schema I/O, Jinja2 stamping, HTML page generators | `stamp_session()`, `render_editor_page()`, `render_player_page()` |
|
|
95
|
+
| `voiceover.py` | edge-tts pipeline: TTS + silence + concat + timings | `generate_voiceover()`, `generate_voiceover_smart()` |
|
|
96
|
+
| `captions.py` | Word-level karaoke captions + scene transitions | `build_captions()`, `build_caption_timeline_js()`, `build_scene_transitions_js()` |
|
|
97
|
+
| `rendering.py` | `npx hyperframes render` subprocess + job tracking | `start_render()`, `get_job()` |
|
|
98
|
+
| `mcp/apply_op.py` | Pure EditOp reducer (port of ovk-web applyOp.ts) | `apply_op()`, `inverse_op()`, `fold_ops()` |
|
|
99
|
+
| `mcp/persist.py` | Execute-on-call engine: fold ops + persist + TTS | `apply_ops()` |
|
|
100
|
+
| `mcp/server.py` | FastMCP server wrapping 26 ai/tools/* + list_projects + create_project | `create_server()` |
|
|
101
|
+
|
|
102
|
+
## Path safety & identifier validation — critical
|
|
103
|
+
|
|
104
|
+
Every filesystem path under `OVK_DATA_DIR` is built by string-concatenating a `project_id`, `slide_id`, `audio_hash`, asset sha, `chat_id`, or `job_id` into a `Path(...)`. These identifiers arrive from **untrusted sources**: HTTP path params, JSON bodies, LangGraph tool args, and (soon) MCP tool args. A single `..`, `/`, `\`, or null byte escapes the sandbox.
|
|
105
|
+
|
|
106
|
+
### The contract — validated AT the path helper, not just at the route
|
|
107
|
+
|
|
108
|
+
The regex gate lives in `src/openvideokit/validation.py` and is enforced **inside** the path helpers themselves, so no caller can bypass it:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# store.py — gate fires here, regardless of caller
|
|
112
|
+
def _project_dir(project_id: str) -> Path:
|
|
113
|
+
validate_project_id(project_id) # ← raises ValueError
|
|
114
|
+
return _DATA_PATH / project_id
|
|
115
|
+
|
|
116
|
+
def _slide_dir(project_id: str, slide_id: str) -> Path:
|
|
117
|
+
return _slides_dir(project_id) / validate_slide_id(slide_id)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Same pattern in `chats._chats_dir`, `rendering._jobs_meta_path`, `rendering._job_dir`, `assets._blob_path`, `assets._meta_path`, `templates._template_dir`, `ai/context.OVKContext.project_dir`, `ai/monitor/sinks._contexts_dir`. Every one calls a `validate_*` before concat.
|
|
121
|
+
|
|
122
|
+
Routes layer (`routes.py:_valid_*`) translate `ValueError → HTTPException(400)` for clean client errors; the underlying gate is the validator, not the route.
|
|
123
|
+
|
|
124
|
+
### Identifier regexes
|
|
125
|
+
|
|
126
|
+
| Identifier | Regex | Used in |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `project_id` | `^[a-z0-9][a-z0-9-]*$` | directory name |
|
|
129
|
+
| `slide_id` | `^[A-Za-z0-9_-]+$` | directory name under `slides/` |
|
|
130
|
+
| `audio_hash` | `^[a-f0-9]+$` | `audio-<hash>.mp3` filename |
|
|
131
|
+
| `asset sha` | `^[a-f0-9]{64}(\.[A-Za-z0-9]+)?$` | blob filename + optional `.<ext>` |
|
|
132
|
+
| `template segment` | `^[a-z0-9][a-z0-9-]*$` | author/name dir under templates |
|
|
133
|
+
| `chat_id` | `^[A-Za-z0-9_-]+$` | `<id>.jsonl` filename |
|
|
134
|
+
| `job_id` | `^[A-Za-z0-9_-]+$` | directory under `jobs/` |
|
|
135
|
+
|
|
136
|
+
All anchored `^...$`. Reject `..`, separators, whitespace, null bytes, percent-encoding.
|
|
137
|
+
|
|
138
|
+
### When adding a new path helper
|
|
139
|
+
|
|
140
|
+
1. **Never** do `_DATA_PATH / user_input` without validating first. Call the matching `validate_*` from `validation.py`.
|
|
141
|
+
2. If the identifier is new (not in the table above), add a regex + `validate_*` to `validation.py` first.
|
|
142
|
+
3. Reuse existing helpers where possible — `store._project_dir(id)` is preferred over `Path(DATA_DIR) / id` because it's already gated.
|
|
143
|
+
4. The `resolve() + relative_to(base)` check in read tools (`read_file`, `tree`, `list_files`) is **defense-in-depth, not the primary gate** — it cannot detect a bad `project_id` because `base` itself is already wrong by that point. The regex gate fires first.
|
|
144
|
+
|
|
145
|
+
### Tampered-disk robustness
|
|
146
|
+
|
|
147
|
+
`list_projects`, `init_store`, and `_scan_disk` iterate `DATA_DIR` and may encounter malformed dir names (manual tampering / legacy data). They use `store._safe_load(name, loader)` which swallows `ValueError` and skips the entry rather than crashing the whole scan. `_load_from_disk` likewise skips slide ids in `project.json.slides[]` that fail the validator.
|
|
148
|
+
|
|
149
|
+
### Tests
|
|
150
|
+
|
|
151
|
+
- `tests/test_validation.py` — pins every validator + every path helper with the full traversal vector set (`..`, `/`, `\`, null bytes, percent-encoding, etc.) AND the tampered-disk graceful-skip behavior.
|
|
152
|
+
- `tests/test_projects_routes.py::TestPathTraversal` — route-level 4xx behavior (client-visible).
|
|
153
|
+
|
|
154
|
+
## Layout field types
|
|
155
|
+
|
|
156
|
+
Layout `fields` in `template.json` support these types:
|
|
157
|
+
|
|
158
|
+
| Type | Form control | What happens on submit |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `text` | `<textarea>` | `__FIELD_ID__` placeholder replacement in layout HTML |
|
|
161
|
+
| `image` | `<input type="file">` | Byte swap to `assets/`, `__FIELD_ID__` path replacement |
|
|
162
|
+
| `voiceover` | `<textarea>` (amber styled) | **Batch TTS pipeline** — see below |
|
|
163
|
+
|
|
164
|
+
### Slot extras
|
|
165
|
+
|
|
166
|
+
- **`priority`**: Template-level field in `template.json`. Higher number = higher on home page (default 0, sorted alphabetically after prioritized ones).
|
|
167
|
+
- **`caption_style`**: Template-level field. Controls caption CSS: `highlight`, `neon`, `editorial`, `eco-green`.
|
|
168
|
+
- **`default_slides`**: Pre-fills the editor with slides and their default content.
|
|
169
|
+
|
|
170
|
+
## Voiceover pipeline
|
|
171
|
+
|
|
172
|
+
When a template has `voiceover` slots, `stamp_session()` batches ALL of them into one TTS run (edge-tts → ffprobe → ffmpeg concat → `voiceover.mp3` + captions). See **[docs/tts.md](docs/tts.md)** for the full pipeline, voice catalog, default voice policy, and cache layers.
|
|
173
|
+
|
|
174
|
+
## Caption styling — CRITICAL RULES
|
|
175
|
+
|
|
176
|
+
**Never use `transform`, `scale()`, `font-size`, or `text-shadow` changes on `.word--active`.** These cause visual layout shifts ("jumping") that look broken.
|
|
177
|
+
|
|
178
|
+
### The correct caption pattern
|
|
179
|
+
|
|
180
|
+
Caption highlighting uses **GSAP direct color tween** (not CSS class toggling):
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
// CORRECT — smooth color tween, zero layout shift
|
|
184
|
+
tl.to(wordSelector, { color: '#ffea00', duration: 0.15, ease: 'power2.out' }, wordStart);
|
|
185
|
+
tl.to(wordSelector, { color: 'rgba(255,255,255,0.4)', duration: 0.15, ease: 'power2.in' }, wordEnd);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```css
|
|
189
|
+
/* CORRECT — base word style */
|
|
190
|
+
.caption-phrase .word {
|
|
191
|
+
display: inline-block;
|
|
192
|
+
font-size: 48px; font-weight: 800;
|
|
193
|
+
color: rgba(255, 255, 255, 0.4); /* dim white default */
|
|
194
|
+
margin: 0 0.1em;
|
|
195
|
+
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
|
196
|
+
transition: color 0.2s ease; /* smooth fallback */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* CORRECT — emphasis (keyword highlight, static, no animation) */
|
|
200
|
+
.caption-phrase .word--emphasis { color: #4ade80; }
|
|
201
|
+
|
|
202
|
+
/* CORRECT — active state is ONLY a color change */
|
|
203
|
+
.caption-phrase .word--active { color: #ffea00; }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### What NOT to do
|
|
207
|
+
|
|
208
|
+
```css
|
|
209
|
+
/* WRONG — causes layout shift / size jumping */
|
|
210
|
+
.caption-phrase .word--active {
|
|
211
|
+
transform: scale(1.15); /* ← BANNED */
|
|
212
|
+
font-size: 56px; /* ← BANNED */
|
|
213
|
+
text-shadow: 0 0 30px rgba(255,234,0,0.6); /* ← BANNED (causes repaint jumps) */
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* WRONG — GSAP className toggle snaps instantly, doesn't animate */
|
|
217
|
+
tl.to(word, { className: '+=word--active', duration: 0.05 }, start);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Why `className` toggle is banned
|
|
221
|
+
|
|
222
|
+
GSAP's `className` plugin reads computed styles before/after the class change and tweens the diff. This sounds smooth but in practice:
|
|
223
|
+
- It can pick up unintended CSS property changes and animate them
|
|
224
|
+
- The `duration: 0.05` is too short to be visible, making it an instant snap
|
|
225
|
+
- It conflicts with CSS `transition` on the same element
|
|
226
|
+
|
|
227
|
+
**Always use direct property tweens** (`color`, `opacity`) for word highlighting.
|
|
228
|
+
|
|
229
|
+
## Sub-composition model (ALL templates)
|
|
230
|
+
|
|
231
|
+
ALL templates use `"mode": "slide-editor"` with HF's `data-composition-src` to embed slides as separate files. There is no flat model.
|
|
232
|
+
|
|
233
|
+
### Template structure
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
templates/my-template/
|
|
237
|
+
├── template.json ← slide-editor config (mode, layouts, default_slides)
|
|
238
|
+
├── index.html ← root shell (audio + slide host divs + timeline markers)
|
|
239
|
+
├── layouts/ ← sub-composition layout files
|
|
240
|
+
│ └── my-layout.html ← bare <template> with __PLACEHOLDER__ markers
|
|
241
|
+
└── assets/ ← images, audio, fonts
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Layout file format — CRITICAL
|
|
245
|
+
|
|
246
|
+
Layout files MUST be bare `<template>` — **no `<html>` wrapper, no `<body>`, no `data-composition-variables`**.
|
|
247
|
+
|
|
248
|
+
```html
|
|
249
|
+
<template>
|
|
250
|
+
<div data-composition-id="__SLIDE_ID__" data-width="1920" data-height="1080">
|
|
251
|
+
<div class="content">
|
|
252
|
+
<h1>__TITLE__</h1>
|
|
253
|
+
<p>__BODY__</p>
|
|
254
|
+
</div>
|
|
255
|
+
<style>
|
|
256
|
+
[data-composition-id="__SLIDE_ID__"] { background: #0a0a14; }
|
|
257
|
+
[data-composition-id="__SLIDE_ID__"] .content {
|
|
258
|
+
text-align: center;
|
|
259
|
+
padding-top: 38vh;
|
|
260
|
+
}
|
|
261
|
+
[data-composition-id="__SLIDE_ID__"] h1 { font-size: 120px; ... }
|
|
262
|
+
</style>
|
|
263
|
+
<script>
|
|
264
|
+
var tl = gsap.timeline({ paused: true });
|
|
265
|
+
tl.from('[data-composition-id="__SLIDE_ID__"] .content > *', { opacity: 0, y: 40, duration: 0.4, stagger: 0.1 });
|
|
266
|
+
window.__timelines['__SLIDE_ID__'] = tl;
|
|
267
|
+
</script>
|
|
268
|
+
</div>
|
|
269
|
+
</template>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Why bare `<template>` (no `<html>` wrapper)
|
|
273
|
+
|
|
274
|
+
HF's runtime extracts `<template>` content and mounts it. An `<html>` wrapper around `<template>` causes HF to NOT extract the content — the sub-comp renders blank. Verified in HF v0.7.3.
|
|
275
|
+
|
|
276
|
+
### Why NOT `data-variable-values` / `getVariables()`
|
|
277
|
+
|
|
278
|
+
HF's `getVariables()` returns empty `{}` in v0.7.3 sub-compositions. Values are stamped directly into the HTML via `__PLACEHOLDER__` string replacement at stamp time.
|
|
279
|
+
|
|
280
|
+
### Why NOT flex/absolute centering
|
|
281
|
+
|
|
282
|
+
HF's sub-comp mounting context doesn't reliably support `display:flex; align-items:center` or `position:absolute; transform:translate(-50%,-50%)`. Use the official HF pattern: `text-align: center` + `padding-top: XXvh` for vertical positioning. The `vh` unit is viewport-relative and works for any video size.
|
|
283
|
+
|
|
284
|
+
### Placeholder convention
|
|
285
|
+
|
|
286
|
+
- `__SLIDE_ID__` → replaced with `slide-0`, `slide-1`, etc.
|
|
287
|
+
- `__TITLE__` → replaced with user's title text
|
|
288
|
+
- `__BODY__` → replaced with user's body text
|
|
289
|
+
- `__IMAGE__` → replaced with image path (if layout has image field)
|
|
290
|
+
- `__STEP__` → replaced with step number (timeline-step layout)
|
|
291
|
+
|
|
292
|
+
Each field ID is uppercased: field `title` → `__TITLE__`, field `body` → `__BODY__`.
|
|
293
|
+
|
|
294
|
+
### Host div format
|
|
295
|
+
|
|
296
|
+
```html
|
|
297
|
+
<div data-composition-id="slide-0"
|
|
298
|
+
data-composition-src="compositions/slide-0.html"
|
|
299
|
+
data-start="0.5" data-duration="5.0"
|
|
300
|
+
class="clip" style="position:absolute;inset:0;z-index:100;"></div>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Key rules:
|
|
304
|
+
- `position:absolute;inset:0;` is REQUIRED on the host div — without it, sub-comp content collapses to zero size
|
|
305
|
+
- `z-index:100-idx` initially — first slide on top, root timeline swaps z-index at each slide start
|
|
306
|
+
- `class="clip"` for HF's visibility management
|
|
307
|
+
|
|
308
|
+
### Template JSON schema
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"id": "my-template",
|
|
313
|
+
"name": "Display Name",
|
|
314
|
+
"description": "Shown on home page + editor.",
|
|
315
|
+
"duration": 30.0,
|
|
316
|
+
"priority": 50,
|
|
317
|
+
"mode": "slide-editor",
|
|
318
|
+
"caption_style": "highlight",
|
|
319
|
+
"layouts": [
|
|
320
|
+
{
|
|
321
|
+
"id": "my-layout",
|
|
322
|
+
"name": "Layout Name",
|
|
323
|
+
"fields": [
|
|
324
|
+
{"id": "title", "type": "text", "label": "Title", "default": "Hello"},
|
|
325
|
+
{"id": "body", "type": "text", "label": "Body", "default": "World"},
|
|
326
|
+
{"id": "voice", "type": "voiceover", "label": "Voiceover", "default": ""}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
"default_slides": [
|
|
331
|
+
{"layout": "my-layout", "title": "Slide 1", "body": "Content", "voice": "Narration text."}
|
|
332
|
+
],
|
|
333
|
+
"slots": []
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Root index.html markers
|
|
338
|
+
|
|
339
|
+
Place these in `index.html` — `_stamp_slides()` replaces them:
|
|
340
|
+
|
|
341
|
+
```html
|
|
342
|
+
<!-- SLIDES_HERE --> → host divs with data-composition-src
|
|
343
|
+
<!-- CAPTION_LAYER --> → caption HTML (word spans)
|
|
344
|
+
/* CAPTION_CSS */ → caption CSS (style determined by caption_style)
|
|
345
|
+
// SCENE_TRANSITIONS → GSAP z-index swap JS
|
|
346
|
+
// CAPTION_TIMELINE → GSAP word-by-word highlight JS
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Debugging with HF tools
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Check for layout issues (text occlusion, overlap, positioning)
|
|
353
|
+
npx hyperframes inspect sessions/<session_id> --at 2
|
|
354
|
+
|
|
355
|
+
# Capture visual frames for verification
|
|
356
|
+
npx hyperframes snapshot sessions/<session_id> --at 2,5,8 --output snapshots/
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Preview audio
|
|
360
|
+
|
|
361
|
+
The preview page (`render_player_page()`) uses external `<audio>` elements synced to the HyperFrames player via event listeners. This bypasses the player's internal muting.
|
|
362
|
+
|
|
363
|
+
- `ext-music`: music-bed, looped, volume 0.08
|
|
364
|
+
- `ext-voiceover`: voiceover track (if `assets/voiceover.mp3` exists), volume 1.0, not looped
|
|
365
|
+
|
|
366
|
+
Both sync to `_player.currentTime` via `timeupdate` events.
|
|
367
|
+
|
|
368
|
+
## Common pitfalls
|
|
369
|
+
|
|
370
|
+
1. **Never bypass the identifier validators**: Don't write `Path(DATA_DIR) / project_id` directly — `project_id` is untrusted and could be `..`. Always go through `store._project_dir(id)` / `store._slide_dir(pid, sid)` / `chats._chats_dir(id)`, or call `validate_*` from `validation.py` first. The regex gate fires inside the path helpers; bypassing it re-opens path traversal. See "Path safety & identifier validation" above.
|
|
371
|
+
2. **edge-tts voice IDs need `Neural` suffix**: Use `vi-VN-HoaiMyNeural`, NOT `vi-VN-HoaiMy`. Default voice is `en-US-AvaMultilingualNeural` (multilingual). See [docs/tts.md](docs/tts.md).
|
|
372
|
+
3. **asyncio.run() in FastAPI**: Must use thread-based wrapper (`tts_sentence_sync` in `voiceover.py`). Calling `asyncio.run()` directly inside a FastAPI route crashes with `RuntimeError`.
|
|
373
|
+
4. **Google Fonts lint error**: All templates trigger `google_fonts_import` error from HF lint. Pre-existing issue, non-blocking. Use `--strict` only if you've switched to local `@font-face`.
|
|
374
|
+
5. **`shutil.copytree` duplicates everything**: Every form submission copies the entire template dir. Keep templates small or add a session janitor.
|
|
375
|
+
6. **Stamping: never use the string form of `replaceAll`/`replace`**: JavaScript's `String.prototype.replaceAll(token, replacement)` with a **string** replacement interprets `$&` (the matched substring), `$$`, `` $` ``, and `$'` — so a field/slide value containing any of those silently corrupts the stamped output. Always use the **function form** `replaceAll(token, () => value)`, which inserts the value literally. In `ovk-web` this is `stampSafe` (`shared/lib/placeholders.ts`); `stampNaive` (the string form) is kept only as a pinned negative test. Python's `str.replace` is literal (no `$` interpretation) and safe, but always `html_escape` values first.
|
|
376
|
+
7. **SVG validation**: Always validate SVGs as XML before committing. Broken SVGs (duplicate attributes, unclosed tags) silently fail to render in Chrome. Run: `python3 -c "import xml.etree.ElementTree as ET; ET.parse('file.svg')"`.
|
|
377
|
+
8. **Form submit sync**: The slide editor intercepts form submit and reads all textarea values from the DOM via `data-field` attributes. The `syncFromDOM()` function must run before any re-render (add/remove/reorder) to avoid losing typed values.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openvideokit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic video templating pipeline — form-driven, minimal LLM, HyperFrames underneath.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: crawl4ai>=0.9
|
|
7
|
+
Requires-Dist: ddgs>=8.0
|
|
8
|
+
Requires-Dist: edge-tts>=6.1.0
|
|
9
|
+
Requires-Dist: fastapi>=0.138.0
|
|
10
|
+
Requires-Dist: fastmcp>=3.4.2
|
|
11
|
+
Requires-Dist: jinja2>=3.1.6
|
|
12
|
+
Requires-Dist: langchain-core>=0.3
|
|
13
|
+
Requires-Dist: langchain-openai>=0.3
|
|
14
|
+
Requires-Dist: langchain>=0.3
|
|
15
|
+
Requires-Dist: langgraph>=0.4
|
|
16
|
+
Requires-Dist: lxml-html-clean>=0.4.4
|
|
17
|
+
Requires-Dist: nh3>=0.3.6
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0
|
|
19
|
+
Requires-Dist: python-multipart>=0.0.32
|
|
20
|
+
Requires-Dist: tomli-w>=1.0
|
|
21
|
+
Requires-Dist: typer>=0.15.0
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.49.0
|
|
23
|
+
Requires-Dist: watchdog>=6.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: requests>=2.32; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|