studio / dwct /render.py
3v324v23's picture
Enable Gradio MCP server (mcp_server=True)
a64dea9
Raw History Blame Contribute Delete
2.64 kB
"""Stage 5 — Hunyuan image-to-video.
cuts : every shot rendered independently from its own keyframe.
chain : the previous clip's LAST frame seeds the next clip, so the whole
track becomes one continuous take."""
from __future__ import annotations
import logging
from pathlib import Path
from . import config, models
from .audio_analysis import AudioTimeline
from .director import Shot
LOG = logging.getLogger("dwct.render")
def _motion_text(shot: Shot, tl: AudioTimeline) -> str:
r = tl.reactive((shot.start + shot.end) / 2)
strength = ("barely moving" if r["motion"] < 0.6
else "steady" if r["motion"] < 1.2 else "fast and forceful")
return f"{shot.motion_prompt}, camera {strength}, motion intensity {r['motion']:.2f}"
def _i2v(image: Path, prompt: str, negative: str, seconds: float, seed: int):
import torch
from PIL import Image as PILImage
pipe = models.hunyuan()
gen = torch.Generator(device="cpu").manual_seed(seed)
frames = max(5, int(round(seconds * config.FPS)))
frames = frames - ((frames - 1) % 4) # Hunyuan wants 4n+1
common = dict(prompt=prompt, negative_prompt=negative,
num_inference_steps=config.STEPS, generator=gen,
height=config.HEIGHT, width=config.WIDTH)
img = PILImage.open(image).convert("RGB")
try:
return pipe(image=img, num_frames=frames, **common).frames[0]
except TypeError:
return pipe(image=img, **common).frames[0]
def _write(frames, out: Path) -> Path:
import imageio.v3 as iio
out.parent.mkdir(parents=True, exist_ok=True)
iio.imwrite(out, frames, fps=config.FPS, codec="libx264", quality=8)
return out
def render_shots(shots: list[Shot], tl: AudioTimeline, out_dir: Path,
mode: str = "cuts", base_seed: int = 7) -> list[Path]:
out_dir.mkdir(parents=True, exist_ok=True)
clips: list[Path] = []
carry = None
for shot in shots:
seed = base_seed + shot.index
start_img = Path(shot.keyframe) if mode == "cuts" else (carry or Path(shot.keyframe))
out = out_dir / f"shot_{shot.index:04d}.mp4"
LOG.info("render shot %d %.2fs mode=%s", shot.index, shot.duration, mode)
frames = _i2v(start_img, _motion_text(shot, tl), shot.negative,
shot.duration, seed)
_write(frames, out)
shot.clip = str(out)
clips.append(out)
if mode == "chain":
from PIL import Image as PILImage
carry = out_dir / f"_carry_{shot.index:04d}.png"
PILImage.fromarray(frames[-1]).save(carry)
return clips