From 732dcaa39d23ce416681b3cf8fbaf07d75f1f49a Mon Sep 17 00:00:00 2001 From: Sagnik Date: Sat, 18 Apr 2026 14:18:46 +0530 Subject: [PATCH] feat: Added support for Anime Style [WAN 2.2 I2V] #1 --- .gitignore | 1 + backend/app/services/orchestrator.py | 38 ++- backend/app/services/workflow_binder.py | 6 +- frontend/src/components/dashboard-client.tsx | 55 ++-- .../wan22_animate_move_anime_style_v1.json | 256 ++++++++++++++++++ 5 files changed, 332 insertions(+), 24 deletions(-) create mode 100644 workflows/animate/wan22_animate_move_anime_style_v1.json diff --git a/.gitignore b/.gitignore index e189a23..b5fa418 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ AI Gen Code/ +Payload/ .env .env.* diff --git a/backend/app/services/orchestrator.py b/backend/app/services/orchestrator.py index cd49911..ab1011f 100644 --- a/backend/app/services/orchestrator.py +++ b/backend/app/services/orchestrator.py @@ -222,6 +222,27 @@ async def _upload_asset_to_comfy(db: Session, asset_id: Optional[str]) -> Option ) +def _apply_model_preset(prompt: str, negative_prompt: str, model_preset: Optional[str]) -> tuple[str, str]: + if model_preset != "wan22-a14b-anime-style": + return prompt, negative_prompt + + positive = prompt + negative = negative_prompt + + if "An1meStyl3" not in positive: + positive = f"An1meStyl3, AnimeStyle, {positive}".strip(", ") + elif "AnimeStyle" not in positive: + positive = f"AnimeStyle, {positive}".strip(", ") + + anime_negative = "(((realistic))), ((photograph))" + if "realistic" not in negative.lower(): + negative = f"{negative}, {anime_negative}".strip(", ") + elif "photograph" not in negative.lower(): + negative = f"{negative}, ((photograph))".strip(", ") + + return positive, negative + + def _validate_job(job: Job) -> list[str]: errors = [] if not job.prompt or not job.prompt.strip(): @@ -265,18 +286,25 @@ async def run_job(job_id: str) -> None: ref_names.append(uploaded) settings_dict = json.loads(job.settings_json) if job.settings_json else {} - binder = WorkflowBinder(select_template_name(job.mode, job.submode)) + model_preset = settings_dict.get("model_preset") + template_name = select_template_name(job.mode, job.submode, model_preset) + binder = WorkflowBinder(template_name) if "PLACEHOLDER" in binder.status.upper(): raise RuntimeError( - f"Workflow template '{select_template_name(job.mode, job.submode)}' is still a placeholder. " + f"Workflow template '{template_name}' is still a placeholder. " "Replace it with the production ComfyUI export before running real generations." ) raw_seed = settings_dict.get("seed", 0) seed = raw_seed if isinstance(raw_seed, int) and raw_seed >= 0 else 0 + positive_prompt, negative_prompt = _apply_model_preset( + job.prompt, + job.negative_prompt or "", + model_preset, + ) params = { - "positive_prompt": job.prompt, - "negative_prompt": job.negative_prompt or "", + "positive_prompt": positive_prompt, + "negative_prompt": negative_prompt, "ground_truth": gt_name, "motion_video": motion_name, "audio": audio_name, @@ -288,7 +316,7 @@ async def run_job(job_id: str) -> None: } workflow = binder.bind(params) await _validate_runtime_models(workflow) - job.workflow_template_name = select_template_name(job.mode, job.submode) + job.workflow_template_name = template_name job.workflow_template_version = binder.version db.commit() diff --git a/backend/app/services/workflow_binder.py b/backend/app/services/workflow_binder.py index deed556..41a9216 100644 --- a/backend/app/services/workflow_binder.py +++ b/backend/app/services/workflow_binder.py @@ -25,8 +25,12 @@ def _discover() -> None: _discover() -def select_template_name(mode: str, submode: Optional[str]) -> str: +def select_template_name(mode: str, submode: Optional[str], model_preset: Optional[str] = None) -> str: if mode == "animate": + if model_preset == "wan22-a14b-anime-style": + if (submode or "move") != "move": + raise ValueError("Anime Style preset is currently supported only for Animate / Move.") + return "wan22_animate_move_anime_style" return f"wan22_animate_{submode or 'move'}" if mode == "audio": return "wan22_s2v" diff --git a/frontend/src/components/dashboard-client.tsx b/frontend/src/components/dashboard-client.tsx index 1ada4ce..6a69620 100644 --- a/frontend/src/components/dashboard-client.tsx +++ b/frontend/src/components/dashboard-client.tsx @@ -41,7 +41,7 @@ type ComposerState = { aspectPreset: "16:9" | "1:1" | "9:16"; durationPreset: "5s" | "8s"; generationCount: 1 | 2 | 3 | 4; - modelPreset: "wan22-a14b"; + modelPreset: "wan22-a14b" | "wan22-a14b-anime-style"; }; type AssetKind = "image" | "video" | "audio" | "pose_sheet"; @@ -428,19 +428,19 @@ export function DashboardClient() { /> {outputMenuOpen ? ( -
-
+
+
Controls
-
-
+
+
-
+
-
+
{composer.mode === "animate" ? ( <>
-
+
{(["9:16", "16:9"] as const).map((key) => ( +
-
- Start frame is live. End frame is planned. Batch `x1-x4` submits real jobs against the live queue. +
+ Start frame is live. End frame is planned. Batch x1-x4 submits real jobs against the live queue.
diff --git a/workflows/animate/wan22_animate_move_anime_style_v1.json b/workflows/animate/wan22_animate_move_anime_style_v1.json new file mode 100644 index 0000000..d267395 --- /dev/null +++ b/workflows/animate/wan22_animate_move_anime_style_v1.json @@ -0,0 +1,256 @@ +{ + "__animatrix_meta__": { + "name": "wan22_animate_move_anime_style", + "version": "1", + "model": "Wan2.2 I2V A14B Local Native + Anime Style v2 LoRAs", + "description": "Official local Comfy-native Wan 2.2 image-to-video runtime with Anime Style v2 LoRAs applied on both high-noise and low-noise stages.", + "param_nodes": { + "positive_prompt": { "node_id": "2", "input": "text" }, + "negative_prompt": { "node_id": "3", "input": "text" }, + "ground_truth": { "node_id": "1", "input": "image" }, + "seed": { "node_id": "13", "input": "noise_seed" }, + "width": { "node_id": "12", "input": "width" }, + "height": { "node_id": "12", "input": "height" }, + "length": { "node_id": "12", "input": "length" } + }, + "status": "production_local_native_graph" + }, + "1": { + "class_type": "LoadImage", + "inputs": { + "image": "ground_truth.png" + } + }, + "2": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "An1meStyl3, AnimeStyle, cinematic character animation from a grounded first frame", + "clip": [ + "4", + 0 + ] + } + }, + "3": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "oversaturated, overexposed, static frame, blurry details, unclear details, watermark, messy background, low quality, jpeg artifacts, deformed limbs, extra fingers, ugly, distorted face, character drift, (((realistic))), ((photograph))", + "clip": [ + "4", + 0 + ] + } + }, + "4": { + "class_type": "CLIPLoader", + "inputs": { + "clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "type": "wan", + "device": "default" + } + }, + "5": { + "class_type": "VAELoader", + "inputs": { + "vae_name": "wan_2.1_vae.safetensors" + } + }, + "6": { + "class_type": "UNETLoader", + "inputs": { + "unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + } + }, + "7": { + "class_type": "UNETLoader", + "inputs": { + "unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + } + }, + "8": { + "class_type": "LoraLoaderModelOnly", + "inputs": { + "model": [ + "6", + 0 + ], + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", + "strength_model": 1.0 + } + }, + "9": { + "class_type": "LoraLoaderModelOnly", + "inputs": { + "model": [ + "7", + 0 + ], + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", + "strength_model": 1.0 + } + }, + "10": { + "class_type": "LoraLoaderModelOnly", + "inputs": { + "model": [ + "8", + 0 + ], + "lora_name": "wan2.2_i2v_animestyle_v2_high.safetensors", + "strength_model": 1.0 + } + }, + "11": { + "class_type": "LoraLoaderModelOnly", + "inputs": { + "model": [ + "9", + 0 + ], + "lora_name": "wan2.2_i2v_animestyle_v2_low.safetensors", + "strength_model": 1.0 + } + }, + "12": { + "class_type": "ModelSamplingSD3", + "inputs": { + "model": [ + "10", + 0 + ], + "shift": 5.0 + } + }, + "13": { + "class_type": "WanImageToVideo", + "inputs": { + "positive": [ + "2", + 0 + ], + "negative": [ + "3", + 0 + ], + "vae": [ + "5", + 0 + ], + "width": 832, + "height": 468, + "length": 81, + "batch_size": 1, + "start_image": [ + "1", + 0 + ] + } + }, + "14": { + "class_type": "KSamplerAdvanced", + "inputs": { + "model": [ + "12", + 0 + ], + "add_noise": "enable", + "noise_seed": 42, + "steps": 4, + "cfg": 1.0, + "sampler_name": "euler", + "scheduler": "simple", + "positive": [ + "13", + 0 + ], + "negative": [ + "13", + 1 + ], + "latent_image": [ + "13", + 2 + ], + "start_at_step": 0, + "end_at_step": 2, + "return_with_leftover_noise": "enable" + } + }, + "15": { + "class_type": "ModelSamplingSD3", + "inputs": { + "model": [ + "11", + 0 + ], + "shift": 5.0 + } + }, + "16": { + "class_type": "KSamplerAdvanced", + "inputs": { + "model": [ + "15", + 0 + ], + "add_noise": "disable", + "noise_seed": 42, + "steps": 4, + "cfg": 1.0, + "sampler_name": "euler", + "scheduler": "simple", + "positive": [ + "13", + 0 + ], + "negative": [ + "13", + 1 + ], + "latent_image": [ + "14", + 0 + ], + "start_at_step": 2, + "end_at_step": 4, + "return_with_leftover_noise": "disable" + } + }, + "17": { + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "16", + 0 + ], + "vae": [ + "5", + 0 + ] + } + }, + "18": { + "class_type": "CreateVideo", + "inputs": { + "images": [ + "17", + 0 + ], + "fps": 16 + } + }, + "19": { + "class_type": "SaveVideo", + "inputs": { + "video": [ + "18", + 0 + ], + "filename_prefix": "AnimatrixAnimeStyle", + "format": "mp4", + "codec": "h264" + } + } +}