Download dwct/render.py from gulle1155/studio: direct link, hf CLI and curl.
- Browser
- Download file 2.64 kB
-
https://huggingface.co/spaces/gulle1155/studio/resolve/main/dwct/render.py
- Command line
-
hf download hf://spaces/gulle1155/studio/dwct/render.py
-
curl -L -o render.py https://huggingface.co/spaces/gulle1155/studio/resolve/main/dwct/render.py
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 | |