diffusers-workflow 0.4.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.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
dw/repl_commands.py ADDED
@@ -0,0 +1,836 @@
1
+ """
2
+ Command handlers for REPL.
3
+
4
+ Contains all the command implementations organized by category:
5
+ - Config commands
6
+ - Argument commands
7
+ - Memory commands
8
+ - Workflow commands
9
+ """
10
+
11
+ import contextlib
12
+ import os
13
+ import shlex
14
+ import logging
15
+ from .security import (
16
+ validate_path,
17
+ validate_workflow_path,
18
+ validate_output_path,
19
+ validate_variable_name,
20
+ validate_string_input,
21
+ SecurityError,
22
+ InvalidInputError,
23
+ MAX_VARIABLE_VALUE_LENGTH,
24
+ MAX_FILE_PATH_LENGTH,
25
+ )
26
+ from .workflow import workflow_from_file
27
+ from .workflow_sources import workflow_names
28
+
29
+ logger = logging.getLogger("dw")
30
+
31
+
32
+ class ConfigCommands:
33
+ """Handles configuration commands."""
34
+
35
+ def __init__(self, repl):
36
+ """Initialize with reference to REPL instance."""
37
+ self.repl = repl
38
+
39
+ def do_config(self, arg: str):
40
+ """Configure global settings. Usage: config ? | show | set <name>=<value>"""
41
+ if not arg or arg == "?":
42
+ print("\nConfig commands:")
43
+ print(" config show - Show all configuration settings")
44
+ print(" config set <name>=<value> - Set a configuration value")
45
+ print("\nAvailable settings:")
46
+ print(" workspace - Directory holding your workflows, prompts,")
47
+ print(" assets and outputs; setting it moves")
48
+ print(" workflow_dir and output_dir with it")
49
+ print(" output_dir - Directory for output files (default: the")
50
+ print(" workspace's outputs/)")
51
+ print(
52
+ " log_level - Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL"
53
+ )
54
+ print(" workflow_dir - Where 'workflow load <name>' and 'workflow list'")
55
+ print(" look for workflows (default: the workspace's")
56
+ print(" workflows/)")
57
+ print()
58
+ print("These apply to this REPL session. Standing settings (device,")
59
+ print("log file, TF32) live in ~/.diffusers_helper/settings.json")
60
+ print()
61
+ return
62
+
63
+ parts = arg.split(None, 1)
64
+ subcommand = parts[0]
65
+ subarg = parts[1] if len(parts) > 1 else ""
66
+
67
+ if subcommand == "show":
68
+ self._config_show(subarg)
69
+ elif subcommand == "set":
70
+ self._config_set(subarg)
71
+ else:
72
+ print(f"Unknown config subcommand: {subcommand}")
73
+ print("Use 'config ?' for help")
74
+
75
+ def _config_show(self, arg: str):
76
+ """Show configuration settings"""
77
+ print("\nCurrent configuration:")
78
+ for name, value in self.repl.globals.items():
79
+ print(f" {name}={value}")
80
+ print()
81
+
82
+ def _config_set(self, arg: str):
83
+ """Set a configuration value"""
84
+ if not arg:
85
+ # If no argument, show all config (backward compatibility with 'set')
86
+ self._config_show(arg)
87
+ return
88
+
89
+ try:
90
+ name, value = arg.split("=", 1)
91
+ name = name.strip()
92
+ value = value.strip()
93
+
94
+ # Special handling for output_dir
95
+ if name == "output_dir":
96
+ try:
97
+ value = validate_output_path(value, None)
98
+ # Check if directory exists
99
+ if not os.path.exists(value):
100
+ print(f"Warning: Directory '{value}' does not exist")
101
+ except SecurityError as e:
102
+ print(f"Error: Invalid output directory: {e}")
103
+ return
104
+
105
+ # Special handling for log_level
106
+ elif name == "log_level":
107
+ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
108
+ value = value.upper()
109
+ if value not in valid_levels:
110
+ print(f"Error: Log level must be one of: {', '.join(valid_levels)}")
111
+ return
112
+ # Update the log level for dw's own logger
113
+ logging.getLogger("dw").setLevel(value)
114
+ print(f"Log level set to {value}")
115
+
116
+ elif name == "workflow_dir":
117
+ try:
118
+ value = validate_path(value, allow_create=False)
119
+ if not os.path.exists(value):
120
+ print(f"Warning: Directory '{value}' does not exist")
121
+ return
122
+ except SecurityError as e:
123
+ print(f"Error: Invalid workflow directory: {e}")
124
+ return
125
+
126
+ # Setting the workspace moves the two directories under it with
127
+ # it - setting either of those alone still overrides just that one
128
+ elif name == "workspace":
129
+ from .workspace import FLAG, Workspace, set_workspace
130
+
131
+ try:
132
+ value = validate_path(value, allow_create=False)
133
+ if not os.path.isdir(value):
134
+ print(f"Warning: Directory '{value}' does not exist")
135
+ return
136
+ except SecurityError as e:
137
+ print(f"Error: Invalid workspace: {e}")
138
+ return
139
+ workspace = set_workspace(Workspace(value, FLAG))
140
+ self.repl.globals["workflow_dir"] = workspace.workflows
141
+ self.repl.globals["output_dir"] = workspace.outputs
142
+ print(f" workflow_dir={workspace.workflows}")
143
+ print(f" output_dir={workspace.outputs}")
144
+ # Warn if a worker is already running - it keeps using the
145
+ # old prompt and asset libraries until restart
146
+ worker_manager = getattr(self.repl, "worker_manager", None)
147
+ if worker_manager and getattr(worker_manager, "worker_active", False):
148
+ print(
149
+ "Note: the running worker keeps the prompt and asset "
150
+ "libraries it started with until 'workflow restart'."
151
+ )
152
+ else:
153
+ print(f"Warning: Unknown setting '{name}'")
154
+
155
+ self.repl.globals[name] = value
156
+ print(f"Set {name}={value}")
157
+
158
+ except ValueError:
159
+ print("Error: Invalid format. Use: config set name=value")
160
+
161
+
162
+ def display_value(value, limit=90):
163
+ """A value shortened for terminal display - a paragraph-long prompt
164
+ default would otherwise bury every other line of arg show."""
165
+ text = str(value)
166
+ text = " ".join(text.split())
167
+ if len(text) > limit:
168
+ return text[: limit - 3] + "..."
169
+ return text
170
+
171
+
172
+ class ArgCommands:
173
+ """Handles workflow argument management commands."""
174
+
175
+ def __init__(self, repl):
176
+ """Initialize with reference to REPL instance."""
177
+ self.repl = repl
178
+
179
+ def do_arg(self, arg: str):
180
+ """Manage workflow arguments. Usage: arg ? | show | set <name>=<value> | clear"""
181
+ if arg == "?":
182
+ print("\nArg commands:")
183
+ print(
184
+ " arg show - Show workflow variables and current values"
185
+ )
186
+ print(" arg set <name>=<value> - Set an argument (quotes optional):")
187
+ print(' arg set prompt="a cat in a hat"')
188
+ print(" arg clear [<name>] - Clear one argument, or all of them")
189
+ print()
190
+ print("Arguments override the workflow's variables on the next run and")
191
+ print("reset when a different workflow is loaded. Shortcuts: 'set n=v'")
192
+ print("is 'arg set', and 'workflow run n=v ...' sets and runs in one line.")
193
+ print()
194
+ return
195
+
196
+ if not arg:
197
+ # If no argument, show args (backward compatibility)
198
+ self._arg_show(arg)
199
+ return
200
+
201
+ parts = arg.split(None, 1)
202
+ subcommand = parts[0]
203
+ subarg = parts[1] if len(parts) > 1 else ""
204
+
205
+ if subcommand == "show":
206
+ self._arg_show(subarg)
207
+ elif subcommand == "set":
208
+ self._arg_set(subarg)
209
+ elif subcommand == "clear":
210
+ self._arg_clear(subarg)
211
+ else:
212
+ # Try to parse as set command for backward compatibility
213
+ self._arg_set(arg)
214
+
215
+ def _arg_show(self, arg: str):
216
+ """Show workflow arguments"""
217
+ if not self.repl.current_workflow:
218
+ print("Error: No workflow loaded. Use 'workflow load' command first")
219
+ return
220
+
221
+ print("\nAvailable variables in workflow and their default values:")
222
+ workflow_vars = self.repl.current_workflow.variables
223
+ if not workflow_vars:
224
+ print(" No variables defined in workflow")
225
+ else:
226
+ for var_name, var_def in workflow_vars.items():
227
+ print(f" {var_name}: {display_value(var_def)}")
228
+
229
+ print("\nCurrent argument values:")
230
+ if not self.repl.workflow_args:
231
+ print(" No arguments set")
232
+ else:
233
+ for name, value in self.repl.workflow_args.items():
234
+ print(f" {name}={display_value(value)}")
235
+ print()
236
+
237
+ def _arg_set(self, arg: str):
238
+ """Set a workflow argument.
239
+
240
+ Returns:
241
+ True when the argument was set, False otherwise - callers that
242
+ set several in a row (workflow run n=v ...) stop at the first
243
+ failure instead of running with half the overrides.
244
+ """
245
+ if not self.repl.current_workflow:
246
+ print("Error: No workflow loaded. Use 'workflow load' command first")
247
+ return False
248
+
249
+ if not arg:
250
+ print("Error: Please specify argument name and value")
251
+ print("Usage: arg set <name>=<value>")
252
+ return False
253
+
254
+ try:
255
+ name, value = arg.split("=", 1)
256
+ name = name.strip()
257
+ value = value.strip()
258
+
259
+ # There is no shell here to strip quotes, so a value quoted the
260
+ # way every example shows it would otherwise keep literal quotes
261
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
262
+ value = value[1:-1]
263
+
264
+ # Validate variable name
265
+ try:
266
+ name = validate_variable_name(name)
267
+ value = validate_string_input(
268
+ value, max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
269
+ )
270
+ except InvalidInputError as e:
271
+ print(f"Error: Invalid input: {e}")
272
+ return False
273
+
274
+ # Verify this is a valid variable name for the workflow
275
+ if name not in self.repl.current_workflow.variables:
276
+ print(f"Error: '{name}' is not defined in workflow variables")
277
+ available = ", ".join(self.repl.current_workflow.variables)
278
+ print(f"Available variables: {available or '(none)'}")
279
+ return False
280
+
281
+ self.repl.workflow_args[name] = value
282
+ print(f"Set argument {name}={value}")
283
+ return True
284
+
285
+ except ValueError:
286
+ print("Error: Invalid format. Use: arg set name=value")
287
+ return False
288
+
289
+ def _arg_clear(self, arg: str):
290
+ """Clear one workflow argument, or all of them"""
291
+ name = arg.strip()
292
+ if name:
293
+ if name in self.repl.workflow_args:
294
+ del self.repl.workflow_args[name]
295
+ print(f"Cleared argument {name}")
296
+ else:
297
+ print(f"Argument '{name}' is not set")
298
+ return
299
+ self.repl.workflow_args = {}
300
+ print("All workflow arguments cleared")
301
+
302
+
303
+ class MemoryCommands:
304
+ """Handles GPU memory management commands."""
305
+
306
+ def __init__(self, repl):
307
+ """Initialize with reference to REPL instance."""
308
+ self.repl = repl
309
+
310
+ def do_memory(self, arg: str):
311
+ """Manage GPU memory. Usage: memory ? | show | clear"""
312
+ if not arg or arg == "?":
313
+ print("\nMemory commands:")
314
+ print(" memory show - Show the worker's GPU memory usage")
315
+ print(
316
+ " memory clear - Release cached models, cached step results, "
317
+ "and empty the device cache"
318
+ )
319
+ print()
320
+ print("The worker process starts on the first 'workflow run', so there")
321
+ print("is nothing to show before that. 'workflow restart' frees")
322
+ print("everything by stopping the worker process itself.")
323
+ print()
324
+ return
325
+
326
+ parts = arg.split(None, 1)
327
+ subcommand = parts[0]
328
+ subarg = parts[1] if len(parts) > 1 else ""
329
+
330
+ if subcommand == "show":
331
+ self._memory_show(subarg)
332
+ elif subcommand == "clear":
333
+ self._memory_clear(subarg)
334
+ else:
335
+ print(f"Unknown memory subcommand: {subcommand}")
336
+ print("Use 'memory ?' for help")
337
+
338
+ def _memory_show(self, arg: str):
339
+ """Show current GPU memory usage"""
340
+ if not self.repl.worker_manager.worker_active:
341
+ print("No worker process running")
342
+ return
343
+
344
+ try:
345
+ self.repl.worker_manager.send_command({"type": "memory_status"})
346
+ result = self.repl.worker_manager.get_result(timeout=5)
347
+
348
+ if result["type"] == "memory_status":
349
+ self.repl._print_memory_info(result.get("info", {}))
350
+ else:
351
+ print(f"Unexpected response: {result}")
352
+ except Exception as e:
353
+ print(f"Error getting memory status: {e}")
354
+
355
+ def _memory_clear(self, arg: str):
356
+ """Clear GPU memory and cached models"""
357
+ if not self.repl.worker_manager.worker_active:
358
+ print("No worker process running")
359
+ return
360
+
361
+ try:
362
+ print("Clearing GPU memory...")
363
+ self.repl.worker_manager.send_command({"type": "clear_memory"})
364
+
365
+ # Wait for response
366
+ result = self.repl.worker_manager.get_result(timeout=30)
367
+ if result["type"] == "memory_cleared":
368
+ self.repl._print_memory_info(result.get("info", {}))
369
+ print("GPU memory cleared successfully")
370
+ else:
371
+ print(f"Unexpected response: {result}")
372
+ except Exception as e:
373
+ print(f"Error clearing memory: {e}")
374
+ self.repl.worker_manager.shutdown_worker()
375
+
376
+
377
+ class WorkflowCommands:
378
+ """Handles workflow management commands."""
379
+
380
+ def __init__(self, repl):
381
+ """Initialize with reference to REPL instance."""
382
+ self.repl = repl
383
+
384
+ def do_workflow(self, arg: str):
385
+ """Manage workflows. Usage: workflow ? | list | load <file> | reload | status | run | restart"""
386
+ if not arg or arg == "?":
387
+ print("\nWorkflow commands:")
388
+ print(
389
+ " workflow list - List workflows in the workflow directory"
390
+ )
391
+ print(
392
+ " workflow load <name> - Load by name from workflow_dir, or by path"
393
+ )
394
+ print(
395
+ " workflow reload - Re-read the current workflow file (keeps args)"
396
+ )
397
+ print(
398
+ " workflow status - Show workflow, arguments and worker state"
399
+ )
400
+ print(" workflow run - Run the loaded workflow")
401
+ print(" workflow run <n>=<v> .. - Set arguments, then run, in one line")
402
+ print(" workflow run ask <arg> - Prompt for one argument value, then run")
403
+ print(
404
+ " workflow restart - Stop the worker process (frees all models)"
405
+ )
406
+ print()
407
+ print("Runs execute in a persistent worker, so models stay loaded between")
408
+ print("runs. After editing the workflow file, just run again - pipelines")
409
+ print("whose definition did not change stay cached. Ctrl+C during a run")
410
+ print("cancels it (models stay cached); a second Ctrl+C stops the worker.")
411
+ print()
412
+ return
413
+
414
+ parts = arg.split(None, 1)
415
+ subcommand = parts[0]
416
+ subarg = parts[1] if len(parts) > 1 else ""
417
+
418
+ if subcommand == "list":
419
+ self._workflow_list(subarg)
420
+ elif subcommand == "load":
421
+ self._workflow_load(subarg)
422
+ elif subcommand == "reload":
423
+ self._workflow_reload(subarg)
424
+ elif subcommand == "status":
425
+ self._workflow_status(subarg)
426
+ elif subcommand == "run":
427
+ self._workflow_run(subarg)
428
+ elif subcommand == "restart":
429
+ self._workflow_restart(subarg)
430
+ else:
431
+ print(f"Unknown workflow subcommand: {subcommand}")
432
+ print("Use 'workflow ?' for help")
433
+
434
+ def workflow_names(self):
435
+ """Workflow names under workflow_dir, as load accepts them.
436
+
437
+ Names are paths relative to workflow_dir with the .json dropped
438
+ ('flux/FluxDev'), sorted for stable listing and completion.
439
+ """
440
+ return workflow_names(self.repl.globals["workflow_dir"])
441
+
442
+ def _workflow_list(self, arg: str):
443
+ """List workflows available in the workflow directory"""
444
+ workflow_dir = self.repl.globals["workflow_dir"]
445
+ names = self.workflow_names()
446
+ if not names:
447
+ print(f"No workflows found in {workflow_dir}")
448
+ print(
449
+ "Point workflow_dir somewhere else with: config set workflow_dir=<path>"
450
+ )
451
+ return
452
+ print(f"\nWorkflows in {workflow_dir}:")
453
+ for name in names:
454
+ print(f" {name}")
455
+ print("\nLoad one with: workflow load <name>")
456
+ print()
457
+
458
+ def _workflow_load(self, arg: str):
459
+ """Load a workflow from a JSON file"""
460
+ if not arg:
461
+ print("Error: Please specify a workflow file path or name")
462
+ return
463
+
464
+ try:
465
+ file_path = validate_string_input(
466
+ arg.strip(), max_length=MAX_FILE_PATH_LENGTH
467
+ )
468
+ except InvalidInputError as e:
469
+ print(f"Error: Invalid file path: {e}")
470
+ return
471
+
472
+ # If this isn't an absolute path or relative path starting with ./ or ../
473
+ if not os.path.isabs(file_path) and not file_path.startswith(("./", "../")):
474
+ # Treat as a workflow name in the default directory
475
+ # Add .json extension if not present
476
+ if not file_path.endswith(".json"):
477
+ file_path = f"{file_path}.json"
478
+ try:
479
+ file_path = validate_path(
480
+ os.path.join(self.repl.globals["workflow_dir"], file_path),
481
+ self.repl.globals["workflow_dir"],
482
+ allow_create=False,
483
+ )
484
+ except SecurityError as e:
485
+ print(f"Error: Invalid workflow path: {e}")
486
+ return
487
+ else:
488
+ try:
489
+ file_path = validate_workflow_path(
490
+ file_path, self.repl.globals["workflow_dir"]
491
+ )
492
+ except SecurityError as e:
493
+ print(f"Error: Invalid workflow path: {e}")
494
+ return
495
+
496
+ if not os.path.exists(file_path):
497
+ print(f"Error: File {file_path} does not exist")
498
+ return
499
+
500
+ try:
501
+ output_dir = self.repl.globals["output_dir"]
502
+ if not os.path.exists(output_dir):
503
+ print(f"Warning: Output directory {output_dir} does not exist")
504
+
505
+ workflow = workflow_from_file(file_path, output_dir)
506
+
507
+ # Try to validate the workflow immediately
508
+ try:
509
+ workflow.validate()
510
+
511
+ # The worker handles workflow switches itself now - it frees
512
+ # the old workflow's models before loading the new one - so
513
+ # loading a different file no longer restarts the process
514
+ self.repl.current_workflow = workflow
515
+ # Clear any existing arguments when loading new workflow
516
+ self.repl.workflow_args = {}
517
+ print(f"Loaded workflow: {workflow.name}")
518
+ print("Workflow validated successfully")
519
+
520
+ except Exception as e:
521
+ print(f"Warning: Workflow validation failed: {str(e)}")
522
+
523
+ except Exception as e:
524
+ print(f"Error loading workflow: {str(e)}")
525
+ self.repl.current_workflow = None
526
+
527
+ def _workflow_reload(self, arg: str):
528
+ """Reload the current workflow from its file"""
529
+ if not self.repl.current_workflow:
530
+ print("Error: No workflow loaded. Use 'workflow load' command first")
531
+ return
532
+
533
+ try:
534
+ file_path = self.repl.current_workflow.file_spec
535
+ print(f"Reloading workflow from: {file_path}")
536
+
537
+ # Load and validate the workflow
538
+ workflow = workflow_from_file(file_path, self.repl.globals["output_dir"])
539
+ workflow.validate()
540
+
541
+ # Replace current workflow
542
+ self.repl.current_workflow = workflow
543
+ print(f"Reloaded workflow: {workflow.name}")
544
+ print("Workflow validated successfully")
545
+
546
+ except Exception as e:
547
+ print(f"Error reloading workflow: {str(e)}")
548
+
549
+ def _workflow_status(self, arg: str):
550
+ """Show current workflow status"""
551
+ if self.repl.current_workflow is None:
552
+ print("No workflow currently loaded")
553
+ print(
554
+ "Use 'workflow list' to see what is available, then 'workflow load <name>'"
555
+ )
556
+ return
557
+
558
+ workflow = self.repl.current_workflow
559
+ print(f"\nCurrent workflow: {workflow.name}")
560
+ print(f" File: {workflow.file_spec}")
561
+ print(f" Output directory: {self.repl.globals['output_dir']}")
562
+
563
+ variables = workflow.variables
564
+ if variables:
565
+ print(" Variables:")
566
+ for name, default in variables.items():
567
+ if name in self.repl.workflow_args:
568
+ print(
569
+ f" {name} = {display_value(self.repl.workflow_args[name])}"
570
+ f" (default: {display_value(default, limit=40)})"
571
+ )
572
+ else:
573
+ print(f" {name} = {display_value(default)}")
574
+ else:
575
+ print(" Variables: none defined")
576
+
577
+ manager = self.repl.worker_manager
578
+ worker_alive = (
579
+ manager.worker_active
580
+ and manager.worker_process is not None
581
+ and manager.worker_process.is_alive()
582
+ )
583
+ if worker_alive:
584
+ print(" Worker: running (models stay cached between runs)")
585
+ else:
586
+ print(" Worker: not running (starts on the next 'workflow run')")
587
+ print()
588
+
589
+ @contextlib.contextmanager
590
+ def _history_suppressed(self):
591
+ """Readline auto-history off for one input, so values are not saved"""
592
+ readline = None
593
+ try:
594
+ import readline
595
+ except ImportError:
596
+ pass
597
+
598
+ suppress = readline is not None and hasattr(readline, "set_auto_history")
599
+ if suppress:
600
+ readline.set_auto_history(False)
601
+ try:
602
+ yield
603
+ finally:
604
+ if suppress:
605
+ readline.set_auto_history(True)
606
+
607
+ def _prompt_for_argument(self, arg_name):
608
+ """Ask the user for one workflow argument's value.
609
+
610
+ Returns:
611
+ True when the value was set, False on invalid input or cancel
612
+ """
613
+ # Validate that the argument exists in the workflow's variables
614
+ if arg_name not in self.repl.current_workflow.variables:
615
+ print(f"Error: '{arg_name}' is not defined in workflow variables")
616
+ print(
617
+ f"Available variables: {', '.join(self.repl.current_workflow.variables.keys())}"
618
+ )
619
+ return False
620
+
621
+ try:
622
+ with self._history_suppressed():
623
+ user_value = input(f"Enter value for '{arg_name}': ").strip()
624
+ except (EOFError, KeyboardInterrupt):
625
+ print("\nCancelled")
626
+ return False
627
+
628
+ try:
629
+ validated_name = validate_variable_name(arg_name)
630
+ validated_value = validate_string_input(
631
+ user_value,
632
+ max_length=MAX_VARIABLE_VALUE_LENGTH,
633
+ allow_empty=True,
634
+ )
635
+ except InvalidInputError as e:
636
+ print(f"Error: Invalid input: {e}")
637
+ return False
638
+
639
+ self.repl.workflow_args[validated_name] = validated_value
640
+ print(f"Set argument {validated_name}={validated_value}")
641
+ print()
642
+ return True
643
+
644
+ def _workflow_run(self, arg: str):
645
+ """Run the currently loaded workflow with set arguments"""
646
+ if not self.repl.current_workflow:
647
+ print("Error: No workflow loaded. Use 'workflow load' command first")
648
+ return
649
+
650
+ # Handle "run ask <arg_name>" subcommand. "ask" with nothing after it
651
+ # (any amount of whitespace) is caught here, so the name is never empty
652
+ if arg.strip() == "ask":
653
+ print("Error: Please specify an argument name")
654
+ print("Usage: workflow run ask <arg_name>")
655
+ return
656
+
657
+ if arg.startswith("ask "):
658
+ if not self._prompt_for_argument(arg[4:].strip()):
659
+ return
660
+ elif arg.strip():
661
+ # Inline overrides: workflow run steps=30 prompt="a cat". Same
662
+ # semantics as arg set for each pair, then run.
663
+ try:
664
+ assignments = shlex.split(arg)
665
+ except ValueError as e:
666
+ print(f"Error: Could not parse arguments: {e}")
667
+ return
668
+ for assignment in assignments:
669
+ if "=" not in assignment:
670
+ print(f"Error: Expected <name>=<value>, got '{assignment}'")
671
+ print("Usage: workflow run [<name>=<value> ...] | ask <arg>")
672
+ return
673
+ for assignment in assignments:
674
+ if not self.repl.arg_commands._arg_set(assignment):
675
+ return
676
+
677
+ try:
678
+ # Validate inputs
679
+ output_dir = validate_output_path(self.repl.globals["output_dir"], None)
680
+ workflow_spec = validate_workflow_path(self.repl.current_workflow.file_spec)
681
+
682
+ # Ensure worker is running
683
+ self.repl.worker_manager.ensure_worker(self.repl.globals["log_level"])
684
+
685
+ print(f"Running workflow: {self.repl.current_workflow.name}")
686
+ if self.repl.workflow_args:
687
+ print(f"Using arguments: {self.repl.workflow_args}")
688
+
689
+ # Send execute command to worker
690
+ self.repl.worker_manager.send_command(
691
+ {
692
+ "type": "execute",
693
+ "workflow_path": workflow_spec,
694
+ "arguments": self.repl.workflow_args,
695
+ "output_dir": output_dir,
696
+ "log_level": self.repl.globals["log_level"],
697
+ }
698
+ )
699
+
700
+ # Process results from worker. There is no fixed timeout - a video
701
+ # generation or cold model download takes however long it takes,
702
+ # progress events stream in while it works, and get_result raises
703
+ # if the worker dies. Ctrl+C asks the worker to cancel the run; a
704
+ # second Ctrl+C gives up on it and shuts the worker down.
705
+ cancelling = False
706
+ inline_progress = False
707
+
708
+ def end_inline():
709
+ nonlocal inline_progress
710
+ if inline_progress:
711
+ print()
712
+ inline_progress = False
713
+
714
+ while True:
715
+ try:
716
+ result = self.repl.worker_manager.get_result()
717
+ except KeyboardInterrupt:
718
+ end_inline()
719
+ if not cancelling:
720
+ cancelling = True
721
+ print("Cancelling... (Ctrl+C again to force-stop the worker)")
722
+ try:
723
+ self.repl.worker_manager.cancel()
724
+ except Exception as e:
725
+ print(f"Could not send cancel: {e}")
726
+ self.repl.worker_manager.shutdown_worker()
727
+ break
728
+ continue
729
+ print("Force-stopping worker...")
730
+ self.repl.worker_manager.shutdown_worker()
731
+ break
732
+ except Exception as e:
733
+ end_inline()
734
+ # A worker killed by a signal sends nothing and leaves no
735
+ # traceback, so its exit code is the only diagnosis there is
736
+ detail = self.repl.worker_manager.crash_details()
737
+ print("\n" + "=" * 80)
738
+ print(f"ERROR receiving results: {detail or e}")
739
+ print("=" * 80)
740
+ print("Worker communication failed. Shutting down worker.\n")
741
+ self.repl.worker_manager.shutdown_worker()
742
+ break
743
+
744
+ result_type = result.get("type")
745
+
746
+ if result_type == "progress":
747
+ event = result.get("event")
748
+ if event == "step_start":
749
+ end_inline()
750
+ print(
751
+ f"[{result['index'] + 1}/{result['total_steps']}] "
752
+ f"{result['step']}"
753
+ )
754
+ elif event == "iteration_start":
755
+ if result["total_iterations"] > 1:
756
+ end_inline()
757
+ print(
758
+ f" iteration {result['iteration']}/"
759
+ f"{result['total_iterations']}"
760
+ )
761
+ elif event == "pipeline_step":
762
+ total = result.get("total_steps") or "?"
763
+ print(
764
+ f"\r denoise step {result['step']}/{total}",
765
+ end="",
766
+ flush=True,
767
+ )
768
+ inline_progress = True
769
+ elif event == "step_end":
770
+ end_inline()
771
+ for saved_file in result.get("files", []):
772
+ print(f" saved {saved_file}")
773
+ # workflow_start / workflow_end stay quiet - the step
774
+ # lines carry the story
775
+ elif result_type == "output":
776
+ end_inline()
777
+ print(result["message"])
778
+ elif result_type == "workflow_loaded":
779
+ end_inline()
780
+ print(f"Workflow loaded: {result['workflow_name']}")
781
+ elif result_type == "memory_info":
782
+ end_inline()
783
+ self.repl._print_memory_info(result["info"])
784
+ elif result_type == "success":
785
+ end_inline()
786
+ print(result["message"])
787
+ break
788
+ elif result_type == "cancelled":
789
+ end_inline()
790
+ print(result.get("message", "Workflow run cancelled"))
791
+ break
792
+ elif result_type == "error":
793
+ end_inline()
794
+ print("\n" + "=" * 80)
795
+ print(f"ERROR: {result['message']}")
796
+ if "traceback" in result:
797
+ print("\nTraceback:")
798
+ print(result["traceback"])
799
+ print("=" * 80)
800
+ print("Worker process encountered an error and stopped.\n")
801
+ break
802
+ elif result_type == "worker_crashed":
803
+ end_inline()
804
+ print("\n" + "=" * 80)
805
+ print(f"WORKER CRASHED: {result['message']}")
806
+ if "traceback" in result:
807
+ print("\nTraceback:")
808
+ print(result["traceback"])
809
+ print("=" * 80)
810
+ print(
811
+ "Worker process has terminated. Use 'workflow restart' to start a new worker.\n"
812
+ )
813
+ # Already crashed - no shutdown handshake to attempt
814
+ self.repl.worker_manager.mark_crashed()
815
+ break
816
+ else:
817
+ end_inline()
818
+ print(f"Unknown result type: {result_type}")
819
+
820
+ except SecurityError as e:
821
+ print("\n" + "=" * 80)
822
+ print(f"SECURITY ERROR: {e}")
823
+ print("=" * 80 + "\n")
824
+ except Exception as e:
825
+ print("\n" + "=" * 80)
826
+ print(f"ERROR running workflow: {str(e)}")
827
+ print("=" * 80)
828
+ print("Shutting down worker.\n")
829
+ self.repl.worker_manager.shutdown_worker()
830
+
831
+ def _workflow_restart(self, arg: str):
832
+ """Restart the worker process"""
833
+ print("Restarting worker process...")
834
+ self.repl.worker_manager.shutdown_worker()
835
+ print("Worker shutdown complete")
836
+ print("Worker will restart on next run")