@effectnode/media 0.1.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 (133) hide show
  1. package/bin/effectnode-media +2 -0
  2. package/dist/backend/index.d.ts +8 -0
  3. package/dist/backend/index.js +13 -0
  4. package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
  5. package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
  6. package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
  7. package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
  8. package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
  9. package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
  10. package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
  11. package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
  12. package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
  13. package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
  14. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
  15. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
  16. package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
  17. package/dist/backend/movie-backend/agent/tools/index.js +59 -0
  18. package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
  19. package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
  20. package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
  21. package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
  22. package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
  23. package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
  24. package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
  25. package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
  26. package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
  27. package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
  28. package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
  29. package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
  30. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
  31. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
  32. package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
  33. package/dist/backend/movie-backend/agent/tools/types.js +1 -0
  34. package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
  35. package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
  36. package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
  37. package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
  38. package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
  39. package/dist/backend/movie-backend/agent/workspace.js +124 -0
  40. package/dist/backend/movie-backend/core.d.ts +4 -0
  41. package/dist/backend/movie-backend/core.js +1167 -0
  42. package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
  43. package/dist/backend/movie-backend/generation-queue.js +766 -0
  44. package/dist/backend/movie-backend/process.d.ts +21 -0
  45. package/dist/backend/movie-backend/process.js +98 -0
  46. package/dist/backend/movie-backend/render-media.d.ts +48 -0
  47. package/dist/backend/movie-backend/render-media.js +3311 -0
  48. package/dist/backend/routes/health.d.ts +1 -0
  49. package/dist/backend/routes/health.js +9 -0
  50. package/dist/backend/routes/index.d.ts +1 -0
  51. package/dist/backend/routes/index.js +11 -0
  52. package/dist/backend/routes/media.d.ts +1 -0
  53. package/dist/backend/routes/media.js +13 -0
  54. package/dist/backend/routes/projects.d.ts +8 -0
  55. package/dist/backend/routes/projects.js +68 -0
  56. package/dist/backend/routes/voice.d.ts +1 -0
  57. package/dist/backend/routes/voice.js +39 -0
  58. package/dist/backend/routes.d.ts +1 -0
  59. package/dist/backend/routes.js +20 -0
  60. package/dist/backend/server.d.ts +1 -0
  61. package/dist/backend/server.js +23 -0
  62. package/dist/backend/workspace.d.ts +5 -0
  63. package/dist/backend/workspace.js +27 -0
  64. package/dist/backend/ws/index.d.ts +3 -0
  65. package/dist/backend/ws/index.js +27 -0
  66. package/dist/backend/ws/messages.d.ts +8 -0
  67. package/dist/backend/ws/messages.js +21 -0
  68. package/dist/backend/ws.d.ts +3 -0
  69. package/dist/backend/ws.js +29 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +80 -0
  72. package/frontend/index.html +22 -0
  73. package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
  74. package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
  75. package/frontend/src/index.css +46 -0
  76. package/frontend/src/main.tsx +13 -0
  77. package/frontend/src/movie-app/AppRouter.tsx +14 -0
  78. package/frontend/src/movie-app/MediaStudio.tsx +9 -0
  79. package/frontend/src/movie-app/SetupPage.tsx +293 -0
  80. package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
  81. package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
  82. package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
  83. package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
  84. package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
  85. package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
  86. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
  87. package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
  88. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
  89. package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
  90. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
  91. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
  92. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
  93. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
  94. package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
  95. package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
  96. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
  97. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
  98. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
  99. package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
  100. package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
  101. package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
  102. package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
  103. package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
  104. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
  105. package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
  106. package/frontend/src/movie-app/index.css +46 -0
  107. package/frontend/src/movie-app/index.html +16 -0
  108. package/frontend/src/movie-app/lambobo.png +0 -0
  109. package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
  110. package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
  111. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
  112. package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
  113. package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
  114. package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
  115. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
  116. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
  117. package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
  118. package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
  119. package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
  120. package/frontend/src/movie-app/stores/characterStore.ts +109 -0
  121. package/frontend/src/movie-app/stores/chatStore.ts +459 -0
  122. package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
  123. package/frontend/src/movie-app/stores/logStore.ts +25 -0
  124. package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
  125. package/frontend/src/movie-app/stores/projectStore.ts +124 -0
  126. package/frontend/src/movie-app/stores/queueStore.ts +214 -0
  127. package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
  128. package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
  129. package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
  130. package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
  131. package/frontend/src/vite-env.d.ts +1 -0
  132. package/frontend/tsconfig.json +19 -0
  133. package/package.json +65 -0
@@ -0,0 +1,381 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useProjectStore, type Project } from "../stores/projectStore";
4
+
5
+ export default function ProjectManager() {
6
+ const navigate = useNavigate();
7
+ const {
8
+ projects,
9
+ loading,
10
+ error,
11
+ fetchProjects,
12
+ createProject,
13
+ updateProject,
14
+ deleteProject,
15
+ openFolder,
16
+ } = useProjectStore();
17
+
18
+ const [showCreate, setShowCreate] = useState(false);
19
+ const [editingId, setEditingId] = useState<string | null>(null);
20
+ const [formName, setFormName] = useState("");
21
+ const [formDesc, setFormDesc] = useState("");
22
+ const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
23
+ const nameInputRef = useRef<HTMLInputElement>(null);
24
+
25
+ useEffect(() => {
26
+ fetchProjects();
27
+ }, [fetchProjects]);
28
+
29
+ useEffect(() => {
30
+ if (showCreate && nameInputRef.current) {
31
+ nameInputRef.current.focus();
32
+ }
33
+ }, [showCreate]);
34
+
35
+ const resetForm = () => {
36
+ setFormName("");
37
+ setFormDesc("");
38
+ setShowCreate(false);
39
+ setEditingId(null);
40
+ };
41
+
42
+ const handleCreate = async () => {
43
+ if (!formName.trim()) return;
44
+ await createProject(formName.trim(), formDesc.trim());
45
+ resetForm();
46
+ };
47
+
48
+ const handleUpdate = async () => {
49
+ if (!editingId || !formName.trim()) return;
50
+ await updateProject(editingId, {
51
+ name: formName.trim(),
52
+ description: formDesc.trim(),
53
+ });
54
+ resetForm();
55
+ };
56
+
57
+ const handleDelete = async (id: string) => {
58
+ await deleteProject(id);
59
+ setDeleteConfirm(null);
60
+ };
61
+
62
+ const startEdit = (p: Project) => {
63
+ setEditingId(p.id);
64
+ setFormName(p.name);
65
+ setFormDesc(p.description);
66
+ setShowCreate(true);
67
+ setTimeout(() => nameInputRef.current?.focus(), 0);
68
+ };
69
+
70
+ const openProject = (id: string) => {
71
+ navigate(`/project/${id}`);
72
+ };
73
+
74
+ // ========== SVG Icons ==========
75
+
76
+ const PlusIcon = (
77
+ <svg
78
+ width="16"
79
+ height="16"
80
+ viewBox="0 0 24 24"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ strokeWidth="2"
84
+ >
85
+ <line x1="12" y1="5" x2="12" y2="19" />
86
+ <line x1="5" y1="12" x2="19" y2="12" />
87
+ </svg>
88
+ );
89
+
90
+ const EditIcon = (
91
+ <svg
92
+ width="14"
93
+ height="14"
94
+ viewBox="0 0 24 24"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ strokeWidth="2"
98
+ >
99
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
100
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
101
+ </svg>
102
+ );
103
+
104
+ const DeleteIcon = (
105
+ <svg
106
+ width="14"
107
+ height="14"
108
+ viewBox="0 0 24 24"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ strokeWidth="2"
112
+ >
113
+ <polyline points="3 6 5 6 21 6" />
114
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
115
+ </svg>
116
+ );
117
+
118
+ const UploadFolderIcon = (
119
+ <svg
120
+ width="14"
121
+ height="14"
122
+ viewBox="0 0 24 24"
123
+ fill="none"
124
+ stroke="currentColor"
125
+ strokeWidth="2"
126
+ >
127
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
128
+ <line x1="12" y1="11" x2="12" y2="17" />
129
+ <polyline points="9 14 12 11 15 14" />
130
+ </svg>
131
+ );
132
+
133
+ const OutputFolderIcon = (
134
+ <svg
135
+ width="14"
136
+ height="14"
137
+ viewBox="0 0 24 24"
138
+ fill="none"
139
+ stroke="currentColor"
140
+ strokeWidth="2"
141
+ >
142
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
143
+ <line x1="12" y1="11" x2="12" y2="17" />
144
+ <polyline points="9 14 12 17 15 14" />
145
+ </svg>
146
+ );
147
+
148
+ const OpenIcon = (
149
+ <svg
150
+ width="14"
151
+ height="14"
152
+ viewBox="0 0 24 24"
153
+ fill="none"
154
+ stroke="currentColor"
155
+ strokeWidth="2"
156
+ >
157
+ <path d="M18 13v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
158
+ <polyline points="15 3 21 3 21 9" />
159
+ <line x1="10" y1="14" x2="21" y2="3" />
160
+ </svg>
161
+ );
162
+
163
+ // ========== Render ==========
164
+
165
+ return (
166
+ <div className="flex flex-col h-full">
167
+ {/* Header */}
168
+ <div className="flex items-center justify-between mb-8">
169
+ <div>
170
+ <h2 className="text-xl font-semibold text-ink-900 tracking-tight">
171
+ Projects
172
+ </h2>
173
+ <p className="text-sm text-ink-600/70 mt-0.5">
174
+ Manage your media projects
175
+ </p>
176
+ </div>
177
+ <button
178
+ onClick={() => {
179
+ resetForm();
180
+ setShowCreate(true);
181
+ }}
182
+ className="flex items-center gap-1.5 px-4 py-2 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 text-ink-950 text-sm font-medium rounded-2xl transition-all duration-150 shadow-glow hover:shadow-glow"
183
+ >
184
+ {PlusIcon}
185
+ New Project
186
+ </button>
187
+ </div>
188
+
189
+ {/* Error */}
190
+ {error && (
191
+ <div className="mb-4 px-4 py-3 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
192
+ {error}
193
+ </div>
194
+ )}
195
+
196
+ {/* Create / Edit Form */}
197
+ {showCreate && (
198
+ <div className="mb-5 p-6 bg-white border border-ink-200 rounded-3xl shadow-card">
199
+ <h3 className="text-sm font-semibold text-ink-900 mb-3">
200
+ {editingId ? "Edit Project" : "New Project"}
201
+ </h3>
202
+ <input
203
+ ref={nameInputRef}
204
+ type="text"
205
+ placeholder="Project name"
206
+ value={formName}
207
+ onChange={(e) => setFormName(e.target.value)}
208
+ onKeyDown={(e) => {
209
+ if (e.key === "Enter")
210
+ editingId ? handleUpdate() : handleCreate();
211
+ if (e.key === "Escape") resetForm();
212
+ }}
213
+ className="w-full mb-3 px-4 py-2.5 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"
214
+ />
215
+ <input
216
+ type="text"
217
+ placeholder="Description (optional)"
218
+ value={formDesc}
219
+ onChange={(e) => setFormDesc(e.target.value)}
220
+ onKeyDown={(e) => {
221
+ if (e.key === "Enter")
222
+ editingId ? handleUpdate() : handleCreate();
223
+ if (e.key === "Escape") resetForm();
224
+ }}
225
+ className="w-full mb-4 px-4 py-2.5 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"
226
+ />
227
+ <div className="flex gap-2">
228
+ <button
229
+ onClick={editingId ? handleUpdate : handleCreate}
230
+ className="px-5 py-2 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 text-ink-950 text-sm font-medium rounded-2xl transition-all duration-150"
231
+ >
232
+ {editingId ? "Update" : "Create"}
233
+ </button>
234
+ <button
235
+ onClick={resetForm}
236
+ className="px-5 py-2 bg-ink-100 hover:bg-ink-300 text-ink-700 text-sm font-medium rounded-2xl transition-all duration-150"
237
+ >
238
+ Cancel
239
+ </button>
240
+ </div>
241
+ </div>
242
+ )}
243
+
244
+ {/* Project List */}
245
+ {loading ? (
246
+ <div className="flex items-center justify-center py-16 text-ink-600/50 text-sm">
247
+ Loading...
248
+ </div>
249
+ ) : projects.length === 0 ? (
250
+ <div className="flex flex-col items-center justify-center py-16">
251
+ <div className="w-16 h-16 bg-ink-100 rounded-3xl flex items-center justify-center mb-4">
252
+ <svg
253
+ width="28"
254
+ height="28"
255
+ viewBox="0 0 24 24"
256
+ fill="none"
257
+ stroke="#81d8d0"
258
+ strokeWidth="1.5"
259
+ >
260
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
261
+ </svg>
262
+ </div>
263
+ <p className="text-sm font-medium text-ink-800">No projects yet</p>
264
+ <p className="text-xs text-ink-600/60 mt-1">
265
+ Create your first project to get started
266
+ </p>
267
+ </div>
268
+ ) : (
269
+ <div className="flex-1 overflow-y-auto space-y-2">
270
+ {projects.map((p) => (
271
+ <div
272
+ key={p.id}
273
+ className="flex items-center gap-3 px-4 py-3.5 bg-white border border-ink-200 hover:border-ink-400 hover:shadow-card-hover rounded-2xl transition-all duration-150 group"
274
+ >
275
+ {/* Project info */}
276
+ <button
277
+ onClick={() => openProject(p.id)}
278
+ className="flex-1 flex items-center gap-3 text-left min-w-0"
279
+ >
280
+ <div className="shrink-0 w-9 h-9 bg-ink-100 border border-ink-200 rounded-2xl flex items-center justify-center">
281
+ <svg
282
+ width="15"
283
+ height="15"
284
+ viewBox="0 0 24 24"
285
+ fill="none"
286
+ stroke="#81d8d0"
287
+ strokeWidth="2"
288
+ >
289
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
290
+ </svg>
291
+ </div>
292
+ <div className="min-w-0">
293
+ <p className="text-sm font-medium text-ink-900 truncate">
294
+ {p.name}
295
+ </p>
296
+ {p.description ? (
297
+ <p className="text-xs text-ink-600/60 truncate mt-0.5">
298
+ {p.description}
299
+ </p>
300
+ ) : (
301
+ <p className="text-xs text-ink-500/60 italic mt-0.5">
302
+ No description
303
+ </p>
304
+ )}
305
+ </div>
306
+ </button>
307
+
308
+ {/* Actions */}
309
+ <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
310
+ <button
311
+ onClick={() => startEdit(p)}
312
+ className="p-2 text-ink-600/40 hover:text-tiffany-600 hover:bg-ink-100 rounded-xl transition-colors"
313
+ title="Edit"
314
+ >
315
+ {EditIcon}
316
+ </button>
317
+ <button
318
+ onClick={() => openFolder(p.id, "upload")}
319
+ className="p-2 text-ink-600/40 hover:text-tiffany-600 hover:bg-ink-100 rounded-xl transition-colors"
320
+ title="Open Uploads Folder"
321
+ >
322
+ {UploadFolderIcon}
323
+ </button>
324
+ <button
325
+ onClick={() => openFolder(p.id, "output")}
326
+ className="p-2 text-ink-600/40 hover:text-tiffany-600 hover:bg-ink-100 rounded-xl transition-colors"
327
+ title="Open Outputs Folder"
328
+ >
329
+ {OutputFolderIcon}
330
+ </button>
331
+ <button
332
+ onClick={() => setDeleteConfirm(p.id)}
333
+ className="p-2 text-ink-600/40 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors"
334
+ title="Delete"
335
+ >
336
+ {DeleteIcon}
337
+ </button>
338
+ <button
339
+ onClick={() => openProject(p.id)}
340
+ className="p-2 text-ink-600/40 hover:text-tiffany-600 hover:bg-ink-100 rounded-xl transition-colors"
341
+ title="Open"
342
+ >
343
+ {OpenIcon}
344
+ </button>
345
+ </div>
346
+ </div>
347
+ ))}
348
+ </div>
349
+ )}
350
+
351
+ {/* Delete Confirmation Modal */}
352
+ {deleteConfirm && (
353
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
354
+ <div className="bg-white border border-ink-200 rounded-3xl p-8 w-80 shadow-modal">
355
+ <h3 className="text-sm font-semibold text-ink-900 mb-2">
356
+ Delete Project
357
+ </h3>
358
+ <p className="text-sm text-ink-600/80 mb-5 leading-relaxed">
359
+ Are you sure you want to delete this project? This action cannot
360
+ be undone.
361
+ </p>
362
+ <div className="flex gap-2 justify-end">
363
+ <button
364
+ onClick={() => setDeleteConfirm(null)}
365
+ className="px-4 py-2 bg-ink-100 hover:bg-ink-300 text-ink-700 text-sm font-medium rounded-2xl transition-colors"
366
+ >
367
+ Cancel
368
+ </button>
369
+ <button
370
+ onClick={() => handleDelete(deleteConfirm)}
371
+ className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white text-sm font-medium rounded-2xl transition-colors"
372
+ >
373
+ Delete
374
+ </button>
375
+ </div>
376
+ </div>
377
+ </div>
378
+ )}
379
+ </div>
380
+ );
381
+ }
@@ -0,0 +1,46 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ color-scheme: light;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ background: #f5f8f9;
10
+ color: #3e4d54;
11
+ font-family:
12
+ "Avenir Next",
13
+ Avenir,
14
+ "Helvetica Neue",
15
+ -apple-system,
16
+ BlinkMacSystemFont,
17
+ "Segoe UI",
18
+ Roboto,
19
+ sans-serif;
20
+ -webkit-font-smoothing: antialiased;
21
+ -moz-osx-font-smoothing: grayscale;
22
+ }
23
+
24
+ code,
25
+ pre,
26
+ .font-mono {
27
+ font-family:
28
+ ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
29
+ "Liberation Mono", monospace;
30
+ }
31
+
32
+ /* Subtle light scrollbar */
33
+ ::-webkit-scrollbar {
34
+ width: 8px;
35
+ height: 8px;
36
+ }
37
+ ::-webkit-scrollbar-track {
38
+ background: transparent;
39
+ }
40
+ ::-webkit-scrollbar-thumb {
41
+ background: #d8e2e6;
42
+ border-radius: 4px;
43
+ }
44
+ ::-webkit-scrollbar-thumb:hover {
45
+ background: #bcc9cf;
46
+ }
@@ -0,0 +1,16 @@
1
+ <!-- <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="color-scheme" content="light" />
8
+ <title>Lambobo Studio</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/main.tsx"></script>
14
+ </body>
15
+
16
+ </html> -->
@@ -0,0 +1,69 @@
1
+ import localforage from "localforage";
2
+ import type {
3
+ AspectRatio,
4
+ Resolution,
5
+ VideoMode,
6
+ } from "../stores/generationStore";
7
+
8
+ // Persisted "UI state" for the batch image-to-video tab: the editable setup
9
+ // (rows with t2i + i2v prompts + shared settings). Generated results are
10
+ // intentionally not persisted.
11
+ export interface PersistedBatchI2VRow {
12
+ id: string;
13
+ t2iPrompt: string;
14
+ i2vPrompt: string;
15
+ duration: number | null;
16
+ }
17
+
18
+ export interface PersistedBatchImageToVideoState {
19
+ rows: PersistedBatchI2VRow[];
20
+ aspectRatio: AspectRatio;
21
+ resolution: Resolution;
22
+ duration: number;
23
+ mode: VideoMode;
24
+ }
25
+
26
+ const store = localforage.createInstance({
27
+ name: "lambobo-studio",
28
+ storeName: "batch-image-to-video",
29
+ });
30
+
31
+ // Each project keeps its own persisted batch image-to-video UI state, keyed by
32
+ // projectId so switching projects restores the right rows + settings.
33
+ function storageKey(projectId: string): string {
34
+ return `batch-image-to-video-ui-state:${projectId}`;
35
+ }
36
+
37
+ export async function loadBatchImageToVideoState(
38
+ projectId: string,
39
+ ): Promise<PersistedBatchImageToVideoState | null> {
40
+ try {
41
+ const value = await store.getItem<PersistedBatchImageToVideoState>(
42
+ storageKey(projectId),
43
+ );
44
+ return value ?? null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export async function saveBatchImageToVideoState(
51
+ projectId: string,
52
+ state: PersistedBatchImageToVideoState,
53
+ ): Promise<void> {
54
+ try {
55
+ await store.setItem(storageKey(projectId), state);
56
+ } catch {
57
+ // Ignore persistence failures — the UI keeps working in memory.
58
+ }
59
+ }
60
+
61
+ export async function clearBatchImageToVideoState(
62
+ projectId: string,
63
+ ): Promise<void> {
64
+ try {
65
+ await store.removeItem(storageKey(projectId));
66
+ } catch {
67
+ // Ignore — nothing to clear.
68
+ }
69
+ }
@@ -0,0 +1,68 @@
1
+ import localforage from "localforage";
2
+ import type {
3
+ AspectRatio,
4
+ Resolution,
5
+ VideoMode,
6
+ } from "../stores/generationStore";
7
+
8
+ // Persisted "UI state" for the batch video tab: the editable setup (rows +
9
+ // shared settings). Transient generation state (status, results, logs) is
10
+ // intentionally not persisted.
11
+ export interface PersistedBatchRow {
12
+ id: string;
13
+ prompt: string;
14
+ imagePath: string | null;
15
+ imageUrl: string | null;
16
+ imageFilename: string | null;
17
+ }
18
+
19
+ export interface PersistedBatchVideoState {
20
+ rows: PersistedBatchRow[];
21
+ duration: number;
22
+ aspectRatio: AspectRatio;
23
+ resolution: Resolution;
24
+ mode: VideoMode;
25
+ }
26
+
27
+ const store = localforage.createInstance({
28
+ name: "lambobo-studio",
29
+ storeName: "batch-video",
30
+ });
31
+
32
+ // Each project keeps its own persisted batch-video UI state, keyed by
33
+ // projectId so switching projects restores the right rows + settings.
34
+ function storageKey(projectId: string): string {
35
+ return `batch-video-ui-state:${projectId}`;
36
+ }
37
+
38
+ export async function loadBatchVideoState(
39
+ projectId: string,
40
+ ): Promise<PersistedBatchVideoState | null> {
41
+ try {
42
+ const value = await store.getItem<PersistedBatchVideoState>(
43
+ storageKey(projectId),
44
+ );
45
+ return value ?? null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export async function saveBatchVideoState(
52
+ projectId: string,
53
+ state: PersistedBatchVideoState,
54
+ ): Promise<void> {
55
+ try {
56
+ await store.setItem(storageKey(projectId), state);
57
+ } catch {
58
+ // Ignore persistence failures — the UI keeps working in memory.
59
+ }
60
+ }
61
+
62
+ export async function clearBatchVideoState(projectId: string): Promise<void> {
63
+ try {
64
+ await store.removeItem(storageKey(projectId));
65
+ } catch {
66
+ // Ignore — nothing to clear.
67
+ }
68
+ }
@@ -0,0 +1,75 @@
1
+ import localforage from "localforage";
2
+ import type {
3
+ AspectRatio,
4
+ Resolution,
5
+ VideoMode,
6
+ } from "../stores/generationStore";
7
+
8
+ // TTS quality tier: "high" = Qwen3 1.7B, "low" = Qwen3 0.6B.
9
+ export type VoiceQuality = "low" | "high";
10
+
11
+ // Persisted "UI state" for the batch custom voice video tab: the editable
12
+ // setup (rows + shared video settings + voice reference). Transient
13
+ // generation state (status, results, logs) is intentionally not persisted.
14
+ export interface PersistedBatchVoiceRow {
15
+ id: string;
16
+ prompt: string;
17
+ script: string;
18
+ imagePath: string | null;
19
+ imageUrl: string | null;
20
+ imageFilename: string | null;
21
+ }
22
+
23
+ export interface PersistedBatchVoiceState {
24
+ rows: PersistedBatchVoiceRow[];
25
+ duration: number;
26
+ aspectRatio: AspectRatio;
27
+ resolution: Resolution;
28
+ mode: VideoMode;
29
+ quality: VoiceQuality;
30
+ voiceRefPath: string | null;
31
+ voiceRefFilename: string | null;
32
+ }
33
+
34
+ const store = localforage.createInstance({
35
+ name: "lambobo-studio",
36
+ storeName: "batch-voice",
37
+ });
38
+
39
+ // Each project keeps its own persisted batch-voice UI state, keyed by
40
+ // projectId so switching projects restores the right rows + settings.
41
+ function storageKey(projectId: string): string {
42
+ return `batch-voice-ui-state:${projectId}`;
43
+ }
44
+
45
+ export async function loadBatchVoiceState(
46
+ projectId: string,
47
+ ): Promise<PersistedBatchVoiceState | null> {
48
+ try {
49
+ const value = await store.getItem<PersistedBatchVoiceState>(
50
+ storageKey(projectId),
51
+ );
52
+ return value ?? null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export async function saveBatchVoiceState(
59
+ projectId: string,
60
+ state: PersistedBatchVoiceState,
61
+ ): Promise<void> {
62
+ try {
63
+ await store.setItem(storageKey(projectId), state);
64
+ } catch {
65
+ // Ignore persistence failures — the UI keeps working in memory.
66
+ }
67
+ }
68
+
69
+ export async function clearBatchVoiceState(projectId: string): Promise<void> {
70
+ try {
71
+ await store.removeItem(storageKey(projectId));
72
+ } catch {
73
+ // Ignore — nothing to clear.
74
+ }
75
+ }
@@ -0,0 +1,42 @@
1
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
2
+ import { toBlobURL } from "@ffmpeg/util";
3
+
4
+ const API_BASE = `http://localhost:${(window as any).PORT}`;
5
+
6
+ let ffmpeg: FFmpeg | null = null;
7
+ let loading: Promise<FFmpeg> | null = null;
8
+
9
+ /**
10
+ * Lazily load a single shared FFmpeg instance. The core .js/.wasm files are
11
+ * served by the backend at `/ffmpeg/...` (see src/bun/core.ts) so they work
12
+ * offline in both dev and packaged builds.
13
+ */
14
+ export async function loadFFmpeg(): Promise<FFmpeg> {
15
+ if (ffmpeg?.loaded) return ffmpeg;
16
+ if (loading) return loading;
17
+
18
+ loading = (async () => {
19
+ const base = `${API_BASE}/ffmpeg`;
20
+ const coreURL = await toBlobURL(
21
+ `${base}/ffmpeg-core.js`,
22
+ "text/javascript",
23
+ );
24
+ const wasmURL = await toBlobURL(
25
+ `${base}/ffmpeg-core.wasm`,
26
+ "application/wasm",
27
+ );
28
+
29
+ const instance = new FFmpeg();
30
+ await instance.load({ coreURL, wasmURL });
31
+ ffmpeg = instance;
32
+ return instance;
33
+ })();
34
+
35
+ try {
36
+ return await loading;
37
+ } catch (e) {
38
+ // Allow a retry on the next call if loading failed part-way.
39
+ loading = null;
40
+ throw e;
41
+ }
42
+ }