@mulmoclaude/mulmoscript-plugin 0.1.0 → 0.2.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/dist/contract-DxKVCRQk.cjs +13 -0
- package/dist/contract-DxKVCRQk.cjs.map +1 -0
- package/dist/contract-vY0niHkh.js +8 -0
- package/dist/contract-vY0niHkh.js.map +1 -0
- package/dist/server/dispatch.d.ts +10 -0
- package/dist/server/dispatch.d.ts.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/mulmoErrorCapture.d.ts +29 -0
- package/dist/server/mulmoErrorCapture.d.ts.map +1 -0
- package/dist/server/ops.d.ts +101 -0
- package/dist/server/ops.d.ts.map +1 -0
- package/dist/server/support.d.ts +9 -0
- package/dist/server/support.d.ts.map +1 -0
- package/dist/server/types.d.ts +74 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server.cjs +992 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +955 -0
- package/dist/server.js.map +1 -0
- package/dist/style.css +87 -0
- package/dist/vue.cjs +3 -7
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1 -5
- package/dist/vue.js.map +1 -1
- package/package.json +10 -2
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
const require_plugin = require("./plugin-CtKUt8DX.cjs");
|
|
25
|
+
const require_contract = require("./contract-DxKVCRQk.cjs");
|
|
26
|
+
let fs = require("fs");
|
|
27
|
+
let path = require("path");
|
|
28
|
+
path = __toESM(path, 1);
|
|
29
|
+
let mulmocast = require("mulmocast");
|
|
30
|
+
let node_fs_promises = require("node:fs/promises");
|
|
31
|
+
let node_async_hooks = require("node:async_hooks");
|
|
32
|
+
let graphai = require("graphai");
|
|
33
|
+
//#region src/server/support.ts
|
|
34
|
+
function errorText(err) {
|
|
35
|
+
if (err instanceof Error) return err.message;
|
|
36
|
+
if (err !== null && typeof err === "object") {
|
|
37
|
+
const obj = err;
|
|
38
|
+
if (typeof obj.details === "string" && obj.details) return obj.details;
|
|
39
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
40
|
+
}
|
|
41
|
+
return String(err);
|
|
42
|
+
}
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function stripDataUri(dataUri) {
|
|
47
|
+
return dataUri.replace(/^data:image\/[^;]+;base64,/, "");
|
|
48
|
+
}
|
|
49
|
+
/** Realpath-based containment: resolve `relPath` against the ROOT's
|
|
50
|
+
* realpath and require the target's realpath to stay inside it. Returns
|
|
51
|
+
* null on ENOENT or traversal (symlink escapes included). */
|
|
52
|
+
function resolveWithinRoot(rootReal, relPath) {
|
|
53
|
+
const normalized = path.default.normalize(relPath || "");
|
|
54
|
+
const resolved = path.default.resolve(rootReal, normalized);
|
|
55
|
+
let resolvedReal;
|
|
56
|
+
try {
|
|
57
|
+
resolvedReal = (0, fs.realpathSync)(resolved);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.default.sep)) return null;
|
|
62
|
+
return resolvedReal;
|
|
63
|
+
}
|
|
64
|
+
async function fileToDataUri(filePath, mimeType) {
|
|
65
|
+
return `data:${mimeType};base64,${(await (0, node_fs_promises.readFile)(filePath)).toString("base64")}`;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/server/mulmoErrorCapture.ts
|
|
69
|
+
var capturedErrors = new node_async_hooks.AsyncLocalStorage();
|
|
70
|
+
var loggerInstalled = false;
|
|
71
|
+
var captureLog = null;
|
|
72
|
+
/** Route captured GraphAI errors into the host logger. Set once by
|
|
73
|
+
* `createMulmoScriptServerOps`; the GraphAILogger sink is global, so the
|
|
74
|
+
* last-configured host logger wins (one ops instance per process). */
|
|
75
|
+
function setMulmoErrorCaptureLogger(log) {
|
|
76
|
+
captureLog = log;
|
|
77
|
+
}
|
|
78
|
+
function formatLogArg(arg) {
|
|
79
|
+
if (typeof arg === "string") return arg;
|
|
80
|
+
if (arg instanceof Error) return arg.message;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.stringify(arg);
|
|
83
|
+
} catch {
|
|
84
|
+
return String(arg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Re-enable GraphAI's error level (everything else stays silenced) and
|
|
89
|
+
* route it into the host logger + the per-operation capture store.
|
|
90
|
+
* Call after every `setGraphAILogger(false)` — that helper disables all
|
|
91
|
+
* levels including error. Idempotent.
|
|
92
|
+
*/
|
|
93
|
+
function enableGraphAIErrorCapture() {
|
|
94
|
+
graphai.GraphAILogger.setLevelEnabled("error", true);
|
|
95
|
+
if (loggerInstalled) return;
|
|
96
|
+
loggerInstalled = true;
|
|
97
|
+
graphai.GraphAILogger.setLogger((level, ...args) => {
|
|
98
|
+
if (level !== "error") return;
|
|
99
|
+
const message = args.map(formatLogArg).join(" ");
|
|
100
|
+
captureLog?.warn("mulmocast generation error", { message });
|
|
101
|
+
capturedErrors.getStore()?.push(message);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
var CAUSE_FIELDS = [
|
|
105
|
+
"type",
|
|
106
|
+
"agentName",
|
|
107
|
+
"envVarName",
|
|
108
|
+
"errorCode",
|
|
109
|
+
"errorType"
|
|
110
|
+
];
|
|
111
|
+
/** Render mulmocast's structured error `cause` as "field=value" pairs. */
|
|
112
|
+
function describeMulmoCause(err) {
|
|
113
|
+
if (!(err instanceof Error) || !isRecord(err.cause)) return null;
|
|
114
|
+
const { cause } = err;
|
|
115
|
+
const parts = CAUSE_FIELDS.flatMap((field) => {
|
|
116
|
+
const value = cause[field];
|
|
117
|
+
return typeof value === "string" && value !== "" ? [`${field}=${value}`] : [];
|
|
118
|
+
});
|
|
119
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Compose the enriched message for a failed mulmocast operation:
|
|
123
|
+
* mulmocast's own message, then its structured cause, then the
|
|
124
|
+
* captured underlying provider error(s). Deduped — GraphAI retries
|
|
125
|
+
* log the same error more than once.
|
|
126
|
+
*/
|
|
127
|
+
function composeMulmoErrorMessage(err, captured) {
|
|
128
|
+
const base = errorText(err);
|
|
129
|
+
const details = [...new Set(captured)].filter((message) => message !== "" && message !== base);
|
|
130
|
+
return [
|
|
131
|
+
base,
|
|
132
|
+
describeMulmoCause(err),
|
|
133
|
+
...details
|
|
134
|
+
].filter(Boolean).join(" — ");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run a mulmocast operation, capturing GraphAI error logs emitted while
|
|
138
|
+
* it executes. On failure, rethrows with the captured provider error(s)
|
|
139
|
+
* appended to the message (original error kept as `cause`). Uses
|
|
140
|
+
* AsyncLocalStorage so concurrent operations don't cross-attribute.
|
|
141
|
+
*/
|
|
142
|
+
async function withMulmoErrorCapture(operation) {
|
|
143
|
+
return capturedErrors.run([], async () => {
|
|
144
|
+
try {
|
|
145
|
+
return await operation();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
throw new Error(composeMulmoErrorMessage(err, capturedErrors.getStore() ?? []), { cause: err });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/server/ops.ts
|
|
153
|
+
var PDF_MODE = "slide";
|
|
154
|
+
var PDF_SIZE = "a4";
|
|
155
|
+
function opBadRequest(error) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
code: "bad_request",
|
|
159
|
+
error
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function opNotFound(error) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: "not_found",
|
|
166
|
+
error
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function opServerError(error) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
code: "server_error",
|
|
173
|
+
error
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
var NOOP_LOG = {
|
|
177
|
+
info: () => {},
|
|
178
|
+
warn: () => {},
|
|
179
|
+
error: () => {}
|
|
180
|
+
};
|
|
181
|
+
async function buildContext(absoluteFilePath, force = false) {
|
|
182
|
+
(0, mulmocast.setGraphAILogger)(false);
|
|
183
|
+
enableGraphAIErrorCapture();
|
|
184
|
+
return (0, mulmocast.initializeContextFromFiles)((0, mulmocast.getFileObject)({
|
|
185
|
+
file: absoluteFilePath,
|
|
186
|
+
basedir: path.default.dirname(absoluteFilePath),
|
|
187
|
+
grouped: true
|
|
188
|
+
}), true, force);
|
|
189
|
+
}
|
|
190
|
+
function buildBeatIdIndex(beats) {
|
|
191
|
+
const idToIndex = /* @__PURE__ */ new Map();
|
|
192
|
+
beats.forEach((beat, index) => {
|
|
193
|
+
const key = beat.id ?? `__index__${index}`;
|
|
194
|
+
idToIndex.set(key, index);
|
|
195
|
+
});
|
|
196
|
+
return idToIndex;
|
|
197
|
+
}
|
|
198
|
+
/** Map identity for the in-flight tracker. JSON array keeps the three
|
|
199
|
+
* fields unambiguous (a human-visible delimiter could collide). */
|
|
200
|
+
function generationMapKey(kind, filePath, key) {
|
|
201
|
+
return JSON.stringify([
|
|
202
|
+
kind,
|
|
203
|
+
filePath,
|
|
204
|
+
key
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build the per-host mulmoScript server ops instance. One instance per
|
|
209
|
+
* process — it owns the in-flight movie/PDF dedup sets and the
|
|
210
|
+
* generation-state tracker, and binds the injected host backend.
|
|
211
|
+
*/
|
|
212
|
+
function createMulmoScriptServerOps(backend) {
|
|
213
|
+
const log = backend.log ?? NOOP_LOG;
|
|
214
|
+
setMulmoErrorCaptureLogger(log);
|
|
215
|
+
const storiesDir = path.default.resolve(backend.storiesDir);
|
|
216
|
+
function toStoryRef(absolutePath) {
|
|
217
|
+
const root = ensureStoriesReal() ?? storiesDir;
|
|
218
|
+
const rel = path.default.relative(root, absolutePath).split(path.default.sep).join("/");
|
|
219
|
+
return rel ? `stories/${rel}` : "stories";
|
|
220
|
+
}
|
|
221
|
+
let storiesRealCache = null;
|
|
222
|
+
function ensureStoriesReal() {
|
|
223
|
+
if (storiesRealCache) return storiesRealCache;
|
|
224
|
+
try {
|
|
225
|
+
(0, fs.mkdirSync)(storiesDir, { recursive: true });
|
|
226
|
+
storiesRealCache = (0, fs.realpathSync)(storiesDir);
|
|
227
|
+
return storiesRealCache;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve and validate a stories wire path to its absolute realpath.
|
|
234
|
+
*
|
|
235
|
+
* Uses the realpath-based resolveWithinRoot helper to defeat
|
|
236
|
+
* symlink-based escapes. Callers pass workspace-relative paths like
|
|
237
|
+
* "stories/foo.json" or "stories/__movies__/bar.mp4". We strip the
|
|
238
|
+
* leading "stories/" segment and resolve the remainder against the
|
|
239
|
+
* realpath of the stories directory itself — this works whether
|
|
240
|
+
* stories/ is a regular directory or a legitimate symlink to another
|
|
241
|
+
* location. ENOENT and traversal are distinguished (404 vs 400).
|
|
242
|
+
*/
|
|
243
|
+
function resolveStory(filePath) {
|
|
244
|
+
const storiesReal = ensureStoriesReal();
|
|
245
|
+
if (!storiesReal) return opServerError("stories directory not available");
|
|
246
|
+
if (path.default.isAbsolute(filePath)) return opBadRequest("Invalid filePath");
|
|
247
|
+
const STORIES_PREFIX = `stories${path.default.sep}`;
|
|
248
|
+
const relFromStories = filePath === "stories" ? "" : filePath.startsWith(STORIES_PREFIX) || filePath.startsWith("stories/") ? filePath.slice(8) : filePath;
|
|
249
|
+
const resolved = resolveWithinRoot(storiesReal, relFromStories);
|
|
250
|
+
if (!resolved) {
|
|
251
|
+
const candidate = path.default.resolve(storiesReal, relFromStories);
|
|
252
|
+
if ((candidate === storiesReal || candidate.startsWith(storiesReal + path.default.sep)) && !(0, fs.existsSync)(candidate)) return opNotFound(`File not found: ${filePath}`);
|
|
253
|
+
return opBadRequest("Invalid filePath");
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
absolutePath: resolved
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Realpath containment pre-guard for wire paths handed to the phase-1
|
|
262
|
+
* core's save/reopen/update executes. The core's own path guard is
|
|
263
|
+
* lexical (it runs against the generic FileOps, whose read/write follows
|
|
264
|
+
* symlinks), so hosts re-assert the realpath boundary here before
|
|
265
|
+
* invoking it — a symlink planted below the stories dir can't read or
|
|
266
|
+
* write outside the tree (Codex P1 on MulmoClaude#2133).
|
|
267
|
+
*
|
|
268
|
+
* Returns null when `filePath` isn't a non-empty string — shape
|
|
269
|
+
* validation (including the script-vs-filePath mode check) belongs to
|
|
270
|
+
* the core.
|
|
271
|
+
*/
|
|
272
|
+
function guardStoryWirePath(filePath) {
|
|
273
|
+
if (typeof filePath !== "string" || filePath === "") return null;
|
|
274
|
+
const resolved = resolveStory(filePath);
|
|
275
|
+
return resolved.ok ? null : resolved;
|
|
276
|
+
}
|
|
277
|
+
function ffmpegGuard() {
|
|
278
|
+
if (backend.isFfmpegAvailable?.() === false) return {
|
|
279
|
+
ok: false,
|
|
280
|
+
code: "unavailable",
|
|
281
|
+
error: "ffmpeg is not installed — movie and beat rendering are unavailable. Install ffmpeg and restart the server."
|
|
282
|
+
};
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const inFlightGenerations = /* @__PURE__ */ new Map();
|
|
286
|
+
function publishGeneration(chatSessionId, kind, filePath, key, finished, error) {
|
|
287
|
+
const mapKey = generationMapKey(kind, filePath, key);
|
|
288
|
+
const existing = inFlightGenerations.get(mapKey);
|
|
289
|
+
if (finished) {
|
|
290
|
+
if (existing && existing.count > 1) {
|
|
291
|
+
existing.count -= 1;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
inFlightGenerations.delete(mapKey);
|
|
295
|
+
} else {
|
|
296
|
+
if (existing) {
|
|
297
|
+
existing.count += 1;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
inFlightGenerations.set(mapKey, {
|
|
301
|
+
kind,
|
|
302
|
+
filePath,
|
|
303
|
+
key,
|
|
304
|
+
count: 1
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const event = {
|
|
308
|
+
kind,
|
|
309
|
+
filePath,
|
|
310
|
+
key,
|
|
311
|
+
done: finished,
|
|
312
|
+
...error ? { error } : {}
|
|
313
|
+
};
|
|
314
|
+
backend.onGenerationEvent?.(chatSessionId, event);
|
|
315
|
+
}
|
|
316
|
+
/** Snapshot of generations currently in flight for one script — the
|
|
317
|
+
* View's mount-time catch-up, filtered to its wire `filePath`. */
|
|
318
|
+
function pendingGenerations(filePath) {
|
|
319
|
+
return [...inFlightGenerations.values()].filter((entry) => entry.filePath === filePath).map(({ kind, key }) => ({
|
|
320
|
+
kind,
|
|
321
|
+
filePath,
|
|
322
|
+
key,
|
|
323
|
+
done: false
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Shared scaffolding for mulmoScript ops. Resolves the wire filePath,
|
|
328
|
+
* builds the mulmo context, and folds unexpected handler errors into a
|
|
329
|
+
* server_error failure (with a warn breadcrumb). Accepts a `deps` param
|
|
330
|
+
* so unit tests can inject fakes without the full mulmocast stack.
|
|
331
|
+
*/
|
|
332
|
+
async function runStoryOp(filePath, options, handler, deps = {}) {
|
|
333
|
+
const resolver = deps.resolveStory ?? resolveStory;
|
|
334
|
+
const build = deps.buildContext ?? buildContext;
|
|
335
|
+
const resolved = resolver(filePath);
|
|
336
|
+
if (!resolved.ok) return resolved;
|
|
337
|
+
try {
|
|
338
|
+
const context = await build(resolved.absolutePath, options.force ?? false);
|
|
339
|
+
if (!context) {
|
|
340
|
+
if (options.onContextMissing) return options.onContextMissing();
|
|
341
|
+
return opServerError("Failed to initialize mulmo context");
|
|
342
|
+
}
|
|
343
|
+
return await withMulmoErrorCapture(() => handler({
|
|
344
|
+
absoluteFilePath: resolved.absolutePath,
|
|
345
|
+
context
|
|
346
|
+
}));
|
|
347
|
+
} catch (err) {
|
|
348
|
+
log.warn("op failed", {
|
|
349
|
+
...options.operation ? { operation: options.operation } : {},
|
|
350
|
+
filePath,
|
|
351
|
+
error: errorText(err)
|
|
352
|
+
});
|
|
353
|
+
return opServerError(errorText(err));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function beatImageOp(filePath, beatIndex) {
|
|
357
|
+
return runStoryOp(filePath, { operation: "beat-image" }, async ({ context }) => {
|
|
358
|
+
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
359
|
+
if (!(0, fs.existsSync)(imagePath)) return {
|
|
360
|
+
ok: true,
|
|
361
|
+
image: null
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
ok: true,
|
|
365
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async function beatAudioOp(filePath, beatIndex) {
|
|
370
|
+
return runStoryOp(filePath, {
|
|
371
|
+
operation: "beat-audio",
|
|
372
|
+
onContextMissing: () => ({
|
|
373
|
+
ok: true,
|
|
374
|
+
audio: null
|
|
375
|
+
})
|
|
376
|
+
}, async ({ context }) => {
|
|
377
|
+
const beat = context.studio.script.beats[beatIndex];
|
|
378
|
+
const audioPath = (0, mulmocast.getBeatAudioPathOrUrl)(beat.text ?? "", context, beat, context.lang);
|
|
379
|
+
if (!audioPath || !(0, fs.existsSync)(audioPath)) return {
|
|
380
|
+
ok: true,
|
|
381
|
+
audio: null
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
audio: await fileToDataUri(audioPath, "audio/mpeg")
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function beatMovieOp(filePath, beatIndex) {
|
|
390
|
+
return runStoryOp(filePath, { operation: "beat-movie" }, async ({ context }) => {
|
|
391
|
+
const { movieFile, soundEffectFile, lipSyncFile } = (0, mulmocast.getBeatMoviePaths)(context, beatIndex);
|
|
392
|
+
const existing = [
|
|
393
|
+
lipSyncFile,
|
|
394
|
+
soundEffectFile,
|
|
395
|
+
movieFile,
|
|
396
|
+
(0, mulmocast.getBeatAnimatedVideoPath)(context, beatIndex)
|
|
397
|
+
].find((candidate) => (0, fs.existsSync)(candidate));
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
moviePath: existing ? toStoryRef(existing) : null
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async function characterImageOp(filePath, key) {
|
|
405
|
+
return runStoryOp(filePath, { operation: "character-image" }, async ({ context }) => {
|
|
406
|
+
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
407
|
+
if (!(0, fs.existsSync)(imagePath)) return {
|
|
408
|
+
ok: true,
|
|
409
|
+
image: null
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/** Shared "output exists and is newer than the source script" gate for
|
|
418
|
+
* movie / PDF status. A stale artifact (script edited after it was
|
|
419
|
+
* generated) reports null so the UI re-offers the Generate button. */
|
|
420
|
+
function freshOutputRef(outputPath, absoluteFilePath) {
|
|
421
|
+
if (!(0, fs.existsSync)(outputPath)) return null;
|
|
422
|
+
if ((0, fs.statSync)(outputPath).mtimeMs < (0, fs.statSync)(absoluteFilePath).mtimeMs) return null;
|
|
423
|
+
return toStoryRef(outputPath);
|
|
424
|
+
}
|
|
425
|
+
async function movieStatusOp(filePath) {
|
|
426
|
+
return runStoryOp(filePath, {
|
|
427
|
+
operation: "movie-status",
|
|
428
|
+
onContextMissing: () => ({
|
|
429
|
+
ok: true,
|
|
430
|
+
moviePath: null
|
|
431
|
+
})
|
|
432
|
+
}, async ({ absoluteFilePath, context }) => ({
|
|
433
|
+
ok: true,
|
|
434
|
+
moviePath: freshOutputRef((0, mulmocast.movieFilePath)(context), absoluteFilePath)
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
async function pdfStatusOp(filePath) {
|
|
438
|
+
return runStoryOp(filePath, {
|
|
439
|
+
operation: "pdf-status",
|
|
440
|
+
onContextMissing: () => ({
|
|
441
|
+
ok: true,
|
|
442
|
+
pdfPath: null
|
|
443
|
+
})
|
|
444
|
+
}, async ({ absoluteFilePath, context }) => ({
|
|
445
|
+
ok: true,
|
|
446
|
+
pdfPath: freshOutputRef((0, mulmocast.pdfFilePath)(context, PDF_MODE), absoluteFilePath)
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
async function renderBeatOp(args) {
|
|
450
|
+
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
451
|
+
const ffmpeg = ffmpegGuard();
|
|
452
|
+
if (ffmpeg) return ffmpeg;
|
|
453
|
+
const mapKey = String(beatIndex);
|
|
454
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, false);
|
|
455
|
+
let genError;
|
|
456
|
+
try {
|
|
457
|
+
const result = await runStoryOp(filePath, {
|
|
458
|
+
force,
|
|
459
|
+
operation: "render-beat"
|
|
460
|
+
}, async ({ context }) => {
|
|
461
|
+
await (0, mulmocast.generateBeatImage)({
|
|
462
|
+
index: beatIndex,
|
|
463
|
+
context,
|
|
464
|
+
args: force ? { forceImage: true } : void 0
|
|
465
|
+
});
|
|
466
|
+
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
467
|
+
if (!(0, fs.existsSync)(imagePath)) return opServerError("Image was not generated");
|
|
468
|
+
return {
|
|
469
|
+
ok: true,
|
|
470
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
if (!result.ok) genError = result.error;
|
|
474
|
+
return result;
|
|
475
|
+
} finally {
|
|
476
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, true, genError);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function generateBeatAudioOp(args) {
|
|
480
|
+
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
481
|
+
const mapKey = String(beatIndex);
|
|
482
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, false);
|
|
483
|
+
let genError;
|
|
484
|
+
try {
|
|
485
|
+
const result = await runStoryOp(filePath, {
|
|
486
|
+
force,
|
|
487
|
+
operation: "generate-beat-audio"
|
|
488
|
+
}, async ({ context }) => {
|
|
489
|
+
await (0, mulmocast.generateBeatAudio)(beatIndex, context, { settings: process.env });
|
|
490
|
+
const beat = context.studio.script.beats[beatIndex];
|
|
491
|
+
const audioPath = context.studio.beats[beatIndex]?.audioFile ?? (0, mulmocast.getBeatAudioPathOrUrl)(beat.text ?? "", context, beat, context.lang);
|
|
492
|
+
if (!audioPath || !(0, fs.existsSync)(audioPath)) {
|
|
493
|
+
log.error("audio was not generated", {
|
|
494
|
+
beatIndex,
|
|
495
|
+
audioPath,
|
|
496
|
+
exists: audioPath ? (0, fs.existsSync)(audioPath) : false,
|
|
497
|
+
beatTextLength: typeof beat?.text === "string" ? beat.text.length : 0,
|
|
498
|
+
audioFilePresent: Boolean(context.studio.beats[beatIndex]?.audioFile)
|
|
499
|
+
});
|
|
500
|
+
return opServerError("Audio was not generated");
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
audio: await fileToDataUri(audioPath, "audio/mpeg")
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
if (!result.ok) genError = result.error;
|
|
508
|
+
return result;
|
|
509
|
+
} finally {
|
|
510
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, true, genError);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function renderCharacterOp(args) {
|
|
514
|
+
const { filePath, key, force, chatSessionId } = args;
|
|
515
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, false);
|
|
516
|
+
let genError;
|
|
517
|
+
try {
|
|
518
|
+
const result = await runStoryOp(filePath, {
|
|
519
|
+
force,
|
|
520
|
+
operation: "render-character"
|
|
521
|
+
}, async ({ context }) => {
|
|
522
|
+
const imageEntries = context.studio.script.imageParams?.images ?? {};
|
|
523
|
+
const imageEntry = imageEntries[key];
|
|
524
|
+
if (!imageEntry || imageEntry.type !== "imagePrompt") return opBadRequest(`No imagePrompt entry for key: ${key}`);
|
|
525
|
+
const index = Object.keys(imageEntries).indexOf(key);
|
|
526
|
+
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
527
|
+
(0, fs.mkdirSync)(path.default.dirname(imagePath), { recursive: true });
|
|
528
|
+
await (0, mulmocast.generateReferenceImage)({
|
|
529
|
+
context,
|
|
530
|
+
key,
|
|
531
|
+
index,
|
|
532
|
+
image: imageEntry,
|
|
533
|
+
force
|
|
534
|
+
});
|
|
535
|
+
if (!(0, fs.existsSync)(imagePath)) return opServerError("Character image was not generated");
|
|
536
|
+
return {
|
|
537
|
+
ok: true,
|
|
538
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
if (!result.ok) genError = result.error;
|
|
542
|
+
return result;
|
|
543
|
+
} finally {
|
|
544
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, true, genError);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function uploadBeatImageOp(filePath, beatIndex, imageData) {
|
|
548
|
+
return runStoryOp(filePath, { operation: "upload-beat-image" }, async ({ context }) => {
|
|
549
|
+
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
550
|
+
const base64 = stripDataUri(imageData);
|
|
551
|
+
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
552
|
+
return {
|
|
553
|
+
ok: true,
|
|
554
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async function uploadCharacterImageOp(filePath, key, imageData) {
|
|
559
|
+
return runStoryOp(filePath, { operation: "upload-character-image" }, async ({ context }) => {
|
|
560
|
+
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
561
|
+
const base64 = stripDataUri(imageData);
|
|
562
|
+
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
image: await fileToDataUri(imagePath, "image/png")
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const inFlightMovies = /* @__PURE__ */ new Set();
|
|
570
|
+
const inFlightPdfs = /* @__PURE__ */ new Set();
|
|
571
|
+
async function runMovieGeneration(absoluteFilePath, onProgressEvent) {
|
|
572
|
+
return withMulmoErrorCapture(() => runMoviePipeline(absoluteFilePath, onProgressEvent));
|
|
573
|
+
}
|
|
574
|
+
async function runMoviePipeline(absoluteFilePath, onProgressEvent) {
|
|
575
|
+
const context = await buildContext(absoluteFilePath);
|
|
576
|
+
if (!context) return {
|
|
577
|
+
ok: false,
|
|
578
|
+
error: "Failed to initialize mulmo context"
|
|
579
|
+
};
|
|
580
|
+
const idToIndex = buildBeatIdIndex(context.studio.script.beats);
|
|
581
|
+
const onProgress = (event) => {
|
|
582
|
+
if (event.kind !== "beat" || event.inSession || event.id === void 0) return;
|
|
583
|
+
const beatIndex = idToIndex.get(event.id);
|
|
584
|
+
if (beatIndex === void 0) return;
|
|
585
|
+
if (event.sessionType !== "image" && event.sessionType !== "audio") return;
|
|
586
|
+
onProgressEvent({
|
|
587
|
+
kind: event.sessionType,
|
|
588
|
+
beatIndex
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
(0, mulmocast.addSessionProgressCallback)(onProgress);
|
|
592
|
+
try {
|
|
593
|
+
const imagesContext = await (0, mulmocast.images)(await (0, mulmocast.audio)(context));
|
|
594
|
+
await (0, mulmocast.movie)(imagesContext);
|
|
595
|
+
const outputPath = (0, mulmocast.movieFilePath)(imagesContext);
|
|
596
|
+
if (!(0, fs.existsSync)(outputPath)) return {
|
|
597
|
+
ok: false,
|
|
598
|
+
error: "Movie was not generated"
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
ok: true,
|
|
602
|
+
outputPath
|
|
603
|
+
};
|
|
604
|
+
} finally {
|
|
605
|
+
(0, mulmocast.removeSessionProgressCallback)(onProgress);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Long-held foreground movie generation (the package View's
|
|
610
|
+
* `generateMovie` dispatch). Resolves when the whole pipeline finishes.
|
|
611
|
+
* Per-beat completions are mirrored to the generation channels so the
|
|
612
|
+
* initiating View (and any other mounted View) reloads assets off disk
|
|
613
|
+
* as they land — the successor of the SSE per-beat events.
|
|
614
|
+
*/
|
|
615
|
+
async function generateMovieOp(filePath, chatSessionId) {
|
|
616
|
+
const ffmpeg = ffmpegGuard();
|
|
617
|
+
if (ffmpeg) return ffmpeg;
|
|
618
|
+
const resolved = resolveStory(filePath);
|
|
619
|
+
if (!resolved.ok) return resolved;
|
|
620
|
+
const absoluteFilePath = resolved.absolutePath;
|
|
621
|
+
if (inFlightMovies.has(absoluteFilePath)) return opBadRequest("Movie generation is already in progress for this script");
|
|
622
|
+
inFlightMovies.add(absoluteFilePath);
|
|
623
|
+
publishGeneration(chatSessionId, "movie", filePath, "", false);
|
|
624
|
+
let genError;
|
|
625
|
+
try {
|
|
626
|
+
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
627
|
+
publishGeneration(chatSessionId, event.kind === "image" ? "beatImage" : "beatAudio", filePath, String(event.beatIndex), true);
|
|
628
|
+
});
|
|
629
|
+
if (!result.ok) {
|
|
630
|
+
genError = result.error;
|
|
631
|
+
return opServerError(result.error);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
moviePath: toStoryRef(result.outputPath)
|
|
636
|
+
};
|
|
637
|
+
} catch (err) {
|
|
638
|
+
genError = errorText(err);
|
|
639
|
+
return opServerError(genError);
|
|
640
|
+
} finally {
|
|
641
|
+
inFlightMovies.delete(absoluteFilePath);
|
|
642
|
+
publishGeneration(chatSessionId, "movie", filePath, "", true, genError);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function triggerAutoBackgroundMovie(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
646
|
+
if (inFlightMovies.has(absoluteFilePath)) return;
|
|
647
|
+
inFlightMovies.add(absoluteFilePath);
|
|
648
|
+
runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId);
|
|
649
|
+
}
|
|
650
|
+
async function runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
651
|
+
const errorSidecarPath = `${absoluteFilePath}.error.txt`;
|
|
652
|
+
try {
|
|
653
|
+
(0, fs.unlinkSync)(errorSidecarPath);
|
|
654
|
+
} catch {}
|
|
655
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", false);
|
|
656
|
+
let genError;
|
|
657
|
+
try {
|
|
658
|
+
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
659
|
+
const eventKind = event.kind === "image" ? "beatImage" : "beatAudio";
|
|
660
|
+
const key = String(event.beatIndex);
|
|
661
|
+
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false);
|
|
662
|
+
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true));
|
|
663
|
+
});
|
|
664
|
+
if (!result.ok) {
|
|
665
|
+
genError = result.error;
|
|
666
|
+
await writeErrorSidecar(errorSidecarPath, result.error);
|
|
667
|
+
log.warn("background movie generation failed", {
|
|
668
|
+
filePath: wireFilePath,
|
|
669
|
+
error: result.error
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
log.info("background movie generation done", {
|
|
674
|
+
filePath: wireFilePath,
|
|
675
|
+
outputPath: result.outputPath
|
|
676
|
+
});
|
|
677
|
+
} catch (err) {
|
|
678
|
+
genError = errorText(err);
|
|
679
|
+
await writeErrorSidecar(errorSidecarPath, genError);
|
|
680
|
+
log.error("background movie generation crashed", {
|
|
681
|
+
filePath: wireFilePath,
|
|
682
|
+
error: genError
|
|
683
|
+
});
|
|
684
|
+
} finally {
|
|
685
|
+
inFlightMovies.delete(absoluteFilePath);
|
|
686
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", true, genError);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function writeErrorSidecar(errorSidecarPath, message) {
|
|
690
|
+
try {
|
|
691
|
+
await backend.writeFileAtomic(errorSidecarPath, message);
|
|
692
|
+
} catch (writeErr) {
|
|
693
|
+
log.error("failed to write error sidecar", {
|
|
694
|
+
errorSidecarPath,
|
|
695
|
+
error: errorText(writeErr)
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function runPdfGeneration(context, onImageBeatDone) {
|
|
700
|
+
return withMulmoErrorCapture(() => runPdfPipeline(context, onImageBeatDone));
|
|
701
|
+
}
|
|
702
|
+
async function runPdfPipeline(context, onImageBeatDone) {
|
|
703
|
+
const idToIndex = buildBeatIdIndex(context.studio.script.beats);
|
|
704
|
+
const onProgress = (event) => {
|
|
705
|
+
if (event.kind !== "beat" || event.inSession || event.id === void 0) return;
|
|
706
|
+
const beatIndex = idToIndex.get(event.id);
|
|
707
|
+
if (beatIndex === void 0) return;
|
|
708
|
+
if (event.sessionType !== "image") return;
|
|
709
|
+
onImageBeatDone(beatIndex);
|
|
710
|
+
};
|
|
711
|
+
(0, mulmocast.addSessionProgressCallback)(onProgress);
|
|
712
|
+
try {
|
|
713
|
+
const imagesContext = await (0, mulmocast.images)(context);
|
|
714
|
+
await (0, mulmocast.pdf)(imagesContext, PDF_MODE, "a4");
|
|
715
|
+
const outputPath = (0, mulmocast.pdfFilePath)(imagesContext, PDF_MODE);
|
|
716
|
+
if (!(0, fs.existsSync)(outputPath)) return {
|
|
717
|
+
ok: false,
|
|
718
|
+
error: "PDF was not generated"
|
|
719
|
+
};
|
|
720
|
+
return {
|
|
721
|
+
ok: true,
|
|
722
|
+
outputPath
|
|
723
|
+
};
|
|
724
|
+
} finally {
|
|
725
|
+
(0, mulmocast.removeSessionProgressCallback)(onProgress);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/** Long-held foreground PDF generation (the package View's `generatePdf`
|
|
729
|
+
* dispatch) — the PDF sibling of `generateMovieOp`. */
|
|
730
|
+
async function generatePdfOp(filePath, chatSessionId) {
|
|
731
|
+
const ffmpeg = ffmpegGuard();
|
|
732
|
+
if (ffmpeg) return ffmpeg;
|
|
733
|
+
const resolved = resolveStory(filePath);
|
|
734
|
+
if (!resolved.ok) return resolved;
|
|
735
|
+
const absoluteFilePath = resolved.absolutePath;
|
|
736
|
+
if (inFlightPdfs.has(absoluteFilePath)) return opBadRequest("PDF generation is already in progress for this script");
|
|
737
|
+
inFlightPdfs.add(absoluteFilePath);
|
|
738
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", false);
|
|
739
|
+
let genError;
|
|
740
|
+
try {
|
|
741
|
+
const context = await buildContext(absoluteFilePath);
|
|
742
|
+
if (!context) {
|
|
743
|
+
genError = "Failed to initialize mulmo context";
|
|
744
|
+
return opServerError(genError);
|
|
745
|
+
}
|
|
746
|
+
const result = await runPdfGeneration(context, (beatIndex) => {
|
|
747
|
+
publishGeneration(chatSessionId, "beatImage", filePath, String(beatIndex), true);
|
|
748
|
+
});
|
|
749
|
+
if (!result.ok) {
|
|
750
|
+
genError = result.error;
|
|
751
|
+
return opServerError(result.error);
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
ok: true,
|
|
755
|
+
pdfPath: toStoryRef(result.outputPath)
|
|
756
|
+
};
|
|
757
|
+
} catch (err) {
|
|
758
|
+
genError = errorText(err);
|
|
759
|
+
return opServerError(genError);
|
|
760
|
+
} finally {
|
|
761
|
+
inFlightPdfs.delete(absoluteFilePath);
|
|
762
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", true, genError);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
backend,
|
|
767
|
+
toStoryRef,
|
|
768
|
+
resolveStory,
|
|
769
|
+
guardStoryWirePath,
|
|
770
|
+
ffmpegGuard,
|
|
771
|
+
runStoryOp,
|
|
772
|
+
publishGeneration,
|
|
773
|
+
pendingGenerations,
|
|
774
|
+
beatImageOp,
|
|
775
|
+
beatAudioOp,
|
|
776
|
+
beatMovieOp,
|
|
777
|
+
characterImageOp,
|
|
778
|
+
movieStatusOp,
|
|
779
|
+
pdfStatusOp,
|
|
780
|
+
renderBeatOp,
|
|
781
|
+
generateBeatAudioOp,
|
|
782
|
+
renderCharacterOp,
|
|
783
|
+
uploadBeatImageOp,
|
|
784
|
+
uploadCharacterImageOp,
|
|
785
|
+
inFlightMovies,
|
|
786
|
+
inFlightPdfs,
|
|
787
|
+
runMovieGeneration,
|
|
788
|
+
runPdfGeneration,
|
|
789
|
+
generateMovieOp,
|
|
790
|
+
generatePdfOp,
|
|
791
|
+
triggerAutoBackgroundMovie
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/server/dispatch.ts
|
|
796
|
+
function fromOpFailure(failure) {
|
|
797
|
+
return {
|
|
798
|
+
ok: false,
|
|
799
|
+
code: failure.code === "unavailable" ? "server_error" : failure.code,
|
|
800
|
+
error: failure.error
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function fromPackageFailure(failure) {
|
|
804
|
+
return {
|
|
805
|
+
ok: false,
|
|
806
|
+
code: failure.code,
|
|
807
|
+
error: failure.error
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function invalidArgs(kind) {
|
|
811
|
+
return {
|
|
812
|
+
ok: false,
|
|
813
|
+
code: "bad_request",
|
|
814
|
+
error: `invalid arguments for mulmoScript dispatch kind "${kind}"`
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function str(value) {
|
|
818
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
819
|
+
}
|
|
820
|
+
function num(value) {
|
|
821
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
822
|
+
}
|
|
823
|
+
/** Pass ok results through untouched; normalize failures for the wire. */
|
|
824
|
+
function envelope(result) {
|
|
825
|
+
return result.ok ? result : fromOpFailure(result);
|
|
826
|
+
}
|
|
827
|
+
function beatArgs(args) {
|
|
828
|
+
const filePath = str(args.filePath);
|
|
829
|
+
const beatIndex = num(args.beatIndex);
|
|
830
|
+
if (!filePath || beatIndex === void 0) return null;
|
|
831
|
+
return {
|
|
832
|
+
filePath,
|
|
833
|
+
beatIndex
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function keyArgs(args) {
|
|
837
|
+
const filePath = str(args.filePath);
|
|
838
|
+
const key = str(args.key);
|
|
839
|
+
if (!filePath || !key) return null;
|
|
840
|
+
return {
|
|
841
|
+
filePath,
|
|
842
|
+
key
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
var PROBE_KINDS = /* @__PURE__ */ new Set([
|
|
846
|
+
"beatImage",
|
|
847
|
+
"beatAudio",
|
|
848
|
+
"beatMovie",
|
|
849
|
+
"characterImage",
|
|
850
|
+
"movieStatus",
|
|
851
|
+
"pdfStatus"
|
|
852
|
+
]);
|
|
853
|
+
var GENERATE_KINDS = /* @__PURE__ */ new Set([
|
|
854
|
+
"renderBeat",
|
|
855
|
+
"generateBeatAudio",
|
|
856
|
+
"renderCharacter",
|
|
857
|
+
"generateMovie",
|
|
858
|
+
"generatePdf"
|
|
859
|
+
]);
|
|
860
|
+
var UPLOAD_KINDS = /* @__PURE__ */ new Set(["uploadBeatImage", "uploadCharacterImage"]);
|
|
861
|
+
/**
|
|
862
|
+
* Build the kind router over an ops instance. The save / reopen / update
|
|
863
|
+
* kinds run the phase-1 core executes against the backend's artifacts
|
|
864
|
+
* FileOps, guarded by the instance's realpath containment
|
|
865
|
+
* (`guardStoryWirePath`) — the core's own guard is lexical.
|
|
866
|
+
*/
|
|
867
|
+
function createMulmoScriptDispatchHandler(ops) {
|
|
868
|
+
const executeContext = { files: { artifacts: ops.backend.artifacts } };
|
|
869
|
+
async function saveKind(args) {
|
|
870
|
+
const guard = ops.guardStoryWirePath(args.filePath);
|
|
871
|
+
if (guard) return fromOpFailure(guard);
|
|
872
|
+
const outcome = await require_plugin.executeMulmoScriptSave(executeContext, {
|
|
873
|
+
script: args.script,
|
|
874
|
+
filename: str(args.filename),
|
|
875
|
+
filePath: str(args.filePath)
|
|
876
|
+
});
|
|
877
|
+
if (!outcome.ok) return fromPackageFailure(outcome);
|
|
878
|
+
return {
|
|
879
|
+
ok: true,
|
|
880
|
+
script: outcome.script,
|
|
881
|
+
filePath: outcome.filePath,
|
|
882
|
+
message: outcome.message
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async function updateKind(kind, args) {
|
|
886
|
+
const guard = ops.guardStoryWirePath(args.filePath);
|
|
887
|
+
if (guard) return fromOpFailure(guard);
|
|
888
|
+
const outcome = kind === "updateBeat" ? await require_plugin.executeUpdateBeat(executeContext, args) : await require_plugin.executeUpdateScript(executeContext, args);
|
|
889
|
+
return outcome.ok ? { ok: true } : fromPackageFailure(outcome);
|
|
890
|
+
}
|
|
891
|
+
const STATUS_OPS = {
|
|
892
|
+
movieStatus: ops.movieStatusOp,
|
|
893
|
+
pdfStatus: ops.pdfStatusOp
|
|
894
|
+
};
|
|
895
|
+
const BEAT_PROBE_OPS = {
|
|
896
|
+
beatImage: ops.beatImageOp,
|
|
897
|
+
beatAudio: ops.beatAudioOp,
|
|
898
|
+
beatMovie: ops.beatMovieOp
|
|
899
|
+
};
|
|
900
|
+
async function probeKind(kind, args) {
|
|
901
|
+
const statusOp = STATUS_OPS[kind];
|
|
902
|
+
if (statusOp) {
|
|
903
|
+
const filePath = str(args.filePath);
|
|
904
|
+
return filePath ? envelope(await statusOp(filePath)) : invalidArgs(kind);
|
|
905
|
+
}
|
|
906
|
+
if (kind === "characterImage") {
|
|
907
|
+
const parsed = keyArgs(args);
|
|
908
|
+
return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key)) : invalidArgs(kind);
|
|
909
|
+
}
|
|
910
|
+
const parsed = beatArgs(args);
|
|
911
|
+
if (!parsed) return invalidArgs(kind);
|
|
912
|
+
return envelope(await BEAT_PROBE_OPS[kind](parsed.filePath, parsed.beatIndex));
|
|
913
|
+
}
|
|
914
|
+
async function generateKind(kind, args) {
|
|
915
|
+
const chatSessionId = str(args.chatSessionId);
|
|
916
|
+
const force = args.force === true;
|
|
917
|
+
if (kind === "generateMovie" || kind === "generatePdf") {
|
|
918
|
+
const filePath = str(args.filePath);
|
|
919
|
+
if (!filePath) return invalidArgs(kind);
|
|
920
|
+
return envelope(kind === "generateMovie" ? await ops.generateMovieOp(filePath, chatSessionId) : await ops.generatePdfOp(filePath, chatSessionId));
|
|
921
|
+
}
|
|
922
|
+
if (kind === "renderCharacter") {
|
|
923
|
+
const parsed = keyArgs(args);
|
|
924
|
+
return parsed ? envelope(await ops.renderCharacterOp({
|
|
925
|
+
...parsed,
|
|
926
|
+
force,
|
|
927
|
+
chatSessionId
|
|
928
|
+
})) : invalidArgs(kind);
|
|
929
|
+
}
|
|
930
|
+
const parsed = beatArgs(args);
|
|
931
|
+
if (!parsed) return invalidArgs(kind);
|
|
932
|
+
return envelope(kind === "renderBeat" ? await ops.renderBeatOp({
|
|
933
|
+
...parsed,
|
|
934
|
+
force,
|
|
935
|
+
chatSessionId
|
|
936
|
+
}) : await ops.generateBeatAudioOp({
|
|
937
|
+
...parsed,
|
|
938
|
+
force,
|
|
939
|
+
chatSessionId
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
async function uploadKind(kind, args) {
|
|
943
|
+
const imageData = str(args.imageData);
|
|
944
|
+
if (!imageData) return invalidArgs(kind);
|
|
945
|
+
if (kind === "uploadCharacterImage") {
|
|
946
|
+
const parsed = keyArgs(args);
|
|
947
|
+
return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData)) : invalidArgs(kind);
|
|
948
|
+
}
|
|
949
|
+
const parsed = beatArgs(args);
|
|
950
|
+
if (!parsed) return invalidArgs(kind);
|
|
951
|
+
return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData));
|
|
952
|
+
}
|
|
953
|
+
return async (args) => {
|
|
954
|
+
const kind = str(args.kind);
|
|
955
|
+
if (!kind) return invalidArgs("<missing>");
|
|
956
|
+
if (kind === "save") return saveKind(args);
|
|
957
|
+
if (kind === "updateBeat" || kind === "updateScript") return updateKind(kind, args);
|
|
958
|
+
if (PROBE_KINDS.has(kind)) return probeKind(kind, args);
|
|
959
|
+
if (GENERATE_KINDS.has(kind)) return generateKind(kind, args);
|
|
960
|
+
if (UPLOAD_KINDS.has(kind)) return uploadKind(kind, args);
|
|
961
|
+
if (kind === "pendingGenerations") {
|
|
962
|
+
const filePath = str(args.filePath);
|
|
963
|
+
if (!filePath) return invalidArgs(kind);
|
|
964
|
+
return {
|
|
965
|
+
ok: true,
|
|
966
|
+
pending: ops.pendingGenerations(filePath)
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
ok: false,
|
|
971
|
+
code: "bad_request",
|
|
972
|
+
error: `unknown mulmoScript dispatch kind "${kind}"`
|
|
973
|
+
};
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
//#endregion
|
|
977
|
+
exports.GENERATION_EVENT = require_contract.GENERATION_EVENT;
|
|
978
|
+
exports.PDF_MODE = PDF_MODE;
|
|
979
|
+
exports.PDF_SIZE = PDF_SIZE;
|
|
980
|
+
exports.buildBeatIdIndex = buildBeatIdIndex;
|
|
981
|
+
exports.buildContext = buildContext;
|
|
982
|
+
exports.composeMulmoErrorMessage = composeMulmoErrorMessage;
|
|
983
|
+
exports.createMulmoScriptDispatchHandler = createMulmoScriptDispatchHandler;
|
|
984
|
+
exports.createMulmoScriptServerOps = createMulmoScriptServerOps;
|
|
985
|
+
exports.describeMulmoCause = describeMulmoCause;
|
|
986
|
+
exports.enableGraphAIErrorCapture = enableGraphAIErrorCapture;
|
|
987
|
+
exports.executeMulmoScriptSave = require_plugin.executeMulmoScriptSave;
|
|
988
|
+
exports.executeUpdateBeat = require_plugin.executeUpdateBeat;
|
|
989
|
+
exports.executeUpdateScript = require_plugin.executeUpdateScript;
|
|
990
|
+
exports.withMulmoErrorCapture = withMulmoErrorCapture;
|
|
991
|
+
|
|
992
|
+
//# sourceMappingURL=server.cjs.map
|