cozyclay 2.0.2 → 2.1.1
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/CHANGELOG.md +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// POST /oauth/logout -> { ok }
|
|
14
14
|
// POST /agent/turn -> SSE, lines of `data: {json}`
|
|
15
15
|
// POST /agent/stop -> { ok }
|
|
16
|
-
// GET /agent/models -> { providers: [{ id, label, signedIn, models }], models: [flat] }
|
|
16
|
+
// GET /agent/models -> { providers: [{ id, label, signedIn, models }], models: [flat], roles: { byMain } }
|
|
17
17
|
// POST /agent/turn/<turnId>/steer -> { ok, queued } while that turn streams
|
|
18
18
|
//
|
|
19
19
|
// Studio hosts add the frozen task-1 contracts on the same routes:
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
// GET /agent/turn/<turnId>/events?after=N -> replay after a dropped stream
|
|
22
22
|
// POST /agent/stop -> { surface, sessionId, turnId, jobId? }
|
|
23
23
|
// POST /agent/jobs/<jobId>/accept -> explicit "Apply with warnings"
|
|
24
|
+
// GET /agent/turn/<turnId>/supervisor -> 200 supervisor note | 204 none (#728)
|
|
24
25
|
|
|
25
26
|
import { bucketMs, track } from "../analytics.js";
|
|
26
27
|
import { appendAttachments, ATTACHMENT_MAX_COUNT } from "./attachment-image.js";
|
|
27
28
|
import { ko } from "../locale.js";
|
|
28
29
|
import { AGENT_TOOL_CATEGORIES, EXECUTION_TELEMETRY_VALUES } from "../execution-telemetry.js";
|
|
29
30
|
import { STUDIO_VARIANTS, validateReceipt } from "../studio-agent-protocol.js";
|
|
31
|
+
import { requestAgentImage, sidecarRequest, sidecarUrl } from "../agent-image-client.js";
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const AGENT_PANEL_WIDTH_KEY = "cozyclay.workflow.agentPanel.width";
|
|
32
34
|
export const AGENT_PANEL_WIDTH_DEFAULT = 360;
|
|
33
35
|
export const AGENT_PANEL_WIDTH_MIN = 300;
|
|
34
36
|
export const AGENT_PANEL_WIDTH_MAX = 560;
|
|
@@ -125,7 +127,7 @@ export const SUGGESTION_CHIPS = [
|
|
|
125
127
|
/** Previs asks, in the Studio's own words: short enough to read at a glance in
|
|
126
128
|
* the Inspector column, and every one of them is something the Studio families
|
|
127
129
|
* can actually do. */
|
|
128
|
-
|
|
130
|
+
const STUDIO_SUGGESTION_CHIPS = [
|
|
129
131
|
ko("Block a two-shot", "\uD22C\uC0F7 \uBE14\uB85C\uD0B9"),
|
|
130
132
|
ko("Frame the selected character", "\uC120\uD0DD\uD55C \uCE90\uB9AD\uD130 \uAD6C\uB3C4 \uC7A1\uAE30"),
|
|
131
133
|
ko("Light the set warmer", "\uC138\uD2B8 \uC870\uBA85 \uB354 \uB530\uB73B\uD558\uAC8C"),
|
|
@@ -263,6 +265,27 @@ export function storeModel(key, storage = globalThis.localStorage) {
|
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
/** The supervisor switch (#728): on unless the author turned it off. It rides
|
|
269
|
+
* on every Studio turn as `supervisor: false` so the sidecar skips the review
|
|
270
|
+
* for that turn; the sidecar's own COZYCLAY_SUPERVISOR=off still wins. */
|
|
271
|
+
const AGENT_SUPERVISOR_KEY = "cozyclay.agent.supervisor";
|
|
272
|
+
|
|
273
|
+
export function readStoredSupervisor(storage = globalThis.localStorage) {
|
|
274
|
+
try {
|
|
275
|
+
return storage?.getItem(AGENT_SUPERVISOR_KEY) !== "off";
|
|
276
|
+
} catch {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function storeSupervisor(enabled, storage = globalThis.localStorage) {
|
|
282
|
+
try {
|
|
283
|
+
storage?.setItem(AGENT_SUPERVISOR_KEY, enabled ? "on" : "off");
|
|
284
|
+
} catch {
|
|
285
|
+
// Private-mode storage denial must never break the toggle.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
266
289
|
/** The model to open with: the remembered key when it is still advertised,
|
|
267
290
|
* otherwise the first model the sidecar listed. */
|
|
268
291
|
export function preferredModel(models, stored = readStoredModel()) {
|
|
@@ -293,6 +316,20 @@ export function nextSelectedModel(providers, models, current) {
|
|
|
293
316
|
return preferredModel(models.filter((entry) => modelIsSelectable(providers, entry.id)));
|
|
294
317
|
}
|
|
295
318
|
|
|
319
|
+
/** The roles the sidecar derived for one main model (#717): `{ helper, vision,
|
|
320
|
+
* advisor, fallback }`, each a `provider/id` key. Read-only here — nothing the
|
|
321
|
+
* panel sends depends on it — so a malformed entry is simply not shown. */
|
|
322
|
+
export function selectedModelRoles(byMain, key) {
|
|
323
|
+
const entry = byMain && typeof byMain === "object" && typeof key === "string" ? byMain[key] : null;
|
|
324
|
+
return entry && ["helper", "vision", "advisor"].every((role) => typeof entry[role] === "string" && entry[role]) ? entry : null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A `provider/id` key without its provider, for a line that has no room for it. */
|
|
328
|
+
export function shortModelId(key) {
|
|
329
|
+
const value = String(key ?? "");
|
|
330
|
+
return value.slice(value.indexOf("/") + 1);
|
|
331
|
+
}
|
|
332
|
+
|
|
296
333
|
/** "resets in 42m" / "resets in 1h 05m" for the account strip and the paused
|
|
297
334
|
* card countdown. Returns null when there is nothing to count down to. */
|
|
298
335
|
export function formatResetIn(resetAt, now = Date.now()) {
|
|
@@ -340,6 +377,28 @@ export const STEER_ERROR_COPY = {
|
|
|
340
377
|
NO_ACTIVE_TURN: "That turn already ended — send it as a new message.",
|
|
341
378
|
};
|
|
342
379
|
|
|
380
|
+
/** Codes a Studio refusal carries: a turn that met one is reviewed by the
|
|
381
|
+
* supervisor even when it authored nothing (#728). */
|
|
382
|
+
const SUPERVISOR_REFUSAL = /^(TARGET_NOT_READY|CAPABILITY_MISSING|CONFIRMATION_REQUIRED):/;
|
|
383
|
+
|
|
384
|
+
/** The next prompt a supervisor note suggests: its blocker and concern issues,
|
|
385
|
+
* or null when it raised none. A note with validated fixes (#743) asks for
|
|
386
|
+
* exactly those run_action calls, in order, in one script; the blockers and
|
|
387
|
+
* concerns without a fix follow under their own header. */
|
|
388
|
+
export function supervisorFollowUp(note) {
|
|
389
|
+
const all = Array.isArray(note?.issues) ? note.issues : [];
|
|
390
|
+
const fixes = (Array.isArray(note?.fixes) ? note.fixes : []).filter((fix) => typeof fix?.action === "string" && fix.args && typeof fix.args === "object");
|
|
391
|
+
const fixed = new Set(fixes.map((fix) => fix.issueIndex));
|
|
392
|
+
const issues = all.filter((issue, index) => (issue?.severity === "blocker" || issue?.severity === "concern") && !fixed.has(index));
|
|
393
|
+
const line = (issue) => `- [${issue.severity}] ${issue.text}${issue.evidence ? ` (${issue.evidence})` : ""}`;
|
|
394
|
+
if (!fixes.length) return issues.length ? [ko("Supervisor review — please address:", "감독 검토 — 다음을 고쳐 주세요:"), ...issues.map(line)].join("\n") : null;
|
|
395
|
+
return [
|
|
396
|
+
ko("Supervisor review — apply these fixes exactly, in order, in ONE script call, then verify_result; do not re-derive them:", "감독 검토 — 아래 수정을 유추 없이 순서대로 script 1회로 실행하고 verify_result로 확인해 주세요:"),
|
|
397
|
+
...fixes.map((fix) => `- [${fix.severity}] ${all[fix.issueIndex]?.text ?? ""} -> run_action ${JSON.stringify({ action: fix.action, args: fix.args })}`),
|
|
398
|
+
...(issues.length ? [ko("Also check:", "추가 확인:"), ...issues.map(line)] : []),
|
|
399
|
+
].join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
343
402
|
export const ERROR_COPY = {
|
|
344
403
|
auth: "Your session expired. Sign in again to continue.",
|
|
345
404
|
entitlement: "This account cannot generate images.",
|
|
@@ -348,8 +407,6 @@ export const ERROR_COPY = {
|
|
|
348
407
|
overloaded: "The model service is overloaded right now. Try again in a moment.",
|
|
349
408
|
};
|
|
350
409
|
|
|
351
|
-
// --- activity line ---------------------------------------------------------
|
|
352
|
-
//
|
|
353
410
|
// Silence is the failure mode this replaces: a live turn says what it is doing
|
|
354
411
|
// and a finished turn says how it ended, in the same line, so "nothing on
|
|
355
412
|
// screen" can never mean "nobody knows".
|
|
@@ -445,14 +502,6 @@ export function parseSseChunk(buffer) {
|
|
|
445
502
|
return { events, tail };
|
|
446
503
|
}
|
|
447
504
|
|
|
448
|
-
// --- real transport (loopback sidecar) ------------------------------------
|
|
449
|
-
|
|
450
|
-
const SIDECAR_ORIGIN = "";
|
|
451
|
-
|
|
452
|
-
function sidecarUrl(path) {
|
|
453
|
-
return `${SIDECAR_ORIGIN}${path}`;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
505
|
const validTelemetryId = (value) => typeof value === "string" && /^[a-f0-9]{32}$/.test(value);
|
|
457
506
|
const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]);
|
|
458
507
|
const exactKeys = (value, keys) => value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -574,7 +623,7 @@ const RESUME_ATTEMPTS = 3;
|
|
|
574
623
|
* — it only needs the variable's NAME, so an env-backed provider can say which
|
|
575
624
|
* variable is already holding one instead of offering an input that would be
|
|
576
625
|
* ignored. */
|
|
577
|
-
|
|
626
|
+
const PROVIDER_ENV_VARS = {
|
|
578
627
|
anthropic: ["ANTHROPIC_API_KEY"],
|
|
579
628
|
openai: ["OPENAI_API_KEY"],
|
|
580
629
|
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
@@ -590,26 +639,7 @@ export function providerEnvLabel(id) {
|
|
|
590
639
|
|
|
591
640
|
export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalThis), surface, capture = track, now = () => performance.now() } = {}) {
|
|
592
641
|
const activeTurns = new Map();
|
|
593
|
-
const request =
|
|
594
|
-
const response = await fetchImpl(sidecarUrl(path), {
|
|
595
|
-
headers: { "content-type": "application/json" },
|
|
596
|
-
...init,
|
|
597
|
-
});
|
|
598
|
-
if (!response.ok) {
|
|
599
|
-
let detail = null;
|
|
600
|
-
try { detail = await response.clone().json(); } catch { /* preserve the status when the server did not send JSON */ }
|
|
601
|
-
const message = typeof detail?.error === "string" ? detail.error : detail?.error?.message;
|
|
602
|
-
const error = new Error(message || `${path} responded ${response.status}`);
|
|
603
|
-
// Keep machine-readable verification evidence alongside the human message.
|
|
604
|
-
// The Workflow node can show why an H3 take was rejected without exposing
|
|
605
|
-
// or retaining the rejected video itself.
|
|
606
|
-
error.status = response.status;
|
|
607
|
-
if (detail?.error && typeof detail.error === "object") Object.assign(error, detail.error);
|
|
608
|
-
if (detail?.preservation && typeof detail.preservation === "object") error.preservation = detail.preservation;
|
|
609
|
-
throw error;
|
|
610
|
-
}
|
|
611
|
-
return response.json();
|
|
612
|
-
};
|
|
642
|
+
const request = (path, init) => sidecarRequest(fetchImpl, path, init);
|
|
613
643
|
return {
|
|
614
644
|
mock: false,
|
|
615
645
|
async status() {
|
|
@@ -635,6 +665,10 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
635
665
|
const providers = Array.isArray(result?.providers)
|
|
636
666
|
? result.providers.filter((provider) => provider?.id && Array.isArray(provider.models))
|
|
637
667
|
: [];
|
|
668
|
+
// The derived model roles (#717) ride along untouched; a sidecar without
|
|
669
|
+
// them simply leaves the roles line hidden.
|
|
670
|
+
const byMain = result?.roles?.byMain;
|
|
671
|
+
if (byMain && typeof byMain === "object" && !Array.isArray(byMain)) return { providers, models, roles: { byMain } };
|
|
638
672
|
return { providers, models };
|
|
639
673
|
},
|
|
640
674
|
/** Steering a turn that is STILL streaming. The id is the one this browser
|
|
@@ -682,8 +716,8 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
682
716
|
// `references` are the scene's identity / environment slots (#167): extra
|
|
683
717
|
// attached pictures with a role, passed through untouched so the sidecar
|
|
684
718
|
// decides how they are described to the model.
|
|
685
|
-
async image(
|
|
686
|
-
return
|
|
719
|
+
async image(payload, signal) {
|
|
720
|
+
return requestAgentImage(payload, { fetchImpl, signal });
|
|
687
721
|
},
|
|
688
722
|
async video(payload, signal) {
|
|
689
723
|
return request("/agent/video", { method: "POST", body: JSON.stringify(payload), signal });
|
|
@@ -691,9 +725,18 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
691
725
|
async videoProviders() {
|
|
692
726
|
return request("/agent/video/providers");
|
|
693
727
|
},
|
|
728
|
+
/** The supervisor note for a finished Studio turn (#728): one long-poll
|
|
729
|
+
* the sidecar holds while the review runs. A 200 is the note; a 204, a
|
|
730
|
+
* failure or anything unreadable is no note. */
|
|
731
|
+
async supervisor(turnId, signal) {
|
|
732
|
+
const response = await fetchImpl(sidecarUrl(`/agent/turn/${encodeURIComponent(turnId)}/supervisor`), { headers: { accept: "application/json" }, credentials: "same-origin", signal });
|
|
733
|
+
if (response?.status !== 200) return null;
|
|
734
|
+
const note = await response.json();
|
|
735
|
+
return note?.type === "supervisor" && ["reviewed", "unavailable"].includes(note.verdict) ? note : null;
|
|
736
|
+
},
|
|
694
737
|
/** Streams sidecar events to `onEvent`. Resolves when the turn ends. */
|
|
695
738
|
async turn(turnRequest, onEvent, signal) {
|
|
696
|
-
const { sessionId, text, attachFrame, model, effort, attachments } = turnRequest;
|
|
739
|
+
const { sessionId, text, attachFrame, model, effort, attachments, supervisor } = turnRequest;
|
|
697
740
|
const attached = Array.isArray(attachments) && attachments.length ? { attachments } : {};
|
|
698
741
|
const studio = turnRequest.surface === "studio";
|
|
699
742
|
const turnId = studio ? turnRequest.turnId : null;
|
|
@@ -748,7 +791,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
748
791
|
const body = studio
|
|
749
792
|
? JSON.stringify({
|
|
750
793
|
surface: "studio", sessionId, turnId, text, context: turnRequest.context,
|
|
751
|
-
...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(attachFrame === undefined ? {} : { attachFrame: Boolean(attachFrame) }), ...attached,
|
|
794
|
+
...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(attachFrame === undefined ? {} : { attachFrame: Boolean(attachFrame) }), ...(supervisor === false ? { supervisor: false } : {}), ...attached,
|
|
752
795
|
})
|
|
753
796
|
: JSON.stringify({ sessionId, text, attachFrame, model, ...(effort ? { effort } : {}), ...attached, ...(telemetry.turnId ? { turn_id: telemetry.turnId } : {}) });
|
|
754
797
|
try {
|
|
@@ -798,8 +841,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
798
841
|
};
|
|
799
842
|
}
|
|
800
843
|
|
|
801
|
-
// --- mock transport (QA)
|
|
802
|
-
|
|
844
|
+
// --- mock transport (QA) ---
|
|
803
845
|
/** ?agent=mock turns the panel onto a scripted transport so every state in the
|
|
804
846
|
* issue can be reached and screenshotted before the sidecar exists. */
|
|
805
847
|
export function mockConfigFromSearch(search = globalThis.location?.search || "") {
|
|
@@ -902,7 +944,7 @@ const MOCK_PROVIDER_MODELS = {
|
|
|
902
944
|
const MOCK_LIVE_ONLY_CODEX_MODEL = { id: "gpt-6-live-preview", label: "gpt-6 live preview", efforts: ["none", "low", "medium", "high"], defaultEffort: "high" };
|
|
903
945
|
/** The shortest key the scripted sidecar will store, so QA can drive the
|
|
904
946
|
* refusal path (PUT → 400) without a real provider. */
|
|
905
|
-
|
|
947
|
+
const MOCK_PROVIDER_KEY_MIN = 8;
|
|
906
948
|
/** How many times the scripted sidecar answered /oauth/status. The panel reads
|
|
907
949
|
* the session once per credential write and never on a timer (#379), and this
|
|
908
950
|
* is how browser QA proves it without reaching inside the component. */
|
|
@@ -1107,8 +1149,6 @@ export function createAgentTransport(options = {}) {
|
|
|
1107
1149
|
return config ? createMockTransport({ surface: options.surface, ...config }) : createHttpTransport(options);
|
|
1108
1150
|
}
|
|
1109
1151
|
|
|
1110
|
-
// --- chat store (framework-free) -------------------------------------------
|
|
1111
|
-
//
|
|
1112
1152
|
// One conversation: session identity, transcript, motion jobs, receipts and
|
|
1113
1153
|
// acknowledged image actions. It lives here rather than inside the component so
|
|
1114
1154
|
// the dock, an embedded Studio host and the test runner drive the SAME state
|
|
@@ -1119,7 +1159,7 @@ export function createAgentTransport(options = {}) {
|
|
|
1119
1159
|
* already committed belongs to its receipt, not here; anything short of an
|
|
1120
1160
|
* explicit not-applied answer stays unknown, because the alternative is telling
|
|
1121
1161
|
* an author the scene is clean when nobody checked. */
|
|
1122
|
-
|
|
1162
|
+
function stopOutcome(result) {
|
|
1123
1163
|
const outcome = result?.outcome;
|
|
1124
1164
|
if (outcome?.status === "already_applied") return null;
|
|
1125
1165
|
if (outcome && outcome.mutated === false) return { status: "not_applied", code: outcome.code ?? "CANCELLED", mutated: false };
|
|
@@ -1171,6 +1211,9 @@ export function createAgentChatStore({
|
|
|
1171
1211
|
// turnId, or the hex id the dock's transport mints. It is what the steer
|
|
1172
1212
|
// route is keyed by, and only the transport can tell us which it is.
|
|
1173
1213
|
let wireTurnId = null;
|
|
1214
|
+
// Supervisor note requests still waiting on the sidecar; a cleared or
|
|
1215
|
+
// replaced conversation abandons them.
|
|
1216
|
+
const supervisorRequests = new Set();
|
|
1174
1217
|
let state = {
|
|
1175
1218
|
sessionId: newId(),
|
|
1176
1219
|
turnId: null,
|
|
@@ -1262,8 +1305,29 @@ export function createAgentChatStore({
|
|
|
1262
1305
|
try { onReceipt?.(receipt); } catch { /* host presentation is advisory */ }
|
|
1263
1306
|
}
|
|
1264
1307
|
|
|
1308
|
+
// One request per reviewable Studio turn, after its stream ended: the note
|
|
1309
|
+
// joins the transcript whenever it arrives, and nothing waits for it.
|
|
1310
|
+
function requestSupervisorNote(turnId) {
|
|
1311
|
+
if (typeof transport.supervisor !== "function") return;
|
|
1312
|
+
const request = new AbortController();
|
|
1313
|
+
supervisorRequests.add(request);
|
|
1314
|
+
Promise.resolve()
|
|
1315
|
+
.then(() => transport.supervisor(turnId, request.signal))
|
|
1316
|
+
.then((note) => {
|
|
1317
|
+
if (!note || request.signal.aborted) return;
|
|
1318
|
+
setItems((items) => [...items, { ...note, kind: "supervisor", id: `supervisor:${turnId}` }]);
|
|
1319
|
+
})
|
|
1320
|
+
.catch(() => { /* a missing note is no note */ })
|
|
1321
|
+
.finally(() => supervisorRequests.delete(request));
|
|
1322
|
+
}
|
|
1323
|
+
const abandonSupervisorNotes = () => { for (const request of supervisorRequests) request.abort(); supervisorRequests.clear(); };
|
|
1324
|
+
|
|
1265
1325
|
function applyEvent(event) {
|
|
1266
1326
|
if (activeTurn && ["text.delta", "tool.start", "tool.done", "image", "job.state", "job.progress", "receipt", "error"].includes(event?.type)) activeTurn.produced = true;
|
|
1327
|
+
// The supervisor's own trigger, read from the same frames: an authored
|
|
1328
|
+
// result or a refusal makes the turn reviewable.
|
|
1329
|
+
if (activeTurn && ((event?.type === "tool.done" && (event.ok ? event.result?.authored === true : SUPERVISOR_REFUSAL.test(String(event.error ?? ""))))
|
|
1330
|
+
|| (event?.type === "receipt" && event.receipt?.authored === true))) activeTurn.reviewable = true;
|
|
1267
1331
|
if (event?.type === "text.delta") {
|
|
1268
1332
|
setItems((items) => {
|
|
1269
1333
|
const last = items[items.length - 1];
|
|
@@ -1341,6 +1405,7 @@ export function createAgentChatStore({
|
|
|
1341
1405
|
}
|
|
1342
1406
|
|
|
1343
1407
|
const resetSession = () => {
|
|
1408
|
+
abandonSupervisorNotes();
|
|
1344
1409
|
seenReceipts.clear();
|
|
1345
1410
|
settledActions.clear();
|
|
1346
1411
|
// Clearing the transcript also retires the server session: a cleared chat
|
|
@@ -1348,6 +1413,7 @@ export function createAgentChatStore({
|
|
|
1348
1413
|
set({ sessionId: newId(), turnId: null, items: [], pendingAttachments: [], rateLimit: null, lastPrompt: "", turnStartedAt: null, lastTurn: null });
|
|
1349
1414
|
};
|
|
1350
1415
|
const restore = (transcript, sessionId = state.sessionId) => {
|
|
1416
|
+
abandonSupervisorNotes();
|
|
1351
1417
|
seenReceipts.clear();
|
|
1352
1418
|
settledActions.clear();
|
|
1353
1419
|
const restoredReceiptIds = new Set();
|
|
@@ -1404,7 +1470,7 @@ export function createAgentChatStore({
|
|
|
1404
1470
|
return;
|
|
1405
1471
|
}
|
|
1406
1472
|
const startedAt = clock();
|
|
1407
|
-
const turn = { produced: false, failure: null };
|
|
1473
|
+
const turn = { produced: false, failure: null, reviewable: false };
|
|
1408
1474
|
activeTurn = turn;
|
|
1409
1475
|
// What the author attached to THIS message: an explicit list wins, the
|
|
1410
1476
|
// composer's pending pictures otherwise. Either way the composer is
|
|
@@ -1435,6 +1501,7 @@ export function createAgentChatStore({
|
|
|
1435
1501
|
attachFrame: Boolean(options.attachFrame),
|
|
1436
1502
|
model: options.model,
|
|
1437
1503
|
effort: options.effort ?? undefined,
|
|
1504
|
+
...(options.supervisor === false ? { supervisor: false } : {}),
|
|
1438
1505
|
}, applyEvent, signal);
|
|
1439
1506
|
} catch (error) {
|
|
1440
1507
|
if (!signal.aborted) applyEvent({ type: "error", code: "upstream", message: String(error?.message || error) });
|
|
@@ -1460,6 +1527,7 @@ export function createAgentChatStore({
|
|
|
1460
1527
|
failure: turn.failure,
|
|
1461
1528
|
},
|
|
1462
1529
|
});
|
|
1530
|
+
if (studio && turn.reviewable && options.supervisor !== false && !signal.aborted) requestSupervisorNote(turnId);
|
|
1463
1531
|
}
|
|
1464
1532
|
}
|
|
1465
1533
|
},
|
|
@@ -744,6 +744,16 @@
|
|
|
744
744
|
|
|
745
745
|
.agent-receipt-summary { line-height: 1.5; word-break: break-word; }
|
|
746
746
|
|
|
747
|
+
/* The supervisor note (#728) reads quieter than the turn it reviews. */
|
|
748
|
+
.agent-supervisor-card { display: flex; flex-direction: column; gap: var(--agent-space-2); padding: var(--agent-space-3) var(--agent-space-4); border-style: dashed; background: var(--agent-bg-sunken); color: var(--agent-text-muted); font-size: var(--agent-font-md); }
|
|
749
|
+
.agent-supervisor-head { display: flex; align-items: center; gap: var(--agent-space-2); }
|
|
750
|
+
.agent-supervisor-toggle { margin-left: auto; }
|
|
751
|
+
.agent-supervisor-summary { margin: 0; line-height: 1.5; word-break: break-word; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
|
|
752
|
+
.agent-supervisor-card[data-open="true"] .agent-supervisor-summary { display: block; }
|
|
753
|
+
.agent-supervisor-badge.blocker { border-color: var(--agent-alert-edge); background: var(--agent-alert-bg); color: var(--agent-alert); }
|
|
754
|
+
.agent-supervisor-fix { display: block; margin-top: var(--agent-space-1); font: var(--agent-font-sm) ui-monospace, monospace; white-space: pre-wrap; word-break: break-all; color: var(--agent-text-dim); }
|
|
755
|
+
.agent-supervisor-badge.note { border-color: var(--agent-border-strong); background: transparent; color: var(--agent-text-dim); }
|
|
756
|
+
|
|
747
757
|
.agent-failure-card { border-color: #6d3a52; background: #2a1c26; }
|
|
748
758
|
.agent-failure-code {
|
|
749
759
|
color: var(--agent-alert);
|
|
@@ -1097,6 +1107,16 @@
|
|
|
1097
1107
|
|
|
1098
1108
|
.agent-model-select option:disabled { color: var(--agent-text-dim); }
|
|
1099
1109
|
|
|
1110
|
+
/* The selected model's derived roles (#717): one muted, read-only line. */
|
|
1111
|
+
.agent-model-roles {
|
|
1112
|
+
margin: var(--agent-space-1) 0 0;
|
|
1113
|
+
overflow: hidden;
|
|
1114
|
+
white-space: nowrap;
|
|
1115
|
+
text-overflow: ellipsis;
|
|
1116
|
+
color: var(--agent-text-dim);
|
|
1117
|
+
font-size: var(--agent-font-sm);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1100
1120
|
.agent-composer-picks { margin-bottom: var(--agent-space-2); }
|
|
1101
1121
|
.agent-effort-select { flex: 0 0 auto; width: auto; max-width: 42%; }
|
|
1102
1122
|
.agent-composer-spacer { flex: 1; }
|
|
@@ -1171,3 +1191,18 @@
|
|
|
1171
1191
|
.agent-resize { display: none; }
|
|
1172
1192
|
}
|
|
1173
1193
|
}
|
|
1194
|
+
|
|
1195
|
+
/* The supervisor switch beside the model picks (#728). */
|
|
1196
|
+
.agent-supervisor-toggle {
|
|
1197
|
+
flex: 0 0 auto;
|
|
1198
|
+
font: inherit;
|
|
1199
|
+
font-size: 12px;
|
|
1200
|
+
padding: 4px 8px;
|
|
1201
|
+
border-radius: 6px;
|
|
1202
|
+
border: 1px solid var(--agent-border, #444);
|
|
1203
|
+
background: transparent;
|
|
1204
|
+
color: var(--agent-text-dim, #999);
|
|
1205
|
+
cursor: pointer;
|
|
1206
|
+
}
|
|
1207
|
+
.agent-supervisor-toggle.is-on { color: var(--agent-text, #ddd); border-color: var(--agent-border-focus, #6a9); }
|
|
1208
|
+
.agent-supervisor-toggle:focus-visible { outline: 2px solid var(--agent-border-focus, #6a9); outline-offset: 1px; }
|
|
@@ -17,7 +17,7 @@ export const ATTACHMENT_MAX_BYTES = 4 * 1024 * 1024;
|
|
|
17
17
|
export const ATTACHMENT_JPEG_QUALITY = 0.85;
|
|
18
18
|
/** The turn envelope accepts exactly these three, so the composer refuses the
|
|
19
19
|
* rest here rather than letting the sidecar reject a turn the author sent. */
|
|
20
|
-
|
|
20
|
+
const ATTACHMENT_TYPES = Object.freeze(["image/png", "image/jpeg", "image/webp"]);
|
|
21
21
|
export const ATTACHMENT_LIMIT_NOTICE = "Up to 4 images per message";
|
|
22
22
|
export const ATTACHMENT_TYPE_NOTICE = "Attach a PNG, JPEG or WebP image";
|
|
23
23
|
export const ATTACHMENT_FAILED_NOTICE = "That image could not be attached";
|
|
@@ -49,10 +49,10 @@ export function createCanvasCommands({ store, makeNode, nodeSchemas }) {
|
|
|
49
49
|
const handlers = {
|
|
50
50
|
get_graph: () => { const graph = clone(current()); return { ...graph, nodes: graph.nodes.map((node) => ({ id: node.id, type: node.type, model: node.data?.model || null, data: node.data, position: node.position })), outputs: Object.fromEntries(graph.nodes.map((node) => [node.id, node.data?.outputs || []])) }; },
|
|
51
51
|
add_node: ({ type, model, data = {}, position } = {}) => { const next = mutate((graph) => {
|
|
52
|
-
if (
|
|
52
|
+
if (!["text", "image", "video", "audio", "api", "video-combiner", "scene", "upload", "concat", "motion-input", "shot-prompt"].includes(type)) throw new Error("Unknown node type");
|
|
53
53
|
if (position && (!Number.isFinite(position.x) || !Number.isFinite(position.y))) throw new Error("position must contain finite x and y");
|
|
54
54
|
const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
55
|
-
const modelValue =
|
|
55
|
+
const modelValue = model;
|
|
56
56
|
const node = makeNode(type, id, position || nextFreePosition(graph), modelValue || null, nodeSchemas);
|
|
57
57
|
if (modelValue && typeof modelValue === "string") {
|
|
58
58
|
const models = nodeSchemas?.categories?.[schemaCategoryForType(type)]?.models || {};
|
|
@@ -71,5 +71,5 @@ export function createCanvasCommands({ store, makeNode, nodeSchemas }) {
|
|
|
71
71
|
undo: () => { const previous = history.pop(); if (!previous) return { undone: false }; publish(previous); return { undone: true, graph: clone(previous) }; },
|
|
72
72
|
history,
|
|
73
73
|
};
|
|
74
|
-
return { ...handlers, handlers
|
|
74
|
+
return { ...handlers, handlers };
|
|
75
75
|
}
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export const COZY_SCENE_NODE_TYPE = "cozyclay-scene";
|
|
11
|
-
|
|
11
|
+
const COZY_SCENE_NODE_VERSION = 1;
|
|
12
12
|
|
|
13
13
|
export const COZY_SCENE_INPUTS = Object.freeze([
|
|
14
14
|
{ id: "asset", label: "Asset", kind: "asset" },
|
|
15
15
|
{ id: "motion", label: "Motion", kind: "motion", legacy: true },
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const COZY_SCENE_CHARACTER_HANDLE_PREFIX = "character:";
|
|
19
19
|
|
|
20
20
|
/** Return the stable Scene target handle for a cast member. */
|
|
21
21
|
export function sceneCharacterHandle(characterId) {
|
|
@@ -46,7 +46,6 @@ export function sceneInputSpecs(characters = [], { includeLegacyMotion = true }
|
|
|
46
46
|
return specs;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** Build the Scene input state represented by ReactFlow edges. */
|
|
50
49
|
/** Restrict Scene inputs to values the Scene can actually consume. */
|
|
51
50
|
export function sceneConnectionAllowed(source, targetHandle, sourceData = {}) {
|
|
52
51
|
const type = typeof source === "string" ? source : source?.type;
|
|
@@ -56,6 +55,7 @@ export function sceneConnectionAllowed(source, targetHandle, sourceData = {}) {
|
|
|
56
55
|
return false;
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
/** Build the Scene input state represented by ReactFlow edges. */
|
|
59
59
|
export function sceneInputsFromEdges(edges, sceneNodeId) {
|
|
60
60
|
const assetInputs = [];
|
|
61
61
|
const motionInputs = [];
|
|
@@ -16,13 +16,13 @@ export function executeLocalWorkflowGraph(graph, { runId = `local-${Date.now()}`
|
|
|
16
16
|
const updates = new Map();
|
|
17
17
|
const order = [];
|
|
18
18
|
while (remaining.size) {
|
|
19
|
-
const ready = [...remaining].filter((id) =>
|
|
19
|
+
const ready = [...remaining].filter((id) => incoming.get(id).every((edge) => !remaining.has(edge.source)));
|
|
20
20
|
const batch = ready.length ? ready : [...remaining];
|
|
21
21
|
for (const id of batch) { remaining.delete(id); order.push(id); }
|
|
22
22
|
}
|
|
23
23
|
for (const id of order) {
|
|
24
24
|
const node = nodeById.get(id);
|
|
25
|
-
const inputs =
|
|
25
|
+
const inputs = incoming.get(id).map((edge) => values.get(edge.source)).filter((value) => value !== undefined);
|
|
26
26
|
const output = localNodeOutput(node, inputs);
|
|
27
27
|
values.set(id, output);
|
|
28
28
|
const run = { id: runId, status: "complete", result: { outputs: [{ value: output }] } };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SCENES_STORAGE_KEY, readSceneDocument } from "../scenes.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const MOTION_INPUT_NODE_TYPE = "motion-input";
|
|
4
|
+
const MOTION_INPUT_NODE_VERSION = 1;
|
|
5
5
|
|
|
6
6
|
function text(value, fallback = "") {
|
|
7
7
|
return typeof value === "string" ? value : fallback;
|
|
@@ -69,7 +69,7 @@ export function characterMotionPatch(characterId, input = {}) {
|
|
|
69
69
|
return {
|
|
70
70
|
characterId: data.characterId || null,
|
|
71
71
|
motionRef: {
|
|
72
|
-
...(data.motionRef
|
|
72
|
+
...(data.motionRef ? clone(data.motionRef) : {}),
|
|
73
73
|
url: data.url || data.objectUrl || null,
|
|
74
74
|
fileName: data.fileName || null,
|
|
75
75
|
mimeType: data.mimeType,
|
|
@@ -38,7 +38,7 @@ export function schemaModelEntries(nodeSchemas, category) {
|
|
|
38
38
|
return Object.entries(models).map(([id, model]) => ({ ...model, id, name: model.name || id.replace(/-/g, " ") }));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
function schemaForModel(nodeSchemas, category, modelId) {
|
|
42
42
|
const model = nodeSchemas?.categories?.[category]?.models?.[modelId];
|
|
43
43
|
const input = model?.input_schema || {};
|
|
44
44
|
return input?.schemas?.input_data || input;
|
|
@@ -34,13 +34,6 @@ function objectHasAsset(object, assetId) {
|
|
|
34
34
|
return [object.assetId, object.sourceAssetId, object.matteAssetId].includes(assetId);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** Return the document's active scene without exposing a mutable reference. */
|
|
38
|
-
export function activeSceneRecord(document) {
|
|
39
|
-
const scenes = Array.isArray(document?.scenes) ? document.scenes : [];
|
|
40
|
-
const index = activeSceneIndex(scenes, document?.activeSceneId);
|
|
41
|
-
return index < 0 ? null : scenes[index] ?? null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
37
|
/**
|
|
45
38
|
* Add an imported image as a cutout in the active scene.
|
|
46
39
|
*
|
|
@@ -116,7 +109,7 @@ export function readStoredSceneDocument(storage = globalThis.localStorage) {
|
|
|
116
109
|
}
|
|
117
110
|
|
|
118
111
|
/** Persist a scene envelope and notify both local and remote Studio tabs. */
|
|
119
|
-
|
|
112
|
+
function publishSceneDocument(document, {
|
|
120
113
|
storage = globalThis.localStorage,
|
|
121
114
|
target = globalThis,
|
|
122
115
|
} = {}) {
|
|
@@ -204,7 +197,7 @@ export function subscribeToScenePlayback(onCommand, {
|
|
|
204
197
|
}
|
|
205
198
|
|
|
206
199
|
/** Import bytes through the canonical asset pipeline and warm the texture cache. */
|
|
207
|
-
|
|
200
|
+
async function importWorkflowImage(file, {
|
|
208
201
|
importer = importImageFile,
|
|
209
202
|
remember = rememberAsset,
|
|
210
203
|
importOptions,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the unit test share one definition of what a Shot Prompt node produces.
|
|
4
4
|
import { buildShotPrompt } from "../shot-prompt.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const SHOT_PROMPT_TARGETS = Object.freeze(["image", "video"]);
|
|
7
7
|
|
|
8
8
|
/** Defaults a fresh Shot Prompt node starts with. */
|
|
9
9
|
export function shotPromptNodeData() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fal H3 Max Turbo I2V schema: https://fal.ai/models/minimax/h3-max-turbo/image-to-video/api
|
|
2
2
|
export const DEFAULT_FAL_VIDEO_MODEL = "minimax/h3-max-turbo/image-to-video";
|
|
3
|
-
|
|
3
|
+
const COMFY_VIDEO_ASPECTS = Object.freeze(["16:9", "9:16", "1:1", "21:9", "12:7"]);
|
|
4
4
|
|
|
5
5
|
export function falVideoContract(model = DEFAULT_FAL_VIDEO_MODEL) {
|
|
6
6
|
if (model === DEFAULT_FAL_VIDEO_MODEL) return { name: "H3 Max Turbo", aspectFromImage: true, cameraLocked: true, aspects: [], minDuration: 5, maxDuration: 15, integerDuration: true, defaultDuration: 5, resolution: ["480P", "768P", "1080P"], defaultResolution: "480P" };
|
|
@@ -52,8 +52,6 @@ export function outputKind(value) {
|
|
|
52
52
|
return value.trim() ? "other" : null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/* ------------------------------------------------------------- paths ---- */
|
|
56
|
-
|
|
57
55
|
function outputPaths(data) {
|
|
58
56
|
const paths = [["resultUrl"], ["videoUrl"], ["lastOutput", "renderUrl"], ["lastOutput", "sceneUrl"], ["fileUrl"]];
|
|
59
57
|
if (Array.isArray(data.versions)) data.versions.forEach((_, index) => paths.push(["versions", index, "dataUrl"]));
|
|
@@ -106,8 +104,6 @@ async function mapOutputs(graph, visit) {
|
|
|
106
104
|
return { ...graph, nodes };
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
/* -------------------------------------------------------------- refs ---- */
|
|
110
|
-
|
|
111
107
|
/**
|
|
112
108
|
* Every output slot that holds something, with where it lives and what it is.
|
|
113
109
|
* `field` reads like a path (`versions[2].dataUrl`) so a manifest row can point
|
|
@@ -126,8 +122,6 @@ export function workflowOutputRefs(graph) {
|
|
|
126
122
|
return refs;
|
|
127
123
|
}
|
|
128
124
|
|
|
129
|
-
/* ----------------------------------------------------------- data URLs -- */
|
|
130
|
-
|
|
131
125
|
function bytesToBase64(bytes) {
|
|
132
126
|
let binary = "";
|
|
133
127
|
for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
@@ -196,8 +190,6 @@ export function imageDimensions(bytes) {
|
|
|
196
190
|
return null;
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
/* --------------------------------------------------------- intern/resolve */
|
|
200
|
-
|
|
201
193
|
/**
|
|
202
194
|
* Save path. Every `data:image/*` output becomes `{ assetRef }` and the bytes
|
|
203
195
|
* come back once per distinct picture as an asset record in the shape the
|