@leejungkiin/awkit 1.7.1 → 1.7.5
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 +42 -5
- package/bin/awk.js +1148 -92
- package/core/CLAUDE.md +1 -1
- package/core/GEMINI.md +152 -167
- package/core/GEMINI.md.bak +153 -116
- package/core/GreetingEngine/index.js +225 -0
- package/core/GreetingEngine/locales/en.json +6 -0
- package/core/GreetingEngine/locales/ja.json +6 -0
- package/core/GreetingEngine/locales/vi.json +6 -0
- package/core/GreetingEngine/test.js +37 -0
- package/core/skill-runtime-manifest.json +3 -0
- package/docs/Claude Fable 5.md +3826 -0
- package/docs/PRD.md +57 -0
- package/docs/android_kotlin_system_instruction.md +210 -0
- package/docs/brainstorm_multi_agent_cli.md +211 -0
- package/docs/brainstorm_ponytail_integration.md +146 -0
- package/docs/brainstorm_smart_setup.md +113 -0
- package/docs/deep-research-report (1).md +293 -0
- package/docs/history/GEMINI.v1.md +135 -0
- package/docs/history/PRD.v1.md +55 -0
- package/docs/history/PRD.v2.md +57 -0
- package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
- package/docs/history/implementation_plan.v1.md +58 -0
- package/docs/history/memoizedfibonacci_spec.v1.md +127 -0
- package/docs/specs/greetingapp_spec.md +97 -0
- package/docs/specs/memoizedfibonacci_spec.md +211 -0
- package/package.json +4 -1
- package/scripts/artifact-storage.js +130 -0
- package/scripts/automation-gate.js +35 -2
- package/scripts/claude-plan.js +76 -0
- package/scripts/dependency-manager.js +210 -0
- package/scripts/exec-rtk.js +11 -5
- package/scripts/i18n-helper.js +381 -0
- package/scripts/multi-model-pipeline.js +386 -0
- package/scripts/qwen-exec.js +147 -0
- package/skill-packs/mobile-ios/pack.json +4 -2
- package/skill-packs/reverse-engineering/pack.json +1 -0
- package/skills/CATALOG.md +20 -0
- package/skills/GEMINI.md +9 -1
- package/skills/TRIGGER_INDEX.md +10 -0
- package/skills/ai-music/SKILL.md +275 -0
- package/skills/android-re-analyzer/SKILL.md +238 -0
- package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
- package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
- package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
- package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
- package/skills/android-re-analyzer/references/setup-guide.md +221 -0
- package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
- package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
- package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
- package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
- package/skills/animal-island-ui-style/SKILL.md +1450 -0
- package/skills/app-store-review-agent/SKILL.md +164 -0
- package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
- package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
- package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
- package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
- package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
- package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
- package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
- package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
- package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
- package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
- package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
- package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
- package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
- package/skills/awf-ponytail/SKILL.md +91 -0
- package/skills/awf-ponytail-review/SKILL.md +67 -0
- package/skills/awf-session-restore/SKILL.md +3 -3
- package/skills/brainstorm-agent/SKILL.md +11 -2
- package/skills/brainstorm-agent/templates/brief-template.md +8 -0
- package/skills/claude-planner/SKILL.md +71 -0
- package/skills/code-review/SKILL.md +87 -0
- package/skills/codex-conductor/SKILL.md +25 -1
- package/skills/expo-game-development/SKILL.md +163 -0
- package/skills/flutter/LICENSE.txt +202 -0
- package/skills/flutter/SKILL.md +127 -0
- package/skills/flutter-project-creater/LICENSE.txt +202 -0
- package/skills/flutter-project-creater/SKILL.md +106 -0
- package/skills/game-developer/SKILL.md +163 -0
- package/skills/game-developer/references/ecs-patterns.md +501 -0
- package/skills/game-developer/references/multiplayer-networking.md +475 -0
- package/skills/game-developer/references/performance-optimization.md +422 -0
- package/skills/game-developer/references/unity-patterns.md +271 -0
- package/skills/game-developer/references/unreal-cpp.md +352 -0
- package/skills/generate-gui-assets/SKILL.md +305 -0
- package/skills/generate-gui-assets/agents/openai.yaml +4 -0
- package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
- package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
- package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
- package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
- package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
- package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
- package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
- package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
- package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
- package/skills/godot-game-development/SKILL.md +142 -0
- package/skills/hatch-pet/LICENSE.txt +201 -0
- package/skills/hatch-pet/SKILL.md +420 -0
- package/skills/hatch-pet/agents/openai.yaml +4 -0
- package/skills/hatch-pet/references/animation-rows.md +29 -0
- package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
- package/skills/hatch-pet/references/qa-rubric.md +60 -0
- package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
- package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
- package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
- package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
- package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
- package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
- package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
- package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
- package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
- package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
- package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
- package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
- package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
- package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
- package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
- package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
- package/skills/i18n-orchestrator/SKILL.md +37 -0
- package/skills/ios-simulator-skill/SKILL.md +390 -0
- package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
- package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
- package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
- package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
- package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
- package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
- package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
- package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
- package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
- package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
- package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
- package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
- package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
- package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
- package/skills/ios-simulator-skill/scripts/container.py +668 -0
- package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
- package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
- package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
- package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
- package/skills/ios-simulator-skill/scripts/location.py +467 -0
- package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
- package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
- package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
- package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
- package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
- package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
- package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
- package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
- package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
- package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
- package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
- package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
- package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
- package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
- package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
- package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
- package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
- package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
- package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
- package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
- package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
- package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
- package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
- package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
- package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
- package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
- package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
- package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
- package/skills/orchestrator/SKILL.md +0 -20
- package/skills/persistent-storage/SKILL.md +55 -0
- package/skills/qwen-conductor/SKILL.md +100 -0
- package/skills/short-maker/SKILL.md +23 -0
- package/skills/short-maker/scripts/effects.js +56 -0
- package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
- package/skills/short-maker/scripts/videomix.js +601 -0
- package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
- package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
- package/skills/smali-to-kotlin/SKILL.md +128 -0
- package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
- package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
- package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
- package/skills/smali-to-kotlin/library-patterns.md +189 -0
- package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
- package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
- package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
- package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
- package/skills/smali-to-kotlin/phase-3-build.md +248 -0
- package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
- package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
- package/skills/smali-to-kotlin/templates/app-map.md +101 -0
- package/skills/smali-to-kotlin/templates/architecture.md +142 -0
- package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
- package/skills/spec-gate/SKILL.md +6 -2
- package/skills/symphony-enforcer/SKILL.md +8 -0
- package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
- package/skills/symphony-enforcer/examples/three-phase.md +16 -0
- package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
- package/skills/unity-game-development/SKILL.md +231 -0
- package/skills/video-edit/SKILL.md +36 -0
- package/skills/video-edit/scripts/video_edit.py +324 -0
- package/templates/project-identity/android.json +2 -2
- package/templates/project-identity/backend-nestjs.json +2 -2
- package/templates/project-identity/expo.json +2 -2
- package/templates/project-identity/ios.json +2 -2
- package/templates/project-identity/web-nextjs.json +2 -2
- package/templates/setup-mapping.json +48 -0
- package/templates/specs/design-template.md +161 -71
- package/templates/specs/requirements-template.md +65 -133
- package/templates/specs/task-spec-template.xml +3 -0
- package/workflows/_uncategorized/critic.md +40 -0
- package/workflows/_uncategorized/git-rebase-flow.md +81 -0
- package/workflows/_uncategorized/image-gen.md +118 -0
- package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
- package/workflows/_uncategorized/pixel-gen.md +86 -0
- package/workflows/_uncategorized/pixel-setup.md +90 -0
- package/workflows/_uncategorized/ponytail-review.md +59 -0
- package/workflows/_uncategorized/reverse-android-build.md +222 -0
- package/workflows/_uncategorized/reverse-android-design.md +139 -0
- package/workflows/_uncategorized/reverse-android-discover.md +150 -0
- package/workflows/_uncategorized/reverse-android-scan.md +158 -0
- package/workflows/_uncategorized/reverse-android.md +143 -0
- package/workflows/_uncategorized/reverse-ios-build.md +240 -0
- package/workflows/_uncategorized/reverse-ios-design.md +112 -0
- package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
- package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
- package/workflows/_uncategorized/reverse-ios.md +152 -0
- package/workflows/_uncategorized/safety-router.md +34 -0
- package/workflows/_uncategorized/teach.md +89 -0
- package/workflows/_uncategorized/verify-ui.md +53 -0
- package/workflows/_uncategorized/visualize-screenshots.md +34 -0
- package/workflows/ads/ads-analyst.md +201 -0
- package/workflows/ads/ads-audit.md +106 -0
- package/workflows/ads/ads-optimize.md +97 -0
- package/workflows/ads/ads-targeting.md +241 -0
- package/workflows/ads/adsExpert.md +160 -0
- package/workflows/ads/smali-ads-config.md +400 -0
- package/workflows/ads/smali-ads-flow.md +331 -0
- package/workflows/ads/smali-ads-interstitial.md +377 -0
- package/workflows/ads/smali-ads-native.md +382 -0
- package/workflows/context/teach.md +89 -0
- package/workflows/gitnexus.md +8 -8
- package/workflows/lifecycle/brainstorm.md +43 -0
- package/workflows/lifecycle/code.md +5 -0
- package/workflows/lifecycle/init.md +23 -5
- package/workflows/lifecycle/multi-model-pipeline.md +60 -0
- package/workflows/multi-agent-pipeline.md +52 -0
- package/workflows/quality/ponytail-review.md +59 -0
- package/workflows/roles/critic.md +40 -0
- package/workflows/roles/safety-router.md +34 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLI Argument Parser
|
|
9
|
+
*/
|
|
10
|
+
function parseArgs(args) {
|
|
11
|
+
const flags = {};
|
|
12
|
+
const positional = [];
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (arg.startsWith('--')) {
|
|
16
|
+
const cleanArg = arg.slice(2);
|
|
17
|
+
if (cleanArg.includes('=')) {
|
|
18
|
+
const [key, val] = cleanArg.split('=');
|
|
19
|
+
flags[key] = val;
|
|
20
|
+
} else {
|
|
21
|
+
if (args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
22
|
+
flags[cleanArg] = args[i + 1];
|
|
23
|
+
i++;
|
|
24
|
+
} else {
|
|
25
|
+
flags[cleanArg] = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (arg.startsWith('-')) {
|
|
29
|
+
const cleanArg = arg.slice(1);
|
|
30
|
+
flags[cleanArg] = true;
|
|
31
|
+
} else {
|
|
32
|
+
positional.push(arg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { positional, flags };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Escapes HTML string characters.
|
|
40
|
+
*/
|
|
41
|
+
function escapeHtml(text) {
|
|
42
|
+
if (!text) return "";
|
|
43
|
+
return text.toString()
|
|
44
|
+
.replace(/&/g, "&")
|
|
45
|
+
.replace(/</g, "<")
|
|
46
|
+
.replace(/>/g, ">")
|
|
47
|
+
.replace(/"/g, """);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure tool is in PATH
|
|
52
|
+
*/
|
|
53
|
+
function checkCommand(cmd) {
|
|
54
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
55
|
+
try {
|
|
56
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
57
|
+
return true;
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get media duration using ffprobe.
|
|
65
|
+
*/
|
|
66
|
+
function getDuration(filepath) {
|
|
67
|
+
try {
|
|
68
|
+
const output = execSync(
|
|
69
|
+
`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filepath}"`,
|
|
70
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
71
|
+
).trim();
|
|
72
|
+
return parseFloat(output) || 0;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 1. ANALYZE Command
|
|
80
|
+
*/
|
|
81
|
+
function handleAnalyze(positional, flags) {
|
|
82
|
+
if (!checkCommand('ffprobe')) {
|
|
83
|
+
console.error("Error: ffprobe was not found on PATH.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targetPath = positional[0];
|
|
88
|
+
if (!targetPath) {
|
|
89
|
+
console.error("Usage: node scripts/videomix.js analyze <file-or-directory-path> [--out-json=<path>]");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const resolved = path.resolve(targetPath);
|
|
94
|
+
let items = [];
|
|
95
|
+
|
|
96
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
97
|
+
const files = fs.readdirSync(resolved);
|
|
98
|
+
const mediaExtensions = /\.(mp4|mov|mkv|webm|avi|mp3|wav|flac|aac)$/i;
|
|
99
|
+
items = files
|
|
100
|
+
.filter(f => mediaExtensions.test(f))
|
|
101
|
+
.map(f => path.join(resolved, f));
|
|
102
|
+
} else {
|
|
103
|
+
items = [resolved];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const results = items.map(item => {
|
|
107
|
+
try {
|
|
108
|
+
const probeOut = execSync(
|
|
109
|
+
`ffprobe -hide_banner -v error -show_format -show_streams -of json "${item}"`,
|
|
110
|
+
{ encoding: 'utf8' }
|
|
111
|
+
);
|
|
112
|
+
const probe = JSON.parse(probeOut);
|
|
113
|
+
return {
|
|
114
|
+
path: item,
|
|
115
|
+
duration: parseFloat(probe.format.duration) || 0,
|
|
116
|
+
format: probe.format.format_name,
|
|
117
|
+
streams: (probe.streams || []).map(s => ({
|
|
118
|
+
type: s.codec_type,
|
|
119
|
+
codec: s.codec_name,
|
|
120
|
+
width: s.width || null,
|
|
121
|
+
height: s.height || null,
|
|
122
|
+
fps: s.r_frame_rate || null
|
|
123
|
+
}))
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error(`Failed probing file ${item}: ${err.message}`);
|
|
127
|
+
return { path: item, error: err.message };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (flags['out-json']) {
|
|
132
|
+
const outPath = path.resolve(flags['out-json']);
|
|
133
|
+
fs.writeFileSync(outPath, JSON.stringify(results, null, 2), 'utf8');
|
|
134
|
+
console.log(`Results written to ${outPath}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.table(results.map(r => ({
|
|
137
|
+
File: path.basename(r.path),
|
|
138
|
+
Duration: r.duration ? `${r.duration.toFixed(2)}s` : 'N/A',
|
|
139
|
+
Format: r.format || 'N/A',
|
|
140
|
+
Streams: r.streams ? r.streams.map(s => `${s.type}(${s.codec})`).join(', ') : 'Error'
|
|
141
|
+
})));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 2. NEW Command (VideoMix Project)
|
|
147
|
+
*/
|
|
148
|
+
function handleNew(positional, flags, isCinematic = false) {
|
|
149
|
+
if (!checkCommand('ffmpeg')) {
|
|
150
|
+
console.error("Error: ffmpeg was not found on PATH.");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const edl = positional[0];
|
|
155
|
+
const outDir = positional[1];
|
|
156
|
+
|
|
157
|
+
if (!edl || !outDir) {
|
|
158
|
+
const cmdName = isCinematic ? 'new:cinematic' : 'new';
|
|
159
|
+
console.error(`Usage: node scripts/videomix.js ${cmdName} <edl.json> <out-dir> [--gsap=<path>] [--force]`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const edlPath = path.resolve(edl);
|
|
164
|
+
if (!fs.existsSync(edlPath)) {
|
|
165
|
+
console.error(`EDL file not found: ${edl}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const targetDir = path.resolve(outDir);
|
|
170
|
+
const force = flags.force || flags.f;
|
|
171
|
+
|
|
172
|
+
if (fs.existsSync(targetDir) && !force) {
|
|
173
|
+
console.error(`Error: OutDir already exists. Use --force to overwrite: ${outDir}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Load EDL
|
|
178
|
+
const config = JSON.parse(fs.readFileSync(edlPath, 'utf8'));
|
|
179
|
+
const project = config.project;
|
|
180
|
+
const shots = config.shots || [];
|
|
181
|
+
|
|
182
|
+
if (shots.length === 0) {
|
|
183
|
+
console.error("Error: EDL has no shots.");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Setup project directory structure
|
|
188
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
189
|
+
const mediaDir = path.join(targetDir, 'media');
|
|
190
|
+
fs.mkdirSync(mediaDir, { recursive: true });
|
|
191
|
+
fs.mkdirSync(path.join(targetDir, 'renders'), { recursive: true });
|
|
192
|
+
fs.mkdirSync(path.join(targetDir, 'snapshots'), { recursive: true });
|
|
193
|
+
|
|
194
|
+
const width = parseInt(project.width) || 1920;
|
|
195
|
+
const height = parseInt(project.height) || 1080;
|
|
196
|
+
const fps = parseInt(project.fps) || 30;
|
|
197
|
+
const duration = parseFloat(project.duration);
|
|
198
|
+
|
|
199
|
+
// Audio & Ducking Logic
|
|
200
|
+
const audioObj = isCinematic ? config.music : config.audio;
|
|
201
|
+
if (audioObj && audioObj.path) {
|
|
202
|
+
const audioOut = path.join(mediaDir, isCinematic ? 'music.m4a' : 'music.mp3');
|
|
203
|
+
const audioStart = parseFloat(audioObj.start) || 0;
|
|
204
|
+
const audioDuration = parseFloat(audioObj.duration) || duration;
|
|
205
|
+
const baseVolume = parseFloat(audioObj.volume) !== undefined ? parseFloat(audioObj.volume) : (isCinematic ? 0.42 : 1.0);
|
|
206
|
+
const fadeIn = parseFloat(audioObj.fadeIn) !== undefined ? parseFloat(audioObj.fadeIn) : 0.2;
|
|
207
|
+
const fadeOut = parseFloat(audioObj.fadeOut) !== undefined ? parseFloat(audioObj.fadeOut) : 0.8;
|
|
208
|
+
const fadeOutStart = Math.max(0, audioDuration - fadeOut);
|
|
209
|
+
|
|
210
|
+
const rawAudioPath = path.isAbsolute(audioObj.path)
|
|
211
|
+
? audioObj.path
|
|
212
|
+
: path.resolve(path.dirname(edlPath), audioObj.path);
|
|
213
|
+
|
|
214
|
+
let audioFilter = `volume=${baseVolume}`;
|
|
215
|
+
|
|
216
|
+
if (isCinematic && audioObj.ducking && Array.isArray(audioObj.ducking) && audioObj.ducking.length > 0) {
|
|
217
|
+
console.log(`[VideoMix] Applying audio ducking for ${audioObj.ducking.length} intervals.`);
|
|
218
|
+
const betweenExprs = audioObj.ducking.map(interval => `between(t,${interval.start},${interval.end})`).join('+');
|
|
219
|
+
const duckVolume = parseFloat(audioObj.duckVolume) || 0.15;
|
|
220
|
+
audioFilter = `volume='if(${betweenExprs}, ${duckVolume}, ${baseVolume})':eval=frame`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
audioFilter += `,afade=t=in:st=0:d=${fadeIn},afade=t=out:st=${fadeOutStart}:d=${fadeOut}`;
|
|
224
|
+
|
|
225
|
+
const audioCodec = isCinematic ? '-c:a aac' : '-c:a libmp3lame';
|
|
226
|
+
const audioCmd = `ffmpeg -hide_banner -y -ss ${audioStart} -t ${audioDuration} -i "${rawAudioPath}" -vn -af "${audioFilter}" ${audioCodec} -b:a 192k "${audioOut}"`;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
console.log(`[VideoMix] Formatting BGM track: ${path.basename(rawAudioPath)}`);
|
|
230
|
+
execSync(audioCmd, { stdio: 'inherit' });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(`BGM processing failed: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Shot Extraction
|
|
238
|
+
const videoTags = [];
|
|
239
|
+
const cutTimes = [];
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < shots.length; i++) {
|
|
242
|
+
const shot = shots[i];
|
|
243
|
+
const indexStr = String(i + 1).padStart(2, '0');
|
|
244
|
+
const id = shot.id || `shot-${indexStr}`;
|
|
245
|
+
const shotFile = `shot-${indexStr}.mp4`;
|
|
246
|
+
const shotOut = path.join(mediaDir, shotFile);
|
|
247
|
+
|
|
248
|
+
const srcStart = parseFloat(shot.sourceStart) || 0;
|
|
249
|
+
const shotDuration = parseFloat(shot.duration);
|
|
250
|
+
const timelineStart = parseFloat(shot.timelineStart) || 0;
|
|
251
|
+
const trackIndex = shot.trackIndex !== undefined ? parseInt(shot.trackIndex) : 0;
|
|
252
|
+
|
|
253
|
+
const rawSource = path.isAbsolute(shot.source)
|
|
254
|
+
? shot.source
|
|
255
|
+
: path.resolve(path.dirname(edlPath), shot.source);
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(rawSource)) {
|
|
258
|
+
console.error(`Source video not found: ${rawSource}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(`[VideoMix] Extracting shot ${i + 1}/${shots.length}: ${id} (duration ${shotDuration}s)`);
|
|
263
|
+
|
|
264
|
+
const vf = `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},format=yuv420p,fps=${fps}`;
|
|
265
|
+
const shotCmd = `ffmpeg -hide_banner -y -ss ${srcStart} -t ${shotDuration + 0.05} -i "${rawSource}" -an -vf "${vf}" -c:v libx264 -preset veryfast -crf 20 -g ${fps} -keyint_min ${fps} -sc_threshold 0 -movflags +faststart "${shotOut}"`;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
execSync(shotCmd, { stdio: 'ignore' });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error(`Shot extraction failed for ${id}: ${err.message}`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (timelineStart > 0) cutTimes.push(timelineStart);
|
|
275
|
+
videoTags.push(` <video id="${id}" class="clip video-shot" data-start="${timelineStart}" data-duration="${shotDuration}" data-track-index="${trackIndex}" src="./media/${shotFile}" muted playsinline preload="auto"></video>`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Dialogue (Cinematic only)
|
|
279
|
+
const dialogueTags = [];
|
|
280
|
+
const dialogue = config.dialogue || [];
|
|
281
|
+
if (isCinematic && dialogue.length > 0) {
|
|
282
|
+
for (let i = 0; i < dialogue.length; i++) {
|
|
283
|
+
const line = dialogue[i];
|
|
284
|
+
const indexStr = String(i + 1).padStart(2, '0');
|
|
285
|
+
const id = line.id || `dialogue-${indexStr}`;
|
|
286
|
+
const dialogueFile = `dialogue-${indexStr}${path.extname(line.source)}`;
|
|
287
|
+
const dialogueOut = path.join(mediaDir, dialogueFile);
|
|
288
|
+
|
|
289
|
+
const srcStart = parseFloat(line.sourceStart) || 0;
|
|
290
|
+
const lineDuration = parseFloat(line.duration);
|
|
291
|
+
const timelineStart = parseFloat(line.timelineStart) || 0;
|
|
292
|
+
const lineVolume = line.volume !== undefined ? parseFloat(line.volume) : 1.0;
|
|
293
|
+
const fadeIn = line.fadeIn !== undefined ? parseFloat(line.fadeIn) : 0.05;
|
|
294
|
+
const fadeOut = line.fadeOut !== undefined ? parseFloat(line.fadeOut) : 0.12;
|
|
295
|
+
const fadeOutStart = Math.max(0, lineDuration - fadeOut);
|
|
296
|
+
|
|
297
|
+
const rawSource = path.isAbsolute(line.source)
|
|
298
|
+
? line.source
|
|
299
|
+
: path.resolve(path.dirname(edlPath), line.source);
|
|
300
|
+
|
|
301
|
+
console.log(`[VideoMix] Extracting dialogue ${i + 1}/${dialogue.length}: ${id}`);
|
|
302
|
+
const diagFilter = `volume=${lineVolume},afade=t=in:st=0:d=${fadeIn},afade=t=out:st=${fadeOutStart}:d=${fadeOut}`;
|
|
303
|
+
const diagCmd = `ffmpeg -hide_banner -y -ss ${srcStart} -t ${lineDuration} -i "${rawSource}" -vn -af "${diagFilter}" -c:a aac -b:a 192k "${dialogueOut}"`;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
execSync(diagCmd, { stdio: 'ignore' });
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(`Dialogue extraction failed for ${id}: ${err.message}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const track = 6 + i;
|
|
313
|
+
dialogueTags.push(` <audio id="${id}" class="clip" data-start="${timelineStart}" data-duration="${lineDuration}" data-track-index="${track}" data-volume="1" src="./media/${dialogueFile}" preload="auto"></audio>`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Captions (Cinematic only)
|
|
318
|
+
const captionTags = [];
|
|
319
|
+
const captions = config.captions || [];
|
|
320
|
+
const captionSource = captions.length > 0 ? captions : dialogue.filter(d => d.subtitle);
|
|
321
|
+
if (isCinematic) {
|
|
322
|
+
for (let i = 0; i < captionSource.length; i++) {
|
|
323
|
+
const cue = captionSource[i];
|
|
324
|
+
const indexStr = String(i + 1).padStart(2, '0');
|
|
325
|
+
const id = cue.id || `caption-${indexStr}`;
|
|
326
|
+
const cueStart = cue.timelineStart !== undefined ? parseFloat(cue.timelineStart) : parseFloat(cue.start);
|
|
327
|
+
const cueDuration = parseFloat(cue.duration);
|
|
328
|
+
const text = cue.text || cue.subtitle;
|
|
329
|
+
captionTags.push(` <div id="${id}" class="clip subtitle-cue" data-start="${cueStart}" data-duration="${cueDuration}" data-track-index="3">${escapeHtml(text)}</div>`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Generate index.html from template
|
|
334
|
+
const templateName = isCinematic ? 'cinematic-character.template.html' : 'index.template.html';
|
|
335
|
+
const templatePath = path.resolve(__dirname, '..', 'templates', 'hyperframes', templateName);
|
|
336
|
+
if (!fs.existsSync(templatePath)) {
|
|
337
|
+
console.error(`Template not found: ${templatePath}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let html = fs.readFileSync(templatePath, 'utf8');
|
|
342
|
+
|
|
343
|
+
if (isCinematic) {
|
|
344
|
+
const titleSize = Math.round(width * 0.035);
|
|
345
|
+
const kickerSize = Math.round(width * 0.012);
|
|
346
|
+
const subtitleSize = Math.round(width * 0.019);
|
|
347
|
+
const titleOut = Math.min(4.8, Math.max(1.8, duration * 0.12));
|
|
348
|
+
|
|
349
|
+
html = html
|
|
350
|
+
.replace(/{{WIDTH}}/g, width.toString())
|
|
351
|
+
.replace(/{{HEIGHT}}/g, height.toString())
|
|
352
|
+
.replace(/{{DURATION}}/g, duration.toString())
|
|
353
|
+
.replace(/{{VIDEO_CLIPS}}/g, videoTags.join('\n'))
|
|
354
|
+
.replace(/{{DIALOGUE_AUDIO}}/g, dialogueTags.join('\n'))
|
|
355
|
+
.replace(/{{CAPTIONS}}/g, captionTags.join('\n'))
|
|
356
|
+
.replace(/{{MUSIC_VOLUME}}/g, "1")
|
|
357
|
+
.replace(/{{KICKER}}/g, escapeHtml(project.kicker))
|
|
358
|
+
.replace(/{{TITLE}}/g, escapeHtml(project.title))
|
|
359
|
+
.replace(/{{TITLE_OUT}}/g, titleOut.toString())
|
|
360
|
+
.replace(/{{TITLE_SIZE}}/g, titleSize.toString())
|
|
361
|
+
.replace(/{{KICKER_SIZE}}/g, kickerSize.toString())
|
|
362
|
+
.replace(/{{SUBTITLE_SIZE}}/g, subtitleSize.toString());
|
|
363
|
+
} else {
|
|
364
|
+
const flashColors = config.style?.flashColors || ["#f7f0cf", "#93ffd4", "#fff0b4", "#ffd28b"];
|
|
365
|
+
html = html
|
|
366
|
+
.replace(/{{WIDTH}}/g, width.toString())
|
|
367
|
+
.replace(/{{HEIGHT}}/g, height.toString())
|
|
368
|
+
.replace(/{{DURATION}}/g, duration.toString())
|
|
369
|
+
.replace(/{{VIDEO_CLIPS}}/g, videoTags.join('\n'))
|
|
370
|
+
.replace(/{{KICKER}}/g, escapeHtml(project.kicker))
|
|
371
|
+
.replace(/{{TITLE}}/g, escapeHtml(project.title))
|
|
372
|
+
.replace(/{{CUT_TIMES}}/g, JSON.stringify(cutTimes))
|
|
373
|
+
.replace(/{{FLASH_COLORS}}/g, JSON.stringify(flashColors))
|
|
374
|
+
.replace(/{{FINAL_VIGNETTE_START}}/g, Math.max(0, duration - 3.3).toString())
|
|
375
|
+
.replace(/{{FINAL_TITLE_START}}/g, Math.max(0, duration - 3.6).toString())
|
|
376
|
+
.replace(/{{FINAL_TITLE_END}}/g, Math.max(0, duration - 0.92).toString());
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fs.writeFileSync(path.join(targetDir, 'index.html'), html, 'utf8');
|
|
380
|
+
|
|
381
|
+
// Handle GSAP asset
|
|
382
|
+
let resolvedGsap = flags.gsap;
|
|
383
|
+
if (!resolvedGsap && config.assets?.gsap) {
|
|
384
|
+
resolvedGsap = config.assets.gsap;
|
|
385
|
+
}
|
|
386
|
+
if (resolvedGsap) {
|
|
387
|
+
const resolvedGsapPath = path.isAbsolute(resolvedGsap)
|
|
388
|
+
? resolvedGsap
|
|
389
|
+
: path.resolve(path.dirname(edlPath), resolvedGsap);
|
|
390
|
+
|
|
391
|
+
if (fs.existsSync(resolvedGsapPath)) {
|
|
392
|
+
fs.copyFileSync(resolvedGsapPath, path.join(targetDir, 'gsap.min.js'));
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
console.warn("Warning: No local gsap.min.js copied. Add --gsap or set assets.gsap in your EDL.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Save EDL copy in target
|
|
399
|
+
fs.copyFileSync(edlPath, path.join(targetDir, 'edl.json'));
|
|
400
|
+
console.log(`✅ Project successfully bootstrapped at: ${targetDir}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 3. RENDER Command
|
|
405
|
+
*/
|
|
406
|
+
function handleRender(positional, flags) {
|
|
407
|
+
const projectDir = positional[0];
|
|
408
|
+
if (!projectDir) {
|
|
409
|
+
console.error("Usage: node scripts/videomix.js render <project-dir> [--output=<path>] [--snapshots=<times>] [--skip-snapshots]");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const resolvedDir = path.resolve(projectDir);
|
|
414
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
415
|
+
console.error(`Project directory not found: ${projectDir}`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const output = flags.output || "renders/final.mp4";
|
|
420
|
+
const snapshotAt = flags.snapshots || "0.5,3.1,6.1,9.1,12.1,15.1,18.1,21.1,24.1";
|
|
421
|
+
const skipSnapshots = !!flags['skip-snapshots'];
|
|
422
|
+
|
|
423
|
+
console.log(`[VideoMix] Rendering project in: ${resolvedDir}`);
|
|
424
|
+
|
|
425
|
+
const originalCwd = process.cwd();
|
|
426
|
+
process.chdir(resolvedDir);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const lint = spawnSync('npx', ['hyperframes', 'lint'], { stdio: 'inherit' });
|
|
430
|
+
if (lint.status !== 0) throw new Error("hyperframes lint failed.");
|
|
431
|
+
|
|
432
|
+
const validate = spawnSync('npx', ['hyperframes', 'validate'], { stdio: 'inherit' });
|
|
433
|
+
if (validate.status !== 0) throw new Error("hyperframes validate failed.");
|
|
434
|
+
|
|
435
|
+
const inspect = spawnSync('npx', ['hyperframes', 'inspect'], { stdio: 'inherit' });
|
|
436
|
+
if (inspect.status !== 0) throw new Error("hyperframes inspect failed.");
|
|
437
|
+
|
|
438
|
+
if (!skipSnapshots) {
|
|
439
|
+
fs.mkdirSync('snapshots', { recursive: true });
|
|
440
|
+
const snapshot = spawnSync('npx', ['hyperframes', 'snapshot', '--at', snapshotAt], { stdio: 'inherit' });
|
|
441
|
+
if (snapshot.status !== 0) throw new Error("hyperframes snapshot failed.");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const render = spawnSync('npx', ['hyperframes', 'render', '--output', output], { stdio: 'inherit' });
|
|
445
|
+
if (render.status !== 0) throw new Error("hyperframes render failed.");
|
|
446
|
+
|
|
447
|
+
console.log(`✅ Render completed. Output written to: ${path.resolve(output)}`);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
console.error(`[VideoMix] Render error: ${err.message}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
} finally {
|
|
452
|
+
process.chdir(originalCwd);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 4. QA Command
|
|
458
|
+
*/
|
|
459
|
+
function handleQa(positional, flags) {
|
|
460
|
+
if (!checkCommand('ffmpeg') || !checkCommand('ffprobe')) {
|
|
461
|
+
console.error("Error: ffmpeg and ffprobe must be on PATH.");
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const projectDir = positional[0];
|
|
466
|
+
if (!projectDir) {
|
|
467
|
+
console.error("Usage: node scripts/videomix.js qa <project-dir> [--final-mp4=<path>] [--times=<times>]");
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const resolvedDir = path.resolve(projectDir);
|
|
472
|
+
let finalMp4 = flags['final-mp4'];
|
|
473
|
+
|
|
474
|
+
if (!finalMp4) {
|
|
475
|
+
const rendersDir = path.join(resolvedDir, 'renders');
|
|
476
|
+
if (!fs.existsSync(rendersDir)) {
|
|
477
|
+
console.error(`Renders folder not found in ${resolvedDir}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const files = fs.readdirSync(rendersDir).filter(f => f.endsWith('.mp4'));
|
|
481
|
+
if (files.length === 0) {
|
|
482
|
+
console.error("No rendered MP4 files found.");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const sorted = files.map(f => {
|
|
486
|
+
const filepath = path.join(rendersDir, f);
|
|
487
|
+
return { filepath, mtime: fs.statSync(filepath).mtime };
|
|
488
|
+
}).sort((a, b) => b.mtime - a.mtime);
|
|
489
|
+
|
|
490
|
+
finalMp4 = sorted[0].filepath;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const qaDir = path.join(resolvedDir, 'qa');
|
|
494
|
+
fs.mkdirSync(qaDir, { recursive: true });
|
|
495
|
+
|
|
496
|
+
console.log(`[VideoMix] Running QA suite on: ${finalMp4}`);
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const probeCmd = `ffprobe -hide_banner -v error -show_entries format=duration:stream=codec_type,width,height,r_frame_rate -of default=nw=1 "${finalMp4}"`;
|
|
500
|
+
const probeOut = execSync(probeCmd, { encoding: 'utf8' });
|
|
501
|
+
fs.writeFileSync(path.join(qaDir, 'ffprobe.txt'), probeOut, 'utf8');
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.error("QA: ffprobe run failed.");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const blackCmd = `ffmpeg -hide_banner -i "${finalMp4}" -vf "blackdetect=d=0.08:pix_th=0.08" -an -f null - 2>&1`;
|
|
508
|
+
const blackOut = execSync(blackCmd, { encoding: 'utf8' });
|
|
509
|
+
fs.writeFileSync(path.join(qaDir, 'blackdetect.txt'), blackOut, 'utf8');
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.error("QA: blackdetect run failed.");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const times = flags.times || "0.5,3.1,6.1,9.1,12.1,15.1,18.1,21.1,24.1";
|
|
515
|
+
const timeList = times.split(',').map(t => t.trim()).filter(Boolean);
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < timeList.length; i++) {
|
|
518
|
+
const t = timeList[i];
|
|
519
|
+
const label = t.replace('.', '_');
|
|
520
|
+
const outImg = path.join(qaDir, `frame-${String(i).padStart(2, '0')}-at-${label}s.png`);
|
|
521
|
+
try {
|
|
522
|
+
execSync(`ffmpeg -hide_banner -y -ss ${t} -i "${finalMp4}" -frames:v 1 -update 1 "${outImg}"`, { stdio: 'ignore' });
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.error(`QA: Failed to extract frame at ${t}s`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
console.log(`✅ QA suite finished. Outputs written to: ${qaDir}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 5. VERTICAL Command
|
|
533
|
+
*/
|
|
534
|
+
function handleVertical(positional, flags) {
|
|
535
|
+
if (!checkCommand('ffmpeg')) {
|
|
536
|
+
console.error("Error: ffmpeg must be on PATH.");
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const inputMp4 = positional[0];
|
|
541
|
+
const outputMp4 = positional[1];
|
|
542
|
+
|
|
543
|
+
if (!inputMp4 || !outputMp4) {
|
|
544
|
+
console.error("Usage: node scripts/videomix.js vertical <input-mp4> <output-mp4> [--hard-crop]");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const resolvedInput = path.resolve(inputMp4);
|
|
549
|
+
const resolvedOutput = path.resolve(outputMp4);
|
|
550
|
+
|
|
551
|
+
let filter = "[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=28,eq=brightness=-0.08:saturation=0.85[bg];[0:v]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,format=yuv420p";
|
|
552
|
+
if (flags['hard-crop'] || flags.h) {
|
|
553
|
+
filter = "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,format=yuv420p";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
console.log(`[VideoMix] Exporting vertical format to: ${resolvedOutput}`);
|
|
557
|
+
const cmd = `ffmpeg -hide_banner -y -i "${resolvedInput}" -filter_complex "${filter}" -c:v libx264 -preset veryfast -crf 20 -c:a aac -b:a 192k -movflags +faststart "${resolvedOutput}"`;
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
561
|
+
console.log(`✅ Vertical format export successful.`);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.error(`[VideoMix] Vertical export failed: ${err.message}`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Main Entry Point
|
|
570
|
+
*/
|
|
571
|
+
function main() {
|
|
572
|
+
const rawArgs = process.argv.slice(2);
|
|
573
|
+
const cmd = rawArgs[0];
|
|
574
|
+
const { positional, flags } = parseArgs(rawArgs.slice(1));
|
|
575
|
+
|
|
576
|
+
switch (cmd) {
|
|
577
|
+
case 'analyze':
|
|
578
|
+
handleAnalyze(positional, flags);
|
|
579
|
+
break;
|
|
580
|
+
case 'new':
|
|
581
|
+
handleNew(positional, flags, false);
|
|
582
|
+
break;
|
|
583
|
+
case 'new:cinematic':
|
|
584
|
+
handleNew(positional, flags, true);
|
|
585
|
+
break;
|
|
586
|
+
case 'render':
|
|
587
|
+
handleRender(positional, flags);
|
|
588
|
+
break;
|
|
589
|
+
case 'qa':
|
|
590
|
+
handleQa(positional, flags);
|
|
591
|
+
break;
|
|
592
|
+
case 'vertical':
|
|
593
|
+
handleVertical(positional, flags);
|
|
594
|
+
break;
|
|
595
|
+
default:
|
|
596
|
+
console.error("Unknown command. Available commands: analyze, new, new:cinematic, render, qa, vertical");
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
main();
|