seereelcli 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/README.md +64 -0
- package/bin/reelyai.js +1334 -0
- package/package.json +21 -0
- package/skills/reelyai-cli/SKILL.md +206 -0
package/bin/reelyai.js
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_URL =
|
|
9
|
+
process.env.SEEREEL_AGENT_BASE_URL ||
|
|
10
|
+
process.env.REELYAI_AGENT_BASE_URL ||
|
|
11
|
+
process.env.CINEMA_AGENT_BASE_URL ||
|
|
12
|
+
"https://seereel.studio";
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = process.env.SEEREEL_CLI_HOME || process.env.REELYAI_CLI_HOME || path.join(os.homedir(), ".seereel");
|
|
15
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
16
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const REELYAI_CLI_SKILL = path.join(PACKAGE_ROOT, "skills", "reelyai-cli", "SKILL.md");
|
|
18
|
+
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
const HELP = `
|
|
22
|
+
SeeReel CLI
|
|
23
|
+
|
|
24
|
+
Create visible SeeReel canvas workflows from natural language, then let a human
|
|
25
|
+
review or take over in the web app.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
seereelcli workflow "a 60s cyberpunk short about ..." [options]
|
|
29
|
+
seereelcli node <get|update-prompt|generate|poll|tailframe|review|repair> --id <nodeId> [options]
|
|
30
|
+
seereelcli publish-storyboards --session <sessionId|latest>
|
|
31
|
+
seereelcli final-review --session <sessionId|latest> [--repair]
|
|
32
|
+
seereelcli render --session <sessionId> [options]
|
|
33
|
+
seereelcli stitch --session <sessionId> [options]
|
|
34
|
+
seereelcli download --session <sessionId|latest> --output ./final.mp4
|
|
35
|
+
seereelcli handoff --session <sessionId|latest> [--open]
|
|
36
|
+
seereelcli skill <install|print|path> [options]
|
|
37
|
+
seereelcli status [options]
|
|
38
|
+
seereelcli configure [options]
|
|
39
|
+
seereelcli open --session <sessionId> [options]
|
|
40
|
+
|
|
41
|
+
Aliases:
|
|
42
|
+
workflow: new, create, plan
|
|
43
|
+
|
|
44
|
+
Global options:
|
|
45
|
+
--base-url <url> SeeReel server. Default: env or https://seereel.studio
|
|
46
|
+
--access-token <token> Shared deployment token, also read from SEEREEL_ACCESS_TOKEN
|
|
47
|
+
--agent-plan-token <token> Browser-scoped Agent Plan key for model generation
|
|
48
|
+
--json Print machine-readable JSON
|
|
49
|
+
--jsonl Print newline-delimited progress events; final result is a complete event
|
|
50
|
+
--progress Print human-readable progress events to stderr
|
|
51
|
+
|
|
52
|
+
workflow options:
|
|
53
|
+
--title <title> Session title
|
|
54
|
+
--duration <sec> Target duration. Default: 60
|
|
55
|
+
--shots <count> Shot count. Default: ceil(duration / 15)
|
|
56
|
+
--style <text> Visual style
|
|
57
|
+
--language <zh|en> Session UI/script language. Default: zh
|
|
58
|
+
--no-script Skip /script/generate
|
|
59
|
+
--no-storyboard Skip /storyboard
|
|
60
|
+
--render Continue into shot generation after storyboard
|
|
61
|
+
--stitch Stitch after render, or after storyboard if shots are ready
|
|
62
|
+
--stitch-partial Stitch ready shots even when some shots failed or were skipped
|
|
63
|
+
--open Open the created session in the browser
|
|
64
|
+
|
|
65
|
+
render options:
|
|
66
|
+
--session <sessionId|latest> Session to render. Default: latest
|
|
67
|
+
--mode <missing|all> Workflow plan mode. Default: missing
|
|
68
|
+
--max-parallel-shots <n> Max parallel independent shots. Default: 1
|
|
69
|
+
--stitch Stitch after shots are ready
|
|
70
|
+
--stitch-partial Stitch ready shots even when some shots failed or were skipped
|
|
71
|
+
--repair-policy <none|safe-retry>
|
|
72
|
+
Retry policy failures with a safer prompt. Default: none
|
|
73
|
+
--max-attempts <n> Max attempts per shot when --repair-policy safe-retry is set. Default: 1
|
|
74
|
+
|
|
75
|
+
status options:
|
|
76
|
+
--session <sessionId|latest> Show one session. Use with --deep for shot/render details
|
|
77
|
+
--deep Include shots, renders, errors, stitch state, and download URL
|
|
78
|
+
|
|
79
|
+
download options:
|
|
80
|
+
--session <sessionId|latest> Session to download. Default: latest
|
|
81
|
+
--output <path> Local output file. Default: ./seereel-<sessionId>.mp4
|
|
82
|
+
|
|
83
|
+
handoff options:
|
|
84
|
+
--session <sessionId|latest> Session to transfer to the current browser user. Default: latest
|
|
85
|
+
--open Open the one-time handoff link in the browser
|
|
86
|
+
|
|
87
|
+
node options:
|
|
88
|
+
--id <nodeId> Shot, asset, or session id. Also accepts --shot / --asset
|
|
89
|
+
--prompt <text> New prompt for update-prompt
|
|
90
|
+
--title <text> Optional shot title for update-prompt
|
|
91
|
+
--duration <sec> Optional shot duration for update-prompt
|
|
92
|
+
--wait Poll after starting shot generation
|
|
93
|
+
--publish-tos Publish a shot tailframe to TOS for Seedance references
|
|
94
|
+
--canvas-node Save tailframe as a session-scoped visible canvas node
|
|
95
|
+
--frame-count <n> VLM review frame count
|
|
96
|
+
--model <model> Asset image model for asset generate
|
|
97
|
+
|
|
98
|
+
skill options:
|
|
99
|
+
--agent <all|codex,claude,cursor,agents>
|
|
100
|
+
Install bundled reelyai-cli skill. Default: all
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
npm install -g seereelcli
|
|
104
|
+
seereelcli skill install --agent all
|
|
105
|
+
seereelcli configure --base-url https://seereel.studio --access-token "$SEEREEL_ACCESS_TOKEN"
|
|
106
|
+
seereelcli workflow "一个失眠导演在午夜便利店遇见未来的自己" --duration 60 --style "neo-noir, rain"
|
|
107
|
+
seereelcli node update-prompt --id shot_xxx --prompt "new Seedance prompt"
|
|
108
|
+
seereelcli node tailframe --id shot_xxx --publish-tos --canvas-node
|
|
109
|
+
seereelcli node review --id shot_xxx --frame-count 8
|
|
110
|
+
seereelcli render --session latest --stitch --progress
|
|
111
|
+
seereelcli status --session latest --deep --json
|
|
112
|
+
seereelcli download --session latest --output ./final.mp4
|
|
113
|
+
seereelcli handoff --session latest --open
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
class CliError extends Error {
|
|
117
|
+
constructor(message, code = 1) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = "CliError";
|
|
120
|
+
this.code = code;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createReporter(options) {
|
|
125
|
+
const jsonl = Boolean(options.jsonl);
|
|
126
|
+
const progress = Boolean(options.progress);
|
|
127
|
+
return {
|
|
128
|
+
event(event, payload = {}) {
|
|
129
|
+
if (!jsonl && !progress) return;
|
|
130
|
+
const line = { event, at: new Date().toISOString(), ...payload };
|
|
131
|
+
if (jsonl) console.log(JSON.stringify(line));
|
|
132
|
+
if (progress) console.error(formatProgressLine(line));
|
|
133
|
+
},
|
|
134
|
+
complete(result) {
|
|
135
|
+
if (jsonl) this.event("complete", { result });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatProgressLine(event) {
|
|
141
|
+
const parts = [event.event];
|
|
142
|
+
if (event.sessionId) parts.push(`session=${event.sessionId}`);
|
|
143
|
+
if (event.shotId) parts.push(`shot=${event.shotId}`);
|
|
144
|
+
if (event.index !== undefined) parts.push(`index=${event.index}`);
|
|
145
|
+
if (event.status) parts.push(`status=${event.status}`);
|
|
146
|
+
if (event.taskId) parts.push(`task=${event.taskId}`);
|
|
147
|
+
if (event.attempt !== undefined) parts.push(`attempt=${event.attempt}`);
|
|
148
|
+
if (event.reason) parts.push(`reason=${event.reason}`);
|
|
149
|
+
return `[reelyai] ${parts.join(" ")}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseArgs(argv) {
|
|
153
|
+
const args = [...argv];
|
|
154
|
+
const command = args[0]?.startsWith("-") ? "help" : args.shift() || "help";
|
|
155
|
+
const positionals = [];
|
|
156
|
+
const options = {};
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
159
|
+
const arg = args[i];
|
|
160
|
+
if (arg === "--") {
|
|
161
|
+
positionals.push(...args.slice(i + 1));
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (!arg.startsWith("--")) {
|
|
165
|
+
positionals.push(arg);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (arg.startsWith("--no-")) {
|
|
169
|
+
options[toCamel(arg.slice(5))] = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const eq = arg.indexOf("=");
|
|
173
|
+
if (eq >= 0) {
|
|
174
|
+
options[toCamel(arg.slice(2, eq))] = arg.slice(eq + 1);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const key = toCamel(arg.slice(2));
|
|
178
|
+
const next = args[i + 1];
|
|
179
|
+
if (next && !next.startsWith("--")) {
|
|
180
|
+
options[key] = next;
|
|
181
|
+
i += 1;
|
|
182
|
+
} else {
|
|
183
|
+
options[key] = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { command, positionals, options };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toCamel(value) {
|
|
190
|
+
return value.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function readConfig() {
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(await readFile(CONFIG_FILE, "utf8"));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error?.code === "ENOENT") return {};
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function writeConfig(config) {
|
|
203
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
204
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveRuntime(config, options) {
|
|
208
|
+
const baseUrl = normalizeBaseUrl(
|
|
209
|
+
String(options.baseUrl || process.env.SEEREEL_AGENT_BASE_URL || process.env.REELYAI_AGENT_BASE_URL || process.env.CINEMA_AGENT_BASE_URL || config.baseUrl || DEFAULT_BASE_URL)
|
|
210
|
+
);
|
|
211
|
+
return {
|
|
212
|
+
baseUrl,
|
|
213
|
+
accessToken:
|
|
214
|
+
stringOption(options.accessToken) ||
|
|
215
|
+
process.env.SEEREEL_ACCESS_TOKEN ||
|
|
216
|
+
process.env.REELYAI_ACCESS_TOKEN ||
|
|
217
|
+
process.env.SEEREEL_CLI_ACCESS_TOKEN ||
|
|
218
|
+
process.env.REELYAI_CLI_ACCESS_TOKEN ||
|
|
219
|
+
config.accessToken ||
|
|
220
|
+
"",
|
|
221
|
+
agentPlanToken:
|
|
222
|
+
stringOption(options.agentPlanToken) ||
|
|
223
|
+
process.env.SEEREEL_AGENT_PLAN_TOKEN ||
|
|
224
|
+
process.env.REELYAI_AGENT_PLAN_TOKEN ||
|
|
225
|
+
process.env.ARK_AGENT_PLAN_KEY ||
|
|
226
|
+
config.agentPlanToken ||
|
|
227
|
+
"",
|
|
228
|
+
cookies: config.cookies || {}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function stringOption(value) {
|
|
233
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeBaseUrl(value) {
|
|
237
|
+
const url = value.trim();
|
|
238
|
+
if (!/^https?:\/\//i.test(url)) throw new CliError(`Invalid --base-url: ${value}`);
|
|
239
|
+
return url.replace(/\/+$/, "");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function originFor(baseUrl) {
|
|
243
|
+
return new URL(baseUrl).origin;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function cookieHeader(runtime) {
|
|
247
|
+
const jar = runtime.cookies?.[originFor(runtime.baseUrl)] || {};
|
|
248
|
+
return Object.entries(jar)
|
|
249
|
+
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
250
|
+
.join("; ");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function rememberCookies(runtime, headers) {
|
|
254
|
+
const setCookies =
|
|
255
|
+
typeof headers.getSetCookie === "function"
|
|
256
|
+
? headers.getSetCookie()
|
|
257
|
+
: splitSetCookie(headers.get("set-cookie") || "");
|
|
258
|
+
if (!setCookies.length) return false;
|
|
259
|
+
|
|
260
|
+
const origin = originFor(runtime.baseUrl);
|
|
261
|
+
runtime.cookies ||= {};
|
|
262
|
+
runtime.cookies[origin] ||= {};
|
|
263
|
+
for (const line of setCookies) {
|
|
264
|
+
const [pair] = line.split(";");
|
|
265
|
+
const index = pair.indexOf("=");
|
|
266
|
+
if (index <= 0) continue;
|
|
267
|
+
const name = pair.slice(0, index).trim();
|
|
268
|
+
const value = decodeURIComponent(pair.slice(index + 1).trim());
|
|
269
|
+
if (value) runtime.cookies[origin][name] = value;
|
|
270
|
+
else delete runtime.cookies[origin][name];
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function splitSetCookie(value) {
|
|
276
|
+
if (!value) return [];
|
|
277
|
+
return value.split(/,(?=[^;,]+=)/).map((item) => item.trim()).filter(Boolean);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function api(runtime, route, init = {}) {
|
|
281
|
+
const headers = {
|
|
282
|
+
Accept: "application/json",
|
|
283
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
284
|
+
...(runtime.accessToken ? { "x-seereel-access": runtime.accessToken, "x-reelyai-access": runtime.accessToken } : {}),
|
|
285
|
+
...(cookieHeader(runtime) ? { Cookie: cookieHeader(runtime) } : {}),
|
|
286
|
+
...(init.headers || {})
|
|
287
|
+
};
|
|
288
|
+
const res = await fetch(`${runtime.baseUrl}${route}`, { ...init, headers });
|
|
289
|
+
const changedCookies = rememberCookies(runtime, res.headers);
|
|
290
|
+
if (changedCookies) {
|
|
291
|
+
const config = await readConfig();
|
|
292
|
+
config.cookies = runtime.cookies;
|
|
293
|
+
await writeConfig(config);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const text = await res.text();
|
|
297
|
+
let body = text;
|
|
298
|
+
try {
|
|
299
|
+
body = text ? JSON.parse(text) : undefined;
|
|
300
|
+
} catch {
|
|
301
|
+
// Keep plain-text body.
|
|
302
|
+
}
|
|
303
|
+
if (!res.ok) {
|
|
304
|
+
const message = body?.error || body?.message || text || `${res.status} ${res.statusText}`;
|
|
305
|
+
if (body?.code === "access_token_required") {
|
|
306
|
+
throw new CliError(`${message}. Pass --access-token or set SEEREEL_ACCESS_TOKEN.`);
|
|
307
|
+
}
|
|
308
|
+
throw new CliError(`${route} failed: ${message}`);
|
|
309
|
+
}
|
|
310
|
+
return body;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function downloadToFile(runtime, route, outputPath) {
|
|
314
|
+
const headers = {
|
|
315
|
+
...(runtime.accessToken ? { "x-seereel-access": runtime.accessToken, "x-reelyai-access": runtime.accessToken } : {}),
|
|
316
|
+
...(cookieHeader(runtime) ? { Cookie: cookieHeader(runtime) } : {})
|
|
317
|
+
};
|
|
318
|
+
const res = await fetch(`${runtime.baseUrl}${route}`, { headers });
|
|
319
|
+
const changedCookies = rememberCookies(runtime, res.headers);
|
|
320
|
+
if (changedCookies) {
|
|
321
|
+
const config = await readConfig();
|
|
322
|
+
config.cookies = runtime.cookies;
|
|
323
|
+
await writeConfig(config);
|
|
324
|
+
}
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
const text = await res.text();
|
|
327
|
+
throw new CliError(`${route} download failed: ${text || `${res.status} ${res.statusText}`}`);
|
|
328
|
+
}
|
|
329
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
330
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
331
|
+
await writeFile(outputPath, bytes);
|
|
332
|
+
return { bytes: bytes.length };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function ensureAgentPlan(runtime, options = {}) {
|
|
336
|
+
const status = await api(runtime, "/api/credentials/agent-plan");
|
|
337
|
+
if (status?.configured) return status;
|
|
338
|
+
if (runtime.agentPlanToken) {
|
|
339
|
+
const configured = await api(runtime, "/api/credentials/agent-plan", {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify({ apiKey: runtime.agentPlanToken })
|
|
342
|
+
});
|
|
343
|
+
options.reporter?.event("agent_plan_configured", { fingerprint: configured?.fingerprint });
|
|
344
|
+
return configured;
|
|
345
|
+
}
|
|
346
|
+
if (options.required) {
|
|
347
|
+
throw new CliError(
|
|
348
|
+
"Agent Plan token is not configured for this CLI/browser scope. Run `seereelcli configure --agent-plan-token \"<AGENT_PLAN_API_KEY>\"`, or set SEEREEL_AGENT_PLAN_TOKEN / ARK_AGENT_PLAN_KEY before render/review commands."
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return status;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sessionUrl(baseUrl, sessionId) {
|
|
355
|
+
return `${baseUrl}/#/s/${encodeURIComponent(sessionId)}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function downloadUrl(baseUrl, sessionId) {
|
|
359
|
+
return `${baseUrl}/api/sessions/${encodeURIComponent(sessionId)}/download`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function createSessionHandoff(runtime, sessionId) {
|
|
363
|
+
return api(runtime, `/api/sessions/${encodeURIComponent(sessionId)}/handoff`, { method: "POST" });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function clampInt(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
367
|
+
const n = Number.parseInt(String(value ?? ""), 10);
|
|
368
|
+
if (!Number.isFinite(n)) return fallback;
|
|
369
|
+
return Math.min(max, Math.max(min, n));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function inferTitle(prompt, explicit) {
|
|
373
|
+
if (explicit) return explicit;
|
|
374
|
+
const compact = prompt.replace(/\s+/g, " ").trim();
|
|
375
|
+
return compact.slice(0, 28) || "SeeReel Workflow";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function readPrompt(positionals) {
|
|
379
|
+
const prompt = positionals.join(" ").trim();
|
|
380
|
+
if (prompt) return prompt;
|
|
381
|
+
if (!process.stdin.isTTY) {
|
|
382
|
+
const chunks = [];
|
|
383
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
384
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
385
|
+
}
|
|
386
|
+
throw new CliError("Missing natural-language prompt. Example: seereelcli workflow \"a 60s short...\"");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function summarizeShots(shots = []) {
|
|
390
|
+
return shots
|
|
391
|
+
.slice()
|
|
392
|
+
.sort((a, b) => (a.index || 0) - (b.index || 0))
|
|
393
|
+
.map((shot) => ({
|
|
394
|
+
index: shot.index,
|
|
395
|
+
id: shot.id,
|
|
396
|
+
title: shot.title,
|
|
397
|
+
durationSec: shot.durationSec,
|
|
398
|
+
status: shot.status,
|
|
399
|
+
videoUrl: shot.videoUrl
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function summarizeAsset(asset) {
|
|
404
|
+
if (!asset) return undefined;
|
|
405
|
+
return {
|
|
406
|
+
id: asset.id,
|
|
407
|
+
name: asset.name,
|
|
408
|
+
type: asset.type,
|
|
409
|
+
mediaKind: asset.mediaKind,
|
|
410
|
+
mediaUrl: asset.mediaUrl,
|
|
411
|
+
imageUrl: asset.imageUrl,
|
|
412
|
+
ownerSessionId: asset.ownerSessionId,
|
|
413
|
+
ownerShotId: asset.ownerShotId,
|
|
414
|
+
imageReviewStatus: asset.imageReviewStatus
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function summarizeShot(shot) {
|
|
419
|
+
if (!shot) return undefined;
|
|
420
|
+
return {
|
|
421
|
+
id: shot.id,
|
|
422
|
+
sessionId: shot.sessionId,
|
|
423
|
+
index: shot.index,
|
|
424
|
+
title: shot.title,
|
|
425
|
+
durationSec: shot.durationSec,
|
|
426
|
+
status: shot.status,
|
|
427
|
+
videoUrl: shot.videoUrl,
|
|
428
|
+
firstFrameAssetId: shot.firstFrameAssetId,
|
|
429
|
+
lastFrameAssetId: shot.lastFrameAssetId,
|
|
430
|
+
usePreviousShotClip: shot.usePreviousShotClip,
|
|
431
|
+
referenceVideoAssetId: shot.referenceVideoAssetId,
|
|
432
|
+
videoReviewStatus: shot.videoReviewStatus,
|
|
433
|
+
rawPrompt: shot.rawPrompt,
|
|
434
|
+
prompt: shot.prompt
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function summarizeRender(render) {
|
|
439
|
+
if (!render) return undefined;
|
|
440
|
+
return {
|
|
441
|
+
id: render.id,
|
|
442
|
+
status: render.status,
|
|
443
|
+
seedancePhase: render.seedancePhase,
|
|
444
|
+
generationTaskId: render.generationTaskId,
|
|
445
|
+
taskAgeSec: ageSec(render.generationStartedAt),
|
|
446
|
+
durationSec: render.durationSec,
|
|
447
|
+
videoUrl: render.videoUrl,
|
|
448
|
+
remoteVideoUrl: render.remoteVideoUrl,
|
|
449
|
+
error: render.error,
|
|
450
|
+
model: render.model,
|
|
451
|
+
createdAt: render.createdAt,
|
|
452
|
+
generationStartedAt: render.generationStartedAt,
|
|
453
|
+
videoGeneratedAt: render.videoGeneratedAt
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function summarizeDeepShot(shot) {
|
|
458
|
+
const selectedRender = Array.isArray(shot.renders) ? shot.renders[0] : undefined;
|
|
459
|
+
return {
|
|
460
|
+
...summarizeShot(shot),
|
|
461
|
+
seedancePhase: shot.seedancePhase,
|
|
462
|
+
generationTaskId: shot.generationTaskId || selectedRender?.generationTaskId,
|
|
463
|
+
taskAgeSec: ageSec(shot.generationStartedAt || selectedRender?.generationStartedAt),
|
|
464
|
+
error: shot.error || selectedRender?.error,
|
|
465
|
+
selectedRenderId: selectedRender?.id,
|
|
466
|
+
renderCount: Array.isArray(shot.renders) ? shot.renders.length : 0,
|
|
467
|
+
renders: (shot.renders || []).map(summarizeRender)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function summarizeDeepSession(runtime, session, state) {
|
|
472
|
+
const shots = (state.shots || [])
|
|
473
|
+
.filter((shot) => shot.sessionId === session.id)
|
|
474
|
+
.sort((a, b) => (a.index || 0) - (b.index || 0));
|
|
475
|
+
const readyShots = shots.filter((shot) => Boolean(shot.videoUrl));
|
|
476
|
+
const failedShots = shots.filter((shot) => shot.status === "error" || shot.status === "cancelled" || !shot.videoUrl);
|
|
477
|
+
return {
|
|
478
|
+
id: session.id,
|
|
479
|
+
title: session.title,
|
|
480
|
+
webUrl: sessionUrl(runtime.baseUrl, session.id),
|
|
481
|
+
finalVideoUrl: session.finalVideoUrl,
|
|
482
|
+
downloadUrl: session.finalVideoUrl ? downloadUrl(runtime.baseUrl, session.id) : undefined,
|
|
483
|
+
stitchStatus: session.stitchStatus,
|
|
484
|
+
stitchProgress: session.stitchProgress,
|
|
485
|
+
stitchError: session.stitchError,
|
|
486
|
+
stitchShotIds: session.stitchShotIds,
|
|
487
|
+
finalVideoReviewStatus: session.finalVideoReviewStatus,
|
|
488
|
+
finalVideoReview: session.finalVideoReview,
|
|
489
|
+
readyShotCount: readyShots.length,
|
|
490
|
+
failedShotCount: failedShots.length,
|
|
491
|
+
skippedShots: summarizeShots(failedShots),
|
|
492
|
+
stitchJobs: session.stitchJobs || [],
|
|
493
|
+
shots: shots.map(summarizeDeepShot)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function ageSec(value) {
|
|
498
|
+
if (!value) return undefined;
|
|
499
|
+
const time = Date.parse(value);
|
|
500
|
+
if (!Number.isFinite(time)) return undefined;
|
|
501
|
+
return Math.max(0, Math.round((Date.now() - time) / 1000));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function commandWorkflow(runtime, positionals, options) {
|
|
505
|
+
await api(runtime, "/api/healthz");
|
|
506
|
+
const agentPlan = await ensureAgentPlan(runtime, { required: Boolean(options.render), reporter: options.reporter });
|
|
507
|
+
const prompt = await readPrompt(positionals);
|
|
508
|
+
const duration = clampInt(options.duration, 60, { min: 1, max: 3600 });
|
|
509
|
+
const shotCount = clampInt(options.shots, Math.ceil(duration / 15), { min: 1, max: 120 });
|
|
510
|
+
const title = inferTitle(prompt, stringOption(options.title));
|
|
511
|
+
const style = stringOption(options.style) || "cinematic short drama, coherent multi-shot continuity";
|
|
512
|
+
const language = options.language === "en" ? "en" : "zh";
|
|
513
|
+
|
|
514
|
+
const session = await api(runtime, "/api/sessions", {
|
|
515
|
+
method: "POST",
|
|
516
|
+
body: JSON.stringify({
|
|
517
|
+
title,
|
|
518
|
+
logline: prompt,
|
|
519
|
+
style,
|
|
520
|
+
language,
|
|
521
|
+
targetDurationSec: duration,
|
|
522
|
+
shotCount
|
|
523
|
+
})
|
|
524
|
+
});
|
|
525
|
+
options.reporter?.event("session_created", { sessionId: session.id, title });
|
|
526
|
+
|
|
527
|
+
let currentSession = session;
|
|
528
|
+
let storyboard;
|
|
529
|
+
if (options.script !== false) {
|
|
530
|
+
options.reporter?.event("script_generating", { sessionId: session.id });
|
|
531
|
+
currentSession = await api(runtime, `/api/sessions/${session.id}/script/generate`, { method: "POST" });
|
|
532
|
+
options.reporter?.event("script_ready", { sessionId: session.id });
|
|
533
|
+
}
|
|
534
|
+
if (options.storyboard !== false) {
|
|
535
|
+
options.reporter?.event("storyboard_generating", { sessionId: session.id });
|
|
536
|
+
storyboard = await api(runtime, `/api/sessions/${session.id}/storyboard`, { method: "POST" });
|
|
537
|
+
currentSession = storyboard.session || currentSession;
|
|
538
|
+
options.reporter?.event("storyboard_ready", { sessionId: session.id, shotCount: (storyboard?.shots || currentSession.shots || []).length });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let renderResult;
|
|
542
|
+
if (options.render) {
|
|
543
|
+
renderResult = await renderSession(runtime, session.id, {
|
|
544
|
+
mode: "missing",
|
|
545
|
+
maxParallelShots: clampInt(options.maxParallelShots, 1, { min: 1, max: 8 }),
|
|
546
|
+
stitch: Boolean(options.stitch),
|
|
547
|
+
stitchPartial: Boolean(options.stitchPartial),
|
|
548
|
+
repairPolicy: normalizeRepairPolicy(options.repairPolicy),
|
|
549
|
+
maxAttempts: clampInt(options.maxAttempts, 1, { min: 1, max: 5 }),
|
|
550
|
+
timeoutMs: clampInt(options.timeoutMs, DEFAULT_TIMEOUT_MS, { min: 10_000 }),
|
|
551
|
+
pollIntervalMs: clampInt(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS, { min: 1000 }),
|
|
552
|
+
reporter: options.reporter
|
|
553
|
+
});
|
|
554
|
+
} else if (options.stitch) {
|
|
555
|
+
renderResult = { stitched: await stitchSession(runtime, session.id, options) };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const handoff = await createSessionHandoff(runtime, session.id);
|
|
559
|
+
const result = {
|
|
560
|
+
baseUrl: runtime.baseUrl,
|
|
561
|
+
sessionId: session.id,
|
|
562
|
+
title: currentSession.title || title,
|
|
563
|
+
webUrl: sessionUrl(runtime.baseUrl, session.id),
|
|
564
|
+
webUrlVisibleInBrowser: false,
|
|
565
|
+
handoffUrl: handoff.handoffUrl,
|
|
566
|
+
handoffExpiresAt: handoff.handoffExpiresAt,
|
|
567
|
+
agentPlan,
|
|
568
|
+
story: currentSession.story,
|
|
569
|
+
shots: summarizeShots(storyboard?.shots || currentSession.shots || session.shots || []),
|
|
570
|
+
render: renderResult
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (options.open) openUrl(result.handoffUrl || result.webUrl);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function commandRender(runtime, options) {
|
|
578
|
+
await api(runtime, "/api/healthz");
|
|
579
|
+
await ensureAgentPlan(runtime, { required: true, reporter: options.reporter });
|
|
580
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
581
|
+
return renderSession(runtime, sessionId, {
|
|
582
|
+
mode: options.mode === "all" ? "all" : "missing",
|
|
583
|
+
maxParallelShots: clampInt(options.maxParallelShots, 1, { min: 1, max: 8 }),
|
|
584
|
+
stitch: Boolean(options.stitch),
|
|
585
|
+
stitchPartial: Boolean(options.stitchPartial),
|
|
586
|
+
repairPolicy: normalizeRepairPolicy(options.repairPolicy),
|
|
587
|
+
maxAttempts: clampInt(options.maxAttempts, 1, { min: 1, max: 5 }),
|
|
588
|
+
timeoutMs: clampInt(options.timeoutMs, DEFAULT_TIMEOUT_MS, { min: 10_000 }),
|
|
589
|
+
pollIntervalMs: clampInt(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS, { min: 1000 }),
|
|
590
|
+
reporter: options.reporter
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function commandNode(runtime, positionals, options, forcedKind) {
|
|
595
|
+
await api(runtime, "/api/healthz");
|
|
596
|
+
const action = positionals[0] || "get";
|
|
597
|
+
const id = resolveNodeArg(positionals, options);
|
|
598
|
+
const node = await resolveNode(runtime, id, forcedKind);
|
|
599
|
+
|
|
600
|
+
if (action === "get" || action === "show") {
|
|
601
|
+
return nodeResult(runtime, node, { action });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (action === "update-prompt" || action === "prompt" || action === "update") {
|
|
605
|
+
const prompt = stringOption(options.prompt) || (positionals.length > 2 ? positionals.slice(2).join(" ").trim() : "");
|
|
606
|
+
if (!prompt) throw new CliError("Missing --prompt for node update-prompt.");
|
|
607
|
+
if (node.kind === "shot") {
|
|
608
|
+
const patch = { rawPrompt: prompt, prompt };
|
|
609
|
+
if (options.title) patch.title = String(options.title);
|
|
610
|
+
if (options.duration) patch.durationSec = clampInt(options.duration, node.value.durationSec || 15, { min: 1, max: 15 });
|
|
611
|
+
const updated = await api(runtime, `/api/shots/${node.id}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
612
|
+
return nodeResult(runtime, { kind: "shot", id: updated.id, value: updated }, { action: "update-prompt" });
|
|
613
|
+
}
|
|
614
|
+
if (node.kind === "asset") {
|
|
615
|
+
const updated = await api(runtime, `/api/assets/${node.id}`, {
|
|
616
|
+
method: "PATCH",
|
|
617
|
+
body: JSON.stringify({ prompt, description: stringOption(options.description) || node.value.description })
|
|
618
|
+
});
|
|
619
|
+
return nodeResult(runtime, { kind: "asset", id: updated.id, value: updated }, { action: "update-prompt" });
|
|
620
|
+
}
|
|
621
|
+
throw new CliError("update-prompt supports shot and asset nodes.");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (action === "generate") {
|
|
625
|
+
await ensureAgentPlan(runtime, { required: true, reporter: options.reporter });
|
|
626
|
+
if (node.kind === "shot") {
|
|
627
|
+
options.reporter?.event("shot_submitted", { shotId: node.id, index: node.value.index, attempt: 1 });
|
|
628
|
+
const generated = await api(runtime, `/api/shots/${node.id}/generate`, { method: "POST" });
|
|
629
|
+
reportShotTask(options.reporter, "task_id", generated);
|
|
630
|
+
if (!options.wait) return nodeResult(runtime, { kind: "shot", id: generated.id, value: generated }, { action: "generate" });
|
|
631
|
+
const rendered = await waitForShot(runtime, node.id, {
|
|
632
|
+
timeoutMs: clampInt(options.timeoutMs, DEFAULT_TIMEOUT_MS, { min: 10_000 }),
|
|
633
|
+
pollIntervalMs: clampInt(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS, { min: 1000 }),
|
|
634
|
+
reporter: options.reporter
|
|
635
|
+
});
|
|
636
|
+
return { action: "generate", kind: "shot", webUrl: sessionUrl(runtime.baseUrl, rendered.sessionId || node.value.sessionId), shot: summarizeShot(rendered) };
|
|
637
|
+
}
|
|
638
|
+
if (node.kind === "asset") {
|
|
639
|
+
const generated = await api(runtime, `/api/assets/${node.id}/generate`, {
|
|
640
|
+
method: "POST",
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
model: stringOption(options.model) || undefined,
|
|
643
|
+
visionReview: options.visionReview === true ? true : undefined,
|
|
644
|
+
maxReviewAttempts: options.maxReviewAttempts ? clampInt(options.maxReviewAttempts, 1, { min: 1, max: 5 }) : undefined
|
|
645
|
+
})
|
|
646
|
+
});
|
|
647
|
+
return nodeResult(runtime, { kind: "asset", id: generated.id, value: generated }, { action: "generate" });
|
|
648
|
+
}
|
|
649
|
+
throw new CliError("generate supports shot and asset nodes.");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (action === "poll") {
|
|
653
|
+
if (node.kind !== "shot") throw new CliError("poll only supports shot nodes.");
|
|
654
|
+
const shot = await api(runtime, `/api/shots/${node.id}/poll`, { method: "POST" });
|
|
655
|
+
return nodeResult(runtime, { kind: "shot", id: shot.id, value: shot }, { action: "poll" });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (action === "tailframe") {
|
|
659
|
+
if (node.kind !== "shot") throw new CliError("tailframe only supports shot nodes with rendered video.");
|
|
660
|
+
const result = await api(runtime, `/api/shots/${node.id}/tailframe`, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
publishToTos: Boolean(options.publishTos),
|
|
664
|
+
canvasNode: Boolean(options.canvasNode)
|
|
665
|
+
})
|
|
666
|
+
});
|
|
667
|
+
return {
|
|
668
|
+
action: "tailframe",
|
|
669
|
+
kind: "shot",
|
|
670
|
+
shotId: node.id,
|
|
671
|
+
webUrl: sessionUrl(runtime.baseUrl, node.value.sessionId),
|
|
672
|
+
asset: summarizeAsset(result.asset)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (action === "review" || action === "vlm") {
|
|
677
|
+
await ensureAgentPlan(runtime, { required: true, reporter: options.reporter });
|
|
678
|
+
if (node.kind === "shot") {
|
|
679
|
+
const reviewed = await api(runtime, `/api/shots/${node.id}/review`, {
|
|
680
|
+
method: "POST",
|
|
681
|
+
body: JSON.stringify({ frameCount: clampInt(options.frameCount, 8, { min: 1, max: 32 }) })
|
|
682
|
+
});
|
|
683
|
+
return nodeResult(runtime, { kind: "shot", id: reviewed.id, value: reviewed }, { action: "review" });
|
|
684
|
+
}
|
|
685
|
+
if (node.kind === "asset") {
|
|
686
|
+
const reviewed = await api(runtime, `/api/assets/${node.id}/review`, { method: "POST" });
|
|
687
|
+
return nodeResult(runtime, { kind: "asset", id: reviewed.id, value: reviewed }, { action: "review" });
|
|
688
|
+
}
|
|
689
|
+
if (node.kind === "session") {
|
|
690
|
+
return commandFinalReview(runtime, { ...options, session: node.id });
|
|
691
|
+
}
|
|
692
|
+
throw new CliError("review supports shot, asset, and session nodes.");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (action === "repair" || action === "repair-prompts") {
|
|
696
|
+
await ensureAgentPlan(runtime, { required: true, reporter: options.reporter });
|
|
697
|
+
if (node.kind === "shot") {
|
|
698
|
+
const repaired = await api(runtime, `/api/shots/${node.id}/review/repair-prompts`, { method: "POST" });
|
|
699
|
+
return nodeResult(runtime, { kind: "shot", id: repaired.id, value: repaired }, { action: "repair" });
|
|
700
|
+
}
|
|
701
|
+
if (node.kind === "session") {
|
|
702
|
+
return commandFinalReview(runtime, { ...options, session: node.id, repair: true });
|
|
703
|
+
}
|
|
704
|
+
throw new CliError("repair supports shot and session nodes.");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
throw new CliError(`Unknown node action: ${action}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function commandPublishStoryboards(runtime, options) {
|
|
711
|
+
await api(runtime, "/api/healthz");
|
|
712
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
713
|
+
const result = await api(runtime, `/api/sessions/${sessionId}/storyboards/publish-tos`, { method: "POST" });
|
|
714
|
+
return {
|
|
715
|
+
action: "publish-storyboards",
|
|
716
|
+
sessionId,
|
|
717
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId),
|
|
718
|
+
assets: (result.assets || []).map(summarizeAsset),
|
|
719
|
+
shots: summarizeShots(result.session?.shots || [])
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function commandFinalReview(runtime, options) {
|
|
724
|
+
await api(runtime, "/api/healthz");
|
|
725
|
+
await ensureAgentPlan(runtime, { required: true, reporter: options.reporter });
|
|
726
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
727
|
+
const route = options.repair ? `/api/sessions/${sessionId}/final-review/repair-prompts` : `/api/sessions/${sessionId}/final-review`;
|
|
728
|
+
const result = await api(runtime, route, {
|
|
729
|
+
method: "POST",
|
|
730
|
+
body: JSON.stringify({
|
|
731
|
+
jobId: stringOption(options.jobId) || undefined,
|
|
732
|
+
frameCount: options.frameCount ? clampInt(options.frameCount, 10, { min: 1, max: 32 }) : undefined
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
action: options.repair ? "final-review-repair" : "final-review",
|
|
737
|
+
sessionId,
|
|
738
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId),
|
|
739
|
+
session: {
|
|
740
|
+
id: result.id,
|
|
741
|
+
title: result.title,
|
|
742
|
+
finalVideoReviewStatus: result.finalVideoReviewStatus,
|
|
743
|
+
finalVideoReview: result.finalVideoReview,
|
|
744
|
+
finalVideoReviewRepairPlan: result.finalVideoReviewRepairPlan
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function renderSession(runtime, sessionId, options) {
|
|
750
|
+
options.reporter?.event("render_plan_requested", { sessionId, mode: options.mode });
|
|
751
|
+
const plan = await api(runtime, `/api/sessions/${sessionId}/workflow/plan`, {
|
|
752
|
+
method: "POST",
|
|
753
|
+
body: JSON.stringify({ mode: options.mode, maxParallelShots: options.maxParallelShots })
|
|
754
|
+
});
|
|
755
|
+
options.reporter?.event("render_plan_ready", { sessionId, summary: plan.summary, layerCount: (plan.layers || []).length });
|
|
756
|
+
const layerResults = [];
|
|
757
|
+
for (let layerIndex = 0; layerIndex < (plan.layers || []).length; layerIndex += 1) {
|
|
758
|
+
const layer = plan.layers[layerIndex];
|
|
759
|
+
options.reporter?.event("render_layer_started", { sessionId, layerIndex, shotCount: layer.length });
|
|
760
|
+
const rendered = await Promise.all(layer.map((item) => renderShot(runtime, item, options)));
|
|
761
|
+
layerResults.push(rendered);
|
|
762
|
+
options.reporter?.event("render_layer_finished", { sessionId, layerIndex });
|
|
763
|
+
}
|
|
764
|
+
const state = await api(runtime, "/api/state");
|
|
765
|
+
const shots = state.shots?.filter((shot) => shot.sessionId === sessionId) || [];
|
|
766
|
+
const failed = shots.filter((shot) => shot.status === "error" || shot.status === "cancelled" || !shot.videoUrl);
|
|
767
|
+
const ready = shots.filter((shot) => shot.videoUrl);
|
|
768
|
+
let stitched;
|
|
769
|
+
if (options.stitch && (failed.length === 0 || options.stitchPartial) && ready.length > 0) {
|
|
770
|
+
stitched = await stitchSession(runtime, sessionId, options);
|
|
771
|
+
} else if (options.stitch && failed.length > 0) {
|
|
772
|
+
options.reporter?.event("stitch_skipped", {
|
|
773
|
+
sessionId,
|
|
774
|
+
reason: "failed_or_missing_shots",
|
|
775
|
+
skippedShotIds: failed.map((shot) => shot.id)
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
baseUrl: runtime.baseUrl,
|
|
780
|
+
sessionId,
|
|
781
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId),
|
|
782
|
+
planSummary: plan.summary,
|
|
783
|
+
layers: layerResults,
|
|
784
|
+
shots: summarizeShots(shots),
|
|
785
|
+
failed: summarizeShots(failed),
|
|
786
|
+
skippedShots: summarizeShots(failed),
|
|
787
|
+
stitched
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function renderShot(runtime, item, options) {
|
|
792
|
+
const shotId = item.shotId || item.id;
|
|
793
|
+
if (!shotId) throw new CliError(`Workflow item is missing shot id: ${JSON.stringify(item)}`);
|
|
794
|
+
const maxAttempts = options.repairPolicy === "safe-retry" ? Math.max(1, options.maxAttempts || 1) : 1;
|
|
795
|
+
let lastResult;
|
|
796
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
797
|
+
if (item.index > 1 && item.action !== "skip") {
|
|
798
|
+
await api(runtime, `/api/shots/${shotId}`, {
|
|
799
|
+
method: "PATCH",
|
|
800
|
+
body: JSON.stringify({ usePreviousShotClip: true, previousShotClipSec: 2 })
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
options.reporter?.event("shot_submitted", { shotId, index: item.index, attempt });
|
|
804
|
+
const submitted = await api(runtime, `/api/shots/${shotId}/generate`, { method: "POST" });
|
|
805
|
+
reportShotTask(options.reporter, "task_id", submitted, { attempt });
|
|
806
|
+
let rendered;
|
|
807
|
+
try {
|
|
808
|
+
rendered = await waitForShot(runtime, shotId, options);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
if (attempt >= maxAttempts || options.repairPolicy !== "safe-retry") throw error;
|
|
811
|
+
const state = await api(runtime, "/api/state");
|
|
812
|
+
const current = (state.shots || []).find((shot) => shot.id === shotId) || submitted;
|
|
813
|
+
const patch = buildSafeRetryPatch(current);
|
|
814
|
+
options.reporter?.event("retrying", {
|
|
815
|
+
shotId,
|
|
816
|
+
index: current.index || item.index,
|
|
817
|
+
attempt: attempt + 1,
|
|
818
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
819
|
+
durationSec: patch.durationSec
|
|
820
|
+
});
|
|
821
|
+
await api(runtime, `/api/shots/${shotId}/cancel`, { method: "POST" }).catch(() => undefined);
|
|
822
|
+
await api(runtime, `/api/shots/${shotId}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
lastResult = rendered;
|
|
826
|
+
if (rendered.status !== "error" && rendered.status !== "cancelled") return rendered;
|
|
827
|
+
if (attempt >= maxAttempts || !isSafeRetryableShotError(rendered.error)) return rendered;
|
|
828
|
+
const patch = buildSafeRetryPatch(rendered);
|
|
829
|
+
options.reporter?.event("retrying", {
|
|
830
|
+
shotId,
|
|
831
|
+
index: rendered.index,
|
|
832
|
+
attempt: attempt + 1,
|
|
833
|
+
reason: rendered.error,
|
|
834
|
+
durationSec: patch.durationSec
|
|
835
|
+
});
|
|
836
|
+
await api(runtime, `/api/shots/${shotId}/cancel`, { method: "POST" }).catch(() => undefined);
|
|
837
|
+
await api(runtime, `/api/shots/${shotId}`, {
|
|
838
|
+
method: "PATCH",
|
|
839
|
+
body: JSON.stringify(patch)
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return lastResult;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function waitForShot(runtime, shotId, options) {
|
|
846
|
+
const started = Date.now();
|
|
847
|
+
let snapshot;
|
|
848
|
+
while (Date.now() - started < options.timeoutMs) {
|
|
849
|
+
snapshot = await api(runtime, `/api/shots/${shotId}/poll`, { method: "POST" });
|
|
850
|
+
options.reporter?.event("poll_status", {
|
|
851
|
+
shotId,
|
|
852
|
+
index: snapshot.index,
|
|
853
|
+
status: snapshot.status,
|
|
854
|
+
phase: snapshot.seedancePhase,
|
|
855
|
+
taskId: snapshot.generationTaskId || latestRender(snapshot)?.generationTaskId,
|
|
856
|
+
elapsedSec: Math.round((Date.now() - started) / 1000)
|
|
857
|
+
});
|
|
858
|
+
if (["ready", "error", "cancelled"].includes(snapshot.status)) {
|
|
859
|
+
return {
|
|
860
|
+
id: snapshot.id,
|
|
861
|
+
sessionId: snapshot.sessionId,
|
|
862
|
+
index: snapshot.index,
|
|
863
|
+
title: snapshot.title,
|
|
864
|
+
durationSec: snapshot.durationSec,
|
|
865
|
+
status: snapshot.status,
|
|
866
|
+
videoUrl: snapshot.videoUrl,
|
|
867
|
+
error: snapshot.error || latestRender(snapshot)?.error,
|
|
868
|
+
rawPrompt: snapshot.rawPrompt,
|
|
869
|
+
prompt: snapshot.prompt
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
await sleep(options.pollIntervalMs);
|
|
873
|
+
}
|
|
874
|
+
await api(runtime, `/api/shots/${shotId}/cancel`, { method: "POST" }).catch(() => undefined);
|
|
875
|
+
throw new CliError(`Timed out waiting for shot ${shotId}`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function latestRender(shot) {
|
|
879
|
+
return Array.isArray(shot?.renders) ? shot.renders[0] : undefined;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function reportShotTask(reporter, event, shot, extra = {}) {
|
|
883
|
+
const render = latestRender(shot);
|
|
884
|
+
const taskId = shot?.generationTaskId || render?.generationTaskId;
|
|
885
|
+
if (!taskId) return;
|
|
886
|
+
reporter?.event(event, {
|
|
887
|
+
shotId: shot.id,
|
|
888
|
+
index: shot.index,
|
|
889
|
+
taskId,
|
|
890
|
+
status: shot.status,
|
|
891
|
+
...extra
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function normalizeRepairPolicy(value) {
|
|
896
|
+
const raw = stringOption(value) || "none";
|
|
897
|
+
if (raw === "none" || raw === "safe-retry") return raw;
|
|
898
|
+
throw new CliError(`Unknown --repair-policy: ${raw}. Expected none or safe-retry.`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function isSafeRetryableShotError(error) {
|
|
902
|
+
const text = String(error || "");
|
|
903
|
+
return /SensitiveContent|PolicyViolation|content.*policy|sensitive|risk|violation|审核|敏感|安全|策略/i.test(text);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function buildSafeRetryPatch(shot) {
|
|
907
|
+
const original = shot.rawPrompt || shot.prompt || "";
|
|
908
|
+
const prompt = safeRetryPrompt(original);
|
|
909
|
+
const currentDuration = Number(shot.durationSec) || 15;
|
|
910
|
+
return {
|
|
911
|
+
rawPrompt: prompt,
|
|
912
|
+
prompt,
|
|
913
|
+
durationSec: Math.max(3, Math.min(8, currentDuration > 8 ? 8 : currentDuration))
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function safeRetryPrompt(prompt) {
|
|
918
|
+
const replacements = [
|
|
919
|
+
[/台北\s*101|taipei\s*101/gi, "一座虚构现代城市地标高楼"],
|
|
920
|
+
[/纽约|new\s*york|东京|tokyo|巴黎|paris|北京|beijing|上海|shanghai|台北|taipei/gi, "虚构现代城市"],
|
|
921
|
+
[/apple|iphone|tesla|bytedance|tiktok|douyin|volcengine|openai|google|microsoft|meta|nvidia/gi, "虚构科技品牌"],
|
|
922
|
+
[/苹果|特斯拉|字节跳动|抖音|火山引擎|谷歌|微软|英伟达/gi, "虚构科技品牌"],
|
|
923
|
+
[/logo|商标|品牌标识/gi, "无可识别品牌标识"]
|
|
924
|
+
];
|
|
925
|
+
let next = prompt;
|
|
926
|
+
for (const [pattern, replacement] of replacements) next = next.replace(pattern, replacement);
|
|
927
|
+
const safety = "安全重试约束:使用虚构地点、虚构建筑和虚构品牌;不要出现真实品牌、真实地名、可识别商标、政治符号、血腥暴力、危险行为或敏感标识;保持电影感和原镜头意图。";
|
|
928
|
+
return next.includes("安全重试约束") ? next : `${next.trim()}\n${safety}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function resolveNodeArg(positionals, options) {
|
|
932
|
+
const id = stringOption(options.id) || stringOption(options.shot) || stringOption(options.asset) || stringOption(options.session) || positionals[1];
|
|
933
|
+
if (!id || id === true) throw new CliError("Missing node id. Use --id shot_xxx / --id asset_xxx / --id ses_xxx.");
|
|
934
|
+
return String(id);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function resolveNode(runtime, id, forcedKind) {
|
|
938
|
+
const state = await api(runtime, "/api/state");
|
|
939
|
+
if (forcedKind === "shot" || (!forcedKind && id.startsWith("shot_"))) {
|
|
940
|
+
const shot = (state.shots || []).find((item) => item.id === id);
|
|
941
|
+
if (!shot) throw new CliError(`Shot not found: ${id}`);
|
|
942
|
+
return { kind: "shot", id, value: shot };
|
|
943
|
+
}
|
|
944
|
+
if (forcedKind === "asset" || (!forcedKind && id.startsWith("asset_"))) {
|
|
945
|
+
const asset = (state.assets || []).find((item) => item.id === id);
|
|
946
|
+
if (!asset) throw new CliError(`Asset not found: ${id}`);
|
|
947
|
+
return { kind: "asset", id, value: asset };
|
|
948
|
+
}
|
|
949
|
+
if (forcedKind === "session" || (!forcedKind && id.startsWith("ses_"))) {
|
|
950
|
+
const session = (state.sessions || []).find((item) => item.id === id);
|
|
951
|
+
if (!session) throw new CliError(`Session not found: ${id}`);
|
|
952
|
+
return { kind: "session", id, value: session };
|
|
953
|
+
}
|
|
954
|
+
const shot = (state.shots || []).find((item) => item.id === id);
|
|
955
|
+
if (shot) return { kind: "shot", id, value: shot };
|
|
956
|
+
const asset = (state.assets || []).find((item) => item.id === id);
|
|
957
|
+
if (asset) return { kind: "asset", id, value: asset };
|
|
958
|
+
const session = (state.sessions || []).find((item) => item.id === id);
|
|
959
|
+
if (session) return { kind: "session", id, value: session };
|
|
960
|
+
throw new CliError(`Node not found: ${id}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function nodeResult(runtime, node, extra = {}) {
|
|
964
|
+
const sessionId = node.kind === "shot" ? node.value.sessionId : node.kind === "asset" ? node.value.ownerSessionId : node.value.id;
|
|
965
|
+
return {
|
|
966
|
+
...extra,
|
|
967
|
+
kind: node.kind,
|
|
968
|
+
id: node.id,
|
|
969
|
+
webUrl: sessionId ? sessionUrl(runtime.baseUrl, sessionId) : undefined,
|
|
970
|
+
shot: node.kind === "shot" ? summarizeShot(node.value) : undefined,
|
|
971
|
+
asset: node.kind === "asset" ? summarizeAsset(node.value) : undefined,
|
|
972
|
+
session: node.kind === "session" ? {
|
|
973
|
+
id: node.value.id,
|
|
974
|
+
title: node.value.title,
|
|
975
|
+
finalVideoUrl: node.value.finalVideoUrl,
|
|
976
|
+
stitchStatus: node.value.stitchStatus,
|
|
977
|
+
finalVideoReviewStatus: node.value.finalVideoReviewStatus
|
|
978
|
+
} : undefined
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function stitchSession(runtime, sessionId, options) {
|
|
983
|
+
const beforeState = await api(runtime, "/api/state");
|
|
984
|
+
const sessionShots = (beforeState.shots || []).filter((shot) => shot.sessionId === sessionId);
|
|
985
|
+
const skippedShots = sessionShots.filter((shot) => !shot.videoUrl);
|
|
986
|
+
options.reporter?.event("stitch_started", {
|
|
987
|
+
sessionId,
|
|
988
|
+
readyShotCount: sessionShots.length - skippedShots.length,
|
|
989
|
+
skippedShotIds: skippedShots.map((shot) => shot.id)
|
|
990
|
+
});
|
|
991
|
+
let session = await api(runtime, `/api/sessions/${sessionId}/stitch`, {
|
|
992
|
+
method: "POST",
|
|
993
|
+
body: JSON.stringify({ force: Boolean(options.force) })
|
|
994
|
+
});
|
|
995
|
+
const started = Date.now();
|
|
996
|
+
const timeoutMs = clampInt(options.timeoutMs, DEFAULT_TIMEOUT_MS, { min: 10_000 });
|
|
997
|
+
const pollIntervalMs = clampInt(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS, { min: 1000 });
|
|
998
|
+
while (session.stitchStatus === "running" && Date.now() - started < timeoutMs) {
|
|
999
|
+
await sleep(pollIntervalMs);
|
|
1000
|
+
session = await api(runtime, `/api/sessions/${sessionId}/stitch/poll`, { method: "POST" });
|
|
1001
|
+
options.reporter?.event("stitch_poll", {
|
|
1002
|
+
sessionId,
|
|
1003
|
+
status: session.stitchStatus,
|
|
1004
|
+
progress: session.stitchProgress
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
options.reporter?.event("stitch_ready", {
|
|
1008
|
+
sessionId,
|
|
1009
|
+
status: session.stitchStatus,
|
|
1010
|
+
finalVideoUrl: session.finalVideoUrl,
|
|
1011
|
+
downloadUrl: session.finalVideoUrl ? downloadUrl(runtime.baseUrl, sessionId) : undefined
|
|
1012
|
+
});
|
|
1013
|
+
return {
|
|
1014
|
+
sessionId,
|
|
1015
|
+
status: session.stitchStatus,
|
|
1016
|
+
progress: session.stitchProgress,
|
|
1017
|
+
error: session.stitchError,
|
|
1018
|
+
finalVideoUrl: session.finalVideoUrl,
|
|
1019
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId),
|
|
1020
|
+
downloadUrl: session.finalVideoUrl ? downloadUrl(runtime.baseUrl, sessionId) : undefined,
|
|
1021
|
+
skippedShots: summarizeShots(skippedShots)
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function resolveSessionId(runtime, value) {
|
|
1026
|
+
if (value && value !== true && value !== "latest") return String(value);
|
|
1027
|
+
const state = await api(runtime, "/api/state");
|
|
1028
|
+
const latest = state.sessions?.[0];
|
|
1029
|
+
if (!latest) throw new CliError("No sessions found.");
|
|
1030
|
+
return latest.id;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function commandStatus(runtime, options) {
|
|
1034
|
+
const health = await api(runtime, "/api/healthz");
|
|
1035
|
+
const agentPlan = await ensureAgentPlan(runtime, { reporter: options.reporter });
|
|
1036
|
+
const state = await api(runtime, "/api/state");
|
|
1037
|
+
const limit = clampInt(options.limit, 10, { min: 1, max: 100 });
|
|
1038
|
+
const sessions = (state.sessions || []).slice(0, limit);
|
|
1039
|
+
const sessionId = options.deep || options.session ? await resolveSessionIdFromState(runtime, state, options.session || "latest") : undefined;
|
|
1040
|
+
const session = sessionId ? (state.sessions || []).find((item) => item.id === sessionId) : undefined;
|
|
1041
|
+
return {
|
|
1042
|
+
action: "status",
|
|
1043
|
+
baseUrl: runtime.baseUrl,
|
|
1044
|
+
health,
|
|
1045
|
+
agentPlan,
|
|
1046
|
+
deep: Boolean(options.deep),
|
|
1047
|
+
session: session && options.deep ? summarizeDeepSession(runtime, session, state) : undefined,
|
|
1048
|
+
sessions: sessions.map((session) => ({
|
|
1049
|
+
id: session.id,
|
|
1050
|
+
title: session.title,
|
|
1051
|
+
webUrl: sessionUrl(runtime.baseUrl, session.id),
|
|
1052
|
+
finalVideoUrl: session.finalVideoUrl,
|
|
1053
|
+
downloadUrl: session.finalVideoUrl ? downloadUrl(runtime.baseUrl, session.id) : undefined,
|
|
1054
|
+
stitchStatus: session.stitchStatus,
|
|
1055
|
+
readyShotCount: (state.shots || []).filter((shot) => shot.sessionId === session.id && shot.videoUrl).length,
|
|
1056
|
+
failedShotCount: (state.shots || []).filter((shot) => shot.sessionId === session.id && (shot.status === "error" || shot.status === "cancelled")).length
|
|
1057
|
+
}))
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function commandDownload(runtime, options) {
|
|
1062
|
+
await api(runtime, "/api/healthz");
|
|
1063
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
1064
|
+
const output = path.resolve(stringOption(options.output) || `seereel-${sessionId}.mp4`);
|
|
1065
|
+
const result = await downloadToFile(runtime, `/api/sessions/${sessionId}/download`, output);
|
|
1066
|
+
return {
|
|
1067
|
+
action: "download",
|
|
1068
|
+
sessionId,
|
|
1069
|
+
output,
|
|
1070
|
+
bytes: result.bytes,
|
|
1071
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function resolveSessionIdFromState(runtime, state, value) {
|
|
1076
|
+
if (value && value !== true && value !== "latest") return String(value);
|
|
1077
|
+
const latest = state.sessions?.[0];
|
|
1078
|
+
if (!latest) return undefined;
|
|
1079
|
+
return latest.id;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function commandConfigure(config, options) {
|
|
1083
|
+
const next = { ...config };
|
|
1084
|
+
const baseUrl = flagValue(options, "baseUrl");
|
|
1085
|
+
const accessToken = flagValue(options, "accessToken");
|
|
1086
|
+
const agentPlanToken = flagValue(options, "agentPlanToken");
|
|
1087
|
+
if (baseUrl) next.baseUrl = normalizeBaseUrl(baseUrl);
|
|
1088
|
+
if (accessToken) next.accessToken = accessToken;
|
|
1089
|
+
if (agentPlanToken) next.agentPlanToken = agentPlanToken;
|
|
1090
|
+
if (options.clearAccessToken) delete next.accessToken;
|
|
1091
|
+
if (options.clearAgentPlanToken) delete next.agentPlanToken;
|
|
1092
|
+
if (options.clearCookies) delete next.cookies;
|
|
1093
|
+
|
|
1094
|
+
const changed =
|
|
1095
|
+
baseUrl ||
|
|
1096
|
+
accessToken ||
|
|
1097
|
+
agentPlanToken ||
|
|
1098
|
+
options.clearAccessToken ||
|
|
1099
|
+
options.clearAgentPlanToken ||
|
|
1100
|
+
options.clearCookies;
|
|
1101
|
+
if (changed) await writeConfig(next);
|
|
1102
|
+
|
|
1103
|
+
return {
|
|
1104
|
+
configFile: CONFIG_FILE,
|
|
1105
|
+
baseUrl: next.baseUrl || DEFAULT_BASE_URL,
|
|
1106
|
+
accessTokenConfigured: Boolean(next.accessToken),
|
|
1107
|
+
agentPlanTokenConfigured: Boolean(next.agentPlanToken),
|
|
1108
|
+
cookieOrigins: Object.keys(next.cookies || {})
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function commandSkill(positionals, options) {
|
|
1113
|
+
const action = positionals[0] || "install";
|
|
1114
|
+
const skillText = await readFile(REELYAI_CLI_SKILL, "utf8");
|
|
1115
|
+
if (action === "print" || action === "show") {
|
|
1116
|
+
return { action: "skill-print", rawText: skillText, skillPath: REELYAI_CLI_SKILL };
|
|
1117
|
+
}
|
|
1118
|
+
if (action === "path") {
|
|
1119
|
+
return { action: "skill-path", skillPath: REELYAI_CLI_SKILL };
|
|
1120
|
+
}
|
|
1121
|
+
if (action !== "install") throw new CliError(`Unknown skill action: ${action}`);
|
|
1122
|
+
|
|
1123
|
+
const targets = resolveSkillTargets(options.agent);
|
|
1124
|
+
const installed = [];
|
|
1125
|
+
for (const target of targets) {
|
|
1126
|
+
const dir = path.join(os.homedir(), target.root, "skills", "reelyai-cli");
|
|
1127
|
+
await mkdir(dir, { recursive: true });
|
|
1128
|
+
const file = path.join(dir, "SKILL.md");
|
|
1129
|
+
await writeFile(file, skillText);
|
|
1130
|
+
installed.push({ agent: target.name, file });
|
|
1131
|
+
}
|
|
1132
|
+
return { action: "skill-install", skillPath: REELYAI_CLI_SKILL, installed };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function resolveSkillTargets(value) {
|
|
1136
|
+
const all = [
|
|
1137
|
+
{ name: "codex", root: ".codex" },
|
|
1138
|
+
{ name: "claude", root: ".claude" },
|
|
1139
|
+
{ name: "cursor", root: ".cursor" },
|
|
1140
|
+
{ name: "agents", root: ".agents" }
|
|
1141
|
+
];
|
|
1142
|
+
const raw = stringOption(value) || "all";
|
|
1143
|
+
if (raw === "all") return all;
|
|
1144
|
+
const names = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1145
|
+
const selected = all.filter((target) => names.includes(target.name));
|
|
1146
|
+
const missing = names.filter((name) => !all.some((target) => target.name === name));
|
|
1147
|
+
if (missing.length) throw new CliError(`Unknown --agent target: ${missing.join(", ")}`);
|
|
1148
|
+
return selected;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function flagValue(options, key) {
|
|
1152
|
+
const value = options[key];
|
|
1153
|
+
if (value === undefined || value === false) return "";
|
|
1154
|
+
if (value === true) throw new CliError(`--${key.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`)} requires a value`);
|
|
1155
|
+
return String(value).trim();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function commandOpen(runtime, options) {
|
|
1159
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
1160
|
+
const webUrl = sessionUrl(runtime.baseUrl, sessionId);
|
|
1161
|
+
openUrl(webUrl);
|
|
1162
|
+
return { webUrl };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function commandHandoff(runtime, options) {
|
|
1166
|
+
await api(runtime, "/api/healthz");
|
|
1167
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
1168
|
+
const handoff = await createSessionHandoff(runtime, sessionId);
|
|
1169
|
+
const result = {
|
|
1170
|
+
action: "handoff",
|
|
1171
|
+
sessionId,
|
|
1172
|
+
webUrl: sessionUrl(runtime.baseUrl, sessionId),
|
|
1173
|
+
webUrlVisibleInBrowser: false,
|
|
1174
|
+
handoffUrl: handoff.handoffUrl,
|
|
1175
|
+
handoffExpiresAt: handoff.handoffExpiresAt
|
|
1176
|
+
};
|
|
1177
|
+
if (options.open) openUrl(result.handoffUrl);
|
|
1178
|
+
return result;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function openUrl(url) {
|
|
1182
|
+
const command =
|
|
1183
|
+
process.platform === "darwin" ? "open" :
|
|
1184
|
+
process.platform === "win32" ? "cmd" :
|
|
1185
|
+
"xdg-open";
|
|
1186
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1187
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
1188
|
+
child.unref();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function sleep(ms) {
|
|
1192
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function printHuman(result, command) {
|
|
1196
|
+
if (result?.rawText) {
|
|
1197
|
+
console.log(result.rawText);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (command === "configure") {
|
|
1201
|
+
console.log(`Config: ${result.configFile}`);
|
|
1202
|
+
console.log(`Base URL: ${result.baseUrl}`);
|
|
1203
|
+
console.log(`Access token: ${result.accessTokenConfigured ? "configured" : "not configured"}`);
|
|
1204
|
+
console.log(`Agent Plan token: ${result.agentPlanTokenConfigured ? "configured" : "not configured"}`);
|
|
1205
|
+
if (result.cookieOrigins.length) console.log(`Cookie origins: ${result.cookieOrigins.join(", ")}`);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (command === "status") {
|
|
1209
|
+
console.log(`SeeReel: ${result.baseUrl}`);
|
|
1210
|
+
console.log(`Health: ${result.health?.ok ? "ok" : "unknown"}`);
|
|
1211
|
+
console.log(`Agent Plan: ${result.agentPlan?.configured ? `configured (${result.agentPlan.fingerprint})` : "not configured"}`);
|
|
1212
|
+
if (result.session) {
|
|
1213
|
+
console.log(`Session: ${result.session.title || result.session.id} [${result.session.id}]`);
|
|
1214
|
+
console.log(`Web: ${result.session.webUrl}`);
|
|
1215
|
+
console.log(`Shots: ${result.session.readyShotCount} ready / ${result.session.failedShotCount} failed-or-missing`);
|
|
1216
|
+
console.log(`Stitch: ${result.session.stitchStatus || "idle"}${result.session.stitchProgress ? ` (${result.session.stitchProgress})` : ""}`);
|
|
1217
|
+
if (result.session.downloadUrl) console.log(`Download: ${result.session.downloadUrl}`);
|
|
1218
|
+
for (const shot of result.session.shots || []) {
|
|
1219
|
+
const age = shot.taskAgeSec !== undefined ? ` age=${shot.taskAgeSec}s` : "";
|
|
1220
|
+
const task = shot.generationTaskId ? ` task=${shot.generationTaskId}` : "";
|
|
1221
|
+
const error = shot.error ? ` error=${shot.error}` : "";
|
|
1222
|
+
console.log(` ${shot.index}. ${shot.title || shot.id} - ${shot.status || "draft"}${task}${age}${error}`);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
for (const session of result.sessions) {
|
|
1227
|
+
console.log(`- ${session.title || session.id} [${session.id}] ${session.webUrl}`);
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (result?.kind || result?.action) {
|
|
1232
|
+
console.log(`Action: ${result.action || command}`);
|
|
1233
|
+
if (result.skillPath) console.log(`Skill: ${result.skillPath}`);
|
|
1234
|
+
if (Array.isArray(result.installed)) {
|
|
1235
|
+
for (const item of result.installed) console.log(`Installed ${item.agent}: ${item.file}`);
|
|
1236
|
+
}
|
|
1237
|
+
if (result.kind) console.log(`Node: ${result.kind} ${result.id || ""}`.trim());
|
|
1238
|
+
if (result.webUrl) console.log(`Web: ${result.webUrl}`);
|
|
1239
|
+
if (result.webUrlVisibleInBrowser === false) console.log("Web visible in normal browser: no (use Handoff)");
|
|
1240
|
+
if (result.handoffUrl) console.log(`Handoff: ${result.handoffUrl}`);
|
|
1241
|
+
if (result.handoffExpiresAt) console.log(`Handoff expires: ${result.handoffExpiresAt}`);
|
|
1242
|
+
if (result.output) console.log(`Output: ${result.output}${result.bytes ? ` (${result.bytes} bytes)` : ""}`);
|
|
1243
|
+
if (result.shot) {
|
|
1244
|
+
console.log(`Shot: ${result.shot.index || ""} ${result.shot.title || result.shot.id} - ${result.shot.status || "unknown"}`.trim());
|
|
1245
|
+
if (result.shot.videoUrl) console.log(`Video: ${result.shot.videoUrl}`);
|
|
1246
|
+
if (result.shot.videoReviewStatus) console.log(`VLM: ${result.shot.videoReviewStatus}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (result.asset) {
|
|
1249
|
+
console.log(`Asset: ${result.asset.name || result.asset.id} - ${result.asset.mediaKind || "unknown"}`);
|
|
1250
|
+
if (result.asset.mediaUrl) console.log(`Media: ${result.asset.mediaUrl}`);
|
|
1251
|
+
if (result.asset.imageReviewStatus) console.log(`VLM: ${result.asset.imageReviewStatus}`);
|
|
1252
|
+
}
|
|
1253
|
+
if (result.session) {
|
|
1254
|
+
console.log(`Session: ${result.session.title || result.session.id}`);
|
|
1255
|
+
if (result.session.finalVideoReviewStatus) console.log(`Final VLM: ${result.session.finalVideoReviewStatus}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (Array.isArray(result.assets) && result.assets.length) console.log(`Assets: ${result.assets.length}`);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
console.log(`Session: ${result.title || result.sessionId} [${result.sessionId}]`);
|
|
1261
|
+
console.log(`Web: ${result.webUrl}`);
|
|
1262
|
+
if (result.webUrlVisibleInBrowser === false) console.log("Web visible in normal browser: no (use Handoff)");
|
|
1263
|
+
if (result.handoffUrl) console.log(`Handoff: ${result.handoffUrl}`);
|
|
1264
|
+
if (result.handoffExpiresAt) console.log(`Handoff expires: ${result.handoffExpiresAt}`);
|
|
1265
|
+
if (result.story?.premise) console.log(`Premise: ${result.story.premise}`);
|
|
1266
|
+
if (result.planSummary) console.log(`Plan: ${result.planSummary}`);
|
|
1267
|
+
if (Array.isArray(result.shots) && result.shots.length) {
|
|
1268
|
+
console.log("Shots:");
|
|
1269
|
+
for (const shot of result.shots) {
|
|
1270
|
+
console.log(` ${shot.index}. ${shot.title || shot.id} - ${shot.status || "draft"}${shot.durationSec ? ` - ${shot.durationSec}s` : ""}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (result.stitched?.downloadUrl) console.log(`Download: ${result.stitched.downloadUrl}`);
|
|
1274
|
+
if (result.render?.stitched?.downloadUrl) console.log(`Download: ${result.render.stitched.downloadUrl}`);
|
|
1275
|
+
if (result.failed?.length) console.log(`Failed shots: ${result.failed.map((shot) => shot.id).join(", ")}`);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function main() {
|
|
1279
|
+
const { command: rawCommand, positionals, options } = parseArgs(process.argv.slice(2));
|
|
1280
|
+
const command = rawCommand === "new" || rawCommand === "create" || rawCommand === "plan" ? "workflow" : rawCommand;
|
|
1281
|
+
if (options.help || command === "help" || command === "--help" || command === "-h") {
|
|
1282
|
+
console.log(HELP.trim());
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const config = await readConfig();
|
|
1287
|
+
const runtime = resolveRuntime(config, options);
|
|
1288
|
+
options.reporter = createReporter(options);
|
|
1289
|
+
let result;
|
|
1290
|
+
if (command === "configure" || command === "config") {
|
|
1291
|
+
result = await commandConfigure(config, options);
|
|
1292
|
+
} else if (command === "workflow") {
|
|
1293
|
+
result = await commandWorkflow(runtime, positionals, options);
|
|
1294
|
+
} else if (command === "node") {
|
|
1295
|
+
result = await commandNode(runtime, positionals, options);
|
|
1296
|
+
} else if (command === "shot") {
|
|
1297
|
+
result = await commandNode(runtime, positionals, options, "shot");
|
|
1298
|
+
} else if (command === "asset") {
|
|
1299
|
+
result = await commandNode(runtime, positionals, options, "asset");
|
|
1300
|
+
} else if (command === "session") {
|
|
1301
|
+
result = await commandNode(runtime, positionals, options, "session");
|
|
1302
|
+
} else if (command === "publish-storyboards") {
|
|
1303
|
+
result = await commandPublishStoryboards(runtime, options);
|
|
1304
|
+
} else if (command === "final-review") {
|
|
1305
|
+
result = await commandFinalReview(runtime, options);
|
|
1306
|
+
} else if (command === "render") {
|
|
1307
|
+
result = await commandRender(runtime, options);
|
|
1308
|
+
} else if (command === "stitch") {
|
|
1309
|
+
const sessionId = await resolveSessionId(runtime, options.session || "latest");
|
|
1310
|
+
result = await stitchSession(runtime, sessionId, options);
|
|
1311
|
+
} else if (command === "skill") {
|
|
1312
|
+
result = await commandSkill(positionals, options);
|
|
1313
|
+
} else if (command === "status") {
|
|
1314
|
+
result = await commandStatus(runtime, options);
|
|
1315
|
+
} else if (command === "download") {
|
|
1316
|
+
result = await commandDownload(runtime, options);
|
|
1317
|
+
} else if (command === "handoff") {
|
|
1318
|
+
result = await commandHandoff(runtime, options);
|
|
1319
|
+
} else if (command === "open") {
|
|
1320
|
+
result = await commandOpen(runtime, options);
|
|
1321
|
+
} else {
|
|
1322
|
+
throw new CliError(`Unknown command: ${rawCommand}\n\n${HELP.trim()}`);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (options.jsonl) options.reporter.complete(result);
|
|
1326
|
+
else if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1327
|
+
else printHuman(result, command);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
main().catch((error) => {
|
|
1331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1332
|
+
console.error(`reelyai: ${message}`);
|
|
1333
|
+
process.exit(error instanceof CliError ? error.code : 1);
|
|
1334
|
+
});
|