Skydata001 commited on
Commit
01b9ca1
·
verified ·
1 Parent(s): 525bdd3

Upload 6 files

Browse files
Files changed (6) hide show
  1. .dockerignore +15 -0
  2. Dockerfile +33 -13
  3. README.md +76 -26
  4. app.py +409 -0
  5. docker-compose.yml +16 -11
  6. requirements.txt +12 -9
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .git/
12
+ .gitignore
13
+ outputs/*.mp4
14
+ outputs/*.jpg
15
+ .DS_Store
Dockerfile CHANGED
@@ -1,24 +1,44 @@
1
- FROM python:3.11-slim
2
 
3
  ENV DEBIAN_FRONTEND=noninteractive
4
  ENV PYTHONUNBUFFERED=1
5
- ENV HF_HOME=/tmp/huggingface
6
- ENV TRANSFORMERS_CACHE=/tmp/huggingface
7
- ENV TORCH_HOME=/tmp/torch
 
8
 
9
- RUN apt-get update && apt-get install -y --no-install-recommends \
10
- build-essential git curl wget libgomp1 \
 
 
 
11
  && rm -rf /var/lib/apt/lists/*
12
 
 
 
 
13
  WORKDIR /app
14
 
15
- RUN pip install --no-cache-dir torch==2.5.1 --index-url https://download.pytorch.org/whl/cu121
16
- RUN pip install --no-cache-dir transformers==4.46.0 accelerate==1.0.0 bitsandbytes==0.44.0
17
- RUN pip install --no-cache-dir protobuf sentencepiece
18
- RUN pip install --no-cache-dir flask==3.0.3 flask-cors==4.0.0 requests==2.31.0
 
 
 
 
 
19
 
20
- COPY server.py .
21
- COPY chat.html .
 
 
 
 
 
 
 
22
 
23
  EXPOSE 7860
24
- CMD ["python3", "server.py"]
 
 
1
+ FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
2
 
3
  ENV DEBIAN_FRONTEND=noninteractive
4
  ENV PYTHONUNBUFFERED=1
5
+ ENV HF_HOME=/app/hf_cache
6
+ ENV CUDA_HOME=/usr/local/cuda
7
+ ENV PATH=${CUDA_HOME}/bin:${PATH}
8
+ ENV LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH}
9
 
10
+ # Install system dependencies
11
+ RUN apt-get update && apt-get install -y \
12
+ python3.11 python3.11-pip python3.11-venv \
13
+ git wget curl ffmpeg libsm6 libxext6 \
14
+ libgl1 libglib2.0-0 \
15
  && rm -rf /var/lib/apt/lists/*
16
 
17
+ RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
18
+ RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1
19
+
20
  WORKDIR /app
21
 
22
+ # Upgrade pip
23
+ RUN python -m pip install --no-cache-dir --upgrade pip setuptools wheel
24
+
25
+ # Install PyTorch with CUDA
26
+ RUN pip install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
27
+
28
+ # Copy requirements and install
29
+ COPY requirements.txt .
30
+ RUN pip install --no-cache-dir -r requirements.txt
31
 
32
+ # Pre-download model components (optional - can also download at runtime)
33
+ # This speeds up first run but increases image size
34
+ # RUN python -c "from diffusers import AutoencoderKLWan, WanImageToVideoPipeline; from transformers import CLIPVisionModel; print('Diffusers ready')"
35
+
36
+ # Copy app code
37
+ COPY . .
38
+
39
+ # Create directories
40
+ RUN mkdir -p /app/outputs /app/hf_cache /app/static
41
 
42
  EXPOSE 7860
43
+
44
+ CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
README.md CHANGED
@@ -1,28 +1,78 @@
1
- ---
2
- title: Titan Group Chat
3
- emoji: 🧠
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: docker
7
- app_port: 7860
8
- ---
9
-
10
- # Titan Group Chat
11
-
12
- نظام دردشة جماعية ذكي بـ 3 نماذج AI (Favor, Brain, Cell).
13
-
14
- ## المميزات
15
- - 3 نماذج AI مختلفة
16
- - أفكار داخلية (Internal Thoughts)
17
- - حالات داخلية (Internal State)
18
- - صلاحيات (Permissions)
19
- - مؤشر "يكتب الآن"
20
- - ذاكرة طويلة المدى (Turso DB)
21
 
22
  ## التشغيل
23
- 1. اضبط Secrets:
24
- - `HF_TOKEN`
25
- - `TURSO_DB_URL`
26
- - `TURSO_DB_TOKEN`
27
- - `ACCESS_PASSWORD`
28
- 2. Factory Rebuild
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Wan 2.1 I2V 14B - Image to Video Generator
2
+
3
+ أداة تحويل الصور إلى فيديو باستخدام نموذج **Wan 2.1 I2V 14B 720P** على NVIDIA A100.
4
+
5
+ ## المتطلبات
6
+
7
+ - NVIDIA GPU مع دعم CUDA (موصى: A100 80GB)
8
+ - Docker + Docker Compose
9
+ - NVIDIA Docker Runtime
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  ## التشغيل
12
+
13
+ ### 1. تعديل المفتاح السري
14
+
15
+ عدّل ملف `.env` أو اضبط متغير البيئة `PASS_KEY`:
16
+
17
+ ```bash
18
+ PASS_KEY=your-secret-key-here
19
+ ```
20
+
21
+ ### 2. البناء والتشغيل
22
+
23
+ ```bash
24
+ cd wan-i2v-tool
25
+
26
+ # باستخدام Docker Compose (موصى)
27
+ docker-compose up --build -d
28
+
29
+ # أو باستخدام Docker مباشرة
30
+ docker build -t wan-i2v .
31
+ docker run -d --gpus all -p 7860:7860 -e PASS_KEY=your-secret-key -e MODEL_ID=Wan-AI/Wan2.1-I2V-14B-720P-Diffusers -v $(pwd)/outputs:/app/outputs -v hf_cache:/app/hf_cache --shm-size=16gb wan-i2v
32
+ ```
33
+
34
+ ### 3. الوصول
35
+
36
+ افتح المتصفح على: `http://localhost:7860`
37
+
38
+ أدخل مفتاح المرور في صفحة `pass.html` للوصول إلى الأداة.
39
+
40
+ ## الميزات
41
+
42
+ - ✅ **Wan 2.1 I2V 14B 720P** - أحدث نموذج لتحريك الصور
43
+ - ✅ **جودة عالية** - Negative prompt محسّن ضد التشوهات
44
+ - ✅ **A100 محسّن** - torch.compile + VAE slicing/tiling
45
+ - ✅ **واجهة عربية** - Dark mode, mobile first, responsive
46
+ - ✅ **مصادقة آمنة** - مفتاح سري في `.env`
47
+ - ✅ **لا قيود** - Safety: Middle, No restrictions
48
+ - ✅ **تحكم كامل** - Resolution, FPS, Frames, Guidance, Steps, Seed
49
+ - ✅ **معاينة مباشرة** - تشغيل الفيديو في المتصفح
50
+ - ✅ **حفظ/تحميل** - زر حفظ الفيديو
51
+ - ✅ **تتبع المهام** - قائمة المهام مع التقدم الحي
52
+
53
+ ## API Endpoints
54
+
55
+ | Endpoint | Method | Description |
56
+ |----------|--------|-------------|
57
+ | `/api/auth` | POST | التحقق من المفتاح |
58
+ | `/api/generate` | POST | توليد فيديو |
59
+ | `/api/status/{job_id}` | GET | حالة المهمة |
60
+ | `/api/download/{job_id}` | GET | تحميل الفيديو |
61
+ | `/api/jobs` | GET | قائمة المهام |
62
+ | `/api/jobs/{job_id}` | DELETE | حذف مهمة |
63
+ | `/health` | GET | حالة النظام |
64
+
65
+ ## الإعدادات الافتراضية
66
+
67
+ - **الدقة:** 720×1280
68
+ - **الإطارات:** 81 (~10 ثواني بـ 8fps)
69
+ - **خطوات الاستنتاج:** 30
70
+ - **Guidance Scale:** 5.0
71
+ - **Negative Prompt:** محسّن تلقائياً ضد التشوهات
72
+
73
+ ## ملاحظات
74
+
75
+ - أول تشغيل يحمل النموذج من HuggingFace (~30GB)
76
+ - يُفضّل كتابة البرومبت بالإنجليزية
77
+ - وقت التوليد: 3-8 دقائق حسب الإعدادات
78
+ - يدعم رفع صور JPG/PNG/WEBP
app.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Wan 2.1 I2V 14B - Image to Video Generator
4
+ Backend: FastAPI + Diffusers
5
+ Optimized for NVIDIA A100 80GB
6
+ """
7
+
8
+ import os
9
+ import io
10
+ import base64
11
+ import uuid
12
+ import json
13
+ import asyncio
14
+ import threading
15
+ from datetime import datetime
16
+ from typing import Optional
17
+ from contextlib import asynccontextmanager
18
+
19
+ import torch
20
+ import numpy as np
21
+ from PIL import Image
22
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
23
+ from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
24
+ from fastapi.staticfiles import StaticFiles
25
+ from fastapi.middleware.cors import CORSMiddleware
26
+ from pydantic import BaseModel
27
+ from dotenv import load_dotenv
28
+
29
+ # Diffusers imports
30
+ from diffusers import AutoencoderKLWan, WanImageToVideoPipeline
31
+ from diffusers.utils import export_to_video, load_image
32
+ from transformers import CLIPVisionModel
33
+
34
+ # Load env
35
+ load_dotenv()
36
+
37
+ # Config
38
+ PASS_KEY = os.getenv("PASS_KEY", "default-key-change-me")
39
+ MODEL_ID = os.getenv("MODEL_ID", "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers")
40
+ DEVICE = os.getenv("DEVICE", "cuda")
41
+ TORCH_DTYPE = torch.bfloat16 if os.getenv("TORCH_DTYPE", "bfloat16") == "bfloat16" else torch.float16
42
+
43
+ # Paths
44
+ OUTPUT_DIR = "/app/outputs"
45
+ STATIC_DIR = "/app/static"
46
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
47
+ os.makedirs(STATIC_DIR, exist_ok=True)
48
+
49
+ # Global pipeline
50
+ pipe = None
51
+ pipe_lock = threading.Lock()
52
+
53
+ # Active jobs
54
+ active_jobs = {}
55
+
56
+ # ============================================================
57
+ # MODEL LOADING
58
+ # ============================================================
59
+ def load_model():
60
+ """Load Wan 2.1 I2V 14B 720P model."""
61
+ global pipe
62
+ print(f"[INIT] Loading model: {MODEL_ID}")
63
+ print(f"[INIT] Device: {DEVICE}, Dtype: {TORCH_DTYPE}")
64
+
65
+ # Load components
66
+ image_encoder = CLIPVisionModel.from_pretrained(
67
+ MODEL_ID,
68
+ subfolder="image_encoder",
69
+ torch_dtype=torch.float32
70
+ )
71
+
72
+ vae = AutoencoderKLWan.from_pretrained(
73
+ MODEL_ID,
74
+ subfolder="vae",
75
+ torch_dtype=torch.float32
76
+ )
77
+
78
+ pipe = WanImageToVideoPipeline.from_pretrained(
79
+ MODEL_ID,
80
+ vae=vae,
81
+ image_encoder=image_encoder,
82
+ torch_dtype=TORCH_DTYPE,
83
+ )
84
+
85
+ pipe.to(DEVICE)
86
+
87
+ # Optimizations for A100
88
+ if hasattr(pipe, 'enable_vae_slicing'):
89
+ pipe.enable_vae_slicing()
90
+ if hasattr(pipe, 'enable_vae_tiling'):
91
+ pipe.enable_vae_tiling()
92
+
93
+ # Optional: torch.compile for transformer (A100 benefits greatly)
94
+ try:
95
+ if hasattr(pipe, 'transformer'):
96
+ pipe.transformer = torch.compile(pipe.transformer, mode="max-autotune", fullgraph=False)
97
+ print("[INIT] torch.compile applied to transformer")
98
+ except Exception as e:
99
+ print(f"[INIT] torch.compile skipped: {e}")
100
+
101
+ print("[INIT] Model loaded successfully")
102
+ return pipe
103
+
104
+
105
+ # ============================================================
106
+ # LIFESPAN
107
+ # ============================================================
108
+ @asynccontextmanager
109
+ async def lifespan(app: FastAPI):
110
+ print("[STARTUP] Initializing Wan 2.1 I2V Generator...")
111
+ load_model()
112
+ print("[STARTUP] Ready")
113
+ yield
114
+ print("[SHUTDOWN] Cleaning up...")
115
+
116
+
117
+ app = FastAPI(
118
+ title="Wan 2.1 I2V Generator",
119
+ description="Image to Video using Wan 2.1 14B 720P",
120
+ version="2.1.0",
121
+ lifespan=lifespan
122
+ )
123
+
124
+ app.add_middleware(
125
+ CORSMiddleware,
126
+ allow_origins=["*"],
127
+ allow_credentials=True,
128
+ allow_methods=["*"],
129
+ allow_headers=["*"],
130
+ )
131
+
132
+ # Static files
133
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
134
+
135
+
136
+ # ============================================================
137
+ # AUTH
138
+ # ============================================================
139
+ class AuthRequest(BaseModel):
140
+ pass_key: str
141
+
142
+ @app.post("/api/auth")
143
+ async def authenticate(data: AuthRequest):
144
+ if data.pass_key != PASS_KEY:
145
+ raise HTTPException(status_code=401, detail="Invalid pass key")
146
+ return {"status": "ok", "token": PASS_KEY}
147
+
148
+
149
+ # ============================================================
150
+ # GENERATION
151
+ # ============================================================
152
+ class GenerationRequest(BaseModel):
153
+ prompt: str
154
+ negative_prompt: Optional[str] = None
155
+ height: int = 720
156
+ width: int = 1280
157
+ num_frames: int = 81
158
+ guidance_scale: float = 5.0
159
+ num_inference_steps: int = 30
160
+ fps: int = 8
161
+ seed: Optional[int] = None
162
+
163
+
164
+ def aspect_ratio_resize(image, max_area=720*1280, mod_value=32):
165
+ """Smart resize maintaining aspect ratio."""
166
+ aspect_ratio = image.height / image.width
167
+ height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
168
+ width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
169
+ if height < 256: height = 256
170
+ if width < 256: width = 256
171
+ image = image.resize((width, height), Image.LANCZOS)
172
+ return image, height, width
173
+
174
+
175
+ def generate_video_task(job_id: str, image_path: str, params: dict):
176
+ """Background video generation task."""
177
+ try:
178
+ active_jobs[job_id] = {"status": "processing", "progress": 0, "message": "Loading image..."}
179
+
180
+ # Load and preprocess image
181
+ image = Image.open(image_path).convert("RGB")
182
+ max_area = params.get("height", 720) * params.get("width", 1280)
183
+ image, height, width = aspect_ratio_resize(image, max_area=max_area)
184
+
185
+ active_jobs[job_id]["message"] = f"Generating {height}x{width} video..."
186
+ active_jobs[job_id]["progress"] = 10
187
+
188
+ # Default negative prompt optimized for quality
189
+ default_negative = (
190
+ "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, "
191
+ "images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, "
192
+ "incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, "
193
+ "misshapen limbs, fused fingers, still picture, messy background, three legs, many people "
194
+ "in the background, walking backwards, distorted face, mutated hands, extra limbs, "
195
+ "malformed anatomy, bad anatomy, watermark, text, logo, cropped, out of frame"
196
+ )
197
+
198
+ negative_prompt = params.get("negative_prompt", "") or default_negative
199
+
200
+ # Seed
201
+ seed = params.get("seed")
202
+ generator = torch.Generator(device=DEVICE).manual_seed(seed) if seed else None
203
+
204
+ # Progress callback
205
+ total_steps = params.get("num_inference_steps", 30)
206
+ def progress_callback(step, timestep, latents):
207
+ progress = int((step / total_steps) * 80) + 10
208
+ active_jobs[job_id]["progress"] = min(progress, 90)
209
+ active_jobs[job_id]["message"] = f"Denoising step {step}/{total_steps}..."
210
+
211
+ active_jobs[job_id]["message"] = "Running inference (this may take 3-8 minutes)..."
212
+
213
+ with pipe_lock:
214
+ output = pipe(
215
+ image=image,
216
+ prompt=params["prompt"],
217
+ negative_prompt=negative_prompt,
218
+ height=height,
219
+ width=width,
220
+ num_frames=params.get("num_frames", 81),
221
+ guidance_scale=params.get("guidance_scale", 5.0),
222
+ num_inference_steps=total_steps,
223
+ generator=generator,
224
+ callback_on_step_end=progress_callback,
225
+ ).frames[0]
226
+
227
+ # Export video
228
+ fps = params.get("fps", 8)
229
+ output_filename = f"{job_id}.mp4"
230
+ output_path = os.path.join(OUTPUT_DIR, output_filename)
231
+ export_to_video(output, output_path, fps=fps)
232
+
233
+ active_jobs[job_id] = {
234
+ "status": "completed",
235
+ "progress": 100,
236
+ "message": "Video generated successfully",
237
+ "output_file": output_filename,
238
+ "output_path": output_path,
239
+ "height": height,
240
+ "width": width,
241
+ "fps": fps,
242
+ "duration": round(params.get("num_frames", 81) / fps, 1),
243
+ "created_at": datetime.now().isoformat()
244
+ }
245
+
246
+ except Exception as e:
247
+ active_jobs[job_id] = {
248
+ "status": "failed",
249
+ "progress": 0,
250
+ "message": str(e)
251
+ }
252
+ print(f"[ERROR] Job {job_id} failed: {e}")
253
+
254
+
255
+ @app.post("/api/generate")
256
+ async def generate_video(
257
+ request: Request,
258
+ image: UploadFile = File(...),
259
+ prompt: str = Form(...),
260
+ negative_prompt: Optional[str] = Form(None),
261
+ height: int = Form(720),
262
+ width: int = Form(1280),
263
+ num_frames: int = Form(81),
264
+ guidance_scale: float = Form(5.0),
265
+ num_inference_steps: int = Form(30),
266
+ fps: int = Form(8),
267
+ seed: Optional[int] = Form(None),
268
+ pass_key: str = Form(...)
269
+ ):
270
+ # Auth check
271
+ if pass_key != PASS_KEY:
272
+ raise HTTPException(status_code=401, detail="Unauthorized")
273
+
274
+ # Validate image
275
+ if not image.content_type or not image.content_type.startswith("image/"):
276
+ raise HTTPException(status_code=400, detail="File must be an image")
277
+
278
+ # Clamp values for safety
279
+ height = max(256, min(1280, height))
280
+ width = max(256, min(1920, width))
281
+ num_frames = max(16, min(161, num_frames))
282
+ guidance_scale = max(1.0, min(20.0, guidance_scale))
283
+ num_inference_steps = max(10, min(100, num_inference_steps))
284
+ fps = max(1, min(60, fps))
285
+
286
+ job_id = str(uuid.uuid4())
287
+
288
+ # Save uploaded image
289
+ image_bytes = await image.read()
290
+ image_path = os.path.join(OUTPUT_DIR, f"{job_id}_input.jpg")
291
+ with open(image_path, "wb") as f:
292
+ f.write(image_bytes)
293
+
294
+ params = {
295
+ "prompt": prompt,
296
+ "negative_prompt": negative_prompt,
297
+ "height": height,
298
+ "width": width,
299
+ "num_frames": num_frames,
300
+ "guidance_scale": guidance_scale,
301
+ "num_inference_steps": num_inference_steps,
302
+ "fps": fps,
303
+ "seed": seed,
304
+ }
305
+
306
+ # Start background task
307
+ active_jobs[job_id] = {"status": "queued", "progress": 0, "message": "Queued..."}
308
+ thread = threading.Thread(target=generate_video_task, args=(job_id, image_path, params))
309
+ thread.start()
310
+
311
+ return {"job_id": job_id, "status": "queued"}
312
+
313
+
314
+ @app.get("/api/status/{job_id}")
315
+ async def get_status(job_id: str, pass_key: str):
316
+ if pass_key != PASS_KEY:
317
+ raise HTTPException(status_code=401, detail="Unauthorized")
318
+
319
+ if job_id not in active_jobs:
320
+ raise HTTPException(status_code=404, detail="Job not found")
321
+
322
+ return active_jobs[job_id]
323
+
324
+
325
+ @app.get("/api/download/{job_id}")
326
+ async def download_video(job_id: str, pass_key: str):
327
+ if pass_key != PASS_KEY:
328
+ raise HTTPException(status_code=401, detail="Unauthorized")
329
+
330
+ if job_id not in active_jobs or active_jobs[job_id].get("status") != "completed":
331
+ raise HTTPException(status_code=404, detail="Video not ready")
332
+
333
+ output_path = active_jobs[job_id]["output_path"]
334
+ if not os.path.exists(output_path):
335
+ raise HTTPException(status_code=404, detail="File not found")
336
+
337
+ return FileResponse(
338
+ output_path,
339
+ media_type="video/mp4",
340
+ filename=f"wan_i2v_{job_id}.mp4"
341
+ )
342
+
343
+
344
+ @app.get("/api/jobs")
345
+ async def list_jobs(pass_key: str):
346
+ if pass_key != PASS_KEY:
347
+ raise HTTPException(status_code=401, detail="Unauthorized")
348
+
349
+ jobs = []
350
+ for jid, info in active_jobs.items():
351
+ jobs.append({
352
+ "job_id": jid,
353
+ "status": info.get("status"),
354
+ "progress": info.get("progress"),
355
+ "message": info.get("message"),
356
+ "created_at": info.get("created_at")
357
+ })
358
+ return {"jobs": sorted(jobs, key=lambda x: x.get("created_at", ""), reverse=True)}
359
+
360
+
361
+ @app.delete("/api/jobs/{job_id}")
362
+ async def delete_job(job_id: str, pass_key: str):
363
+ if pass_key != PASS_KEY:
364
+ raise HTTPException(status_code=401, detail="Unauthorized")
365
+
366
+ if job_id in active_jobs:
367
+ info = active_jobs[job_id]
368
+ # Cleanup files
369
+ for ext in ["_input.jpg", ".mp4"]:
370
+ fpath = os.path.join(OUTPUT_DIR, f"{job_id}{ext}")
371
+ if os.path.exists(fpath):
372
+ os.remove(fpath)
373
+ del active_jobs[job_id]
374
+
375
+ return {"status": "deleted"}
376
+
377
+
378
+ # ============================================================
379
+ # ROOT & PAGES
380
+ # ============================================================
381
+ @app.get("/", response_class=HTMLResponse)
382
+ async def root():
383
+ with open(os.path.join(STATIC_DIR, "pass.html"), "r", encoding="utf-8") as f:
384
+ return HTMLResponse(content=f.read())
385
+
386
+
387
+ @app.get("/app", response_class=HTMLResponse)
388
+ async def app_page():
389
+ with open(os.path.join(STATIC_DIR, "index.html"), "r", encoding="utf-8") as f:
390
+ return HTMLResponse(content=f.read())
391
+
392
+
393
+ @app.get("/health")
394
+ async def health():
395
+ return {
396
+ "status": "ok",
397
+ "model_loaded": pipe is not None,
398
+ "model_id": MODEL_ID,
399
+ "device": str(DEVICE),
400
+ "torch_version": torch.__version__,
401
+ "cuda_available": torch.cuda.is_available(),
402
+ "cuda_device": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
403
+ "vram_gb": round(torch.cuda.get_device_properties(0).total_memory / 1024**3, 1) if torch.cuda.is_available() else 0
404
+ }
405
+
406
+
407
+ if __name__ == "__main__":
408
+ import uvicorn
409
+ uvicorn.run(app, host="0.0.0.0", port=7860)
docker-compose.yml CHANGED
@@ -1,16 +1,22 @@
1
- version: "3.8"
2
 
3
  services:
4
- titan-chat:
5
  build: .
6
- container_name: titan-chat
 
 
 
 
 
 
 
 
 
 
 
7
  ports:
8
  - "7860:7860"
9
- environment:
10
- - HF_TOKEN=${HF_TOKEN}
11
- - TURSO_DB_URL=${TURSO_DB_URL}
12
- - TURSO_DB_TOKEN=${TURSO_DB_TOKEN}
13
- - ACCESS_PASSWORD=${ACCESS_PASSWORD}
14
  deploy:
15
  resources:
16
  reservations:
@@ -18,9 +24,8 @@ services:
18
  - driver: nvidia
19
  count: 1
20
  capabilities: [gpu]
21
- volumes:
22
- - huggingface-cache:/tmp/huggingface
23
  restart: unless-stopped
 
24
 
25
  volumes:
26
- huggingface-cache:
 
1
+ version: '3.8'
2
 
3
  services:
4
+ wan-i2v:
5
  build: .
6
+ image: wan-i2v:latest
7
+ container_name: wan-i2v-generator
8
+ runtime: nvidia
9
+ environment:
10
+ - NVIDIA_VISIBLE_DEVICES=all
11
+ - PASS_KEY=${PASS_KEY:-wan2i2v-secret-2026}
12
+ - MODEL_ID=Wan-AI/Wan2.1-I2V-14B-720P-Diffusers
13
+ - TORCH_DTYPE=bfloat16
14
+ - HF_HOME=/app/hf_cache
15
+ volumes:
16
+ - ./outputs:/app/outputs
17
+ - hf_cache:/app/hf_cache
18
  ports:
19
  - "7860:7860"
 
 
 
 
 
20
  deploy:
21
  resources:
22
  reservations:
 
24
  - driver: nvidia
25
  count: 1
26
  capabilities: [gpu]
 
 
27
  restart: unless-stopped
28
+ shm_size: '16gb'
29
 
30
  volumes:
31
+ hf_cache:
requirements.txt CHANGED
@@ -1,9 +1,12 @@
1
- torch==2.5.1
2
- transformers==4.46.0
3
- accelerate==1.0.0
4
- bitsandbytes==0.44.0
5
- protobuf
6
- sentencepiece
7
- flask==3.0.3
8
- flask-cors==4.0.0
9
- requests==2.31.0
 
 
 
 
1
+ torch>=2.6.0
2
+ torchvision>=0.21.0
3
+ diffusers>=0.33.0
4
+ transformers>=4.49.0
5
+ accelerate>=1.6.0
6
+ fastapi>=0.115.0
7
+ uvicorn[standard]>=0.34.0
8
+ python-multipart>=0.0.20
9
+ pillow>=11.0.0
10
+ numpy>=2.0.0
11
+ python-dotenv>=1.0.0
12
+ aiofiles>=24.0.0