@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.
- package/bin/effectnode-media +2 -0
- package/dist/backend/index.d.ts +8 -0
- package/dist/backend/index.js +13 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
- package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
- package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
- package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
- package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
- package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
- package/dist/backend/movie-backend/agent/tools/index.js +59 -0
- package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
- package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
- package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
- package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
- package/dist/backend/movie-backend/agent/tools/types.js +1 -0
- package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
- package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
- package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
- package/dist/backend/movie-backend/agent/workspace.js +124 -0
- package/dist/backend/movie-backend/core.d.ts +4 -0
- package/dist/backend/movie-backend/core.js +1167 -0
- package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
- package/dist/backend/movie-backend/generation-queue.js +766 -0
- package/dist/backend/movie-backend/process.d.ts +21 -0
- package/dist/backend/movie-backend/process.js +98 -0
- package/dist/backend/movie-backend/render-media.d.ts +48 -0
- package/dist/backend/movie-backend/render-media.js +3311 -0
- package/dist/backend/routes/health.d.ts +1 -0
- package/dist/backend/routes/health.js +9 -0
- package/dist/backend/routes/index.d.ts +1 -0
- package/dist/backend/routes/index.js +11 -0
- package/dist/backend/routes/media.d.ts +1 -0
- package/dist/backend/routes/media.js +13 -0
- package/dist/backend/routes/projects.d.ts +8 -0
- package/dist/backend/routes/projects.js +68 -0
- package/dist/backend/routes/voice.d.ts +1 -0
- package/dist/backend/routes/voice.js +39 -0
- package/dist/backend/routes.d.ts +1 -0
- package/dist/backend/routes.js +20 -0
- package/dist/backend/server.d.ts +1 -0
- package/dist/backend/server.js +23 -0
- package/dist/backend/workspace.d.ts +5 -0
- package/dist/backend/workspace.js +27 -0
- package/dist/backend/ws/index.d.ts +3 -0
- package/dist/backend/ws/index.js +27 -0
- package/dist/backend/ws/messages.d.ts +8 -0
- package/dist/backend/ws/messages.js +21 -0
- package/dist/backend/ws.d.ts +3 -0
- package/dist/backend/ws.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/frontend/index.html +22 -0
- package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/index.css +46 -0
- package/frontend/src/main.tsx +13 -0
- package/frontend/src/movie-app/AppRouter.tsx +14 -0
- package/frontend/src/movie-app/MediaStudio.tsx +9 -0
- package/frontend/src/movie-app/SetupPage.tsx +293 -0
- package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
- package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
- package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
- package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
- package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
- package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
- package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
- package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
- package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
- package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
- package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
- package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
- package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
- package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
- package/frontend/src/movie-app/index.css +46 -0
- package/frontend/src/movie-app/index.html +16 -0
- package/frontend/src/movie-app/lambobo.png +0 -0
- package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
- package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
- package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
- package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
- package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
- package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
- package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
- package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
- package/frontend/src/movie-app/stores/characterStore.ts +109 -0
- package/frontend/src/movie-app/stores/chatStore.ts +459 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
- package/frontend/src/movie-app/stores/logStore.ts +25 -0
- package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
- package/frontend/src/movie-app/stores/projectStore.ts +124 -0
- package/frontend/src/movie-app/stores/queueStore.ts +214 -0
- package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
- package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
- package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
- package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +19 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createServer as createNetServer } from "node:net";
|
|
5
|
+
import { spawn } from "./process";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import express from "express";
|
|
9
|
+
import cors from "cors";
|
|
10
|
+
import { renderMediaRoutes } from "./render-media";
|
|
11
|
+
import { agentBackend } from "./agent/agent-backend";
|
|
12
|
+
import { generationQueueSetup } from "./generation-queue";
|
|
13
|
+
import { createServer } from "node:http";
|
|
14
|
+
// import { readdir } from "node:fs/promises";
|
|
15
|
+
// import { rename } from "node:fs/promises";
|
|
16
|
+
// import { execSync } from "node:child_process";
|
|
17
|
+
// const DEV_SERVER_PORT = 5173;
|
|
18
|
+
// const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
|
19
|
+
// // Check if Vite dev server is running for HMR
|
|
20
|
+
// async function getMainViewUrl(): Promise<string> {
|
|
21
|
+
// const channel = await Updater.localInfo.channel();
|
|
22
|
+
// if (channel === "dev") {
|
|
23
|
+
// try {
|
|
24
|
+
// await fetch(DEV_SERVER_URL, { method: "HEAD" });
|
|
25
|
+
// console.log(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`);
|
|
26
|
+
// return DEV_SERVER_URL;
|
|
27
|
+
// } catch {
|
|
28
|
+
// console.log(
|
|
29
|
+
// "Vite dev server not running. Run 'bun run dev:hmr' for HMR support.",
|
|
30
|
+
// );
|
|
31
|
+
// }
|
|
32
|
+
// }
|
|
33
|
+
// return "views://mainview/index.html";
|
|
34
|
+
// }
|
|
35
|
+
// App configuration
|
|
36
|
+
// const APP_NAME = "Media Studio by loklok";
|
|
37
|
+
const APP_DATA_DIR = join(homedir(), "media-studio");
|
|
38
|
+
const PYTHON_DIR = join(APP_DATA_DIR, "python-src");
|
|
39
|
+
const OUTPUT_DIR = join(APP_DATA_DIR, "output");
|
|
40
|
+
const UPLOAD_DIR = join(APP_DATA_DIR, "upload");
|
|
41
|
+
const JSON_DIR = join(APP_DATA_DIR, "json");
|
|
42
|
+
const BACKEND_PORT_START = 4000;
|
|
43
|
+
let BACKEND_PORT = BACKEND_PORT_START;
|
|
44
|
+
let setupState = {
|
|
45
|
+
imageEditTestRendered: false,
|
|
46
|
+
qwenImageTestRendered: false,
|
|
47
|
+
port: "",
|
|
48
|
+
homebrewInstalled: false,
|
|
49
|
+
ffmpegInstalled: false,
|
|
50
|
+
uvInstalled: false,
|
|
51
|
+
pythonInstalled: false,
|
|
52
|
+
depsInstalled: false,
|
|
53
|
+
backendRunning: false,
|
|
54
|
+
imageTestRendered: false,
|
|
55
|
+
videoTestRendered: false,
|
|
56
|
+
allOK: false,
|
|
57
|
+
error: "",
|
|
58
|
+
};
|
|
59
|
+
export async function runSetup({ port = 4000, }) {
|
|
60
|
+
[APP_DATA_DIR, PYTHON_DIR, OUTPUT_DIR, JSON_DIR, UPLOAD_DIR].forEach((dir) => {
|
|
61
|
+
if (!existsSync(dir)) {
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
BACKEND_PORT = await findFreePort(port);
|
|
66
|
+
console.log("BACKEND_PORT", BACKEND_PORT);
|
|
67
|
+
const app = express();
|
|
68
|
+
app.use(cors());
|
|
69
|
+
app.use(express.json({ limit: "100gb" }));
|
|
70
|
+
app.get("/api/hi", (req, res) => {
|
|
71
|
+
res.json({ hi: "hi" });
|
|
72
|
+
});
|
|
73
|
+
// app.get("/lambobo.png", (req, res) => {
|
|
74
|
+
// const imagePath = join(
|
|
75
|
+
// __filename,
|
|
76
|
+
// "..",
|
|
77
|
+
// "..",
|
|
78
|
+
// "python-src",
|
|
79
|
+
// "images",
|
|
80
|
+
// "lambobo.png",
|
|
81
|
+
// );
|
|
82
|
+
// res.sendFile(imagePath);
|
|
83
|
+
// });
|
|
84
|
+
// Serve ffmpeg.wasm core files for client-side video stitching. These live
|
|
85
|
+
// in src/mainview/public/ffmpeg (copied to dist/ffmpeg by Vite, then into
|
|
86
|
+
// views/mainview/ffmpeg in the packaged build). Search a few candidate roots
|
|
87
|
+
// so the route works in both dev and packaged layouts.
|
|
88
|
+
const FFMPEG_CORE_FILES = ["ffmpeg-core.js", "ffmpeg-core.wasm"];
|
|
89
|
+
app.get("/ffmpeg/:file", (req, res) => {
|
|
90
|
+
const file = req.params.file;
|
|
91
|
+
if (!FFMPEG_CORE_FILES.includes(file)) {
|
|
92
|
+
return res.status(404).end();
|
|
93
|
+
}
|
|
94
|
+
const base = dirname(__filename);
|
|
95
|
+
const candidates = [
|
|
96
|
+
join(base, "..", "mainview", "public", "ffmpeg", file),
|
|
97
|
+
join(base, "..", "..", "mainview", "public", "ffmpeg", file),
|
|
98
|
+
join(base, "..", "..", "dist", "ffmpeg", file),
|
|
99
|
+
join(base, "..", "..", "views", "mainview", "ffmpeg", file),
|
|
100
|
+
];
|
|
101
|
+
const found = candidates.find((p) => existsSync(p));
|
|
102
|
+
if (!found)
|
|
103
|
+
return res.status(404).end();
|
|
104
|
+
if (file.endsWith(".wasm")) {
|
|
105
|
+
res.setHeader("Content-Type", "application/wasm");
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
res.setHeader("Content-Type", "text/javascript");
|
|
109
|
+
}
|
|
110
|
+
res.sendFile(found);
|
|
111
|
+
});
|
|
112
|
+
app.get("/api/setup", async (req, res) => {
|
|
113
|
+
// Set SSE headers
|
|
114
|
+
res.writeHead(200, {
|
|
115
|
+
"Content-Type": "text/event-stream",
|
|
116
|
+
"Cache-Control": "no-cache",
|
|
117
|
+
Connection: "keep-alive",
|
|
118
|
+
"X-Accel-Buffering": "no",
|
|
119
|
+
});
|
|
120
|
+
const send = (event, data) => {
|
|
121
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
122
|
+
};
|
|
123
|
+
// Helper to run a step and send progress
|
|
124
|
+
const runStep = async (step, label, fn) => {
|
|
125
|
+
send("progress", { step, status: "running", label });
|
|
126
|
+
try {
|
|
127
|
+
const ok = await fn();
|
|
128
|
+
if (ok) {
|
|
129
|
+
send("progress", { step, status: "completed", label });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
send("progress", { step, status: "error", label });
|
|
133
|
+
}
|
|
134
|
+
return ok;
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
send("progress", {
|
|
138
|
+
step,
|
|
139
|
+
status: "error",
|
|
140
|
+
label,
|
|
141
|
+
error: String(e),
|
|
142
|
+
});
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
// Reset state
|
|
147
|
+
setupState = {
|
|
148
|
+
port: `${BACKEND_PORT}`,
|
|
149
|
+
homebrewInstalled: false,
|
|
150
|
+
ffmpegInstalled: false,
|
|
151
|
+
uvInstalled: false,
|
|
152
|
+
pythonInstalled: false,
|
|
153
|
+
depsInstalled: false,
|
|
154
|
+
backendRunning: false,
|
|
155
|
+
imageTestRendered: false,
|
|
156
|
+
imageEditTestRendered: false,
|
|
157
|
+
videoTestRendered: false,
|
|
158
|
+
qwenImageTestRendered: false,
|
|
159
|
+
error: "",
|
|
160
|
+
allOK: false,
|
|
161
|
+
};
|
|
162
|
+
send("progress", {
|
|
163
|
+
step: "init",
|
|
164
|
+
status: "running",
|
|
165
|
+
label: "Running setup...",
|
|
166
|
+
});
|
|
167
|
+
// Step 0a: Check/install Homebrew
|
|
168
|
+
setupState.homebrewInstalled = await checkHomebrewInstalled();
|
|
169
|
+
if (!setupState.homebrewInstalled) {
|
|
170
|
+
setupState.homebrewInstalled = await runStep("homebrew", "Installing Homebrew package manager...", installHomebrew);
|
|
171
|
+
if (!setupState.homebrewInstalled) {
|
|
172
|
+
setupState.error = "Failed to install Homebrew";
|
|
173
|
+
send("error", { error: setupState.error });
|
|
174
|
+
res.end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
send("progress", {
|
|
180
|
+
step: "homebrew",
|
|
181
|
+
status: "completed",
|
|
182
|
+
label: "Homebrew package manager found",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Step 0b: Check/install ffmpeg
|
|
186
|
+
setupState.ffmpegInstalled = await checkFfmpegInstalled();
|
|
187
|
+
if (!setupState.ffmpegInstalled) {
|
|
188
|
+
setupState.ffmpegInstalled = await runStep("ffmpeg", "Installing ffmpeg via Homebrew...", installFfmpeg);
|
|
189
|
+
if (!setupState.ffmpegInstalled) {
|
|
190
|
+
// Non-fatal: ffmpeg is useful but the app may still function
|
|
191
|
+
console.warn("Failed to install ffmpeg, continuing...");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
send("progress", {
|
|
196
|
+
step: "ffmpeg",
|
|
197
|
+
status: "completed",
|
|
198
|
+
label: "ffmpeg found",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Step 1: Check/install uv
|
|
202
|
+
setupState.uvInstalled = await checkUvInstalled();
|
|
203
|
+
if (!setupState.uvInstalled) {
|
|
204
|
+
setupState.uvInstalled = await runStep("uv", "Installing uv package manager...", installUv);
|
|
205
|
+
if (!setupState.uvInstalled) {
|
|
206
|
+
setupState.error = "Failed to install uv package manager";
|
|
207
|
+
send("error", { error: setupState.error });
|
|
208
|
+
res.end();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
send("progress", {
|
|
214
|
+
step: "uv",
|
|
215
|
+
status: "completed",
|
|
216
|
+
label: "uv package manager found",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Step 2: Setup Python environment
|
|
220
|
+
setupState.pythonInstalled = await runStep("python", "Setting up Python environment...", setupPythonEnvironment);
|
|
221
|
+
if (!setupState.pythonInstalled) {
|
|
222
|
+
setupState.error = "Failed to setup Python environment";
|
|
223
|
+
send("error", { error: setupState.error });
|
|
224
|
+
res.end();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Step 3: Install Python dependencies
|
|
228
|
+
setupState.depsInstalled = await runStep("deps", "Installing Python dependencies...", installPythonDependencies);
|
|
229
|
+
if (!setupState.depsInstalled) {
|
|
230
|
+
setupState.error = "Failed to install Python dependencies";
|
|
231
|
+
send("error", { error: setupState.error });
|
|
232
|
+
res.end();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Step 4: Test render video
|
|
236
|
+
// setupState.videoTestRendered = await runStep(
|
|
237
|
+
// "render-video",
|
|
238
|
+
// "Rendering test video...",
|
|
239
|
+
// async () => testRenderVideo({ send }),
|
|
240
|
+
// );
|
|
241
|
+
// if (!setupState.videoTestRendered) {
|
|
242
|
+
// // Non-fatal: still mark setup as complete
|
|
243
|
+
// console.warn("Test video render failed, continuing...");
|
|
244
|
+
// }
|
|
245
|
+
// // Step 7: Test qwen image generation
|
|
246
|
+
// setupState.qwenImageTestRendered = await runStep(
|
|
247
|
+
// "qwen-image",
|
|
248
|
+
// "Processing qwen image generation task...",
|
|
249
|
+
// async () => testQwenImageGeneration({ send }),
|
|
250
|
+
// );
|
|
251
|
+
// if (!setupState.qwenImageTestRendered) {
|
|
252
|
+
// console.warn("Test qwen image generation failed, continuing...");
|
|
253
|
+
// }
|
|
254
|
+
// // Step 6: Test edit image
|
|
255
|
+
// setupState.imageEditTestRendered = await runStep(
|
|
256
|
+
// "edit-image",
|
|
257
|
+
// "Processing image editing task...",
|
|
258
|
+
// async () => testQwenImageEditGeneration({ send }),
|
|
259
|
+
// );
|
|
260
|
+
// if (!setupState.imageEditTestRendered) {
|
|
261
|
+
// // Non-fatal: still mark setup as complete
|
|
262
|
+
// console.warn("Test image edit failed, continuing...");
|
|
263
|
+
// }
|
|
264
|
+
setupState.allOK = true;
|
|
265
|
+
send("complete", { success: true, port: BACKEND_PORT });
|
|
266
|
+
res.end();
|
|
267
|
+
});
|
|
268
|
+
renderMediaRoutes({ app, getUvPath });
|
|
269
|
+
//
|
|
270
|
+
//
|
|
271
|
+
agentBackend({ app, getUvPath, backendPort: BACKEND_PORT });
|
|
272
|
+
//
|
|
273
|
+
generationQueueSetup({ app, getUvPath });
|
|
274
|
+
//
|
|
275
|
+
app.listen(BACKEND_PORT);
|
|
276
|
+
// const port = options.port ?? 4000;
|
|
277
|
+
// const app = express();
|
|
278
|
+
// // CORS — allow cross-origin requests (set CORS_ORIGIN to restrict origins).
|
|
279
|
+
// app.use(
|
|
280
|
+
// cors({
|
|
281
|
+
// origin: process.env.CORS_ORIGIN ?? "*",
|
|
282
|
+
// }),
|
|
283
|
+
// );
|
|
284
|
+
// // A large JSON limit so the voice-clone endpoint can accept base64 reference
|
|
285
|
+
// // audio (a 24-bit PCM mono WAV is several MB before base64 encoding).
|
|
286
|
+
// app.use(express.json({ limit: "100mb" }));
|
|
287
|
+
// app.use("/api", router);
|
|
288
|
+
const server = createServer(app);
|
|
289
|
+
// createWsServer(server);
|
|
290
|
+
// await new Promise<void>((resolve, reject) => {
|
|
291
|
+
// server.once("error", reject);
|
|
292
|
+
// server.listen(port, () => {
|
|
293
|
+
// server.off("error", reject);
|
|
294
|
+
// resolve();
|
|
295
|
+
// });
|
|
296
|
+
// });
|
|
297
|
+
// console.log(`[backend] REST API → http://localhost:${port}/api`);
|
|
298
|
+
// console.log(`[backend] WebSocket → ws://localhost:${port}/ws`);
|
|
299
|
+
return server;
|
|
300
|
+
}
|
|
301
|
+
async function findFreePort(startPort) {
|
|
302
|
+
for (let port = startPort; port < startPort + 100; port++) {
|
|
303
|
+
if (await isPortFree(port)) {
|
|
304
|
+
return port;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`No free port found in range ${startPort}-${startPort + 99}`);
|
|
308
|
+
}
|
|
309
|
+
function isPortFree(port) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
const server = createNetServer();
|
|
312
|
+
server.once("error", () => resolve(false));
|
|
313
|
+
// Bind the same wildcard address as `app.listen(port)` (Node defaults to
|
|
314
|
+
// the IPv6 dual-stack wildcard), so the probe and the real listen agree on
|
|
315
|
+
// which address family is in use.
|
|
316
|
+
server.listen(port, () => {
|
|
317
|
+
server.close(() => resolve(true));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// ========== Utility Functions ==========
|
|
322
|
+
async function runCommand(command, args, options) {
|
|
323
|
+
try {
|
|
324
|
+
const proc = spawn([command, ...args], {
|
|
325
|
+
cwd: options?.cwd,
|
|
326
|
+
env: { ...process.env, ...options?.env },
|
|
327
|
+
stdout: "pipe",
|
|
328
|
+
stderr: "pipe",
|
|
329
|
+
});
|
|
330
|
+
const output = await new Response(proc.stdout).text();
|
|
331
|
+
const error = await new Response(proc.stderr).text();
|
|
332
|
+
const exitCode = await proc.exited;
|
|
333
|
+
return {
|
|
334
|
+
success: exitCode === 0,
|
|
335
|
+
output: output.trim(),
|
|
336
|
+
error: error.trim(),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
output: "",
|
|
343
|
+
error: String(e),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function checkCommand(command) {
|
|
348
|
+
const result = await runCommand("which", [command]);
|
|
349
|
+
return result.success;
|
|
350
|
+
}
|
|
351
|
+
// ========== Homebrew Functions ==========
|
|
352
|
+
async function checkHomebrewInstalled() {
|
|
353
|
+
// Check common Homebrew paths first (faster than spawning a process)
|
|
354
|
+
const brewPaths = [
|
|
355
|
+
"/opt/homebrew/bin/brew", // Apple Silicon
|
|
356
|
+
"/usr/local/bin/brew", // Intel Mac
|
|
357
|
+
join(homedir(), "homebrew", "bin", "brew"),
|
|
358
|
+
];
|
|
359
|
+
for (const path of brewPaths) {
|
|
360
|
+
if (existsSync(path)) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Fall back to which
|
|
365
|
+
return await checkCommand("brew");
|
|
366
|
+
}
|
|
367
|
+
async function installHomebrew() {
|
|
368
|
+
console.log("Installing Homebrew...");
|
|
369
|
+
const result = await runCommand("/bin/bash", [
|
|
370
|
+
"-c",
|
|
371
|
+
'"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
372
|
+
]);
|
|
373
|
+
if (result.success) {
|
|
374
|
+
console.log("Homebrew installed successfully");
|
|
375
|
+
// After install, Homebrew may be at different paths depending on architecture
|
|
376
|
+
// The user may need to add it to PATH, but we try common paths
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
console.error("Failed to install Homebrew:", result.error);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ========== ffmpeg Functions ==========
|
|
385
|
+
async function checkFfmpegInstalled() {
|
|
386
|
+
// Check common ffmpeg paths
|
|
387
|
+
const ffmpegPaths = [
|
|
388
|
+
"/opt/homebrew/bin/ffmpeg", // Apple Silicon Homebrew
|
|
389
|
+
"/usr/local/bin/ffmpeg", // Intel Mac Homebrew
|
|
390
|
+
];
|
|
391
|
+
for (const path of ffmpegPaths) {
|
|
392
|
+
if (existsSync(path)) {
|
|
393
|
+
const binDir = dirname(path);
|
|
394
|
+
// Ensure the directory is on PATH for child processes
|
|
395
|
+
if (!process.env.PATH?.includes(binDir)) {
|
|
396
|
+
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
397
|
+
}
|
|
398
|
+
// Persist to shell config files so it survives terminal restarts
|
|
399
|
+
persistBinDirToShellRc(binDir);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Fall back to which
|
|
404
|
+
return await checkCommand("ffmpeg");
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Add a directory to the user's shell config files (~/.bashrc, ~/.zshrc)
|
|
408
|
+
* if it isn't already there.
|
|
409
|
+
*/
|
|
410
|
+
function persistBinDirToShellRc(binDir) {
|
|
411
|
+
const home = homedir();
|
|
412
|
+
const exportLine = `export PATH="${binDir}:$PATH"`;
|
|
413
|
+
const rcFiles = [join(home, ".zshrc"), join(home, ".bashrc")];
|
|
414
|
+
for (const rcPath of rcFiles) {
|
|
415
|
+
try {
|
|
416
|
+
if (!existsSync(rcPath))
|
|
417
|
+
continue;
|
|
418
|
+
const content = readFileSync(rcPath, "utf-8");
|
|
419
|
+
if (content.includes(binDir))
|
|
420
|
+
continue; // already configured
|
|
421
|
+
// Append the export line
|
|
422
|
+
appendFileSync(rcPath, `\n# Added by lambobo-studio\n${exportLine}\n`);
|
|
423
|
+
console.log(`Added ${binDir} to PATH in ${rcPath}`);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
// Non-fatal: the session PATH is already set above
|
|
427
|
+
console.warn(`Could not update ${rcPath}:`, err);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function installFfmpeg() {
|
|
432
|
+
console.log("Installing ffmpeg via Homebrew...");
|
|
433
|
+
// Resolve brew path
|
|
434
|
+
let brewPath = "brew";
|
|
435
|
+
const brewCandidates = [
|
|
436
|
+
"/opt/homebrew/bin/brew",
|
|
437
|
+
"/usr/local/bin/brew",
|
|
438
|
+
join(homedir(), "homebrew", "bin", "brew"),
|
|
439
|
+
];
|
|
440
|
+
for (const candidate of brewCandidates) {
|
|
441
|
+
if (existsSync(candidate)) {
|
|
442
|
+
brewPath = candidate;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const result = await runCommand(brewPath, ["install", "ffmpeg"]);
|
|
447
|
+
if (result.success) {
|
|
448
|
+
console.log("ffmpeg installed successfully");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
console.error("Failed to install ffmpeg:", result.error);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ========== Setup Functions ==========
|
|
457
|
+
async function checkUvInstalled() {
|
|
458
|
+
if (await checkCommand("uv")) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
const uvPaths = [
|
|
462
|
+
"/opt/homebrew/bin/uv", // Apple Silicon Homebrew
|
|
463
|
+
join(homedir(), ".local", "bin", "uv"),
|
|
464
|
+
join(homedir(), ".cargo", "bin", "uv"),
|
|
465
|
+
"/usr/local/bin/uv",
|
|
466
|
+
];
|
|
467
|
+
for (const path of uvPaths) {
|
|
468
|
+
if (existsSync(path)) {
|
|
469
|
+
const binDir = dirname(path);
|
|
470
|
+
// Ensure the directory is on PATH for child processes
|
|
471
|
+
if (!process.env.PATH?.includes(binDir)) {
|
|
472
|
+
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
473
|
+
}
|
|
474
|
+
// Persist to shell config files
|
|
475
|
+
persistBinDirToShellRc(binDir);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
async function installUv() {
|
|
482
|
+
console.log("Installing uv via Homebrew...");
|
|
483
|
+
// Resolve brew path
|
|
484
|
+
let brewPath = "brew";
|
|
485
|
+
const brewCandidates = [
|
|
486
|
+
"/opt/homebrew/bin/brew",
|
|
487
|
+
"/usr/local/bin/brew",
|
|
488
|
+
join(homedir(), "homebrew", "bin", "brew"),
|
|
489
|
+
];
|
|
490
|
+
for (const candidate of brewCandidates) {
|
|
491
|
+
if (existsSync(candidate)) {
|
|
492
|
+
brewPath = candidate;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const result = await runCommand(brewPath, ["install", "uv"]);
|
|
497
|
+
if (result.success) {
|
|
498
|
+
console.log("uv installed successfully via Homebrew");
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.error("Failed to install uv:", result.error);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export async function getUvPath() {
|
|
507
|
+
// Fall back to which
|
|
508
|
+
if (await checkCommand("uv")) {
|
|
509
|
+
const result = await runCommand("which", ["uv"]);
|
|
510
|
+
return result.output;
|
|
511
|
+
}
|
|
512
|
+
// Check known paths first — always return absolute path so child processes
|
|
513
|
+
// don't need uv on PATH
|
|
514
|
+
const uvPaths = [
|
|
515
|
+
"/usr/local/bin/uv",
|
|
516
|
+
"/opt/homebrew/bin/uv", // Apple Silicon Homebrew
|
|
517
|
+
join(homedir(), ".local", "bin", "uv"),
|
|
518
|
+
join(homedir(), ".cargo", "bin", "uv"),
|
|
519
|
+
];
|
|
520
|
+
for (const path of uvPaths) {
|
|
521
|
+
if (existsSync(path)) {
|
|
522
|
+
return path;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
throw new Error("uv not found");
|
|
526
|
+
}
|
|
527
|
+
async function setupPythonEnvironment() {
|
|
528
|
+
console.log("Setting up Python environment...");
|
|
529
|
+
const uvPath = await getUvPath();
|
|
530
|
+
const venvPath = join(PYTHON_DIR, ".venv");
|
|
531
|
+
if (!existsSync(venvPath)) {
|
|
532
|
+
console.log("Creating Python 3.10 virtual environment...");
|
|
533
|
+
const result = await runCommand(uvPath, [
|
|
534
|
+
"venv",
|
|
535
|
+
"--python",
|
|
536
|
+
"3.10",
|
|
537
|
+
venvPath,
|
|
538
|
+
]);
|
|
539
|
+
if (!result.success) {
|
|
540
|
+
console.error("Failed to create venv:", result.error);
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
async function installPythonDependencies() {
|
|
547
|
+
console.log("Installing Python dependencies...");
|
|
548
|
+
{
|
|
549
|
+
const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
550
|
+
if (!existsSync(pythonAppSrcDir)) {
|
|
551
|
+
mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
const ltxFolder = join(pythonAppSrcDir, "ltx-2-mlx");
|
|
554
|
+
if (!existsSync(ltxFolder)) {
|
|
555
|
+
let cloneCMD = await runCommand("git", [`clone`, `https://github.com/dgrauet/ltx-2-mlx.git`, "ltx-2-mlx"], { cwd: pythonAppSrcDir });
|
|
556
|
+
console.log(cloneCMD.success, cloneCMD.output);
|
|
557
|
+
}
|
|
558
|
+
const uvPath = await getUvPath();
|
|
559
|
+
const uvSyncResult = await runCommand(uvPath, [
|
|
560
|
+
//
|
|
561
|
+
"sync",
|
|
562
|
+
"--all-extras",
|
|
563
|
+
], {
|
|
564
|
+
cwd: ltxFolder,
|
|
565
|
+
});
|
|
566
|
+
if (!uvSyncResult.success) {
|
|
567
|
+
console.error("Failed to install ltx model:", uvSyncResult.error);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// {
|
|
572
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
573
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
574
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
575
|
+
// }
|
|
576
|
+
// const featureFolder = join(pythonAppSrcDir, "mlx-h3");
|
|
577
|
+
// if (!existsSync(featureFolder)) {
|
|
578
|
+
// let cloneCMD = await runCommand(
|
|
579
|
+
// "git",
|
|
580
|
+
// [`clone`, `https://github.com/appautomaton/mlx-h3.git`, "mlx-h3"],
|
|
581
|
+
// { cwd: pythonAppSrcDir },
|
|
582
|
+
// );
|
|
583
|
+
// console.log(cloneCMD.success, cloneCMD.output);
|
|
584
|
+
// }
|
|
585
|
+
// const uvPath = await getUvPath();
|
|
586
|
+
// const uvSyncResult = await runCommand(
|
|
587
|
+
// uvPath,
|
|
588
|
+
// [
|
|
589
|
+
// //
|
|
590
|
+
// "sync",
|
|
591
|
+
// // "--all-extras",
|
|
592
|
+
// ],
|
|
593
|
+
// {
|
|
594
|
+
// cwd: featureFolder,
|
|
595
|
+
// },
|
|
596
|
+
// );
|
|
597
|
+
// if (!uvSyncResult.success) {
|
|
598
|
+
// console.error("Failed to install uv in mlx h3:", uvSyncResult.error);
|
|
599
|
+
// return false;
|
|
600
|
+
// }
|
|
601
|
+
// {
|
|
602
|
+
// const uvSyncResult = await runCommand(
|
|
603
|
+
// uvPath,
|
|
604
|
+
// [
|
|
605
|
+
// //
|
|
606
|
+
// "tool",
|
|
607
|
+
// "install",
|
|
608
|
+
// "--prerelease",
|
|
609
|
+
// "allow",
|
|
610
|
+
// "mlx-h3",
|
|
611
|
+
// ],
|
|
612
|
+
// {
|
|
613
|
+
// cwd: featureFolder,
|
|
614
|
+
// },
|
|
615
|
+
// );
|
|
616
|
+
// if (!uvSyncResult.success) {
|
|
617
|
+
// console.error(
|
|
618
|
+
// "Failed to install uv mlx-h3 global:",
|
|
619
|
+
// uvSyncResult.error,
|
|
620
|
+
// );
|
|
621
|
+
// return false;
|
|
622
|
+
// }
|
|
623
|
+
// }
|
|
624
|
+
// }
|
|
625
|
+
{
|
|
626
|
+
const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
627
|
+
if (!existsSync(pythonAppSrcDir)) {
|
|
628
|
+
mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
629
|
+
}
|
|
630
|
+
const featureFolder = join(pythonAppSrcDir, "mlx-audio");
|
|
631
|
+
if (!existsSync(featureFolder)) {
|
|
632
|
+
let cloneCMD = await runCommand("git", [
|
|
633
|
+
`clone`,
|
|
634
|
+
`git clone https://github.com/Blaizzy/mlx-audio.git`,
|
|
635
|
+
"mlx-audio",
|
|
636
|
+
], { cwd: pythonAppSrcDir });
|
|
637
|
+
console.log(cloneCMD.success, cloneCMD.output);
|
|
638
|
+
}
|
|
639
|
+
const uvPath = await getUvPath();
|
|
640
|
+
const uvSyncResult = await runCommand(uvPath, [
|
|
641
|
+
//
|
|
642
|
+
"tool",
|
|
643
|
+
"install",
|
|
644
|
+
"mlx-audio",
|
|
645
|
+
"--force",
|
|
646
|
+
"--prerelease=allow",
|
|
647
|
+
], {
|
|
648
|
+
//
|
|
649
|
+
});
|
|
650
|
+
console.log(uvSyncResult);
|
|
651
|
+
if (!uvSyncResult.success) {
|
|
652
|
+
console.error("Failed to install uv mlx-audio global:", uvSyncResult.error);
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
//
|
|
657
|
+
// {
|
|
658
|
+
// const uvPath = await getUvPath();
|
|
659
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
660
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
661
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
662
|
+
// }
|
|
663
|
+
// const mlxgenFolder = join(pythonAppSrcDir, "mlxgen");
|
|
664
|
+
// if (!existsSync(mlxgenFolder)) {
|
|
665
|
+
// let cloneCMD = await runCommand(
|
|
666
|
+
// "git",
|
|
667
|
+
// [`clone`, `https://github.com/lpalbou/mlx-gen`, "mlxgen"],
|
|
668
|
+
// { cwd: pythonAppSrcDir },
|
|
669
|
+
// );
|
|
670
|
+
// console.log(cloneCMD.success, cloneCMD.output);
|
|
671
|
+
// }
|
|
672
|
+
// {
|
|
673
|
+
// const uvSyncResult = await runCommand(
|
|
674
|
+
// uvPath,
|
|
675
|
+
// [
|
|
676
|
+
// //
|
|
677
|
+
// "tool",
|
|
678
|
+
// "install",
|
|
679
|
+
// "--upgrade",
|
|
680
|
+
// "mlx-gen",
|
|
681
|
+
// ],
|
|
682
|
+
// {
|
|
683
|
+
// cwd: mlxgenFolder,
|
|
684
|
+
// },
|
|
685
|
+
// );
|
|
686
|
+
// if (!uvSyncResult.success) {
|
|
687
|
+
// console.error("Failed to install mlx-gen tool:", uvSyncResult.error);
|
|
688
|
+
// return false;
|
|
689
|
+
// }
|
|
690
|
+
// }
|
|
691
|
+
// {
|
|
692
|
+
// const uvSyncResult = await runCommand(
|
|
693
|
+
// uvPath,
|
|
694
|
+
// [
|
|
695
|
+
// //
|
|
696
|
+
// "run",
|
|
697
|
+
// "mlxgen",
|
|
698
|
+
// "download",
|
|
699
|
+
// "--model",
|
|
700
|
+
// "AbstractFramework/qwen-image-edit-2511-4bit",
|
|
701
|
+
// ],
|
|
702
|
+
// {
|
|
703
|
+
// cwd: mlxgenFolder,
|
|
704
|
+
// },
|
|
705
|
+
// );
|
|
706
|
+
// if (!uvSyncResult.success) {
|
|
707
|
+
// console.error(
|
|
708
|
+
// "Failed to download mlx-gen AbstractFramework/qwen-image-edit-2511-4bit:",
|
|
709
|
+
// uvSyncResult.error,
|
|
710
|
+
// );
|
|
711
|
+
// return false;
|
|
712
|
+
// }
|
|
713
|
+
// }
|
|
714
|
+
// //
|
|
715
|
+
// //uv run mlxgen download --model AbstractFramework/qwen-image-edit-2511-4bit
|
|
716
|
+
// //
|
|
717
|
+
// }
|
|
718
|
+
//
|
|
719
|
+
console.log("Python dependencies installed");
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
// ========== Render Helpers ==========
|
|
723
|
+
/** Stream stdout/stderr from a process to console and SSE logs. Returns all text. */
|
|
724
|
+
async function streamProcessOutput(readable, prefix, send) {
|
|
725
|
+
let text = "";
|
|
726
|
+
const reader = readable?.getReader();
|
|
727
|
+
if (!reader)
|
|
728
|
+
return text;
|
|
729
|
+
try {
|
|
730
|
+
while (true) {
|
|
731
|
+
const { done, value } = await reader.read();
|
|
732
|
+
if (done)
|
|
733
|
+
break;
|
|
734
|
+
const chunk = new TextDecoder().decode(value);
|
|
735
|
+
console.log(`[${prefix}]`, chunk);
|
|
736
|
+
text += chunk;
|
|
737
|
+
send("log", { text: chunk });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
reader.releaseLock();
|
|
742
|
+
}
|
|
743
|
+
return text;
|
|
744
|
+
}
|
|
745
|
+
// // ========== Render Functions ==========
|
|
746
|
+
// let firstImageProcess: Subprocess | null = null;
|
|
747
|
+
// async function testRenderImage({
|
|
748
|
+
// send,
|
|
749
|
+
// }: {
|
|
750
|
+
// send: (event: string, data: object) => void;
|
|
751
|
+
// }): Promise<boolean> {
|
|
752
|
+
// console.log("Try Render Image...");
|
|
753
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
754
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
755
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
756
|
+
// }
|
|
757
|
+
// const zImageFolder = join(pythonAppSrcDir, "z-image-mps");
|
|
758
|
+
// const uvPath = await getUvPath();
|
|
759
|
+
// // Kill any previous image process
|
|
760
|
+
// if (firstImageProcess && !firstImageProcess.killed) {
|
|
761
|
+
// firstImageProcess.kill();
|
|
762
|
+
// }
|
|
763
|
+
// if (!existsSync(join(OUTPUT_DIR, "welcome"))) {
|
|
764
|
+
// mkdirSync(join(OUTPUT_DIR, "welcome"), { recursive: true });
|
|
765
|
+
// }
|
|
766
|
+
// const outputPath = join(OUTPUT_DIR, "welcome", "thank-you.png");
|
|
767
|
+
// if (existsSync(outputPath)) {
|
|
768
|
+
// console.log("Image already rendered, skipping.");
|
|
769
|
+
// return true;
|
|
770
|
+
// }
|
|
771
|
+
// firstImageProcess = spawn(
|
|
772
|
+
// [
|
|
773
|
+
// uvPath,
|
|
774
|
+
// "run",
|
|
775
|
+
// "z-image-mps.py",
|
|
776
|
+
// "-p",
|
|
777
|
+
// "A very cute lamb stadning on two feet, two hands at a park. The image is render in kids cartoon 3d movie style. a line of text that says: Thank you so much for using Lambobo AI Studio!",
|
|
778
|
+
// "--aspect",
|
|
779
|
+
// "1:1",
|
|
780
|
+
// "--height",
|
|
781
|
+
// "512",
|
|
782
|
+
// "--width",
|
|
783
|
+
// "512",
|
|
784
|
+
// "--output",
|
|
785
|
+
// outputPath,
|
|
786
|
+
// "--device",
|
|
787
|
+
// "mps",
|
|
788
|
+
// ],
|
|
789
|
+
// {
|
|
790
|
+
// cwd: zImageFolder,
|
|
791
|
+
// stdout: "pipe",
|
|
792
|
+
// stderr: "pipe",
|
|
793
|
+
// },
|
|
794
|
+
// );
|
|
795
|
+
// const proc: Subprocess = firstImageProcess;
|
|
796
|
+
// // Stream stdout and stderr concurrently (cast: we always use "pipe" mode)
|
|
797
|
+
// const stdoutPromise = streamProcessOutput(
|
|
798
|
+
// proc.stdout as ReadableStream<Uint8Array>,
|
|
799
|
+
// "Image",
|
|
800
|
+
// send,
|
|
801
|
+
// );
|
|
802
|
+
// const stderrText = await streamProcessOutput(
|
|
803
|
+
// proc.stderr as ReadableStream<Uint8Array>,
|
|
804
|
+
// "Image",
|
|
805
|
+
// send,
|
|
806
|
+
// );
|
|
807
|
+
// await stdoutPromise;
|
|
808
|
+
// // Wait for process to exit and check result
|
|
809
|
+
// const exitCode = await proc.exited;
|
|
810
|
+
// const success = exitCode === 0 && existsSync(outputPath);
|
|
811
|
+
// if (success) {
|
|
812
|
+
// send("progress", {
|
|
813
|
+
// step: "render-image",
|
|
814
|
+
// status: "completed",
|
|
815
|
+
// label: "Rendering test image...",
|
|
816
|
+
// });
|
|
817
|
+
// } else {
|
|
818
|
+
// send("progress", {
|
|
819
|
+
// step: "render-image",
|
|
820
|
+
// status: "error",
|
|
821
|
+
// label: "Rendering test image...",
|
|
822
|
+
// error: stderrText || `Process exited with code ${exitCode}`,
|
|
823
|
+
// });
|
|
824
|
+
// }
|
|
825
|
+
// return success;
|
|
826
|
+
// }
|
|
827
|
+
// let firstVideoProcess: Subprocess | null = null;
|
|
828
|
+
// async function testRenderVideo({
|
|
829
|
+
// send,
|
|
830
|
+
// }: {
|
|
831
|
+
// send: (event: string, data: object) => void;
|
|
832
|
+
// }): Promise<boolean> {
|
|
833
|
+
// console.log("Try Render Video...");
|
|
834
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
835
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
836
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
837
|
+
// }
|
|
838
|
+
// const ltxFolder = join(pythonAppSrcDir, "ltx-2-mlx");
|
|
839
|
+
// const uvPath = await getUvPath();
|
|
840
|
+
// // Kill any previous video process
|
|
841
|
+
// if (firstVideoProcess && !firstVideoProcess.killed) {
|
|
842
|
+
// firstVideoProcess.kill();
|
|
843
|
+
// }
|
|
844
|
+
// if (!existsSync(join(OUTPUT_DIR, "welcome"))) {
|
|
845
|
+
// mkdirSync(join(OUTPUT_DIR, "welcome"), { recursive: true });
|
|
846
|
+
// }
|
|
847
|
+
// const outputPath = join(OUTPUT_DIR, "welcome", "thank-you.mp4");
|
|
848
|
+
// if (existsSync(outputPath)) {
|
|
849
|
+
// console.log("Video already rendered, skipping.");
|
|
850
|
+
// return true;
|
|
851
|
+
// }
|
|
852
|
+
// const lambobo = join(
|
|
853
|
+
// __filename,
|
|
854
|
+
// "..",
|
|
855
|
+
// "..",
|
|
856
|
+
// "python-src",
|
|
857
|
+
// "images",
|
|
858
|
+
// "lambobo.png",
|
|
859
|
+
// );
|
|
860
|
+
// firstVideoProcess = spawn(
|
|
861
|
+
// [
|
|
862
|
+
// uvPath,
|
|
863
|
+
// "run",
|
|
864
|
+
// "ltx-2-mlx",
|
|
865
|
+
// "generate",
|
|
866
|
+
// "--model",
|
|
867
|
+
// "dgrauet/ltx-2.3-mlx-q4",
|
|
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 Lambobo Studio!")}`,
|
|
870
|
+
// "--distilled",
|
|
871
|
+
// "--low-ram",
|
|
872
|
+
// "--frames",
|
|
873
|
+
// "121",
|
|
874
|
+
// "--width",
|
|
875
|
+
// "448",
|
|
876
|
+
// "--height",
|
|
877
|
+
// "448",
|
|
878
|
+
// "--frame-rate",
|
|
879
|
+
// "24",
|
|
880
|
+
// "--image",
|
|
881
|
+
// `${lambobo}`,
|
|
882
|
+
// "--output",
|
|
883
|
+
// outputPath,
|
|
884
|
+
// ],
|
|
885
|
+
// {
|
|
886
|
+
// cwd: ltxFolder,
|
|
887
|
+
// stdout: "pipe",
|
|
888
|
+
// stderr: "pipe",
|
|
889
|
+
// },
|
|
890
|
+
// );
|
|
891
|
+
// const proc: Subprocess = firstVideoProcess;
|
|
892
|
+
// // Stream stdout and stderr concurrently (cast: we always use "pipe" mode)
|
|
893
|
+
// const stdoutPromise = streamProcessOutput(
|
|
894
|
+
// proc.stdout as ReadableStream<Uint8Array>,
|
|
895
|
+
// "Video",
|
|
896
|
+
// send,
|
|
897
|
+
// );
|
|
898
|
+
// const stderrText = await streamProcessOutput(
|
|
899
|
+
// proc.stderr as ReadableStream<Uint8Array>,
|
|
900
|
+
// "Video",
|
|
901
|
+
// send,
|
|
902
|
+
// );
|
|
903
|
+
// await stdoutPromise;
|
|
904
|
+
// // Wait for process to exit and check result
|
|
905
|
+
// const exitCode = await proc.exited;
|
|
906
|
+
// const success = exitCode === 0 && existsSync(outputPath);
|
|
907
|
+
// if (success) {
|
|
908
|
+
// send("progress", {
|
|
909
|
+
// step: "render-video",
|
|
910
|
+
// status: "completed",
|
|
911
|
+
// label: "Rendering test video...",
|
|
912
|
+
// });
|
|
913
|
+
// } else {
|
|
914
|
+
// send("progress", {
|
|
915
|
+
// step: "render-video",
|
|
916
|
+
// status: "error",
|
|
917
|
+
// label: "Rendering test video...",
|
|
918
|
+
// error: stderrText || `Process exited with code ${exitCode}`,
|
|
919
|
+
// });
|
|
920
|
+
// }
|
|
921
|
+
// return success;
|
|
922
|
+
// }
|
|
923
|
+
// // ========== Render Functions ==========
|
|
924
|
+
// let firstImageEdit: Subprocess | null = null;
|
|
925
|
+
// async function testQwenImageEditGeneration({
|
|
926
|
+
// send,
|
|
927
|
+
// }: {
|
|
928
|
+
// send: (event: string, data: object) => void;
|
|
929
|
+
// }): Promise<boolean> {
|
|
930
|
+
// const uvPath = await getUvPath();
|
|
931
|
+
// console.log("Try Render Image...");
|
|
932
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
933
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
934
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
935
|
+
// }
|
|
936
|
+
// const zImageFolder = join(pythonAppSrcDir, "qwen-image-mps");
|
|
937
|
+
// // Kill any previous image process
|
|
938
|
+
// if (firstImageEdit && !firstImageEdit.killed) {
|
|
939
|
+
// firstImageEdit.kill();
|
|
940
|
+
// }
|
|
941
|
+
// if (!existsSync(join(OUTPUT_DIR, "welcome"))) {
|
|
942
|
+
// mkdirSync(join(OUTPUT_DIR, "welcome"), { recursive: true });
|
|
943
|
+
// }
|
|
944
|
+
// //
|
|
945
|
+
// const outputPath = join(OUTPUT_DIR, "welcome", "thank-you-edit.png");
|
|
946
|
+
// if (existsSync(outputPath)) {
|
|
947
|
+
// console.log("Image already rendered, skipping.");
|
|
948
|
+
// return true;
|
|
949
|
+
// }
|
|
950
|
+
// const lambobo = join(
|
|
951
|
+
// __filename,
|
|
952
|
+
// "..",
|
|
953
|
+
// "..",
|
|
954
|
+
// "python-src",
|
|
955
|
+
// "images",
|
|
956
|
+
// "lambobo.png",
|
|
957
|
+
// );
|
|
958
|
+
// firstImageEdit = spawn(
|
|
959
|
+
// [
|
|
960
|
+
// uvPath,
|
|
961
|
+
// "run",
|
|
962
|
+
// "qwen-image-mps",
|
|
963
|
+
// "edit",
|
|
964
|
+
// "-i",
|
|
965
|
+
// JSON.stringify(lambobo),
|
|
966
|
+
// "-p",
|
|
967
|
+
// "Change the background to sunset",
|
|
968
|
+
// "--ultra-fast",
|
|
969
|
+
// "--quantization",
|
|
970
|
+
// "Q4_0",
|
|
971
|
+
// "--output",
|
|
972
|
+
// JSON.stringify(outputPath),
|
|
973
|
+
// ],
|
|
974
|
+
// {
|
|
975
|
+
// cwd: zImageFolder,
|
|
976
|
+
// stdout: "pipe",
|
|
977
|
+
// stderr: "pipe",
|
|
978
|
+
// },
|
|
979
|
+
// );
|
|
980
|
+
// const proc: Subprocess = firstImageEdit;
|
|
981
|
+
// // Stream stdout and stderr concurrently (cast: we always use "pipe" mode)
|
|
982
|
+
// const stdoutPromise = streamProcessOutput(
|
|
983
|
+
// proc.stdout as ReadableStream<Uint8Array>,
|
|
984
|
+
// "EditImage",
|
|
985
|
+
// send,
|
|
986
|
+
// );
|
|
987
|
+
// const stderrText = await streamProcessOutput(
|
|
988
|
+
// proc.stderr as ReadableStream<Uint8Array>,
|
|
989
|
+
// "EditImage",
|
|
990
|
+
// send,
|
|
991
|
+
// );
|
|
992
|
+
// await stdoutPromise;
|
|
993
|
+
// // Wait for process to exit and check result
|
|
994
|
+
// const exitCode = await proc.exited;
|
|
995
|
+
// const success = exitCode === 0 && existsSync(outputPath);
|
|
996
|
+
// if (success) {
|
|
997
|
+
// send("progress", {
|
|
998
|
+
// step: "edit-image",
|
|
999
|
+
// status: "completed",
|
|
1000
|
+
// label: "Processing edit image task...",
|
|
1001
|
+
// });
|
|
1002
|
+
// } else {
|
|
1003
|
+
// send("progress", {
|
|
1004
|
+
// step: "edit-image",
|
|
1005
|
+
// status: "error",
|
|
1006
|
+
// label: "Processing edit image task...",
|
|
1007
|
+
// error: stderrText || `Process exited with code ${exitCode}`,
|
|
1008
|
+
// });
|
|
1009
|
+
// }
|
|
1010
|
+
// return success;
|
|
1011
|
+
// }
|
|
1012
|
+
// // ========== Render Functions ==========
|
|
1013
|
+
// let firstQwenImageGen: Subprocess | null = null;
|
|
1014
|
+
// async function testQwenImageGeneration({
|
|
1015
|
+
// send,
|
|
1016
|
+
// }: {
|
|
1017
|
+
// send: (event: string, data: object) => void;
|
|
1018
|
+
// }): Promise<boolean> {
|
|
1019
|
+
// console.log("Try Generate Image...");
|
|
1020
|
+
// const pythonAppSrcDir = join(APP_DATA_DIR, "python-src");
|
|
1021
|
+
// if (!existsSync(pythonAppSrcDir)) {
|
|
1022
|
+
// mkdirSync(pythonAppSrcDir, { recursive: true });
|
|
1023
|
+
// }
|
|
1024
|
+
// const binaryFolder = join(pythonAppSrcDir, "qwen-image-mps");
|
|
1025
|
+
// const uvPath = await getUvPath();
|
|
1026
|
+
// // Kill any previous image process
|
|
1027
|
+
// if (firstQwenImageGen && !firstQwenImageGen.killed) {
|
|
1028
|
+
// firstQwenImageGen.kill();
|
|
1029
|
+
// }
|
|
1030
|
+
// if (!existsSync(join(OUTPUT_DIR, "welcome"))) {
|
|
1031
|
+
// mkdirSync(join(OUTPUT_DIR, "welcome"), { recursive: true });
|
|
1032
|
+
// }
|
|
1033
|
+
// //
|
|
1034
|
+
// const outputPath = join(
|
|
1035
|
+
// OUTPUT_DIR,
|
|
1036
|
+
// "welcome",
|
|
1037
|
+
// "thank-you-qwen-image-gen.png",
|
|
1038
|
+
// );
|
|
1039
|
+
// // const outputFolderPath = join(OUTPUT_DIR, "welcome");
|
|
1040
|
+
// if (existsSync(outputPath)) {
|
|
1041
|
+
// console.log("Image already rendered, skipping.");
|
|
1042
|
+
// return true;
|
|
1043
|
+
// }
|
|
1044
|
+
// // const lambobo = join(
|
|
1045
|
+
// // __filename,
|
|
1046
|
+
// // "..",
|
|
1047
|
+
// // "..",
|
|
1048
|
+
// // "python-src",
|
|
1049
|
+
// // "images",
|
|
1050
|
+
// // "lambobo.png",
|
|
1051
|
+
// // );
|
|
1052
|
+
// let tempFolder = join(
|
|
1053
|
+
// OUTPUT_DIR,
|
|
1054
|
+
// "welcome",
|
|
1055
|
+
// "generation-temp",
|
|
1056
|
+
// `_${Math.random().toString(36).slice(2, 9)}`,
|
|
1057
|
+
// );
|
|
1058
|
+
// mkdirSync(tempFolder, { recursive: true });
|
|
1059
|
+
// firstQwenImageGen = spawn(
|
|
1060
|
+
// [
|
|
1061
|
+
// uvPath,
|
|
1062
|
+
// "run",
|
|
1063
|
+
// "qwen-image-mps",
|
|
1064
|
+
// "generate",
|
|
1065
|
+
// "-p",
|
|
1066
|
+
// "Draw a sunset",
|
|
1067
|
+
// "--outdir",
|
|
1068
|
+
// JSON.stringify(tempFolder),
|
|
1069
|
+
// "--ultra-fast",
|
|
1070
|
+
// "--quantization",
|
|
1071
|
+
// "Q4_0",
|
|
1072
|
+
// "--aspect",
|
|
1073
|
+
// "16:9",
|
|
1074
|
+
// ],
|
|
1075
|
+
// {
|
|
1076
|
+
// cwd: binaryFolder,
|
|
1077
|
+
// stdout: "pipe",
|
|
1078
|
+
// stderr: "pipe",
|
|
1079
|
+
// },
|
|
1080
|
+
// );
|
|
1081
|
+
// const proc: Subprocess = firstQwenImageGen;
|
|
1082
|
+
// // Stream stdout and stderr concurrently (cast: we always use "pipe" mode)
|
|
1083
|
+
// const stdoutPromise = streamProcessOutput(
|
|
1084
|
+
// proc.stdout as ReadableStream<Uint8Array>,
|
|
1085
|
+
// "QwenImageGen",
|
|
1086
|
+
// send,
|
|
1087
|
+
// );
|
|
1088
|
+
// const stderrText = await streamProcessOutput(
|
|
1089
|
+
// proc.stderr as ReadableStream<Uint8Array>,
|
|
1090
|
+
// "QwenImageGen",
|
|
1091
|
+
// send,
|
|
1092
|
+
// );
|
|
1093
|
+
// await stdoutPromise;
|
|
1094
|
+
// // Wait for process to exit and check result
|
|
1095
|
+
// const exitCode = await proc.exitCode;
|
|
1096
|
+
// const success = exitCode === 0;
|
|
1097
|
+
// if (success) {
|
|
1098
|
+
// // please list the first file in tempFolder
|
|
1099
|
+
// async function listFiles(directoryPath: string) {
|
|
1100
|
+
// try {
|
|
1101
|
+
// const files = await readdir(directoryPath);
|
|
1102
|
+
// console.log(files); // Array of file and folder names
|
|
1103
|
+
// return files;
|
|
1104
|
+
// } catch (err) {
|
|
1105
|
+
// console.error("Error reading directory:", err);
|
|
1106
|
+
// return [];
|
|
1107
|
+
// }
|
|
1108
|
+
// }
|
|
1109
|
+
// const items = await listFiles(`${tempFolder}`);
|
|
1110
|
+
// for await (let item of items) {
|
|
1111
|
+
// if (item.includes(".png")) {
|
|
1112
|
+
// await rename(item, join(outputPath));
|
|
1113
|
+
// }
|
|
1114
|
+
// }
|
|
1115
|
+
// unlinkSync(tempFolder);
|
|
1116
|
+
// send("progress", {
|
|
1117
|
+
// step: "qwen-image",
|
|
1118
|
+
// status: "completed",
|
|
1119
|
+
// label: "Test qwen image generation...",
|
|
1120
|
+
// });
|
|
1121
|
+
// } else {
|
|
1122
|
+
// send("progress", {
|
|
1123
|
+
// step: "qwen-image",
|
|
1124
|
+
// status: "error",
|
|
1125
|
+
// label: "Test qwen image generation...",
|
|
1126
|
+
// error: stderrText || `Process exited with code ${exitCode}`,
|
|
1127
|
+
// });
|
|
1128
|
+
// }
|
|
1129
|
+
// return success;
|
|
1130
|
+
// }
|
|
1131
|
+
// ========== Application Menu ==========
|
|
1132
|
+
// Electron-only: disabled after the Bun → Node.js migration. `ApplicationMenu`
|
|
1133
|
+
// is no longer imported, so this would throw `ReferenceError` at module load.
|
|
1134
|
+
/*
|
|
1135
|
+
ApplicationMenu.setApplicationMenu([
|
|
1136
|
+
{
|
|
1137
|
+
label: `Lambobo AI Studio`,
|
|
1138
|
+
submenu: [
|
|
1139
|
+
{ role: "about" },
|
|
1140
|
+
{ type: "separator" },
|
|
1141
|
+
{ role: "hide" },
|
|
1142
|
+
{ role: "hideOthers" },
|
|
1143
|
+
{ role: "showAll" },
|
|
1144
|
+
{ type: "separator" },
|
|
1145
|
+
{ role: "quit" },
|
|
1146
|
+
],
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
label: "Edit",
|
|
1150
|
+
submenu: [
|
|
1151
|
+
{ role: "undo" },
|
|
1152
|
+
{ role: "redo" },
|
|
1153
|
+
{ type: "separator" },
|
|
1154
|
+
{ role: "cut" },
|
|
1155
|
+
{ role: "copy" },
|
|
1156
|
+
{ role: "paste" },
|
|
1157
|
+
{ role: "pasteAndMatchStyle" },
|
|
1158
|
+
{ role: "delete" },
|
|
1159
|
+
{ role: "selectAll" },
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
label: "Window",
|
|
1164
|
+
submenu: [{ role: "minimize" }, { role: "zoom" }, { role: "close" }],
|
|
1165
|
+
},
|
|
1166
|
+
]);
|
|
1167
|
+
*/
|