feat: Added support for Anime Style [WAN 2.2 I2V] #1

This commit is contained in:
Sagnik
2026-04-18 14:19:36 +05:30
5 changed files with 332 additions and 24 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
AI Gen Code/
Payload/
.env
.env.*

View File

@@ -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()

View File

@@ -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"

View File

@@ -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 ? (
<div className="absolute bottom-[4.75rem] right-5 z-20 w-full max-w-md rounded-[26px] border border-white/10 bg-[rgba(12,16,23,0.98)] p-4 shadow-glow backdrop-blur-xl sm:right-6">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="fixed right-4 top-6 z-30 w-[min(24rem,calc(100vw-2rem))] max-h-[min(34rem,calc(100vh-3rem))] overflow-y-auto rounded-[24px] border border-white/10 bg-[rgba(12,16,23,0.98)] p-3.5 shadow-glow backdrop-blur-xl sm:right-6 sm:top-8">
<div className="mb-2.5 flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-subtext">Controls</div>
<button
className="flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-white/[0.04] text-subtext transition hover:bg-white/[0.08] hover:text-white"
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/10 bg-white/[0.04] text-subtext transition hover:bg-white/[0.08] hover:text-white"
onClick={() => setOutputMenuOpen(false)}
type="button"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid gap-3">
<div className="grid grid-cols-2 gap-2">
<div className="grid gap-2.5">
<div className="grid grid-cols-2 gap-1.5">
<button
className={composer.mode === "animate" ? "btn-primary" : "btn-secondary"}
onClick={() => setComposer((current) => ({ ...current, mode: "animate" }))}
@@ -451,7 +451,7 @@ export function DashboardClient() {
</button>
<button
className={composer.mode === "audio" ? "btn-primary" : "btn-secondary"}
onClick={() => setComposer((current) => ({ ...current, mode: "audio" }))}
onClick={() => setComposer((current) => ({ ...current, mode: "audio", modelPreset: "wan22-a14b" }))}
type="button"
>
<Mic2 className="mr-2 h-4 w-4" />
@@ -459,7 +459,7 @@ export function DashboardClient() {
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-1.5">
<button
className={composer.activeSurface === "frames" ? "btn-primary" : "btn-secondary"}
onClick={() => setComposer((current) => ({ ...current, activeSurface: "frames" }))}
@@ -476,7 +476,7 @@ export function DashboardClient() {
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-1.5">
{composer.mode === "animate" ? (
<>
<button
@@ -488,7 +488,7 @@ export function DashboardClient() {
</button>
<button
className={composer.submode === "mix" ? "btn-primary" : "btn-secondary"}
onClick={() => setComposer((current) => ({ ...current, submode: "mix" }))}
onClick={() => setComposer((current) => ({ ...current, submode: "mix", modelPreset: "wan22-a14b" }))}
type="button"
>
Mix
@@ -501,7 +501,7 @@ export function DashboardClient() {
)}
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-1.5">
{(["9:16", "16:9"] as const).map((key) => (
<button
className={composer.aspectPreset === key ? "btn-primary" : "btn-secondary"}
@@ -514,7 +514,7 @@ export function DashboardClient() {
))}
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-1.5">
{(["5s", "8s"] as const).map((key) => (
<button
className={composer.durationPreset === key ? "btn-primary" : "btn-secondary"}
@@ -527,7 +527,7 @@ export function DashboardClient() {
))}
</div>
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-1.5">
{[1, 2, 3, 4].map((count) => (
<button
className={composer.generationCount === count ? "btn-primary" : "btn-secondary"}
@@ -545,13 +545,32 @@ export function DashboardClient() {
))}
</div>
<div className="rounded-[18px] border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
Wan 2.2 A14B
<div className="mt-1 text-xs text-subtext">More Coming Soon...</div>
<div className="grid gap-1.5">
<button
className={composer.modelPreset === "wan22-a14b" ? "btn-primary" : "btn-secondary"}
onClick={() => setComposer((current) => ({ ...current, modelPreset: "wan22-a14b" }))}
type="button"
>
Wan 2.2 A14B Base
</button>
<button
className={composer.modelPreset === "wan22-a14b-anime-style" ? "btn-primary" : "btn-secondary"}
onClick={() =>
setComposer((current) => ({
...current,
mode: "animate",
submode: "move",
modelPreset: "wan22-a14b-anime-style",
}))
}
type="button"
>
Anime Style [WAN 2.2 I2V]
</button>
</div>
<div className="text-center text-xs leading-5 text-subtext">
Start frame is live. End frame is planned. Batch `x1-x4` submits real jobs against the live queue.
<div className="px-1 text-center text-[11px] leading-5 text-subtext">
Start frame is live. End frame is planned. Batch x1-x4 submits real jobs against the live queue.
</div>
</div>
</div>

View File

@@ -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"
}
}
}