@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 @@
|
|
|
1
|
+
export declare const healthRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const router: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { healthRouter } from "./health.js";
|
|
3
|
+
import { mediaRouter } from "./media.js";
|
|
4
|
+
import { projectsRouter } from "./projects.js";
|
|
5
|
+
import { voiceRouter } from "./voice.js";
|
|
6
|
+
// Combined REST API router, mounted at /api.
|
|
7
|
+
export const router = Router();
|
|
8
|
+
router.use(healthRouter);
|
|
9
|
+
router.use(mediaRouter);
|
|
10
|
+
router.use(projectsRouter);
|
|
11
|
+
router.use(voiceRouter);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const mediaRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
export const mediaRouter = Router();
|
|
3
|
+
const media = [
|
|
4
|
+
{ id: 1, title: "Sintel", type: "film" },
|
|
5
|
+
{ id: 2, title: "Big Buck Bunny", type: "animation" },
|
|
6
|
+
{ id: 3, title: "Tears of Steel", type: "short" },
|
|
7
|
+
];
|
|
8
|
+
mediaRouter.get("/media", (_req, res) => {
|
|
9
|
+
res.json({ media });
|
|
10
|
+
});
|
|
11
|
+
mediaRouter.post("/media", (req, res) => {
|
|
12
|
+
res.status(201).json({ ok: true, received: req.body ?? {} });
|
|
13
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readJson, writeJson } from "../workspace.js";
|
|
4
|
+
export const projectsRouter = Router();
|
|
5
|
+
const FILE = "projects.json";
|
|
6
|
+
const EMPTY = { projects: [] };
|
|
7
|
+
async function loadProjects() {
|
|
8
|
+
const data = await readJson(FILE, EMPTY);
|
|
9
|
+
return data.projects ?? [];
|
|
10
|
+
}
|
|
11
|
+
async function saveProjects(projects) {
|
|
12
|
+
await writeJson(FILE, { projects });
|
|
13
|
+
}
|
|
14
|
+
projectsRouter.get("/projects", async (_req, res) => {
|
|
15
|
+
const projects = await loadProjects();
|
|
16
|
+
projects.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
17
|
+
res.json({ projects });
|
|
18
|
+
});
|
|
19
|
+
projectsRouter.post("/projects", async (req, res) => {
|
|
20
|
+
const { name, description } = req.body ?? {};
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
const project = {
|
|
23
|
+
id: randomUUID(),
|
|
24
|
+
name: typeof name === "string" && name.trim() ? name.trim() : "Untitled",
|
|
25
|
+
description: typeof description === "string" ? description : "",
|
|
26
|
+
createdAt: now,
|
|
27
|
+
updatedAt: now,
|
|
28
|
+
};
|
|
29
|
+
const projects = await loadProjects();
|
|
30
|
+
projects.push(project);
|
|
31
|
+
await saveProjects(projects);
|
|
32
|
+
res.status(201).json({ project });
|
|
33
|
+
});
|
|
34
|
+
projectsRouter.get("/projects/:id", async (req, res) => {
|
|
35
|
+
const projects = await loadProjects();
|
|
36
|
+
const project = projects.find((p) => p.id === req.params.id);
|
|
37
|
+
if (!project) {
|
|
38
|
+
res.status(404).json({ error: "Project not found" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
res.json({ project });
|
|
42
|
+
});
|
|
43
|
+
projectsRouter.put("/projects/:id", async (req, res) => {
|
|
44
|
+
const projects = await loadProjects();
|
|
45
|
+
const project = projects.find((p) => p.id === req.params.id);
|
|
46
|
+
if (!project) {
|
|
47
|
+
res.status(404).json({ error: "Project not found" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { name, description } = req.body ?? {};
|
|
51
|
+
if (typeof name === "string" && name.trim())
|
|
52
|
+
project.name = name.trim();
|
|
53
|
+
if (typeof description === "string")
|
|
54
|
+
project.description = description;
|
|
55
|
+
project.updatedAt = new Date().toISOString();
|
|
56
|
+
await saveProjects(projects);
|
|
57
|
+
res.json({ project });
|
|
58
|
+
});
|
|
59
|
+
projectsRouter.delete("/projects/:id", async (req, res) => {
|
|
60
|
+
const projects = await loadProjects();
|
|
61
|
+
const next = projects.filter((p) => p.id !== req.params.id);
|
|
62
|
+
if (next.length === projects.length) {
|
|
63
|
+
res.status(404).json({ error: "Project not found" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await saveProjects(next);
|
|
67
|
+
res.status(204).end();
|
|
68
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const voiceRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
export const voiceRouter = Router();
|
|
3
|
+
const VOICE_API_URL = process.env.VOICE_API_URL ?? "http://127.0.0.1:11234";
|
|
4
|
+
const DEFAULT_MODEL = "mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16";
|
|
5
|
+
/**
|
|
6
|
+
* Proxy to the mlx-serve OpenAI-compatible TTS endpoint so the browser never
|
|
7
|
+
* talks to it directly (avoids CORS and keeps the URL configurable via
|
|
8
|
+
* VOICE_API_URL). Expects { model?, input, ref_audio? } and returns WAV bytes.
|
|
9
|
+
*/
|
|
10
|
+
voiceRouter.post("/voice/speech", async (req, res) => {
|
|
11
|
+
const { model, input, ref_audio } = req.body ?? {};
|
|
12
|
+
if (typeof input !== "string" || !input.trim()) {
|
|
13
|
+
res.status(400).json({ error: "'input' is required" });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const payload = {
|
|
17
|
+
model: typeof model === "string" && model ? model : DEFAULT_MODEL,
|
|
18
|
+
input: input.trim(),
|
|
19
|
+
...(typeof ref_audio === "string" && ref_audio ? { ref_audio } : {}),
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const upstream = await fetch(`${VOICE_API_URL}/v1/audio/speech`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(payload),
|
|
26
|
+
});
|
|
27
|
+
if (!upstream.ok) {
|
|
28
|
+
const text = await upstream.text();
|
|
29
|
+
res.status(upstream.status).json({ error: text });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const audio = Buffer.from(await upstream.arrayBuffer());
|
|
33
|
+
res.set("Content-Type", "audio/wav");
|
|
34
|
+
res.send(audio);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
res.status(502).json({ error: e.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const router: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
export const router = Router();
|
|
3
|
+
const media = [
|
|
4
|
+
{ id: 1, title: "Sintel", type: "film" },
|
|
5
|
+
{ id: 2, title: "Big Buck Bunny", type: "animation" },
|
|
6
|
+
{ id: 3, title: "Tears of Steel", type: "short" },
|
|
7
|
+
];
|
|
8
|
+
router.get("/health", (_req, res) => {
|
|
9
|
+
res.json({
|
|
10
|
+
ok: true,
|
|
11
|
+
uptime: process.uptime(),
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
router.get("/media", (_req, res) => {
|
|
16
|
+
res.json({ media });
|
|
17
|
+
});
|
|
18
|
+
router.post("/media", (req, res) => {
|
|
19
|
+
res.status(201).json({ ok: true, received: req.body ?? {} });
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createBackendServer } from "./index.js";
|
|
2
|
+
// Standalone backend entry for `bun run dev`. The frontend (Vite) runs as a
|
|
3
|
+
// separate process (see vite.config.ts), so nodemon can restart this on
|
|
4
|
+
// backend edits without disturbing the browser's HMR connection.
|
|
5
|
+
const port = Number(process.env.BACKEND_PORT ?? 4000);
|
|
6
|
+
async function main() {
|
|
7
|
+
//
|
|
8
|
+
const server = await createBackendServer({ port });
|
|
9
|
+
const shutdown = () => {
|
|
10
|
+
try {
|
|
11
|
+
server.close(() => process.exit(0));
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
console.error(e);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
process.once("SIGINT", shutdown);
|
|
18
|
+
process.once("SIGTERM", shutdown);
|
|
19
|
+
}
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
console.error(`[backend] failed to start: ${err.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const WORKSPACE_DIR: string;
|
|
2
|
+
/** Read a JSON file from the workspace, returning `fallback` if missing/invalid. */
|
|
3
|
+
export declare function readJson<T>(file: string, fallback: T): Promise<T>;
|
|
4
|
+
/** Write JSON to the workspace atomically (temp file + rename). */
|
|
5
|
+
export declare function writeJson(file: string, data: unknown): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// Local JSON database root: ~/media-workspace.
|
|
5
|
+
export const WORKSPACE_DIR = path.join(os.homedir(), "effectnode-media");
|
|
6
|
+
async function ensureWorkspace() {
|
|
7
|
+
await fs.mkdir(WORKSPACE_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
/** Read a JSON file from the workspace, returning `fallback` if missing/invalid. */
|
|
10
|
+
export async function readJson(file, fallback) {
|
|
11
|
+
await ensureWorkspace();
|
|
12
|
+
const full = path.join(WORKSPACE_DIR, file);
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(await fs.readFile(full, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Write JSON to the workspace atomically (temp file + rename). */
|
|
21
|
+
export async function writeJson(file, data) {
|
|
22
|
+
await ensureWorkspace();
|
|
23
|
+
const full = path.join(WORKSPACE_DIR, file);
|
|
24
|
+
const tmp = `${full}.tmp`;
|
|
25
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
26
|
+
await fs.rename(tmp, full);
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
import { handleMessage } from "./messages.js";
|
|
3
|
+
export function createWsServer(server) {
|
|
4
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
5
|
+
const broadcast = (text) => {
|
|
6
|
+
for (const client of wss.clients) {
|
|
7
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
8
|
+
client.send(text);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
wss.on("connection", (socket) => {
|
|
13
|
+
console.log("[ws] client connected");
|
|
14
|
+
socket.on("message", (data) => {
|
|
15
|
+
const text = data.toString();
|
|
16
|
+
console.log("[ws] received:", text);
|
|
17
|
+
handleMessage(text, { broadcast });
|
|
18
|
+
});
|
|
19
|
+
socket.on("close", () => console.log("[ws] client disconnected"));
|
|
20
|
+
});
|
|
21
|
+
// Broadcast a clock tick to every connected client every 5 seconds.
|
|
22
|
+
const tick = setInterval(() => {
|
|
23
|
+
broadcast(`clock: ${new Date().toISOString()}`);
|
|
24
|
+
}, 5000);
|
|
25
|
+
wss.on("close", () => clearInterval(tick));
|
|
26
|
+
return wss;
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface MessageContext {
|
|
2
|
+
broadcast: (text: string) => void;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Route an incoming WebSocket message by its JSON `type`, falling back to a
|
|
6
|
+
* plain echo for non-JSON or untyped input.
|
|
7
|
+
*/
|
|
8
|
+
export declare function handleMessage(raw: string, ctx: MessageContext): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route an incoming WebSocket message by its JSON `type`, falling back to a
|
|
3
|
+
* plain echo for non-JSON or untyped input.
|
|
4
|
+
*/
|
|
5
|
+
export function handleMessage(raw, ctx) {
|
|
6
|
+
let text = raw;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(raw);
|
|
9
|
+
if (parsed.type === "ping") {
|
|
10
|
+
ctx.broadcast("pong");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (parsed.type === "chat" && typeof parsed.message === "string") {
|
|
14
|
+
text = parsed.message;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Non-JSON input — treat as a plain chat string.
|
|
19
|
+
}
|
|
20
|
+
ctx.broadcast(`echo: ${text}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
export function createWsServer(server) {
|
|
3
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
4
|
+
wss.on("connection", (socket) => {
|
|
5
|
+
console.log("[ws] client connected");
|
|
6
|
+
socket.on("message", (data) => {
|
|
7
|
+
const text = data.toString();
|
|
8
|
+
console.log("[ws] received:", text);
|
|
9
|
+
// Broadcast back to all connected clients.
|
|
10
|
+
for (const client of wss.clients) {
|
|
11
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
12
|
+
client.send(`echo: ${text}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
socket.on("close", () => console.log("[ws] client disconnected"));
|
|
17
|
+
});
|
|
18
|
+
// Broadcast a clock tick to every connected client every 5 seconds.
|
|
19
|
+
const tick = setInterval(() => {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
for (const client of wss.clients) {
|
|
22
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
23
|
+
client.send(`clock: ${now}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}, 5000);
|
|
27
|
+
wss.on("close", () => clearInterval(tick));
|
|
28
|
+
return wss;
|
|
29
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { createServer as createViteServer } from "vite";
|
|
8
|
+
import react from "@vitejs/plugin-react";
|
|
9
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
10
|
+
import { createBackendServer } from "./backend/index.js";
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// The Vite app source lives at the package root, next to dist/ (shipped in
|
|
13
|
+
// "files"), so this resolves correctly from both src/ (tsx) and dist/ (build).
|
|
14
|
+
const FRONTEND_ROOT = path.resolve(__dirname, "..", "frontend");
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name("effectnode-media")
|
|
18
|
+
.description("Start the effectnode-media services — a Vite + React + TypeScript frontend " +
|
|
19
|
+
"and an Express backend (REST + WebSocket) — then open the browser.")
|
|
20
|
+
.version("0.1.0")
|
|
21
|
+
.option("--host <host>", "Host to bind", "localhost")
|
|
22
|
+
.option("--frontend-port <port>", "Frontend (Vite) port", "5177")
|
|
23
|
+
.option("--backend-port <port>", "Backend (Express) port", "4000")
|
|
24
|
+
.option("--no-open", "Do not open the browser")
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
const host = options.host;
|
|
27
|
+
const frontendPort = Number(options.frontendPort);
|
|
28
|
+
const backendPort = Number(options.backendPort);
|
|
29
|
+
// 1. Backend service — Express (REST + WebSocket) on :backendPort.
|
|
30
|
+
const backend = await createBackendServer({ port: backendPort });
|
|
31
|
+
// 2. Frontend service — Vite dev server on :frontendPort, proxying
|
|
32
|
+
// /api and /ws to the backend so browser calls are same-origin.
|
|
33
|
+
const vite = await createViteServer({
|
|
34
|
+
root: FRONTEND_ROOT,
|
|
35
|
+
plugins: [react(), tailwindcss()],
|
|
36
|
+
env: {
|
|
37
|
+
PORT: backendPort,
|
|
38
|
+
},
|
|
39
|
+
server: {
|
|
40
|
+
host,
|
|
41
|
+
port: frontendPort,
|
|
42
|
+
strictPort: true,
|
|
43
|
+
proxy: {
|
|
44
|
+
"/api": {
|
|
45
|
+
target: `http://localhost:${backendPort}`,
|
|
46
|
+
changeOrigin: true,
|
|
47
|
+
},
|
|
48
|
+
"/ws": {
|
|
49
|
+
target: `ws://localhost:${backendPort}`,
|
|
50
|
+
ws: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
await vite.listen();
|
|
56
|
+
const url = `http://${host}:${frontendPort}`;
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(chalk.green("effectnode-media is running:"));
|
|
59
|
+
console.log(chalk.cyan(` Frontend → ${url}`));
|
|
60
|
+
console.log(chalk.cyan(` REST API → http://localhost:${backendPort}/api`));
|
|
61
|
+
console.log(chalk.cyan(` WebSocket → ws://localhost:${backendPort}/ws`));
|
|
62
|
+
console.log(chalk.dim(" Press Ctrl+C to stop."));
|
|
63
|
+
console.log("");
|
|
64
|
+
// 3. Open the frontend in the browser (skip when disabled or headless).
|
|
65
|
+
if (options.open !== false &&
|
|
66
|
+
process.env.EFFECTNODE_MEDIA_NO_OPEN !== "1") {
|
|
67
|
+
await open(url);
|
|
68
|
+
}
|
|
69
|
+
const shutdown = () => {
|
|
70
|
+
void vite.close();
|
|
71
|
+
backend.close();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
};
|
|
74
|
+
process.once("SIGINT", shutdown);
|
|
75
|
+
process.once("SIGTERM", shutdown);
|
|
76
|
+
});
|
|
77
|
+
program.parseAsync().catch((err) => {
|
|
78
|
+
console.error(chalk.red(`\n✖ Failed to start effectnode-media: ${err.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
<title>effectnode-media</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script>
|
|
13
|
+
// The backend (src/backend/movie-backend/core.ts) binds to the first free
|
|
14
|
+
// port starting at BACKEND_PORT_START = 4000. Previously this was injected
|
|
15
|
+
// by an Electron `preload` script; with the Bun → Node.js migration it must
|
|
16
|
+
// be set here, before any module reads `window.PORT`.
|
|
17
|
+
window.PORT = "4000";
|
|
18
|
+
</script>
|
|
19
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
20
|
+
</body>
|
|
21
|
+
|
|
22
|
+
</html>
|