| import os |
| import sys |
| import subprocess |
| import base64 |
| import tempfile |
| import traceback |
|
|
| |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", |
| "diffusers", "flask", "accelerate", "sentencepiece", "protobuf", "imageio[ffmpeg]", "transformers", "huggingface_hub"]) |
|
|
| import torch |
| from flask import Flask, request, jsonify |
|
|
| print(f"[video] torch version: {torch.__version__}", flush=True) |
| print(f"[video] CUDA available: {torch.cuda.is_available()}", flush=True) |
| if torch.cuda.is_available(): |
| print(f"[video] GPU: {torch.cuda.get_device_name(0)}", flush=True) |
| print(f"[video] VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB", flush=True) |
|
|
| app = Flask(__name__) |
| pipe = None |
|
|
| def load_model(): |
| global pipe |
| from diffusers import AutoencoderKLWan, WanPipeline |
| print("[video] Loading Wan2.2-TI2V-5B pipeline...", flush=True) |
|
|
| vae = AutoencoderKLWan.from_pretrained( |
| "Wan-AI/Wan2.2-TI2V-5B-Diffusers", |
| subfolder="vae", |
| torch_dtype=torch.bfloat16, |
| ) |
| pipe = WanPipeline.from_pretrained( |
| "Wan-AI/Wan2.2-TI2V-5B-Diffusers", |
| vae=vae, |
| torch_dtype=torch.bfloat16, |
| ) |
| pipe.enable_model_cpu_offload() |
| print("[video] Model loaded with CPU offload!", flush=True) |
|
|
| @app.route("/health", methods=["GET"]) |
| def health(): |
| if pipe is not None: |
| return jsonify({"status": "healthy"}), 200 |
| return jsonify({"status": "loading"}), 503 |
|
|
| @app.route("/", methods=["POST"]) |
| def generate(): |
| try: |
| from diffusers.utils import export_to_video |
|
|
| data = request.get_json(force=True) |
| |
| prompt = data.get("inputs", "") or data.get("prompt", "") |
| params = data.get("parameters", {}) |
|
|
| num_frames = int(params.get("num_frames", 25)) |
| height = int(params.get("height", 480)) |
| width = int(params.get("width", 832)) |
| steps = int(params.get("num_inference_steps", 15)) |
| fps = int(params.get("fps", 24)) |
| guidance = float(params.get("guidance_scale", 5.0)) |
| negative = params.get("negative_prompt", "low quality, blurry, distorted") |
|
|
| print(f"[video] Generating {num_frames} frames: {prompt[:100]}", flush=True) |
| print(f"[video] Params: {width}x{height}, steps={steps}, fps={fps}", flush=True) |
|
|
| frames = pipe( |
| prompt=prompt, |
| negative_prompt=negative, |
| num_frames=num_frames, |
| height=height, |
| width=width, |
| num_inference_steps=steps, |
| guidance_scale=guidance, |
| ).frames[0] |
|
|
| |
| with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: |
| tmp_path = tmp.name |
|
|
| export_to_video(frames, tmp_path, fps=fps) |
|
|
| with open(tmp_path, "rb") as f: |
| video_bytes = f.read() |
| os.unlink(tmp_path) |
|
|
| video_b64 = base64.b64encode(video_bytes).decode("utf-8") |
| duration = len(frames) / fps |
| print(f"[video] Done: {len(frames)} frames, {duration:.1f}s, {len(video_bytes)} bytes", flush=True) |
|
|
| return jsonify({ |
| "video": video_b64, |
| "format": "mp4", |
| "frames": len(frames), |
| "duration": duration, |
| "success": True, |
| "video_base64": video_b64, |
| "duration_seconds": duration, |
| "model": "wan2.2-ti2v-5b", |
| }) |
| except Exception as e: |
| tb = traceback.format_exc() |
| print(f"[video] ERROR: {e}\n{tb}", flush=True) |
| return jsonify({"error": str(e), "traceback": tb, "success": False}), 500 |
|
|
| |
| @app.route("/api/generate-video", methods=["POST"]) |
| def api_generate_video(): |
| """Handle the backend's /api/generate-video format by forwarding to generate()""" |
| return generate() |
|
|
| if __name__ == "__main__": |
| load_model() |
| app.run(host="0.0.0.0", port=8000) |
|
|