@effectnode/media 0.5.0 → 0.6.0
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/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +169 -83
- package/dist/backend/movie-backend/agent/tools/index.js +0 -2
- package/dist/backend/movie-backend/core.js +1 -1
- package/dist/backend/movie-backend/generation-queue.js +8 -1
- package/dist/backend/movie-backend/render-media.js +28 -254
- package/frontend/index.html +3 -5
- package/frontend/src/movie-app/SetupPage.tsx +1 -1
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +127 -23
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +28 -23
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +0 -33
- package/frontend/src/movie-app/components/ProjectManager.tsx +1 -1
- package/frontend/src/movie-app/index.css +46 -20
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +16 -7
- package/frontend/src/movie-app/stores/generationStore.ts +0 -1
- package/frontend/src/movie-app/stores/movieStudioStore.ts +27 -3
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
|
@@ -4,7 +4,7 @@ import { type Application } from "express";
|
|
|
4
4
|
* Persists the result to studio/:projectId/data/*.json and returns the parsed
|
|
5
5
|
* result. Used both by the HTTP route below and by the generation queue worker.
|
|
6
6
|
*/
|
|
7
|
-
export declare function generateMovieStudioBible(projectId: string, idea: string, model?: string): Promise<{
|
|
7
|
+
export declare function generateMovieStudioBible(projectId: string, idea: string, model?: string, onProgress?: (statusText: string, current: number, total: number) => void): Promise<{
|
|
8
8
|
characters: any[];
|
|
9
9
|
places: any[];
|
|
10
10
|
scenes: any[];
|
|
@@ -48,7 +48,7 @@ function toOpenAIMessages(messages) {
|
|
|
48
48
|
function buildSystemPrompt() {
|
|
49
49
|
const toolList = TOOLS.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
50
50
|
return [
|
|
51
|
-
"You are a media assistant that helps the user
|
|
51
|
+
"You are a media assistant that helps the user render videos in their workspace.",
|
|
52
52
|
"",
|
|
53
53
|
"Available tools:",
|
|
54
54
|
toolList,
|
|
@@ -56,34 +56,16 @@ function buildSystemPrompt() {
|
|
|
56
56
|
"Rules:",
|
|
57
57
|
"- Call a tool only when you actually need its result. Once you have enough information, answer the user directly WITHOUT calling any tool.",
|
|
58
58
|
"- Do not call the same tool twice with the same arguments.",
|
|
59
|
-
"- ALWAYS confirm with the user before generating
|
|
59
|
+
"- ALWAYS confirm with the user before generating. Never call `image_to_video_generation` or `text_to_video_generation` until the user has explicitly said yes.",
|
|
60
60
|
"",
|
|
61
61
|
"## Getting started",
|
|
62
62
|
"",
|
|
63
63
|
'When the user greets you (e.g. says "hi" or "hello") or asks for help, FIRST ask them what they want to do:',
|
|
64
|
-
"1.
|
|
65
|
-
"2. Render a video from
|
|
66
|
-
"3. Render a video from an image",
|
|
64
|
+
"1. Render a video from text",
|
|
65
|
+
"2. Render a video from an image",
|
|
67
66
|
"",
|
|
68
67
|
"Then follow the matching workflow below.",
|
|
69
68
|
"",
|
|
70
|
-
"## Workflow: Edit an image",
|
|
71
|
-
"",
|
|
72
|
-
"### Step 1 — Image",
|
|
73
|
-
"- Call `list_files` to see what is in the agent workspace.",
|
|
74
|
-
'- List every image file (files whose `kind` is "image") to the user as a numbered list so they can pick one.',
|
|
75
|
-
"- You may call `show_image` to preview an image before the user decides.",
|
|
76
|
-
"- If there are no images, tell the user and ask them to upload one first.",
|
|
77
|
-
"",
|
|
78
|
-
"### Step 2 — Edit instruction",
|
|
79
|
-
"- Plan the edit together with the user. Ask what they want to change: background, style, color, lighting, expression, object, and so on.",
|
|
80
|
-
"- Confirm the wording of the edit prompt before moving on.",
|
|
81
|
-
"",
|
|
82
|
-
"### Step 3 — Summary and confirmation",
|
|
83
|
-
"- Prepare a short summary containing the image and the edit instruction, and ask the user to confirm.",
|
|
84
|
-
"- Do NOT start editing yet. Wait for the user to say yes.",
|
|
85
|
-
"- Only after the user confirms, call `edit_image` with the image and prompt.",
|
|
86
|
-
"",
|
|
87
69
|
"## Workflow: Render a video",
|
|
88
70
|
"",
|
|
89
71
|
"To generate a video you need a motion PROMPT and a DURATION. An IMAGE is optional but recommended.",
|
|
@@ -151,53 +133,80 @@ function sanitizeTrace(messages) {
|
|
|
151
133
|
});
|
|
152
134
|
}
|
|
153
135
|
// ========== Movie Studio ==========
|
|
154
|
-
const
|
|
136
|
+
const MOVIE_STUDIO_CHARACTERS_PLAN_PROMPT = [
|
|
155
137
|
"You are a movie pre-production planner. Given a movie or story idea, identify the characters.",
|
|
156
138
|
"",
|
|
157
139
|
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
158
140
|
"",
|
|
159
|
-
'{"artStyle":"string","characters":[{"slug":"string","name":"string"
|
|
141
|
+
'{"artStyle":"string","characters":[{"slug":"string","name":"string"}]}',
|
|
160
142
|
"",
|
|
161
143
|
"Rules:",
|
|
162
144
|
'- "slug" is a short lowercase hyphenated identifier (e.g. "the-lamb").',
|
|
163
145
|
'- "artStyle" is ONE consistent art style for the entire film (e.g. cinematic photorealistic, 3D animation, hand-painted watercolor, anime).',
|
|
164
146
|
'- If the idea does not mention an art style, set "artStyle" to "photo realistic render".',
|
|
165
|
-
"- A character's imagePrompt is a standalone text-to-image prompt that begins with the character's name and fully describes their appearance (age, face, build, outfit, distinctive features) together with their cultural background — the era/age, culture, ethnicity, and region the character belongs to — so the generated image is historically and culturally accurate and consistent.",
|
|
166
|
-
"- Apply the same artStyle to every character imagePrompt.",
|
|
167
|
-
"- Write prompts as natural-language English sentences, never comma-separated keyword tags.",
|
|
168
147
|
].join("\n");
|
|
169
|
-
const
|
|
148
|
+
const CHARACTER_IMAGE_PROMPT = [
|
|
149
|
+
"You are a movie pre-production planner. Given a movie idea, its art style, and one character, write that character's standalone image prompt.",
|
|
150
|
+
"",
|
|
151
|
+
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
152
|
+
"",
|
|
153
|
+
'{"imagePrompt":"string"}',
|
|
154
|
+
"",
|
|
155
|
+
"Rules:",
|
|
156
|
+
"- The imagePrompt begins with the character's name and fully describes their appearance (age, face, build, outfit, distinctive features) together with their cultural background — the era/age, culture, ethnicity, and region the character belongs to — so the generated image is historically and culturally accurate and consistent.",
|
|
157
|
+
"- Apply the given artStyle.",
|
|
158
|
+
"- Write the prompt as natural-language English sentences, never comma-separated keyword tags.",
|
|
159
|
+
].join("\n");
|
|
160
|
+
const MOVIE_STUDIO_PLACES_PLAN_PROMPT = [
|
|
170
161
|
"You are a movie pre-production planner. Given a movie idea, its art style, and its characters, identify the places/locations.",
|
|
171
162
|
"",
|
|
172
163
|
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
173
164
|
"",
|
|
174
|
-
'{"places":[{"slug":"string","name":"string"
|
|
165
|
+
'{"places":[{"slug":"string","name":"string"}]}',
|
|
175
166
|
"",
|
|
176
167
|
"Rules:",
|
|
177
168
|
'- "slug" is a short lowercase hyphenated identifier (e.g. "sunny-meadow").',
|
|
178
|
-
"- A place's imagePrompt is a standalone text-to-image prompt that fully describes the location/environment (time of day, lighting, atmosphere, visual style).",
|
|
179
|
-
"- Apply the given artStyle to every place imagePrompt so it matches the characters.",
|
|
180
|
-
"- Write prompts as natural-language English sentences, never comma-separated keyword tags.",
|
|
181
169
|
].join("\n");
|
|
182
|
-
const
|
|
170
|
+
const PLACE_IMAGE_PROMPT = [
|
|
171
|
+
"You are a movie pre-production planner. Given a movie idea, its art style, and one place, write that place's standalone image prompt.",
|
|
172
|
+
"",
|
|
173
|
+
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
174
|
+
"",
|
|
175
|
+
'{"imagePrompt":"string"}',
|
|
176
|
+
"",
|
|
177
|
+
"Rules:",
|
|
178
|
+
"- The imagePrompt fully describes the location/environment (time of day, lighting, atmosphere, visual style).",
|
|
179
|
+
"- Apply the given artStyle.",
|
|
180
|
+
"- Write the prompt as natural-language English sentences, never comma-separated keyword tags.",
|
|
181
|
+
].join("\n");
|
|
182
|
+
const MOVIE_STUDIO_SCENES_PLAN_PROMPT = [
|
|
183
183
|
"You are a movie pre-production planner. Given a movie idea, its art style, its characters, and its places, break the story into scenes.",
|
|
184
184
|
"",
|
|
185
185
|
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
186
186
|
"",
|
|
187
|
-
'{"scenes":[{"slug":"string","duration":number,"
|
|
187
|
+
'{"scenes":[{"slug":"string","duration":number,"characterSlugs":["string"],"placeSlug":"string"}]}',
|
|
188
188
|
"",
|
|
189
189
|
"Rules:",
|
|
190
|
-
|
|
191
|
-
"-
|
|
192
|
-
"- Never reduce or summarize the dialogue:
|
|
193
|
-
"- Preserve as much detail as possible in every description and imagePrompt.",
|
|
190
|
+
'- "slug" is a short lowercase hyphenated identifier (e.g. "opening-meadow").',
|
|
191
|
+
"- Break the story into a natural number of scenes. Group related action beats and dialogue together — do not inflate the scene count by splitting every moment into its own scene.",
|
|
192
|
+
"- Never reduce or summarize the dialogue: keep every spoken line, grouping multiple lines into a scene where it reads naturally.",
|
|
194
193
|
"- Each scene references the characters and the place involved via their slugs (characterSlugs is a list; placeSlug is a single slug).",
|
|
195
|
-
"- A scene's imagePrompt is ONE coherent shot that combines the referenced characters AND the place together, using the given artStyle.",
|
|
196
|
-
"- scriptLines is one entry per spoken line, each with the speaking character's slug and the exact spoken line. Include every single line of dialogue in the scene.",
|
|
197
|
-
"- voiceOver is the narration/voiceover for that scene (use an empty string when there is none).",
|
|
198
194
|
"- Each scene has a duration in seconds (typically 3-20) matching how long the shot should last.",
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
].join("\n");
|
|
196
|
+
const SCENE_DETAIL_PROMPT = [
|
|
197
|
+
"You are a movie pre-production planner. Given a movie idea, its art style, its characters, its places, and one scene, write that scene's full details.",
|
|
198
|
+
"",
|
|
199
|
+
"Return ONLY valid JSON (no markdown fences, no commentary) matching this exact shape:",
|
|
200
|
+
"",
|
|
201
|
+
'{"description":"string","scriptLines":[{"characterSlug":"string","line":"string"}],"voiceOver":"string","imagePrompt":"string"}',
|
|
202
|
+
"",
|
|
203
|
+
"Rules:",
|
|
204
|
+
"- description is a short description of the scene's action.",
|
|
205
|
+
"- scriptLines is one entry per spoken line, each with the speaking character's slug and the exact spoken line. Include every line of dialogue that belongs to this scene.",
|
|
206
|
+
"- voiceOver is the narration/voiceover for that scene (use an empty string when there is none).",
|
|
207
|
+
"- imagePrompt is ONE coherent shot that combines the referenced characters AND the place together, using the given artStyle.",
|
|
208
|
+
"- Apply the given artStyle to the imagePrompt.",
|
|
209
|
+
"- Write descriptions, prompts and dialogue as natural-language English sentences, never comma-separated keyword tags.",
|
|
201
210
|
].join("\n");
|
|
202
211
|
/** Extract the first JSON object from a model response (handles code fences). */
|
|
203
212
|
function extractJsonObject(text) {
|
|
@@ -217,7 +226,7 @@ function extractJsonObject(text) {
|
|
|
217
226
|
* Persists the result to studio/:projectId/data/*.json and returns the parsed
|
|
218
227
|
* result. Used both by the HTTP route below and by the generation queue worker.
|
|
219
228
|
*/
|
|
220
|
-
export async function generateMovieStudioBible(projectId, idea, model) {
|
|
229
|
+
export async function generateMovieStudioBible(projectId, idea, model, onProgress) {
|
|
221
230
|
const resolvedModel = typeof model === "string" && model.trim() ? model.trim() : DEFAULT_MODEL;
|
|
222
231
|
const client = new OpenAI({
|
|
223
232
|
baseURL: "http://localhost:8881/v1",
|
|
@@ -229,51 +238,127 @@ export async function generateMovieStudioBible(projectId, idea, model) {
|
|
|
229
238
|
return Number.isFinite(n) ? n : 0;
|
|
230
239
|
};
|
|
231
240
|
const ask = async (systemPrompt, userContent) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
let prompt = systemPrompt;
|
|
242
|
+
let lastError = null;
|
|
243
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
244
|
+
try {
|
|
245
|
+
const completion = await client.chat.completions.create({
|
|
246
|
+
model: resolvedModel,
|
|
247
|
+
messages: [
|
|
248
|
+
{ role: "system", content: prompt },
|
|
249
|
+
{ role: "user", content: userContent },
|
|
250
|
+
],
|
|
251
|
+
temperature: 0.7,
|
|
252
|
+
max_tokens: 8192,
|
|
253
|
+
});
|
|
254
|
+
const text = completion.choices?.[0]?.message?.content ?? "";
|
|
255
|
+
return extractJsonObject(text);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
lastError = e;
|
|
259
|
+
// The model likely truncated its JSON — retry with an explicit
|
|
260
|
+
// instruction to emit one complete, valid JSON object.
|
|
261
|
+
prompt = `${systemPrompt}\n\nIMPORTANT: Return the complete JSON object in a single response. Do not truncate; close every string, object, and array.`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw lastError instanceof Error
|
|
265
|
+
? lastError
|
|
266
|
+
: new Error("Failed to parse model JSON response");
|
|
267
|
+
};
|
|
268
|
+
// Generate one item through the model, falling back to `fallback` on failure
|
|
269
|
+
// (after the ask-level retries) so a single bad item never fails the whole
|
|
270
|
+
// production bible.
|
|
271
|
+
const generateItem = async (systemPrompt, userContent, fallback) => {
|
|
272
|
+
try {
|
|
273
|
+
return await ask(systemPrompt, userContent);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
// Progress reporting (SSE-synced to the UI by the queue worker).
|
|
280
|
+
let current = 0;
|
|
281
|
+
let total = 0;
|
|
282
|
+
const addTotal = (n) => {
|
|
283
|
+
total += n;
|
|
242
284
|
};
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
285
|
+
const note = (statusText) => onProgress?.(statusText, current, total);
|
|
286
|
+
const advance = (statusText) => {
|
|
287
|
+
current += 1;
|
|
288
|
+
onProgress?.(statusText, current, total);
|
|
289
|
+
};
|
|
290
|
+
// 1. Character plan — a compact list of slugs/names + the film's art style.
|
|
291
|
+
note("Identifying characters…");
|
|
292
|
+
const charPlan = await ask(MOVIE_STUDIO_CHARACTERS_PLAN_PROMPT, idea.trim());
|
|
293
|
+
const artStyle = toStr(charPlan?.artStyle) || "photo realistic render";
|
|
294
|
+
const charList = (Array.isArray(charPlan.characters) ? charPlan.characters : [])
|
|
295
|
+
.map((c) => ({ slug: toStr(c?.slug), name: toStr(c?.name) }))
|
|
296
|
+
.filter((c) => c.slug);
|
|
297
|
+
addTotal(charList.length);
|
|
298
|
+
// 2. Character details — one image prompt per character (agentic loop).
|
|
299
|
+
const characters = [];
|
|
300
|
+
for (const c of charList) {
|
|
301
|
+
advance(`Generating character image: ${c.name || c.slug}`);
|
|
302
|
+
const detail = await generateItem(CHARACTER_IMAGE_PROMPT, `Art style: ${artStyle}\n\nIdea: ${idea.trim()}\n\nCharacter slug: ${c.slug}\nCharacter name: ${c.name}`, { imagePrompt: "" });
|
|
303
|
+
characters.push({
|
|
304
|
+
slug: c.slug,
|
|
305
|
+
name: c.name,
|
|
306
|
+
imagePrompt: toStr(detail?.imagePrompt),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// 3. Place plan + details.
|
|
310
|
+
note("Identifying places…");
|
|
311
|
+
const placePlan = await ask(MOVIE_STUDIO_PLACES_PLAN_PROMPT, `Art style: ${artStyle}\n\nIdea: ${idea.trim()}\n\nCharacters:\n${JSON.stringify(characters.map((c) => ({ slug: c.slug, name: c.name })))}`);
|
|
312
|
+
const placeList = (Array.isArray(placePlan.places) ? placePlan.places : [])
|
|
313
|
+
.map((p) => ({ slug: toStr(p?.slug), name: toStr(p?.name) }))
|
|
314
|
+
.filter((p) => p.slug);
|
|
315
|
+
addTotal(placeList.length);
|
|
316
|
+
const places = [];
|
|
317
|
+
for (const p of placeList) {
|
|
318
|
+
advance(`Generating place image: ${p.name || p.slug}`);
|
|
319
|
+
const detail = await generateItem(PLACE_IMAGE_PROMPT, `Art style: ${artStyle}\n\nIdea: ${idea.trim()}\n\nPlace slug: ${p.slug}\nPlace name: ${p.name}`, { imagePrompt: "" });
|
|
320
|
+
places.push({
|
|
321
|
+
slug: p.slug,
|
|
322
|
+
name: p.name,
|
|
323
|
+
imagePrompt: toStr(detail?.imagePrompt),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// 4. Scene plan + details — one scene's full details per request.
|
|
327
|
+
note("Planning scenes…");
|
|
328
|
+
const scenePlan = await ask(MOVIE_STUDIO_SCENES_PLAN_PROMPT, `Art style: ${artStyle}\n\nIdea: ${idea.trim()}\n\nCharacters:\n${JSON.stringify(characters.map((c) => ({ slug: c.slug, name: c.name })))}\n\nPlaces:\n${JSON.stringify(places.map((p) => ({ slug: p.slug, name: p.name })))}`);
|
|
329
|
+
const sceneList = (Array.isArray(scenePlan.scenes) ? scenePlan.scenes : [])
|
|
330
|
+
.map((s) => ({
|
|
261
331
|
slug: toStr(s?.slug),
|
|
262
332
|
duration: toNum(s?.duration),
|
|
263
|
-
description: toStr(s?.description),
|
|
264
333
|
characterSlugs: Array.isArray(s?.characterSlugs)
|
|
265
334
|
? s.characterSlugs.map(toStr).filter(Boolean)
|
|
266
335
|
: [],
|
|
267
336
|
placeSlug: toStr(s?.placeSlug),
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
337
|
+
}))
|
|
338
|
+
.filter((s) => s.slug);
|
|
339
|
+
addTotal(sceneList.length);
|
|
340
|
+
const scenes = [];
|
|
341
|
+
for (const s of sceneList) {
|
|
342
|
+
advance(`Writing scene: ${s.slug}`);
|
|
343
|
+
const detail = await generateItem(SCENE_DETAIL_PROMPT, `Art style: ${artStyle}\n\nIdea: ${idea.trim()}\n\nCharacters:\n${JSON.stringify(characters.map((c) => ({ slug: c.slug, name: c.name })))}\n\nPlaces:\n${JSON.stringify(places.map((p) => ({ slug: p.slug, name: p.name })))}\n\nScene slug: ${s.slug}\nDuration: ${s.duration}s\nCharacter slugs: ${s.characterSlugs.join(", ")}\nPlace slug: ${s.placeSlug}`, { description: "", scriptLines: [], voiceOver: "", imagePrompt: "" });
|
|
344
|
+
scenes.push({
|
|
345
|
+
slug: s.slug,
|
|
346
|
+
duration: s.duration,
|
|
347
|
+
description: toStr(detail?.description),
|
|
348
|
+
characterSlugs: s.characterSlugs,
|
|
349
|
+
placeSlug: s.placeSlug,
|
|
350
|
+
scriptLines: Array.isArray(detail?.scriptLines)
|
|
351
|
+
? detail.scriptLines
|
|
352
|
+
.map((l) => ({
|
|
353
|
+
characterSlug: toStr(l?.characterSlug),
|
|
354
|
+
line: toStr(l?.line),
|
|
355
|
+
}))
|
|
356
|
+
.filter((l) => l.line)
|
|
357
|
+
: [],
|
|
358
|
+
voiceOver: toStr(detail?.voiceOver),
|
|
359
|
+
imagePrompt: toStr(detail?.imagePrompt),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
277
362
|
// Persist the generated production bible to studio/:projectId/data/*.json
|
|
278
363
|
const dataDir = movieStudioDataDir(projectId);
|
|
279
364
|
ensureDir(dataDir);
|
|
@@ -684,7 +769,7 @@ export async function agentBackend({ app, getUvPath: _getUvPath, backendPort, })
|
|
|
684
769
|
}
|
|
685
770
|
});
|
|
686
771
|
app.post("/api/movie-studio/state", (req, res) => {
|
|
687
|
-
const { projectId, idea, result, assets, videos, sceneImages } = req.body || {};
|
|
772
|
+
const { projectId, idea, result, assets, videos, sceneImages, renderedScenes, } = req.body || {};
|
|
688
773
|
if (!projectId || !/^[a-zA-Z0-9_-]{1,64}$/.test(String(projectId))) {
|
|
689
774
|
res.status(400).json({ error: "Invalid project ID" });
|
|
690
775
|
return;
|
|
@@ -697,6 +782,7 @@ export async function agentBackend({ app, getUvPath: _getUvPath, backendPort, })
|
|
|
697
782
|
assets: Array.isArray(assets) ? assets : [],
|
|
698
783
|
videos: Array.isArray(videos) ? videos : [],
|
|
699
784
|
sceneImages: Array.isArray(sceneImages) ? sceneImages : [],
|
|
785
|
+
renderedScenes: Array.isArray(renderedScenes) ? renderedScenes : [],
|
|
700
786
|
}, null, 2), "utf-8");
|
|
701
787
|
res.json({ ok: true });
|
|
702
788
|
});
|
|
@@ -7,7 +7,6 @@ import removeFileTool from "./remove-file.js";
|
|
|
7
7
|
import renameFileTool from "./rename-file.js";
|
|
8
8
|
import grepFilesTool from "./grep-files.js";
|
|
9
9
|
import showImageTool from "./show-image.js";
|
|
10
|
-
import editImageTool from "./edit-image.js";
|
|
11
10
|
import imageToVideoGenerationTool from "./image-to-video-generation.js";
|
|
12
11
|
import textToVideoGenerationTool from "./text-to-video-generation.js";
|
|
13
12
|
import stitchVideosTool from "./stitch-videos.js";
|
|
@@ -21,7 +20,6 @@ export const TOOLS = [
|
|
|
21
20
|
renameFileTool,
|
|
22
21
|
grepFilesTool,
|
|
23
22
|
showImageTool,
|
|
24
|
-
editImageTool,
|
|
25
23
|
imageToVideoGenerationTool,
|
|
26
24
|
textToVideoGenerationTool,
|
|
27
25
|
stitchVideosTool,
|
|
@@ -866,7 +866,7 @@ async function streamProcessOutput(readable, prefix, send) {
|
|
|
866
866
|
// "--model",
|
|
867
867
|
// "dgrauet/ltx-2.3-mlx-q4",
|
|
868
868
|
// "--prompt",
|
|
869
|
-
// `${JSON.stringify("No text on screen. a 5 years old cute lamb wanting to have a hug, he says: Hi! Thank you for using
|
|
869
|
+
// `${JSON.stringify("No text on screen. a 5 years old cute lamb wanting to have a hug, he says: Hi! Thank you for using EffectNode Media!")}`,
|
|
870
870
|
// "--distilled",
|
|
871
871
|
// "--low-ram",
|
|
872
872
|
// "--frames",
|
|
@@ -379,7 +379,9 @@ const handlers = {
|
|
|
379
379
|
throw new Error("Idea is required");
|
|
380
380
|
}
|
|
381
381
|
ctx.log(`Planning production bible (characters → places → scenes)…\n`);
|
|
382
|
-
const result = await generateMovieStudioBible(ctx.projectId, idea.trim(), typeof model === "string" && model.trim() ? model.trim() : undefined)
|
|
382
|
+
const result = await generateMovieStudioBible(ctx.projectId, idea.trim(), typeof model === "string" && model.trim() ? model.trim() : undefined, (statusText, current, total) => {
|
|
383
|
+
ctx.update({ statusText, progress: { current, total } });
|
|
384
|
+
});
|
|
383
385
|
ctx.log("Production bible ready.\n");
|
|
384
386
|
return result;
|
|
385
387
|
},
|
|
@@ -574,6 +576,11 @@ function syncToMovieStudioState(projectId, type, result) {
|
|
|
574
576
|
if (type === "render-videos" || type === "render") {
|
|
575
577
|
state.videos = Array.isArray(result?.videos) ? result.videos : state.videos ?? [];
|
|
576
578
|
}
|
|
579
|
+
if (type === "render") {
|
|
580
|
+
state.renderedScenes = Array.isArray(result?.renderedScenes)
|
|
581
|
+
? result.renderedScenes
|
|
582
|
+
: state.renderedScenes ?? [];
|
|
583
|
+
}
|
|
577
584
|
if (type === "regenerate-asset" && result) {
|
|
578
585
|
const key = `${result.kind}:${result.slug}`;
|
|
579
586
|
const arr = Array.isArray(state.assets) ? state.assets : [];
|