lcode-cli 0.43.0__py3-none-any.whl → 0.44.0__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.
- lcode/__init__.py +1 -1
- lcode/agent.py +4 -0
- lcode/api.py +1 -0
- lcode/cli.py +3 -11
- lcode/config.py +4 -3
- lcode/imageopts.py +21 -2
- lcode/media.py +103 -19
- lcode/media.toml +33 -0
- lcode/media_catalog.py +1 -0
- lcode/modes.py +9 -9
- lcode/repl.py +35 -20
- lcode/workflows/wan22-ti2v-5b.json +152 -0
- {lcode_cli-0.43.0.dist-info → lcode_cli-0.44.0.dist-info}/METADATA +4 -3
- {lcode_cli-0.43.0.dist-info → lcode_cli-0.44.0.dist-info}/RECORD +17 -16
- {lcode_cli-0.43.0.dist-info → lcode_cli-0.44.0.dist-info}/WHEEL +0 -0
- {lcode_cli-0.43.0.dist-info → lcode_cli-0.44.0.dist-info}/entry_points.txt +0 -0
- {lcode_cli-0.43.0.dist-info → lcode_cli-0.44.0.dist-info}/licenses/LICENSE +0 -0
lcode/__init__.py
CHANGED
lcode/agent.py
CHANGED
|
@@ -309,6 +309,7 @@ class Settings:
|
|
|
309
309
|
comfyui_path: str | None = None # a ComfyUI install lcode doesn't find by itself (lcode.comfy)
|
|
310
310
|
media_dir: str = ".lcode/media" # where image and video mode save their files (lcode.media)
|
|
311
311
|
image_model: str = "flux2-klein-4b" # the media catalog's model image mode uses (lcode.media_catalog)
|
|
312
|
+
video_model: str = "wan22-ti2v-5b" # and video mode's
|
|
312
313
|
preview: str = "auto" # images in the terminal: auto, kitty, iterm, blocks or off (lcode.preview)
|
|
313
314
|
num_batch: int | None = None
|
|
314
315
|
keep_alive: str = "30m"
|
|
@@ -698,6 +699,7 @@ class Agent:
|
|
|
698
699
|
"model": self.settings.model,
|
|
699
700
|
"mode": self.settings.mode,
|
|
700
701
|
"image_model": self.settings.image_model,
|
|
702
|
+
"video_model": self.settings.video_model,
|
|
701
703
|
"media": {"results": self.media_results, "options": self.image_options.to_json(), "refs": self.image_refs},
|
|
702
704
|
"name": self.session_name,
|
|
703
705
|
"title": self.session_title,
|
|
@@ -749,6 +751,8 @@ class Agent:
|
|
|
749
751
|
self.settings.mode = data["mode"] # a session belongs to the mode it was made in
|
|
750
752
|
if isinstance(data.get("image_model"), str):
|
|
751
753
|
self.settings.image_model = data["image_model"]
|
|
754
|
+
if isinstance(data.get("video_model"), str):
|
|
755
|
+
self.settings.video_model = data["video_model"]
|
|
752
756
|
media = data.get("media") or {}
|
|
753
757
|
self.media_results = [r for r in media.get("results") or [] if isinstance(r, dict) and "path" in r]
|
|
754
758
|
self.image_options = type(self.image_options).from_json(media.get("options") or {})
|
lcode/api.py
CHANGED
|
@@ -201,6 +201,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
201
201
|
comfyui_path=cfg["comfyui_path"],
|
|
202
202
|
media_dir=cfg["media_dir"],
|
|
203
203
|
image_model=cfg["image_model"],
|
|
204
|
+
video_model=cfg["video_model"],
|
|
204
205
|
preview=cfg["preview"],
|
|
205
206
|
web=switch(options.web, cfg["web"], "on"),
|
|
206
207
|
search_backend=cfg["search_backend"],
|
lcode/cli.py
CHANGED
|
@@ -801,16 +801,8 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
|
801
801
|
"""`lcode --mode image -p "…"`: make the image, say where it went (or, with --output json, a result object)."""
|
|
802
802
|
import json
|
|
803
803
|
|
|
804
|
-
from lcode import api, media
|
|
804
|
+
from lcode import api, media
|
|
805
805
|
|
|
806
|
-
if agent.settings.mode == "video":
|
|
807
|
-
message = modes.COMING["video"]
|
|
808
|
-
if quiet:
|
|
809
|
-
print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
|
|
810
|
-
else:
|
|
811
|
-
out.print(f"[yellow]{escape(message)}[/]")
|
|
812
|
-
api.close_agent(agent)
|
|
813
|
-
sys.exit(2)
|
|
814
806
|
try:
|
|
815
807
|
made = media.request(agent, prompt)
|
|
816
808
|
except (media.MediaError, KeyboardInterrupt) as e:
|
|
@@ -825,7 +817,7 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
|
825
817
|
result = {
|
|
826
818
|
"type": "result",
|
|
827
819
|
"status": "success",
|
|
828
|
-
"mode":
|
|
820
|
+
"mode": agent.settings.mode,
|
|
829
821
|
"files": [str(path) for one in made for path in one.files],
|
|
830
822
|
"model": made[0].model,
|
|
831
823
|
"seed": made[0].seed,
|
|
@@ -1106,7 +1098,7 @@ def cmd_media(args) -> None:
|
|
|
1106
1098
|
where = (
|
|
1107
1099
|
f"ComfyUI at {server.url}" if running else (f"{install.path} (not running)" if install else "no ComfyUI found")
|
|
1108
1100
|
)
|
|
1109
|
-
console.print(f"[dim]{hw.describe()} · {where} · image
|
|
1101
|
+
console.print(f"[dim]{hw.describe()} · {where} · image: {cfg['image_model']} · video: {cfg['video_model']}[/]\n")
|
|
1110
1102
|
for mode in ("image", "video", "tool"):
|
|
1111
1103
|
models = cat.for_mode(mode)
|
|
1112
1104
|
console.print(
|
lcode/config.py
CHANGED
|
@@ -46,6 +46,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
46
46
|
"comfyui_path": (None, str, "your ComfyUI folder, if lcode doesn't find it (lcode starts it when needed)"),
|
|
47
47
|
"media_dir": (".lcode/media", str, "where images and videos go: a folder in the project, or an absolute path"),
|
|
48
48
|
"image_model": ("flux2-klein-4b", str, "the model image mode uses: a key from `lcode media`"),
|
|
49
|
+
"video_model": ("wan22-ti2v-5b", str, "the model video mode uses: a key from `lcode media`"),
|
|
49
50
|
"preview": ("auto", str, "images in the terminal: auto | kitty | iterm | blocks (colour text) | off"),
|
|
50
51
|
"think": (True, bool, "let the model reason before answering (slower, better)"),
|
|
51
52
|
"web": ("on", str, "web search and page fetching for the model: on | ask | off"),
|
|
@@ -217,12 +218,12 @@ def coerce(key: str, value: object) -> object:
|
|
|
217
218
|
|
|
218
219
|
if value not in PROTOCOLS:
|
|
219
220
|
raise ConfigError(f"preview must be one of {', '.join(PROTOCOLS)}")
|
|
220
|
-
if key
|
|
221
|
+
if key in ("image_model", "video_model"):
|
|
221
222
|
from lcode import media_catalog
|
|
222
223
|
|
|
223
|
-
keys = [m.key for m in media_catalog.load().for_mode("
|
|
224
|
+
keys = [m.key for m in media_catalog.load().for_mode(key.removesuffix("_model"))]
|
|
224
225
|
if value not in keys:
|
|
225
|
-
raise ConfigError(f"
|
|
226
|
+
raise ConfigError(f"{key} must be one of {', '.join(keys)} (see lcode media)")
|
|
226
227
|
if typ is bool and isinstance(value, str):
|
|
227
228
|
if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
|
228
229
|
raise ConfigError(f"{key} must be true or false")
|
lcode/imageopts.py
CHANGED
|
@@ -13,6 +13,7 @@ from dataclasses import asdict, dataclass, replace
|
|
|
13
13
|
|
|
14
14
|
QUALITIES = ("fast", "balanced", "best")
|
|
15
15
|
MAX_COUNT = 8
|
|
16
|
+
MAX_SECONDS = 8 # video: longer clips need more memory than a laptop GPU has, and models drift
|
|
16
17
|
SIZES = { # names for aspect ratios
|
|
17
18
|
"square": "1:1",
|
|
18
19
|
"portrait": "3:4",
|
|
@@ -46,6 +47,8 @@ ALIASES = {
|
|
|
46
47
|
"no": "negative",
|
|
47
48
|
"negative": "negative",
|
|
48
49
|
"style": "style",
|
|
50
|
+
"seconds": "seconds",
|
|
51
|
+
"duration": "seconds",
|
|
49
52
|
}
|
|
50
53
|
FLAG = re.compile(r"(?:^|\s)--?(" + "|".join(sorted(ALIASES, key=len, reverse=True)) + r")\b(?=[\s=]|$)")
|
|
51
54
|
DIMENSIONS = re.compile(r"^(\d{2,4})\s*[x×]\s*(\d{2,4})$")
|
|
@@ -64,10 +67,13 @@ class Options:
|
|
|
64
67
|
quality: str = "balanced"
|
|
65
68
|
negative: str = ""
|
|
66
69
|
style: str = ""
|
|
70
|
+
seconds: float = 5.0 # video only
|
|
67
71
|
|
|
68
|
-
def describe(self) -> str:
|
|
72
|
+
def describe(self, video: bool = False) -> str:
|
|
69
73
|
seed = "random" if self.seed is None else str(self.seed)
|
|
70
74
|
parts = [f"n={self.n}", f"size={self.size}", f"seed={seed}", f"quality={self.quality}"]
|
|
75
|
+
if video:
|
|
76
|
+
parts.append(f"seconds={self.seconds:g}")
|
|
71
77
|
if self.style:
|
|
72
78
|
parts.append(f"style={self.style}")
|
|
73
79
|
if self.negative:
|
|
@@ -92,7 +98,7 @@ def set_values(options: Options, values: dict[str, object]) -> Options:
|
|
|
92
98
|
for raw_key, raw in values.items():
|
|
93
99
|
key = ALIASES.get(str(raw_key).lower().lstrip("-"))
|
|
94
100
|
if key is None:
|
|
95
|
-
raise OptionError(f"unknown option {raw_key!r}: use n, ar (or size), seed, quality, style or
|
|
101
|
+
raise OptionError(f"unknown option {raw_key!r}: use n, ar (or size), seed, quality, style, no or seconds")
|
|
96
102
|
value = str(raw).strip() if raw is not None else ""
|
|
97
103
|
if key == "n":
|
|
98
104
|
if not value.isdigit() or not 1 <= int(value) <= MAX_COUNT:
|
|
@@ -120,6 +126,14 @@ def set_values(options: Options, values: dict[str, object]) -> Options:
|
|
|
120
126
|
raise OptionError(f"style is one of {', '.join(STYLES)} (or none)")
|
|
121
127
|
elif key == "negative":
|
|
122
128
|
out.negative = "" if value.lower() in ("none", "off") else value
|
|
129
|
+
elif key == "seconds":
|
|
130
|
+
try:
|
|
131
|
+
seconds = float(value.rstrip("s"))
|
|
132
|
+
except ValueError:
|
|
133
|
+
seconds = 0
|
|
134
|
+
if not 1 <= seconds <= MAX_SECONDS:
|
|
135
|
+
raise OptionError(f"seconds is how long a video is: 1 to {MAX_SECONDS}")
|
|
136
|
+
out.seconds = seconds
|
|
123
137
|
return out
|
|
124
138
|
|
|
125
139
|
|
|
@@ -203,5 +217,10 @@ def replace_seed(options: Options, n: int | None = None) -> Options:
|
|
|
203
217
|
return replace(options, seed=None, n=n or options.n)
|
|
204
218
|
|
|
205
219
|
|
|
220
|
+
def frames(seconds: float, fps: int) -> int:
|
|
221
|
+
"""Frames for a clip: a multiple of 4 plus 1, as video models that compress time 4 to 1 need."""
|
|
222
|
+
return max(1, round(seconds * fps / 4)) * 4 + 1
|
|
223
|
+
|
|
224
|
+
|
|
206
225
|
def styled(prompt: str, style: str) -> str:
|
|
207
226
|
return f"{prompt}, {STYLES[style]}" if style else prompt
|
lcode/media.py
CHANGED
|
@@ -27,7 +27,7 @@ from lcode import media_catalog as catalog
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from lcode.agent import Agent
|
|
29
29
|
|
|
30
|
-
SOON
|
|
30
|
+
SOON: dict[str, str] = {}
|
|
31
31
|
IGNORED = "# lcode's images and videos: not part of the project unless you add them\n*\n"
|
|
32
32
|
|
|
33
33
|
|
|
@@ -44,9 +44,11 @@ def installed_lister(agent: Agent) -> catalog.Installed | None:
|
|
|
44
44
|
return catalog.lister(install=install) if install else None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def current(agent: Agent) -> catalog.MediaModel:
|
|
48
|
-
"""The
|
|
47
|
+
def current(agent: Agent, mode: str | None = None) -> catalog.MediaModel:
|
|
48
|
+
"""The model the agent uses in image or video mode (image_model, video_model, or /model there)."""
|
|
49
49
|
cat = catalog.load()
|
|
50
|
+
if (mode or agent.settings.mode) == "video":
|
|
51
|
+
return cat.find(agent.settings.video_model) or cat.models[catalog.DEFAULT_VIDEO]
|
|
50
52
|
return cat.find(agent.settings.image_model) or cat.models[catalog.DEFAULT_IMAGE]
|
|
51
53
|
|
|
52
54
|
|
|
@@ -61,6 +63,7 @@ class Made:
|
|
|
61
63
|
prompt: str = ""
|
|
62
64
|
numbers: list[int] = field(default_factory=list) # in the session's gallery
|
|
63
65
|
thumbnail: bytes = b"" # a small PNG, for the half-block preview
|
|
66
|
+
poster: bytes = b"" # a video's first frame, for terminals that show pictures
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
# ----------------------------------------------------------------------------- ComfyUI, started and stopped
|
|
@@ -227,25 +230,36 @@ def ensure_installed(
|
|
|
227
230
|
|
|
228
231
|
|
|
229
232
|
THUMB_NODES = ("9001", "9002") # the thumbnail lcode adds to a workflow, for the half-block preview
|
|
233
|
+
POSTER_NODES = ("9003", "9004") # a video's first frame, picked and kept (not saved) for the preview
|
|
230
234
|
|
|
231
235
|
|
|
232
236
|
def add_thumbnail(graph: dict, width: int, height: int) -> None:
|
|
233
|
-
"""Have ComfyUI also make a small copy of the image (not saved, a preview)
|
|
237
|
+
"""Have ComfyUI also make a small copy of the image, or of a video's first frame (not saved, a preview)."""
|
|
234
238
|
save = next((nid for nid, node in graph.items() if node.get("class_type") == "SaveImage"), None)
|
|
235
|
-
if
|
|
239
|
+
video = next((nid for nid, node in graph.items() if node.get("class_type") == "CreateVideo"), None)
|
|
240
|
+
if save is not None:
|
|
241
|
+
frames = graph[save]["inputs"]["images"]
|
|
242
|
+
elif video is not None:
|
|
243
|
+
first, keep = POSTER_NODES
|
|
244
|
+
graph[first] = {
|
|
245
|
+
"class_type": "ImageFromBatch",
|
|
246
|
+
"inputs": {"image": graph[video]["inputs"]["images"], "batch_index": 0, "length": 1},
|
|
247
|
+
}
|
|
248
|
+
graph[keep] = {"class_type": "PreviewImage", "inputs": {"images": [first, 0]}}
|
|
249
|
+
frames = [first, 0]
|
|
250
|
+
else:
|
|
236
251
|
return
|
|
237
252
|
tw, th = preview.thumbnail_size(width, height)
|
|
238
253
|
scale, show = THUMB_NODES
|
|
239
254
|
graph[scale] = {
|
|
240
255
|
"class_type": "ImageScale",
|
|
241
|
-
"inputs": {"image":
|
|
242
|
-
|
|
243
|
-
} # fmt: skip
|
|
256
|
+
"inputs": {"image": frames, "upscale_method": "area", "width": tw, "height": th, "crop": "disabled"},
|
|
257
|
+
}
|
|
244
258
|
graph[show] = {"class_type": "PreviewImage", "inputs": {"images": [scale, 0]}}
|
|
245
259
|
|
|
246
260
|
|
|
247
|
-
def thumbnail(server: comfy.Server, outputs: dict) -> bytes:
|
|
248
|
-
for item in (outputs.get(
|
|
261
|
+
def thumbnail(server: comfy.Server, outputs: dict, node: str = THUMB_NODES[1]) -> bytes:
|
|
262
|
+
for item in (outputs.get(node) or {}).get("images", []):
|
|
249
263
|
try:
|
|
250
264
|
return server.fetch(item)
|
|
251
265
|
except (comfy.ComfyError, OSError, requests.RequestException):
|
|
@@ -275,6 +289,12 @@ def request(
|
|
|
275
289
|
sources = [resolve(agent, ref) for ref in refs] + [Path(p) for p in agent.image_refs if Path(p).exists()]
|
|
276
290
|
if mask and not sources:
|
|
277
291
|
raise MediaError("--mask needs the image it masks: @photo.png --mask mask.png make the sky purple")
|
|
292
|
+
if agent.settings.mode == "video":
|
|
293
|
+
if mask:
|
|
294
|
+
raise MediaError("Video mode doesn't take a mask: @photo.png starts a video from a picture.")
|
|
295
|
+
job = {"kind": "video", "prompt": prompt, "options": options, "sources": [str(p) for p in sources[:1]],
|
|
296
|
+
"mask": None, "pads": None, "keep_shape": not SIZE_FLAG.search(text)} # fmt: skip
|
|
297
|
+
return redo(agent, job, cancelled, ask)
|
|
278
298
|
job = {"kind": "edit" if sources else "image", "prompt": prompt, "options": options,
|
|
279
299
|
"sources": [str(p) for p in sources], "mask": str(mask) if mask else None, "pads": None,
|
|
280
300
|
"keep_shape": not SIZE_FLAG.search(text)} # fmt: skip
|
|
@@ -286,9 +306,10 @@ def redo(
|
|
|
286
306
|
) -> list[Made]:
|
|
287
307
|
"""Make what `job` describes (also what /again and /vary repeat): images, or edits of its sources."""
|
|
288
308
|
prompt, options = job["prompt"], job["options"]
|
|
289
|
-
if not prompt.strip() and job["kind"] in ("image", "edit") and not job.get("pads"):
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
if not prompt.strip() and job["kind"] in ("image", "edit", "video") and not job.get("pads"):
|
|
310
|
+
what = {"image": "Describe the image you want.", "edit": "Say what to change."}
|
|
311
|
+
raise MediaError(what.get(job["kind"], "Describe the video you want."))
|
|
312
|
+
model = current(agent, "video" if job["kind"] == "video" else "image")
|
|
292
313
|
if options.negative and not model.negative:
|
|
293
314
|
agent.say(f"{model.name} doesn't use a negative prompt: describe what you want instead (--no is ignored).")
|
|
294
315
|
agent.last_request = job
|
|
@@ -299,6 +320,8 @@ def redo(
|
|
|
299
320
|
one = replace(options, seed=seed, n=1)
|
|
300
321
|
if job["kind"] == "image":
|
|
301
322
|
made.append(generate_image(agent, prompt, cancelled, ask, one, label))
|
|
323
|
+
elif job["kind"] == "video":
|
|
324
|
+
made.append(generate_video(agent, job, one, cancelled, ask, label))
|
|
302
325
|
else:
|
|
303
326
|
made.append(edit_image(agent, job, one, cancelled, ask, label))
|
|
304
327
|
return made
|
|
@@ -368,7 +391,7 @@ def generate_image(
|
|
|
368
391
|
raise MediaError("Describe the image you want.")
|
|
369
392
|
options = options or imageopts.Options()
|
|
370
393
|
server = prepare(agent, ask, cancelled)
|
|
371
|
-
model = current(agent)
|
|
394
|
+
model = current(agent, "image")
|
|
372
395
|
files = ensure_installed(agent, model, server, ask)
|
|
373
396
|
workflow = comfy.load_workflow(model.workflow)
|
|
374
397
|
seed = options.seed if options.seed is not None else random.randrange(2**32)
|
|
@@ -397,7 +420,7 @@ def edit_image(
|
|
|
397
420
|
"""An edit of the job's source images: by instruction, through a mask (inpainting), or on a bigger canvas
|
|
398
421
|
(outpainting). The first source sets the shape unless the request gave one."""
|
|
399
422
|
server = prepare(agent, ask, cancelled)
|
|
400
|
-
chosen = current(agent)
|
|
423
|
+
chosen = current(agent, "image")
|
|
401
424
|
model = catalog.load().editor(chosen)
|
|
402
425
|
if model is None:
|
|
403
426
|
raise MediaError("None of lcode's image models edits pictures yet.")
|
|
@@ -453,6 +476,62 @@ def edit_image(
|
|
|
453
476
|
return run_plan(agent, server, plan, cancelled, label)
|
|
454
477
|
|
|
455
478
|
|
|
479
|
+
def generate_video(
|
|
480
|
+
agent: Agent,
|
|
481
|
+
job: dict,
|
|
482
|
+
options: imageopts.Options,
|
|
483
|
+
cancelled: Callable[[], bool] = lambda: False,
|
|
484
|
+
ask: Callable[[str], bool] | None = None,
|
|
485
|
+
label: str = "",
|
|
486
|
+
) -> Made:
|
|
487
|
+
"""One video: from the prompt, or starting from the job's image. Its shape is 16:9, or 9:16 for an upright start
|
|
488
|
+
image (the shapes video models know best; the image is cropped to it), unless the request gives one."""
|
|
489
|
+
prompt = job["prompt"].strip()
|
|
490
|
+
server = prepare(agent, ask, cancelled)
|
|
491
|
+
model = current(agent, "video")
|
|
492
|
+
files = ensure_installed(agent, model, server, ask)
|
|
493
|
+
workflow = comfy.load_workflow(model.workflow)
|
|
494
|
+
start = Path(job["sources"][0]) if job.get("sources") else None
|
|
495
|
+
seed = options.seed if options.seed is not None else random.randrange(2**32)
|
|
496
|
+
steps, megapixels = model.preset(options.quality)
|
|
497
|
+
ratio = options.size
|
|
498
|
+
if job.get("keep_shape", True) and options.size == imageopts.Options().size: # no shape asked for
|
|
499
|
+
ratio = "16:9"
|
|
500
|
+
if start is not None:
|
|
501
|
+
try:
|
|
502
|
+
w, h = edits.image_size(start)
|
|
503
|
+
ratio = "9:16" if h > w else "16:9" # a square or odd shape breaks up; this crops it
|
|
504
|
+
except (OSError, edits.EditError):
|
|
505
|
+
pass
|
|
506
|
+
width, height = imageopts.dimensions(imageopts.SIZES.get(ratio, ratio), megapixels, multiple=32)
|
|
507
|
+
fps = int(workflow.defaults.get("fps", 24))
|
|
508
|
+
text = imageopts.styled(prompt, options.style)
|
|
509
|
+
values = {**workflow.defaults, **files, "prompt": text, "seed": seed, "width": width, "height": height,
|
|
510
|
+
"length": imageopts.frames(options.seconds, fps), "fps": fps} # fmt: skip
|
|
511
|
+
if steps:
|
|
512
|
+
values["steps"] = steps
|
|
513
|
+
if options.negative:
|
|
514
|
+
values["negative"] = options.negative
|
|
515
|
+
graph = comfy.fill(workflow.graph, values)
|
|
516
|
+
if start is not None:
|
|
517
|
+
try:
|
|
518
|
+
name = server.upload(start)
|
|
519
|
+
except (comfy.ComfyError, OSError) as e:
|
|
520
|
+
raise MediaError(f"Couldn't hand the image to ComfyUI: {e}") from e
|
|
521
|
+
latent = edits.node_of(graph, "Wan22ImageToVideoLatent")
|
|
522
|
+
if latent is None:
|
|
523
|
+
raise MediaError(f"{model.name} can't start from an image.")
|
|
524
|
+
graph["8100"] = {"class_type": "LoadImage", "inputs": {"image": name}}
|
|
525
|
+
graph[latent]["inputs"]["start_image"] = ["8100", 0]
|
|
526
|
+
add_thumbnail(graph, width, height)
|
|
527
|
+
meta = describe_job(model, workflow, prompt, text, seed, values, options, files)
|
|
528
|
+
meta.update({"seconds": round((values["length"] - 1) / fps, 2), "fps": fps, "frames": values["length"],
|
|
529
|
+
"start_image": shown(agent, start) if start else ""}) # fmt: skip
|
|
530
|
+
remembered = {k: job.get(k) for k in ("kind", "prompt", "sources", "mask", "pads", "keep_shape")}
|
|
531
|
+
plan = Plan(model, workflow, graph, meta, prompt, seed, options, "video", job.get("sources") or [], remembered)
|
|
532
|
+
return run_plan(agent, server, plan, cancelled, label)
|
|
533
|
+
|
|
534
|
+
|
|
456
535
|
def apply_tool(
|
|
457
536
|
agent: Agent,
|
|
458
537
|
task: str,
|
|
@@ -580,6 +659,8 @@ def run_plan(agent: Agent, server: comfy.Server, plan: Plan, cancelled, label: s
|
|
|
580
659
|
pass
|
|
581
660
|
made = Made(saved, plan.model.name, plan.seed, width, height, time.monotonic() - line.started, plan.prompt)
|
|
582
661
|
made.thumbnail = thumbnail(server, outputs)
|
|
662
|
+
if plan.kind == "video":
|
|
663
|
+
made.poster = thumbnail(server, outputs, POSTER_NODES[1])
|
|
583
664
|
made.numbers = record(agent, made, plan)
|
|
584
665
|
show(agent, made)
|
|
585
666
|
return made
|
|
@@ -626,18 +707,21 @@ def show(agent: Agent, made: Made) -> None:
|
|
|
626
707
|
+ Text(f" {made.width}×{made.height} · {made.model}{seed} · {made.seconds:.1f} s", style="dim")
|
|
627
708
|
)
|
|
628
709
|
if made.files:
|
|
629
|
-
show_preview(agent, made.files[0], made.thumbnail)
|
|
710
|
+
show_preview(agent, made.files[0], made.thumbnail, made.poster)
|
|
630
711
|
|
|
631
712
|
|
|
632
|
-
def show_preview(agent: Agent, path: Path, thumb: bytes = b"") -> None:
|
|
633
|
-
"""The image in the terminal: itself where the terminal can show pictures, else a
|
|
713
|
+
def show_preview(agent: Agent, path: Path, thumb: bytes = b"", poster: bytes = b"") -> None:
|
|
714
|
+
"""The image (or a video's first frame) in the terminal: itself where the terminal can show pictures, else a
|
|
715
|
+
half-block preview."""
|
|
634
716
|
setting = agent.settings.preview
|
|
635
717
|
how = preview.protocol(setting)
|
|
636
718
|
if how == "off" or (setting == "auto" and not agent.console.is_terminal):
|
|
637
719
|
return
|
|
638
720
|
try:
|
|
639
721
|
if how in ("kitty", "iterm"):
|
|
640
|
-
data = path.read_bytes()
|
|
722
|
+
data = path.read_bytes() if edits.is_image(path) else poster
|
|
723
|
+
if not data:
|
|
724
|
+
return
|
|
641
725
|
sequence = preview.kitty(data) if how == "kitty" else preview.iterm(data)
|
|
642
726
|
agent.console.file.write(sequence)
|
|
643
727
|
agent.console.file.flush()
|
lcode/media.toml
CHANGED
|
@@ -113,3 +113,36 @@ tasks = ["remove-background"]
|
|
|
113
113
|
workflow = "remove-background"
|
|
114
114
|
files = { model = "birefnet" }
|
|
115
115
|
notes = "/nobg: a transparent PNG"
|
|
116
|
+
|
|
117
|
+
[files.wan22-ti2v-5b]
|
|
118
|
+
folder = "diffusion_models"
|
|
119
|
+
name = "wan2.2_ti2v_5B_fp16.safetensors"
|
|
120
|
+
url = "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors"
|
|
121
|
+
gb = 10.00
|
|
122
|
+
|
|
123
|
+
[files.umt5-xxl-fp8]
|
|
124
|
+
folder = "text_encoders"
|
|
125
|
+
name = "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
|
126
|
+
url = "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
|
127
|
+
gb = 6.74
|
|
128
|
+
alternatives = ["umt5_xxl_fp16.safetensors"]
|
|
129
|
+
|
|
130
|
+
[files.wan22-vae]
|
|
131
|
+
folder = "vae"
|
|
132
|
+
name = "wan2.2_vae.safetensors"
|
|
133
|
+
url = "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/vae/wan2.2_vae.safetensors"
|
|
134
|
+
gb = 1.41
|
|
135
|
+
|
|
136
|
+
[models.wan22-ti2v-5b]
|
|
137
|
+
name = "Wan 2.2 TI2V 5B"
|
|
138
|
+
mode = "video"
|
|
139
|
+
publisher = "Wan-AI (Alibaba)"
|
|
140
|
+
license = "Apache 2.0"
|
|
141
|
+
tasks = ["text-to-video", "image-to-video"]
|
|
142
|
+
workflow = "wan22-ti2v-5b"
|
|
143
|
+
files = { model = "wan22-ti2v-5b", text_encoder = "umt5-xxl-fp8", vae = "wan22-vae" }
|
|
144
|
+
gpu_gb = 0.0
|
|
145
|
+
measured = "5 s clip: 100 s fast, 150 s balanced (864×480), 10.5 min best (1280×704) on an RTX 4080 Laptop, 12 GB"
|
|
146
|
+
notes = "5 s at 24 fps, up to 1280×704; text or a start image"
|
|
147
|
+
quality = { fast = [12, 0.4], balanced = [20, 0.4], best = [30, 0.859] }
|
|
148
|
+
negative = true
|
lcode/media_catalog.py
CHANGED
|
@@ -29,6 +29,7 @@ else: # pragma: no cover
|
|
|
29
29
|
import tomli as tomllib
|
|
30
30
|
|
|
31
31
|
DEFAULT_IMAGE = "flux2-klein-4b"
|
|
32
|
+
DEFAULT_VIDEO = "wan22-ti2v-5b"
|
|
32
33
|
DISK_MARGIN_GB = 2.0 # left free after a download
|
|
33
34
|
GPU_MARGIN_GIB = 0.5
|
|
34
35
|
WORKING_GB = 1.5 # activations and buffers on top of the largest file, for models not measured yet
|
lcode/modes.py
CHANGED
|
@@ -27,23 +27,23 @@ TITLES = {"code": "local coding agent", "image": "local image generation", "vide
|
|
|
27
27
|
MEDIA_SERVERS = frozenset({"comfyui", "xai"}) # the MCP catalog's image and video servers
|
|
28
28
|
MEDIA_COMMANDS = ("comfy-mcp", "lcode-mcp-xai") # the same servers, under any name
|
|
29
29
|
NOTED_FILE = "media-servers-noted"
|
|
30
|
-
COMING = {
|
|
31
|
-
"video": (
|
|
32
|
-
"Video mode doesn't generate yet: the video models (#150) and video generation (#153) come next. "
|
|
33
|
-
"/mode image makes images now; /mode code switches back."
|
|
34
|
-
),
|
|
35
|
-
}
|
|
30
|
+
COMING: dict[str, str] = {}
|
|
36
31
|
|
|
37
32
|
|
|
38
33
|
def ready(agent: Agent) -> str:
|
|
39
34
|
"""What to do in the agent's mode, said on entering it."""
|
|
40
|
-
|
|
41
|
-
from lcode import media
|
|
35
|
+
from lcode import media
|
|
42
36
|
|
|
37
|
+
if agent.settings.mode == "image":
|
|
43
38
|
return (
|
|
44
39
|
f"Describe an image and press Enter: {media.current(agent).name} makes it on your GPU, into "
|
|
45
40
|
f"{agent.settings.media_dir}. /mode code switches back."
|
|
46
41
|
)
|
|
42
|
+
if agent.settings.mode == "video":
|
|
43
|
+
return (
|
|
44
|
+
f"Describe a video and press Enter (@photo.png starts it from a picture): {media.current(agent).name} "
|
|
45
|
+
f"makes it on your GPU, into {agent.settings.media_dir}. /mode code switches back."
|
|
46
|
+
)
|
|
47
47
|
return COMING.get(agent.settings.mode, "")
|
|
48
48
|
|
|
49
49
|
|
|
@@ -113,7 +113,7 @@ def switch(agent: Agent, new: str) -> list[str]:
|
|
|
113
113
|
if new == "code" and agent.no_coding_model:
|
|
114
114
|
reason = agent.no_coding_model.strip().splitlines()[0]
|
|
115
115
|
lines.append(f"lcode started without its coding model ({reason}). Fix that, then restart lcode.")
|
|
116
|
-
if new
|
|
116
|
+
if new in ("image", "video"):
|
|
117
117
|
from lcode import media
|
|
118
118
|
|
|
119
119
|
warming = media.warm(agent)
|
lcode/repl.py
CHANGED
|
@@ -89,6 +89,8 @@ COMMANDS = {
|
|
|
89
89
|
"/extend": "Image mode: outpaint an image to a shape or by sides, e.g. /extend 3 --ar 16:9 (or --left 256)",
|
|
90
90
|
"/upscale": "Image mode: 2 or 4 times the pixels, e.g. /upscale 3 4",
|
|
91
91
|
"/nobg": "Image mode: remove the background (a transparent PNG), e.g. /nobg 3",
|
|
92
|
+
"/animate": "Image mode: make a video starting from an image, e.g. /animate 3 the waves roll in (video mode)",
|
|
93
|
+
"/play": "Play a video in your player, e.g. /play 4 (default: the last)",
|
|
92
94
|
"/cd": "Change the working directory",
|
|
93
95
|
"/todos": "Show the current todo list",
|
|
94
96
|
"/status": "The task sheet: your requests, the plan and notes, files changed, the last check",
|
|
@@ -230,7 +232,7 @@ def model_lines(agent: Agent) -> str:
|
|
|
230
232
|
if s.mode == "image":
|
|
231
233
|
return f"[dim]model[/] {escape(media.current(agent).name)} [dim]· ComfyUI · {escape(s.media_dir)}[/]\n"
|
|
232
234
|
if s.mode == "video":
|
|
233
|
-
return "[dim]model[/] [dim]
|
|
235
|
+
return f"[dim]model[/] {escape(media.current(agent).name)} [dim]· ComfyUI · {escape(s.media_dir)}[/]\n"
|
|
234
236
|
server = f" [dim]· {escape(agent.ollama.describe())}[/]" if backends.is_openai(agent.ollama) else ""
|
|
235
237
|
return f"[dim]model[/] {escape(s.model)}{server}\n[dim]context[/] {format_tokens(s.context)} tokens\n"
|
|
236
238
|
|
|
@@ -931,13 +933,13 @@ def media_model_command(agent: Agent, arg: str, hardware: Hardware) -> None:
|
|
|
931
933
|
if model is None:
|
|
932
934
|
c.print(f"[red]There's no {s.mode} model {escape(arg)!r}: choose 1 to {len(choices)}, or a name.[/]")
|
|
933
935
|
return
|
|
934
|
-
s.
|
|
935
|
-
c.print(f"[green]
|
|
936
|
+
setattr(s, f"{s.mode}_model", model.key)
|
|
937
|
+
c.print(f"[green]{s.mode.capitalize()} model: {escape(model.name)}.[/]")
|
|
936
938
|
if installed is not None:
|
|
937
939
|
_, missing = catalog.chosen_files(model, installed)
|
|
938
940
|
if missing:
|
|
939
941
|
size = sum(f.gb for f in missing)
|
|
940
|
-
c.print(f"[dim]Not installed yet: {size:.1f} GB download with your next
|
|
942
|
+
c.print(f"[dim]Not installed yet: {size:.1f} GB download with your next {s.mode} (you're asked first).[/]")
|
|
941
943
|
|
|
942
944
|
|
|
943
945
|
def switch_model(agent: Agent, wanted: str, hardware: Hardware) -> None:
|
|
@@ -1347,10 +1349,7 @@ def repl(agent: Agent, prompt: str | None, hardware: Hardware, cont: bool = Fals
|
|
|
1347
1349
|
if info:
|
|
1348
1350
|
resume_session(agent, info)
|
|
1349
1351
|
session_start(agent)
|
|
1350
|
-
if prompt and agent.settings.mode
|
|
1351
|
-
agent.console.print(f"[yellow]{escape(modes.ready(agent))}[/]")
|
|
1352
|
-
prompt = None
|
|
1353
|
-
if prompt and agent.settings.mode == "image":
|
|
1352
|
+
if prompt and agent.settings.mode in ("image", "video"):
|
|
1354
1353
|
respond(agent, prompt)
|
|
1355
1354
|
return
|
|
1356
1355
|
if prompt:
|
|
@@ -1367,7 +1366,7 @@ def repl(agent: Agent, prompt: str | None, hardware: Hardware, cont: bool = Fals
|
|
|
1367
1366
|
if agent.settings.mode == "code":
|
|
1368
1367
|
preload(agent, session)
|
|
1369
1368
|
else:
|
|
1370
|
-
warming = media.warm(agent)
|
|
1369
|
+
warming = media.warm(agent)
|
|
1371
1370
|
if warming:
|
|
1372
1371
|
agent.say(warming)
|
|
1373
1372
|
agent.console.print(f"[yellow]{escape(modes.ready(agent))}[/]")
|
|
@@ -1396,17 +1395,18 @@ def repl(agent: Agent, prompt: str | None, hardware: Hardware, cont: bool = Fals
|
|
|
1396
1395
|
|
|
1397
1396
|
def respond(agent: Agent, line: str) -> None:
|
|
1398
1397
|
"""What a typed request does: the coding agent works on it, or image and video mode take it."""
|
|
1399
|
-
if agent.settings.mode
|
|
1398
|
+
if agent.settings.mode in ("image", "video"): # no coding model involved
|
|
1400
1399
|
make_image(agent, line)
|
|
1401
1400
|
return
|
|
1402
|
-
if agent.settings.mode != "code":
|
|
1403
|
-
agent.console.print(f"[yellow]{escape(modes.ready(agent))}[/]")
|
|
1404
|
-
return
|
|
1405
1401
|
agent.console.print(Rule(style="dim"))
|
|
1406
1402
|
run_safely(agent, line)
|
|
1407
1403
|
|
|
1408
1404
|
|
|
1409
|
-
IMAGE_COMMANDS = (
|
|
1405
|
+
IMAGE_COMMANDS = (
|
|
1406
|
+
"/options", "/again", "/vary", "/gallery", "/open", "/play", "/edit", "/use", "/extend", "/upscale", "/nobg",
|
|
1407
|
+
"/animate",
|
|
1408
|
+
) # fmt: skip
|
|
1409
|
+
VIDEO_TOO = ("/options", "/again", "/vary", "/gallery", "/open", "/play", "/use") # what video mode shares
|
|
1410
1410
|
NUMBER = re.compile(r"^#?(\d+)$")
|
|
1411
1411
|
|
|
1412
1412
|
|
|
@@ -1418,9 +1418,10 @@ def image_command(agent: Agent, cmd: str, arg: str) -> None:
|
|
|
1418
1418
|
from lcode import edits, imageopts
|
|
1419
1419
|
|
|
1420
1420
|
c = agent.console
|
|
1421
|
+
video = agent.settings.mode == "video"
|
|
1421
1422
|
if cmd == "/options":
|
|
1422
1423
|
if not arg.strip():
|
|
1423
|
-
c.print(f"
|
|
1424
|
+
c.print(f"Options: [bold]{escape(agent.image_options.describe(video))}[/]")
|
|
1424
1425
|
c.print(
|
|
1425
1426
|
'[dim]Set them with /options n=4 ar=16:9 seed=42 quality=best style=photo no="text"; '
|
|
1426
1427
|
f"styles: {', '.join(imageopts.STYLES)}. Flags in a request (--n 4) win for that request.[/]"
|
|
@@ -1431,13 +1432,14 @@ def image_command(agent: Agent, cmd: str, arg: str) -> None:
|
|
|
1431
1432
|
except imageopts.OptionError as e:
|
|
1432
1433
|
c.print(f"[red]{escape(str(e))}[/]")
|
|
1433
1434
|
return
|
|
1434
|
-
c.print(f"
|
|
1435
|
+
c.print(f"Options: [bold]{escape(agent.image_options.describe(video))}[/]")
|
|
1435
1436
|
return
|
|
1436
1437
|
if cmd == "/gallery":
|
|
1437
1438
|
gallery(agent, everything=arg.strip() == "all")
|
|
1438
1439
|
return
|
|
1439
|
-
if agent.settings.mode
|
|
1440
|
-
|
|
1440
|
+
if cmd not in ("/open", "/play") and not (agent.settings.mode == "image" or (video and cmd in VIDEO_TOO)):
|
|
1441
|
+
where = "image or video mode" if cmd in VIDEO_TOO else "image mode"
|
|
1442
|
+
c.print(f"[yellow]{cmd} works in {where}: /mode {'image' if where == 'image mode' else 'video'}.[/]")
|
|
1441
1443
|
return
|
|
1442
1444
|
first, _, rest = arg.strip().partition(" ")
|
|
1443
1445
|
number = first if NUMBER.match(first) else ""
|
|
@@ -1456,13 +1458,13 @@ def image_command(agent: Agent, cmd: str, arg: str) -> None:
|
|
|
1456
1458
|
return
|
|
1457
1459
|
entry = media.result(agent, f"#{NUMBER.match(number).group(1)}" if number else "")
|
|
1458
1460
|
source = Path(entry["path"])
|
|
1459
|
-
if cmd
|
|
1461
|
+
if cmd in ("/open", "/play"):
|
|
1460
1462
|
problem = media.open_file(source)
|
|
1461
1463
|
c.print(f"[yellow]{escape(problem)}[/]" if problem else f"[dim]Opened #{entry['n']}.[/]")
|
|
1462
1464
|
return
|
|
1463
1465
|
if cmd == "/vary":
|
|
1464
1466
|
job = dict(entry.get("job") or {"kind": "image", "sources": [], "mask": None, "pads": None})
|
|
1465
|
-
if entry.get("kind") in ("upscale", "remove-background") or job["kind"] not in ("image", "edit"):
|
|
1467
|
+
if entry.get("kind") in ("upscale", "remove-background") or job["kind"] not in ("image", "edit", "video"):
|
|
1466
1468
|
c.print(f"[yellow]#{entry['n']} is {escape(entry['prompt'])}: /vary the image it came from.[/]")
|
|
1467
1469
|
return
|
|
1468
1470
|
job["prompt"] = entry["prompt"] if job["kind"] == "image" else job.get("prompt", entry["prompt"])
|
|
@@ -1471,6 +1473,17 @@ def image_command(agent: Agent, cmd: str, arg: str) -> None:
|
|
|
1471
1473
|
c.print(f"[dim]Variations of #{entry['n']}: {escape(entry['prompt'])}[/]")
|
|
1472
1474
|
make_image(agent, run=lambda cancel, ask: media.redo(agent, job, cancel, ask))
|
|
1473
1475
|
return
|
|
1476
|
+
if cmd == "/animate":
|
|
1477
|
+
if not edits.is_image(source):
|
|
1478
|
+
c.print(f"[yellow]#{entry['n']} isn't a picture: /animate takes an image.[/]")
|
|
1479
|
+
return
|
|
1480
|
+
for line in modes.switch(agent, "video"):
|
|
1481
|
+
c.print(f"[dim]{escape(line)}[/]")
|
|
1482
|
+
text, options = imageopts.parse(rest, imageopts.replace_seed(agent.image_options, n=1))
|
|
1483
|
+
job = {"kind": "video", "prompt": text or entry["prompt"], "options": options, "sources": [str(source)],
|
|
1484
|
+
"mask": None, "pads": None, "keep_shape": "--ar" not in rest and "--size" not in rest} # fmt: skip
|
|
1485
|
+
make_image(agent, run=lambda cancel, ask: media.redo(agent, job, cancel, ask))
|
|
1486
|
+
return
|
|
1474
1487
|
if cmd == "/edit":
|
|
1475
1488
|
text, refs = edits.split_references(rest)
|
|
1476
1489
|
instruction, options = imageopts.parse(text, imageopts.replace_seed(agent.image_options))
|
|
@@ -1571,6 +1584,7 @@ def make_image(agent: Agent, prompt: str = "", options=None, run=None) -> None:
|
|
|
1571
1584
|
answer = steering.ask(f" {question} [y/N] ") if reading else input(f" {question} [y/N] ")
|
|
1572
1585
|
return answer.strip().lower() in ("y", "yes")
|
|
1573
1586
|
|
|
1587
|
+
started = time.monotonic()
|
|
1574
1588
|
try:
|
|
1575
1589
|
asking = ask if agent.interactive else None
|
|
1576
1590
|
if run is not None:
|
|
@@ -1593,6 +1607,7 @@ def make_image(agent: Agent, prompt: str = "", options=None, run=None) -> None:
|
|
|
1593
1607
|
agent.unsent = typed
|
|
1594
1608
|
agent.console.print(Text("✎ What you typed is in the prompt below.", style="cyan"))
|
|
1595
1609
|
agent.save() # the gallery and options carry over when the session is resumed
|
|
1610
|
+
agent.finished(prompt or "image or video", time.monotonic() - started) # a long render: notify
|
|
1596
1611
|
|
|
1597
1612
|
|
|
1598
1613
|
def preload(agent: Agent, session: PromptSession | None = None) -> None:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "ComfyUI's template video_wan2_2_5B_ti2v, in API format",
|
|
3
|
+
"negative_note": "Wan's own default negative prompt (Chinese): oversaturated, overexposed, static, blurry details, subtitles, worst quality, extra fingers, deformed limbs, cluttered background, walking backwards…",
|
|
4
|
+
"defaults": {
|
|
5
|
+
"width": 1280,
|
|
6
|
+
"height": 704,
|
|
7
|
+
"length": 121,
|
|
8
|
+
"fps": 24,
|
|
9
|
+
"steps": 20,
|
|
10
|
+
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
|
|
11
|
+
},
|
|
12
|
+
"labels": {
|
|
13
|
+
"1": "loading the model",
|
|
14
|
+
"2": "loading the text encoder",
|
|
15
|
+
"3": "loading the VAE",
|
|
16
|
+
"5": "reading the prompt",
|
|
17
|
+
"6": "reading the prompt",
|
|
18
|
+
"7": "preparing the frames",
|
|
19
|
+
"8": "drawing the frames",
|
|
20
|
+
"9": "decoding the frames",
|
|
21
|
+
"10": "making the video",
|
|
22
|
+
"11": "saving"
|
|
23
|
+
},
|
|
24
|
+
"graph": {
|
|
25
|
+
"1": {
|
|
26
|
+
"class_type": "UNETLoader",
|
|
27
|
+
"inputs": {
|
|
28
|
+
"unet_name": "$model",
|
|
29
|
+
"weight_dtype": "default"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"2": {
|
|
33
|
+
"class_type": "CLIPLoader",
|
|
34
|
+
"inputs": {
|
|
35
|
+
"clip_name": "$text_encoder",
|
|
36
|
+
"type": "wan",
|
|
37
|
+
"device": "default"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"3": {
|
|
41
|
+
"class_type": "VAELoader",
|
|
42
|
+
"inputs": {
|
|
43
|
+
"vae_name": "$vae"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"4": {
|
|
47
|
+
"class_type": "ModelSamplingSD3",
|
|
48
|
+
"inputs": {
|
|
49
|
+
"model": [
|
|
50
|
+
"1",
|
|
51
|
+
0
|
|
52
|
+
],
|
|
53
|
+
"shift": 8
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"5": {
|
|
57
|
+
"class_type": "CLIPTextEncode",
|
|
58
|
+
"inputs": {
|
|
59
|
+
"text": "$prompt",
|
|
60
|
+
"clip": [
|
|
61
|
+
"2",
|
|
62
|
+
0
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"6": {
|
|
67
|
+
"class_type": "CLIPTextEncode",
|
|
68
|
+
"inputs": {
|
|
69
|
+
"text": "$negative",
|
|
70
|
+
"clip": [
|
|
71
|
+
"2",
|
|
72
|
+
0
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"7": {
|
|
77
|
+
"class_type": "Wan22ImageToVideoLatent",
|
|
78
|
+
"inputs": {
|
|
79
|
+
"vae": [
|
|
80
|
+
"3",
|
|
81
|
+
0
|
|
82
|
+
],
|
|
83
|
+
"width": "$width",
|
|
84
|
+
"height": "$height",
|
|
85
|
+
"length": "$length",
|
|
86
|
+
"batch_size": 1
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"8": {
|
|
90
|
+
"class_type": "KSampler",
|
|
91
|
+
"inputs": {
|
|
92
|
+
"model": [
|
|
93
|
+
"4",
|
|
94
|
+
0
|
|
95
|
+
],
|
|
96
|
+
"seed": "$seed",
|
|
97
|
+
"steps": "$steps",
|
|
98
|
+
"cfg": 5,
|
|
99
|
+
"sampler_name": "uni_pc",
|
|
100
|
+
"scheduler": "simple",
|
|
101
|
+
"positive": [
|
|
102
|
+
"5",
|
|
103
|
+
0
|
|
104
|
+
],
|
|
105
|
+
"negative": [
|
|
106
|
+
"6",
|
|
107
|
+
0
|
|
108
|
+
],
|
|
109
|
+
"latent_image": [
|
|
110
|
+
"7",
|
|
111
|
+
0
|
|
112
|
+
],
|
|
113
|
+
"denoise": 1
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"9": {
|
|
117
|
+
"class_type": "VAEDecode",
|
|
118
|
+
"inputs": {
|
|
119
|
+
"samples": [
|
|
120
|
+
"8",
|
|
121
|
+
0
|
|
122
|
+
],
|
|
123
|
+
"vae": [
|
|
124
|
+
"3",
|
|
125
|
+
0
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"10": {
|
|
130
|
+
"class_type": "CreateVideo",
|
|
131
|
+
"inputs": {
|
|
132
|
+
"images": [
|
|
133
|
+
"9",
|
|
134
|
+
0
|
|
135
|
+
],
|
|
136
|
+
"fps": "$fps"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"11": {
|
|
140
|
+
"class_type": "SaveVideo",
|
|
141
|
+
"inputs": {
|
|
142
|
+
"video": [
|
|
143
|
+
"10",
|
|
144
|
+
0
|
|
145
|
+
],
|
|
146
|
+
"filename_prefix": "lcode/video",
|
|
147
|
+
"format": "mp4",
|
|
148
|
+
"codec": "auto"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.44.0
|
|
4
4
|
Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
|
|
5
5
|
Project-URL: Homepage, https://nasser1941.github.io/lcode/
|
|
6
6
|
Project-URL: Documentation, https://nasser1941.github.io/lcode/
|
|
@@ -107,8 +107,9 @@ through [Ollama](https://ollama.com), and when it needs current information it c
|
|
|
107
107
|
`lcode --worktree` runs parallel sessions on one repository.
|
|
108
108
|
- **In your editor.** Zed, JetBrains IDEs, Neovim and other editors that speak the Agent Client
|
|
109
109
|
Protocol run lcode with `lcode acp`, with diffs and approvals in the editor.
|
|
110
|
-
- **Image
|
|
111
|
-
seconds
|
|
110
|
+
- **Image and video modes.** `/mode image`, describe a picture, and FLUX.2 klein 4B makes it on your
|
|
111
|
+
GPU in seconds; edit it, extend it, upscale it or cut it out. `/mode video` makes clips with
|
|
112
|
+
Wan 2.2, from a description or a start image. lcode drives ComfyUI itself
|
|
112
113
|
([modes](https://nasser1941.github.io/lcode/modes/)).
|
|
113
114
|
- **Automation.** `lcode -p --output json` for scripts and CI, a Python API (`from lcode import
|
|
114
115
|
Session`), and a GitHub Action that answers `@lcode` in issues and pull requests on your own
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
lcode/__init__.py,sha256=
|
|
1
|
+
lcode/__init__.py,sha256=RsG17qPpn7di8jUUOV-g7wyfYOlPM9AIXTiqeUvvG8Y,473
|
|
2
2
|
lcode/__main__.py,sha256=s5N5pryX2FAu93GGZ3FxUrIXHWGzxz66lKUoaV_h8Ws,35
|
|
3
3
|
lcode/acp.py,sha256=d97VV3sbqXdWKhPQQNbYRaDTm8WrUk0mGbDLiOyX7jE,29058
|
|
4
4
|
lcode/action.py,sha256=wuT5F9EJCdEligPeytzmEi8FpahWBhXer528tpuTj_k,16711
|
|
5
|
-
lcode/agent.py,sha256=
|
|
6
|
-
lcode/api.py,sha256=
|
|
5
|
+
lcode/agent.py,sha256=i2Ks-VSzmBKtq1L0B3yrqsjKKHToxJAH2t3miC5qNWE,88955
|
|
6
|
+
lcode/api.py,sha256=DkAWgcoPk5rwp_NPrnV5RbAnA8QKrvDwpaJzTPAfxxA,17704
|
|
7
7
|
lcode/backends.py,sha256=343alwyDZin-bEhb5-Es4f8ek1oBQUzEvV6RFWR1VZo,21160
|
|
8
8
|
lcode/bench.py,sha256=HxeUfy9Kv_0Rug1HAlDLyHYfYd9dw2bMgbXsvUy9nd4,43096
|
|
9
9
|
lcode/bench_backend.py,sha256=X2fo-zRX92Xp3KMxSsFlWioc2vRupBTtowj5kLME8mw,14287
|
|
@@ -11,12 +11,12 @@ lcode/bench_runtime.py,sha256=EjzNgIvUkdbT8cbSy1TpmlyeIk6e-ipjlDFpM7Dnqx0,6835
|
|
|
11
11
|
lcode/catalog.py,sha256=pZ1azX9OH2r4JE2c6E5OfwywpgS39-W2oXzoCHJhmtM,4141
|
|
12
12
|
lcode/checkpoints.py,sha256=eqPtmPe44-aweWqc2H21ROogwzIt3NpvcUobsaCHoGY,18354
|
|
13
13
|
lcode/checks.py,sha256=haYYIPhadKavGYEhh8YWAaWrPMcfzrpT2i6638f4xjQ,12933
|
|
14
|
-
lcode/cli.py,sha256=
|
|
14
|
+
lcode/cli.py,sha256=gmbgWdKxLeBg-9uANbZKV9aIeafudcRfFXGE8BQK0oQ,51230
|
|
15
15
|
lcode/clipboard.py,sha256=nTSdJyCXPdtjH947RjvB3QvRqnL5HZsS9KB8EkC42cc,5138
|
|
16
16
|
lcode/cloud.py,sha256=q5Dl5GuqWvKzBCIso-eh6yomvEOsnQ0LW8ZgaAGwBxc,2131
|
|
17
17
|
lcode/codesearch.py,sha256=Io8leN1nb_Y-Dvlm9xaGtwUWYXX-ooswuznJV6dGU1s,10766
|
|
18
18
|
lcode/comfy.py,sha256=_pbLVQS_1clsa7awPrxQ7xIOwp5XzyMHPDAagomEswc,26614
|
|
19
|
-
lcode/config.py,sha256=
|
|
19
|
+
lcode/config.py,sha256=cOPG7SqsTEiPwPan_0ILtdLY1m8Qbk6WGyMrWZf5iz4,13989
|
|
20
20
|
lcode/context.py,sha256=wgK16ZcNNnq4ngHx0EmtyKHDFTImciGEmtC5NP8tKJQ,20435
|
|
21
21
|
lcode/edits.py,sha256=iYXSbKv4ZfxP12B1ov6-CLEWvR_tWFUdHRQbER3O8d0,13340
|
|
22
22
|
lcode/escalate.py,sha256=f-_6_moPaHi2kLuJw2JtkzvTkCl6DoBQgYBOCYLhunM,15120
|
|
@@ -25,17 +25,17 @@ lcode/frontmatter.py,sha256=Bx0n4vLPhp7ZCWx-g7xqzqNy5VoKBxyeeikh5UFB_gA,2441
|
|
|
25
25
|
lcode/gitflow.py,sha256=6j_nnCBZXkX1qhTI7K4CyZ5D0NAZLpH96weFEo64AkQ,27913
|
|
26
26
|
lcode/hardware.py,sha256=2VbknZCxTZJwJUD8YWdQ7ryEQo_LcjtoOBOHOU9Y0fw,3085
|
|
27
27
|
lcode/hooks.py,sha256=K0itOiAuH4rxndJ9gne_uCrqby6oWAzFPRrfB3tCq-E,9324
|
|
28
|
-
lcode/imageopts.py,sha256=
|
|
28
|
+
lcode/imageopts.py,sha256=msg8Y3OoP6sg1j3L_0K_WhtwdkV2fQl4_IUMq0QsoCE,8610
|
|
29
29
|
lcode/jobs.py,sha256=P1P3FqI6EqL4LgdMX9YGZwCW0Bh5BRXFA_FnQxpHNjE,5618
|
|
30
30
|
lcode/limits.py,sha256=oJzVQoMyTZ0_Fkpu5t3qxrf9v7Hv9S8U8BRQR5M1aKA,1237
|
|
31
31
|
lcode/lsp.py,sha256=NtSW7xapv7fg2HAQLuYK1nwIqT7nG0LK1hiTpOmla_4,26086
|
|
32
32
|
lcode/matching.py,sha256=2N3_qwKJ5gd8el2m_y_egKkFM9rsF1eUhYJcRCVIp98,14015
|
|
33
|
-
lcode/media.py,sha256=
|
|
34
|
-
lcode/media.toml,sha256=
|
|
35
|
-
lcode/media_catalog.py,sha256=
|
|
33
|
+
lcode/media.py,sha256=MZ4ZSuehInueDRvyYCm1BXRePM5IWk5NE23VGw37C8c,33647
|
|
34
|
+
lcode/media.toml,sha256=gEVwlSLYWOFsYaLj_3J6crY5J6weHYKaqgITtHXs2q4,6010
|
|
35
|
+
lcode/media_catalog.py,sha256=ABHlhvZEAZmei1YzNs70_1kyaS0h0TEzbLZWLISNCkk,14363
|
|
36
36
|
lcode/memory.py,sha256=WZEHTp-lVxAwS-yyLQR2xB3TMZ9sejmX2Ivg7T-tl3w,21949
|
|
37
37
|
lcode/models.toml,sha256=lt2G64zzo_Ryxv2ISRSNZoVD_JqCqYqsM01n8WoXGs8,4098
|
|
38
|
-
lcode/modes.py,sha256=
|
|
38
|
+
lcode/modes.py,sha256=MG2OuwGxzbPxzP5ax_pdPPxMsjEd5Lv4kCQbCoFKaNo,5095
|
|
39
39
|
lcode/notify.py,sha256=uieDeXe7XwuVHjOk4tqVyZEW_85XWkoWQmFOWEf77oc,1538
|
|
40
40
|
lcode/ollama.py,sha256=xFHU2XW47ANJ34-1aeSMQVj28GhqcWiFi6zASH9FiYY,8053
|
|
41
41
|
lcode/permissions.py,sha256=q7CjDsc_Xrblw7t3_BbxA5LUjxFSswGs1RiJmtMi0po,12288
|
|
@@ -43,7 +43,7 @@ lcode/planning.py,sha256=Q2upaoZPnXDMQT2F-ETxX1lk_wDntTzhSKNVYIQSoQA,6540
|
|
|
43
43
|
lcode/preview.py,sha256=ATzEs6yaI1__bfo2_juWznrTDXUD4QTc_aQMPRCytgA,6253
|
|
44
44
|
lcode/render.py,sha256=gMvf4W3qkLUJ4Vot3az_hP_zRDAItJC9pP-68dHFkfA,1375
|
|
45
45
|
lcode/repeats.py,sha256=OTrPdb6BKArDLQEgHaXmJZ-tIFsY_1bk4P0wzzANKLY,5964
|
|
46
|
-
lcode/repl.py,sha256=
|
|
46
|
+
lcode/repl.py,sha256=VLbVvjGtukGUXOP-Wuj29KbGcLiDnD57CoDexW3dUFU,73153
|
|
47
47
|
lcode/repomap.py,sha256=1GRJgEUjBl9XHSXsF7tSGXBCEgsY-3CzwMohaTudZ_M,13969
|
|
48
48
|
lcode/residency.py,sha256=B9pwqG6yx5aQD2M5xyQ-bMnYmhD7fv3wCVw_X6VIeAo,7012
|
|
49
49
|
lcode/review.py,sha256=0QBIS-R1B2b3zqx5TcC1qZptpKBbrDhYvMKSNCaDAUo,4686
|
|
@@ -78,9 +78,10 @@ lcode/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
78
78
|
lcode/workflows/flux2-klein-4b-t2i.json,sha256=yGrYDT8pG2bns9DS1JavhlX5fwhDgIKfAwrHrRCyE8k,1788
|
|
79
79
|
lcode/workflows/remove-background.json,sha256=iMNaXqlkcWN-mCQWezD3NUbW94cHN3JcPYsSUsfOl64,794
|
|
80
80
|
lcode/workflows/upscale-esrgan.json,sha256=quDmTnncvycggskgvw6zvfNsub7mS0WY3a4J99fWeXc,745
|
|
81
|
+
lcode/workflows/wan22-ti2v-5b.json,sha256=RcTehoC2pNizzlb7Ed3keJ56DiE7V5lhXmsvR2Y0smQ,3371
|
|
81
82
|
lcode/workflows/z-image-turbo-t2i.json,sha256=sHsL7xnnuULOuWGYlQrDl4jOTySdAmb2T82c_cChv0s,1585
|
|
82
|
-
lcode_cli-0.
|
|
83
|
-
lcode_cli-0.
|
|
84
|
-
lcode_cli-0.
|
|
85
|
-
lcode_cli-0.
|
|
86
|
-
lcode_cli-0.
|
|
83
|
+
lcode_cli-0.44.0.dist-info/METADATA,sha256=bRKpVWXONhz8JdV7UgzC7HdXXCvNHiKaet245hR8-4Q,13469
|
|
84
|
+
lcode_cli-0.44.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
85
|
+
lcode_cli-0.44.0.dist-info/entry_points.txt,sha256=Hxsn_itbnbgft3U0FwxWdmEt0yoAV2LB-nJDnL9WT20,172
|
|
86
|
+
lcode_cli-0.44.0.dist-info/licenses/LICENSE,sha256=JWjmoL7nNIC1XUa-dZm2hwLUgjUQMFo6k8APb0idNzs,1074
|
|
87
|
+
lcode_cli-0.44.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|