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