storyboard-bridge 0.4.0 → 0.5.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/index.mjs +33 -18
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -73,7 +73,7 @@ const HF_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'higgsfield_clie
|
|
|
73
73
|
// prints raw text, and passing the flag makes it dump --help. Newer gemini supports it (wraps as
|
|
74
74
|
// {response}) — opt in with GEMINI_JSON=1. extractText() handles wrapped OR raw either way.
|
|
75
75
|
const USE_JSON = PROVIDER === 'claude' || process.env.GEMINI_JSON === '1';
|
|
76
|
-
const JOB_TIMEOUT_MS =
|
|
76
|
+
const JOB_TIMEOUT_MS = 900_000; // 15 min — kills a `claude -p` that runs this long (SIGTERM → "exited null")
|
|
77
77
|
|
|
78
78
|
const log = (m) => console.log(`[bridge ${new Date().toISOString().slice(11, 19)}] ${m}`);
|
|
79
79
|
|
|
@@ -102,13 +102,25 @@ function runCli(bin, args, stdinInput, cwd) {
|
|
|
102
102
|
return new Promise((resolve, reject) => {
|
|
103
103
|
// On Windows the CLI is usually a `.cmd` shim (claude.cmd) that spawn() can't launch without a
|
|
104
104
|
// shell; shell:true uses PATHEXT to resolve it. POSIX (Linux/WSL/macOS) spawns the binary directly.
|
|
105
|
+
const t0 = Date.now();
|
|
105
106
|
const child = spawn(bin, args, { timeout: JOB_TIMEOUT_MS, shell: IS_WIN, cwd });
|
|
106
107
|
let out = '';
|
|
107
108
|
let err = '';
|
|
108
109
|
child.stdout.on('data', (d) => (out += d));
|
|
109
110
|
child.stderr.on('data', (d) => (err += d));
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
// heartbeat: proves the job is alive and shows whether it's PRODUCING output or truly hung (stdout stuck at 0)
|
|
112
|
+
const hb = setInterval(() => log(` … ${bin} still running ${((Date.now() - t0) / 1000).toFixed(0)}s (stdout ${out.length}b, stderr ${err.length}b)`), 30_000);
|
|
113
|
+
child.on('error', (e) => { clearInterval(hb); reject(e); });
|
|
114
|
+
child.on('close', (code, signal) => {
|
|
115
|
+
clearInterval(hb);
|
|
116
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
117
|
+
if (code === 0) return resolve(out);
|
|
118
|
+
// code===null → killed by a SIGNAL (not a clean error). SIGTERM here = our JOB_TIMEOUT_MS hit; else OS/OOM.
|
|
119
|
+
const how = signal
|
|
120
|
+
? `killed by ${signal}${signal === 'SIGTERM' ? ` — hit the ${JOB_TIMEOUT_MS / 1000}s job timeout (it was HANGING, not producing)` : ' (external kill — likely OS/out-of-memory)'}`
|
|
121
|
+
: `exited ${code}`;
|
|
122
|
+
reject(new Error(`${bin} ${how} after ${secs}s — produced ${out.length}b stdout; stderr: ${err.slice(0, 800).trim() || '(empty — nothing printed)'}`));
|
|
123
|
+
});
|
|
112
124
|
// Feed the prompt via STDIN (not argv) — see runJob for why this matters on Windows.
|
|
113
125
|
if (stdinInput != null) child.stdin.write(stdinInput);
|
|
114
126
|
child.stdin.end();
|
|
@@ -281,14 +293,19 @@ async function handleHiggsfieldCli(msg, ws) {
|
|
|
281
293
|
const tmp = [];
|
|
282
294
|
const cleanup = () => { for (const f of tmp) unlink(f).catch(() => {}); };
|
|
283
295
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
// write the shipped bytes to temp files; the CLI AUTO-UPLOADS local paths given to its media flags,
|
|
297
|
+
// so we pass --image/--video directly (no manual `upload create`/--medias). Videos cap at 3 (Seedance).
|
|
298
|
+
const writeTmp = async (arr, ext) => {
|
|
299
|
+
const out = [];
|
|
300
|
+
for (let i = 0; i < arr.length; i++) {
|
|
301
|
+
const f = join(tmpdir(), `sbhfcli_${id}_${ext}${i}.${ext}`);
|
|
302
|
+
await writeFile(f, Buffer.from(String(arr[i]), 'base64'));
|
|
303
|
+
tmp.push(f); out.push(f);
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
};
|
|
307
|
+
const imageFiles = await writeTmp(Array.isArray(p.images) ? p.images : [], 'png');
|
|
308
|
+
const videoFiles = await writeTmp((Array.isArray(p.videos) ? p.videos : []).slice(0, 3), 'mp4');
|
|
292
309
|
const runHf = (a) => new Promise((resolve, reject) => {
|
|
293
310
|
const child = spawn(HF_CLI, a, { shell: IS_WIN, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
294
311
|
hfCliChildren.set(id, child);
|
|
@@ -298,12 +315,6 @@ async function handleHiggsfieldCli(msg, ws) {
|
|
|
298
315
|
child.on('error', (e) => reject(new Error(`${HF_CLI}: ${e.message}`)));
|
|
299
316
|
child.on('close', (code) => (code !== 0 ? reject(new Error(`higgsfield CLI exited ${code}: ${(errTail || out).slice(-8000)}`)) : resolve(out)));
|
|
300
317
|
});
|
|
301
|
-
const medias = [];
|
|
302
|
-
for (const f of tmp) {
|
|
303
|
-
const o = JSON.parse(await runHf(['upload', 'create', f, '--json']));
|
|
304
|
-
if (!o?.id) throw new Error('higgsfield upload returned no id');
|
|
305
|
-
medias.push({ role: 'image', data: { id: o.id, type: 'media_input' } }); // 'media_input' = uploaded file ('image' is rejected)
|
|
306
|
-
}
|
|
307
318
|
const promptStr = String(p.prompt ?? '');
|
|
308
319
|
const args = ['generate', 'create', String(p.slug || 'seedance_2_0'),
|
|
309
320
|
'--prompt', promptStr.startsWith('@') ? ' ' + promptStr : promptStr, // leading '@' = "read from file" to the CLI; a space neutralizes it
|
|
@@ -312,7 +323,8 @@ async function handleHiggsfieldCli(msg, ws) {
|
|
|
312
323
|
'--aspect_ratio', String(p.aspectRatio || '16:9'),
|
|
313
324
|
'--json', '--wait'];
|
|
314
325
|
if (p.mode) args.push('--mode', String(p.mode));
|
|
315
|
-
|
|
326
|
+
for (const f of imageFiles) args.push('--image', f);
|
|
327
|
+
for (const f of videoFiles) args.push('--video', f);
|
|
316
328
|
const out = await runHf(args);
|
|
317
329
|
hfCliChildren.delete(id);
|
|
318
330
|
cleanup();
|
|
@@ -382,6 +394,9 @@ function connect() {
|
|
|
382
394
|
}
|
|
383
395
|
} else if (msg.type === 'job' && msg.id) {
|
|
384
396
|
const t0 = Date.now();
|
|
397
|
+
const promptLen = String(msg.prompt ?? '').length;
|
|
398
|
+
const imgCount = Array.isArray(msg.images) ? msg.images.length : 0;
|
|
399
|
+
log(`job ${msg.id} → running (prompt ${(promptLen / 1000).toFixed(1)}k chars${imgCount ? `, ${imgCount} image${imgCount === 1 ? '' : 's'}` : ''})`);
|
|
385
400
|
try {
|
|
386
401
|
const text = await runJob(String(msg.prompt ?? ''), Array.isArray(msg.images) ? msg.images : null);
|
|
387
402
|
deliver({ type: 'result', id: msg.id, text }); // survives a mid-job reconnect (queued + flushed)
|
package/package.json
CHANGED