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.
Files changed (2) hide show
  1. package/index.mjs +33 -18
  2. 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 = 300_000;
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
- child.on('error', reject);
111
- child.on('close', (code) => (code === 0 ? resolve(out) : reject(new Error(`${bin} exited ${code}: ${err.slice(0, 300)}`))));
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
- const images = Array.isArray(p.images) ? p.images : [];
285
- for (let i = 0; i < images.length; i++) {
286
- const f = join(tmpdir(), `sbhfcli_${id}_${i}.png`);
287
- await writeFile(f, Buffer.from(String(images[i]), 'base64'));
288
- tmp.push(f);
289
- }
290
- // Seedance multi-ref: upload each temp file → a medias[] array of role 'image', IN ORDER (= @imageN
291
- // order). The single --start-image/--image flags map everything to one "start_image" and reject >1.
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
- if (medias.length) args.push('--medias', JSON.stringify(medias));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyboard-bridge",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Desktop bridge that powers a hosted Storyboard AI webapp with your own local Claude Code / Gemini CLI login (no API key).",
5
5
  "type": "module",
6
6
  "bin": {