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.
Files changed (77) hide show
  1. inline_core/__init__.py +7 -0
  2. inline_core/components/__init__.py +1 -0
  3. inline_core/components/conditioning.py +20 -0
  4. inline_core/components/interfaces.py +91 -0
  5. inline_core/config.py +33 -0
  6. inline_core/device/__init__.py +1 -0
  7. inline_core/device/auto.py +35 -0
  8. inline_core/device/detect.py +41 -0
  9. inline_core/device/memory.py +168 -0
  10. inline_core/device/policy.py +82 -0
  11. inline_core/device/types.py +31 -0
  12. inline_core/errors.py +42 -0
  13. inline_core/graph/__init__.py +1 -0
  14. inline_core/graph/cache.py +83 -0
  15. inline_core/graph/descriptor.py +81 -0
  16. inline_core/graph/executor.py +102 -0
  17. inline_core/graph/primitives.py +137 -0
  18. inline_core/graph/registry.py +61 -0
  19. inline_core/graph/runners.py +72 -0
  20. inline_core/graph/schema.py +136 -0
  21. inline_core/graph/topo.py +49 -0
  22. inline_core/graph/validate.py +56 -0
  23. inline_core/importer/__init__.py +1 -0
  24. inline_core/importer/comfy.py +162 -0
  25. inline_core/media.py +11 -0
  26. inline_core/models/__init__.py +8 -0
  27. inline_core/models/catalog.py +98 -0
  28. inline_core/models/zimage/__init__.py +11 -0
  29. inline_core/models/zimage/requirements.py +180 -0
  30. inline_core/models/zimage/runner.py +386 -0
  31. inline_core/parallel/__init__.py +13 -0
  32. inline_core/parallel/config.py +50 -0
  33. inline_core/parallel/group.py +136 -0
  34. inline_core/parallel/launch.py +44 -0
  35. inline_core/parallel/protocol.py +53 -0
  36. inline_core/parallel/registry.py +31 -0
  37. inline_core/parallel/worker.py +75 -0
  38. inline_core/runtime/__init__.py +1 -0
  39. inline_core/runtime/context.py +31 -0
  40. inline_core/runtime/file_store.py +51 -0
  41. inline_core/runtime/progress.py +83 -0
  42. inline_core/runtime/run.py +113 -0
  43. inline_core/runtime/store.py +18 -0
  44. inline_core/sampling/__init__.py +1 -0
  45. inline_core/sampling/batch.py +116 -0
  46. inline_core/server/__init__.py +1 -0
  47. inline_core/server/__main__.py +61 -0
  48. inline_core/server/app.py +327 -0
  49. inline_core/server/assets.py +47 -0
  50. inline_core/server/bootstrap.py +21 -0
  51. inline_core/server/frontend.py +37 -0
  52. inline_core/server/manager.py +195 -0
  53. inline_core/server/rpc.py +60 -0
  54. inline_core/server/run_store.py +155 -0
  55. inline_core/server/serialize.py +144 -0
  56. inline_core/studio/__init__.py +7 -0
  57. inline_core/studio/assets.py +194 -0
  58. inline_core/studio/config.py +29 -0
  59. inline_core/studio/fal.py +237 -0
  60. inline_core/studio/frames.py +552 -0
  61. inline_core/studio/generation.py +136 -0
  62. inline_core/studio/graph_build.py +109 -0
  63. inline_core/studio/handlers.py +236 -0
  64. inline_core/studio/models.py +173 -0
  65. inline_core/studio/moodboard.py +400 -0
  66. inline_core/studio/schema.py +278 -0
  67. inline_core/studio/store.py +291 -0
  68. inline_core/studio/timeline/__init__.py +6 -0
  69. inline_core/studio/timeline/compose.py +130 -0
  70. inline_core/studio/timeline/ffmpeg.py +76 -0
  71. inline_core/studio/timeline/render.py +120 -0
  72. inline_core/studio/timeline/resolve.py +189 -0
  73. inline_core/takes.py +31 -0
  74. inline_core-1.1.1.dist-info/METADATA +35 -0
  75. inline_core-1.1.1.dist-info/RECORD +77 -0
  76. inline_core-1.1.1.dist-info/WHEEL +4 -0
  77. 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"]