inline-core 1.1.1__py3-none-any.whl
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.
- inline_core/__init__.py +7 -0
- inline_core/components/__init__.py +1 -0
- inline_core/components/conditioning.py +20 -0
- inline_core/components/interfaces.py +91 -0
- inline_core/config.py +33 -0
- inline_core/device/__init__.py +1 -0
- inline_core/device/auto.py +35 -0
- inline_core/device/detect.py +41 -0
- inline_core/device/memory.py +168 -0
- inline_core/device/policy.py +82 -0
- inline_core/device/types.py +31 -0
- inline_core/errors.py +42 -0
- inline_core/graph/__init__.py +1 -0
- inline_core/graph/cache.py +83 -0
- inline_core/graph/descriptor.py +81 -0
- inline_core/graph/executor.py +102 -0
- inline_core/graph/primitives.py +137 -0
- inline_core/graph/registry.py +61 -0
- inline_core/graph/runners.py +72 -0
- inline_core/graph/schema.py +136 -0
- inline_core/graph/topo.py +49 -0
- inline_core/graph/validate.py +56 -0
- inline_core/importer/__init__.py +1 -0
- inline_core/importer/comfy.py +162 -0
- inline_core/media.py +11 -0
- inline_core/models/__init__.py +8 -0
- inline_core/models/catalog.py +98 -0
- inline_core/models/zimage/__init__.py +11 -0
- inline_core/models/zimage/requirements.py +180 -0
- inline_core/models/zimage/runner.py +386 -0
- inline_core/parallel/__init__.py +13 -0
- inline_core/parallel/config.py +50 -0
- inline_core/parallel/group.py +136 -0
- inline_core/parallel/launch.py +44 -0
- inline_core/parallel/protocol.py +53 -0
- inline_core/parallel/registry.py +31 -0
- inline_core/parallel/worker.py +75 -0
- inline_core/runtime/__init__.py +1 -0
- inline_core/runtime/context.py +31 -0
- inline_core/runtime/file_store.py +51 -0
- inline_core/runtime/progress.py +83 -0
- inline_core/runtime/run.py +113 -0
- inline_core/runtime/store.py +18 -0
- inline_core/sampling/__init__.py +1 -0
- inline_core/sampling/batch.py +116 -0
- inline_core/server/__init__.py +1 -0
- inline_core/server/__main__.py +61 -0
- inline_core/server/app.py +327 -0
- inline_core/server/assets.py +47 -0
- inline_core/server/bootstrap.py +21 -0
- inline_core/server/frontend.py +37 -0
- inline_core/server/manager.py +195 -0
- inline_core/server/rpc.py +60 -0
- inline_core/server/run_store.py +155 -0
- inline_core/server/serialize.py +144 -0
- inline_core/studio/__init__.py +7 -0
- inline_core/studio/assets.py +194 -0
- inline_core/studio/config.py +29 -0
- inline_core/studio/fal.py +237 -0
- inline_core/studio/frames.py +552 -0
- inline_core/studio/generation.py +136 -0
- inline_core/studio/graph_build.py +109 -0
- inline_core/studio/handlers.py +236 -0
- inline_core/studio/models.py +173 -0
- inline_core/studio/moodboard.py +400 -0
- inline_core/studio/schema.py +278 -0
- inline_core/studio/store.py +291 -0
- inline_core/studio/timeline/__init__.py +6 -0
- inline_core/studio/timeline/compose.py +130 -0
- inline_core/studio/timeline/ffmpeg.py +76 -0
- inline_core/studio/timeline/render.py +120 -0
- inline_core/studio/timeline/resolve.py +189 -0
- inline_core/takes.py +31 -0
- inline_core-1.1.1.dist-info/METADATA +35 -0
- inline_core-1.1.1.dist-info/RECORD +77 -0
- inline_core-1.1.1.dist-info/WHEEL +4 -0
- inline_core-1.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""Frames + takes + frame-inputs, ported from the Studio ``electron/main/frames/store.ts``.
|
|
2
|
+
|
|
3
|
+
A frame is the atomic unit: an optional input asset plus a history of generated takes; its hero take
|
|
4
|
+
is the Output that flows downstream. All frames live in a single auto-created default sequence.
|
|
5
|
+
|
|
6
|
+
Functions operate on an open ``sqlite3.Connection`` (the project.db Core owns). Fal creation
|
|
7
|
+
(``create_fal_frame`` / ``set_model`` / ``set_provider``) takes the model's ``kind`` + default
|
|
8
|
+
``params`` from the caller: the fal node definitions live studio-side (TypeScript ``src/shared/``)
|
|
9
|
+
and are never duplicated here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sqlite3
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_FRAME_COLUMNS = (
|
|
21
|
+
"id, sequence_id, name, kind, position, input_asset_id, hero_take_id, provider, model_id, "
|
|
22
|
+
"params, workflow_template_id, comfy_workflow_name, comfy_workflow_ready, created_at, "
|
|
23
|
+
"updated_at"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _now() -> int:
|
|
28
|
+
return int(time.time() * 1000)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _uuid() -> str:
|
|
32
|
+
return str(uuid.uuid4())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_params(raw: str | None) -> dict[str, Any]:
|
|
36
|
+
if not raw:
|
|
37
|
+
return {}
|
|
38
|
+
try:
|
|
39
|
+
parsed = json.loads(raw)
|
|
40
|
+
except (json.JSONDecodeError, TypeError):
|
|
41
|
+
return {}
|
|
42
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _row_to_frame(row: sqlite3.Row) -> dict[str, Any]:
|
|
46
|
+
provider = row["provider"]
|
|
47
|
+
provider = provider if provider in ("fal", "unset", "core") else "comfy"
|
|
48
|
+
return {
|
|
49
|
+
"id": row["id"],
|
|
50
|
+
"sequenceId": row["sequence_id"],
|
|
51
|
+
"name": row["name"],
|
|
52
|
+
"kind": row["kind"],
|
|
53
|
+
"position": row["position"],
|
|
54
|
+
"inputAssetId": row["input_asset_id"],
|
|
55
|
+
"heroTakeId": row["hero_take_id"],
|
|
56
|
+
"provider": provider,
|
|
57
|
+
"modelId": row["model_id"],
|
|
58
|
+
"params": _parse_params(row["params"]),
|
|
59
|
+
"workflowTemplateId": row["workflow_template_id"],
|
|
60
|
+
"comfyWorkflowName": row["comfy_workflow_name"],
|
|
61
|
+
"comfyWorkflowReady": row["comfy_workflow_ready"] == 1,
|
|
62
|
+
"createdAt": row["created_at"],
|
|
63
|
+
"updatedAt": row["updated_at"],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _row_to_take(row: sqlite3.Row) -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"id": row["id"],
|
|
70
|
+
"frameId": row["frame_id"],
|
|
71
|
+
"filePath": row["file_path"],
|
|
72
|
+
"kind": row["kind"],
|
|
73
|
+
"params": _parse_params(row["params"]),
|
|
74
|
+
"comfyPromptId": row["comfy_prompt_id"],
|
|
75
|
+
"createdAt": row["created_at"],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _row_to_input(row: sqlite3.Row) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"id": row["id"],
|
|
82
|
+
"frameId": row["frame_id"],
|
|
83
|
+
"assetId": row["asset_id"],
|
|
84
|
+
"sourceFrameId": row["source_frame_id"],
|
|
85
|
+
"position": row["position"],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _project_id(conn: sqlite3.Connection) -> str:
|
|
90
|
+
row = conn.execute("SELECT id FROM project LIMIT 1").fetchone()
|
|
91
|
+
if row is None:
|
|
92
|
+
raise RuntimeError("No project is open.")
|
|
93
|
+
return row["id"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_sequence_id(conn: sqlite3.Connection) -> str:
|
|
97
|
+
"""The single default sequence frames are created in; created on first use."""
|
|
98
|
+
row = conn.execute("SELECT id FROM sequences ORDER BY position LIMIT 1").fetchone()
|
|
99
|
+
if row is not None:
|
|
100
|
+
return row["id"]
|
|
101
|
+
seq_id = _uuid()
|
|
102
|
+
conn.execute(
|
|
103
|
+
"INSERT INTO sequences (id, project_id, name, position) VALUES (?, ?, ?, 0)",
|
|
104
|
+
(seq_id, _project_id(conn), "Main"),
|
|
105
|
+
)
|
|
106
|
+
return seq_id
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_frame(conn: sqlite3.Connection, frame_id: str) -> dict[str, Any]:
|
|
110
|
+
row = conn.execute("SELECT * FROM frames WHERE id = ?", (frame_id,)).fetchone()
|
|
111
|
+
if row is None:
|
|
112
|
+
raise ValueError("Frame not found.")
|
|
113
|
+
return _row_to_frame(row)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_frames(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
117
|
+
seq_id = default_sequence_id(conn)
|
|
118
|
+
rows = conn.execute(
|
|
119
|
+
"SELECT * FROM frames WHERE sequence_id = ? ORDER BY position", (seq_id,)
|
|
120
|
+
).fetchall()
|
|
121
|
+
return [_row_to_frame(r) for r in rows]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _frame_count(conn: sqlite3.Connection, seq_id: str) -> int:
|
|
125
|
+
return conn.execute(
|
|
126
|
+
"SELECT COUNT(*) AS n FROM frames WHERE sequence_id = ?", (seq_id,)
|
|
127
|
+
).fetchone()["n"]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _insert_frame(conn: sqlite3.Connection, frame: dict[str, Any]) -> None:
|
|
131
|
+
conn.execute(
|
|
132
|
+
f"INSERT INTO frames ({_FRAME_COLUMNS}) VALUES "
|
|
133
|
+
"(:id, :sequence_id, :name, :kind, :position, :input_asset_id, :hero_take_id, :provider, "
|
|
134
|
+
":model_id, :params, :workflow_template_id, :comfy_workflow_name, :comfy_workflow_ready, "
|
|
135
|
+
":created_at, :updated_at)",
|
|
136
|
+
frame,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _new_frame_row(
|
|
141
|
+
conn: sqlite3.Connection,
|
|
142
|
+
*,
|
|
143
|
+
kind: str,
|
|
144
|
+
input_asset_id: str | None,
|
|
145
|
+
provider: str,
|
|
146
|
+
model_id: str | None = None,
|
|
147
|
+
params: dict[str, Any] | None = None,
|
|
148
|
+
name: str | None = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
seq_id = default_sequence_id(conn)
|
|
151
|
+
position = _frame_count(conn, seq_id)
|
|
152
|
+
now = _now()
|
|
153
|
+
return {
|
|
154
|
+
"id": _uuid(),
|
|
155
|
+
"sequence_id": seq_id,
|
|
156
|
+
"name": name if name is not None else str(position + 1),
|
|
157
|
+
"kind": kind,
|
|
158
|
+
"position": position,
|
|
159
|
+
"input_asset_id": input_asset_id,
|
|
160
|
+
"hero_take_id": None,
|
|
161
|
+
"provider": provider,
|
|
162
|
+
"model_id": model_id,
|
|
163
|
+
"params": json.dumps(params or {}),
|
|
164
|
+
"workflow_template_id": None,
|
|
165
|
+
"comfy_workflow_name": None,
|
|
166
|
+
"comfy_workflow_ready": 0,
|
|
167
|
+
"created_at": now,
|
|
168
|
+
"updated_at": now,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def add_from_asset(conn: sqlite3.Connection, asset_id: str) -> dict[str, Any]:
|
|
173
|
+
asset = conn.execute("SELECT id, kind FROM assets WHERE id = ?", (asset_id,)).fetchone()
|
|
174
|
+
if asset is None:
|
|
175
|
+
raise ValueError("Asset not found.")
|
|
176
|
+
frame = _new_frame_row(conn, kind=asset["kind"], input_asset_id=asset_id, provider="unset")
|
|
177
|
+
_insert_frame(conn, frame)
|
|
178
|
+
conn.execute(
|
|
179
|
+
"INSERT INTO frame_inputs (id, frame_id, asset_id, position) VALUES (?, ?, ?, 0)",
|
|
180
|
+
(_uuid(), frame["id"], asset_id),
|
|
181
|
+
)
|
|
182
|
+
return get_frame(conn, frame["id"])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_empty_frame(conn: sqlite3.Connection) -> dict[str, Any]:
|
|
186
|
+
frame = _new_frame_row(conn, kind="image", input_asset_id=None, provider="unset")
|
|
187
|
+
_insert_frame(conn, frame)
|
|
188
|
+
return get_frame(conn, frame["id"])
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def create_fal_frame(
|
|
192
|
+
conn: sqlite3.Connection, model_id: str, kind: str, params: dict[str, Any], title: str
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""Create a fal generation frame. ``kind``/``params``/``title`` come from the node def."""
|
|
195
|
+
frame = _new_frame_row(
|
|
196
|
+
conn, kind=kind, input_asset_id=None, provider="fal", model_id=model_id, params=params,
|
|
197
|
+
name=title,
|
|
198
|
+
)
|
|
199
|
+
_insert_frame(conn, frame)
|
|
200
|
+
return get_frame(conn, frame["id"])
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def set_model(
|
|
204
|
+
conn: sqlite3.Connection, frame_id: str, model_id: str, kind: str, params: dict[str, Any]
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
get_frame(conn, frame_id)
|
|
207
|
+
conn.execute(
|
|
208
|
+
"UPDATE frames SET model_id = ?, params = ?, kind = ?, updated_at = ? WHERE id = ?",
|
|
209
|
+
(model_id, json.dumps(params), kind, _now(), frame_id),
|
|
210
|
+
)
|
|
211
|
+
return get_frame(conn, frame_id)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def set_provider(
|
|
215
|
+
conn: sqlite3.Connection,
|
|
216
|
+
frame_id: str,
|
|
217
|
+
provider: str,
|
|
218
|
+
model_id: str | None = None,
|
|
219
|
+
kind: str | None = None,
|
|
220
|
+
params: dict[str, Any] | None = None,
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
get_frame(conn, frame_id)
|
|
223
|
+
now = _now()
|
|
224
|
+
if provider == "fal":
|
|
225
|
+
if not model_id or kind is None:
|
|
226
|
+
raise ValueError("A fal provider needs a model id + kind from the node def.")
|
|
227
|
+
conn.execute(
|
|
228
|
+
"UPDATE frames SET provider = 'fal', model_id = ?, params = ?, kind = ?, "
|
|
229
|
+
"updated_at = ? WHERE id = ?",
|
|
230
|
+
(model_id, json.dumps(params or {}), kind, now, frame_id),
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
conn.execute(
|
|
234
|
+
"UPDATE frames SET provider = 'comfy', model_id = NULL, updated_at = ? WHERE id = ?",
|
|
235
|
+
(now, frame_id),
|
|
236
|
+
)
|
|
237
|
+
return get_frame(conn, frame_id)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def unlink_workflow(conn: sqlite3.Connection, frame_id: str) -> dict[str, Any]:
|
|
241
|
+
"""Detach a frame's ComfyUI workflow link (desktop legacy; a reset on the web path)."""
|
|
242
|
+
get_frame(conn, frame_id)
|
|
243
|
+
conn.execute(
|
|
244
|
+
"UPDATE frames SET comfy_workflow_name = NULL, comfy_workflow_ready = 0, "
|
|
245
|
+
"updated_at = ? WHERE id = ?",
|
|
246
|
+
(_now(), frame_id),
|
|
247
|
+
)
|
|
248
|
+
return get_frame(conn, frame_id)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def set_fal_params(
|
|
252
|
+
conn: sqlite3.Connection, frame_id: str, params: dict[str, Any]
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
get_frame(conn, frame_id)
|
|
255
|
+
conn.execute(
|
|
256
|
+
"UPDATE frames SET params = ?, updated_at = ? WHERE id = ?",
|
|
257
|
+
(json.dumps(params or {}), _now(), frame_id),
|
|
258
|
+
)
|
|
259
|
+
return get_frame(conn, frame_id)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def rename_frame(conn: sqlite3.Connection, frame_id: str, name: str) -> dict[str, Any]:
|
|
263
|
+
trimmed = name.strip()
|
|
264
|
+
if not trimmed:
|
|
265
|
+
raise ValueError("Frame name is required.")
|
|
266
|
+
get_frame(conn, frame_id)
|
|
267
|
+
conn.execute(
|
|
268
|
+
"UPDATE frames SET name = ?, updated_at = ? WHERE id = ?", (trimmed, _now(), frame_id)
|
|
269
|
+
)
|
|
270
|
+
return get_frame(conn, frame_id)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def reorder_frames(conn: sqlite3.Connection, ordered_ids: list[str]) -> None:
|
|
274
|
+
now = _now()
|
|
275
|
+
conn.executemany(
|
|
276
|
+
"UPDATE frames SET position = ?, updated_at = ? WHERE id = ?",
|
|
277
|
+
[(i, now, fid) for i, fid in enumerate(ordered_ids)],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def delete_frame(conn: sqlite3.Connection, frame_id: str) -> None:
|
|
282
|
+
conn.execute("DELETE FROM takes WHERE frame_id = ?", (frame_id,))
|
|
283
|
+
conn.execute("DELETE FROM frame_inputs WHERE frame_id = ?", (frame_id,))
|
|
284
|
+
items = conn.execute(
|
|
285
|
+
"SELECT id FROM moodboard_items WHERE frame_id = ? AND type = 'frame'", (frame_id,)
|
|
286
|
+
).fetchall()
|
|
287
|
+
for item in items:
|
|
288
|
+
conn.execute(
|
|
289
|
+
"DELETE FROM moodboard_connectors WHERE from_item_id = ? OR to_item_id = ?",
|
|
290
|
+
(item["id"], item["id"]),
|
|
291
|
+
)
|
|
292
|
+
conn.execute("DELETE FROM moodboard_items WHERE frame_id = ? AND type = 'frame'", (frame_id,))
|
|
293
|
+
conn.execute("DELETE FROM frames WHERE id = ?", (frame_id,))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def clone_frame(conn: sqlite3.Connection, frame_id: str) -> dict[str, Any]:
|
|
297
|
+
src = get_frame(conn, frame_id)
|
|
298
|
+
clone = _new_frame_row(
|
|
299
|
+
conn,
|
|
300
|
+
kind=src["kind"],
|
|
301
|
+
input_asset_id=src["inputAssetId"],
|
|
302
|
+
provider=src["provider"],
|
|
303
|
+
model_id=src["modelId"],
|
|
304
|
+
params=src["params"],
|
|
305
|
+
name=f"{src['name']} copy",
|
|
306
|
+
)
|
|
307
|
+
_insert_frame(conn, clone)
|
|
308
|
+
inputs = conn.execute(
|
|
309
|
+
"SELECT asset_id, source_frame_id, position FROM frame_inputs WHERE frame_id = ?",
|
|
310
|
+
(frame_id,),
|
|
311
|
+
).fetchall()
|
|
312
|
+
conn.executemany(
|
|
313
|
+
"INSERT INTO frame_inputs (id, frame_id, asset_id, source_frame_id, position) "
|
|
314
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
315
|
+
[
|
|
316
|
+
(_uuid(), clone["id"], i["asset_id"], i["source_frame_id"], i["position"])
|
|
317
|
+
for i in inputs
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
return get_frame(conn, clone["id"])
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# --- inputs -------------------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _input_rows(conn: sqlite3.Connection, frame_id: str) -> list[sqlite3.Row]:
|
|
327
|
+
return conn.execute(
|
|
328
|
+
"SELECT * FROM frame_inputs WHERE frame_id = ? ORDER BY position", (frame_id,)
|
|
329
|
+
).fetchall()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def list_inputs(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
333
|
+
rows = conn.execute("SELECT * FROM frame_inputs ORDER BY frame_id, position").fetchall()
|
|
334
|
+
return [_row_to_input(r) for r in rows]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def resolve_frame_file(
|
|
338
|
+
conn: sqlite3.Connection, frame_id: str, seen: set[str] | None = None
|
|
339
|
+
) -> dict[str, str] | None:
|
|
340
|
+
"""A frame's output file: its hero take, else newest take, else its first input (an asset, or an
|
|
341
|
+
upstream frame's output followed up the flow chain). Returns {filePath, kind} or None."""
|
|
342
|
+
seen = seen if seen is not None else set()
|
|
343
|
+
if frame_id in seen:
|
|
344
|
+
return None
|
|
345
|
+
seen.add(frame_id)
|
|
346
|
+
row = conn.execute("SELECT hero_take_id FROM frames WHERE id = ?", (frame_id,)).fetchone()
|
|
347
|
+
take = None
|
|
348
|
+
if row and row["hero_take_id"]:
|
|
349
|
+
take = conn.execute(
|
|
350
|
+
"SELECT file_path, kind FROM takes WHERE id = ?", (row["hero_take_id"],)
|
|
351
|
+
).fetchone()
|
|
352
|
+
if take is None:
|
|
353
|
+
take = conn.execute(
|
|
354
|
+
"SELECT file_path, kind FROM takes WHERE frame_id = ? "
|
|
355
|
+
"ORDER BY created_at DESC, rowid DESC LIMIT 1",
|
|
356
|
+
(frame_id,),
|
|
357
|
+
).fetchone()
|
|
358
|
+
if take is not None:
|
|
359
|
+
return {"filePath": take["file_path"], "kind": take["kind"]}
|
|
360
|
+
for inp in _input_rows(conn, frame_id):
|
|
361
|
+
if inp["asset_id"]:
|
|
362
|
+
asset = conn.execute(
|
|
363
|
+
"SELECT file_path, kind FROM assets WHERE id = ?", (inp["asset_id"],)
|
|
364
|
+
).fetchone()
|
|
365
|
+
if asset is not None:
|
|
366
|
+
return {"filePath": asset["file_path"], "kind": asset["kind"]}
|
|
367
|
+
elif inp["source_frame_id"]:
|
|
368
|
+
up = resolve_frame_file(conn, inp["source_frame_id"], seen)
|
|
369
|
+
if up is not None:
|
|
370
|
+
return up
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def frame_input_media(conn: sqlite3.Connection, frame_id: str) -> list[dict[str, str]]:
|
|
375
|
+
"""A frame's inputs resolved to {filePath, kind}, in order (skipping unresolvable ones)."""
|
|
376
|
+
out: list[dict[str, str]] = []
|
|
377
|
+
for inp in _input_rows(conn, frame_id):
|
|
378
|
+
if inp["asset_id"]:
|
|
379
|
+
asset = conn.execute(
|
|
380
|
+
"SELECT file_path, kind FROM assets WHERE id = ?", (inp["asset_id"],)
|
|
381
|
+
).fetchone()
|
|
382
|
+
if asset is not None:
|
|
383
|
+
out.append({"filePath": asset["file_path"], "kind": asset["kind"]})
|
|
384
|
+
elif inp["source_frame_id"]:
|
|
385
|
+
up = resolve_frame_file(conn, inp["source_frame_id"])
|
|
386
|
+
if up is not None:
|
|
387
|
+
out.append(up)
|
|
388
|
+
return out
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def add_input(conn: sqlite3.Connection, frame_id: str, asset_id: str) -> dict[str, Any]:
|
|
392
|
+
get_frame(conn, frame_id)
|
|
393
|
+
existing = _input_rows(conn, frame_id)
|
|
394
|
+
dup = next((r for r in existing if r["asset_id"] == asset_id), None)
|
|
395
|
+
if dup is not None:
|
|
396
|
+
return _row_to_input(dup)
|
|
397
|
+
row = {
|
|
398
|
+
"id": _uuid(),
|
|
399
|
+
"frameId": frame_id,
|
|
400
|
+
"assetId": asset_id,
|
|
401
|
+
"sourceFrameId": None,
|
|
402
|
+
"position": len(existing),
|
|
403
|
+
}
|
|
404
|
+
conn.execute(
|
|
405
|
+
"INSERT INTO frame_inputs (id, frame_id, asset_id, position) VALUES (?, ?, ?, ?)",
|
|
406
|
+
(row["id"], frame_id, asset_id, row["position"]),
|
|
407
|
+
)
|
|
408
|
+
return row
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def add_inputs(
|
|
412
|
+
conn: sqlite3.Connection, frame_id: str, asset_ids: list[str]
|
|
413
|
+
) -> list[dict[str, Any]]:
|
|
414
|
+
get_frame(conn, frame_id)
|
|
415
|
+
existing = _input_rows(conn, frame_id)
|
|
416
|
+
have = {r["asset_id"] for r in existing if r["asset_id"]}
|
|
417
|
+
added: list[dict[str, Any]] = []
|
|
418
|
+
pos = len(existing)
|
|
419
|
+
for asset_id in asset_ids:
|
|
420
|
+
if asset_id in have:
|
|
421
|
+
continue
|
|
422
|
+
have.add(asset_id)
|
|
423
|
+
row = {
|
|
424
|
+
"id": _uuid(),
|
|
425
|
+
"frameId": frame_id,
|
|
426
|
+
"assetId": asset_id,
|
|
427
|
+
"sourceFrameId": None,
|
|
428
|
+
"position": pos,
|
|
429
|
+
}
|
|
430
|
+
conn.execute(
|
|
431
|
+
"INSERT INTO frame_inputs (id, frame_id, asset_id, position) VALUES (?, ?, ?, ?)",
|
|
432
|
+
(row["id"], frame_id, asset_id, pos),
|
|
433
|
+
)
|
|
434
|
+
added.append(row)
|
|
435
|
+
pos += 1
|
|
436
|
+
return added
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def add_source_input(
|
|
440
|
+
conn: sqlite3.Connection, frame_id: str, source_frame_id: str
|
|
441
|
+
) -> dict[str, Any]:
|
|
442
|
+
get_frame(conn, frame_id)
|
|
443
|
+
get_frame(conn, source_frame_id)
|
|
444
|
+
if frame_id == source_frame_id:
|
|
445
|
+
raise ValueError("A frame cannot use its own output as input.")
|
|
446
|
+
existing = _input_rows(conn, frame_id)
|
|
447
|
+
dup = next((r for r in existing if r["source_frame_id"] == source_frame_id), None)
|
|
448
|
+
if dup is not None:
|
|
449
|
+
return _row_to_input(dup)
|
|
450
|
+
row = {
|
|
451
|
+
"id": _uuid(),
|
|
452
|
+
"frameId": frame_id,
|
|
453
|
+
"assetId": None,
|
|
454
|
+
"sourceFrameId": source_frame_id,
|
|
455
|
+
"position": len(existing),
|
|
456
|
+
}
|
|
457
|
+
conn.execute(
|
|
458
|
+
"INSERT INTO frame_inputs (id, frame_id, asset_id, source_frame_id, position) "
|
|
459
|
+
"VALUES (?, ?, NULL, ?, ?)",
|
|
460
|
+
(row["id"], frame_id, source_frame_id, row["position"]),
|
|
461
|
+
)
|
|
462
|
+
return row
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def remove_input(conn: sqlite3.Connection, frame_id: str, asset_id: str) -> None:
|
|
466
|
+
conn.execute(
|
|
467
|
+
"DELETE FROM frame_inputs WHERE frame_id = ? AND asset_id = ?", (frame_id, asset_id)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def remove_input_by_id(conn: sqlite3.Connection, frame_id: str, input_id: str) -> None:
|
|
472
|
+
conn.execute("DELETE FROM frame_inputs WHERE frame_id = ? AND id = ?", (frame_id, input_id))
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def reorder_inputs(conn: sqlite3.Connection, frame_id: str, ordered_asset_ids: list[str]) -> None:
|
|
476
|
+
conn.executemany(
|
|
477
|
+
"UPDATE frame_inputs SET position = ? WHERE frame_id = ? AND asset_id = ?",
|
|
478
|
+
[(i, frame_id, aid) for i, aid in enumerate(ordered_asset_ids)],
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# --- takes --------------------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def list_takes(conn: sqlite3.Connection, frame_id: str) -> list[dict[str, Any]]:
|
|
486
|
+
# rowid DESC breaks created_at ties deterministically (newest-inserted first).
|
|
487
|
+
rows = conn.execute(
|
|
488
|
+
"SELECT * FROM takes WHERE frame_id = ? ORDER BY created_at DESC, rowid DESC", (frame_id,)
|
|
489
|
+
).fetchall()
|
|
490
|
+
return [_row_to_take(r) for r in rows]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def list_all_takes(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
494
|
+
rows = conn.execute(
|
|
495
|
+
"SELECT * FROM takes ORDER BY frame_id, created_at DESC, rowid DESC"
|
|
496
|
+
).fetchall()
|
|
497
|
+
return [_row_to_take(r) for r in rows]
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def hero_takes(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
501
|
+
rows = conn.execute(
|
|
502
|
+
"SELECT t.* FROM takes t JOIN frames s ON s.hero_take_id = t.id"
|
|
503
|
+
).fetchall()
|
|
504
|
+
return [_row_to_take(r) for r in rows]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def set_hero(conn: sqlite3.Connection, frame_id: str, take_id: str | None) -> dict[str, Any]:
|
|
508
|
+
get_frame(conn, frame_id)
|
|
509
|
+
conn.execute(
|
|
510
|
+
"UPDATE frames SET hero_take_id = ?, updated_at = ? WHERE id = ?",
|
|
511
|
+
(take_id, _now(), frame_id),
|
|
512
|
+
)
|
|
513
|
+
return get_frame(conn, frame_id)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def add_take(
|
|
517
|
+
conn: sqlite3.Connection,
|
|
518
|
+
frame_id: str,
|
|
519
|
+
file_path: str,
|
|
520
|
+
kind: str,
|
|
521
|
+
params: dict[str, Any],
|
|
522
|
+
comfy_prompt_id: str | None = None,
|
|
523
|
+
) -> dict[str, Any]:
|
|
524
|
+
"""Insert a generated take for a frame and make it the hero (Output)."""
|
|
525
|
+
get_frame(conn, frame_id)
|
|
526
|
+
take_id = _uuid()
|
|
527
|
+
now = _now()
|
|
528
|
+
conn.execute(
|
|
529
|
+
"INSERT INTO takes (id, frame_id, file_path, kind, params, comfy_prompt_id, created_at) "
|
|
530
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
531
|
+
(take_id, frame_id, file_path, kind, json.dumps(params), comfy_prompt_id, now),
|
|
532
|
+
)
|
|
533
|
+
set_hero(conn, frame_id, take_id)
|
|
534
|
+
return {
|
|
535
|
+
"id": take_id,
|
|
536
|
+
"frameId": frame_id,
|
|
537
|
+
"filePath": file_path,
|
|
538
|
+
"kind": kind,
|
|
539
|
+
"params": params,
|
|
540
|
+
"comfyPromptId": comfy_prompt_id,
|
|
541
|
+
"createdAt": now,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def delete_take(conn: sqlite3.Connection, take_id: str) -> str | None:
|
|
546
|
+
"""Delete a take (clearing it as hero). Returns its file_path so the caller can unlink it."""
|
|
547
|
+
take = conn.execute("SELECT file_path FROM takes WHERE id = ?", (take_id,)).fetchone()
|
|
548
|
+
if take is None:
|
|
549
|
+
return None
|
|
550
|
+
conn.execute("UPDATE frames SET hero_take_id = NULL WHERE hero_take_id = ?", (take_id,))
|
|
551
|
+
conn.execute("DELETE FROM takes WHERE id = ?", (take_id,))
|
|
552
|
+
return take["file_path"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Core-node generation on the single-process path — ported from the Studio
|
|
2
|
+
``electron/main/generation/coreExecutor.ts``, but running the graph through Core's in-process
|
|
3
|
+
``RunManager`` instead of over HTTP.
|
|
4
|
+
|
|
5
|
+
``run_workflow`` serializes a canvas node's upstream closure, submits it, and drains the run's event
|
|
6
|
+
stream, translating Core run events into the Studio generation events the SPA listens for
|
|
7
|
+
(``events:generationProgress`` / ``NodeDone`` / ``Done`` / ``Error``). Each produced take is copied
|
|
8
|
+
into the project's ``takes/`` dir and stored as its Core node's output. Fire-and-forget: the call
|
|
9
|
+
returns immediately; progress arrives over ``/events``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import shutil
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from ..graph.schema import parse_graph
|
|
21
|
+
from ..runtime.progress import (
|
|
22
|
+
CancelledEvent,
|
|
23
|
+
ErrorEvent,
|
|
24
|
+
NodeDoneEvent,
|
|
25
|
+
ProgressEvent,
|
|
26
|
+
RunDoneEvent,
|
|
27
|
+
)
|
|
28
|
+
from . import moodboard as mb
|
|
29
|
+
from .graph_build import build_workflow_graph
|
|
30
|
+
|
|
31
|
+
_EXT = {"image": ".png", "video": ".mp4", "audio": ".mp3"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _kind_str(kind: Any) -> str:
|
|
35
|
+
return kind.value if hasattr(kind, "value") else str(kind)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CoreGeneration:
|
|
39
|
+
"""Drives core-node runs and streams their progress as Studio generation events."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, store: Any, manager: Any, events: Any) -> None:
|
|
42
|
+
self._store = store
|
|
43
|
+
self._manager = manager
|
|
44
|
+
self._events = events
|
|
45
|
+
self._active: dict[str, str] = {} # canvas item id -> run id
|
|
46
|
+
|
|
47
|
+
def run_workflow(self, item_id: str) -> None:
|
|
48
|
+
graph_dict, target = build_workflow_graph(
|
|
49
|
+
self._store.conn(), self._store.folder(), item_id
|
|
50
|
+
)
|
|
51
|
+
self._progress(item_id, 0.02, "Submitting")
|
|
52
|
+
record, _created = self._manager.submit(parse_graph(graph_dict), target)
|
|
53
|
+
self._active[item_id] = record.state.run_id
|
|
54
|
+
asyncio.create_task(self._drain(item_id, record))
|
|
55
|
+
|
|
56
|
+
def cancel(self, item_id: str | None = None) -> None:
|
|
57
|
+
ids = [item_id] if item_id else list(self._active.keys())
|
|
58
|
+
for iid in ids:
|
|
59
|
+
run_id = self._active.pop(iid, None)
|
|
60
|
+
if run_id:
|
|
61
|
+
self._manager.cancel(run_id)
|
|
62
|
+
|
|
63
|
+
async def _drain(self, item_id: str, record: Any) -> None:
|
|
64
|
+
queue: asyncio.Queue[Any] = asyncio.Queue()
|
|
65
|
+
record.subscribers.add(queue)
|
|
66
|
+
seen: set[str] = set()
|
|
67
|
+
result = "done"
|
|
68
|
+
try:
|
|
69
|
+
# Snapshot takes produced before we subscribed (fast/cached runs), deduped by id.
|
|
70
|
+
for take in list(record.state.takes):
|
|
71
|
+
if take.id not in seen:
|
|
72
|
+
seen.add(take.id)
|
|
73
|
+
self._save_take(item_id, take)
|
|
74
|
+
if record.done:
|
|
75
|
+
self._events.broadcast("events:generationDone", {"targetFrameId": item_id})
|
|
76
|
+
return
|
|
77
|
+
while True:
|
|
78
|
+
event = await queue.get()
|
|
79
|
+
if event is None: # terminal sentinel
|
|
80
|
+
break
|
|
81
|
+
if isinstance(event, ProgressEvent):
|
|
82
|
+
self._progress(item_id, max(0.05, event.fraction), event.status or None)
|
|
83
|
+
elif isinstance(event, NodeDoneEvent):
|
|
84
|
+
for take in event.takes:
|
|
85
|
+
if take.id in seen:
|
|
86
|
+
continue
|
|
87
|
+
seen.add(take.id)
|
|
88
|
+
self._save_take(item_id, take)
|
|
89
|
+
self._events.broadcast(
|
|
90
|
+
"events:generationNodeDone", {"frameId": item_id, "takeId": take.id}
|
|
91
|
+
)
|
|
92
|
+
elif isinstance(event, RunDoneEvent):
|
|
93
|
+
break
|
|
94
|
+
elif isinstance(event, CancelledEvent):
|
|
95
|
+
result = "cancelled"
|
|
96
|
+
break
|
|
97
|
+
elif isinstance(event, ErrorEvent):
|
|
98
|
+
result = "error"
|
|
99
|
+
self._events.broadcast(
|
|
100
|
+
"events:generationError",
|
|
101
|
+
{
|
|
102
|
+
"targetFrameId": item_id,
|
|
103
|
+
"frameId": event.node_id,
|
|
104
|
+
"error": event.message,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
break
|
|
108
|
+
if result == "done":
|
|
109
|
+
self._events.broadcast("events:generationDone", {"targetFrameId": item_id})
|
|
110
|
+
except Exception as error: # noqa: BLE001
|
|
111
|
+
self._events.broadcast(
|
|
112
|
+
"events:generationError", {"targetFrameId": item_id, "error": str(error)}
|
|
113
|
+
)
|
|
114
|
+
finally:
|
|
115
|
+
record.subscribers.discard(queue)
|
|
116
|
+
self._active.pop(item_id, None)
|
|
117
|
+
|
|
118
|
+
def _progress(self, item_id: str, fraction: float, status: str | None) -> None:
|
|
119
|
+
self._events.broadcast(
|
|
120
|
+
"events:generationProgress",
|
|
121
|
+
{"frameId": item_id, "fraction": fraction, "status": status},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _save_take(self, item_id: str, take: Any) -> None:
|
|
125
|
+
"""Copy a take's bytes into the project's takes/ dir and set its Core node's output."""
|
|
126
|
+
folder: Path = self._store.folder()
|
|
127
|
+
kind = _kind_str(take.kind)
|
|
128
|
+
src = Path(take.uri)
|
|
129
|
+
ext = src.suffix or _EXT.get(kind, ".png")
|
|
130
|
+
rel = f"takes/{uuid.uuid4()}{ext}"
|
|
131
|
+
(folder / "takes").mkdir(parents=True, exist_ok=True)
|
|
132
|
+
shutil.copyfile(src, folder / rel)
|
|
133
|
+
# take.node_id is the canvas item that produced it (node ids == item ids).
|
|
134
|
+
mb.set_core_node_output(
|
|
135
|
+
self._store.conn(), take.node_id, {"takeId": take.id, "filePath": rel, "kind": kind}
|
|
136
|
+
)
|