@amg555/frameforge-mcp 0.0.0-stage → 2.3.1
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/.bob/artifacts/enterprise-ai-video-agent-platform-agnostic-implementation-guide.html +464 -0
- package/.claude-plugin/marketplace.json +45 -0
- package/.gitattributes +19 -0
- package/.mcp.json +45 -0
- package/CHANGELOG.md +447 -0
- package/CLAUDE.md +320 -0
- package/CONTRIBUTING.md +112 -0
- package/LICENSE +21 -0
- package/Procfile +1 -0
- package/README.md +377 -2
- package/SECURITY.md +71 -0
- package/START_HERE.md +200 -0
- package/assets/metadata/assets.json +5 -0
- package/assets/metadata/embeddings.json +5 -0
- package/install.sh +64 -0
- package/package.json +58 -4
- package/src/animation/animation-system.js +229 -0
- package/src/asset-library/README.md +188 -0
- package/src/asset-library/asset-library.js +639 -0
- package/src/audio-mixer/mixer.js +187 -0
- package/src/cli/doctor.js +173 -0
- package/src/cli/make.js +655 -0
- package/src/cli/setup-keys.js +392 -0
- package/src/config/load-config.js +72 -0
- package/src/fact-checker/checker.js +384 -0
- package/src/fact-checker/review-gate.js +306 -0
- package/src/fact-checker/validation-set.js +1078 -0
- package/src/mcp-server/server.js +1299 -0
- package/src/mcp-server/stdio.js +87 -0
- package/src/pipeline/artifact-manifest.js +323 -0
- package/src/pipeline/artifact-retention.js +189 -0
- package/src/pipeline/external-adapter.js +366 -0
- package/src/pipeline/providers.js +853 -0
- package/src/pipeline/retry-policy.js +134 -0
- package/src/pipeline/state-machine.js +297 -0
- package/src/pipeline/telemetry.js +162 -0
- package/src/quality-gate/gate.js +229 -0
- package/src/schema/scene.schema.json +101 -0
- package/src/shorts-extractor/extractor.js +343 -0
- package/src/utils/log-redaction.js +274 -0
- package/src/video-renderer/renderer.js +364 -0
- package/src/worker/server-render-free.js +420 -0
- package/src/worker/server.js +328 -0
- package/src/workflows/assets.js +131 -0
- package/src/workflows/ingest.js +299 -0
- package/src/workflows/packaging.js +156 -0
- package/src/workflows/publishing.js +206 -0
- package/src/workflows/render.js +102 -0
- package/src/workflows/research.js +151 -0
- package/src/workflows/script.js +210 -0
- package/src/workflows/shorts.js +113 -0
package/install.sh
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install the video-pipeline skill into your Claude/Bob skills folder.
|
|
3
|
+
#
|
|
4
|
+
# ./install.sh copy into ~/.claude/skills (every project)
|
|
5
|
+
# ./install.sh --project copy into ./.claude/skills (this project only)
|
|
6
|
+
# ./install.sh --link symlink instead of copy (edits to repo are live)
|
|
7
|
+
# ./install.sh --bob copy into ~/.bob/skills (IBM Bob global)
|
|
8
|
+
#
|
|
9
|
+
# An existing skill with the same name is moved aside to <name>.bak.<ts>.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
SRC="$HERE/plugins/video-pipeline/skills"
|
|
14
|
+
DEST="$HOME/.claude/skills"
|
|
15
|
+
MODE=copy
|
|
16
|
+
|
|
17
|
+
for a in "$@"; do
|
|
18
|
+
case "$a" in
|
|
19
|
+
--link) MODE=link ;;
|
|
20
|
+
--project) DEST="$PWD/.claude/skills" ;;
|
|
21
|
+
--bob) DEST="$HOME/.bob/skills" ;;
|
|
22
|
+
-h|--help) sed -n 2,10p "$0"; exit 0 ;;
|
|
23
|
+
*) echo "unknown option: $a"; exit 1 ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
mkdir -p "$DEST"
|
|
28
|
+
|
|
29
|
+
for dir in "$SRC"/*; do
|
|
30
|
+
name=$(basename "$dir")
|
|
31
|
+
target="$DEST/$name"
|
|
32
|
+
if [ -e "$target" ] || [ -L "$target" ]; then
|
|
33
|
+
mv "$target" "$target.bak.$(date +%Y%m%d%H%M%S)"
|
|
34
|
+
echo " moved existing $name aside"
|
|
35
|
+
fi
|
|
36
|
+
if [ "$MODE" = link ]; then
|
|
37
|
+
ln -s "$dir" "$target"
|
|
38
|
+
else
|
|
39
|
+
cp -R "$dir" "$target"
|
|
40
|
+
fi
|
|
41
|
+
echo " installed $name → $target ($MODE)"
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
echo
|
|
45
|
+
echo "Dependencies (must be available in PATH):"
|
|
46
|
+
for t in ffmpeg ffprobe node npm; do
|
|
47
|
+
printf " %-10s %s\n" "$t" "$(command -v "$t" 2>/dev/null || echo 'MISSING — install it')"
|
|
48
|
+
done
|
|
49
|
+
# Optional: edge-tts for free TTS
|
|
50
|
+
if command -v edge-tts >/dev/null 2>&1; then
|
|
51
|
+
echo " edge-tts $(command -v edge-tts) (free TTS, no key)"
|
|
52
|
+
else
|
|
53
|
+
echo " edge-tts not found — run: npm install -g edge-tts"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo
|
|
57
|
+
echo "Next steps:"
|
|
58
|
+
echo " 1. cp docs/env.template .env"
|
|
59
|
+
echo " 2. Edit .env with your API keys (run 'npm run setup' for interactive wizard)"
|
|
60
|
+
echo " 3. Deploy Cloudflare Remote MCP (recommended): cd cloudflare-mcp && wrangler deploy"
|
|
61
|
+
echo " OR start local HTTP MCP: node src/mcp-server/server.js"
|
|
62
|
+
echo " 4. Register the MCP endpoint in your IDE:"
|
|
63
|
+
echo " Codex / opencode / Cursor / IBM Bob / Claude Code / Windsurf — see README.md"
|
|
64
|
+
echo " 5. Restart your IDE to load the skill."
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amg555/frameforge-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 17 MCP tools.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/mcp-server/server.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"frameforge": "src/cli/make.js",
|
|
11
|
+
"frameforge-mcp": "src/mcp-server/stdio.js",
|
|
12
|
+
"frameforge-setup": "src/cli/setup-keys.js",
|
|
13
|
+
"frameforge-doctor": "src/cli/doctor.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"make": "node src/cli/make.js",
|
|
17
|
+
"setup": "node src/cli/setup-keys.js",
|
|
18
|
+
"setup:check": "node src/cli/setup-keys.js --check",
|
|
19
|
+
"doctor": "node src/cli/doctor.js",
|
|
20
|
+
"mcp": "node src/mcp-server/server.js",
|
|
21
|
+
"mcp:stdio": "node src/mcp-server/stdio.js",
|
|
22
|
+
"start": "node src/mcp-server/server.js",
|
|
23
|
+
"start:worker": "node src/worker/server.js",
|
|
24
|
+
"start:worker:render": "node src/worker/server-render-free.js",
|
|
25
|
+
"start:worker:local": "cross-env RENDER_MODE=full node src/worker/server-render-free.js",
|
|
26
|
+
"start:local-render": "cross-env RENDER_MODE=full WORKER_PORT=3211 node src/worker/server-render-free.js",
|
|
27
|
+
"db:migrate": "node scripts/db-migrate.js",
|
|
28
|
+
"validate": "node scripts/deep-validate.js",
|
|
29
|
+
"test": "node --test src/pipeline/retry-policy.test.js src/pipeline/telemetry.test.js src/pipeline/artifact-manifest.test.js src/pipeline/artifact-retention.test.js src/pipeline/external-adapter.test.js src/workflows/publishing.test.js src/workflows/script.test.js src/animation/animation-system.test.js src/fact-checker/review-gate.test.js src/fact-checker/checker.test.js src/utils/log-redaction.test.js src/worker/server-render-free.test.js src/worker/server.test.js src/mcp-server/server.test.js src/quality-gate/gate.test.js src/shorts-extractor/extractor.test.js src/audio-mixer/mixer.test.js src/video-renderer/renderer.test.js plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.test.js src/integration.test.js",
|
|
30
|
+
"lint": "echo 'No linter configured — add eslint for production'",
|
|
31
|
+
"check-credentials": "node src/utils/log-redaction.js",
|
|
32
|
+
"generate-assets": "node scripts/generate-initial-assets.js",
|
|
33
|
+
"pipeline:research": "node src/workflows/research.js",
|
|
34
|
+
"pipeline:script": "node src/workflows/script.js"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"playwright": "^1.64.1"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"frameforge",
|
|
44
|
+
"video",
|
|
45
|
+
"ai",
|
|
46
|
+
"mcp",
|
|
47
|
+
"cloudflare",
|
|
48
|
+
"automation",
|
|
49
|
+
"ibm-bob",
|
|
50
|
+
"cursor",
|
|
51
|
+
"windsurf",
|
|
52
|
+
"codex",
|
|
53
|
+
"opencode"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/AMG555/FrameForge.git"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation System — Task #11
|
|
3
|
+
*
|
|
4
|
+
* Narrative-driven camera movement presets for the video renderer.
|
|
5
|
+
* Every preset is a pure function: (scene metadata) → FFmpeg filter string.
|
|
6
|
+
*
|
|
7
|
+
* Camera hints map directly to the scene.camera_hint field in schema v1.0.0:
|
|
8
|
+
* slow_zoom_in | slow_zoom_out | pan_left | pan_right
|
|
9
|
+
* tilt_up | tilt_down | ken_burns | static | auto
|
|
10
|
+
*
|
|
11
|
+
* Transition types (used between scenes in the concat filter):
|
|
12
|
+
* cut | fade | dissolve | wipe_left | wipe_right
|
|
13
|
+
*
|
|
14
|
+
* All zoom values stay within ±1.12x to prevent quality loss (safe_zone = 1.12).
|
|
15
|
+
* All coordinates use FFmpeg expression notation (evaluated per-frame by zoompan).
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const { buildCameraFilter, buildTransitionFilter, selectCameraHint } = require('./animation-system');
|
|
19
|
+
* const vf = buildCameraFilter('slow_zoom_in', { duration: 6 });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const FPS = 24; // target frames per second
|
|
27
|
+
const MAX_ZOOM = 1.12; // maximum safe zoom factor
|
|
28
|
+
const BASE_ZOOM = 1.0; // starting zoom (no zoom)
|
|
29
|
+
const DEFAULT_W = 1920;
|
|
30
|
+
const DEFAULT_H = 1080;
|
|
31
|
+
|
|
32
|
+
// Per-frame zoom step for a smooth motion over ~8s at 24fps
|
|
33
|
+
// 1.12 - 1.00 = 0.12 over (8 * 24) = 192 frames → 0.000625/frame
|
|
34
|
+
// Round to 0.0007 for slightly snappier feel
|
|
35
|
+
const ZOOM_STEP_IN = 0.0007;
|
|
36
|
+
const ZOOM_STEP_OUT = 0.0007;
|
|
37
|
+
|
|
38
|
+
// ── Camera filter builders ───────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build an FFmpeg -vf filter string for the given camera preset.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} hint - camera_hint value from scene schema
|
|
44
|
+
* @param {{ duration?: number, focalX?: number, focalY?: number, w?: number, h?: number }} opts
|
|
45
|
+
* duration scene duration in seconds (used to calibrate speed)
|
|
46
|
+
* focalX/Y focal point override (0-1 relative, default 0.5/0.35)
|
|
47
|
+
* w/h output resolution (default 1920×1080)
|
|
48
|
+
* @returns {string} FFmpeg filter chain (ready for -vf)
|
|
49
|
+
*/
|
|
50
|
+
function buildCameraFilter(hint, { duration = 6, focalX = 0.5, focalY = 0.35, w = DEFAULT_W, h = DEFAULT_H } = {}) {
|
|
51
|
+
const frames = Math.max(48, Math.round(duration * FPS));
|
|
52
|
+
const zoomBase = `${w}x${h}`;
|
|
53
|
+
// Focal point in pixels
|
|
54
|
+
const px = Math.round(focalX * w);
|
|
55
|
+
const py = Math.round(focalY * h);
|
|
56
|
+
|
|
57
|
+
switch ((hint || 'auto').toLowerCase()) {
|
|
58
|
+
case 'slow_zoom_in':
|
|
59
|
+
return _zoomIn(frames, ZOOM_STEP_IN, zoomBase);
|
|
60
|
+
case 'slow_zoom_out':
|
|
61
|
+
return _zoomOut(frames, ZOOM_STEP_OUT, zoomBase);
|
|
62
|
+
case 'zoom_in':
|
|
63
|
+
return _zoomIn(frames, 0.0012, zoomBase);
|
|
64
|
+
case 'zoom_out':
|
|
65
|
+
return _zoomOut(frames, 0.0012, zoomBase);
|
|
66
|
+
case 'pan_left':
|
|
67
|
+
return _pan('left', frames, zoomBase);
|
|
68
|
+
case 'pan_right':
|
|
69
|
+
return _pan('right', frames, zoomBase);
|
|
70
|
+
case 'tilt_up':
|
|
71
|
+
return _tilt('up', frames, zoomBase);
|
|
72
|
+
case 'tilt_down':
|
|
73
|
+
return _tilt('down', frames, zoomBase);
|
|
74
|
+
case 'ken_burns':
|
|
75
|
+
return _kenBurns(frames, px, py, w, h, zoomBase);
|
|
76
|
+
case 'static':
|
|
77
|
+
return _static(w, h);
|
|
78
|
+
case 'auto':
|
|
79
|
+
default:
|
|
80
|
+
// Deterministic selection based on focal-point quadrant
|
|
81
|
+
return _autoSelect(focalX, focalY, frames, w, h, zoomBase);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the FFmpeg overlay filter for a cross-dissolve transition between
|
|
87
|
+
* two pre-rendered scene clips. Used in complex_filter assembly.
|
|
88
|
+
*
|
|
89
|
+
* Note: hard cuts require no filter (FFmpeg concat demuxer default).
|
|
90
|
+
* Fades/dissolves need xfade filter (FFmpeg ≥4.3).
|
|
91
|
+
*
|
|
92
|
+
* @param {'cut'|'fade'|'dissolve'|'wipe_left'|'wipe_right'} type
|
|
93
|
+
* @param {number} offset time offset (seconds) at which the transition starts
|
|
94
|
+
* @param {number} duration transition duration in seconds (default 0.5)
|
|
95
|
+
* @returns {{ xfadeArgs: string[], isCut: boolean }}
|
|
96
|
+
*/
|
|
97
|
+
function buildTransitionFilter(type, offset, duration = 0.5) {
|
|
98
|
+
if (type === 'cut' || !type) {
|
|
99
|
+
return { isCut: true, xfadeArgs: [] };
|
|
100
|
+
}
|
|
101
|
+
const xfadeType = {
|
|
102
|
+
fade: 'fade',
|
|
103
|
+
dissolve: 'dissolve',
|
|
104
|
+
wipe_left: 'wipeleft',
|
|
105
|
+
wipe_right: 'wiperight',
|
|
106
|
+
}[type] || 'fade';
|
|
107
|
+
|
|
108
|
+
// xfade filter args (applied in filtergraph)
|
|
109
|
+
return {
|
|
110
|
+
isCut: false,
|
|
111
|
+
xfadeArgs: [`transition=${xfadeType}`, `offset=${offset.toFixed(3)}`, `duration=${duration}`],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Select an appropriate camera hint for a scene when none is specified
|
|
117
|
+
* by the LLM. Alternates between zoom_in / zoom_out / pan to avoid
|
|
118
|
+
* monotony.
|
|
119
|
+
*
|
|
120
|
+
* @param {number} sceneIndex 0-based scene index
|
|
121
|
+
* @param {{ focalX?: number, focalY?: number }} focal
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function selectCameraHint(sceneIndex, { focalX = 0.5, focalY = 0.35 } = {}) {
|
|
125
|
+
const rotation = ['slow_zoom_in', 'pan_right', 'slow_zoom_out', 'pan_left', 'tilt_up', 'ken_burns'];
|
|
126
|
+
return rotation[sceneIndex % rotation.length];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Private filter implementations ───────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function _zoomIn(frames, step = ZOOM_STEP_IN, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
132
|
+
const maxZ = MAX_ZOOM.toFixed(4);
|
|
133
|
+
const rangeStep = frames > 0 ? (MAX_ZOOM - BASE_ZOOM) / frames : step;
|
|
134
|
+
const s = Math.min(rangeStep, step * 2).toFixed(6);
|
|
135
|
+
return (
|
|
136
|
+
`zoompan=` +
|
|
137
|
+
`z='min(zoom+${s},${maxZ})':` +
|
|
138
|
+
`x='iw/2-(iw/zoom/2)':` +
|
|
139
|
+
`y='ih/2-(ih/zoom/2)':` +
|
|
140
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _zoomOut(frames, step = ZOOM_STEP_OUT, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
145
|
+
const maxZ = MAX_ZOOM.toFixed(4);
|
|
146
|
+
const rangeStep = frames > 0 ? (MAX_ZOOM - BASE_ZOOM) / frames : step;
|
|
147
|
+
const s = Math.min(rangeStep, step * 2).toFixed(6);
|
|
148
|
+
return (
|
|
149
|
+
`zoompan=` +
|
|
150
|
+
`z='if(lte(zoom,1.0),${maxZ},max(zoom-${s},1.0))':` +
|
|
151
|
+
`x='iw/2-(iw/zoom/2)':` +
|
|
152
|
+
`y='ih/2-(ih/zoom/2)':` +
|
|
153
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _pan(direction, frames, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
158
|
+
if (direction === 'left') {
|
|
159
|
+
return (
|
|
160
|
+
`zoompan=` +
|
|
161
|
+
`z='1.06':` +
|
|
162
|
+
`x='if(gte(on,1),x-0.6,iw*0.04)':` +
|
|
163
|
+
`y='ih*0.06':` +
|
|
164
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return (
|
|
168
|
+
`zoompan=` +
|
|
169
|
+
`z='1.06':` +
|
|
170
|
+
`x='if(gte(on,1),x+0.6,iw*0.02)':` +
|
|
171
|
+
`y='ih*0.06':` +
|
|
172
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function _tilt(direction, frames, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
177
|
+
if (direction === 'up') {
|
|
178
|
+
return (
|
|
179
|
+
`zoompan=` +
|
|
180
|
+
`z='1.06':` +
|
|
181
|
+
`x='iw*0.06':` +
|
|
182
|
+
`y='if(gte(on,1),y-0.4,ih*0.12)':` +
|
|
183
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
`zoompan=` +
|
|
188
|
+
`z='1.06':` +
|
|
189
|
+
`x='iw*0.06':` +
|
|
190
|
+
`y='if(gte(on,1),y+0.4,ih*0.02)':` +
|
|
191
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _kenBurns(frames, focalPx, focalPy, w = DEFAULT_W, h = DEFAULT_H, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
196
|
+
const maxZ = MAX_ZOOM.toFixed(4);
|
|
197
|
+
const startX = Math.max(0, focalPx - w / 2);
|
|
198
|
+
const startY = Math.max(0, focalPy - h / 2);
|
|
199
|
+
return (
|
|
200
|
+
`zoompan=` +
|
|
201
|
+
`z='max(${maxZ}-0.0006*on,1.0)':` +
|
|
202
|
+
`x='if(eq(on,1),${startX},x+(iw/2-(iw/zoom/2)-x)*0.02)':` +
|
|
203
|
+
`y='if(eq(on,1),${startY},y+(ih/2-(ih/zoom/2)-y)*0.02)':` +
|
|
204
|
+
`d=1:s=${zoomBase}:fps=${FPS}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _static(w = DEFAULT_W, h = DEFAULT_H) {
|
|
209
|
+
return `scale=${w}:${h}:force_original_aspect_ratio=increase:flags=lanczos,crop=${w}:${h}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function _autoSelect(focalX, focalY, frames, w = DEFAULT_W, h = DEFAULT_H, zoomBase = `${DEFAULT_W}x${DEFAULT_H}`) {
|
|
213
|
+
if (focalY < 0.4 && focalX < 0.5) return _zoomIn(frames, ZOOM_STEP_IN, zoomBase);
|
|
214
|
+
if (focalY < 0.4 && focalX >= 0.5) return _kenBurns(frames, Math.round(focalX * w), Math.round(focalY * h), w, h, zoomBase);
|
|
215
|
+
if (focalX < 0.4) return _pan('right', frames, zoomBase);
|
|
216
|
+
if (focalX > 0.6) return _pan('left', frames, zoomBase);
|
|
217
|
+
return _zoomOut(frames, ZOOM_STEP_OUT, zoomBase);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
buildCameraFilter,
|
|
224
|
+
buildTransitionFilter,
|
|
225
|
+
selectCameraHint,
|
|
226
|
+
// constants exposed for testing
|
|
227
|
+
FPS,
|
|
228
|
+
MAX_ZOOM,
|
|
229
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Asset Library System
|
|
2
|
+
|
|
3
|
+
Composable asset management for consistent video production.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
### Composable Asset System
|
|
8
|
+
|
|
9
|
+
Instead of pre-generating all combinations (2 chars × 4 poses × 6 emotions = 48 combinations), we store:
|
|
10
|
+
|
|
11
|
+
- **Character identities:** 2 base characters (ancient_man, modern_man)
|
|
12
|
+
- **Pose variants:** 4 poses per character (standing, running, sitting, jumping)
|
|
13
|
+
- **Emotion overlays:** 6 emotions per character (neutral, happy, angry, sad, surprised, determined)
|
|
14
|
+
- **Props:** 15 reusable items (spear, phone, fire, etc.)
|
|
15
|
+
- **Backgrounds:** 10 scenes (forest, cave, city, etc.)
|
|
16
|
+
- **Effects:** 10 SVG overlays (arrow, question, etc.)
|
|
17
|
+
|
|
18
|
+
**Total:** ~47 composable assets vs 67-83 pre-generated combinations
|
|
19
|
+
|
|
20
|
+
### Runtime Composition
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Request: character=ancient_man, pose=running, emotion=determined
|
|
24
|
+
|
|
25
|
+
Composition:
|
|
26
|
+
1. Load identity: ancient_man_identity.png
|
|
27
|
+
2. Load pose: ancient_man/poses/running.png
|
|
28
|
+
3. Load emotion: ancient_man/emotions/determined.png
|
|
29
|
+
4. Composite layers → final image
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Directory Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
assets/
|
|
36
|
+
├── characters/
|
|
37
|
+
│ ├── ancient_man/
|
|
38
|
+
│ │ ├── identity_v1.png # Base character
|
|
39
|
+
│ │ ├── schema_version.json # Metadata
|
|
40
|
+
│ │ ├── poses/
|
|
41
|
+
│ │ │ ├── standing.png
|
|
42
|
+
│ │ │ ├── running.png
|
|
43
|
+
│ │ │ ├── sitting.png
|
|
44
|
+
│ │ │ └── jumping.png
|
|
45
|
+
│ │ └── emotions/
|
|
46
|
+
│ │ ├── neutral.png
|
|
47
|
+
│ │ ├── happy.png
|
|
48
|
+
│ │ ├── angry.png
|
|
49
|
+
│ │ ├── sad.png
|
|
50
|
+
│ │ ├── surprised.png
|
|
51
|
+
│ │ └── determined.png
|
|
52
|
+
│ └── modern_man/
|
|
53
|
+
│ ├── identity_v1.png
|
|
54
|
+
│ ├── schema_version.json
|
|
55
|
+
│ ├── poses/
|
|
56
|
+
│ └── emotions/
|
|
57
|
+
├── props/
|
|
58
|
+
│ ├── spear.png
|
|
59
|
+
│ ├── phone.png
|
|
60
|
+
│ ├── fire.png
|
|
61
|
+
│ └── ... (15 total)
|
|
62
|
+
├── backgrounds/
|
|
63
|
+
│ ├── forest.png
|
|
64
|
+
│ ├── cave.png
|
|
65
|
+
│ ├── city.png
|
|
66
|
+
│ └── ... (10 total)
|
|
67
|
+
├── effects/
|
|
68
|
+
│ ├── arrow.svg
|
|
69
|
+
│ ├── question.svg
|
|
70
|
+
│ └── ... (10 total)
|
|
71
|
+
└── metadata/
|
|
72
|
+
├── assets.json # Asset registry
|
|
73
|
+
└── embeddings.json # Semantic search cache
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Asset Metadata Schema
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"asset_id": "ancient_man_identity_v1",
|
|
81
|
+
"asset_type": "character_identity",
|
|
82
|
+
"character_id": "ancient_man",
|
|
83
|
+
"pose": null,
|
|
84
|
+
"emotion": null,
|
|
85
|
+
"visual_description": "Caveman with wild hair, tattered animal skin clothing",
|
|
86
|
+
"file_path": "assets/characters/ancient_man/identity_v1.png",
|
|
87
|
+
"style_parameters": {
|
|
88
|
+
"ink_style": "clean_line",
|
|
89
|
+
"color_palette": ["#000000", "#FFFFFF", "#8B4513"],
|
|
90
|
+
"line_weight": 3,
|
|
91
|
+
"resolution": "1920x1080"
|
|
92
|
+
},
|
|
93
|
+
"metadata": {
|
|
94
|
+
"creation_date": "2026-09-09T10:00:00Z",
|
|
95
|
+
"usage_count": 42,
|
|
96
|
+
"variants": ["pose:running", "emotion:determined"]
|
|
97
|
+
},
|
|
98
|
+
"schema_version": "1.0.0"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Matching Algorithm
|
|
103
|
+
|
|
104
|
+
### 1. Metadata Filtering (Primary)
|
|
105
|
+
|
|
106
|
+
```sql
|
|
107
|
+
SELECT * FROM assets
|
|
108
|
+
WHERE asset_type = 'character_identity'
|
|
109
|
+
AND character_id = 'ancient_man'
|
|
110
|
+
AND (pose = 'running' OR pose IS NULL)
|
|
111
|
+
AND (emotion = 'determined' OR emotion IS NULL);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Fast metadata lookup in PostgreSQL for exact matches.
|
|
115
|
+
|
|
116
|
+
### 2. Semantic Fallback (Secondary)
|
|
117
|
+
|
|
118
|
+
If no metadata match:
|
|
119
|
+
1. Compute embedding of visual description
|
|
120
|
+
2. Search embeddings with cosine similarity
|
|
121
|
+
3. Return if similarity > 0.85
|
|
122
|
+
4. Otherwise, generate new asset
|
|
123
|
+
|
|
124
|
+
## Style Consistency
|
|
125
|
+
|
|
126
|
+
All generated assets must match:
|
|
127
|
+
|
|
128
|
+
- **Ink style:** clean_line (Simple History / Doodle style)
|
|
129
|
+
- **Color palette:** Limited colors (black, white, earth tones)
|
|
130
|
+
- **Line weight:** 2-4px (consistent thickness)
|
|
131
|
+
- **Resolution:** 1920x1080 (landscape master)
|
|
132
|
+
- **Background:** White or transparent
|
|
133
|
+
- **Character scale:** Consistent proportions
|
|
134
|
+
|
|
135
|
+
## Asset Generation
|
|
136
|
+
|
|
137
|
+
When no match found:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
const prompt = `
|
|
141
|
+
Simple History doodle style illustration.
|
|
142
|
+
Character: ${character_id}
|
|
143
|
+
Pose: ${pose}
|
|
144
|
+
Emotion: ${emotion}
|
|
145
|
+
Style: Thick black ink lines, clean line art, white background.
|
|
146
|
+
No photorealism. Exaggerated facial expression.
|
|
147
|
+
Single character only.
|
|
148
|
+
1920x1080 landscape.
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const imageUrl = await pollinations.generate({
|
|
152
|
+
prompt,
|
|
153
|
+
width: 1920,
|
|
154
|
+
height: 1080,
|
|
155
|
+
seed: deterministicSeed
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## API
|
|
160
|
+
|
|
161
|
+
See `asset-library.js` for implementation:
|
|
162
|
+
|
|
163
|
+
- `initialize()` - Load asset registry
|
|
164
|
+
- `findAsset(query)` - Search with metadata + semantic fallback
|
|
165
|
+
- `generateAsset(description)` - Create new asset
|
|
166
|
+
- `validateAsset(asset)` - Check style consistency
|
|
167
|
+
- `composeCharacter(identity, pose, emotion)` - Layer composition
|
|
168
|
+
|
|
169
|
+
## Schema Versioning
|
|
170
|
+
|
|
171
|
+
All assets track `schema_version: "1.0.0"` for evolution:
|
|
172
|
+
|
|
173
|
+
- **1.0.0:** Initial composable system
|
|
174
|
+
- **1.1.0:** (Future) Add animation keyframes
|
|
175
|
+
- **2.0.0:** (Future) 3D pose variants
|
|
176
|
+
|
|
177
|
+
## Performance
|
|
178
|
+
|
|
179
|
+
- **Metadata lookup:** <10ms (PostgreSQL indexed)
|
|
180
|
+
- **Semantic search:** <200ms (cached embeddings)
|
|
181
|
+
- **Asset composition:** <500ms (image layering)
|
|
182
|
+
- **Asset generation:** 5-15s (Pollinations API)
|
|
183
|
+
|
|
184
|
+
## Storage
|
|
185
|
+
|
|
186
|
+
- **Per identity:** ~2MB (base + poses + emotions)
|
|
187
|
+
- **Total library:** ~100MB for 47 composable assets
|
|
188
|
+
- **Cache efficiency:** 70%+ asset reuse across videos
|