oh-my-agent-commandcode 0.0.0-stage → 20.38.3
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/README.md +35 -2
- package/filter-test-output.sh +52 -0
- package/mods/oma.ts +347 -0
- package/package.json +30 -4
- package/skills/_shared/conditional/experiment-ledger.md +28 -0
- package/skills/_shared/conditional/exploration-loop.md +22 -0
- package/skills/_shared/conditional/quality-score.md +33 -0
- package/skills/_shared/core/anti-ai-prose.md +191 -0
- package/skills/_shared/core/api-contracts/README.md +28 -0
- package/skills/_shared/core/api-contracts/template.md +90 -0
- package/skills/_shared/core/clarification-protocol.md +23 -0
- package/skills/_shared/core/code-intelligence.md +46 -0
- package/skills/_shared/core/collaboration-policy.md +28 -0
- package/skills/_shared/core/common-checklist.md +28 -0
- package/skills/_shared/core/context-budget.md +22 -0
- package/skills/_shared/core/context-loading.md +34 -0
- package/skills/_shared/core/difficulty-guide.md +26 -0
- package/skills/_shared/core/execution-policy.md +31 -0
- package/skills/_shared/core/lessons-learned.md +33 -0
- package/skills/_shared/core/prompt-structure.md +18 -0
- package/skills/_shared/core/quality-principles.md +11 -0
- package/skills/_shared/core/session-metrics.md +35 -0
- package/skills/_shared/core/skill-routing.md +39 -0
- package/skills/_shared/core/test-approach.md +54 -0
- package/skills/_shared/core/vendor-detection.md +52 -0
- package/skills/_shared/runtime/memory-protocol.md +118 -0
- package/skills/_shared/runtime/result-contract.md +64 -0
- package/skills/_shared/ui-interaction-quality.md +133 -0
- package/skills/oma-academic-writing/SKILL.md +181 -0
- package/skills/oma-academic-writing/resources/abstract-guide.md +51 -0
- package/skills/oma-academic-writing/resources/academic-verb-tiers.md +42 -0
- package/skills/oma-academic-writing/resources/anti-ai-checklist.md +52 -0
- package/skills/oma-academic-writing/resources/hedging-guide.md +132 -0
- package/skills/oma-academic-writing/resources/sentence-structure-reference.md +151 -0
- package/skills/oma-architecture/SKILL.md +203 -0
- package/skills/oma-architecture/resources/api-evolution.md +41 -0
- package/skills/oma-architecture/resources/checklist.md +42 -0
- package/skills/oma-architecture/resources/execution-protocol.md +115 -0
- package/skills/oma-architecture/resources/methodology-selection.md +92 -0
- package/skills/oma-architecture/resources/migration-patterns.md +28 -0
- package/skills/oma-architecture/resources/output-templates.md +111 -0
- package/skills/oma-architecture/resources/stakeholder-synthesis.md +80 -0
- package/skills/oma-backend/SKILL.md +202 -0
- package/skills/oma-backend/resources/checklist.md +45 -0
- package/skills/oma-backend/resources/error-playbook.md +97 -0
- package/skills/oma-backend/resources/execution-protocol.md +39 -0
- package/skills/oma-backend/resources/orm-reference.md +61 -0
- package/skills/oma-backend/variants/node/api-template.ts +315 -0
- package/skills/oma-backend/variants/node/snippets.md +475 -0
- package/skills/oma-backend/variants/node/stack.yaml +21 -0
- package/skills/oma-backend/variants/node/tech-stack.md +33 -0
- package/skills/oma-backend/variants/python/api-template.py +378 -0
- package/skills/oma-backend/variants/python/snippets.md +199 -0
- package/skills/oma-backend/variants/python/stack.yaml +18 -0
- package/skills/oma-backend/variants/python/tech-stack.md +45 -0
- package/skills/oma-backend/variants/rust/api-template.rs +635 -0
- package/skills/oma-backend/variants/rust/snippets.md +471 -0
- package/skills/oma-backend/variants/rust/stack.yaml +20 -0
- package/skills/oma-backend/variants/rust/tech-stack.md +36 -0
- package/skills/oma-backend/variants/stack.schema.json +73 -0
- package/skills/oma-brainstorm/SKILL.md +166 -0
- package/skills/oma-brainstorm/resources/triz-lite.md +117 -0
- package/skills/oma-coordination/SKILL.md +171 -0
- package/skills/oma-db/SKILL.md +201 -0
- package/skills/oma-db/resources/anti-patterns.md +177 -0
- package/skills/oma-db/resources/checklist.md +67 -0
- package/skills/oma-db/resources/document-templates.md +107 -0
- package/skills/oma-db/resources/error-playbook.md +50 -0
- package/skills/oma-db/resources/examples.md +170 -0
- package/skills/oma-db/resources/execution-protocol.md +127 -0
- package/skills/oma-db/resources/iso-controls.md +98 -0
- package/skills/oma-db/resources/migration-playbook.md +93 -0
- package/skills/oma-db/resources/query-tuning.md +57 -0
- package/skills/oma-db/resources/vector-db.md +159 -0
- package/skills/oma-debug/SKILL.md +146 -0
- package/skills/oma-debug/resources/bug-report-template.md +327 -0
- package/skills/oma-debug/resources/checklist.md +31 -0
- package/skills/oma-debug/resources/common-patterns.md +734 -0
- package/skills/oma-debug/resources/debugging-checklist.md +364 -0
- package/skills/oma-debug/resources/error-playbook.md +94 -0
- package/skills/oma-debug/resources/execution-protocol.md +40 -0
- package/skills/oma-design/SKILL.md +135 -0
- package/skills/oma-design/resources/asset-strategy.md +34 -0
- package/skills/oma-design/resources/checklist.md +63 -0
- package/skills/oma-design/resources/data-grid.md +48 -0
- package/skills/oma-design/resources/design-md-spec.md +107 -0
- package/skills/oma-design/resources/design-tokens.md +53 -0
- package/skills/oma-design/resources/error-playbook.md +18 -0
- package/skills/oma-design/resources/execution-protocol.md +95 -0
- package/skills/oma-design/resources/external-design-skills.md +130 -0
- package/skills/oma-design/resources/getdesign-fetcher.md +104 -0
- package/skills/oma-design/resources/product-interface-review.md +59 -0
- package/skills/oma-design/resources/redesign-protocol.md +52 -0
- package/skills/oma-design/resources/stitch-integration.md +244 -0
- package/skills/oma-dev-workflow/SKILL.md +375 -0
- package/skills/oma-dev-workflow/resources/affected-checks.py +58 -0
- package/skills/oma-dev-workflow/resources/api-workflows.md +58 -0
- package/skills/oma-dev-workflow/resources/database-patterns.md +107 -0
- package/skills/oma-dev-workflow/resources/i18n-patterns.md +63 -0
- package/skills/oma-dev-workflow/resources/release-coordination.md +240 -0
- package/skills/oma-dev-workflow/resources/tooling-profiles.md +217 -0
- package/skills/oma-dev-workflow/resources/troubleshooting.md +78 -0
- package/skills/oma-dev-workflow/resources/validation-pipeline.md +246 -0
- package/skills/oma-docs/SKILL.md +77 -0
- package/skills/oma-docs/resources/commands.md +76 -0
- package/skills/oma-explanation/SKILL.md +212 -0
- package/skills/oma-explanation/resources/document-structure.md +115 -0
- package/skills/oma-explanation/resources/draft-format.md +199 -0
- package/skills/oma-explanation/resources/html-contract.md +102 -0
- package/skills/oma-frontend/SKILL.md +227 -0
- package/skills/oma-frontend/resources/angular-rules.md +110 -0
- package/skills/oma-frontend/resources/checklist.md +76 -0
- package/skills/oma-frontend/resources/error-playbook.md +125 -0
- package/skills/oma-frontend/resources/execution-protocol.md +46 -0
- package/skills/oma-frontend/resources/snippets.md +354 -0
- package/skills/oma-frontend/resources/specialized-visual-implementation.md +193 -0
- package/skills/oma-frontend/resources/tailwind-rules.md +83 -0
- package/skills/oma-frontend/resources/tech-stack.md +156 -0
- package/skills/oma-hwp/SKILL.md +166 -0
- package/skills/oma-hwp/config/hwp-config.yaml +35 -0
- package/skills/oma-hwp/resources/balanced-tables.ts +39 -0
- package/skills/oma-hwp/resources/execution-protocol.md +145 -0
- package/skills/oma-hwp/resources/flatten-tables.ts +101 -0
- package/skills/oma-hwp/resources/package.json +13 -0
- package/skills/oma-hwp/resources/troubleshooting.md +70 -0
- package/skills/oma-hwp/resources/tsconfig.json +10 -0
- package/skills/oma-hwp/resources/turndown-plugin-gfm.d.ts +9 -0
- package/skills/oma-image/SKILL.md +142 -0
- package/skills/oma-image/config/image-config.yaml +70 -0
- package/skills/oma-image/resources/checklist.md +18 -0
- package/skills/oma-image/resources/execution-protocol.md +117 -0
- package/skills/oma-image/resources/invocation.md +105 -0
- package/skills/oma-image/resources/prompt-tips.md +71 -0
- package/skills/oma-image/resources/vendor-matrix.md +90 -0
- package/skills/oma-market/SKILL.md +142 -0
- package/skills/oma-market/resources/checklist.md +12 -0
- package/skills/oma-market/resources/error-playbook.md +12 -0
- package/skills/oma-market/resources/execution-protocol.md +60 -0
- package/skills/oma-market/resources/frameworks/pestel.md +101 -0
- package/skills/oma-market/resources/frameworks/porters-5f.md +90 -0
- package/skills/oma-market/resources/frameworks/swot.md +61 -0
- package/skills/oma-market/resources/intent-rules.md +35 -0
- package/skills/oma-market/resources/output-laws.md +53 -0
- package/skills/oma-mobile/SKILL.md +168 -0
- package/skills/oma-mobile/resources/checklist.md +77 -0
- package/skills/oma-mobile/resources/error-playbook.md +198 -0
- package/skills/oma-mobile/resources/execution-protocol.md +47 -0
- package/skills/oma-mobile/resources/screen-template.dart +331 -0
- package/skills/oma-mobile/resources/screen-template.swift +239 -0
- package/skills/oma-mobile/resources/screen-template.tsx +179 -0
- package/skills/oma-mobile/resources/tech-stack.md +69 -0
- package/skills/oma-mobile/variants/README.md +45 -0
- package/skills/oma-mobile/variants/flutter/api-template.dart +461 -0
- package/skills/oma-mobile/variants/flutter/snippets.md +1353 -0
- package/skills/oma-mobile/variants/flutter/stack.yaml +21 -0
- package/skills/oma-mobile/variants/flutter/tech-stack.md +201 -0
- package/skills/oma-mobile/variants/kotlin-android/api-template.kt +182 -0
- package/skills/oma-mobile/variants/kotlin-android/snippets.md +871 -0
- package/skills/oma-mobile/variants/kotlin-android/stack.yaml +32 -0
- package/skills/oma-mobile/variants/kotlin-android/tech-stack.md +159 -0
- package/skills/oma-mobile/variants/react-native/api-template.ts +531 -0
- package/skills/oma-mobile/variants/react-native/snippets.md +1014 -0
- package/skills/oma-mobile/variants/react-native/stack.yaml +22 -0
- package/skills/oma-mobile/variants/react-native/tech-stack.md +192 -0
- package/skills/oma-mobile/variants/stack.schema.json +68 -0
- package/skills/oma-mobile/variants/swift-ios/api-template.swift +165 -0
- package/skills/oma-mobile/variants/swift-ios/snippets.md +1028 -0
- package/skills/oma-mobile/variants/swift-ios/stack.yaml +25 -0
- package/skills/oma-mobile/variants/swift-ios/tech-stack.md +174 -0
- package/skills/oma-observability/SKILL.md +225 -0
- package/skills/oma-observability/resources/anti-patterns.md +555 -0
- package/skills/oma-observability/resources/boundaries/cross-application.md +348 -0
- package/skills/oma-observability/resources/boundaries/multi-tenant.md +290 -0
- package/skills/oma-observability/resources/boundaries/release.md +372 -0
- package/skills/oma-observability/resources/boundaries/slo.md +225 -0
- package/skills/oma-observability/resources/checklist.md +123 -0
- package/skills/oma-observability/resources/examples.md +448 -0
- package/skills/oma-observability/resources/execution-protocol.md +166 -0
- package/skills/oma-observability/resources/incident-forensics.md +348 -0
- package/skills/oma-observability/resources/intent-rules.md +307 -0
- package/skills/oma-observability/resources/layers/L3-network.md +281 -0
- package/skills/oma-observability/resources/layers/L4-transport.md +299 -0
- package/skills/oma-observability/resources/layers/L7-application/crash-analytics.md +359 -0
- package/skills/oma-observability/resources/layers/L7-application/mobile-rum.md +278 -0
- package/skills/oma-observability/resources/layers/L7-application/waf.md +246 -0
- package/skills/oma-observability/resources/layers/L7-application/web-rum.md +350 -0
- package/skills/oma-observability/resources/layers/mesh.md +368 -0
- package/skills/oma-observability/resources/matrix.md +177 -0
- package/skills/oma-observability/resources/meta-observability.md +605 -0
- package/skills/oma-observability/resources/observability-as-code.md +666 -0
- package/skills/oma-observability/resources/signals/audit.md +310 -0
- package/skills/oma-observability/resources/signals/cost.md +303 -0
- package/skills/oma-observability/resources/signals/logs.md +263 -0
- package/skills/oma-observability/resources/signals/metrics.md +478 -0
- package/skills/oma-observability/resources/signals/privacy.md +351 -0
- package/skills/oma-observability/resources/signals/profiles.md +245 -0
- package/skills/oma-observability/resources/signals/traces.md +360 -0
- package/skills/oma-observability/resources/standards.md +214 -0
- package/skills/oma-observability/resources/transport/collector-topology.md +308 -0
- package/skills/oma-observability/resources/transport/otlp-grpc-vs-http.md +256 -0
- package/skills/oma-observability/resources/transport/sampling-recipes.md +297 -0
- package/skills/oma-observability/resources/transport/udp-statsd-mtu.md +167 -0
- package/skills/oma-observability/resources/vendor-categories.md +352 -0
- package/skills/oma-orchestration/SKILL.md +302 -0
- package/skills/oma-orchestration/config/cli-config.yaml +92 -0
- package/skills/oma-orchestration/resources/memory-schema.md +232 -0
- package/skills/oma-orchestration/resources/subagent-prompt-template.md +30 -0
- package/skills/oma-orchestration/scripts/parallel-run.sh +6 -0
- package/skills/oma-orchestration/scripts/spawn-agent.sh +5 -0
- package/skills/oma-orchestration/scripts/verify.sh +20 -0
- package/skills/oma-orchestration/templates/backend-task.md +19 -0
- package/skills/oma-orchestration/templates/debug-task.md +16 -0
- package/skills/oma-orchestration/templates/frontend-task.md +18 -0
- package/skills/oma-orchestration/templates/mobile-task.md +18 -0
- package/skills/oma-orchestration/templates/qa-task.md +16 -0
- package/skills/oma-orchestration/templates/tasks-example.yaml +15 -0
- package/skills/oma-pdf/SKILL.md +162 -0
- package/skills/oma-pdf/config/pdf-config.yaml +27 -0
- package/skills/oma-pdf/resources/execution-protocol.md +127 -0
- package/skills/oma-pm/SKILL.md +157 -0
- package/skills/oma-pm/resources/error-playbook.md +75 -0
- package/skills/oma-pm/resources/examples.md +439 -0
- package/skills/oma-pm/resources/execution-protocol.md +62 -0
- package/skills/oma-pm/resources/iso-planning.md +110 -0
- package/skills/oma-pm/resources/plan-phase-protocol.md +66 -0
- package/skills/oma-pm/resources/task-template.json +86 -0
- package/skills/oma-qa/SKILL.md +141 -0
- package/skills/oma-qa/resources/checklist.md +339 -0
- package/skills/oma-qa/resources/error-playbook.md +98 -0
- package/skills/oma-qa/resources/execution-protocol.md +169 -0
- package/skills/oma-qa/resources/iso-quality.md +82 -0
- package/skills/oma-qa/resources/verify-ship-protocol.md +116 -0
- package/skills/oma-recap/SKILL.md +245 -0
- package/skills/oma-recap/resources/output-formats.md +83 -0
- package/skills/oma-refactor/SKILL.md +184 -0
- package/skills/oma-refactor/resources/definition.md +108 -0
- package/skills/oma-refactor/resources/governance.md +33 -0
- package/skills/oma-refactor/resources/hotspots.py +44 -0
- package/skills/oma-refactor/resources/measurement.md +93 -0
- package/skills/oma-scholar/SKILL.md +276 -0
- package/skills/oma-scholar/config/scholar-config.yaml +79 -0
- package/skills/oma-scholar/resources/api-endpoints.md +174 -0
- package/skills/oma-scholar/resources/checklist.md +92 -0
- package/skills/oma-scholar/resources/execution-protocol.md +224 -0
- package/skills/oma-scholar/resources/fallback-providers.md +143 -0
- package/skills/oma-scholar/resources/setup-openalex.md +85 -0
- package/skills/oma-scholar/resources/sidecar-spec.md +341 -0
- package/skills/oma-scholar/resources/upstream-spec-cache.md +972 -0
- package/skills/oma-scm/SKILL.md +103 -0
- package/skills/oma-scm/resources/cm-operations.md +56 -0
- package/skills/oma-scm/resources/codeowners-playbook.md +45 -0
- package/skills/oma-scm/resources/conventional-commits.md +108 -0
- package/skills/oma-scm/resources/merge-risk.md +76 -0
- package/skills/oma-scm/resources/onboarding-risk-signals.md +71 -0
- package/skills/oma-search/SKILL.md +125 -0
- package/skills/oma-search/resources/checklist.md +36 -0
- package/skills/oma-search/resources/error-playbook.md +66 -0
- package/skills/oma-search/resources/examples.md +80 -0
- package/skills/oma-search/resources/execution-protocol.md +86 -0
- package/skills/oma-search/resources/intent-rules.md +47 -0
- package/skills/oma-search/resources/trust-registry.md +53 -0
- package/skills/oma-security/SKILL.md +172 -0
- package/skills/oma-security/resources/artex.md +108 -0
- package/skills/oma-security/resources/ci.md +134 -0
- package/skills/oma-security/resources/cisco-source.md +108 -0
- package/skills/oma-security/resources/deepsec-config.md +45 -0
- package/skills/oma-security/resources/deepsec-matchers.md +35 -0
- package/skills/oma-security/resources/deepsec-scanning.md +45 -0
- package/skills/oma-security/resources/deepsec-setup.md +41 -0
- package/skills/oma-security/resources/findings-contract.md +408 -0
- package/skills/oma-security/resources/mcp-scanning.md +122 -0
- package/skills/oma-security/resources/skill-scanning.md +106 -0
- package/skills/oma-security/resources/validation.md +71 -0
- package/skills/oma-skill-creation/SKILL.md +84 -0
- package/skills/oma-skill-creation/resources/prompt-evaluation.md +36 -0
- package/skills/oma-skill-creation/resources/ssl-lite-template.md +108 -0
- package/skills/oma-skill-creation/resources/validation-checklist.md +101 -0
- package/skills/oma-slide/SKILL.md +277 -0
- package/skills/oma-slide/resources/animation-patterns.md +245 -0
- package/skills/oma-slide/resources/assets/deck-stage.js +514 -0
- package/skills/oma-slide/resources/assets/viewport-base.css +257 -0
- package/skills/oma-slide/resources/canva-integration.md +203 -0
- package/skills/oma-slide/resources/checklist.md +46 -0
- package/skills/oma-slide/resources/design-doctrine.md +260 -0
- package/skills/oma-slide/resources/fixed-stage.md +232 -0
- package/skills/oma-slide/resources/generation-protocol.md +393 -0
- package/skills/oma-slide/resources/native-pptx.md +84 -0
- package/skills/oma-slide/resources/selection-index.json +461 -0
- package/skills/oma-slide/resources/style-presets.LICENSE +10 -0
- package/skills/oma-slide/resources/style-presets.md +520 -0
- package/skills/oma-tf-infra/SKILL.md +201 -0
- package/skills/oma-tf-infra/resources/checklist.md +53 -0
- package/skills/oma-tf-infra/resources/cost-optimization.md +574 -0
- package/skills/oma-tf-infra/resources/error-playbook.md +68 -0
- package/skills/oma-tf-infra/resources/examples.md +38 -0
- package/skills/oma-tf-infra/resources/execution-protocol.md +129 -0
- package/skills/oma-tf-infra/resources/iso-42001-infra.md +229 -0
- package/skills/oma-tf-infra/resources/multi-cloud-examples.md +241 -0
- package/skills/oma-tf-infra/resources/policy-testing-examples.md +332 -0
- package/skills/oma-translation/SKILL.md +229 -0
- package/skills/oma-translation/resources/anti-ai-patterns.md +104 -0
- package/skills/oma-translation/resources/lang/_template.md +73 -0
- package/skills/oma-translation/resources/lang/en.md +123 -0
- package/skills/oma-translation/resources/lang/ja.md +274 -0
- package/skills/oma-translation/resources/lang/ko.md +228 -0
- package/skills/oma-translation/resources/lang/zh.md +285 -0
- package/skills/oma-translation/resources/style-skills.md +104 -0
- package/skills/oma-translation/resources/translation-rubric.md +126 -0
- package/skills/oma-video/SKILL.md +229 -0
- package/skills/oma-video/config/video-config.yaml +60 -0
- package/skills/oma-video/resources/checklist.md +26 -0
- package/skills/oma-video/resources/execution-protocol.md +122 -0
- package/skills/oma-video/resources/existing-media.md +185 -0
- package/skills/oma-video/resources/hyperframes-authoring/README.md +44 -0
- package/skills/oma-video/resources/hyperframes-authoring/demo.md +31 -0
- package/skills/oma-video/resources/hyperframes-authoring/explainer.md +10 -0
- package/skills/oma-video/resources/hyperframes-authoring/shorts.md +10 -0
- package/skills/oma-video/resources/motion-principles.md +81 -0
- package/skills/oma-video/resources/mpt/driver.py +301 -0
- package/skills/oma-video/resources/prompt-tips.md +75 -0
- package/skills/oma-video/resources/script-schema.md +81 -0
- package/skills/oma-video/resources/sound-design.md +127 -0
- package/skills/oma-video/resources/strudel/package.json +11 -0
- package/skills/oma-video/resources/strudel/page.html +87 -0
- package/skills/oma-video/resources/strudel/render.mjs +295 -0
- package/skills/oma-video/resources/vendor-matrix.md +124 -0
- package/skills/oma-voice/SKILL.md +249 -0
- package/skills/oma-voice/config/voice-config.yaml +30 -0
- package/skills/oma-voice/resources/audio-processing.md +119 -0
- package/skills/oma-voice/resources/checklist.md +47 -0
- package/skills/oma-voice/resources/execution-protocol.md +215 -0
- package/skills/oma-voice/resources/prompt-tips.md +43 -0
- package/skills/oma-voice/resources/voice-matrix.md +51 -0
- package/skills/oma-voice/resources/voicestudio.md +87 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Prompt Tips
|
|
2
|
+
|
|
3
|
+
A good video brief specifies the **mode**, the **topic/source**, and the
|
|
4
|
+
**arc** (what the viewer should feel/learn). The brief drives the script; the
|
|
5
|
+
script's per-scene `visual.prompt` then drives oma-image (English prompts —
|
|
6
|
+
image models are trained predominantly on English captions).
|
|
7
|
+
|
|
8
|
+
## Brief structure
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Mode + audience → Topic/source → Arc (hook → body → payoff) → Tone/pacing
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Example (shorts): `30s vertical short for a dev audience: how oma-video turns a topic into a finished clip; hook with the pain, show the 3-step flow, end on the one-command CTA; upbeat, fast cuts`
|
|
15
|
+
|
|
16
|
+
## Per-mode guidance
|
|
17
|
+
|
|
18
|
+
### shorts (9:16)
|
|
19
|
+
- **Hook in the first 1.5s** — the first scene's on-screen text must earn the swipe.
|
|
20
|
+
- Keep scenes 2–4s; favor 6–10 scenes for a 30s clip (≤ `max_scenes` 40).
|
|
21
|
+
- oma-image stills stay static (no Ken Burns) — write prompts that frame one clear subject.
|
|
22
|
+
- TikTok captions are centered, static windowed cues (one SRT cue on screen at a time, CSS-wrapped — no per-word animation); keep narration lines short (≤ ~8 words/segment) so cues switch cleanly.
|
|
23
|
+
|
|
24
|
+
### explainer (16:9 / 9:16)
|
|
25
|
+
- Source is a README / code / data — point the brief at the file and the *one* thing to teach.
|
|
26
|
+
- Mix oma-slide frames (structure, bullet beats) with oma-image diagrams (concepts) and code frames.
|
|
27
|
+
- Code frames use one fixed deterministic theme (v1). Keep snippets short and legible at 1920×1080.
|
|
28
|
+
- Narration should explain *why*, not read the code line-by-line.
|
|
29
|
+
- One diagram per scene, about 15–20s each; see `motion-principles.md` for the scene cast and diagram steps.
|
|
30
|
+
|
|
31
|
+
### demo (16:9)
|
|
32
|
+
- The visual is a human-recorded capture (`--capture`). The brief drives the zoom/callout beats and outro.
|
|
33
|
+
- Call out *where to look* on screen — HyperFrames adds zoom + callouts over the capture.
|
|
34
|
+
- Open on the product; add a title card only when the user asks for one.
|
|
35
|
+
|
|
36
|
+
## Per-scene image prompts (oma-image)
|
|
37
|
+
|
|
38
|
+
The script's `scenes[].visual.prompt` is forwarded to oma-image. Write them with
|
|
39
|
+
the same structure oma-image expects:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Scene/backdrop → Subject → Details → Constraints
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Mode | Example per-scene prompt |
|
|
46
|
+
|------|--------------------------|
|
|
47
|
+
| shorts | `Aerial drone shot of Jeju coastline, turquoise water meeting volcanic rock, golden hour, vertical composition with headroom for a slow zoom` |
|
|
48
|
+
| shorts | `Close-up of a hand pouring espresso into a glass over ice, warm cafe light, shallow depth of field` |
|
|
49
|
+
| explainer | `Clean isometric diagram of a 3-stage pipeline (script → assets → render), flat vector, labeled boxes, neutral background` |
|
|
50
|
+
| demo | `Minimal title card: product name centered, dark UI background, subtle accent gradient, 16:9` |
|
|
51
|
+
|
|
52
|
+
## Do's
|
|
53
|
+
|
|
54
|
+
- Anchor the **arc**: hook → body → payoff. A short without a hook gets swiped past.
|
|
55
|
+
- Match **aspect to mode** (9:16 shorts, 16:9 explainer/demo) or use `auto`.
|
|
56
|
+
- Keep narration **per-scene and short** so caption pages and scene boundaries align.
|
|
57
|
+
- Pick **music** that matches pacing (`upbeat` for shorts, `calm` for explainer) — Strudel renders the bed offline and mixes it at −18 dB under narration. Needs a one-time `oma video doctor --install-strudel`; without it the run still succeeds, just silent.
|
|
58
|
+
|
|
59
|
+
## Don'ts
|
|
60
|
+
|
|
61
|
+
- Don't write a brief with no mode and no topic — the agent will have to clarify.
|
|
62
|
+
- Don't request paid stock/AIGC visuals without the key — the chain silently falls back to oma-image stills (annotated in `warnings`), which may surprise you.
|
|
63
|
+
- Don't exceed `max_duration_sec` (180) or `max_scenes` (40).
|
|
64
|
+
- Don't put secrets in the brief unless `--no-brief-in-manifest` is set.
|
|
65
|
+
|
|
66
|
+
## Localization
|
|
67
|
+
|
|
68
|
+
- Narration + on-screen text are authored in `--locale`. For a non-source locale, oma-translation translates the script text before TTS/captions (key-free). If the translator is absent, the source text is kept and a warning is recorded.
|
|
69
|
+
- oma-image prompts are sent in **English** regardless of `--locale`; translate the user's subject and show the translated prompt during amplification.
|
|
70
|
+
|
|
71
|
+
## Determinism
|
|
72
|
+
|
|
73
|
+
- `--seed <n>` makes the render reproducible. The same `render-spec.json` + assets + seed + embedded Pretendard font produce a byte-stable render. The font is fetched once by `oma video doctor --install`; if it is missing (e.g. the fetch ran offline), the render gracefully falls back to system fonts and byte-identical output is only guaranteed once the font is present.
|
|
74
|
+
- Re-render an existing run with `oma video render <runDir>` — it consumes `render-spec.json` only, so script/voice/visual generation is not re-run.
|
|
75
|
+
- `OMA_VIDEO_MOCK=1` replays golden fixtures for deterministic tests.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# script.json Schema — agent-as-key authoring reference
|
|
2
|
+
|
|
3
|
+
`oma video generate --script <path>` hard-validates the file against this
|
|
4
|
+
schema (mirrors `cli/commands/video/types.ts` `ScriptSchema`). A mismatch is
|
|
5
|
+
exit 4 (invalid-input) with the offending field named. **`schemaVersion` is
|
|
6
|
+
required** — omitting it is the most common authoring failure.
|
|
7
|
+
|
|
8
|
+
## Fields
|
|
9
|
+
|
|
10
|
+
| Field | Type | Required | Notes |
|
|
11
|
+
|-------|------|:---:|-------|
|
|
12
|
+
| `schemaVersion` | `"1.0"` (literal) | ✅ | exactly the string `"1.0"` |
|
|
13
|
+
| `mode` | `shorts` \| `explainer` \| `demo` | ✅ | must match the run's `--mode` |
|
|
14
|
+
| `aspect` | `9:16` \| `16:9` \| `1:1` | ✅ | concrete value — `auto` is a CLI flag, not a script value |
|
|
15
|
+
| `locale` | string (min 1) | ✅ | narration/caption language tag, e.g. `en`, `ko` |
|
|
16
|
+
| `title` | string (min 1) | ✅ | also drives the output filename slug: `<mode>-<slug>.mp4` |
|
|
17
|
+
| `scenes` | array (min 1, ≤ 40) | ✅ | see per-scene fields below |
|
|
18
|
+
| `music` | `upbeat` \| `calm` \| `cinematic` \| `lofi` \| `piano` \| `none` | ✅ | drives the Strudel BGM bed; mixed at −18 dB under narration |
|
|
19
|
+
| `brand` | object | — | free-form; defaults to `{}` |
|
|
20
|
+
|
|
21
|
+
### Per-scene fields (`scenes[]`)
|
|
22
|
+
|
|
23
|
+
| Field | Type | Required | Notes |
|
|
24
|
+
|-------|------|:---:|-------|
|
|
25
|
+
| `id` | string (min 1) | ✅ | convention: `scene-01`, `scene-02`, … |
|
|
26
|
+
| `durationSec` | number > 0 | ✅ | sum ≤ 180 (`limits.max_duration_sec`); also anchors estimated caption timing |
|
|
27
|
+
| `narration` | string | ✅ | may be `""` for a silent scene; keep ≤ ~8 words/segment for clean caption cues |
|
|
28
|
+
| `onScreenText` | string[] | — | overlay lines; defaults to `[]` |
|
|
29
|
+
| `visual` | object | ✅ | see below |
|
|
30
|
+
| `visual.kind` | `still` \| `clip` \| `mixed` \| `slide` \| `capture` | ✅ | `still` (oma-image), `slide` (oma-slide, explainer), `capture` (demo footage), `clip`/`mixed` (stock/AIGC) |
|
|
31
|
+
| `visual.prompt` | string | — | English image prompt forwarded to oma-image (see `prompt-tips.md`) |
|
|
32
|
+
| `visual.ref` | string | — | reference to an existing asset (e.g. capture segment) |
|
|
33
|
+
| `visual.source` | string | — | provenance marker (e.g. `agent-authored`) |
|
|
34
|
+
| `transition` | string | — | transition-out hint |
|
|
35
|
+
|
|
36
|
+
## Minimal valid example
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"schemaVersion": "1.0",
|
|
41
|
+
"mode": "shorts",
|
|
42
|
+
"aspect": "9:16",
|
|
43
|
+
"locale": "en",
|
|
44
|
+
"title": "Jeju coffee in 30 seconds",
|
|
45
|
+
"music": "none",
|
|
46
|
+
"brand": {},
|
|
47
|
+
"scenes": [
|
|
48
|
+
{
|
|
49
|
+
"id": "scene-01",
|
|
50
|
+
"durationSec": 3,
|
|
51
|
+
"narration": "Jeju's coffee scene is exploding.",
|
|
52
|
+
"onScreenText": ["JEJU ☕"],
|
|
53
|
+
"visual": {
|
|
54
|
+
"kind": "still",
|
|
55
|
+
"prompt": "Aerial drone shot of Jeju coastline at golden hour, vertical composition with headroom",
|
|
56
|
+
"source": "agent-authored"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "scene-02",
|
|
61
|
+
"durationSec": 4,
|
|
62
|
+
"narration": "Volcanic soil, ocean air, and a cafe on every corner.",
|
|
63
|
+
"onScreenText": [],
|
|
64
|
+
"visual": {
|
|
65
|
+
"kind": "still",
|
|
66
|
+
"prompt": "Close-up of a hand pouring espresso over ice in a seaside cafe, warm light, shallow depth of field",
|
|
67
|
+
"source": "agent-authored"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Authoring checklist
|
|
75
|
+
|
|
76
|
+
- [ ] `schemaVersion: "1.0"` present (required literal).
|
|
77
|
+
- [ ] `aspect` is concrete (`9:16` / `16:9` / `1:1`) — never `auto`.
|
|
78
|
+
- [ ] Scene count ≤ 40, total `durationSec` ≤ 180.
|
|
79
|
+
- [ ] Every scene has a `visual.kind` from the enum; `still` scenes carry an English `visual.prompt`.
|
|
80
|
+
- [ ] `music` is set (use `"none"` unless the user asked for a bed).
|
|
81
|
+
- [ ] Validate cheaply before rendering: pass the file via `--script` with `--dry-run` first.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Sound Effects and Soundtrack Listening
|
|
2
|
+
|
|
3
|
+
Use this branch when a video needs sound effects, foley, ambience or a
|
|
4
|
+
sound-designed soundtrack, or when the audio of a rendered or supplied video
|
|
5
|
+
must be judged: sync to picture, loudness, mix, or a choice between candidate
|
|
6
|
+
sounds. Narration, transcription and TTS stay with `oma-voice`; the Strudel
|
|
7
|
+
music bed stays with the creation pipeline. Sound effects are off unless the
|
|
8
|
+
brief asks for them or the user accepts a proposal.
|
|
9
|
+
|
|
10
|
+
## Route to the upstream skill
|
|
11
|
+
|
|
12
|
+
`Leonxlnx/earshot` ships two skills. Route each request to the one that
|
|
13
|
+
supports it:
|
|
14
|
+
|
|
15
|
+
| Request | Upstream skill | Upstream section |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Effects for a generated video (ticks, pops, whooshes, hits, typing, a lockup) | `earshot` | Loop 5: `sfx` / `kit` |
|
|
18
|
+
| Choose, trim or compare candidate sound files | `earshot` | Loop 1, Loop 3 |
|
|
19
|
+
| Review a video's audio: events, sync, loudness, launch-film scorecard | `earshot` | Loop 2 |
|
|
20
|
+
| Make a new sound in code, only when explicitly requested | `earshot` | Loop 4: `forge` |
|
|
21
|
+
| A soundtrack from real licensed recordings (foley, ambience, a licensed music track) | `real-sound` | its full process |
|
|
22
|
+
|
|
23
|
+
The shipped bank is made in code, not recorded. When the brief asks for
|
|
24
|
+
recorded or real sound, route to `real-sound`; never present a bank take as a
|
|
25
|
+
recording.
|
|
26
|
+
|
|
27
|
+
1. Check the runtime skill catalog for `earshot` or `real-sound` from
|
|
28
|
+
`https://github.com/Leonxlnx/earshot`. Reuse its advertised installed
|
|
29
|
+
location and read its complete `SKILL.md` when available.
|
|
30
|
+
2. If it is absent, load it without a persistent installation or another agent:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bunx skills use Leonxlnx/earshot@earshot
|
|
34
|
+
bunx skills use Leonxlnx/earshot@real-sound
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Omit `--agent`. Read the emitted instructions and use the actual
|
|
38
|
+
supporting-files directory from the result for every relative path.
|
|
39
|
+
`real-sound` needs its complete `scripts/`, `templates/` and `references/`.
|
|
40
|
+
3. A loaded skill is instructions, not the runtime. Resolve earshot in this
|
|
41
|
+
order: an already-connected earshot MCP server (discover its actual tools
|
|
42
|
+
before calling them), `EARSHOT_PYTHON`, `earshot` on PATH, then a Python
|
|
43
|
+
where `import earshot` succeeds. FFmpeg/FFprobe must be on PATH. Record the
|
|
44
|
+
resolved interpreter, the earshot version and the bank version from
|
|
45
|
+
`kit.lock.json`.
|
|
46
|
+
4. If no runtime resolves, report the missing capability with the upstream
|
|
47
|
+
setup steps and stop this branch. Do not clone the repository, create a
|
|
48
|
+
virtual environment, install torch or fetch models without the user's
|
|
49
|
+
consent. The video can still render without effects; say so in the report.
|
|
50
|
+
5. Follow the loaded upstream instructions for commands, flags, kit schema,
|
|
51
|
+
presets, roles, alignment, levels and QA thresholds. Do not copy its cookbook
|
|
52
|
+
into OMA resources.
|
|
53
|
+
|
|
54
|
+
`sfx`, `kit` and `measure` load no models. `describe`, `match`, `rank`, `find`,
|
|
55
|
+
`realness`, `timeline`, `sheet` and `critique` load CLAP and AST (about 1.1 GB
|
|
56
|
+
on first use); for more than a few calls, use the MCP server or one Python
|
|
57
|
+
process. `earshot llm` uploads audio to Google: run it only when a Gemini key is
|
|
58
|
+
set and the user explicitly agreed to the upload. Never pass `--yes-upload` or
|
|
59
|
+
`consent=true` on your own.
|
|
60
|
+
|
|
61
|
+
## Bank effects in an OMA video
|
|
62
|
+
|
|
63
|
+
OMA owns the cue times and the composition; earshot owns the voices.
|
|
64
|
+
|
|
65
|
+
1. Take cue times from the picture: scene boundaries in `render-spec.json`
|
|
66
|
+
(`fromFrame / fps`), on-screen text entrances, transitions and the authored
|
|
67
|
+
GSAP tween times in `hyperframes/index.html`. Write them in the upstream cue
|
|
68
|
+
format at `<runDir>/audio/sfx/cues.json`. Follow the upstream restraint
|
|
69
|
+
rules; hard cuts and cursor motion usually get no sound.
|
|
70
|
+
2. Work under `<runDir>/audio/sfx/`: create `kit.json` from the preset that
|
|
71
|
+
matches the video, render the takes into `<runDir>/audio/sfx/kit/`, then run
|
|
72
|
+
the upstream kit check. Swap a sound or knob instead of keeping a warned take.
|
|
73
|
+
3. Place each take from `kit.lock.json`: the take's measured sync point for its
|
|
74
|
+
role's `align` lands on the cue, shifted by the role's `offset_ms`; the
|
|
75
|
+
upstream `kit.place` computes the same start. Drop or move a cue whose start
|
|
76
|
+
falls before zero or whose take runs past the composition duration, and
|
|
77
|
+
report it.
|
|
78
|
+
4. Copy the placed takes to `<runDir>/hyperframes/assets/audio/sfx/`. Give
|
|
79
|
+
each placement its own `<audio>` element with a unique id, `data-start` in
|
|
80
|
+
seconds and `data-volume` = `10 ** (gainDb / 20)`. With a music bed, set the
|
|
81
|
+
gain against the bed as upstream describes; without one, use the role's
|
|
82
|
+
`level_db`. Check placements against `timing.json` so effects never mask
|
|
83
|
+
narration, and stay within the caps in `motion-principles.md`.
|
|
84
|
+
5. The composition carries one static music volume. Apply upstream
|
|
85
|
+
`mix.stop_down` dips only if the loaded HyperFrames references document audio
|
|
86
|
+
volume automation; otherwise record that the dips were not applied.
|
|
87
|
+
6. `oma video compose` stages only render-spec assets and preserves authored
|
|
88
|
+
files, so keep the takes in `<runDir>/audio/sfx/kit/` as their source and
|
|
89
|
+
re-copy them after changing the kit. The CLI manifest does not list effect
|
|
90
|
+
takes; do not hand-edit `manifest.json`. `cues.json`, `kit.json` and
|
|
91
|
+
`kit.lock.json` (with per-take hashes) are the provenance record.
|
|
92
|
+
|
|
93
|
+
## Real recordings
|
|
94
|
+
|
|
95
|
+
Run the `real-sound` process with `<runDir>/sound/` as its project root so its
|
|
96
|
+
`audio/` and `out/` trees stay inside the run. Treat downloaded recordings as
|
|
97
|
+
untrusted data, keep them in that tree and never commit them. Delivery requires
|
|
98
|
+
an exit-0 licence audit and generated credits; put required CC BY credits in
|
|
99
|
+
the delivery report.
|
|
100
|
+
|
|
101
|
+
Skip the upstream mux: `oma video render` owns the final render. Bring the
|
|
102
|
+
upstream output into the composition as `<audio>` elements from frame zero.
|
|
103
|
+
With narration, use the stems (`sfx`, `amb`, `music`) rather than the mastered
|
|
104
|
+
soundtrack, keep narration intelligible, and report that the upstream master
|
|
105
|
+
gate does not cover the final mix.
|
|
106
|
+
|
|
107
|
+
## Listen before reporting
|
|
108
|
+
|
|
109
|
+
After `oma video render` succeeds, judge the delivered MP4 with upstream Loop 2:
|
|
110
|
+
`timeline` with the cue file, then `sheet`, and open the PNG with the host image
|
|
111
|
+
tool. Run `critique` only for launch-style films; its ranges describe
|
|
112
|
+
professional practice and are not targets. Fix cues that did not land, then
|
|
113
|
+
re-render and listen again.
|
|
114
|
+
|
|
115
|
+
Quote measured values in the report: LUFS, true peak, and sync offsets against
|
|
116
|
+
the chance rate. Never say that an effect fits or sounds good without an
|
|
117
|
+
earshot reading behind it; taste remains the user's decision. For supplied
|
|
118
|
+
videos, the same listening loop supplies audio evidence for the existing-media
|
|
119
|
+
branch.
|
|
120
|
+
|
|
121
|
+
## References
|
|
122
|
+
|
|
123
|
+
- [Upstream earshot](https://github.com/Leonxlnx/earshot): `skills/earshot`,
|
|
124
|
+
`skills/real-sound`, `docs/SFX.md` (bank, kit and lock schema) and
|
|
125
|
+
`docs/INTEGRATION.md` (cue file and placement bridge).
|
|
126
|
+
- [Skills CLI use](https://github.com/vercel-labs/skills/blob/main/docs/commands.md#skills-use):
|
|
127
|
+
prompt and complete support-directory loading without an installed skill.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oma-video-strudel",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Vendored Strudel BGM renderer for oma-video. Deps are installed on demand by `oma video doctor --install-strudel`. @strudel/* is AGPL-3.0-or-later and is NEVER imported by the MIT-licensed oma CLI — render.mjs runs as a subprocess (same boundary as the Remotion projects).",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@strudel/web": "^1.3.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>oma-video strudel offline render</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<!-- Served by render.mjs from the resolved @strudel/web dist. The IIFE
|
|
9
|
+
bundle exposes everything on `window.strudel` + `window.initStrudel`. -->
|
|
10
|
+
<script src="/strudel.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
window.__omaLog = [];
|
|
13
|
+
window.addEventListener('error', (e) =>
|
|
14
|
+
window.__omaLog.push('error: ' + e.message),
|
|
15
|
+
);
|
|
16
|
+
window.addEventListener('unhandledrejection', (e) =>
|
|
17
|
+
window.__omaLog.push('rejection: ' + (e.reason?.message ?? e.reason)),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render `code` to a WAV and stash it on window as base64.
|
|
22
|
+
*
|
|
23
|
+
* `strudel.renderPatternAudio()` is a real OfflineAudioContext render
|
|
24
|
+
* (faster than realtime, no audio device involved) but it delivers the
|
|
25
|
+
* finished wav as a browser DOWNLOAD. Rather than plumb a download
|
|
26
|
+
* directory through CDP, we intercept URL.createObjectURL to capture the
|
|
27
|
+
* Blob in-page and hand the bytes back over the debugger protocol.
|
|
28
|
+
*
|
|
29
|
+
* Returns the base64 length; the driver pulls window.__omaWav in chunks.
|
|
30
|
+
*/
|
|
31
|
+
window.__omaRender = async function (code, seconds, cps, sampleRate) {
|
|
32
|
+
await window.initStrudel();
|
|
33
|
+
|
|
34
|
+
// repl.evaluate(code, autostart=false) transpiles + evaluates without
|
|
35
|
+
// starting the realtime scheduler, and returns the Pattern itself.
|
|
36
|
+
const evaluated = await window.strudel.evaluate(code, false);
|
|
37
|
+
const pattern = evaluated?.pattern ?? evaluated;
|
|
38
|
+
if (!pattern || typeof pattern.queryArc !== 'function') {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'strudel: pattern code did not evaluate to a Pattern (got ' +
|
|
41
|
+
typeof pattern +
|
|
42
|
+
')',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let captured = null;
|
|
47
|
+
const realCreate = URL.createObjectURL.bind(URL);
|
|
48
|
+
const realRevoke = URL.revokeObjectURL.bind(URL);
|
|
49
|
+
URL.createObjectURL = (blob) => {
|
|
50
|
+
captured = blob;
|
|
51
|
+
return 'blob:oma-captured';
|
|
52
|
+
};
|
|
53
|
+
URL.revokeObjectURL = () => {};
|
|
54
|
+
try {
|
|
55
|
+
await window.strudel.renderPatternAudio(
|
|
56
|
+
pattern,
|
|
57
|
+
cps,
|
|
58
|
+
0,
|
|
59
|
+
seconds * cps,
|
|
60
|
+
sampleRate,
|
|
61
|
+
128,
|
|
62
|
+
false,
|
|
63
|
+
'oma-bgm',
|
|
64
|
+
);
|
|
65
|
+
} finally {
|
|
66
|
+
URL.createObjectURL = realCreate;
|
|
67
|
+
URL.revokeObjectURL = realRevoke;
|
|
68
|
+
}
|
|
69
|
+
if (!captured) {
|
|
70
|
+
throw new Error('strudel: renderPatternAudio produced no audio blob');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const bytes = new Uint8Array(await captured.arrayBuffer());
|
|
74
|
+
let binary = '';
|
|
75
|
+
const CHUNK = 0x8000; // stay under the String.fromCharCode arg limit
|
|
76
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
77
|
+
binary += String.fromCharCode.apply(
|
|
78
|
+
null,
|
|
79
|
+
bytes.subarray(i, i + CHUNK),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
window.__omaWav = btoa(binary);
|
|
83
|
+
return window.__omaWav.length;
|
|
84
|
+
};
|
|
85
|
+
</script>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Strudel BGM renderer — boundary-safe subprocess entrypoint.
|
|
3
|
+
//
|
|
4
|
+
// oma-video NEVER imports Strudel. @strudel/* is AGPL-3.0-or-later while the
|
|
5
|
+
// oma CLI is MIT, so the TypeScript provider (`providers/music-strudel.ts` +
|
|
6
|
+
// `internal/strudel-project.ts`) only ever LOCATES this project on disk and
|
|
7
|
+
// spawns *this* script. Same boundary the Remotion projects use.
|
|
8
|
+
//
|
|
9
|
+
// MECHANISM: `@strudel/webaudio.renderPatternAudio()` renders a pattern through
|
|
10
|
+
// an OfflineAudioContext — faster than realtime, no audio device, no autoplay
|
|
11
|
+
// gesture. A 30s bed renders in well under a second. The bundle only runs in a
|
|
12
|
+
// browser, so we drive a headless Chrome over raw CDP (Node's global WebSocket;
|
|
13
|
+
// zero npm deps here beyond @strudel/web itself) and serve the bundle from a
|
|
14
|
+
// loopback-only HTTP server.
|
|
15
|
+
//
|
|
16
|
+
// DETERMINISM: oscillator sounds (sine/triangle/square/sawtooth) render
|
|
17
|
+
// byte-identically across runs. Noise-based sounds (white/pink/brown) do NOT —
|
|
18
|
+
// superdough fills those buffers from Math.random(). Patterns that must be
|
|
19
|
+
// reproducible should stay oscillator-only.
|
|
20
|
+
//
|
|
21
|
+
// Contract — flags in, ONE JSON result line out (stdout's LAST line):
|
|
22
|
+
// IN --pattern-file <path> file holding the strudel pattern code (required)
|
|
23
|
+
// --out <abs wav path> output wav, inside a run dir (required)
|
|
24
|
+
// --chrome <path> Chrome/Chromium executable (required)
|
|
25
|
+
// --seconds <float> bed length in seconds (required)
|
|
26
|
+
// --cps <float> cycles per second (default 0.5)
|
|
27
|
+
// --sample-rate <int> default 44100
|
|
28
|
+
// --timeout <ms> hard ceiling for the whole render (default 120000)
|
|
29
|
+
// OUT {"ok":true,"output":"<abs wav>","seconds":<f>,"bytes":<n>,"sha256":"<hex>"}
|
|
30
|
+
// | {"ok":false,"error":"<message>","code":"<reason>"}
|
|
31
|
+
//
|
|
32
|
+
// Exit code is 0 on success, 1 on failure; the JSON result line is authoritative.
|
|
33
|
+
import { spawn } from "node:child_process";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
36
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
37
|
+
import { createServer } from "node:http";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import process from "node:process";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
|
|
43
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const DIST = path.join(HERE, "node_modules", "@strudel", "web", "dist");
|
|
45
|
+
|
|
46
|
+
const MIME = {
|
|
47
|
+
".js": "text/javascript",
|
|
48
|
+
".mjs": "text/javascript",
|
|
49
|
+
".html": "text/html",
|
|
50
|
+
".json": "application/json",
|
|
51
|
+
".wasm": "application/wasm",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let server;
|
|
55
|
+
let chrome;
|
|
56
|
+
let userDataDir;
|
|
57
|
+
let hardTimer;
|
|
58
|
+
|
|
59
|
+
function fail(message, code = "render_failed") {
|
|
60
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message, code })}\n`);
|
|
61
|
+
cleanup().finally(() => process.exit(1));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function cleanup() {
|
|
65
|
+
if (hardTimer) clearTimeout(hardTimer);
|
|
66
|
+
try {
|
|
67
|
+
chrome?.kill("SIGTERM");
|
|
68
|
+
} catch {}
|
|
69
|
+
try {
|
|
70
|
+
server?.close();
|
|
71
|
+
} catch {}
|
|
72
|
+
if (userDataDir) {
|
|
73
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseArgs(argv) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const token = argv[i];
|
|
81
|
+
if (!token.startsWith("--")) continue;
|
|
82
|
+
const eq = token.indexOf("=");
|
|
83
|
+
if (eq !== -1) {
|
|
84
|
+
out[token.slice(2, eq)] = token.slice(eq + 1);
|
|
85
|
+
} else {
|
|
86
|
+
out[token.slice(2)] = argv[i + 1]?.startsWith("--") ? "" : argv[++i];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const args = parseArgs(process.argv.slice(2));
|
|
93
|
+
const patternFile = args["pattern-file"];
|
|
94
|
+
const outPath = args.out;
|
|
95
|
+
const chromePath = args.chrome;
|
|
96
|
+
const seconds = Number(args.seconds);
|
|
97
|
+
const cps = Number(args.cps ?? 0.5);
|
|
98
|
+
const sampleRate = Number(args["sample-rate"] ?? 44100);
|
|
99
|
+
const timeoutMs = Number(args.timeout ?? 120000);
|
|
100
|
+
|
|
101
|
+
if (!patternFile || !outPath || !chromePath) {
|
|
102
|
+
fail("missing required flag: --pattern-file / --out / --chrome", "bad_args");
|
|
103
|
+
} else if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
104
|
+
fail(`--seconds must be a positive number (got ${args.seconds})`, "bad_args");
|
|
105
|
+
} else if (!existsSync(DIST)) {
|
|
106
|
+
fail(
|
|
107
|
+
`@strudel/web is not installed in ${HERE} — run \`oma video doctor --install-strudel\``,
|
|
108
|
+
"not_installed",
|
|
109
|
+
);
|
|
110
|
+
} else if (!existsSync(chromePath)) {
|
|
111
|
+
fail(`chrome executable not found: ${chromePath}`, "no_chrome");
|
|
112
|
+
} else {
|
|
113
|
+
main().catch((err) => fail(err?.message ?? String(err)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
hardTimer = setTimeout(
|
|
118
|
+
() => fail(`render exceeded ${timeoutMs}ms`, "timeout"),
|
|
119
|
+
timeoutMs,
|
|
120
|
+
);
|
|
121
|
+
hardTimer.unref?.();
|
|
122
|
+
|
|
123
|
+
const code = await readFile(patternFile, "utf8");
|
|
124
|
+
|
|
125
|
+
// 1. Loopback-only static server for the page + the strudel bundle. Paths are
|
|
126
|
+
// confined to this dir and the resolved dist (no traversal).
|
|
127
|
+
server = createServer(async (req, res) => {
|
|
128
|
+
const pathname = new URL(req.url, "http://127.0.0.1").pathname;
|
|
129
|
+
let file;
|
|
130
|
+
if (pathname === "/" || pathname === "/page.html") {
|
|
131
|
+
file = path.join(HERE, "page.html");
|
|
132
|
+
} else if (pathname === "/strudel.js") {
|
|
133
|
+
file = path.join(DIST, "index.js");
|
|
134
|
+
} else {
|
|
135
|
+
const resolved = path.resolve(DIST, `.${pathname}`);
|
|
136
|
+
file = resolved.startsWith(DIST) ? resolved : null;
|
|
137
|
+
}
|
|
138
|
+
if (!file) {
|
|
139
|
+
res.writeHead(403).end("forbidden");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const body = await readFile(file);
|
|
144
|
+
res.writeHead(200, {
|
|
145
|
+
"content-type": MIME[path.extname(file)] ?? "application/octet-stream",
|
|
146
|
+
"cross-origin-opener-policy": "same-origin",
|
|
147
|
+
"cross-origin-embedder-policy": "require-corp",
|
|
148
|
+
"cross-origin-resource-policy": "cross-origin",
|
|
149
|
+
});
|
|
150
|
+
res.end(body);
|
|
151
|
+
} catch {
|
|
152
|
+
res.writeHead(404).end("not found");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
156
|
+
const httpPort = server.address().port;
|
|
157
|
+
|
|
158
|
+
// 2. Headless Chrome. No audio device is needed — OfflineAudioContext renders
|
|
159
|
+
// into a buffer, so this works on a bare CI box.
|
|
160
|
+
userDataDir = await mkdtemp(path.join(tmpdir(), "oma-strudel-"));
|
|
161
|
+
chrome = spawn(
|
|
162
|
+
chromePath,
|
|
163
|
+
[
|
|
164
|
+
"--headless=new",
|
|
165
|
+
"--disable-gpu",
|
|
166
|
+
"--no-first-run",
|
|
167
|
+
"--no-default-browser-check",
|
|
168
|
+
"--disable-dev-shm-usage",
|
|
169
|
+
`--user-data-dir=${userDataDir}`,
|
|
170
|
+
"--remote-debugging-port=0",
|
|
171
|
+
"about:blank",
|
|
172
|
+
],
|
|
173
|
+
{ stdio: ["ignore", "ignore", "pipe"] },
|
|
174
|
+
);
|
|
175
|
+
chrome.on("error", (err) => fail(`chrome failed to start: ${err.message}`, "no_chrome"));
|
|
176
|
+
|
|
177
|
+
const devtoolsPort = await waitForDevtoolsPort(
|
|
178
|
+
path.join(userDataDir, "DevToolsActivePort"),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// 3. Drive it over raw CDP.
|
|
182
|
+
const targets = await (
|
|
183
|
+
await fetch(`http://127.0.0.1:${devtoolsPort}/json/list`)
|
|
184
|
+
).json();
|
|
185
|
+
const page = targets.find((t) => t.type === "page");
|
|
186
|
+
if (!page) throw new Error("chrome exposed no page target");
|
|
187
|
+
|
|
188
|
+
const cdp = await connect(page.webSocketDebuggerUrl);
|
|
189
|
+
await cdp.send("Runtime.enable");
|
|
190
|
+
await cdp.send("Page.enable");
|
|
191
|
+
await cdp.send("Page.navigate", {
|
|
192
|
+
url: `http://127.0.0.1:${httpPort}/page.html`,
|
|
193
|
+
});
|
|
194
|
+
await cdp.waitForLoad();
|
|
195
|
+
|
|
196
|
+
const base64Length = await cdp.evaluate(
|
|
197
|
+
`window.__omaRender(${JSON.stringify(code)}, ${seconds}, ${cps}, ${sampleRate})`,
|
|
198
|
+
);
|
|
199
|
+
if (!base64Length || base64Length <= 0) {
|
|
200
|
+
throw new Error("strudel produced an empty buffer");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 4. Pull the wav back in slices so no single CDP message gets huge.
|
|
204
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
205
|
+
let b64 = "";
|
|
206
|
+
for (let offset = 0; offset < base64Length; offset += CHUNK) {
|
|
207
|
+
b64 += await cdp.evaluate(
|
|
208
|
+
`window.__omaWav.slice(${offset}, ${offset + CHUNK})`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const wav = Buffer.from(b64, "base64");
|
|
212
|
+
await writeFile(outPath, wav);
|
|
213
|
+
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
`${JSON.stringify({
|
|
216
|
+
ok: true,
|
|
217
|
+
output: outPath,
|
|
218
|
+
seconds,
|
|
219
|
+
bytes: wav.length,
|
|
220
|
+
sha256: createHash("sha256").update(wav).digest("hex"),
|
|
221
|
+
})}\n`,
|
|
222
|
+
);
|
|
223
|
+
await cleanup();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Chrome writes its chosen debugger port here once it is listening. */
|
|
228
|
+
async function waitForDevtoolsPort(portFile) {
|
|
229
|
+
for (let i = 0; i < 200; i++) {
|
|
230
|
+
if (existsSync(portFile)) {
|
|
231
|
+
const [line] = readFileSync(portFile, "utf8").split("\n");
|
|
232
|
+
const port = Number(line);
|
|
233
|
+
if (Number.isFinite(port) && port > 0) return port;
|
|
234
|
+
}
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
236
|
+
}
|
|
237
|
+
throw new Error("chrome never reported a devtools port");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Minimal CDP client over Node's global WebSocket. */
|
|
241
|
+
async function connect(wsUrl) {
|
|
242
|
+
const ws = new WebSocket(wsUrl);
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
ws.onopen = resolve;
|
|
245
|
+
ws.onerror = () => reject(new Error("cdp websocket failed to open"));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let nextId = 0;
|
|
249
|
+
const pending = new Map();
|
|
250
|
+
let loaded = false;
|
|
251
|
+
const loadWaiters = [];
|
|
252
|
+
|
|
253
|
+
ws.onmessage = (event) => {
|
|
254
|
+
const msg = JSON.parse(event.data);
|
|
255
|
+
if (msg.id && pending.has(msg.id)) {
|
|
256
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
257
|
+
pending.delete(msg.id);
|
|
258
|
+
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
|
|
259
|
+
else resolve(msg.result);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (msg.method === "Page.loadEventFired") {
|
|
263
|
+
loaded = true;
|
|
264
|
+
while (loadWaiters.length) loadWaiters.shift()();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const send = (method, params = {}) =>
|
|
269
|
+
new Promise((resolve, reject) => {
|
|
270
|
+
const id = ++nextId;
|
|
271
|
+
pending.set(id, { resolve, reject });
|
|
272
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
send,
|
|
277
|
+
waitForLoad: () =>
|
|
278
|
+
loaded ? Promise.resolve() : new Promise((resolve) => loadWaiters.push(resolve)),
|
|
279
|
+
async evaluate(expression) {
|
|
280
|
+
const res = await send("Runtime.evaluate", {
|
|
281
|
+
expression,
|
|
282
|
+
awaitPromise: true,
|
|
283
|
+
returnByValue: true,
|
|
284
|
+
});
|
|
285
|
+
if (res.exceptionDetails) {
|
|
286
|
+
const detail =
|
|
287
|
+
res.exceptionDetails.exception?.description ??
|
|
288
|
+
res.exceptionDetails.text ??
|
|
289
|
+
"unknown page error";
|
|
290
|
+
throw new Error(detail.split("\n")[0]);
|
|
291
|
+
}
|
|
292
|
+
return res.result.value;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|