comfyui-skill-cli 0.2.2__tar.gz → 0.2.3__tar.gz

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 (28) hide show
  1. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/PKG-INFO +1 -1
  2. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/PKG-INFO +1 -1
  3. comfyui_skill_cli-0.2.3/comfyui_skills_cli/__init__.py +1 -0
  4. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/run.py +58 -16
  5. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/workflow.py +41 -11
  6. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/main.py +8 -0
  7. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/pyproject.toml +1 -1
  8. comfyui_skill_cli-0.2.2/comfyui_skills_cli/commands/__init__.py +0 -0
  9. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/LICENSE +0 -0
  10. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/README.md +0 -0
  11. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/SOURCES.txt +0 -0
  12. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/dependency_links.txt +0 -0
  13. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/entry_points.txt +0 -0
  14. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/requires.txt +0 -0
  15. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skill_cli.egg-info/top_level.txt +0 -0
  16. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/__main__.py +0 -0
  17. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/client.py +0 -0
  18. {comfyui_skill_cli-0.2.2/comfyui_skills_cli → comfyui_skill_cli-0.2.3/comfyui_skills_cli/commands}/__init__.py +0 -0
  19. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/config.py +0 -0
  20. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/deps.py +0 -0
  21. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/history.py +0 -0
  22. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/server.py +0 -0
  23. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/skill.py +0 -0
  24. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/commands/upload.py +0 -0
  25. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/config.py +0 -0
  26. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/output.py +0 -0
  27. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/comfyui_skills_cli/storage.py +0 -0
  28. {comfyui_skill_cli-0.2.2 → comfyui_skill_cli-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -4,11 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  import copy
6
6
  import json
7
+ import logging
7
8
  import os
8
9
  import time
9
10
  import uuid
11
+ from pathlib import Path
10
12
  from typing import Any
11
13
 
14
+ logger = logging.getLogger(__name__)
15
+
12
16
  import typer
13
17
 
14
18
  from ..client import ComfyUIClient
@@ -28,7 +32,7 @@ def run_cmd(
28
32
  ):
29
33
  """Execute a skill (blocking — waits for completion)."""
30
34
  base_dir, server_id, workflow_id = _resolve_skill(ctx, skill_id)
31
- client, schema_data, workflow_data = _prepare(ctx, base_dir, server_id, workflow_id)
35
+ client, schema_data, workflow_data, server_config = _prepare(ctx, base_dir, server_id, workflow_id)
32
36
 
33
37
  input_args = _parse_args(ctx, args)
34
38
  parameters = _get_parameters(schema_data)
@@ -65,8 +69,9 @@ def run_cmd(
65
69
  status_info = history.get("status", {})
66
70
 
67
71
  if status_info.get("completed", False) or outputs:
68
- images = _collect_outputs(outputs)
69
- output_event(ctx, "completed", prompt_id=prompt_id, outputs=images)
72
+ collected = _collect_outputs(outputs)
73
+ collected = _download_outputs(client, collected, base_dir, server_config)
74
+ output_event(ctx, "completed", prompt_id=prompt_id, outputs=collected)
70
75
 
71
76
  # Final result — json and stream-json both get this
72
77
  if fmt == OutputFormat.STREAM_JSON:
@@ -74,7 +79,7 @@ def run_cmd(
74
79
  output_result(ctx, {
75
80
  "status": "success",
76
81
  "prompt_id": prompt_id,
77
- "outputs": images,
82
+ "outputs": collected,
78
83
  })
79
84
  return
80
85
 
@@ -118,7 +123,7 @@ def submit_cmd(
118
123
  ):
119
124
  """Submit a skill for execution (non-blocking — returns immediately)."""
120
125
  base_dir, server_id, workflow_id = _resolve_skill(ctx, skill_id)
121
- client, schema_data, workflow_data = _prepare(ctx, base_dir, server_id, workflow_id)
126
+ client, schema_data, workflow_data, _server_config = _prepare(ctx, base_dir, server_id, workflow_id)
122
127
 
123
128
  input_args = _parse_args(ctx, args)
124
129
  parameters = _get_parameters(schema_data)
@@ -157,8 +162,8 @@ def status_cmd(
157
162
  status_info = history.get("status", {})
158
163
  outputs = history.get("outputs", {})
159
164
  if status_info.get("completed", False) or outputs:
160
- images = _collect_outputs(outputs)
161
- output_result(ctx, {"status": "success", "prompt_id": prompt_id, "outputs": images})
165
+ collected = _collect_outputs(outputs)
166
+ output_result(ctx, {"status": "success", "prompt_id": prompt_id, "outputs": collected})
162
167
  return
163
168
  if status_info.get("status_str") == "error":
164
169
  output_result(ctx, {"status": "error", "prompt_id": prompt_id, "error": _format_errors(history)})
@@ -203,7 +208,7 @@ def _prepare(ctx: typer.Context, base_dir: Any, server_id: str, workflow_id: str
203
208
  output_error(ctx, "SKILL_NOT_FOUND", f'Skill "{server_id}/{workflow_id}" not found.')
204
209
 
205
210
  client = _build_client(server_config)
206
- return client, schema_data or {}, workflow_data
211
+ return client, schema_data or {}, workflow_data, server_config
207
212
 
208
213
 
209
214
  def _build_client(server_config: dict[str, Any]) -> ComfyUIClient:
@@ -249,18 +254,55 @@ def _inject_params(
249
254
  return workflow
250
255
 
251
256
 
257
+ _MEDIA_KEYS: dict[str, str] = {
258
+ "images": "image",
259
+ "audio": "audio",
260
+ "gifs": "image",
261
+ "video": "video",
262
+ }
263
+
264
+
252
265
  def _collect_outputs(outputs: dict[str, Any]) -> list[dict[str, str]]:
253
- images = []
266
+ collected: list[dict[str, str]] = []
254
267
  for node_output in outputs.values():
255
268
  if not isinstance(node_output, dict):
256
269
  continue
257
- for img in node_output.get("images", []):
258
- images.append({
259
- "filename": img.get("filename", ""),
260
- "subfolder": img.get("subfolder", ""),
261
- "type": img.get("type", "output"),
262
- })
263
- return images
270
+ for key, media_type in _MEDIA_KEYS.items():
271
+ for item in node_output.get(key, []):
272
+ collected.append({
273
+ "filename": item.get("filename", ""),
274
+ "subfolder": item.get("subfolder", ""),
275
+ "type": item.get("type", "output"),
276
+ "media_type": media_type,
277
+ })
278
+ return collected
279
+
280
+
281
+ def _download_outputs(
282
+ client: ComfyUIClient,
283
+ outputs: list[dict[str, str]],
284
+ base_dir: Path,
285
+ server_config: dict[str, Any],
286
+ ) -> list[dict[str, str]]:
287
+ raw_dir = str(server_config.get("output_dir", "./outputs")).strip() or "./outputs"
288
+ output_dir = Path(raw_dir) if Path(raw_dir).is_absolute() else base_dir / raw_dir
289
+ for item in outputs:
290
+ try:
291
+ data = client.download_output(
292
+ item["filename"],
293
+ item.get("subfolder", ""),
294
+ item.get("type", "output"),
295
+ )
296
+ subfolder = item.get("subfolder", "")
297
+ local_dir = output_dir / subfolder if subfolder else output_dir
298
+ local_dir.mkdir(parents=True, exist_ok=True)
299
+ local_path = local_dir / item["filename"]
300
+ local_path.write_bytes(data)
301
+ item["local_path"] = str(local_path)
302
+ except Exception as exc:
303
+ logger.warning("Failed to download %s: %s", item["filename"], exc)
304
+ item["local_path"] = ""
305
+ return outputs
264
306
 
265
307
 
266
308
  def _format_errors(history: dict[str, Any]) -> str:
@@ -57,6 +57,20 @@ _AUTO_EXPOSE_FIELDS: dict[str, dict[str, Any]] = {
57
57
  "filename_prefix": {"exposed": True, "required": False, "description": "Output file prefix"},
58
58
  }
59
59
 
60
+ _MEDIA_TYPE_FIELDS: dict[str, dict[str, dict[str, Any]]] = {
61
+ "audio": {
62
+ "tags": {"exposed": True, "required": True, "description": "Music style/genre tags"},
63
+ "lyrics": {"exposed": True, "required": False, "description": "Song lyrics"},
64
+ "bpm": {"exposed": True, "required": False, "description": "Beats per minute"},
65
+ "duration": {"exposed": True, "required": False, "description": "Audio duration"},
66
+ "seconds": {"exposed": True, "required": False, "description": "Duration in seconds"},
67
+ "language": {"exposed": True, "required": False, "description": "Language code"},
68
+ "keyscale": {"exposed": True, "required": False, "description": "Musical key and scale"},
69
+ "cfg_scale": {"exposed": True, "required": False, "description": "Classifier-free guidance scale"},
70
+ "temperature": {"exposed": True, "required": False, "description": "Sampling temperature"},
71
+ },
72
+ }
73
+
60
74
  _LOAD_IMAGE_CLASSES = {"LoadImage", "LoadImageMask"}
61
75
 
62
76
 
@@ -70,8 +84,17 @@ def _get_type_guess(value: Any) -> str:
70
84
  return "string"
71
85
 
72
86
 
73
- def _extract_schema(workflow_data: dict[str, Any]) -> dict[str, dict[str, Any]]:
74
- """Extract parameters from API-format workflow and build schema."""
87
+ def _extract_schema(workflow_data: dict[str, Any], media_type: str = "image") -> dict[str, dict[str, Any]]:
88
+ """Extract parameters from API-format workflow and build schema.
89
+
90
+ *media_type* selects additional field-exposure rules beyond the base
91
+ set. ``"image"`` (default) uses only the generic rules.
92
+ ``"audio"`` adds audio-specific fields like tags, lyrics, bpm, etc.
93
+ """
94
+ expose_fields = dict(_AUTO_EXPOSE_FIELDS)
95
+ if media_type in _MEDIA_TYPE_FIELDS:
96
+ expose_fields.update(_MEDIA_TYPE_FIELDS[media_type])
97
+
75
98
  raw_params: list[dict[str, Any]] = []
76
99
 
77
100
  for node_id, node in workflow_data.items():
@@ -92,8 +115,8 @@ def _extract_schema(workflow_data: dict[str, Any]) -> dict[str, dict[str, Any]]:
92
115
  exposed, required = True, True
93
116
  description = "Upload an image"
94
117
  field_type = "image"
95
- elif field in _AUTO_EXPOSE_FIELDS:
96
- info = _AUTO_EXPOSE_FIELDS[field]
118
+ elif field in expose_fields:
119
+ info = expose_fields[field]
97
120
  exposed = info["exposed"]
98
121
  required = info["required"]
99
122
  description = info["description"]
@@ -360,11 +383,18 @@ def workflow_import(
360
383
  ctx: typer.Context,
361
384
  json_path: str = typer.Argument(None, help="Path to workflow JSON file (omit when using --from-server)"),
362
385
  name: str = typer.Option("", "--name", "-n", help="Workflow ID (default: derived from filename)"),
386
+ media_type: str = typer.Option("image", "--type", "-t", help="Media type preset for parameter detection: image (default), audio"),
363
387
  from_server: bool = typer.Option(False, "--from-server", help="Import from ComfyUI server userdata"),
364
388
  preview: bool = typer.Option(False, "--preview", help="Preview only, don't import"),
365
389
  check_deps: bool = typer.Option(False, "--check-deps", help="Check dependencies after import"),
366
390
  ):
367
- """Import a workflow from local JSON or ComfyUI server."""
391
+ """Import a workflow from local JSON or ComfyUI server.
392
+
393
+ Use --type to select a parameter detection preset. The default (image)
394
+ detects generic fields like seed, steps, and prompt. Use --type audio
395
+ to also detect audio-specific fields like tags, lyrics, bpm, duration,
396
+ keyscale, language, cfg_scale, and temperature.
397
+ """
368
398
  base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
369
399
  config = load_config(base_dir)
370
400
  server_id = ctx.obj.get("server") or get_default_server_id(config)
@@ -375,9 +405,9 @@ def workflow_import(
375
405
  return
376
406
 
377
407
  if from_server:
378
- _import_from_server(ctx, base_dir, server_id, server_config, name, preview, check_deps)
408
+ _import_from_server(ctx, base_dir, server_id, server_config, name, preview, check_deps, media_type)
379
409
  elif json_path:
380
- _import_from_file(ctx, base_dir, server_id, server_config, json_path, name, preview, check_deps)
410
+ _import_from_file(ctx, base_dir, server_id, server_config, json_path, name, preview, check_deps, media_type)
381
411
  else:
382
412
  output_error(ctx, "INVALID_ARGS", "Provide a JSON file path or use --from-server.")
383
413
 
@@ -385,7 +415,7 @@ def workflow_import(
385
415
  def _import_from_file(
386
416
  ctx: typer.Context, base_dir: Path, server_id: str,
387
417
  server_config: dict[str, Any], json_path: str, name: str,
388
- preview: bool, check_deps: bool,
418
+ preview: bool, check_deps: bool, media_type: str = "image",
389
419
  ) -> None:
390
420
  if not os.path.isfile(json_path):
391
421
  output_error(ctx, "FILE_NOT_FOUND", f'File not found: "{json_path}"')
@@ -425,7 +455,7 @@ def _import_from_file(
425
455
  return
426
456
 
427
457
  # Generate schema
428
- parameters = _extract_schema(api_data)
458
+ parameters = _extract_schema(api_data, media_type)
429
459
 
430
460
  if preview:
431
461
  output_result(ctx, {
@@ -496,7 +526,7 @@ def _import_from_file(
496
526
  def _import_from_server(
497
527
  ctx: typer.Context, base_dir: Path, server_id: str,
498
528
  server_config: dict[str, Any], name_filter: str,
499
- preview: bool, check_deps: bool,
529
+ preview: bool, check_deps: bool, media_type: str = "image",
500
530
  ) -> None:
501
531
  client = ComfyUIClient(
502
532
  server_url=server_config.get("url", "http://127.0.0.1:8188"),
@@ -553,7 +583,7 @@ def _import_from_server(
553
583
 
554
584
  filename = os.path.basename(wf_path)
555
585
  workflow_id = _suggest_workflow_id(api_data, filename)
556
- parameters = _extract_schema(api_data)
586
+ parameters = _extract_schema(api_data, media_type)
557
587
 
558
588
  workflow_dir = base_dir / "data" / server_id / workflow_id
559
589
  workflow_dir.mkdir(parents=True, exist_ok=True)
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import typer
6
6
 
7
+ from . import __version__
7
8
  from .commands import config, deps, history, run, server, skill, upload, workflow
8
9
 
9
10
  app = typer.Typer(
@@ -15,9 +16,16 @@ app = typer.Typer(
15
16
  )
16
17
 
17
18
 
19
+ def _version_callback(value: bool) -> None:
20
+ if value:
21
+ typer.echo(f"comfyui-skill {__version__}")
22
+ raise typer.Exit()
23
+
24
+
18
25
  @app.callback()
19
26
  def main(
20
27
  ctx: typer.Context,
28
+ version: bool = typer.Option(False, "--version", "-V", callback=_version_callback, is_eager=True, help="Show version and exit"),
21
29
  json_output: bool = typer.Option(False, "--json", "-j", help="JSON output (shortcut for --output-format json)"),
22
30
  output_format: str = typer.Option("", "--output-format", help="Output format: text, json, stream-json"),
23
31
  server_id: str = typer.Option("", "--server", "-s", help="Server ID"),
@@ -4,7 +4,7 @@ requires = ["setuptools>=61"]
4
4
 
5
5
  [project]
6
6
  name = "comfyui-skill-cli"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "ComfyUI Skill CLI — Agent-friendly workflow management"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"