From bb3367c9ac0210000635776d98037127dea602b8 Mon Sep 17 00:00:00 2001 From: SolitaryThinker Date: Sun, 27 Jul 2025 01:16:59 +0000 Subject: [PATCH] add demo --- examples/inference/basic/basic_dmd.py | 3 +- examples/inference/gradio/README_ray_serve.md | 222 ++++++++++++++ examples/inference/gradio/gradio_frontend.py | 290 ++++++++++++++++++ .../inference/gradio/ray_serve_backend.py | 209 +++++++++++++ .../inference/gradio/start_ray_serve_app.py | 224 ++++++++++++++ 5 files changed, 947 insertions(+), 1 deletion(-) create mode 100644 examples/inference/gradio/README_ray_serve.md create mode 100644 examples/inference/gradio/gradio_frontend.py create mode 100644 examples/inference/gradio/ray_serve_backend.py create mode 100644 examples/inference/gradio/start_ray_serve_app.py diff --git a/examples/inference/basic/basic_dmd.py b/examples/inference/basic/basic_dmd.py index ed30e6e0a..d65ce4400 100644 --- a/examples/inference/basic/basic_dmd.py +++ b/examples/inference/basic/basic_dmd.py @@ -31,6 +31,7 @@ def main(): prompt = ( "A neon-lit alley in futuristic Tokyo during a heavy rainstorm at night. The puddles reflect glowing signs in kanji, advertising ramen, karaoke, and VR arcades. A woman in a translucent raincoat walks briskly with an LED umbrella. Steam rises from a street food cart, and a cat darts across the screen. Raindrops are visible on the camera lens, creating a cinematic bokeh effect." ) + prompt = "A vintage train snakes through the mountains, its plume of white steam rising dramatically against the jagged peaks. The cars glint in the late afternoon sun, their deep crimson and gold accents lending a touch of elegance. The tracks carve a precarious path along the cliffside, revealing glimpses of a roaring river far below. Inside, passengers peer out the large windows, their faces lit with awe as the landscape unfolds." start_time = time.perf_counter() video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param) end_time = time.perf_counter() @@ -45,7 +46,7 @@ def main(): "embodying the raw energy of the wild. Low angle, steady tracking shot, " "cinematic.") start_time = time.perf_counter() - video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True) + video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=False) end_time = time.perf_counter() gen_time2 = end_time - start_time diff --git a/examples/inference/gradio/README_ray_serve.md b/examples/inference/gradio/README_ray_serve.md new file mode 100644 index 000000000..60f8420af --- /dev/null +++ b/examples/inference/gradio/README_ray_serve.md @@ -0,0 +1,222 @@ +# FastVideo with Ray Serve Backend and Gradio Frontend + +This setup provides a scalable web application for FastVideo inference using Ray Serve as the backend and Gradio as the frontend. + +## Architecture + +- **Backend**: Ray Serve handles video generation requests with GPU acceleration +- **Frontend**: Gradio provides a user-friendly web interface +- **Communication**: HTTP REST API between frontend and backend + +## Features + +- ✅ Scalable backend with Ray Serve +- ✅ GPU-accelerated video generation +- ✅ User-friendly Gradio interface +- ✅ Health monitoring and error handling +- ✅ All original functionality preserved +- ✅ Easy deployment and management + +## Installation + +1. Install the additional dependencies: + +```bash +pip install -r requirements_ray_serve.txt +``` + +2. Ensure you have the FastVideo model available (the default is `FastVideo/FastHunyuan-diffusers`) + +## Usage + +### Option 1: Start Both Services Together (Recommended) + +Use the startup script to launch both backend and frontend: + +```bash +python start_ray_serve_app.py +``` + +This will: +- Start the Ray Serve backend on port 8000 +- Start the Gradio frontend on port 7860 +- Monitor both services and provide unified logging +- Handle graceful shutdown with Ctrl+C + +### Option 2: Start Services Separately + +#### Start Backend Only + +```bash +python ray_serve_backend.py --model_path FastVideo/FastHunyuan-diffusers --output_path outputs +``` + +#### Start Frontend Only + +```bash +python gradio_frontend.py --backend_url http://localhost:8000 --model_path FastVideo/FastHunyuan-diffusers +``` + +## Configuration + +### Command Line Arguments + +#### Startup Script (`start_ray_serve_app.py`) + +- `--model_path`: Path to the FastVideo model (default: `FastVideo/FastHunyuan-diffusers`) +- `--output_path`: Directory to save generated videos (default: `outputs`) +- `--backend_host`: Backend host to bind to (default: `0.0.0.0`) +- `--backend_port`: Backend port (default: `8000`) +- `--frontend_host`: Frontend host to bind to (default: `0.0.0.0`) +- `--frontend_port`: Frontend port (default: `7860`) +- `--skip_backend_check`: Skip backend health check + +#### Backend (`ray_serve_backend.py`) + +- `--model_path`: Path to the FastVideo model +- `--output_path`: Directory to save generated videos +- `--host`: Host to bind to +- `--port`: Port to bind to + +#### Frontend (`gradio_frontend.py`) + +- `--backend_url`: URL of the Ray Serve backend +- `--model_path`: Path to the model (for default parameters) +- `--host`: Host to bind to +- `--port`: Port to bind to + +### Environment Variables + +You can also set these environment variables: + +- `FASTVIDEO_MODEL_PATH`: Path to the FastVideo model +- `FASTVIDEO_OUTPUT_PATH`: Directory to save generated videos +- `RAY_SERVE_HOST`: Backend host +- `RAY_SERVE_PORT`: Backend port +- `GRADIO_HOST`: Frontend host +- `GRADIO_PORT`: Frontend port + +## API Endpoints + +### Backend API (Ray Serve) + +- `GET /health`: Health check endpoint +- `POST /generate_video`: Video generation endpoint + +#### Video Generation Request + +```json +{ + "prompt": "A beautiful sunset over the ocean", + "negative_prompt": "blurry, low quality", + "use_negative_prompt": true, + "seed": 42, + "guidance_scale": 7.5, + "num_frames": 21, + "height": 512, + "width": 512, + "num_inference_steps": 20, + "randomize_seed": false +} +``` + +#### Video Generation Response + +```json +{ + "output_path": "/path/to/generated/video.mp4", + "seed": 42, + "success": true, + "error_message": null +} +``` + +## Deployment + +### Local Development + +1. Start the application: + ```bash + python start_ray_serve_app.py + ``` + +2. Access the frontend at: `http://localhost:7860` +3. Access the backend API at: `http://localhost:8000` + +### Production Deployment + +For production deployment, consider: + +1. **Load Balancing**: Use a reverse proxy (nginx, traefik) in front of the services +2. **Monitoring**: Add monitoring and logging (Prometheus, Grafana) +3. **Scaling**: Configure Ray Serve for horizontal scaling +4. **Security**: Add authentication and rate limiting +5. **Storage**: Use shared storage for video outputs + +### Docker Deployment + +Create a Dockerfile for containerized deployment: + +```dockerfile +FROM python:3.9-slim + +WORKDIR /app + +# Install dependencies +COPY requirements_ray_serve.txt . +RUN pip install -r requirements_ray_serve.txt + +# Copy application files +COPY . . + +# Expose ports +EXPOSE 8000 7860 + +# Start the application +CMD ["python", "start_ray_serve_app.py"] +``` + +## Troubleshooting + +### Common Issues + +1. **Backend not starting**: Check GPU availability and Ray installation +2. **Frontend can't connect**: Verify backend URL and network connectivity +3. **Video generation fails**: Check model path and GPU memory +4. **Port conflicts**: Change ports using command line arguments + +### Logs + +- Backend logs are prefixed with `[BACKEND]` +- Frontend logs are prefixed with `[FRONTEND]` +- Use `--skip_backend_check` if you need to debug startup issues + +### Performance Tuning + +- Adjust `num_replicas` in the Ray Serve deployment for scaling +- Configure `max_concurrent_queries` based on GPU memory +- Use multiple GPUs by modifying `ray_actor_options` + +## Migration from Original Gradio Demo + +The new setup maintains full compatibility with the original functionality: + +1. All parameters and options are preserved +2. The same example prompts are included +3. The UI layout and behavior are identical +4. Video generation quality is the same + +The main differences are: +- Backend processing is now handled by Ray Serve +- Better error handling and status monitoring +- Scalable architecture for production use +- Separation of concerns between frontend and backend + +## Contributing + +To extend this setup: + +1. Add new endpoints to `ray_serve_backend.py` +2. Update the frontend in `gradio_frontend.py` +3. Modify the startup script if needed +4. Update this README with new features \ No newline at end of file diff --git a/examples/inference/gradio/gradio_frontend.py b/examples/inference/gradio/gradio_frontend.py new file mode 100644 index 000000000..8a28820d8 --- /dev/null +++ b/examples/inference/gradio/gradio_frontend.py @@ -0,0 +1,290 @@ +import argparse +import os +import requests +import json +from typing import Optional + +import gradio as gr +import torch + +from fastvideo.configs.sample.base import SamplingParam + + +class RayServeClient: + def __init__(self, backend_url: str): + self.backend_url = backend_url.rstrip('/') + self.generate_endpoint = f"{self.backend_url}/generate_video" + self.health_endpoint = f"{self.backend_url}/health" + # Default request timeout in seconds. Increase if generation may run longer. + self.request_timeout_s = int(os.getenv("FASTVIDEO_GENERATION_TIMEOUT", "900")) # 15 minutes default + + def check_health(self) -> bool: + """Check if the backend is healthy""" + try: + response = requests.get(self.health_endpoint, timeout=5) + return response.status_code == 200 + except Exception: + return False + + def generate_video(self, request_data: dict) -> dict: + """Send video generation request to the backend""" + try: + response = requests.post( + self.generate_endpoint, + json=request_data, + timeout=self.request_timeout_s, + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return { + "success": False, + "error_message": f"Backend request failed: {str(e)}", + "output_path": "", + "seed": request_data.get("seed", 42) + } + + +def create_gradio_interface(backend_url: str, default_params: SamplingParam): + """Create the Gradio interface""" + + # Initialize the Ray Serve client + client = RayServeClient(backend_url) + + def generate_video( + prompt, + negative_prompt, + use_negative_prompt, + seed, + guidance_scale, + num_frames, + height, + width, + num_inference_steps, + randomize_seed=False, + ): + # Check backend health first + if not client.check_health(): + return None, f"Backend is not available. Please check if Ray Serve is running at {backend_url}" + + # Prepare request data + request_data = { + "prompt": prompt, + "negative_prompt": negative_prompt, + "use_negative_prompt": use_negative_prompt, + "seed": seed, + "guidance_scale": guidance_scale, + "num_frames": num_frames, + "height": height, + "width": width, + "num_inference_steps": num_inference_steps, + "randomize_seed": randomize_seed + } + + # Send request to backend + response = client.generate_video(request_data) + + if response.get("success", False): + output_path = response.get("output_path", "") + used_seed = response.get("seed", seed) + print(f"Used seed: {used_seed}") + print(f"Output path: {output_path}") + + # Check if the video file exists + if os.path.exists(output_path): + return output_path, used_seed + else: + return None, f"Video generated but file not found at {output_path}" + else: + error_msg = response.get("error_message", "Unknown error occurred") + return None, f"Generation failed: {error_msg}" + + # Example prompts + examples = [ + "A hand enters the frame, pulling a sheet of plastic wrap over three balls of dough placed on a wooden surface. The plastic wrap is stretched to cover the dough more securely. The hand adjusts the wrap, ensuring that it is tight and smooth over the dough. The scene focuses on the hand's movements as it secures the edges of the plastic wrap. No new objects appear, and the camera remains stationary, focusing on the action of covering the dough.", + "A vintage train snakes through the mountains, its plume of white steam rising dramatically against the jagged peaks. The cars glint in the late afternoon sun, their deep crimson and gold accents lending a touch of elegance. The tracks carve a precarious path along the cliffside, revealing glimpses of a roaring river far below. Inside, passengers peer out the large windows, their faces lit with awe as the landscape unfolds.", + "A crowded rooftop bar buzzes with energy, the city skyline twinkling like a field of stars in the background. Strings of fairy lights hang above, casting a warm, golden glow over the scene. Groups of people gather around high tables, their laughter blending with the soft rhythm of live jazz. The aroma of freshly mixed cocktails and charred appetizers wafts through the air, mingling with the cool night breeze.", + ] + + # Create Gradio interface + with gr.Blocks() as demo: + gr.Markdown("# FastVideo Inference Demo (Ray Serve Backend)") + gr.Markdown(f"**Backend URL:** {backend_url}") + + # Backend status indicator + status_text = gr.Text( + label="Backend Status", + value="Checking backend status...", + interactive=False + ) + + def update_status(): + if client.check_health(): + return "✅ Backend is healthy and ready" + else: + return "❌ Backend is not available" + + with gr.Group(): + with gr.Row(): + prompt = gr.Text( + label="Prompt", + show_label=False, + max_lines=1, + placeholder="Enter your prompt", + container=False, + ) + run_button = gr.Button("Run", scale=0) + + result = gr.Video(label="Result", show_label=False) + error_output = gr.Text(label="Error", visible=False) + + with gr.Accordion("Advanced options", open=False): + with gr.Group(): + with gr.Row(): + height = gr.Slider( + label="Height", + minimum=256, + maximum=1024, + step=32, + value=448, + ) + width = gr.Slider( + label="Width", + minimum=256, + maximum=1024, + step=32, + value=832 + ) + + with gr.Row(): + num_frames = gr.Slider( + label="Number of Frames", + minimum=16, + maximum=160, + step=16, + value=61, + ) + guidance_scale = gr.Slider( + label="Guidance Scale", + minimum=1, + maximum=12, + value=3.0, + ) + num_inference_steps = gr.Slider( + label="Inference Steps", + minimum=3, + maximum=100, + value=3, + ) + + with gr.Row(): + use_negative_prompt = gr.Checkbox( + label="Use negative prompt", value=False) + negative_prompt = gr.Text( + label="Negative prompt", + max_lines=1, + placeholder="Enter a negative prompt", + visible=False, + ) + + seed = gr.Slider( + label="Seed", + minimum=0, + maximum=1000000, + step=1, + value=1024 + ) + randomize_seed = gr.Checkbox(label="Randomize seed", value=False) + seed_output = gr.Number(label="Used Seed") + + gr.Examples(examples=examples, inputs=prompt) + + # Event handlers + use_negative_prompt.change( + fn=lambda x: gr.update(visible=x), + inputs=use_negative_prompt, + outputs=negative_prompt, + ) + + def handle_generation(*args): + result_path, seed_or_error = generate_video(*args) + + if result_path and os.path.exists(result_path): + return result_path, seed_or_error, gr.update(visible=False) + else: + return None, seed_or_error, gr.update(visible=True, value=seed_or_error) + + run_button.click( + fn=handle_generation, + inputs=[ + prompt, + negative_prompt, + use_negative_prompt, + seed, + guidance_scale, + num_frames, + height, + width, + num_inference_steps, + randomize_seed, + ], + outputs=[result, seed_output, error_output], + ) + + # Update status periodically + demo.load(update_status, outputs=status_text) + + return demo + + +def main(): + parser = argparse.ArgumentParser(description="FastVideo Gradio Frontend") + parser.add_argument("--backend_url", + type=str, + default="http://localhost:8000", + help="URL of the Ray Serve backend") + parser.add_argument("--model_path", + type=str, + default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers", + help="Path to the model (for default parameters)") + parser.add_argument("--host", + type=str, + default="0.0.0.0", + help="Host to bind to") + parser.add_argument("--port", + type=int, + default=7860, + help="Port to bind to") + + args = parser.parse_args() + + # Load default parameters from the model + # try: + # default_params = SamplingParam.from_pretrained(args.model_path) + # except Exception as e: + # print(f"Warning: Could not load default parameters from {args.model_path}: {e}") + # print("Using fallback default parameters...") + # # Create fallback default parameters + default_params = SamplingParam() + default_params.height = 448 + default_params.width = 832 + default_params.num_frames = 21 + default_params.guidance_scale = 7.5 + default_params.num_inference_steps = 20 + default_params.seed = 1024 + + # Create and launch the interface + demo = create_gradio_interface(args.backend_url, default_params) + + print(f"Starting Gradio frontend at http://{args.host}:{args.port}") + print(f"Backend URL: {args.backend_url}") + + demo.queue(max_size=20).launch( + server_name=args.host, + server_port=args.port, + allowed_paths=[os.path.abspath("outputs")] + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/inference/gradio/ray_serve_backend.py b/examples/inference/gradio/ray_serve_backend.py new file mode 100644 index 000000000..99fb53bc6 --- /dev/null +++ b/examples/inference/gradio/ray_serve_backend.py @@ -0,0 +1,209 @@ +import os +import torch +from copy import deepcopy +from typing import Dict, Any, Optional + +import ray +from ray import serve +from fastapi import FastAPI +from pydantic import BaseModel + + +class VideoGenerationRequest(BaseModel): + prompt: str + negative_prompt: Optional[str] = None + use_negative_prompt: bool = False + seed: int = 42 + guidance_scale: float = 7.5 + num_frames: int = 21 + height: int = 448 + width: int = 832 + num_inference_steps: int = 20 + randomize_seed: bool = False + + +class VideoGenerationResponse(BaseModel): + output_path: str + seed: int + success: bool + error_message: Optional[str] = None + + +# Create FastAPI app +app = FastAPI() + + +@serve.deployment( + num_replicas=1, + ray_actor_options={"num_gpus": 1}, +) +@serve.ingress(app) +class FastVideoAPI: + def __init__(self, model_path: str, output_path: str): + self.model_path = model_path + self.output_path = output_path + + # Initialize the video generator + self.generator = None # Initialize to None + self.default_params = None # Initialize to None + + # Ensure output directory exists + os.makedirs(output_path, exist_ok=True) + + def _initialize_model(self): + if self.generator is None: + # Set VSA environment variable + os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN" + + # Import only when needed - use direct imports to avoid module-level execution + from fastvideo.entrypoints.video_generator import VideoGenerator + from fastvideo.configs.sample.base import SamplingParam + + self.generator = VideoGenerator.from_pretrained( + model_path=self.model_path, + num_gpus=1, + use_fsdp_inference=True, + # Adjust these offload parameters if you have < 32GB of VRAM + text_encoder_cpu_offload=False, + dit_cpu_offload=False, + vae_cpu_offload=False, + VSA_sparsity=0.8, + ) + self.default_params = SamplingParam.from_pretrained(self.model_path) + + @app.post("/generate_video", response_model=VideoGenerationResponse) + async def generate_video(self, request: VideoGenerationRequest) -> VideoGenerationResponse: + try: + self._initialize_model() # Ensure model is initialized + + # Create a copy of default parameters + params = deepcopy(self.default_params) + + # Update parameters with request values + params.prompt = request.prompt + # Only override negative prompt if user explicitly opts in + if request.use_negative_prompt: + params.negative_prompt = request.negative_prompt + + params.seed = request.seed + params.guidance_scale = request.guidance_scale + params.num_frames = request.num_frames + params.height = request.height + params.width = request.width + params.num_inference_steps = request.num_inference_steps + + # Handle seed randomization + if request.randomize_seed: + params.seed = torch.randint(0, 1000000, (1,)).item() + + # Ensure negative_prompt is a non-None string; FastVideo validation disallows None + if params.negative_prompt is None: + params.negative_prompt = "" # empty string satisfies validator + + # Set up output path and video saving + params.save_video = True + params.output_path = self.output_path + # params.return_frames = False # avoid keeping frames in memory + + # Create a clean filename from the prompt + safe_prompt = request.prompt[:100].replace(' ', '_').replace('/', '_').replace('\\', '_') + # Store desired video name inside the SamplingParam to avoid unknown kwarg errors + setattr(params, "output_video_name", safe_prompt) + # Generate the video with proper output path and filename + self.generator.generate_video( + prompt=request.prompt, + sampling_param=params, + ) + + # The actual output path where the video was saved + output_path = os.path.join(self.output_path, f"{safe_prompt}.mp4") + + # Verify the file exists + if not os.path.exists(output_path): + raise FileNotFoundError(f"Video was not saved to expected location: {output_path}") + + + response = VideoGenerationResponse( + output_path=output_path, + seed=params.seed, + success=True + ) + + # Memory cleanup to avoid OOM in repeated generations + import gc + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + return response + except Exception as e: + return VideoGenerationResponse( + output_path="", + seed=request.seed, + success=False, + error_message=str(e) + ) + + @app.get("/health") + async def health_check(self): + return {"status": "healthy"} + + +def start_ray_serve( + model_path: str = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers", + output_path: str = "outputs", + host: str = "0.0.0.0", + port: int = 8000 +): + """Start the Ray Serve backend""" + # Initialize Ray + if not ray.is_initialized(): + ray.init() + + # Deploy the API + api = FastVideoAPI.bind(model_path, output_path) + serve.run(api, route_prefix="/", name="fast_video") # detach + + print(f"Ray Serve backend started at http://{host}:{port}") + print(f"Health check: http://{host}:{port}/health") + print(f"Video generation endpoint: http://{host}:{port}/generate_video") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="FastVideo Ray Serve Backend") + parser.add_argument("--model_path", + type=str, + default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers", + help="Path to the model") + parser.add_argument("--output_path", + type=str, + default="outputs", + help="Path to save generated videos") + parser.add_argument("--host", + type=str, + default="0.0.0.0", + help="Host to bind to") + parser.add_argument("--port", + type=int, + default=8000, + help="Port to bind to") + + args = parser.parse_args() + + start_ray_serve( + model_path=args.model_path, + output_path=args.output_path, + host=args.host, + port=args.port, + ) + + # ---- keep the process alive --------------------------------- + import signal, sys, time + signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) # Ctrl-C + signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) # docker stop etc. + + print("✅ FastVideo backend is running. Press Ctrl-C to stop.") + while True: + time.sleep(3600) \ No newline at end of file diff --git a/examples/inference/gradio/start_ray_serve_app.py b/examples/inference/gradio/start_ray_serve_app.py new file mode 100644 index 000000000..d87bebb24 --- /dev/null +++ b/examples/inference/gradio/start_ray_serve_app.py @@ -0,0 +1,224 @@ +""" +Startup script for FastVideo with Ray Serve backend and Gradio frontend. +This script starts both the backend and frontend services. +""" + +import argparse +import os +import subprocess +import sys +import time +import threading +import signal +import requests +from pathlib import Path + +# Add the project root to the Python path +project_root = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(project_root)) + + +def check_backend_health(backend_url: str, max_retries: int = 30) -> bool: + """Check if the backend is healthy""" + health_url = f"{backend_url}/health" + + for i in range(max_retries): + try: + response = requests.get(health_url, timeout=5) + if response.status_code == 200: + print(f"✅ Backend is healthy at {backend_url}") + return True + except requests.exceptions.RequestException: + pass + + if i < max_retries - 1: + print(f"⏳ Waiting for backend to start... ({i+1}/{max_retries})") + time.sleep(2) + + print(f"❌ Backend failed to start within {max_retries * 2} seconds") + return False + + +def start_backend(args): + """Start the Ray Serve backend""" + backend_script = Path(__file__).parent / "ray_serve_backend.py" + + cmd = [ + sys.executable, str(backend_script), + "--model_path", args.model_path, + "--output_path", args.output_path, + "--host", args.backend_host, + "--port", str(args.backend_port) + ] + + print(f"🚀 Starting Ray Serve backend...") + print(f"Command: {' '.join(cmd)}") + + # Start the backend process + backend_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1 + ) + + # Monitor backend output + def monitor_backend(): + for line in backend_process.stdout: + print(f"[BACKEND] {line.rstrip()}") + + monitor_thread = threading.Thread(target=monitor_backend, daemon=True) + monitor_thread.start() + + return backend_process + + +def start_frontend(args): + """Start the Gradio frontend""" + frontend_script = Path(__file__).parent / "gradio_frontend.py" + backend_url = f"http://{args.backend_host}:{args.backend_port}" + + cmd = [ + sys.executable, str(frontend_script), + "--backend_url", backend_url, + "--model_path", args.model_path, + "--host", args.frontend_host, + "--port", str(args.frontend_port) + ] + + print(f"🎨 Starting Gradio frontend...") + print(f"Command: {' '.join(cmd)}") + + # Start the frontend process + frontend_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1 + ) + + # Monitor frontend output + def monitor_frontend(): + for line in frontend_process.stdout: + print(f"[FRONTEND] {line.rstrip()}") + + monitor_thread = threading.Thread(target=monitor_frontend, daemon=True) + monitor_thread.start() + + return frontend_process + + +def main(): + parser = argparse.ArgumentParser(description="FastVideo Ray Serve App") + + # Model and output settings + parser.add_argument("--model_path", + type=str, + default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers", + help="Path to the model") + parser.add_argument("--output_path", + type=str, + default="outputs", + help="Path to save generated videos") + + # Backend settings + parser.add_argument("--backend_host", + type=str, + default="0.0.0.0", + help="Backend host to bind to") + parser.add_argument("--backend_port", + type=int, + default=8000, + help="Backend port to bind to") + + # Frontend settings + parser.add_argument("--frontend_host", + type=str, + default="0.0.0.0", + help="Frontend host to bind to") + parser.add_argument("--frontend_port", + type=int, + default=7860, + help="Frontend port to bind to") + + # Other settings + parser.add_argument("--skip_backend_check", + action="store_true", + help="Skip backend health check") + + args = parser.parse_args() + + # Ensure output directory exists + os.makedirs(args.output_path, exist_ok=True) + + print("🎬 FastVideo Ray Serve App") + print("=" * 50) + print(f"Model: {args.model_path}") + print(f"Output: {args.output_path}") + print(f"Backend: http://{args.backend_host}:{args.backend_port}") + print(f"Frontend: http://{args.frontend_host}:{args.frontend_port}") + print("=" * 50) + + # Start backend + backend_process = start_backend(args) + + # Wait for backend to be ready + backend_url = f"http://{args.backend_host}:{args.backend_port}" + + if not args.skip_backend_check: + if not check_backend_health(backend_url): + print("❌ Backend failed to start. Terminating...") + backend_process.terminate() + sys.exit(1) + + # Start frontend + frontend_process = start_frontend(args) + + print("\n🎉 Both services are starting up!") + print(f"📺 Frontend will be available at: http://{args.frontend_host}:{args.frontend_port}") + print(f"🔧 Backend API will be available at: {backend_url}") + print("\nPress Ctrl+C to stop both services...") + + # Signal handler for graceful shutdown + def signal_handler(signum, frame): + print("\n🛑 Shutting down services...") + frontend_process.terminate() + backend_process.terminate() + + # Wait for processes to terminate + try: + frontend_process.wait(timeout=5) + backend_process.wait(timeout=5) + except subprocess.TimeoutExpired: + print("⚠️ Force killing processes...") + frontend_process.kill() + backend_process.kill() + + print("✅ Services stopped") + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Monitor processes + try: + while True: + # Check if processes are still running + if frontend_process.poll() is not None: + print("❌ Frontend process died unexpectedly") + break + + if backend_process.poll() is not None: + print("❌ Backend process died unexpectedly") + break + + time.sleep(1) + + except KeyboardInterrupt: + signal_handler(signal.SIGINT, None) + + +if __name__ == "__main__": + main() \ No newline at end of file