storyboard-bridge 0.7.0 → 0.7.2
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 +4 -0
- package/package.json +1 -1
- package/tryinfer_client.py +112 -48
package/index.mjs
CHANGED
|
@@ -323,11 +323,15 @@ function handleTryinfer(msg, ws) {
|
|
|
323
323
|
args.push('--resume-task', String(p.resumeTaskId)); // poll an existing task, no submit
|
|
324
324
|
} else {
|
|
325
325
|
args.push('--prompt', String(p.prompt ?? ''),
|
|
326
|
+
'--capability', String(p.capability || 'reference-to-video'),
|
|
326
327
|
'--duration', String(Math.max(1, Math.round(p.durationSec || 5))),
|
|
327
328
|
'--aspect', String(p.aspectRatio || '1:1'),
|
|
328
329
|
'--resolution', String(p.resolution || '1080p'));
|
|
329
330
|
if (p.audio === false) args.push('--no-audio');
|
|
331
|
+
if (p.numImages && p.numImages > 1) args.push('--num', String(p.numImages)); // Seedream image batch (2x)
|
|
330
332
|
for (const u of (Array.isArray(p.imageUrls) ? p.imageUrls : [])) args.push('--image-url', String(u));
|
|
333
|
+
if (p.lastFrameUrl) args.push('--last-frame-url', String(p.lastFrameUrl)); // image-to-video END frame
|
|
334
|
+
|
|
331
335
|
}
|
|
332
336
|
const child = spawn(PYTHON_BIN, args, { env: { ...process.env, CDP_HTTP }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
333
337
|
tfChildren.set(id, child);
|
package/package.json
CHANGED
package/tryinfer_client.py
CHANGED
|
@@ -123,29 +123,58 @@ def _fetch_expr(method, url, body):
|
|
|
123
123
|
return "".join(parts)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
# CDP errors that mean the execution context we ran fetch() in is gone — the Studio tab NAVIGATED (it changes
|
|
127
|
+
# route when a generation starts/finishes) or reloaded. We recover by re-attaching to the tab's fresh context.
|
|
128
|
+
_CTX_DEAD = ("navigated", "context", "closed", "-32000", "detached", "Session with given id")
|
|
129
|
+
|
|
130
|
+
|
|
126
131
|
class BrowserSession:
|
|
127
132
|
def __init__(self, cdp_http, match):
|
|
133
|
+
self.cdp_http = cdp_http
|
|
134
|
+
self.match = match
|
|
128
135
|
ver = http_json(f"{cdp_http}/json/version")
|
|
129
136
|
log(f"Connected to {ver.get('Browser')}")
|
|
137
|
+
self.browser_ws = ver["webSocketDebuggerUrl"]
|
|
138
|
+
self.cdp = None
|
|
139
|
+
self._attach()
|
|
140
|
+
|
|
141
|
+
def _attach(self):
|
|
142
|
+
"""(Re)resolve the matching tab and attach a fresh Runtime session. Safe to call repeatedly — used
|
|
143
|
+
both at startup and to recover after the Studio tab navigates mid-poll (which kills the context)."""
|
|
144
|
+
if self.cdp:
|
|
145
|
+
try: self.cdp.close()
|
|
146
|
+
except Exception: pass
|
|
130
147
|
tid = url = None
|
|
131
|
-
for t in http_json(f"{cdp_http}/json/list"):
|
|
132
|
-
if t.get("type") == "page" and match in (t.get("url") or ""):
|
|
148
|
+
for t in http_json(f"{self.cdp_http}/json/list"):
|
|
149
|
+
if t.get("type") == "page" and self.match in (t.get("url") or ""):
|
|
133
150
|
tid, url = t["id"], t["url"]
|
|
134
151
|
break
|
|
135
152
|
if not tid:
|
|
136
|
-
raise RuntimeError(f"No open tab whose URL contains '{match}'. Open tryinfer.com and log in.")
|
|
137
|
-
|
|
138
|
-
self.cdp = CDP(ver["webSocketDebuggerUrl"])
|
|
153
|
+
raise RuntimeError(f"No open tab whose URL contains '{self.match}'. Open tryinfer.com and log in.")
|
|
154
|
+
self.cdp = CDP(self.browser_ws)
|
|
139
155
|
self.session_id = self.cdp.call("Target.attachToTarget", {"targetId": tid, "flatten": True})["sessionId"]
|
|
140
156
|
self.cdp.call("Runtime.enable", session_id=self.session_id)
|
|
157
|
+
log(f"Routing API calls through tab: {url}")
|
|
141
158
|
|
|
142
|
-
def request(self, method, url, body=None, timeout=120, retries=
|
|
159
|
+
def request(self, method, url, body=None, timeout=120, retries=4):
|
|
143
160
|
last = None
|
|
144
161
|
for attempt in range(retries):
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
try:
|
|
163
|
+
res = self.cdp.call(
|
|
164
|
+
"Runtime.evaluate",
|
|
165
|
+
{"expression": _fetch_expr(method, url, body), "awaitPromise": True, "returnByValue": True},
|
|
166
|
+
session_id=self.session_id, timeout=timeout)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
last = f"CDP evaluate error: {e}"
|
|
169
|
+
# tab navigated / context died → re-attach to the new context and retry (don't lose the poll).
|
|
170
|
+
if any(s in str(e) for s in _CTX_DEAD):
|
|
171
|
+
log(f" tab navigated — re-attaching… ({e})")
|
|
172
|
+
time.sleep(1)
|
|
173
|
+
try: self._attach()
|
|
174
|
+
except Exception as e2: last = f"re-attach failed: {e2}"
|
|
175
|
+
if attempt < retries - 1:
|
|
176
|
+
time.sleep(2); continue
|
|
177
|
+
raise RuntimeError(last)
|
|
149
178
|
val = res.get("result", {}).get("value")
|
|
150
179
|
if val is None:
|
|
151
180
|
last = f"evaluate failed: {res.get('exceptionDetails')}"
|
|
@@ -174,30 +203,41 @@ DONE = {"SUCCEEDED", "COMPLETED"}
|
|
|
174
203
|
FAILED = {"FAILED", "ERROR", "CANCELLED", "CANCELED"}
|
|
175
204
|
|
|
176
205
|
|
|
177
|
-
def submit(session, prompt, image_urls, duration, aspect, resolution, audio
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
def submit(session, prompt, image_urls, duration, aspect, resolution, audio,
|
|
207
|
+
capability="reference-to-video", model="seedance-2.0-pro", last_frame_url=None,
|
|
208
|
+
num_images=1, input_json=None):
|
|
209
|
+
"""POST a generation task. Video + image shapes, all URL-based (no upload). Returns the task id.
|
|
210
|
+
• reference-to-video : reference_image_urls[] + resolution + audio
|
|
211
|
+
• image-to-video : image_url (start) [+ last_frame_image_url] + audio (no resolution)
|
|
212
|
+
• edit (Seedream) : image_url + prompt + num_images (UI hides aspect)
|
|
213
|
+
• text-to-image : prompt + num_images + aspect_ratio (no image)
|
|
214
|
+
`input_json` overrides the WHOLE input dict verbatim — for probing undocumented combos."""
|
|
215
|
+
medium = "image" if capability in ("edit", "text-to-image") else "video"
|
|
216
|
+
if input_json is not None:
|
|
217
|
+
inp = json.loads(input_json) # PROBE: send exactly this
|
|
218
|
+
meta = {"prompt": prompt, "medium": medium}
|
|
219
|
+
elif capability == "image-to-video":
|
|
220
|
+
if not image_urls:
|
|
221
|
+
raise RuntimeError("image-to-video needs a start frame (--image-url)")
|
|
222
|
+
inp = {"image_url": image_urls[0], "prompt": prompt, "duration_seconds": duration, "aspect_ratio": aspect, "audio": audio}
|
|
223
|
+
if last_frame_url:
|
|
224
|
+
inp["last_frame_image_url"] = last_frame_url
|
|
225
|
+
meta = {"prompt": prompt, "medium": "video", "ratio": RATIO_WORD.get(aspect, "square"), "kind": "animate", "sourceUrl": image_urls[0]}
|
|
226
|
+
if last_frame_url:
|
|
227
|
+
meta["lastFrameUrl"] = last_frame_url
|
|
228
|
+
elif capability == "edit":
|
|
229
|
+
if not image_urls:
|
|
230
|
+
raise RuntimeError("edit needs a source image (--image-url)")
|
|
231
|
+
inp = {"image_url": image_urls[0], "prompt": prompt, "num_images": num_images}
|
|
232
|
+
meta = {"prompt": prompt, "medium": "image", "kind": "edit", "sourceUrl": image_urls[0]}
|
|
233
|
+
elif capability == "text-to-image":
|
|
234
|
+
inp = {"prompt": prompt, "num_images": num_images, "aspect_ratio": aspect}
|
|
235
|
+
meta = {"prompt": prompt, "medium": "image", "kind": "generate"}
|
|
236
|
+
else: # reference-to-video
|
|
237
|
+
inp = {"reference_image_urls": image_urls, "prompt": prompt, "duration_seconds": duration, "aspect_ratio": aspect, "resolution": resolution, "audio": audio}
|
|
238
|
+
meta = {"prompt": prompt, "medium": "video", "ratio": RATIO_WORD.get(aspect, "square"), "kind": "reference", "referenceImageUrls": image_urls, "referenceRequestIds": []}
|
|
239
|
+
body = {"args": {"model": model, "capability": capability, "input": inp},
|
|
240
|
+
"group": {"groupId": str(uuid.uuid4()), "position": 0, "meta": meta}}
|
|
201
241
|
r = session.request("POST", f"{API}/create/generation-tasks", body=body)
|
|
202
242
|
if r.status_code not in (200, 201, 202):
|
|
203
243
|
raise RuntimeError(f"submit failed ({r.status_code}): {r.text[:800]}")
|
|
@@ -228,23 +268,33 @@ def poll(session, task_id, on_status=None, timeout=1800, interval=3):
|
|
|
228
268
|
|
|
229
269
|
|
|
230
270
|
def get_result(session, task_id):
|
|
231
|
-
"""GET the result
|
|
271
|
+
"""GET the result. Returns {'videoUrl':…} for video, or {'images':[url,…]} for image. Raises on
|
|
272
|
+
moderation block / empty output."""
|
|
232
273
|
j = session.request("GET", f"{API}/create/generation-tasks/{task_id}/result").json()
|
|
233
274
|
mod = j.get("moderation_status")
|
|
234
275
|
if mod and mod != "allowed":
|
|
235
276
|
raise RuntimeError(f"moderation blocked: {mod}")
|
|
236
277
|
out = j.get("output") or {}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
278
|
+
if out.get("video_url"):
|
|
279
|
+
return {"videoUrl": out["video_url"], "output": out}
|
|
280
|
+
imgs = [x.get("url") for x in (out.get("images") or []) if isinstance(x, dict) and x.get("url")]
|
|
281
|
+
if imgs:
|
|
282
|
+
return {"images": imgs, "output": out}
|
|
283
|
+
raise RuntimeError(f"no video_url/images in result: {json.dumps(j)[:800]}")
|
|
241
284
|
|
|
242
285
|
|
|
243
286
|
def main():
|
|
244
287
|
ap = argparse.ArgumentParser()
|
|
245
288
|
ap.add_argument("--prompt", default="")
|
|
289
|
+
ap.add_argument("--capability", default="reference-to-video",
|
|
290
|
+
choices=["reference-to-video", "image-to-video", "edit", "text-to-image"])
|
|
291
|
+
ap.add_argument("--model", default=None, help="defaults by capability: image→seedream-5.0-pro, video→seedance-2.0-pro")
|
|
246
292
|
ap.add_argument("--image-url", action="append", default=[],
|
|
247
|
-
help="public
|
|
293
|
+
help="public image URL. reference-to-video: repeat for ordered refs. image-to-video/edit: first = start/source.")
|
|
294
|
+
ap.add_argument("--last-frame-url", default=None, help="image-to-video only: END frame URL (optional)")
|
|
295
|
+
ap.add_argument("--num", type=int, default=1, help="num_images (image capabilities) — probe >1 here")
|
|
296
|
+
ap.add_argument("--input-json", default=None,
|
|
297
|
+
help="PROBE: raw JSON for args.input verbatim (override the built payload — test undocumented combos)")
|
|
248
298
|
ap.add_argument("--duration", type=int, default=5)
|
|
249
299
|
ap.add_argument("--aspect", default="1:1")
|
|
250
300
|
ap.add_argument("--resolution", default="1080p")
|
|
@@ -261,6 +311,8 @@ def main():
|
|
|
261
311
|
_JSON_OUT = sys.stdout
|
|
262
312
|
sys.stdout = sys.stderr # any stray print() can't corrupt the event stream
|
|
263
313
|
|
|
314
|
+
# model default is capability-aware: image caps use seedream (seedANCE is the VIDEO model — a common mixup).
|
|
315
|
+
model = args.model or ("seedream-5.0-pro" if args.capability in ("edit", "text-to-image") else "seedance-2.0-pro")
|
|
264
316
|
image_urls = args.image_url or ["https://www.gstatic.com/webp/gallery/1.jpg"] # default = permissive test image
|
|
265
317
|
sess = BrowserSession(f"http://{args.host}:{args.port}", args.match)
|
|
266
318
|
try:
|
|
@@ -269,11 +321,13 @@ def main():
|
|
|
269
321
|
log(f"resuming task {task_id}")
|
|
270
322
|
event(event="progress", phase="rendering", taskId=task_id)
|
|
271
323
|
else:
|
|
272
|
-
task_id = submit(sess, args.prompt, image_urls, args.duration, args.aspect, args.resolution, not args.no_audio
|
|
324
|
+
task_id = submit(sess, args.prompt, image_urls, args.duration, args.aspect, args.resolution, not args.no_audio,
|
|
325
|
+
capability=args.capability, model=model, last_frame_url=args.last_frame_url,
|
|
326
|
+
num_images=args.num, input_json=args.input_json)
|
|
273
327
|
log(f"submitted task {task_id}")
|
|
274
328
|
event(event="progress", phase="submitted", taskId=task_id)
|
|
275
329
|
poll(sess, task_id, on_status=lambda st: (log(f" status={st}"), event(event="progress", phase=st.lower(), taskId=task_id)))
|
|
276
|
-
|
|
330
|
+
res = get_result(sess, task_id)
|
|
277
331
|
except Exception as e:
|
|
278
332
|
event(event="error", reason="tryinfer", detail=str(e))
|
|
279
333
|
log(f"\n❌ {e}")
|
|
@@ -281,12 +335,22 @@ def main():
|
|
|
281
335
|
finally:
|
|
282
336
|
sess.close()
|
|
283
337
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
338
|
+
if res.get("images"):
|
|
339
|
+
event(event="done", taskId=task_id, images=res["images"])
|
|
340
|
+
dims = {x.get("url"): (x.get("width"), x.get("height")) for x in ((res.get("output") or {}).get("images") or []) if isinstance(x, dict)}
|
|
341
|
+
log(f"\n✅ {len(res['images'])} image(s):")
|
|
342
|
+
for u in res["images"]:
|
|
343
|
+
w, h = dims.get(u, (None, None))
|
|
344
|
+
log(f" {w}x{h} {u}")
|
|
345
|
+
if not args.emit_json:
|
|
346
|
+
print(json.dumps({"ok": True, "images": res["images"]}))
|
|
347
|
+
else:
|
|
348
|
+
out = res.get("output") or {}
|
|
349
|
+
event(event="done", taskId=task_id, videoUrl=res["videoUrl"],
|
|
350
|
+
width=out.get("width"), height=out.get("height"), duration=out.get("duration_seconds"))
|
|
351
|
+
log(f"\n✅ {out.get('width')}x{out.get('height')} {out.get('duration_seconds')}s")
|
|
352
|
+
if not args.emit_json:
|
|
353
|
+
print(json.dumps({"ok": True, "video_url": res["videoUrl"]}))
|
|
290
354
|
|
|
291
355
|
|
|
292
356
|
if __name__ == "__main__":
|