@effectnode/media 0.8.0 → 0.10.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 (33) hide show
  1. package/dist/backend/movie-backend/agent/prompt/script.md +213 -0
  2. package/dist/backend/movie-backend/core.js +33 -0
  3. package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
  4. package/dist/backend/movie-backend/generation-queue.js +82 -6
  5. package/dist/backend/movie-backend/render-media.d.ts +89 -1
  6. package/dist/backend/movie-backend/render-media.js +772 -476
  7. package/frontend/src/movie-app/components/EditorTabs/AdvancedVoiceCloneTab.tsx +366 -0
  8. package/frontend/src/movie-app/components/EditorTabs/AudioToVideoTab.tsx +406 -0
  9. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +67 -51
  10. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +155 -1
  11. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +33 -13
  12. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +26 -5
  13. package/frontend/src/movie-app/components/EditorTabs/UpscaleTab.tsx +342 -0
  14. package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
  15. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +131 -126
  16. package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +207 -0
  17. package/frontend/src/movie-app/stores/aiModelStore.ts +25 -2
  18. package/frontend/src/movie-app/stores/audioToVideoStore.ts +274 -0
  19. package/frontend/src/movie-app/stores/generationStore.ts +250 -345
  20. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  21. package/frontend/src/movie-app/stores/projectStore.ts +7 -3
  22. package/frontend/src/movie-app/stores/queueStore.ts +48 -1
  23. package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
  24. package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
  25. package/package.json +1 -1
  26. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
  27. package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +0 -234
  28. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  29. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  30. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  31. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  32. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  33. package/frontend/src/movie-app/stores/sceneVisualStore.ts +0 -251
@@ -1,234 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
-
3
- export interface SheetItem {
4
- id: string;
5
- name: string;
6
- url: string;
7
- }
8
-
9
- interface Props {
10
- items: SheetItem[];
11
- projectId: string;
12
- }
13
-
14
- const API_BASE = `http://localhost:${(window as any).PORT}`;
15
-
16
- // Base layout in "cell units"; scaled up so the sheet's longest edge is TARGET px.
17
- const CELL_W = 160;
18
- const CELL_H = 160;
19
- const LABEL_H = 18;
20
- const GAP = 12;
21
- const PADDING = 12;
22
- const COLUMNS = 5;
23
- const TARGET = 4096;
24
-
25
- function loadImage(src: string): Promise<HTMLImageElement> {
26
- return new Promise((resolve, reject) => {
27
- const img = new Image();
28
- img.crossOrigin = "anonymous";
29
- img.onload = () => resolve(img);
30
- img.onerror = () => reject(new Error("Failed to load image"));
31
- img.src = src;
32
- });
33
- }
34
-
35
- /** Draw a contain-fit image into a cell, centered. */
36
- function drawContain(
37
- ctx: CanvasRenderingContext2D,
38
- img: HTMLImageElement,
39
- x: number,
40
- y: number,
41
- cellW: number,
42
- cellH: number,
43
- ) {
44
- const scale = Math.min(cellW / img.width, cellH / img.height);
45
- const dw = img.width * scale;
46
- const dh = img.height * scale;
47
- ctx.drawImage(img, x + (cellW - dw) / 2, y + (cellH - dh) / 2, dw, dh);
48
- }
49
-
50
- export default function CharacterSheet({ items, projectId }: Props) {
51
- const canvasRef = useRef<HTMLCanvasElement>(null);
52
- const [saving, setSaving] = useState(false);
53
- const [saveError, setSaveError] = useState<string | null>(null);
54
- const [saveInfo, setSaveInfo] = useState<string | null>(null);
55
-
56
- useEffect(() => {
57
- const canvas = canvasRef.current;
58
- if (!canvas) return;
59
- const ctx = canvas.getContext("2d");
60
- if (!ctx) return;
61
-
62
- const cols = Math.max(1, COLUMNS);
63
- const rows = Math.ceil(items.length / cols) || 1;
64
-
65
- const baseWidth = cols * CELL_W + (cols - 1) * GAP + PADDING * 2;
66
- const baseHeight =
67
- rows * (CELL_H + LABEL_H) + (rows - 1) * GAP + PADDING * 2;
68
- const scale = TARGET / Math.max(baseWidth, baseHeight);
69
-
70
- const cellW = CELL_W * scale;
71
- const cellH = CELL_H * scale;
72
- const labelH = LABEL_H * scale;
73
- const gap = GAP * scale;
74
- const pad = PADDING * scale;
75
- const width = Math.round(baseWidth * scale);
76
- const height = Math.round(baseHeight * scale);
77
-
78
- canvas.width = width;
79
- canvas.height = height;
80
-
81
- ctx.fillStyle = "#ffffff";
82
- ctx.fillRect(0, 0, width, height);
83
-
84
- if (items.length === 0) {
85
- ctx.fillStyle = "#94a3b8";
86
- ctx.font = `${Math.round(12 * scale)}px sans-serif`;
87
- ctx.textAlign = "center";
88
- ctx.textBaseline = "middle";
89
- ctx.fillText("No characters yet", width / 2, height / 2);
90
- return;
91
- }
92
-
93
- // Guard against stale async draws if `items` changes mid-load.
94
- let cancelled = false;
95
-
96
- items.forEach(async (item, i) => {
97
- const col = i % cols;
98
- const row = Math.floor(i / cols);
99
- const x = pad + col * (cellW + gap);
100
- const y = pad + row * (cellH + labelH + gap);
101
-
102
- // Cell background
103
- ctx.fillStyle = "#f0fdfa";
104
- ctx.fillRect(x, y, cellW, cellH + labelH);
105
-
106
- let img: HTMLImageElement | null = null;
107
- try {
108
- img = await loadImage(item.url);
109
- } catch {
110
- img = null;
111
- }
112
- if (cancelled) return;
113
-
114
- if (img) drawContain(ctx, img, x, y, cellW, cellH);
115
-
116
- // Label bar
117
- ctx.fillStyle = "rgba(0,0,0,0.65)";
118
- ctx.fillRect(x, y + cellH, cellW, labelH);
119
- ctx.fillStyle = "#ffffff";
120
- ctx.font = `${Math.round(11 * scale)}px sans-serif`;
121
- ctx.textAlign = "center";
122
- ctx.textBaseline = "middle";
123
- ctx.fillText(item.name, x + cellW / 2, y + cellH + labelH / 2);
124
- });
125
-
126
- return () => {
127
- cancelled = true;
128
- };
129
- }, [items]);
130
-
131
- const downloadSheet = () => {
132
- const canvas = canvasRef.current;
133
- if (!canvas) return;
134
- const a = document.createElement("a");
135
- a.href = canvas.toDataURL("image/png");
136
- a.download = "character-sheet.png";
137
- a.click();
138
- };
139
-
140
- const saveSheet = async () => {
141
- const canvas = canvasRef.current;
142
- if (!canvas || items.length === 0) return;
143
- setSaving(true);
144
- setSaveError(null);
145
- setSaveInfo(null);
146
- try {
147
- const dataUrl = canvas.toDataURL("image/png");
148
- const res = await fetch(`${API_BASE}/api/character-sheet`, {
149
- method: "POST",
150
- headers: { "Content-Type": "application/json" },
151
- body: JSON.stringify({ image: dataUrl, projectId }),
152
- });
153
- if (!res.ok) throw new Error(await res.text());
154
- const data = await res.json();
155
- setSaveInfo(`Saved current.png (+ backup ${data.backupFilename})`);
156
- } catch (e) {
157
- setSaveError(String(e));
158
- } finally {
159
- setSaving(false);
160
- }
161
- };
162
-
163
- const SaveIcon = (
164
- <svg
165
- width="14"
166
- height="14"
167
- viewBox="0 0 24 24"
168
- fill="none"
169
- stroke="currentColor"
170
- strokeWidth="2"
171
- strokeLinecap="round"
172
- strokeLinejoin="round"
173
- >
174
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
175
- <polyline points="17 21 17 13 7 13 7 21" />
176
- <polyline points="7 3 7 8 15 8" />
177
- </svg>
178
- );
179
-
180
- const DownloadIcon = (
181
- <svg
182
- width="14"
183
- height="14"
184
- viewBox="0 0 24 24"
185
- fill="none"
186
- stroke="currentColor"
187
- strokeWidth="2"
188
- strokeLinecap="round"
189
- strokeLinejoin="round"
190
- >
191
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
192
- <polyline points="7 10 12 15 17 10" />
193
- <line x1="12" y1="15" x2="12" y2="3" />
194
- </svg>
195
- );
196
-
197
- return (
198
- <div className="flex flex-col gap-3">
199
- <div className="flex items-center justify-between">
200
- <p className="text-xs text-ink-600/60">
201
- {items.length} character{items.length === 1 ? "" : "s"}
202
- </p>
203
- <div className="flex items-center gap-2">
204
- <button
205
- onClick={saveSheet}
206
- disabled={saving || items.length === 0}
207
- className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 transition-colors"
208
- >
209
- {SaveIcon}
210
- {saving ? "Saving..." : "Save Sheet"}
211
- </button>
212
- <button
213
- onClick={downloadSheet}
214
- disabled={items.length === 0}
215
- className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium rounded-xl border border-ink-200 bg-white text-ink-600 hover:border-ink-300 disabled:opacity-50 transition-colors"
216
- >
217
- {DownloadIcon}
218
- Download Sheet
219
- </button>
220
- </div>
221
- </div>
222
-
223
- {saveInfo && (
224
- <p className="text-xs text-emerald-600">{saveInfo}</p>
225
- )}
226
- {saveError && <p className="text-xs text-red-600">{saveError}</p>}
227
-
228
- <canvas
229
- ref={canvasRef}
230
- className="rounded-2xl border border-ink-200 max-w-full h-auto"
231
- />
232
- </div>
233
- );
234
- }
@@ -1,305 +0,0 @@
1
- import { useEffect, useRef, useMemo, useState } from "react";
2
- import { useGenerationStore } from "../../stores/generationStore";
3
-
4
- interface Props {
5
- projectId: string;
6
- }
7
-
8
- export default function ExtendVideoTab({ projectId }: Props) {
9
- const store = useGenerationStore();
10
- const extendLogRef = useRef<HTMLPreElement | any>(null);
11
- const [search, setSearch] = useState("");
12
-
13
- const filteredVideos = useMemo(() => {
14
- if (!search.trim()) return store.projectVideos;
15
- const q = search.toLowerCase();
16
- return store.projectVideos.filter((v) =>
17
- v.filename.toLowerCase().includes(q),
18
- );
19
- }, [store.projectVideos, search]);
20
-
21
- useEffect(() => {
22
- if (extendLogRef.current) {
23
- extendLogRef.current.scrollTop = 100000;
24
- }
25
- }, [store.extend.logs]);
26
-
27
- const handleGenerateExtend = async () => {
28
- await store.generateExtend(
29
- projectId,
30
- store.selectedVideo?.url || store.selectedVideo?.filename || "",
31
- );
32
- document.body.scrollTop = 99999999999;
33
- };
34
-
35
- // ========== SVG Icons ==========
36
-
37
- const ExtendIcon = (
38
- <svg
39
- width="18"
40
- height="18"
41
- viewBox="0 0 24 24"
42
- fill="none"
43
- stroke="currentColor"
44
- strokeWidth="2"
45
- strokeLinecap="round"
46
- strokeLinejoin="round"
47
- >
48
- <polygon points="23 7 16 12 23 17 23 7" />
49
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
50
- <line x1="5" y1="12" x2="11" y2="12" />
51
- <line x1="8" y1="9" x2="8" y2="15" />
52
- </svg>
53
- );
54
-
55
- const SparkleIcon = (
56
- <svg
57
- width="16"
58
- height="16"
59
- viewBox="0 0 24 24"
60
- fill="none"
61
- stroke="currentColor"
62
- strokeWidth="2"
63
- >
64
- <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
65
- </svg>
66
- );
67
-
68
- return (
69
- <div className="flex flex-col gap-7">
70
- <div className="flex items-center gap-2">
71
- <span className="text-tiffany-600">{ExtendIcon}</span>
72
- <h2 className="text-base font-semibold text-ink-900">
73
- Extend Video
74
- </h2>
75
- </div>
76
-
77
- {/* Source video picker */}
78
- <div className="flex gap-6">
79
- {/* Video list */}
80
- <div className="flex-1 min-w-0">
81
- <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
82
- Source Video
83
- </label>
84
- {store.projectVideos.length > 0 && (
85
- <div className="relative mb-2">
86
- <svg
87
- className="absolute left-3 top-1/2 -translate-y-1/2 text-ink-500"
88
- width="14"
89
- height="14"
90
- viewBox="0 0 24 24"
91
- fill="none"
92
- stroke="currentColor"
93
- strokeWidth="2"
94
- strokeLinecap="round"
95
- strokeLinejoin="round"
96
- >
97
- <circle cx="11" cy="11" r="8" />
98
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
99
- </svg>
100
- <input
101
- type="text"
102
- placeholder="Filter videos..."
103
- value={search}
104
- onChange={(e) => setSearch(e.target.value)}
105
- className="w-full pl-9 pr-3 py-2 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all"
106
- />
107
- </div>
108
- )}
109
- {store.projectVideosLoading ? (
110
- <p className="text-xs text-ink-500 italic py-4">
111
- Loading videos...
112
- </p>
113
- ) : store.projectVideos.length === 0 ? (
114
- <p className="text-xs text-ink-500 italic py-4 border border-dashed border-ink-200 rounded-2xl text-center">
115
- No videos yet. Generate a video in the "Generate Video" tab.
116
- </p>
117
- ) : filteredVideos.length === 0 ? (
118
- <p className="text-xs text-ink-500 italic py-4 border border-dashed border-ink-200 rounded-2xl text-center">
119
- No videos match "{search}".
120
- </p>
121
- ) : (
122
- <div className="border border-ink-200 rounded-2xl overflow-hidden max-h-64 overflow-y-auto">
123
- {filteredVideos.map((v) => {
124
- const isSelected = store.selectedVideo?.filename === v.filename;
125
- return (
126
- <button
127
- key={v.filename}
128
- onClick={() => store.selectVideo(v)}
129
- disabled={store.extend.generating}
130
- className={`w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors border-b border-ink-200 last:border-b-0 ${
131
- isSelected
132
- ? "bg-ink-100 text-ink-900 font-medium"
133
- : "text-ink-700 hover:bg-ink-100"
134
- } disabled:opacity-50`}
135
- >
136
- <svg
137
- width="14"
138
- height="14"
139
- viewBox="0 0 24 24"
140
- fill="none"
141
- stroke="currentColor"
142
- strokeWidth="2"
143
- className="shrink-0 text-ink-500"
144
- >
145
- <polygon points="23 7 16 12 23 17 23 7" />
146
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
147
- </svg>
148
- <span className="truncate">{v.filename}</span>
149
- </button>
150
- );
151
- })}
152
- </div>
153
- )}
154
- </div>
155
-
156
- {/* Preview box */}
157
- {store.selectedVideo && (
158
- <div className="w-1/2 shrink-0">
159
- <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
160
- Preview
161
- </label>
162
- <div className="relative rounded-2xl overflow-hidden border border-ink-200 bg-black aspect-video">
163
- <video
164
- src={
165
- store.selectedVideo.url.startsWith("/")
166
- ? `http://localhost:${(window as any).PORT}${store.selectedVideo.url}`
167
- : store.selectedVideo.url
168
- }
169
- muted={false}
170
- playsInline
171
- controls
172
- className="w-full h-full object-contain"
173
- />
174
- </div>
175
- <p className="text-[10px] text-tiffany-600 mt-1 truncate">
176
- {store.selectedVideo.filename}
177
- </p>
178
- </div>
179
- )}
180
- </div>
181
-
182
- {/* Prompt */}
183
- <div>
184
- <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
185
- Continue the Scene Prompt
186
- </label>
187
- <textarea
188
- value={store.extend.prompt}
189
- onChange={(e) => store.setExtendPrompt(e.target.value)}
190
- placeholder="Describe how the video should continue, e.g. the camera holds, the motion continues naturally..."
191
- rows={4}
192
- disabled={store.extend.generating}
193
- className="w-full px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
194
- />
195
- </div>
196
-
197
- {/* Duration (seconds) */}
198
- <div>
199
- <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
200
- Duration (seconds)
201
- </label>
202
- <div className="flex flex-wrap gap-2">
203
- {[0.5, 1, 2, 3, 5, 7.5, 10].map((d) => (
204
- <button
205
- key={d}
206
- onClick={() => store.setExtendDuration(d)}
207
- disabled={store.extend.generating}
208
- className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
209
- store.extend.extendDuration === d
210
- ? "bg-ink-100 border-ink-300 text-ink-800"
211
- : "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
212
- } disabled:opacity-50`}
213
- >
214
- {d}s
215
- </button>
216
- ))}
217
- </div>
218
- <p className="text-xs text-ink-600/50 mt-1.5">
219
- {store.extend.extendFrames} frames ({store.extend.extendDuration}s ×
220
- 24 fps + 1)
221
- </p>
222
- </div>
223
-
224
- {/* Generate / Stop button */}
225
- {store.extend.generating ? (
226
- <div className="flex items-center gap-3">
227
- <div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
228
- <svg
229
- className="animate-spin text-tiffany-600"
230
- width="16"
231
- height="16"
232
- viewBox="0 0 24 24"
233
- fill="none"
234
- stroke="currentColor"
235
- strokeWidth="2"
236
- >
237
- <circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
238
- <path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
239
- </svg>
240
- <span className="text-sm font-medium text-ink-700">
241
- Extending...
242
- </span>
243
- </div>
244
- <button
245
- onClick={() => store.cancelGenerate()}
246
- className="flex items-center justify-center gap-1.5 px-5 py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
247
- >
248
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
249
- <rect x="4" y="4" width="16" height="16" rx="2" />
250
- </svg>
251
- Stop
252
- </button>
253
- </div>
254
- ) : (
255
- <button
256
- onClick={handleGenerateExtend}
257
- disabled={!store.selectedVideo || !store.extend.prompt.trim()}
258
- className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm hover:shadow-md disabled:shadow-none"
259
- >
260
- {SparkleIcon}
261
- Extend Video
262
- </button>
263
- )}
264
-
265
- {/* Error */}
266
- {store.extend.error && (
267
- <div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
268
- {store.extend.error}
269
- </div>
270
- )}
271
-
272
- {/* Logs */}
273
- {store.extend.logs.length > 0 && (
274
- <div
275
- ref={extendLogRef}
276
- id="extend-logs"
277
- className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto"
278
- >
279
- <p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
280
- Logs
281
- </p>
282
- <pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
283
- {store.extend.logs.join("\n")}
284
- </pre>
285
- </div>
286
- )}
287
-
288
- {/* Result */}
289
- {store.extend.result && (
290
- <div>
291
- <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
292
- Extended Video
293
- </label>
294
- <div className="relative rounded-2xl overflow-hidden border border-ink-200 shadow-card bg-black w-[500px]">
295
- <video
296
- src={store.extend.result}
297
- controls
298
- className="w-full h-auto"
299
- />
300
- </div>
301
- </div>
302
- )}
303
- </div>
304
- );
305
- }