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/run.py ADDED
@@ -0,0 +1,92 @@
1
+ import argparse
2
+ import os
3
+ from . import startup
4
+ from .workflow import workflow_from_file
5
+ from .security import (
6
+ validate_workflow_path,
7
+ validate_output_path,
8
+ validate_variable_name,
9
+ validate_string_input,
10
+ SecurityError,
11
+ MAX_VARIABLE_VALUE_LENGTH,
12
+ )
13
+
14
+
15
+ def main():
16
+ parser = argparse.ArgumentParser(description="Run a workflow from a file.")
17
+ parser.add_argument(
18
+ "file_name", type=str, help="The filespec to of the workflow to run"
19
+ )
20
+ parser.add_argument(
21
+ "-o",
22
+ "--output_dir",
23
+ type=str,
24
+ default="./outputs",
25
+ help="The folder to write the outputs to",
26
+ )
27
+ parser.add_argument(
28
+ "variables",
29
+ nargs="*", # Accept 0 or more parameters
30
+ help="Optional parameters in name=value format",
31
+ )
32
+ parser.add_argument(
33
+ "-l",
34
+ "--log_level",
35
+ type=str,
36
+ default="INFO",
37
+ help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
38
+ )
39
+ args = parser.parse_args()
40
+
41
+ # Parse key-value pairs with validation
42
+ variables = {}
43
+ for variable in args.variables:
44
+ try:
45
+ name, value = variable.split("=", 1)
46
+ # Validate variable name and value
47
+ validated_name = validate_variable_name(name.strip())
48
+ validated_value = validate_string_input(
49
+ value.strip(), max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
50
+ )
51
+ variables[validated_name] = validated_value
52
+ except ValueError:
53
+ print(f"Error: Variable '{variable}' is not in name=value format")
54
+ exit(1)
55
+ except SecurityError as e:
56
+ print(f"Error: Invalid variable input: {e}")
57
+ exit(1)
58
+
59
+ # Validate and secure file paths
60
+ try:
61
+ validated_output_dir = validate_output_path(args.output_dir, None)
62
+ if not os.path.exists(validated_output_dir):
63
+ # Create output directory if it doesn't exist
64
+ os.makedirs(validated_output_dir, exist_ok=True)
65
+ print(f"Created output directory: {validated_output_dir}")
66
+
67
+ validated_file_path = validate_workflow_path(args.file_name)
68
+ if not os.path.exists(validated_file_path):
69
+ raise FileNotFoundError(f"File {validated_file_path} does not exist")
70
+
71
+ except SecurityError as e:
72
+ print(f"Error: Security validation failed: {e}")
73
+ exit(1)
74
+
75
+ startup(args.log_level)
76
+
77
+ workflow = workflow_from_file(validated_file_path, validated_output_dir)
78
+ try:
79
+ workflow.validate()
80
+ except Exception as e:
81
+ print(f"Error validating workflow '{args.file_name}': {e}")
82
+ exit(1)
83
+
84
+ try:
85
+ workflow.run(variables)
86
+ except Exception as e:
87
+ print(f"Error running workflow '{args.file_name}': {e}")
88
+ exit(1)
89
+
90
+
91
+ if __name__ == "__main__":
92
+ main()
dw/schema.py ADDED
@@ -0,0 +1,24 @@
1
+ import json
2
+ import os
3
+ from jsonschema import validate, ValidationError
4
+
5
+
6
+ def validate_data(data, schema):
7
+ try:
8
+ validate(instance=data, schema=schema)
9
+ return True, "Validation successful"
10
+
11
+ except ValidationError as ve:
12
+ return False, f"Validation error: {ve.message}"
13
+ except json.JSONDecodeError as je:
14
+ return False, f"JSON parsing error: {str(je)}"
15
+ except Exception as e:
16
+ return False, f"Unexpected error: {str(e)}"
17
+
18
+
19
+ def load_schema(schema_name):
20
+ file_spec = os.path.join(
21
+ os.path.dirname(os.path.abspath(__file__)), f"{schema_name}_schema.json"
22
+ )
23
+ with open(file_spec, "r") as file:
24
+ return json.load(file)
dw/security.py ADDED
@@ -0,0 +1,379 @@
1
+ """
2
+ Security utilities for input validation and safe file operations.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import logging
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+ from typing import Union, List, Optional
11
+
12
+ logger = logging.getLogger("dw")
13
+
14
+ # Security constants
15
+ MAX_PATH_LENGTH = 4096
16
+ MAX_FILENAME_LENGTH = 255
17
+ MAX_JSON_SIZE = 50 * 1024 * 1024 # 50MB
18
+ MAX_VARIABLE_NAME_LENGTH = 100
19
+ MAX_VARIABLE_VALUE_LENGTH = 10000
20
+ MAX_CONSTANT_NAME_LENGTH = 200
21
+ DEFAULT_MAX_STRING_LENGTH = 1000
22
+ MAX_FILE_PATH_LENGTH = 1000
23
+ ALLOWED_JSON_EXTENSIONS = {".json"}
24
+ ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
25
+ ALLOWED_VIDEO_EXTENSIONS = {".mp4", ".avi", ".mkv", ".mov", ".webm"}
26
+ ALLOWED_AUDIO_EXTENSIONS = {".wav", ".mp3", ".flac", ".ogg"}
27
+
28
+ # Dangerous path patterns (handle both Unix and Windows paths)
29
+ DANGEROUS_PATTERNS = [
30
+ r"\.\.", # Parent directory traversal (.. anywhere)
31
+ r"~[\\/]", # Home directory expansion
32
+ r"^/dev/", # Device files (Unix)
33
+ r"^/proc/", # Process files (Unix)
34
+ r"^/sys/", # System files (Unix)
35
+ ]
36
+
37
+
38
+ class SecurityError(Exception):
39
+ """Base exception for security-related errors."""
40
+
41
+ pass
42
+
43
+
44
+ class PathTraversalError(SecurityError):
45
+ """Raised when path traversal attempt is detected."""
46
+
47
+ pass
48
+
49
+
50
+ class InvalidInputError(SecurityError):
51
+ """Raised when input validation fails."""
52
+
53
+ pass
54
+
55
+
56
+ def validate_path(
57
+ path: Union[str, Path], base_dir: Optional[str] = None, allow_create: bool = True
58
+ ) -> str:
59
+ """
60
+ Validate and sanitize file paths to prevent path traversal attacks.
61
+
62
+ Args:
63
+ path: The path to validate
64
+ base_dir: Optional base directory to restrict access to
65
+ allow_create: Whether to allow creation of non-existent paths
66
+
67
+ Returns:
68
+ Absolute, sanitized path
69
+
70
+ Raises:
71
+ PathTraversalError: If path contains dangerous patterns
72
+ InvalidInputError: If path is invalid or too long
73
+ """
74
+ if not path:
75
+ raise InvalidInputError("Path cannot be empty")
76
+
77
+ path_str = str(path)
78
+
79
+ # Check path length
80
+ if len(path_str) > MAX_PATH_LENGTH:
81
+ raise InvalidInputError(f"Path too long: {len(path_str)} > {MAX_PATH_LENGTH}")
82
+
83
+ # Check for null bytes
84
+ if "\x00" in path_str:
85
+ raise InvalidInputError("Path contains null bytes")
86
+
87
+ # Normalize path separators for consistent checking across platforms
88
+ normalized_path = path_str.replace("\\", "/")
89
+
90
+ # Check for dangerous patterns
91
+ for pattern in DANGEROUS_PATTERNS:
92
+ if re.search(pattern, normalized_path, re.IGNORECASE):
93
+ raise PathTraversalError(
94
+ f"Path contains dangerous pattern matching {pattern}"
95
+ )
96
+
97
+ # Convert to absolute path and resolve
98
+ try:
99
+ abs_path = os.path.abspath(os.path.expanduser(path_str))
100
+ resolved_path = os.path.realpath(abs_path)
101
+ except (OSError, ValueError) as e:
102
+ raise InvalidInputError(f"Invalid path: {e}")
103
+
104
+ # Check if path is within base directory if specified
105
+ if base_dir:
106
+ try:
107
+ base_abs = os.path.abspath(os.path.expanduser(base_dir))
108
+ base_real = os.path.realpath(base_abs)
109
+
110
+ # Use os.path.commonpath to properly check if path is within base_dir
111
+ # This handles edge cases like different drives on Windows
112
+ try:
113
+ common = os.path.commonpath([base_real, resolved_path])
114
+ # The common path should be the base directory (or parent of it)
115
+ # Fixed: Check if resolved_path starts with base_real for proper containment
116
+ if not (
117
+ common == base_real
118
+ or resolved_path.startswith(base_real + os.sep)
119
+ or resolved_path == base_real
120
+ ):
121
+ raise PathTraversalError(
122
+ f"Path outside allowed directory: {resolved_path}"
123
+ )
124
+ except ValueError:
125
+ # Raised when paths are on different drives (Windows)
126
+ raise PathTraversalError(
127
+ f"Path on different drive than allowed directory: {resolved_path}"
128
+ )
129
+ except (OSError, ValueError) as e:
130
+ raise InvalidInputError(f"Invalid base directory: {e}")
131
+
132
+ # Check filename length
133
+ filename = os.path.basename(resolved_path)
134
+ if len(filename) > MAX_FILENAME_LENGTH:
135
+ raise InvalidInputError(
136
+ f"Filename too long: {len(filename)} > {MAX_FILENAME_LENGTH}"
137
+ )
138
+
139
+ # Check if path exists or creation is allowed
140
+ if not os.path.exists(resolved_path) and not allow_create:
141
+ raise InvalidInputError(f"Path does not exist: {resolved_path}")
142
+
143
+ logger.debug(f"Validated path: {path_str} -> {resolved_path}")
144
+ return resolved_path
145
+
146
+
147
+ def validate_file_extension(path: str, allowed_extensions: set) -> str:
148
+ """
149
+ Validate file extension against allowed list.
150
+
151
+ Args:
152
+ path: File path to validate
153
+ allowed_extensions: Set of allowed extensions (with dots)
154
+
155
+ Returns:
156
+ The validated path
157
+
158
+ Raises:
159
+ InvalidInputError: If extension is not allowed
160
+ """
161
+ ext = os.path.splitext(path)[1].lower()
162
+ if ext not in allowed_extensions:
163
+ raise InvalidInputError(f"File extension not allowed: {ext}")
164
+ return path
165
+
166
+
167
+ def validate_workflow_path(path: str, workflow_dir: str = None) -> str:
168
+ """Validate workflow file paths."""
169
+ validated = validate_path(path, workflow_dir, allow_create=False)
170
+ return validate_file_extension(validated, ALLOWED_JSON_EXTENSIONS)
171
+
172
+
173
+ def validate_output_path(path: str, output_dir: str) -> str:
174
+ """Validate output file paths."""
175
+ return validate_path(path, output_dir, allow_create=True)
176
+
177
+
178
+ def validate_url(url: str) -> str:
179
+ """
180
+ Validate URL format and scheme.
181
+
182
+ Args:
183
+ url: URL to validate
184
+
185
+ Returns:
186
+ The validated URL
187
+
188
+ Raises:
189
+ InvalidInputError: If URL is invalid or uses dangerous scheme
190
+ """
191
+ if not url:
192
+ raise InvalidInputError("URL cannot be empty")
193
+
194
+ try:
195
+ parsed = urlparse(url)
196
+ if parsed.scheme not in {"http", "https"}:
197
+ raise InvalidInputError(f"URL scheme not allowed: {parsed.scheme}")
198
+ if not parsed.netloc:
199
+ raise InvalidInputError("URL must have a valid domain")
200
+ return url
201
+ except Exception as e:
202
+ raise InvalidInputError(f"Invalid URL: {e}")
203
+
204
+
205
+ def sanitize_command_args(args: List[str]) -> List[str]:
206
+ """
207
+ Sanitize command arguments for subprocess execution with shell=False.
208
+
209
+ When using subprocess with a list of arguments and shell=False, Python
210
+ handles argument separation safely without shell interpretation. This
211
+ function validates that arguments don't contain shell metacharacters
212
+ that could be dangerous if shell=True were accidentally used.
213
+
214
+ Args:
215
+ args: List of command arguments
216
+
217
+ Returns:
218
+ List of validated arguments (no modification needed for shell=False)
219
+
220
+ Raises:
221
+ InvalidInputError: If arguments contain dangerous content
222
+ """
223
+ sanitized = []
224
+
225
+ for arg in args:
226
+ if not isinstance(arg, str):
227
+ arg = str(arg)
228
+
229
+ # Check for dangerous characters that would be problematic with shell=True
230
+ # Even though we use shell=False, this prevents accidental security issues
231
+ if any(char in arg for char in ["`", "$", "|", "&", ";", ">", "<", "\n", "\r"]):
232
+ raise InvalidInputError(f"Argument contains dangerous characters: {arg}")
233
+
234
+ # With shell=False, we don't need shlex.quote() - Python handles it safely
235
+ # Just validate and pass through
236
+ sanitized.append(arg)
237
+
238
+ return sanitized
239
+
240
+
241
+ def validate_variable_name(name: str) -> str:
242
+ """
243
+ Validate variable names to prevent injection attacks.
244
+
245
+ Args:
246
+ name: Variable name to validate
247
+
248
+ Returns:
249
+ The validated variable name
250
+
251
+ Raises:
252
+ InvalidInputError: If name is invalid
253
+ """
254
+ if not name:
255
+ raise InvalidInputError("Variable name cannot be empty")
256
+
257
+ # Allow only alphanumeric characters, underscores, and hyphens
258
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_-]*$", name):
259
+ raise InvalidInputError(f"Invalid variable name: {name}")
260
+
261
+ if len(name) > MAX_VARIABLE_NAME_LENGTH:
262
+ raise InvalidInputError(
263
+ f"Variable name too long: {len(name)} > {MAX_VARIABLE_NAME_LENGTH}"
264
+ )
265
+
266
+ return name
267
+
268
+
269
+ # A dotted python name: identifiers separated by dots, and nothing else
270
+ CONSTANT_NAME_PATTERN = r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$"
271
+
272
+
273
+ def validate_constant_name(name: str) -> str:
274
+ """
275
+ Validate the dotted name of a python constant a workflow references.
276
+
277
+ Resolving a name imports the module it lives in, which runs that module's
278
+ code, so the name is checked before anything is imported - identifiers and
279
+ dots only, no relative paths and nothing to evaluate.
280
+
281
+ Args:
282
+ name: Dotted name to validate
283
+
284
+ Returns:
285
+ The validated name
286
+
287
+ Raises:
288
+ InvalidInputError: If name is invalid
289
+ """
290
+ if not name:
291
+ raise InvalidInputError("Constant name cannot be empty")
292
+
293
+ if not re.match(CONSTANT_NAME_PATTERN, name):
294
+ raise InvalidInputError(
295
+ f"Invalid constant name: {name} - a constant is named by its module "
296
+ f"and the attribute to read from it, like "
297
+ f"'diffusers.pipelines.ltx2.utils.DISTILLED_SIGMA_VALUES'"
298
+ )
299
+
300
+ if len(name) > MAX_CONSTANT_NAME_LENGTH:
301
+ raise InvalidInputError(
302
+ f"Constant name too long: {len(name)} > {MAX_CONSTANT_NAME_LENGTH}"
303
+ )
304
+
305
+ return name
306
+
307
+
308
+ def validate_json_size(file_path: str) -> None:
309
+ """
310
+ Validate JSON file size before loading.
311
+
312
+ Args:
313
+ file_path: Path to JSON file
314
+
315
+ Raises:
316
+ InvalidInputError: If file is too large
317
+ """
318
+ try:
319
+ size = os.path.getsize(file_path)
320
+ if size > MAX_JSON_SIZE:
321
+ raise InvalidInputError(f"JSON file too large: {size} > {MAX_JSON_SIZE}")
322
+ except OSError as e:
323
+ raise InvalidInputError(f"Cannot check file size: {e}")
324
+
325
+
326
+ def validate_string_input(
327
+ value: str, max_length: int = DEFAULT_MAX_STRING_LENGTH, allow_empty: bool = False
328
+ ) -> str:
329
+ """
330
+ Validate string input for basic safety.
331
+
332
+ Args:
333
+ value: String to validate
334
+ max_length: Maximum allowed length
335
+ allow_empty: Whether empty strings are allowed
336
+
337
+ Returns:
338
+ The validated string
339
+
340
+ Raises:
341
+ InvalidInputError: If string is invalid
342
+ """
343
+ if not allow_empty and not value:
344
+ raise InvalidInputError("String cannot be empty")
345
+
346
+ if len(value) > max_length:
347
+ raise InvalidInputError(f"String too long: {len(value)} > {max_length}")
348
+
349
+ # Check for null bytes and control characters
350
+ if "\x00" in value or any(ord(c) < 32 for c in value if c not in "\t\n\r"):
351
+ raise InvalidInputError("String contains invalid characters")
352
+
353
+ return value
354
+
355
+
356
+ def safe_join_path(*parts: str) -> str:
357
+ """
358
+ Safely join path components with validation.
359
+
360
+ Args:
361
+ *parts: Path components to join
362
+
363
+ Returns:
364
+ Safely joined path
365
+
366
+ Raises:
367
+ InvalidInputError: If any component is invalid
368
+ """
369
+ # Validate each component
370
+ for part in parts:
371
+ if not part:
372
+ continue
373
+ validate_string_input(part, MAX_FILENAME_LENGTH)
374
+ if ".." in part or "/" in part or "\\" in part:
375
+ raise InvalidInputError(
376
+ f"Path component contains invalid characters: {part}"
377
+ )
378
+
379
+ return os.path.join(*parts)
dw/serve.py ADDED
@@ -0,0 +1,70 @@
1
+ """Run the workflow engine as a local HTTP server.
2
+
3
+ python -m dw.serve
4
+ python -m dw.serve --port 8765 --workflow-dir ./examples
5
+
6
+ Binds to localhost by default - this serves your GPU to your own tools,
7
+ not to the network. Interactive API docs at http://127.0.0.1:8765/docs
8
+ """
9
+
10
+ import argparse
11
+ import multiprocessing
12
+
13
+ # Spawn start method before anything touches multiprocessing (CUDA/MPS)
14
+ if multiprocessing.get_start_method(allow_none=True) != "spawn":
15
+ try:
16
+ multiprocessing.set_start_method("spawn", force=True)
17
+ except RuntimeError:
18
+ pass
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description="Serve diffusers workflows over HTTP.")
23
+ parser.add_argument(
24
+ "--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)"
25
+ )
26
+ parser.add_argument("--port", type=int, default=8765, help="Port (default: 8765)")
27
+ parser.add_argument(
28
+ "--workflow-dir", default="./examples", help="Directory of workflow JSON files"
29
+ )
30
+ parser.add_argument(
31
+ "--output-dir", default="./outputs", help="Directory results are written to"
32
+ )
33
+ parser.add_argument(
34
+ "-l",
35
+ "--log_level",
36
+ default="INFO",
37
+ help="DEBUG, INFO, WARNING, ERROR, CRITICAL",
38
+ )
39
+ args = parser.parse_args()
40
+
41
+ try:
42
+ import uvicorn
43
+ except ImportError:
44
+ print(
45
+ "The server needs fastapi and uvicorn: pip install fastapi 'uvicorn[standard]'"
46
+ )
47
+ raise SystemExit(1)
48
+
49
+ from . import startup
50
+
51
+ startup(args.log_level)
52
+
53
+ from .server.app import create_app
54
+
55
+ from .server.app import default_ui_dir
56
+
57
+ app = create_app(
58
+ workflow_dir=args.workflow_dir,
59
+ output_dir=args.output_dir,
60
+ log_level=args.log_level,
61
+ )
62
+ ui = " - UI at /" if default_ui_dir() else ""
63
+ print(
64
+ f"diffusers-workflow server on http://{args.host}:{args.port} (docs at /docs{ui})"
65
+ )
66
+ uvicorn.run(app, host=args.host, port=args.port, log_level=args.log_level.lower())
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
dw/server/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """HTTP server exposing the workflow engine: submit jobs, stream progress,
2
+ cancel, browse workflows and outputs. See dw/serve.py for the entry point."""