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,237 @@
|
|
|
1
|
+
"""Fal generation on the single-process path — the server side of the fal relay.
|
|
2
|
+
|
|
3
|
+
The browser builds the fal request (endpoint + input body) from the studio-side node def, since fal
|
|
4
|
+
node definitions live there. Core owns the run: it submits to ``queue.fal.run`` with the key
|
|
5
|
+
(server-side only, never shipped to the page), polls to completion, parses the standard output
|
|
6
|
+
shapes, downloads the result into the project's ``takes/`` dir, and streams the generation events.
|
|
7
|
+
|
|
8
|
+
Ports the submit/poll/cancel logic of the Node ``electron/main/fal/client.ts``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import base64
|
|
15
|
+
import re
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from . import frames as fr
|
|
21
|
+
from . import moodboard as mb
|
|
22
|
+
|
|
23
|
+
_QUEUE_BASE = "https://queue.fal.run"
|
|
24
|
+
|
|
25
|
+
_MIME_BY_EXT = {
|
|
26
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
|
|
27
|
+
".gif": "image/gif", ".bmp": "image/bmp", ".tiff": "image/tiff",
|
|
28
|
+
".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm",
|
|
29
|
+
".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4",
|
|
30
|
+
}
|
|
31
|
+
_EXT_BY_KIND = {"image": ".png", "video": ".mp4", "audio": ".mp3"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _clamp01(n: float) -> float:
|
|
35
|
+
return max(0.0, min(1.0, n))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- input resolution (frame inputs + prompt -> fal-usable data URIs) ----------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def file_to_data_uri(abs_path: Path) -> str:
|
|
42
|
+
mime = _MIME_BY_EXT.get(abs_path.suffix.lower(), "application/octet-stream")
|
|
43
|
+
data = base64.b64encode(abs_path.read_bytes()).decode("ascii")
|
|
44
|
+
return f"data:{mime};base64,{data}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_fal_inputs(conn: Any, folder: Path, frame_id: str) -> dict[str, Any]:
|
|
48
|
+
"""A frame's inputs + prompt, resolved for the browser to build the fal request: media inputs as
|
|
49
|
+
base64 data URIs grouped by kind, and the prompt text from a connected Prompt node."""
|
|
50
|
+
images: list[str] = []
|
|
51
|
+
videos: list[str] = []
|
|
52
|
+
audios: list[str] = []
|
|
53
|
+
for media in fr.frame_input_media(conn, frame_id):
|
|
54
|
+
uri = file_to_data_uri(folder / media["filePath"])
|
|
55
|
+
kind = media["kind"]
|
|
56
|
+
(videos if kind == "video" else audios if kind == "audio" else images).append(uri)
|
|
57
|
+
return {
|
|
58
|
+
"images": images,
|
|
59
|
+
"videos": videos,
|
|
60
|
+
"audios": audios,
|
|
61
|
+
"prompt": mb.prompt_text_for_frame(conn, frame_id),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# --- output parsing (the standard fal response shapes) -------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ext_from(url: str, content_type: str | None, default: str) -> str:
|
|
69
|
+
if content_type and "/" in content_type:
|
|
70
|
+
sub = content_type.split("/")[-1].split(";")[0]
|
|
71
|
+
mapped = {"jpeg": ".jpg", "quicktime": ".mov", "mpeg": ".mp3"}.get(sub)
|
|
72
|
+
if mapped:
|
|
73
|
+
return mapped
|
|
74
|
+
if sub:
|
|
75
|
+
return f".{sub}"
|
|
76
|
+
match = re.search(r"\.[A-Za-z0-9]{1,5}(?:\?|$)", url)
|
|
77
|
+
return match.group(0).split("?")[0] if match else default
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_outputs(response: Any, output_kind: str) -> list[dict[str, str]]:
|
|
81
|
+
"""Extract output refs from a fal response. Covers the shared shapes the node defs produce:
|
|
82
|
+
``{images:[{url,...}]}`` (image), ``{video:{url,...}}`` / ``{videos:[...]}`` (video/audio)."""
|
|
83
|
+
if not isinstance(response, dict):
|
|
84
|
+
return []
|
|
85
|
+
refs: list[dict[str, str]] = []
|
|
86
|
+
default_ext = _EXT_BY_KIND.get(output_kind, ".bin")
|
|
87
|
+
if output_kind == "image":
|
|
88
|
+
for img in response.get("images") or []:
|
|
89
|
+
url = img.get("url") if isinstance(img, dict) else None
|
|
90
|
+
if url:
|
|
91
|
+
refs.append({
|
|
92
|
+
"url": url,
|
|
93
|
+
"ext": _ext_from(url, img.get("content_type"), default_ext),
|
|
94
|
+
"kind": "image",
|
|
95
|
+
})
|
|
96
|
+
else:
|
|
97
|
+
single = response.get(output_kind) # "video" | "audio"
|
|
98
|
+
items = [single] if isinstance(single, dict) else (response.get(f"{output_kind}s") or [])
|
|
99
|
+
for item in items:
|
|
100
|
+
url = item.get("url") if isinstance(item, dict) else None
|
|
101
|
+
if url:
|
|
102
|
+
refs.append({
|
|
103
|
+
"url": url,
|
|
104
|
+
"ext": _ext_from(url, item.get("content_type"), default_ext),
|
|
105
|
+
"kind": output_kind,
|
|
106
|
+
})
|
|
107
|
+
return refs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- the fal HTTP client (submit / poll / cancel) ------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve_queue_urls(endpoint: str, submitted: dict[str, Any]) -> dict[str, str]:
|
|
114
|
+
request_id = submitted["request_id"]
|
|
115
|
+
base = "/".join(endpoint.split("/")[:2]) # fal queue lives under the base app id (owner/app)
|
|
116
|
+
status_url = submitted.get("status_url") or f"{_QUEUE_BASE}/{base}/requests/{request_id}/status"
|
|
117
|
+
response_url = (
|
|
118
|
+
submitted.get("response_url")
|
|
119
|
+
or re.sub(r"/status(\?.*)?$", "", submitted.get("status_url") or "")
|
|
120
|
+
or f"{_QUEUE_BASE}/{base}/requests/{request_id}"
|
|
121
|
+
)
|
|
122
|
+
return {"requestId": request_id, "statusUrl": status_url, "responseUrl": response_url}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _progress_from_status(status: dict[str, Any]) -> tuple[float, str]:
|
|
126
|
+
state = status.get("status")
|
|
127
|
+
if state == "IN_QUEUE":
|
|
128
|
+
pos = status.get("queue_position")
|
|
129
|
+
return 0.05, f"Queued (#{pos})" if pos else "Queued"
|
|
130
|
+
if state == "IN_PROGRESS":
|
|
131
|
+
for log in reversed(status.get("logs") or []):
|
|
132
|
+
msg = (log or {}).get("message") or ""
|
|
133
|
+
step = re.search(r"(\d+)\s*(?:/|of)\s*(\d+)", msg, re.I)
|
|
134
|
+
if step:
|
|
135
|
+
cur, total = int(step.group(1)), int(step.group(2))
|
|
136
|
+
if total > 0 and 0 <= cur <= total:
|
|
137
|
+
return _clamp01(0.1 + 0.85 * (cur / total)), f"Generating {cur}/{total}"
|
|
138
|
+
pct = re.search(r"(\d{1,3}(?:\.\d+)?)\s*%", msg)
|
|
139
|
+
if pct:
|
|
140
|
+
p = min(100.0, float(pct.group(1)))
|
|
141
|
+
return _clamp01(0.1 + 0.85 * (p / 100)), f"Generating {round(p)}%"
|
|
142
|
+
return 0.5, "Generating"
|
|
143
|
+
if state == "COMPLETED":
|
|
144
|
+
return 1.0, "Done"
|
|
145
|
+
return 0.1, str(state or "Working")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class FalGeneration:
|
|
149
|
+
"""Runs a browser-built fal request server-side and streams it as Studio generation events."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, store: Any, events: Any) -> None:
|
|
152
|
+
self._store = store
|
|
153
|
+
self._events = events
|
|
154
|
+
self._active: dict[str, bool] = {}
|
|
155
|
+
|
|
156
|
+
def run(self, frame_id: str, request: dict[str, Any]) -> None:
|
|
157
|
+
key = self._store.fal_key()
|
|
158
|
+
if not key:
|
|
159
|
+
self._events.broadcast(
|
|
160
|
+
"events:generationError",
|
|
161
|
+
{"targetFrameId": frame_id, "error": "Add a fal API key in Settings to generate."},
|
|
162
|
+
)
|
|
163
|
+
return
|
|
164
|
+
self._active[frame_id] = True
|
|
165
|
+
asyncio.create_task(self._run(frame_id, request, key))
|
|
166
|
+
|
|
167
|
+
def cancel(self, frame_id: str | None = None) -> None:
|
|
168
|
+
for fid in [frame_id] if frame_id else list(self._active.keys()):
|
|
169
|
+
self._active.pop(fid, None)
|
|
170
|
+
|
|
171
|
+
async def _run(self, frame_id: str, request: dict[str, Any], key: str) -> None:
|
|
172
|
+
import httpx
|
|
173
|
+
|
|
174
|
+
endpoint = request["endpoint"]
|
|
175
|
+
body = request.get("body") or request.get("input") or {}
|
|
176
|
+
output_kind = request.get("outputKind") or "image"
|
|
177
|
+
headers = {"Authorization": f"Key {key}"}
|
|
178
|
+
try:
|
|
179
|
+
self._events.broadcast(
|
|
180
|
+
"events:generationProgress",
|
|
181
|
+
{"frameId": frame_id, "fraction": 0.05, "status": "Queued"},
|
|
182
|
+
)
|
|
183
|
+
async with httpx.AsyncClient(timeout=600) as client:
|
|
184
|
+
sub = await client.post(f"{_QUEUE_BASE}/{endpoint}", headers=headers, json=body)
|
|
185
|
+
sub.raise_for_status()
|
|
186
|
+
handle = _resolve_queue_urls(endpoint, sub.json())
|
|
187
|
+
sep = "&" if "?" in handle["statusUrl"] else "?"
|
|
188
|
+
status_url = handle["statusUrl"] + sep + "logs=1"
|
|
189
|
+
while self._active.get(frame_id):
|
|
190
|
+
await asyncio.sleep(1.5)
|
|
191
|
+
res = await client.get(status_url, headers=headers)
|
|
192
|
+
if res.status_code >= 500:
|
|
193
|
+
continue
|
|
194
|
+
res.raise_for_status()
|
|
195
|
+
status = res.json()
|
|
196
|
+
fraction, label = _progress_from_status(status)
|
|
197
|
+
self._events.broadcast(
|
|
198
|
+
"events:generationProgress",
|
|
199
|
+
{"frameId": frame_id, "fraction": fraction, "status": label},
|
|
200
|
+
)
|
|
201
|
+
if status.get("status") == "COMPLETED":
|
|
202
|
+
break
|
|
203
|
+
if not self._active.get(frame_id):
|
|
204
|
+
return # cancelled
|
|
205
|
+
result = await client.get(handle["responseUrl"], headers=headers)
|
|
206
|
+
result.raise_for_status()
|
|
207
|
+
refs = parse_outputs(result.json(), output_kind)
|
|
208
|
+
if not refs:
|
|
209
|
+
raise RuntimeError("The model returned no output.")
|
|
210
|
+
take_id = None
|
|
211
|
+
for ref in refs:
|
|
212
|
+
take_id = await self._save(client, frame_id, ref, handle["requestId"], body)
|
|
213
|
+
if take_id:
|
|
214
|
+
self._events.broadcast(
|
|
215
|
+
"events:generationNodeDone", {"frameId": frame_id, "takeId": take_id}
|
|
216
|
+
)
|
|
217
|
+
self._events.broadcast("events:generationDone", {"targetFrameId": frame_id})
|
|
218
|
+
except Exception as error: # noqa: BLE001
|
|
219
|
+
self._events.broadcast(
|
|
220
|
+
"events:generationError", {"targetFrameId": frame_id, "error": str(error)}
|
|
221
|
+
)
|
|
222
|
+
finally:
|
|
223
|
+
self._active.pop(frame_id, None)
|
|
224
|
+
|
|
225
|
+
async def _save(
|
|
226
|
+
self, client: Any, frame_id: str, ref: dict[str, str], request_id: str, params: dict
|
|
227
|
+
) -> str:
|
|
228
|
+
folder: Path = self._store.folder()
|
|
229
|
+
data = await client.get(ref["url"])
|
|
230
|
+
data.raise_for_status()
|
|
231
|
+
rel = f"takes/{uuid.uuid4()}{ref['ext']}"
|
|
232
|
+
(folder / "takes").mkdir(parents=True, exist_ok=True)
|
|
233
|
+
(folder / rel).write_bytes(data.content)
|
|
234
|
+
take = fr.add_take(
|
|
235
|
+
self._store.conn(), frame_id, rel, ref["kind"], params, comfy_prompt_id=request_id
|
|
236
|
+
)
|
|
237
|
+
return take["id"]
|