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.
- diffusers_workflow-0.4.0a3.dist-info/METADATA +310 -0
- diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
- diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
- diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
- dw/__init__.py +353 -0
- dw/arguments.py +906 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +145 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/events.py +78 -0
- dw/hub_cache.py +289 -0
- dw/introspection.py +458 -0
- dw/log_setup.py +45 -0
- dw/pipeline_processors/chain.py +750 -0
- dw/pipeline_processors/config_objects.py +235 -0
- dw/pipeline_processors/pipeline.py +1687 -0
- dw/pipeline_processors/remote.py +18 -0
- dw/previous_results.py +259 -0
- dw/prompt_weighting.py +378 -0
- dw/repl.py +298 -0
- dw/repl_commands.py +808 -0
- dw/repl_worker.py +129 -0
- dw/result.py +850 -0
- dw/run.py +92 -0
- dw/schema.py +24 -0
- dw/security.py +379 -0
- dw/serve.py +70 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +588 -0
- dw/server/jobs.py +547 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CEh6hWi2.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CExg3_mM.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-DH6orYh2.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-CbrMVW4Q.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-YDNPZw2M.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
- dw/server/ui/assets/index-B7-VcYS-.css +1 -0
- dw/server/ui/assets/index-D_EiPU3b.js +13 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-dYuBvioq.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-CUqLM39V.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-D6vxBzMv.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-SHQb6vmD.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-CP-s5rcP.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-x0_EGHq9.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-BZC4LQDP.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-BTfA6SbD.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-CWA4MsNk.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-CWU0uvj5.js +1 -0
- dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
- dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/settings.py +77 -0
- dw/step.py +132 -0
- dw/tasks/audio_utils.py +266 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/concat_videos.py +80 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +139 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +661 -0
- dw/tasks/interpolate_frames.py +227 -0
- dw/tasks/model_cache.py +39 -0
- dw/tasks/pair_audio.py +58 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/task.py +474 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +168 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +154 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +376 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +68 -0
- dw/validate.py +43 -0
- dw/variables.py +153 -0
- dw/worker.py +517 -0
- dw/workflow.py +553 -0
- dw/workflow_schema.json +1157 -0
- dw/workflows/augment_prompt.json +65 -0
- dw/workflows/describe_image.json +58 -0
- dw/workflows/h3_context_ir.json +57 -0
- 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