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) => (
-
+
{(["5s", "8s"] as const).map((key) => (
-
+
{[1, 2, 3, 4].map((count) => (
-
- Wan 2.2 A14B
-
More Coming Soon...
+
+ setComposer((current) => ({ ...current, modelPreset: "wan22-a14b" }))}
+ type="button"
+ >
+ Wan 2.2 A14B Base
+
+
+ setComposer((current) => ({
+ ...current,
+ mode: "animate",
+ submode: "move",
+ modelPreset: "wan22-a14b-anime-style",
+ }))
+ }
+ type="button"
+ >
+ Anime Style [WAN 2.2 I2V]
+
-
- 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"
+ }
+ }
+}