@leejungkiin/awkit 1.7.1 → 1.7.4
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/bin/awk.js +576 -84
- package/core/CLAUDE.md +1 -1
- package/core/GEMINI.md +148 -167
- package/core/GEMINI.md.bak +149 -116
- package/core/skill-runtime-manifest.json +3 -0
- package/docs/Claude Fable 5.md +3826 -0
- package/docs/android_kotlin_system_instruction.md +210 -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/brainstorm_antigravity_unified_architecture.v1.md +105 -0
- package/docs/history/implementation_plan.v1.md +58 -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 +144 -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 +47 -0
- package/skills/code-review/SKILL.md +87 -0
- 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/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/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();
|