storyboard-bridge 0.3.6 → 0.3.8
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 +97 -5
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -31,12 +31,17 @@ import { join, resolve, dirname, sep } from 'node:path';
|
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { tmpdir } from 'node:os';
|
|
33
33
|
import { WebSocket } from 'ws';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// read from package.json so the printed version can NEVER drift from the published one (the bug where 0.3.6
|
|
37
|
+
// still announced v0.3.5 + nagged forever). package.json ships in the npm tarball next to this file.
|
|
38
|
+
const VERSION = createRequire(import.meta.url)('./package.json').version;
|
|
36
39
|
|
|
37
40
|
// in-flight Higgsfield render children, keyed by job id — so a `higgsfield-cancel` can kill the right one
|
|
38
41
|
// (the backend sends it when the user cancels, freeing the single render slot instead of leaving it running).
|
|
39
42
|
const hfChildren = new Map();
|
|
43
|
+
// in-flight official-CLI render children (the `higgsfield-cli` credit-billed path — separate from the CDP one).
|
|
44
|
+
const hfCliChildren = new Map();
|
|
40
45
|
|
|
41
46
|
// ---- config ----
|
|
42
47
|
const arg = (name, fallback) => {
|
|
@@ -60,6 +65,8 @@ const HIGGSFIELD = process.argv.includes('--higgsfield') || process.env.STORYBOA
|
|
|
60
65
|
// Opt out with --no-own-rights or STORYBOARD_OWN_RIGHTS=0.
|
|
61
66
|
const OWN_RIGHTS = !(process.argv.includes('--no-own-rights') || process.env.STORYBOARD_OWN_RIGHTS === '0');
|
|
62
67
|
const PYTHON_BIN = arg('python-bin', process.env.PYTHON_BIN || (IS_WIN ? 'python.exe' : 'python3'));
|
|
68
|
+
// the official Higgsfield CLI (credit-billed i2v). Available iff installed + `higgsfield auth login` done.
|
|
69
|
+
const HF_CLI = arg('higgsfield-cli-bin', process.env.HIGGSFIELD_CLI_BIN || 'higgsfield');
|
|
63
70
|
const CDP_HTTP = arg('cdp', process.env.CDP_HTTP || 'http://localhost:9222'); // the debug Chrome endpoint
|
|
64
71
|
const HF_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'higgsfield_client.py');
|
|
65
72
|
// claude supports `--output-format json` (wraps as {result}); gemini's CLI (<=0.1.x) does NOT — it
|
|
@@ -226,12 +233,12 @@ async function handleHiggsfield(msg, ws) {
|
|
|
226
233
|
ws.send(JSON.stringify({ type: 'hfEvent', id, ...ev })); // forward progress/verified/done/error
|
|
227
234
|
}
|
|
228
235
|
});
|
|
229
|
-
child.stderr.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-
|
|
236
|
+
child.stderr.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-8000); });
|
|
230
237
|
child.on('error', (e) => { hfChildren.delete(id); cleanup(); ws.send(JSON.stringify({ type: 'hfEvent', id, event: 'error', reason: 'spawn', detail: `${PYTHON_BIN}: ${e.message}` })); });
|
|
231
238
|
child.on('close', (code) => {
|
|
232
239
|
hfChildren.delete(id);
|
|
233
240
|
cleanup();
|
|
234
|
-
if (!sawDone && code !== 0) ws.send(JSON.stringify({ type: 'hfEvent', id, event: 'error', reason: 'exit', detail: `python exited ${code}: ${stderrTail
|
|
241
|
+
if (!sawDone && code !== 0) ws.send(JSON.stringify({ type: 'hfEvent', id, event: 'error', reason: 'exit', detail: `python exited ${code}: ${stderrTail}` }));
|
|
235
242
|
});
|
|
236
243
|
} catch (e) {
|
|
237
244
|
cleanup();
|
|
@@ -239,6 +246,85 @@ async function handleHiggsfield(msg, ws) {
|
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
|
|
249
|
+
// Pull the result video URL out of the CLI's `--json` output. The exact schema isn't publicly documented,
|
|
250
|
+
// so scan defensively: each line (the CLI may print logs then a final JSON object), then known fields, then
|
|
251
|
+
// any media URL. On a miss the caller returns the raw tail in the error so we can lock the field fast.
|
|
252
|
+
function extractCliVideoUrl(out) {
|
|
253
|
+
const fromObj = (o) => {
|
|
254
|
+
if (!o || typeof o !== 'object') return null;
|
|
255
|
+
if (Array.isArray(o)) { for (const r of o) { const u = fromObj(r); if (u) return u; } return null; } // `generate create --json` returns [{ result_url }, …]
|
|
256
|
+
for (const k of ['result_url', 'output_url', 'video_url', 'url', 'resultUrl', 'outputUrl']) {
|
|
257
|
+
if (typeof o[k] === 'string' && /^https?:\/\//.test(o[k])) return o[k];
|
|
258
|
+
}
|
|
259
|
+
for (const arr of [o.results, o.outputs, o.assets]) {
|
|
260
|
+
if (Array.isArray(arr)) for (const r of arr) {
|
|
261
|
+
if (typeof r === 'string' && /^https?:\/\//.test(r)) return r;
|
|
262
|
+
const u = fromObj(r); if (u) return u;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return fromObj(o.result) || fromObj(o.job) || fromObj(o.data) || null;
|
|
266
|
+
};
|
|
267
|
+
const lines = out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
268
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
269
|
+
try { const u = fromObj(JSON.parse(lines[i])); if (u) return u; } catch { /* not json */ }
|
|
270
|
+
}
|
|
271
|
+
try { const u = fromObj(JSON.parse(out)); if (u) return u; } catch { /* not one json blob */ }
|
|
272
|
+
const m = out.match(/https?:\/\/[^\s"']+\.(?:mp4|mov|webm|m4v)/i);
|
|
273
|
+
return m ? m[0] : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Run a CREDIT-BILLED Seedance render via the official `higgsfield` CLI (separate from handleHiggsfield's CDP
|
|
277
|
+
// path). The CLI blocks until done (--wait) and prints the result as JSON; we parse the URL and reply 'done'.
|
|
278
|
+
async function handleHiggsfieldCli(msg, ws) {
|
|
279
|
+
const id = msg.id;
|
|
280
|
+
const p = msg.params || {};
|
|
281
|
+
const tmp = [];
|
|
282
|
+
const cleanup = () => { for (const f of tmp) unlink(f).catch(() => {}); };
|
|
283
|
+
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.
|
|
292
|
+
const runHf = (a) => new Promise((resolve, reject) => {
|
|
293
|
+
const child = spawn(HF_CLI, a, { shell: IS_WIN, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
294
|
+
hfCliChildren.set(id, child);
|
|
295
|
+
let out = '', errTail = '';
|
|
296
|
+
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
297
|
+
child.stderr.on('data', (d) => { errTail = (errTail + d.toString()).slice(-8000); });
|
|
298
|
+
child.on('error', (e) => reject(new Error(`${HF_CLI}: ${e.message}`)));
|
|
299
|
+
child.on('close', (code) => (code !== 0 ? reject(new Error(`higgsfield CLI exited ${code}: ${(errTail || out).slice(-8000)}`)) : resolve(out)));
|
|
300
|
+
});
|
|
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
|
+
const args = ['generate', 'create', String(p.slug || 'seedance_2_0'),
|
|
308
|
+
'--prompt', String(p.prompt ?? ''),
|
|
309
|
+
'--duration', String(Math.max(1, Math.round(p.durationSec || 5))),
|
|
310
|
+
'--resolution', String(p.resolution || '720p'),
|
|
311
|
+
'--aspect_ratio', String(p.aspectRatio || '16:9'),
|
|
312
|
+
'--json', '--wait'];
|
|
313
|
+
if (p.mode) args.push('--mode', String(p.mode));
|
|
314
|
+
if (medias.length) args.push('--medias', JSON.stringify(medias));
|
|
315
|
+
const out = await runHf(args);
|
|
316
|
+
hfCliChildren.delete(id);
|
|
317
|
+
cleanup();
|
|
318
|
+
const url = extractCliVideoUrl(out);
|
|
319
|
+
if (url) ws.send(JSON.stringify({ type: 'hfCliEvent', id, event: 'done', videoUrl: url }));
|
|
320
|
+
else ws.send(JSON.stringify({ type: 'hfCliEvent', id, event: 'error', reason: 'parse', detail: `no result URL in CLI output: ${out.slice(-8000)}` }));
|
|
321
|
+
} catch (e) {
|
|
322
|
+
hfCliChildren.delete(id);
|
|
323
|
+
cleanup();
|
|
324
|
+
ws.send(JSON.stringify({ type: 'hfCliEvent', id, event: 'error', reason: 'bridge', detail: String(e?.message ?? e) }));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
242
328
|
// ---- connection (reconnects forever with backoff) ----
|
|
243
329
|
let backoff = 1000;
|
|
244
330
|
function connect() {
|
|
@@ -251,8 +337,9 @@ function connect() {
|
|
|
251
337
|
claude: await cliAvailable(BIN_FOR.claude),
|
|
252
338
|
gemini: await cliAvailable(BIN_FOR.gemini),
|
|
253
339
|
};
|
|
254
|
-
|
|
255
|
-
|
|
340
|
+
const hfCli = await cliAvailable(HF_CLI); // official `higgsfield` CLI present + runnable → can serve credit-billed renders
|
|
341
|
+
ws.send(JSON.stringify({ type: 'hello', version: VERSION, provider: PROVIDER, providers, files: !!FILES_ROOT, higgsfield: HIGGSFIELD, higgsfieldCli: hfCli }));
|
|
342
|
+
log(`connected to ${url} — serving jobs with: ${PROVIDER} (${BIN})${FILES_ROOT ? `; files → ${FILES_ROOT}` : ''}${HIGGSFIELD ? `; HIGGSFIELD HOST (python: ${PYTHON_BIN}, cdp: ${CDP_HTTP})` : ''}${hfCli ? `; HIGGSFIELD CLI (${HF_CLI})` : ''}`);
|
|
256
343
|
if (!providers[PROVIDER]) {
|
|
257
344
|
log(`WARNING: '${BIN}' was not found / not runnable. Install it and log in, or pass --provider/--*_BIN. Jobs will fail until then.`);
|
|
258
345
|
}
|
|
@@ -297,6 +384,11 @@ function connect() {
|
|
|
297
384
|
// user cancelled → stop this render so the single account slot frees (the python client honours SIGTERM)
|
|
298
385
|
const child = hfChildren.get(msg.id);
|
|
299
386
|
if (child) { log(`higgsfield ${msg.id} → cancel`); try { child.kill('SIGTERM'); } catch { /* already gone */ } hfChildren.delete(msg.id); }
|
|
387
|
+
} else if (msg.type === 'higgsfield-cli' && msg.id) {
|
|
388
|
+
log(`higgsfield-cli ${msg.id} → running`); handleHiggsfieldCli(msg, ws);
|
|
389
|
+
} else if (msg.type === 'higgsfield-cli-cancel' && msg.id) {
|
|
390
|
+
const child = hfCliChildren.get(msg.id);
|
|
391
|
+
if (child) { log(`higgsfield-cli ${msg.id} → cancel`); try { child.kill('SIGTERM'); } catch { /* already gone */ } hfCliChildren.delete(msg.id); }
|
|
300
392
|
}
|
|
301
393
|
});
|
|
302
394
|
|
package/package.json
CHANGED