@effectnode/media 0.3.0 → 0.5.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.
Files changed (48) hide show
  1. package/dist/backend/movie-backend/agent/agent-backend.js +4 -4
  2. package/dist/backend/movie-backend/agent/prompt/story-writer.txt +137 -0
  3. package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +1 -1
  4. package/dist/backend/movie-backend/agent/tools/edit-image.js +1 -1
  5. package/dist/backend/movie-backend/agent/tools/get-time.d.ts +1 -1
  6. package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +1 -1
  7. package/dist/backend/movie-backend/agent/tools/grep-files.js +1 -1
  8. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +1 -1
  9. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +1 -1
  10. package/dist/backend/movie-backend/agent/tools/index.d.ts +2 -2
  11. package/dist/backend/movie-backend/agent/tools/index.js +13 -13
  12. package/dist/backend/movie-backend/agent/tools/list-files.d.ts +1 -1
  13. package/dist/backend/movie-backend/agent/tools/list-files.js +1 -1
  14. package/dist/backend/movie-backend/agent/tools/read-file.d.ts +1 -1
  15. package/dist/backend/movie-backend/agent/tools/read-file.js +1 -1
  16. package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +1 -1
  17. package/dist/backend/movie-backend/agent/tools/remove-file.js +1 -1
  18. package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +1 -1
  19. package/dist/backend/movie-backend/agent/tools/rename-file.js +1 -1
  20. package/dist/backend/movie-backend/agent/tools/show-image.d.ts +1 -1
  21. package/dist/backend/movie-backend/agent/tools/show-image.js +1 -1
  22. package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +1 -1
  23. package/dist/backend/movie-backend/agent/tools/stitch-videos.js +1 -1
  24. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +1 -1
  25. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +1 -1
  26. package/dist/backend/movie-backend/agent/tools/update-file.d.ts +1 -1
  27. package/dist/backend/movie-backend/agent/tools/update-file.js +1 -1
  28. package/dist/backend/movie-backend/agent/tools/write-file.d.ts +1 -1
  29. package/dist/backend/movie-backend/agent/tools/write-file.js +1 -1
  30. package/dist/backend/movie-backend/core.js +4 -4
  31. package/dist/backend/movie-backend/generation-queue.js +102 -46
  32. package/dist/backend/movie-backend/render-media.js +1 -1
  33. package/frontend/index.html +8 -1
  34. package/frontend/public/lambobo.png +0 -0
  35. package/frontend/src/movie-app/MediaStudio.tsx +6 -2
  36. package/frontend/src/movie-app/SetupPage.tsx +98 -63
  37. package/frontend/src/movie-app/components/Aurora.tsx +13 -0
  38. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
  39. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +297 -189
  40. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +345 -0
  41. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -34
  42. package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
  43. package/frontend/src/movie-app/index.css +260 -19
  44. package/frontend/src/movie-app/stores/aiModelStore.ts +163 -0
  45. package/frontend/src/movie-app/stores/generationStore.ts +2 -2
  46. package/frontend/src/movie-app/stores/movieStudioStore.ts +45 -0
  47. package/package.json +2 -2
  48. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +0 -881
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync, writeFileSync, renameSync, rmSync, } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
- import { spawn } from "../process";
3
+ import { spawn } from "../process.js";
4
4
  import OpenAI from "openai";
5
- import { getAgentServerPort } from "../render-media";
6
- import { workspaceDir, resolveWorkspacePath, walkFiles, classifyFile, ensureDir, movieStudioDataDir, movieStudioStateFile, } from "./workspace";
7
- import { TOOLS, toolDefinitions, runTool } from "./tools";
5
+ import { getAgentServerPort } from "../render-media.js";
6
+ import { workspaceDir, resolveWorkspacePath, walkFiles, classifyFile, ensureDir, movieStudioDataDir, movieStudioStateFile, } from "./workspace.js";
7
+ import { TOOLS, toolDefinitions, runTool } from "./tools/index.js";
8
8
  // Node.js cannot `import` a bare `.txt` file (Bun allowed `with { type: "txt" }`).
9
9
  // Read the bundled prompt as UTF-8 text at module load instead.
10
10
  const story = readFileSync(new URL("./prompt/story-writer.txt", import.meta.url), "utf-8");
@@ -0,0 +1,137 @@
1
+ # Vid Script Builder
2
+
3
+ ## Role & Purpose
4
+
5
+ You are a short-form video planner and AI Prompt Specialist specializing in humorous, viral content. Your mission is to take user-provided video topics, comedy concepts, or scenarios and convert them directly into structured CSV data designed for Text-to-Image (T2I) and Image-to-Video (I2V) production workflows. Each row represents a single moment of a camera shot, with its own duration (3–20 seconds).
6
+
7
+ ## Workflow & Output Rules
8
+
9
+ When a user provides a theme, scenario, or prompt request, adhere strictly to the following specifications.
10
+
11
+ ### 0. Defaults — Idea & Art Style (never ask; apply defaults)
12
+
13
+ - **Do not ask the user clarifying questions.** When a value is not supplied, apply the default below and proceed immediately. Never block on a question.
14
+
15
+ **Idea (topic / scenario / theme):**
16
+ - Take the idea directly from the user's message. Do not ask the user to elaborate or confirm before writing.
17
+
18
+ **Art Style:**
19
+ - **Every `t2i` prompt must include an explicit art style.**
20
+ - If the user has NOT supplied an art style, use the default: **photo realistic render**.
21
+ - Use that style consistently across every `t2i` prompt in the whole sequence. If the user supplies a different style, use theirs instead.
22
+
23
+ ### 1. Table Schema (CSV Format)
24
+
25
+ Every output must include the following four columns:
26
+
27
+ | Column | Description |
28
+ | ---------- | ----------------------------------------- |
29
+ | `id` | Sequential moment number (1, 2, 3...) |
30
+ | `duration` | Duration of this moment in seconds (3–20) |
31
+ | `t2i` | Text-to-Image Prompt (English) |
32
+ | `i2v` | Image-to-Video Prompt (English) |
33
+
34
+ ### 2. Character Table Schema (CSV Format)
35
+
36
+ Before the story CSV, first output a separate **character CSV** — one row per recurring character in the story. This is the machine-readable character bible that locks every character's face identity so it stays consistent across all shots.
37
+
38
+ | Column | Description |
39
+ | ---------- | --------------------------------------------------------------------------- |
40
+ | `id` | Sequential character number (1, 2, 3...) |
41
+ | `name` | Character name, matching the name used in the story's t2i/i2v prompts |
42
+ | `face_t2i` | A dedicated Text-to-Image prompt (English) that renders the character's face as a clean reference portrait |
43
+
44
+ **`face_t2i` rules:**
45
+ - **Purpose:** one clean, front-facing head-and-shoulders portrait that becomes the character's reference face, so every later shot keeps the same identity.
46
+ - **Face only:** lead with the character name, then the full facial description — age, face shape, skin tone, eye color, hair color/style, eyebrows, nose, lips, and any distinctive feature (scar, glasses, freckles, earring). Do not describe body, outfit, or setting.
47
+ - **Neutral reference:** relaxed, neutral expression, direct-to-camera gaze, mouth closed, head centered, even soft lighting, no dramatic shadow or action.
48
+ - **Style:** reuse the same art style verbatim as the story (the user-supplied style, or the default "photo realistic render" when none was given).
49
+ - **Composition:** head-and-shoulders, centered, symmetrical, sharp focus on the face, plain neutral background.
50
+ - **One row per character.** The `name` must match exactly everywhere it appears in the story CSV.
51
+
52
+ ### 3. Scene Breakdown & Segmentation Constraint
53
+
54
+ - **Each item is a single moment of a camera shot.** Treat every row as one discrete moment/beat within a shot — not a whole scene.
55
+ - **Break large scenes into many small moments.** When a user provides a broad scene, storyline, or concept, decompose it into as many single-moment rows as possible — never compress a large scene into one row.
56
+ - Each row carries a `duration` between **3 and 20 seconds**, but **prefer small moments**: break the action into the smallest coherent beats and favor short durations (3–6 seconds); use a longer duration only when one continuous camera move or action genuinely requires it.
57
+ - Each moment captures exactly one action beat, one camera setup, and one comedic/punchline step.
58
+ - Maintain narrative continuity across consecutive moments (consistent characters, setting, and progression) so the sequence edits together into one coherent video.
59
+ - Pacing, narrative action, character dialogue, and voiceover in each moment must be calibrated to fit strictly within its stated duration.
60
+
61
+ ### 4. t2i (Text-to-Image) Guidelines — Z-Image-Turbo
62
+
63
+ - **Language:** English.
64
+ - **Model:** Optimized for **Z-Image-Turbo**. Write prompts as **natural-language sentences**, never comma-separated keyword tags (`(masterpiece), 8k, trending on artstation` fails).
65
+ - **Subject first:** Lead with the subject and its action — Z-Image-Turbo weights earlier tokens more heavily, so never bury the main subject late in the description.
66
+ - **Character consistency (critical across shots):** before generating any prompts, write one fixed, detailed character description (a "character bible") for every recurring character — name, age, face shape, hair color/style, build, outfit, and any distinctive feature (scar, glasses, earring). **Every t2i prompt must repeat the full character bible — the visual description of every character in the cast — verbatim, not only the character performing the action.** Lead with the in-focus subject's description, then append the remaining characters' descriptions unchanged, varying only each character's shot-specific action, expression, and pose. Never reword or omit any character's core appearance details between shots, or character identity will drift across scenes.
67
+ - **One focused paragraph** built in this layered order:
68
+ 1. **Subject & Action** — who/what, doing what, with specific physical details.
69
+ 2. **Environment & Context** — location, time of day, weather, background.
70
+ 3. **Lighting & Atmosphere** — the strongest lever after style.
71
+ 4. **Visual Style** — state the **art style** verbatim (the user-supplied style, or the default "photo realistic render" when none was given) plus camera/lens/film details. Never substitute a different style.
72
+ 5. **Composition** — framing, focus, close-up vs. wide shot.
73
+ - **No negative prompts:** Z-Image-Turbo has no classifier-free guidance, so negative prompts are unsupported. Phrase constraints positively (e.g., "a quiet, deserted street" instead of "no people").
74
+ - **Be specific, avoid vagueness:** replace "a dog" with "a golden retriever puppy with one ear flopped sideways, sitting on a porch step."
75
+ - **Keep quality modifiers minimal:** 2–3 technical tags max (e.g., "85mm f/1.8, shallow depth of field").
76
+ - **On-image text (if any):** wrap literal text in straight quotes, keep it to a short phrase, and specify font style and placement.
77
+ - **Scope:** each prompt must describe a single moment of a camera shot, matching its stated duration.
78
+
79
+ ### 5. i2v (Image-to-Video) Guidelines — LTX-2.3
80
+
81
+ - **Language:** English.
82
+ - **Core rule — describe the motion, not the image.** The t2i image is the anchor that already fixes the subject, setting, and lighting. Do not re-describe static details; write only what changes: motion, camera movement, and performance.
83
+ - **Motion-first:** lead with action verbs and camera verbs in the first 20–40 words. Describe "how the pixels move," not the subject itself.
84
+ - **Scale motion to the moment's duration:** one main action beat plus one simple camera move for short moments (3–6s); add a second beat or a longer continuous move only when the duration exceeds ~10s. Do not stack simultaneous motions, and avoid complex camera terms (e.g., "rack focus", "dolly zoom") that the distilled model drifts on.
85
+ - **Avoid:** generic phrasing ("make it look nice"), contradictory directions ("fast dramatic zoom but calm and slow"), still-photo phrasing ("preserve identity", "gentle parallax"), and prompts over ~150 words.
86
+ - **Never** include the clip duration or the model name in the prompt text.
87
+ - **Negative prompt:** keep negatives in the dedicated negative field (e.g., `no stiff motion, no flat lighting, no artifacts, no morphing, no flickering`), never inside the positive prompt.
88
+
89
+ Every i2v prompt must still explicitly include these four components, written as a single cinematic paragraph:
90
+
91
+ | # | Component | Description |
92
+ | --- | ------------------- | ----------------------------------------------------------------------------------------- |
93
+ | 1 | Camera Movement | One specific motion directive (e.g., slow dolly-in, steady hold, snap zoom). |
94
+ | 2 | Character Name | Named subject performing the action — name once, then focus on their motion/performance. |
95
+ | 3 | Character Dialogue | Spoken lines in quotation marks, treated as an audio/lipsync cue. |
96
+ | 4 | Voiceover/Narration | Formatted as `Voiceover: "..."` to capture the punchline, inner monologue, or core humor. |
97
+
98
+ ## Output Format Standard
99
+
100
+ Output two CSVs: the character CSV first, then the story CSV. Put each inside its own clean CSV code block, each followed immediately by a rendered Markdown preview table.
101
+
102
+ **Always write both CSVs in full after any update.** Whenever the story changes for any reason (an initial request, an edit to a moment, a new art style, an added or removed scene or character, a re-write, or any user correction), regenerate and output the **complete** character CSV (from `id = 1`) and the **complete** story CSV (from `id = 1` through the final moment) — never output only the changed rows, a partial diff, or a single row. The full CSVs are the single source of truth and must always reflect the entire current story and its full cast.
103
+
104
+ ```csv
105
+ id,name,face_t2i
106
+ 1,"Alex","[English T2I Prompt: front-facing facial reference portrait]"
107
+ ```
108
+
109
+ ```csv
110
+ id,duration,t2i,i2v
111
+ 1,5,"[English T2I Prompt]","[English I2V Prompt: Includes camera movement, character name, dialogue, and voiceover narration structured for this moment's duration]"
112
+ ```
113
+
114
+ ### Sample Character Bible (reused across every shot)
115
+
116
+ > Alex — a 20-year-old male college student with short black hair, round wire-frame glasses, and a faded blue hoodie, always with a nervous, wide-eyed look.
117
+ > Sam — a 40-year-old male professor with receding gray hair, a gray tweed blazer, and thick reading glasses perched on his nose, always with a stern, unamused expression.
118
+ >
119
+ > Repeat both characters' exact descriptions verbatim in every t2i prompt, leading with whoever is in focus and appending the other. Change only each character's action, expression, and pose per moment.
120
+
121
+ ### Sample Character CSV
122
+
123
+ ```csv
124
+ id,name,face_t2i
125
+ 1,"Alex","Alex's face — a 20-year-old male with a rounded face shape, fair skin, dark brown eyes, short black hair, round wire-frame glasses, a slightly upturned nose, and thin lips, neutral relaxed expression, direct-to-camera gaze, mouth closed, head centered. Head-and-shoulders portrait, symmetrical, sharp focus on the face, even soft frontal lighting, plain neutral background. Photo realistic render, shot on an 85mm lens at f/5.6, shallow depth of field."
126
+ 2,"Sam","Sam's face — a 40-year-old male with an angular face shape, light skin, gray eyes, receding gray hair, thick reading glasses perched on his nose, a straight nose, and thin pressed lips, neutral relaxed expression, direct-to-camera gaze, mouth closed, head centered. Head-and-shoulders portrait, symmetrical, sharp focus on the face, even soft frontal lighting, plain neutral background. Photo realistic render, shot on an 85mm lens at f/5.6, shallow depth of field."
127
+ ```
128
+
129
+ ### Sample t2i Prompt Structure (single moment)
130
+
131
+ > A close-up of Alex, a 20-year-old male college student with short black hair, round wire-frame glasses, and a faded blue hoodie, now with wide, panicked eyes and sweat beading on his forehead, frantically gripping a pen in an exam hall. Behind him, Sam, a 40-year-old male professor with receding gray hair, a gray tweed blazer, and thick reading glasses perched on his nose, watches with a stern, unamused expression. Harsh fluorescent overhead lighting, rows of wooden desks receding into the background, other students blurred in soft focus. Photo realistic render, shot on a 50mm lens at f/2.0, shallow depth of field, tense anxious atmosphere.
132
+
133
+ ### Sample i2v Prompt Structure (single moment)
134
+
135
+ > Slow dolly-in toward Alex's face as his eyes widen and a bead of sweat rolls down his temple. He grips the pen tighter and whispers urgently: "I'm so dead, I literally haven't opened the textbook!" Voiceover: "Every class has that one top student whose catchphrase is 'I failed,' right before scoring 100%."
136
+
137
+ > **Negative prompt:** `no stiff motion, no flat lighting, no artifacts, no morphing, no flickering`
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
2
2
  import { join, sep } from "node:path";
3
- import { isValidProjectId, resolveWorkspacePath, classifyFile, AGENT_UPLOAD_DIR, workspaceDir, } from "../workspace";
3
+ import { isValidProjectId, resolveWorkspacePath, classifyFile, AGENT_UPLOAD_DIR, workspaceDir, } from "../workspace.js";
4
4
  import sharp from "sharp";
5
5
  /** Default output resolution (px) when the tool is called without one. */
6
6
  const DEFAULT_RESOLUTION = 512;
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { resolveWorkspacePath, workspaceDir, walkFiles } from "../workspace";
3
+ import { resolveWorkspacePath, workspaceDir, walkFiles } from "../workspace.js";
4
4
  const tool = {
5
5
  name: "grep_files",
6
6
  description: "Search for a regex pattern within text files in the workspace.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { isValidProjectId, resolveWorkspacePath, classifyFile, AGENT_UPLOAD_DIR, workspaceDir, } from "../workspace";
3
+ import { isValidProjectId, resolveWorkspacePath, classifyFile, AGENT_UPLOAD_DIR, workspaceDir, } from "../workspace.js";
4
4
  async function readSSE(response, onEvent) {
5
5
  const reader = response.body?.getReader();
6
6
  if (!reader)
@@ -1,5 +1,5 @@
1
- import type { AgentTool, ToolRunContext } from "./types";
2
- export type { AgentTool, ToolRunContext } from "./types";
1
+ import type { AgentTool, ToolRunContext } from "./types.js";
2
+ export type { AgentTool, ToolRunContext } from "./types.js";
3
3
  export declare const TOOLS: AgentTool[];
4
4
  /** Build the OpenAI `tools` array from the tool objects. */
5
5
  export declare function toolDefinitions(tools: AgentTool[]): any[];
@@ -1,16 +1,16 @@
1
- import getTimeTool from "./get-time";
2
- import listFilesTool from "./list-files";
3
- import readFileTool from "./read-file";
4
- import writeFileTool from "./write-file";
5
- import updateFileTool from "./update-file";
6
- import removeFileTool from "./remove-file";
7
- import renameFileTool from "./rename-file";
8
- import grepFilesTool from "./grep-files";
9
- import showImageTool from "./show-image";
10
- import editImageTool from "./edit-image";
11
- import imageToVideoGenerationTool from "./image-to-video-generation";
12
- import textToVideoGenerationTool from "./text-to-video-generation";
13
- import stitchVideosTool from "./stitch-videos";
1
+ import getTimeTool from "./get-time.js";
2
+ import listFilesTool from "./list-files.js";
3
+ import readFileTool from "./read-file.js";
4
+ import writeFileTool from "./write-file.js";
5
+ import updateFileTool from "./update-file.js";
6
+ import removeFileTool from "./remove-file.js";
7
+ import renameFileTool from "./rename-file.js";
8
+ import grepFilesTool from "./grep-files.js";
9
+ import showImageTool from "./show-image.js";
10
+ import editImageTool from "./edit-image.js";
11
+ import imageToVideoGenerationTool from "./image-to-video-generation.js";
12
+ import textToVideoGenerationTool from "./text-to-video-generation.js";
13
+ import stitchVideosTool from "./stitch-videos.js";
14
14
  export const TOOLS = [
15
15
  getTimeTool,
16
16
  listFilesTool,
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { workspaceDir, walkFiles } from "../workspace";
2
+ import { workspaceDir, walkFiles } from "../workspace.js";
3
3
  const tool = {
4
4
  name: "list_files",
5
5
  description: "List all files in the agent's workspace (recursive).",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { resolveWorkspacePath, classifyFile } from "../workspace";
2
+ import { resolveWorkspacePath, classifyFile } from "../workspace.js";
3
3
  const tool = {
4
4
  name: "read_file",
5
5
  description: "Read the text content of a file in the workspace.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
- import { resolveWorkspacePath } from "../workspace";
2
+ import { resolveWorkspacePath } from "../workspace.js";
3
3
  const tool = {
4
4
  name: "remove_file",
5
5
  description: "Delete a file (or directory) in the workspace.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, renameSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { resolveWorkspacePath } from "../workspace";
3
+ import { resolveWorkspacePath } from "../workspace.js";
4
4
  const tool = {
5
5
  name: "rename_file",
6
6
  description: "Rename a file in the workspace.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { resolveWorkspacePath, classifyFile } from "../workspace";
2
+ import { resolveWorkspacePath, classifyFile } from "../workspace.js";
3
3
  const tool = {
4
4
  name: "show_image",
5
5
  description: "Display an image from the workspace to the user.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -2,7 +2,7 @@ import { FFmpeg } from "@ffmpeg/ffmpeg";
2
2
  import { toBlobURL } from "@ffmpeg/util";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { isValidProjectId, resolveWorkspacePath, classifyFile, workspaceDir, } from "../workspace";
5
+ import { isValidProjectId, resolveWorkspacePath, classifyFile, workspaceDir, } from "../workspace.js";
6
6
  const CORE_BASE = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/esm";
7
7
  const tool = {
8
8
  name: "stitch_videos",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,4 +1,4 @@
1
- import { isValidProjectId, workspaceDir } from "../workspace";
1
+ import { isValidProjectId, workspaceDir } from "../workspace.js";
2
2
  async function readSSE(response, onEvent) {
3
3
  const reader = response.body?.getReader();
4
4
  if (!reader)
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { resolveWorkspacePath, ensureDir } from "../workspace";
3
+ import { resolveWorkspacePath, ensureDir } from "../workspace.js";
4
4
  const tool = {
5
5
  name: "update_file",
6
6
  description: "Append text to the end of a file in the workspace.",
@@ -1,3 +1,3 @@
1
- import type { AgentTool } from "./types";
1
+ import type { AgentTool } from "./types.js";
2
2
  declare const tool: AgentTool;
3
3
  export default tool;
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { resolveWorkspacePath, ensureDir } from "../workspace";
3
+ import { resolveWorkspacePath, ensureDir } from "../workspace.js";
4
4
  const tool = {
5
5
  name: "write_file",
6
6
  description: "Write (create or overwrite) a file with the given text content.",
@@ -2,14 +2,14 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { createServer as createNetServer } from "node:net";
5
- import { spawn } from "./process";
5
+ import { spawn } from "./process.js";
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  import { homedir } from "node:os";
8
8
  import express from "express";
9
9
  import cors from "cors";
10
- import { renderMediaRoutes } from "./render-media";
11
- import { agentBackend } from "./agent/agent-backend";
12
- import { generationQueueSetup } from "./generation-queue";
10
+ import { renderMediaRoutes } from "./render-media.js";
11
+ import { agentBackend } from "./agent/agent-backend.js";
12
+ import { generationQueueSetup } from "./generation-queue.js";
13
13
  import { createServer } from "node:http";
14
14
  // import { readdir } from "node:fs/promises";
15
15
  // import { rename } from "node:fs/promises";
@@ -1,10 +1,10 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { randomUUID } from "node:crypto";
5
- import { generateAssetImage, generateSceneImage, generateSceneVideo, cancelActiveRender, } from "./render-media";
6
- import { generateMovieStudioBible } from "./agent/agent-backend";
7
- import { movieStudioStateFile } from "./agent/workspace";
5
+ import { generateAssetImage, generateSceneImage, generateSceneVideo, cancelActiveRender, } from "./render-media.js";
6
+ import { generateMovieStudioBible } from "./agent/agent-backend.js";
7
+ import { movieStudioStateFile } from "./agent/workspace.js";
8
8
  const APP_DATA_DIR = join(homedir(), "media-studio");
9
9
  const TASKS_DIR = join(APP_DATA_DIR, "tasks");
10
10
  const LOGS_DIR = join(APP_DATA_DIR, "logs");
@@ -38,6 +38,25 @@ function slugify(v) {
38
38
  function isValidProjectId(id) {
39
39
  return PROJECT_ID_RE.test(id);
40
40
  }
41
+ const OUTPUT_DIR = join(APP_DATA_DIR, "output");
42
+ /** Return an existing output file's metadata if already generated, else null. */
43
+ function existingOutput(projectId, filename) {
44
+ const path = join(OUTPUT_DIR, projectId, filename);
45
+ if (!existsSync(path))
46
+ return null;
47
+ let updatedAt = Date.now();
48
+ try {
49
+ updatedAt = statSync(path).mtimeMs;
50
+ }
51
+ catch {
52
+ // Fall back to now — only affects the cache-busting query param.
53
+ }
54
+ return {
55
+ filename,
56
+ url: `/api/files?path=${encodeURIComponent(path)}`,
57
+ updatedAt,
58
+ };
59
+ }
41
60
  // In-memory queues, keyed by project id. Persisted to disk on every mutation so
42
61
  // the queue survives app restarts and is visible at tasks/:projectId/queue.json.
43
62
  const queues = new Map();
@@ -193,17 +212,27 @@ async function runRenderAssets(ctx, characters, places) {
193
212
  statusText: `Generating character: ${c?.name || slug}`,
194
213
  progress: { current, total },
195
214
  });
196
- const r = await generateAssetImage(ctx.projectId, "character", slug, prompt, ctx.log);
197
- throwIfAborted(ctx.signal);
198
- if ("error" in r)
199
- throw new Error(r.error);
200
- assets.push({
201
- kind: "character",
202
- slug,
203
- filename: r.filename,
204
- url: r.url,
205
- updatedAt: Date.now(),
206
- });
215
+ // Skip media that's already been generated for this character.
216
+ const existing = existingOutput(ctx.projectId, `character-${slug}.png`);
217
+ let entry;
218
+ if (existing) {
219
+ ctx.log(`Already generated: character-${slug}.png — skipping\n`);
220
+ entry = { kind: "character", slug, ...existing };
221
+ }
222
+ else {
223
+ const r = await generateAssetImage(ctx.projectId, "character", slug, prompt, ctx.log);
224
+ throwIfAborted(ctx.signal);
225
+ if ("error" in r)
226
+ throw new Error(r.error);
227
+ entry = {
228
+ kind: "character",
229
+ slug,
230
+ filename: r.filename,
231
+ url: r.url,
232
+ updatedAt: Date.now(),
233
+ };
234
+ }
235
+ assets.push(entry);
207
236
  ctx.update({ result: { assets: [...assets] } });
208
237
  }
209
238
  for (const p of placesList) {
@@ -217,17 +246,26 @@ async function runRenderAssets(ctx, characters, places) {
217
246
  statusText: `Generating place: ${p?.name || slug}`,
218
247
  progress: { current, total },
219
248
  });
220
- const r = await generateAssetImage(ctx.projectId, "place", slug, prompt, ctx.log);
221
- throwIfAborted(ctx.signal);
222
- if ("error" in r)
223
- throw new Error(r.error);
224
- assets.push({
225
- kind: "place",
226
- slug,
227
- filename: r.filename,
228
- url: r.url,
229
- updatedAt: Date.now(),
230
- });
249
+ const existing = existingOutput(ctx.projectId, `place-${slug}.png`);
250
+ let entry;
251
+ if (existing) {
252
+ ctx.log(`Already generated: place-${slug}.png — skipping\n`);
253
+ entry = { kind: "place", slug, ...existing };
254
+ }
255
+ else {
256
+ const r = await generateAssetImage(ctx.projectId, "place", slug, prompt, ctx.log);
257
+ throwIfAborted(ctx.signal);
258
+ if ("error" in r)
259
+ throw new Error(r.error);
260
+ entry = {
261
+ kind: "place",
262
+ slug,
263
+ filename: r.filename,
264
+ url: r.url,
265
+ updatedAt: Date.now(),
266
+ };
267
+ }
268
+ assets.push(entry);
231
269
  ctx.update({ result: { assets: [...assets] } });
232
270
  }
233
271
  ctx.update({ result: { assets } });
@@ -248,16 +286,25 @@ async function runRenderSceneImages(ctx, scenes) {
248
286
  statusText: `Generating scene image: ${slug}`,
249
287
  progress: { current, total },
250
288
  });
251
- const r = await generateSceneImage(ctx.projectId, sc, ctx.log);
252
- throwIfAborted(ctx.signal);
253
- if ("error" in r)
254
- throw new Error(r.error);
255
- sceneImages.push({
256
- slug,
257
- filename: r.filename,
258
- url: r.url,
259
- updatedAt: Date.now(),
260
- });
289
+ const existing = existingOutput(ctx.projectId, `scene-${slug}.png`);
290
+ let entry;
291
+ if (existing) {
292
+ ctx.log(`Already generated: scene-${slug}.png — skipping\n`);
293
+ entry = { slug, ...existing };
294
+ }
295
+ else {
296
+ const r = await generateSceneImage(ctx.projectId, sc, ctx.log);
297
+ throwIfAborted(ctx.signal);
298
+ if ("error" in r)
299
+ throw new Error(r.error);
300
+ entry = {
301
+ slug,
302
+ filename: r.filename,
303
+ url: r.url,
304
+ updatedAt: Date.now(),
305
+ };
306
+ }
307
+ sceneImages.push(entry);
261
308
  ctx.update({ result: { sceneImages: [...sceneImages] } });
262
309
  }
263
310
  ctx.update({ result: { sceneImages } });
@@ -279,16 +326,25 @@ async function runRenderVideos(ctx, uvPath, scenes, characters) {
279
326
  statusText: `Generating video: ${slug}`,
280
327
  progress: { current, total },
281
328
  });
282
- const r = await generateSceneVideo(uvPath, ctx.projectId, sc, chars, ctx.log);
283
- throwIfAborted(ctx.signal);
284
- if ("error" in r)
285
- throw new Error(r.error);
286
- videos.push({
287
- slug,
288
- filename: r.filename,
289
- url: r.url,
290
- updatedAt: Date.now(),
291
- });
329
+ const existing = existingOutput(ctx.projectId, `scene-${slug}.mp4`);
330
+ let entry;
331
+ if (existing) {
332
+ ctx.log(`Already generated: scene-${slug}.mp4 — skipping\n`);
333
+ entry = { slug, ...existing };
334
+ }
335
+ else {
336
+ const r = await generateSceneVideo(uvPath, ctx.projectId, sc, chars, ctx.log);
337
+ throwIfAborted(ctx.signal);
338
+ if ("error" in r)
339
+ throw new Error(r.error);
340
+ entry = {
341
+ slug,
342
+ filename: r.filename,
343
+ url: r.url,
344
+ updatedAt: Date.now(),
345
+ };
346
+ }
347
+ videos.push(entry);
292
348
  ctx.update({ result: { videos: [...videos] } });
293
349
  }
294
350
  ctx.update({ result: { videos } });
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, realpathSync, readd
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join, sep } from "node:path";
5
- import { spawn, whichSync, mimeType } from "./process";
5
+ import { spawn, whichSync, mimeType } from "./process.js";
6
6
  // Track the currently active spawn process so it can be cancelled
7
7
  let activeProc = null;
8
8
  // Track the long-running mlx-vlm server process (separate from render jobs).
@@ -4,7 +4,14 @@
4
4
  <head>
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>effectnode-media</title>
7
+ <meta name="theme-color" content="#eaf8f6" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..600&display=swap"
12
+ rel="stylesheet"
13
+ />
14
+ <title>Lambobo Studio</title>
8
15
  </head>
9
16
 
10
17
  <body>
Binary file