@amg555/frameforge-mcp 2.3.2 → 2.3.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/.mcp.json +2 -4
- package/CHANGELOG.md +24 -1
- package/CLAUDE.md +104 -26
- package/README.md +17 -12
- package/SECURITY.md +1 -3
- package/package.json +7 -3
- package/plugins/video-pipeline/.claude-plugin/plugin.json +19 -0
- package/plugins/video-pipeline/skills/html-renderer/SKILL.md +292 -0
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +308 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +257 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/STORYBOARD.md +77 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/index.html +80 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +43 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +56 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +42 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +140 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +870 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +138 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +167 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +174 -0
- package/scripts/db-migrate.js +112 -0
- package/scripts/deep-validate.js +326 -0
- package/scripts/generate-initial-assets.js +342 -0
- package/scripts/html-renderer/new-project.mjs +408 -0
- package/scripts/html-renderer/transcribe.py +294 -0
- package/src/asset-library/README.md +2 -2
- package/src/asset-library/asset-library.js +1 -1
- package/src/cli/doctor.js +12 -5
- package/src/cli/make.js +52 -71
- package/src/cli/setup-keys.js +15 -17
- package/src/fact-checker/review-gate.js +5 -1
- package/src/harness/bin/harness.mjs +324 -0
- package/src/harness/lib/browser.mjs +137 -0
- package/src/harness/lib/capture.mjs +161 -0
- package/src/harness/lib/encode.mjs +89 -0
- package/src/harness/lib/jobs.mjs +135 -0
- package/src/harness/lib/pixels.mjs +137 -0
- package/src/harness/lib/sheets.mjs +164 -0
- package/src/harness/lib/verify-purity.mjs +119 -0
- package/src/harness/test/fixtures/broken-scene.html +27 -0
- package/src/harness/test/fixtures/reference-scene.html +23 -0
- package/src/harness/test/fixtures/sample-script.json +18 -0
- package/src/mcp-server/server.js +285 -144
- package/src/pipeline/telemetry.js +1 -0
- package/src/utils/log-redaction.js +5 -3
- package/src/video-renderer/renderer.js +12 -4
- package/src/worker/server-render-free.js +2 -3
- package/src/workflows/assets.js +20 -43
- package/src/workflows/ingest.js +5 -5
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* virtual-clock.js — FrameForge Deterministic Virtual Clock Shim
|
|
3
|
+
*
|
|
4
|
+
* Intercepts browser timing primitives to enable frame-exact, deterministic
|
|
5
|
+
* time control for headless video rendering.
|
|
6
|
+
*
|
|
7
|
+
* Laws enforced:
|
|
8
|
+
* 1. Read time, not frames (seek(t) is absolute time in seconds).
|
|
9
|
+
* 2. Seed everything (Math.random replaced with seeded PRNG).
|
|
10
|
+
* 3. No integrating physics (timers scheduled along virtual timeline).
|
|
11
|
+
* 4. Await document.fonts.ready before resolving seek().
|
|
12
|
+
*/
|
|
13
|
+
(() => {
|
|
14
|
+
let virtualTime = 0;
|
|
15
|
+
const rafCallbacks = new Map();
|
|
16
|
+
let rafIdCounter = 0;
|
|
17
|
+
const timeoutQueue = [];
|
|
18
|
+
let timeoutIdCounter = 0;
|
|
19
|
+
const startDate = Date.now();
|
|
20
|
+
|
|
21
|
+
window.__playwright = true;
|
|
22
|
+
|
|
23
|
+
// 1. Hijack performance.now & Date
|
|
24
|
+
window.performance.now = () => virtualTime;
|
|
25
|
+
const OriginalDate = window.Date;
|
|
26
|
+
class VirtualDate extends OriginalDate {
|
|
27
|
+
constructor(...args) {
|
|
28
|
+
if (args.length === 0) {
|
|
29
|
+
super(startDate + virtualTime);
|
|
30
|
+
} else {
|
|
31
|
+
super(...args);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
static now() {
|
|
35
|
+
return Math.floor(startDate + virtualTime);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
window.Date = VirtualDate;
|
|
39
|
+
|
|
40
|
+
// 2. Hijack requestAnimationFrame & cancelAnimationFrame
|
|
41
|
+
window.requestAnimationFrame = (callback) => {
|
|
42
|
+
const id = ++rafIdCounter;
|
|
43
|
+
rafCallbacks.set(id, callback);
|
|
44
|
+
return id;
|
|
45
|
+
};
|
|
46
|
+
window.cancelAnimationFrame = (id) => {
|
|
47
|
+
rafCallbacks.delete(id);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 3. Hijack setTimeout / clearTimeout / setInterval / clearInterval
|
|
51
|
+
window.setTimeout = (callback, delay = 0, ...args) => {
|
|
52
|
+
const id = ++timeoutIdCounter;
|
|
53
|
+
timeoutQueue.push({ id, runAt: virtualTime + Math.max(0, delay), callback, args, repeat: 0 });
|
|
54
|
+
timeoutQueue.sort((a, b) => a.runAt - b.runAt);
|
|
55
|
+
return id;
|
|
56
|
+
};
|
|
57
|
+
window.clearTimeout = (id) => {
|
|
58
|
+
const idx = timeoutQueue.findIndex(item => item.id === id);
|
|
59
|
+
if (idx !== -1) timeoutQueue.splice(idx, 1);
|
|
60
|
+
};
|
|
61
|
+
window.setInterval = (callback, interval = 0, ...args) => {
|
|
62
|
+
const id = ++timeoutIdCounter;
|
|
63
|
+
const step = Math.max(16, interval);
|
|
64
|
+
timeoutQueue.push({ id, runAt: virtualTime + step, callback, args, repeat: step });
|
|
65
|
+
timeoutQueue.sort((a, b) => a.runAt - b.runAt);
|
|
66
|
+
return id;
|
|
67
|
+
};
|
|
68
|
+
window.clearInterval = (id) => {
|
|
69
|
+
const idx = timeoutQueue.findIndex(item => item.id === id);
|
|
70
|
+
if (idx !== -1) timeoutQueue.splice(idx, 1);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 4. Seeded PRNG replacement for Math.random()
|
|
74
|
+
let seed = 0x2F6E2B1;
|
|
75
|
+
window.Math.random = () => {
|
|
76
|
+
seed = (seed + 0x6D2B79F5) | 0;
|
|
77
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
78
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
79
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// 5. Global seek contract
|
|
83
|
+
window.seek = async (tSeconds) => {
|
|
84
|
+
virtualTime = tSeconds * 1000;
|
|
85
|
+
|
|
86
|
+
// Flush due timeouts
|
|
87
|
+
while (timeoutQueue.length > 0 && timeoutQueue[0].runAt <= virtualTime) {
|
|
88
|
+
const item = timeoutQueue.shift();
|
|
89
|
+
try {
|
|
90
|
+
item.callback(...item.args);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("VirtualClock Timeout Error:", err);
|
|
93
|
+
}
|
|
94
|
+
if (item.repeat > 0) {
|
|
95
|
+
item.runAt = virtualTime + item.repeat;
|
|
96
|
+
timeoutQueue.push(item);
|
|
97
|
+
timeoutQueue.sort((a, b) => a.runAt - b.runAt);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Flush current RAF callbacks
|
|
102
|
+
const currentRafs = Array.from(rafCallbacks.values());
|
|
103
|
+
rafCallbacks.clear();
|
|
104
|
+
for (const cb of currentRafs) {
|
|
105
|
+
try {
|
|
106
|
+
cb(virtualTime);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error("VirtualClock RAF Error:", err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Pause & seek CSS animations
|
|
113
|
+
try {
|
|
114
|
+
if (document.getAnimations) {
|
|
115
|
+
document.getAnimations().forEach(a => {
|
|
116
|
+
if (a.playState !== 'paused') a.pause();
|
|
117
|
+
a.currentTime = virtualTime;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
|
|
122
|
+
// Custom scene render hook if exposed
|
|
123
|
+
if (typeof window.renderFrame === "function") {
|
|
124
|
+
await window.renderFrame(tSeconds);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Await font readiness
|
|
128
|
+
if (document.fonts && document.fonts.ready) {
|
|
129
|
+
await document.fonts.ready;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Bridge for renderer
|
|
134
|
+
window.__seek = window.seek;
|
|
135
|
+
|
|
136
|
+
window.__VIRTUAL_CLOCK_INSTALLED__ = true;
|
|
137
|
+
})();
|
|
138
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# MCP Tool Reference
|
|
2
|
+
|
|
3
|
+
FrameForge exposes 13 tools on the HTTP MCP server (`http://localhost:3210`)
|
|
4
|
+
and via the Cloudflare Remote MCP proxy. Any MCP-compatible client works:
|
|
5
|
+
IBM Bob, Claude Code, Codex, opencode, Cursor, Windsurf.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Agent-first path (recommended)
|
|
10
|
+
|
|
11
|
+
The IDE writes the scenes array; FrameForge renders it.
|
|
12
|
+
|
|
13
|
+
### create_video_from_script
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"scenes": [
|
|
18
|
+
{
|
|
19
|
+
"narration": "WiFi sends data as invisible radio waves.",
|
|
20
|
+
"caption": "RADIO WAVES",
|
|
21
|
+
"visual_description": "router emitting waves, flat illustration",
|
|
22
|
+
"camera_hint": "slow_zoom_in",
|
|
23
|
+
"music_mood": "calm"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"title": "How WiFi Works",
|
|
27
|
+
"skipImages": false,
|
|
28
|
+
"withMusic": true
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Returns: `{ productionId, masterPath, passed, sizeBytes, sceneCount }`
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Full pipeline path
|
|
37
|
+
|
|
38
|
+
### research_topic
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "topic": "How does GPS work?", "claimCount": 10, "style": "educational" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### write_script
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"productionId": "prod-001",
|
|
49
|
+
"claims": ["..."],
|
|
50
|
+
"topic": "How does GPS work?",
|
|
51
|
+
"persona": "tech explainer",
|
|
52
|
+
"sceneCount": 15
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Returns: `{ productionId, scenes, title, description, tags, sceneCount }`
|
|
57
|
+
|
|
58
|
+
### render_from_scenes
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{ "productionId": "prod-001", "skipImages": false, "withMusic": true }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### publish_video
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"productionId": "prod-001",
|
|
69
|
+
"masterPath": "/path/to/final_video.mp4",
|
|
70
|
+
"channels": ["file"],
|
|
71
|
+
"outputDir": "/path/to/output"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Channels:
|
|
76
|
+
- `file` — copy master + shorts to `outputDir` (or `N8N_FILES_DIR/published/`)
|
|
77
|
+
- `webhook` — POST JSON summary to `PUBLISH_WEBHOOK_URL`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Edit path
|
|
82
|
+
|
|
83
|
+
### ingest_media
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "files": ["/path/to/clip1.mp4", "/path/to/clip2.mp4"], "title": "My Video" }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### edit_scenes
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"productionId": "prod-001",
|
|
94
|
+
"edits": [{ "sceneIndex": 0, "field": "caption", "value": "NEW CAPTION" }]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## TTS
|
|
101
|
+
|
|
102
|
+
### multi_voice_tts
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"text": "The text to speak aloud.",
|
|
107
|
+
"outputPath": "/files-dir/voice_0.m4a",
|
|
108
|
+
"platform": "elevenlabs",
|
|
109
|
+
"voice": "your_cloned_voice_id",
|
|
110
|
+
"apiKey": "sk_..."
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Async job control
|
|
117
|
+
|
|
118
|
+
### queue_full_pipeline
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"topic": "Why planes avoid straight lines",
|
|
123
|
+
"style": "educational",
|
|
124
|
+
"channels": ["file"]
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Returns `{ jobId, status }` immediately. Poll `get_job_status` for completion.
|
|
129
|
+
|
|
130
|
+
### get_job_status
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "jobId": "job_1234567890_abc123" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Returns `{ status: "RUNNING"|"SUCCEEDED"|"FAILED", result, error }`
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## External platform
|
|
141
|
+
|
|
142
|
+
### external_platform_call
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{ "action": "ingest", "files": ["/path/to/clip.mp4"] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Actions: `ingest` | `render` | `status` | `download`.
|
|
149
|
+
Configure with `EXTERNAL_PLATFORM_URL` + `EXTERNAL_PLATFORM_KEY` in `.env`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Calling from any IDE
|
|
154
|
+
|
|
155
|
+
Any IDE that supports MCP calls these tools via JSON-RPC:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
POST http://localhost:3210
|
|
159
|
+
{
|
|
160
|
+
"method": "tools/call",
|
|
161
|
+
"params": {
|
|
162
|
+
"name": "create_video_from_script",
|
|
163
|
+
"arguments": { "scenes": [...] }
|
|
164
|
+
},
|
|
165
|
+
"id": 1
|
|
166
|
+
}
|
|
167
|
+
```
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Provider Environment Variable Reference
|
|
2
|
+
|
|
3
|
+
All secrets come from environment variables. Never hardcode them.
|
|
4
|
+
Copy `docs/env.template` → `.env` to get started.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## LLM Providers (`PROVIDER_LLM`)
|
|
9
|
+
|
|
10
|
+
### groq (default)
|
|
11
|
+
```bash
|
|
12
|
+
PROVIDER_LLM=groq
|
|
13
|
+
GROQ_API_KEY=gsk_...
|
|
14
|
+
# Optional overrides:
|
|
15
|
+
LLM_MODEL=llama-3.3-70b-versatile # default
|
|
16
|
+
LLM_BASE_URL=https://api.groq.com/openai/v1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### watsonx (IBM — enterprise / data-residency)
|
|
20
|
+
```bash
|
|
21
|
+
PROVIDER_LLM=watsonx
|
|
22
|
+
WATSONX_API_KEY=... # IBM Cloud API key
|
|
23
|
+
WATSONX_PROJECT_ID=... # from IBM Cloud → watsonx.ai → Projects
|
|
24
|
+
LLM_BASE_URL=https://us-south.ml.cloud.ibm.com/ml/v1 # or eu-de / jp-tok
|
|
25
|
+
LLM_MODEL=ibm/granite-3-3-8b-instruct # or granite-3-3-2b-instruct
|
|
26
|
+
```
|
|
27
|
+
Granite 3.3 supports function calling and 128 K context. All inference stays
|
|
28
|
+
in your IBM Cloud region — no data leaves your residency boundary.
|
|
29
|
+
|
|
30
|
+
### openai
|
|
31
|
+
```bash
|
|
32
|
+
PROVIDER_LLM=openai
|
|
33
|
+
OPENAI_API_KEY=sk-...
|
|
34
|
+
LLM_MODEL=gpt-4o # default
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### ollama (local, no key)
|
|
38
|
+
```bash
|
|
39
|
+
PROVIDER_LLM=ollama
|
|
40
|
+
LLM_BASE_URL=http://localhost:11434/api
|
|
41
|
+
LLM_MODEL=llama3.2 # whatever you have pulled
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## TTS Providers (`PROVIDER_TTS`)
|
|
47
|
+
|
|
48
|
+
### edge-tts (default — free, no key, 300+ voices, offline)
|
|
49
|
+
```bash
|
|
50
|
+
PROVIDER_TTS=edge-tts
|
|
51
|
+
# No key required. Needs: npm install -g edge-tts
|
|
52
|
+
# Voice list: edge-tts --list-voices
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### voxforge (free, private multi-engine neural TTS & voice cloning in Rust)
|
|
56
|
+
```bash
|
|
57
|
+
PROVIDER_TTS=voxforge
|
|
58
|
+
VOXFORGE_URL=http://localhost:5005 # or your self-hosted VoxForge workstation URL
|
|
59
|
+
# Optional:
|
|
60
|
+
VOXFORGE_KEY=... # optional auth token if enabled
|
|
61
|
+
VOXFORGE_VOICE=default # voice/clone reference
|
|
62
|
+
VOXFORGE_MODEL=voxforge-v1 # engine model (XTTS, Piper, Kokoro, Whisper)
|
|
63
|
+
```
|
|
64
|
+
Repo: [AMG555/VoxForge](https://github.com/AMG555/VoxForge) — local, private neural TTS & voice cloning workstation.
|
|
65
|
+
|
|
66
|
+
### elevenlabs (voice cloning)
|
|
67
|
+
```bash
|
|
68
|
+
PROVIDER_TTS=elevenlabs
|
|
69
|
+
ELEVENLABS_API_KEY=sk_...
|
|
70
|
+
# Optional:
|
|
71
|
+
ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM # default: Rachel
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### azure (enterprise)
|
|
75
|
+
```bash
|
|
76
|
+
PROVIDER_TTS=azure
|
|
77
|
+
AZURE_SPEECH_KEY=...
|
|
78
|
+
AZURE_SPEECH_REGION=eastus # or westeurope, etc.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### openai
|
|
82
|
+
```bash
|
|
83
|
+
PROVIDER_TTS=openai
|
|
84
|
+
OPENAI_API_KEY=sk-...
|
|
85
|
+
# Uses tts-1-hd model, alloy voice by default
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Visuals & Media
|
|
91
|
+
|
|
92
|
+
FrameForge is **code-driven first**. By default, scenes render using vector animations, kinetic typography, and user-provided media (`image_path` / `video_path`). Diffusion image generation is optional and disabled by default.
|
|
93
|
+
|
|
94
|
+
### Code-driven animation (default)
|
|
95
|
+
```bash
|
|
96
|
+
# Default mode — zero keys needed, broadcast vector quality
|
|
97
|
+
# Automatically creates animated cards, syntax blocks, terminals, and typography
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Optional Diffusion Image Providers (`ENABLE_AI_IMAGES=true`)
|
|
101
|
+
|
|
102
|
+
### pollinations (default — free, no key)
|
|
103
|
+
```bash
|
|
104
|
+
PROVIDER_IMAGE=pollinations
|
|
105
|
+
# No key required. Uses flux model.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### fal (fast, FLUX-based — recommended for production)
|
|
109
|
+
```bash
|
|
110
|
+
PROVIDER_IMAGE=fal
|
|
111
|
+
FAL_API_KEY=...
|
|
112
|
+
# Also accepted as: FALAI_API_KEY (legacy alias)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### replicate
|
|
116
|
+
```bash
|
|
117
|
+
PROVIDER_IMAGE=replicate
|
|
118
|
+
REPLICATE_API_TOKEN=r8_...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### dalle (DALL-E 3)
|
|
122
|
+
```bash
|
|
123
|
+
PROVIDER_IMAGE=dalle
|
|
124
|
+
OPENAI_API_KEY=sk-...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Delivery
|
|
130
|
+
|
|
131
|
+
### Webhook
|
|
132
|
+
```bash
|
|
133
|
+
PUBLISH_WEBHOOK_URL=https://your-receiver.example.com/webhook
|
|
134
|
+
REVIEW_NOTIFY_URL=https://your-receiver.example.com/review
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Local File (default)
|
|
138
|
+
```bash
|
|
139
|
+
# Copies master and shorts to output folder
|
|
140
|
+
# No external credentials needed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Telemetry / state (optional PostgreSQL)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/vidpipeline
|
|
149
|
+
```
|
|
150
|
+
Without this, state is stored to `.n8n-files/productions/` (JSON files)
|
|
151
|
+
and telemetry is written to `logs/usage.ndjson`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## IBM Bob specific
|
|
156
|
+
|
|
157
|
+
Bob discovers MCP servers from `~/.bob/mcp.json`:
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"mcpServers": {
|
|
161
|
+
"video-pipeline": {
|
|
162
|
+
"command": "node",
|
|
163
|
+
"args": ["/absolute/path/to/src/mcp-server/server.js"],
|
|
164
|
+
"env": {
|
|
165
|
+
"PROVIDER_LLM": "watsonx",
|
|
166
|
+
"WATSONX_API_KEY": "${WATSONX_API_KEY}",
|
|
167
|
+
"WATSONX_PROJECT_ID": "${WATSONX_PROJECT_ID}",
|
|
168
|
+
"PROVIDER_TTS": "edge-tts"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
Restart Bob after adding. The 6 tools appear in Bob's tool palette.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* db-migrate.js — Apply FrameForge PostgreSQL schema.
|
|
4
|
+
*
|
|
5
|
+
* Reads DATABASE_URL from environment and runs the production state-machine
|
|
6
|
+
* schema (CREATE TABLE IF NOT EXISTS productions). Safe to run multiple
|
|
7
|
+
* times — all statements are idempotent.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* DATABASE_URL=postgresql://user:pass@host:5432/frameforge npm run db:migrate
|
|
11
|
+
*
|
|
12
|
+
* Or directly:
|
|
13
|
+
* DATABASE_URL=... node scripts/db-migrate.js
|
|
14
|
+
*
|
|
15
|
+
* Required: pg (automatically available when DATABASE_URL is set on Render,
|
|
16
|
+
* Railway, Supabase, Neon, or any managed PostgreSQL host)
|
|
17
|
+
*
|
|
18
|
+
* The script uses only Node.js built-in pg calls — no ORM, no migration
|
|
19
|
+
* framework, no extra dependencies beyond the 'pg' package.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const DATABASE_URL = process.env.DATABASE_URL;
|
|
25
|
+
|
|
26
|
+
if (!DATABASE_URL) {
|
|
27
|
+
console.error('[db:migrate] ERROR: DATABASE_URL environment variable is not set.');
|
|
28
|
+
console.error(' Set it in .env or export it before running:');
|
|
29
|
+
console.error(' DATABASE_URL=postgresql://user:pass@localhost:5432/frameforge npm run db:migrate');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Schema SQL ────────────────────────────────────────────────────────────────
|
|
34
|
+
// Mirrors the inline CREATE TABLE in src/pipeline/state-machine.js _pgInit().
|
|
35
|
+
// Keep in sync if the state-machine schema changes.
|
|
36
|
+
|
|
37
|
+
const MIGRATIONS = [
|
|
38
|
+
{
|
|
39
|
+
name: '001_create_productions',
|
|
40
|
+
sql: `
|
|
41
|
+
CREATE TABLE IF NOT EXISTS productions (
|
|
42
|
+
production_id VARCHAR(64) PRIMARY KEY,
|
|
43
|
+
status VARCHAR(20) NOT NULL DEFAULT 'QUEUED',
|
|
44
|
+
pipeline_version VARCHAR(20),
|
|
45
|
+
schema_version VARCHAR(20),
|
|
46
|
+
topic TEXT,
|
|
47
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
48
|
+
started_at TIMESTAMP,
|
|
49
|
+
completed_at TIMESTAMP,
|
|
50
|
+
config_hash VARCHAR(32),
|
|
51
|
+
stages JSONB DEFAULT '{}',
|
|
52
|
+
metadata JSONB DEFAULT '{}'
|
|
53
|
+
);
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '002_productions_indexes',
|
|
58
|
+
sql: `
|
|
59
|
+
CREATE INDEX IF NOT EXISTS productions_status_idx
|
|
60
|
+
ON productions (status);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS productions_created_at_idx
|
|
62
|
+
ON productions (created_at DESC);
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ── Run migrations ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
async function run() {
|
|
70
|
+
let pg;
|
|
71
|
+
try {
|
|
72
|
+
pg = require('pg');
|
|
73
|
+
} catch {
|
|
74
|
+
console.error('[db:migrate] ERROR: "pg" package not found.');
|
|
75
|
+
console.error(' Install it: npm install pg');
|
|
76
|
+
console.error(' (Only required when DATABASE_URL is set — not needed for file-mode operation.)');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { Pool } = pg;
|
|
81
|
+
const pool = new Pool({ connectionString: DATABASE_URL, ssl: DATABASE_URL.includes('sslmode=disable') ? false : { rejectUnauthorized: false } });
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await pool.query('SELECT 1'); // connectivity check
|
|
85
|
+
console.log('[db:migrate] Connected to PostgreSQL.');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(`[db:migrate] ERROR: could not connect — ${err.message}`);
|
|
88
|
+
console.error(' Check DATABASE_URL and that the database server is reachable.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const migration of MIGRATIONS) {
|
|
93
|
+
try {
|
|
94
|
+
await pool.query(migration.sql);
|
|
95
|
+
console.log(`[db:migrate] ✔ ${migration.name}`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[db:migrate] FAILED: ${migration.name} — ${err.message}`);
|
|
98
|
+
await pool.end();
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await pool.end();
|
|
104
|
+
console.log('[db:migrate] All migrations applied. FrameForge PostgreSQL schema is ready.');
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(' Next steps:');
|
|
107
|
+
console.log(' 1. Set RENDER_MODE=api_only in your deployment environment variables.');
|
|
108
|
+
console.log(' 2. The StateMachine will switch to pg mode automatically when DATABASE_URL is set.');
|
|
109
|
+
console.log(' 3. Run `npm run doctor` to verify the full stack.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
run();
|