Spaces:
Running
Running
Add video model selector with cloud/pod backend support
Browse files- Added video model dropdown for img2video mode (WAN I2V, Kling, Veo, Seedance, Sora)
- Added backend selector for video generation (Cloud API vs RunPod)
- Added cloud video generation endpoint using WaveSpeed API
- Added more text-to-image models (FLUX Pro, WAN 2.6, Dreamina, Qwen)
- Added more video models (Kling O1/O3/Pro, Veo 3/3.1, Sora 2, Seedance)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
src/content_engine/api/routes_video.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
"""Video generation routes — WAN 2.2 img2video on RunPod pod."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 6 |
import logging
|
| 7 |
import os
|
| 8 |
import time
|
|
@@ -20,6 +21,14 @@ router = APIRouter(prefix="/api/video", tags=["video"])
|
|
| 20 |
# Video jobs tracking
|
| 21 |
_video_jobs: dict[str, dict] = {}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Pod state is shared from routes_pod
|
| 24 |
def _get_pod_state():
|
| 25 |
from content_engine.api.routes_pod import _pod_state
|
|
@@ -127,6 +136,168 @@ async def generate_video(
|
|
| 127 |
raise HTTPException(500, f"Generation failed: {e}")
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
async def _poll_video_job(job_id: str, prompt_id: str):
|
| 131 |
"""Poll ComfyUI for video job completion."""
|
| 132 |
import httpx
|
|
|
|
| 1 |
+
"""Video generation routes — WAN 2.2 img2video on RunPod pod or WaveSpeed cloud."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import asyncio
|
| 6 |
+
import base64
|
| 7 |
import logging
|
| 8 |
import os
|
| 9 |
import time
|
|
|
|
| 21 |
# Video jobs tracking
|
| 22 |
_video_jobs: dict[str, dict] = {}
|
| 23 |
|
| 24 |
+
# WaveSpeed provider (initialized from main.py)
|
| 25 |
+
_wavespeed_provider = None
|
| 26 |
+
|
| 27 |
+
def init_wavespeed(provider):
|
| 28 |
+
"""Initialize WaveSpeed provider for cloud video generation."""
|
| 29 |
+
global _wavespeed_provider
|
| 30 |
+
_wavespeed_provider = provider
|
| 31 |
+
|
| 32 |
# Pod state is shared from routes_pod
|
| 33 |
def _get_pod_state():
|
| 34 |
from content_engine.api.routes_pod import _pod_state
|
|
|
|
| 136 |
raise HTTPException(500, f"Generation failed: {e}")
|
| 137 |
|
| 138 |
|
| 139 |
+
@router.post("/generate/cloud")
|
| 140 |
+
async def generate_video_cloud(
|
| 141 |
+
image: UploadFile = File(...),
|
| 142 |
+
prompt: str = Form("smooth motion, high quality video"),
|
| 143 |
+
negative_prompt: str = Form(""),
|
| 144 |
+
model: str = Form("wan-2.6-i2v"),
|
| 145 |
+
num_frames: int = Form(81),
|
| 146 |
+
fps: int = Form(24),
|
| 147 |
+
seed: int = Form(-1),
|
| 148 |
+
):
|
| 149 |
+
"""Generate a video using WaveSpeed cloud API (Kling, WAN I2V, Veo, etc)."""
|
| 150 |
+
import random
|
| 151 |
+
import httpx
|
| 152 |
+
|
| 153 |
+
if not _wavespeed_provider:
|
| 154 |
+
raise HTTPException(500, "WaveSpeed API not configured")
|
| 155 |
+
|
| 156 |
+
job_id = str(uuid.uuid4())[:8]
|
| 157 |
+
seed = seed if seed >= 0 else random.randint(0, 2**32 - 1)
|
| 158 |
+
|
| 159 |
+
# Read the image
|
| 160 |
+
image_bytes = await image.read()
|
| 161 |
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
| 162 |
+
|
| 163 |
+
# Create job entry
|
| 164 |
+
_video_jobs[job_id] = {
|
| 165 |
+
"status": "running",
|
| 166 |
+
"seed": seed,
|
| 167 |
+
"started_at": time.time(),
|
| 168 |
+
"num_frames": num_frames,
|
| 169 |
+
"fps": fps,
|
| 170 |
+
"model": model,
|
| 171 |
+
"backend": "cloud",
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
logger.info("Cloud video generation started: %s (model=%s)", job_id, model)
|
| 175 |
+
|
| 176 |
+
# Start background task for cloud video generation
|
| 177 |
+
asyncio.create_task(_generate_cloud_video(job_id, image_bytes, prompt, negative_prompt, model, seed))
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"job_id": job_id,
|
| 181 |
+
"status": "running",
|
| 182 |
+
"seed": seed,
|
| 183 |
+
"model": model,
|
| 184 |
+
"estimated_time": "~30-120 seconds",
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
async def _generate_cloud_video(
|
| 189 |
+
job_id: str,
|
| 190 |
+
image_bytes: bytes,
|
| 191 |
+
prompt: str,
|
| 192 |
+
negative_prompt: str,
|
| 193 |
+
model: str,
|
| 194 |
+
seed: int,
|
| 195 |
+
):
|
| 196 |
+
"""Background task to generate video via WaveSpeed cloud API."""
|
| 197 |
+
import httpx
|
| 198 |
+
import aiohttp
|
| 199 |
+
|
| 200 |
+
try:
|
| 201 |
+
# Upload image to temporary hosting (WaveSpeed needs URL)
|
| 202 |
+
image_url = await _wavespeed_provider._upload_temp_image(image_bytes)
|
| 203 |
+
|
| 204 |
+
# Resolve model to WaveSpeed model ID
|
| 205 |
+
from content_engine.services.cloud_providers.wavespeed_provider import VIDEO_MODEL_MAP
|
| 206 |
+
wavespeed_model = VIDEO_MODEL_MAP.get(model, VIDEO_MODEL_MAP.get("default", "alibaba/wan-2.6-i2v-720p"))
|
| 207 |
+
|
| 208 |
+
# Call WaveSpeed video API
|
| 209 |
+
api_key = _wavespeed_provider._api_key
|
| 210 |
+
endpoint = f"https://api.wavespeed.ai/api/v3/{wavespeed_model}"
|
| 211 |
+
|
| 212 |
+
payload = {
|
| 213 |
+
"image": image_url,
|
| 214 |
+
"prompt": prompt,
|
| 215 |
+
"enable_sync_mode": True,
|
| 216 |
+
}
|
| 217 |
+
if negative_prompt:
|
| 218 |
+
payload["negative_prompt"] = negative_prompt
|
| 219 |
+
|
| 220 |
+
async with httpx.AsyncClient(timeout=300) as client:
|
| 221 |
+
resp = await client.post(
|
| 222 |
+
endpoint,
|
| 223 |
+
json=payload,
|
| 224 |
+
headers={
|
| 225 |
+
"Authorization": f"Bearer {api_key}",
|
| 226 |
+
"Content-Type": "application/json",
|
| 227 |
+
},
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
if resp.status_code != 200:
|
| 231 |
+
error_text = resp.text[:500]
|
| 232 |
+
logger.error("WaveSpeed video API error: %s", error_text)
|
| 233 |
+
_video_jobs[job_id]["status"] = "failed"
|
| 234 |
+
_video_jobs[job_id]["error"] = f"API error: {error_text[:200]}"
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
result = resp.json()
|
| 238 |
+
data = result.get("data", result)
|
| 239 |
+
|
| 240 |
+
# Check for failed status
|
| 241 |
+
if data.get("status") == "failed":
|
| 242 |
+
error_msg = data.get("error", "Unknown error")
|
| 243 |
+
_video_jobs[job_id]["status"] = "failed"
|
| 244 |
+
_video_jobs[job_id]["error"] = error_msg
|
| 245 |
+
return
|
| 246 |
+
|
| 247 |
+
# Extract video URL
|
| 248 |
+
video_url = None
|
| 249 |
+
outputs = data.get("outputs", [])
|
| 250 |
+
if outputs:
|
| 251 |
+
video_url = outputs[0]
|
| 252 |
+
elif "output" in data:
|
| 253 |
+
out = data["output"]
|
| 254 |
+
if isinstance(out, list) and out:
|
| 255 |
+
video_url = out[0]
|
| 256 |
+
elif isinstance(out, str):
|
| 257 |
+
video_url = out
|
| 258 |
+
|
| 259 |
+
if not video_url:
|
| 260 |
+
_video_jobs[job_id]["status"] = "failed"
|
| 261 |
+
_video_jobs[job_id]["error"] = "No video URL in response"
|
| 262 |
+
return
|
| 263 |
+
|
| 264 |
+
# Download the video
|
| 265 |
+
logger.info("Downloading cloud video: %s", video_url[:80])
|
| 266 |
+
video_resp = await client.get(video_url)
|
| 267 |
+
|
| 268 |
+
if video_resp.status_code != 200:
|
| 269 |
+
_video_jobs[job_id]["status"] = "failed"
|
| 270 |
+
_video_jobs[job_id]["error"] = "Failed to download video"
|
| 271 |
+
return
|
| 272 |
+
|
| 273 |
+
# Save to local output directory
|
| 274 |
+
from content_engine.config import settings
|
| 275 |
+
output_dir = settings.paths.output_dir / "videos"
|
| 276 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 277 |
+
|
| 278 |
+
# Determine extension from URL or default to mp4
|
| 279 |
+
ext = ".mp4"
|
| 280 |
+
if video_url.endswith(".webm"):
|
| 281 |
+
ext = ".webm"
|
| 282 |
+
elif video_url.endswith(".webp"):
|
| 283 |
+
ext = ".webp"
|
| 284 |
+
|
| 285 |
+
local_path = output_dir / f"video_{job_id}{ext}"
|
| 286 |
+
local_path.write_bytes(video_resp.content)
|
| 287 |
+
|
| 288 |
+
_video_jobs[job_id]["status"] = "completed"
|
| 289 |
+
_video_jobs[job_id]["output_path"] = str(local_path)
|
| 290 |
+
_video_jobs[job_id]["completed_at"] = time.time()
|
| 291 |
+
_video_jobs[job_id]["filename"] = local_path.name
|
| 292 |
+
|
| 293 |
+
logger.info("Cloud video saved: %s", local_path)
|
| 294 |
+
|
| 295 |
+
except Exception as e:
|
| 296 |
+
logger.error("Cloud video generation failed: %s", e)
|
| 297 |
+
_video_jobs[job_id]["status"] = "failed"
|
| 298 |
+
_video_jobs[job_id]["error"] = str(e)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
async def _poll_video_job(job_id: str, prompt_id: str):
|
| 302 |
"""Poll ComfyUI for video job completion."""
|
| 303 |
import httpx
|
src/content_engine/api/ui.html
CHANGED
|
@@ -838,11 +838,28 @@ select { cursor: pointer; }
|
|
| 838 |
<div id="cloud-model-select" style="display:none">
|
| 839 |
<label>Cloud Model (Text-to-Image)</label>
|
| 840 |
<select id="gen-cloud-model">
|
| 841 |
-
<
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
</select>
|
| 847 |
</div>
|
| 848 |
|
|
@@ -891,7 +908,39 @@ select { cursor: pointer; }
|
|
| 891 |
<img id="video-preview-img" style="max-width:100%; border-radius:6px">
|
| 892 |
</div>
|
| 893 |
<div class="section-title" style="margin-top:12px">Video Settings</div>
|
| 894 |
-
<div style="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
<div>
|
| 896 |
<label>Duration</label>
|
| 897 |
<select id="video-duration">
|
|
@@ -908,8 +957,8 @@ select { cursor: pointer; }
|
|
| 908 |
</select>
|
| 909 |
</div>
|
| 910 |
</div>
|
| 911 |
-
<div style="font-size:11px;color:var(--text-secondary);margin-top:8px">
|
| 912 |
-
|
| 913 |
</div>
|
| 914 |
</div>
|
| 915 |
|
|
@@ -1244,7 +1293,11 @@ select { cursor: pointer; }
|
|
| 1244 |
<span style="color:var(--text-secondary)">Checking...</span>
|
| 1245 |
</div>
|
| 1246 |
</div>
|
| 1247 |
-
<div id="pod-controls" style="display:flex; gap:8px; align-items:center">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
<select id="pod-gpu-select" style="padding:8px 12px; border-radius:6px; background:var(--bg-primary); border:1px solid var(--border); color:var(--text-primary)">
|
| 1249 |
<option value="NVIDIA GeForce RTX 4090">RTX 4090 - $0.44/hr (24GB)</option>
|
| 1250 |
<option value="NVIDIA RTX A6000">RTX A6000 - $0.76/hr (48GB)</option>
|
|
@@ -1388,6 +1441,7 @@ const API = ''; // same origin
|
|
| 1388 |
let currentPage = 'generate';
|
| 1389 |
let selectedRating = 'sfw';
|
| 1390 |
let selectedBackend = 'pod';
|
|
|
|
| 1391 |
let selectedMode = 'txt2img';
|
| 1392 |
let templatesData = [];
|
| 1393 |
let charactersData = [];
|
|
@@ -1633,11 +1687,10 @@ function selectMode(chip, mode) {
|
|
| 1633 |
selectedMode = mode;
|
| 1634 |
document.getElementById('img2img-section').style.display = mode === 'img2img' ? '' : 'none';
|
| 1635 |
document.getElementById('img2video-section').style.display = mode === 'img2video' ? '' : 'none';
|
| 1636 |
-
//
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
selectedBackend = 'pod';
|
| 1641 |
}
|
| 1642 |
// Update generate button text
|
| 1643 |
const btn = document.getElementById('generate-btn');
|
|
@@ -1753,6 +1806,24 @@ function selectBackend(chip, backend) {
|
|
| 1753 |
updateCloudModelVisibility();
|
| 1754 |
}
|
| 1755 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1756 |
function updateCloudModelVisibility() {
|
| 1757 |
const isCloud = selectedBackend === 'cloud';
|
| 1758 |
const isPod = selectedBackend === 'pod';
|
|
@@ -1844,12 +1915,20 @@ async function doGenerate() {
|
|
| 1844 |
formData.append('num_frames', document.getElementById('video-duration').value || '81');
|
| 1845 |
formData.append('fps', document.getElementById('video-fps').value || '24');
|
| 1846 |
formData.append('seed', document.getElementById('gen-seed').value || '-1');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1847 |
|
| 1848 |
-
const
|
|
|
|
| 1849 |
const data = await res.json();
|
| 1850 |
if (!res.ok) throw new Error(data.detail || 'Video generation failed');
|
| 1851 |
|
| 1852 |
-
|
|
|
|
| 1853 |
await pollForVideo(data.job_id);
|
| 1854 |
return;
|
| 1855 |
}
|
|
@@ -2734,6 +2813,7 @@ function updatePodUI(pod) {
|
|
| 2734 |
|
| 2735 |
async function startPod() {
|
| 2736 |
const gpuType = document.getElementById('pod-gpu-select').value;
|
|
|
|
| 2737 |
const btn = document.getElementById('pod-start-btn');
|
| 2738 |
btn.disabled = true;
|
| 2739 |
btn.textContent = 'Starting...';
|
|
@@ -2742,7 +2822,7 @@ async function startPod() {
|
|
| 2742 |
const res = await fetch(API + '/api/pod/start', {
|
| 2743 |
method: 'POST',
|
| 2744 |
headers: {'Content-Type': 'application/json'},
|
| 2745 |
-
body: JSON.stringify({gpu_type: gpuType})
|
| 2746 |
});
|
| 2747 |
const data = await res.json();
|
| 2748 |
|
|
@@ -2750,7 +2830,8 @@ async function startPod() {
|
|
| 2750 |
throw new Error(data.detail || 'Failed to start pod');
|
| 2751 |
}
|
| 2752 |
|
| 2753 |
-
|
|
|
|
| 2754 |
loadPodStatus();
|
| 2755 |
} catch(e) {
|
| 2756 |
toast('Failed to start pod: ' + e.message, 'error');
|
|
|
|
| 838 |
<div id="cloud-model-select" style="display:none">
|
| 839 |
<label>Cloud Model (Text-to-Image)</label>
|
| 840 |
<select id="gen-cloud-model">
|
| 841 |
+
<optgroup label="SeeDream (ByteDance)">
|
| 842 |
+
<option value="seedream-4.5">SeeDream v4.5 (Best Quality)</option>
|
| 843 |
+
<option value="seedream-4">SeeDream v4</option>
|
| 844 |
+
<option value="seedream-3.1">SeeDream v3.1</option>
|
| 845 |
+
</optgroup>
|
| 846 |
+
<optgroup label="NanoBanana (Google)">
|
| 847 |
+
<option value="nano-banana-pro">NanoBanana Pro</option>
|
| 848 |
+
<option value="nano-banana">NanoBanana</option>
|
| 849 |
+
</optgroup>
|
| 850 |
+
<optgroup label="FLUX (Black Forest Labs)">
|
| 851 |
+
<option value="flux-pro">FLUX Pro (Highest Quality)</option>
|
| 852 |
+
<option value="flux-dev">FLUX Dev</option>
|
| 853 |
+
<option value="flux-schnell">FLUX Schnell (Fast)</option>
|
| 854 |
+
</optgroup>
|
| 855 |
+
<optgroup label="WAN (Alibaba)">
|
| 856 |
+
<option value="wan-2.6">WAN 2.6 (Latest)</option>
|
| 857 |
+
<option value="wan-2.2">WAN 2.2</option>
|
| 858 |
+
</optgroup>
|
| 859 |
+
<optgroup label="Other">
|
| 860 |
+
<option value="dreamina-3.1">Dreamina v3.1</option>
|
| 861 |
+
<option value="qwen-image">Qwen Image</option>
|
| 862 |
+
</optgroup>
|
| 863 |
</select>
|
| 864 |
</div>
|
| 865 |
|
|
|
|
| 908 |
<img id="video-preview-img" style="max-width:100%; border-radius:6px">
|
| 909 |
</div>
|
| 910 |
<div class="section-title" style="margin-top:12px">Video Settings</div>
|
| 911 |
+
<div style="margin-bottom:8px">
|
| 912 |
+
<label>Backend</label>
|
| 913 |
+
<div id="video-backend-chips" class="chips" style="margin-top:4px">
|
| 914 |
+
<div class="chip selected" onclick="selectVideoBackend(this, 'cloud')">Cloud API (WaveSpeed)</div>
|
| 915 |
+
<div class="chip" onclick="selectVideoBackend(this, 'pod')">RunPod (WAN 2.2)</div>
|
| 916 |
+
</div>
|
| 917 |
+
</div>
|
| 918 |
+
<div id="video-cloud-model-select">
|
| 919 |
+
<label>Video Model</label>
|
| 920 |
+
<select id="video-cloud-model">
|
| 921 |
+
<optgroup label="WAN I2V (Alibaba)">
|
| 922 |
+
<option value="wan-2.6-i2v" selected>WAN 2.6 I2V 720p (Best)</option>
|
| 923 |
+
<option value="wan-2.2-i2v">WAN 2.2 I2V 480p</option>
|
| 924 |
+
</optgroup>
|
| 925 |
+
<optgroup label="Kling (Kuaishou)">
|
| 926 |
+
<option value="kling-o3-pro">Kling O3 Pro (Highest Quality)</option>
|
| 927 |
+
<option value="kling-o3">Kling O3</option>
|
| 928 |
+
<option value="kling-o1">Kling O1</option>
|
| 929 |
+
</optgroup>
|
| 930 |
+
<optgroup label="Seedance (ByteDance)">
|
| 931 |
+
<option value="seedance-1.5-pro">Seedance 1.5 Pro</option>
|
| 932 |
+
<option value="seedance-1.5">Seedance 1.5</option>
|
| 933 |
+
</optgroup>
|
| 934 |
+
<optgroup label="Veo (Google)">
|
| 935 |
+
<option value="veo-3.1">Veo 3.1 (Latest)</option>
|
| 936 |
+
<option value="veo-3">Veo 3</option>
|
| 937 |
+
</optgroup>
|
| 938 |
+
<optgroup label="Sora (OpenAI)">
|
| 939 |
+
<option value="sora-2">Sora 2</option>
|
| 940 |
+
</optgroup>
|
| 941 |
+
</select>
|
| 942 |
+
</div>
|
| 943 |
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px">
|
| 944 |
<div>
|
| 945 |
<label>Duration</label>
|
| 946 |
<select id="video-duration">
|
|
|
|
| 957 |
</select>
|
| 958 |
</div>
|
| 959 |
</div>
|
| 960 |
+
<div id="video-note" style="font-size:11px;color:var(--text-secondary);margin-top:8px">
|
| 961 |
+
Cloud API: Fast generation via WaveSpeed. RunPod: Uses WAN 2.2 I2V (~2 sec per frame).
|
| 962 |
</div>
|
| 963 |
</div>
|
| 964 |
|
|
|
|
| 1293 |
<span style="color:var(--text-secondary)">Checking...</span>
|
| 1294 |
</div>
|
| 1295 |
</div>
|
| 1296 |
+
<div id="pod-controls" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap">
|
| 1297 |
+
<select id="pod-model-type" style="padding:8px 12px; border-radius:6px; background:var(--bg-primary); border:1px solid var(--border); color:var(--text-primary)">
|
| 1298 |
+
<option value="flux">FLUX.2 (Realistic)</option>
|
| 1299 |
+
<option value="wan">WAN 2.2 (General/Anime)</option>
|
| 1300 |
+
</select>
|
| 1301 |
<select id="pod-gpu-select" style="padding:8px 12px; border-radius:6px; background:var(--bg-primary); border:1px solid var(--border); color:var(--text-primary)">
|
| 1302 |
<option value="NVIDIA GeForce RTX 4090">RTX 4090 - $0.44/hr (24GB)</option>
|
| 1303 |
<option value="NVIDIA RTX A6000">RTX A6000 - $0.76/hr (48GB)</option>
|
|
|
|
| 1441 |
let currentPage = 'generate';
|
| 1442 |
let selectedRating = 'sfw';
|
| 1443 |
let selectedBackend = 'pod';
|
| 1444 |
+
let selectedVideoBackend = 'cloud';
|
| 1445 |
let selectedMode = 'txt2img';
|
| 1446 |
let templatesData = [];
|
| 1447 |
let charactersData = [];
|
|
|
|
| 1687 |
selectedMode = mode;
|
| 1688 |
document.getElementById('img2img-section').style.display = mode === 'img2img' ? '' : 'none';
|
| 1689 |
document.getElementById('img2video-section').style.display = mode === 'img2video' ? '' : 'none';
|
| 1690 |
+
// Hide regular backend chips for video mode (video has its own backend selector)
|
| 1691 |
+
const backendSection = document.getElementById('backend-chips').parentElement;
|
| 1692 |
+
if (backendSection) {
|
| 1693 |
+
backendSection.style.display = mode === 'img2video' ? 'none' : '';
|
|
|
|
| 1694 |
}
|
| 1695 |
// Update generate button text
|
| 1696 |
const btn = document.getElementById('generate-btn');
|
|
|
|
| 1806 |
updateCloudModelVisibility();
|
| 1807 |
}
|
| 1808 |
|
| 1809 |
+
function selectVideoBackend(chip, backend) {
|
| 1810 |
+
chip.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('selected'));
|
| 1811 |
+
chip.classList.add('selected');
|
| 1812 |
+
selectedVideoBackend = backend;
|
| 1813 |
+
// Show/hide video model dropdown based on backend
|
| 1814 |
+
const videoModelSelect = document.getElementById('video-cloud-model-select');
|
| 1815 |
+
if (videoModelSelect) {
|
| 1816 |
+
videoModelSelect.style.display = backend === 'cloud' ? '' : 'none';
|
| 1817 |
+
}
|
| 1818 |
+
// Update note
|
| 1819 |
+
const videoNote = document.getElementById('video-note');
|
| 1820 |
+
if (videoNote) {
|
| 1821 |
+
videoNote.textContent = backend === 'cloud'
|
| 1822 |
+
? 'Cloud API: Fast generation via WaveSpeed. Pay per video.'
|
| 1823 |
+
: 'RunPod: Uses WAN 2.2 I2V on your pod (~2 sec per frame).';
|
| 1824 |
+
}
|
| 1825 |
+
}
|
| 1826 |
+
|
| 1827 |
function updateCloudModelVisibility() {
|
| 1828 |
const isCloud = selectedBackend === 'cloud';
|
| 1829 |
const isPod = selectedBackend === 'pod';
|
|
|
|
| 1915 |
formData.append('num_frames', document.getElementById('video-duration').value || '81');
|
| 1916 |
formData.append('fps', document.getElementById('video-fps').value || '24');
|
| 1917 |
formData.append('seed', document.getElementById('gen-seed').value || '-1');
|
| 1918 |
+
formData.append('backend', selectedVideoBackend);
|
| 1919 |
+
|
| 1920 |
+
// Add video model for cloud backend
|
| 1921 |
+
if (selectedVideoBackend === 'cloud') {
|
| 1922 |
+
formData.append('model', document.getElementById('video-cloud-model').value);
|
| 1923 |
+
}
|
| 1924 |
|
| 1925 |
+
const endpoint = selectedVideoBackend === 'cloud' ? '/api/video/generate/cloud' : '/api/video/generate';
|
| 1926 |
+
const res = await fetch(API + endpoint, { method: 'POST', body: formData });
|
| 1927 |
const data = await res.json();
|
| 1928 |
if (!res.ok) throw new Error(data.detail || 'Video generation failed');
|
| 1929 |
|
| 1930 |
+
const backendLabel = selectedVideoBackend === 'cloud' ? 'Cloud API' : 'RunPod';
|
| 1931 |
+
toast(`Video generating via ${backendLabel}...`, 'info');
|
| 1932 |
await pollForVideo(data.job_id);
|
| 1933 |
return;
|
| 1934 |
}
|
|
|
|
| 2813 |
|
| 2814 |
async function startPod() {
|
| 2815 |
const gpuType = document.getElementById('pod-gpu-select').value;
|
| 2816 |
+
const modelType = document.getElementById('pod-model-type').value;
|
| 2817 |
const btn = document.getElementById('pod-start-btn');
|
| 2818 |
btn.disabled = true;
|
| 2819 |
btn.textContent = 'Starting...';
|
|
|
|
| 2822 |
const res = await fetch(API + '/api/pod/start', {
|
| 2823 |
method: 'POST',
|
| 2824 |
headers: {'Content-Type': 'application/json'},
|
| 2825 |
+
body: JSON.stringify({gpu_type: gpuType, model_type: modelType})
|
| 2826 |
});
|
| 2827 |
const data = await res.json();
|
| 2828 |
|
|
|
|
| 2830 |
throw new Error(data.detail || 'Failed to start pod');
|
| 2831 |
}
|
| 2832 |
|
| 2833 |
+
const modelName = modelType === 'wan' ? 'WAN 2.2' : 'FLUX.2';
|
| 2834 |
+
toast(`Starting ${modelName} pod... This takes 3-5 minutes`, 'info');
|
| 2835 |
loadPodStatus();
|
| 2836 |
} catch(e) {
|
| 2837 |
toast('Failed to start pod: ' + e.message, 'error');
|
src/content_engine/main.py
CHANGED
|
@@ -138,6 +138,10 @@ async def lifespan(app: FastAPI):
|
|
| 138 |
routes_catalog.init_routes(catalog)
|
| 139 |
routes_system.init_routes(comfyui_client, catalog, template_engine, character_profiles)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# Initialize LoRA trainer (local)
|
| 142 |
from content_engine.services.lora_trainer import LoRATrainer
|
| 143 |
lora_trainer = LoRATrainer()
|
|
|
|
| 138 |
routes_catalog.init_routes(catalog)
|
| 139 |
routes_system.init_routes(comfyui_client, catalog, template_engine, character_profiles)
|
| 140 |
|
| 141 |
+
# Initialize video routes with WaveSpeed provider for cloud video generation
|
| 142 |
+
if wavespeed_provider:
|
| 143 |
+
routes_video.init_wavespeed(wavespeed_provider)
|
| 144 |
+
|
| 145 |
# Initialize LoRA trainer (local)
|
| 146 |
from content_engine.services.lora_trainer import LoRATrainer
|
| 147 |
lora_trainer = LoRATrainer()
|
src/content_engine/services/cloud_providers/wavespeed_provider.py
CHANGED
|
@@ -35,16 +35,49 @@ logger = logging.getLogger(__name__)
|
|
| 35 |
|
| 36 |
# Map friendly names to WaveSpeed model IDs (text-to-image)
|
| 37 |
MODEL_MAP = {
|
| 38 |
-
# NanoBanana
|
| 39 |
-
"nano-banana": "google
|
| 40 |
-
"nano-banana-pro": "google
|
| 41 |
-
# SeeDream
|
| 42 |
-
"seedream-3": "bytedance
|
| 43 |
-
"seedream-3.1": "bytedance
|
| 44 |
-
"seedream-4": "bytedance
|
| 45 |
-
"seedream-4.5": "bytedance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# Default
|
| 47 |
-
"default": "bytedance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
# Map friendly names to WaveSpeed edit model API paths
|
|
|
|
| 35 |
|
| 36 |
# Map friendly names to WaveSpeed model IDs (text-to-image)
|
| 37 |
MODEL_MAP = {
|
| 38 |
+
# NanoBanana (Google)
|
| 39 |
+
"nano-banana": "google/nano-banana",
|
| 40 |
+
"nano-banana-pro": "google/nano-banana-pro",
|
| 41 |
+
# SeeDream (ByteDance)
|
| 42 |
+
"seedream-3": "bytedance/seedream-v3",
|
| 43 |
+
"seedream-3.1": "bytedance/seedream-v3.1",
|
| 44 |
+
"seedream-4": "bytedance/seedream-v4",
|
| 45 |
+
"seedream-4.5": "bytedance/seedream-v4.5",
|
| 46 |
+
# WAN (Alibaba)
|
| 47 |
+
"wan-2.2": "alibaba/wan-2.2",
|
| 48 |
+
"wan-2.6": "alibaba/wan-2.6",
|
| 49 |
+
# FLUX (Black Forest Labs)
|
| 50 |
+
"flux-dev": "black-forest-labs/flux-dev",
|
| 51 |
+
"flux-schnell": "black-forest-labs/flux-schnell",
|
| 52 |
+
"flux-pro": "black-forest-labs/flux-pro",
|
| 53 |
+
# Dreamina (ByteDance)
|
| 54 |
+
"dreamina-3": "bytedance/dreamina-v3",
|
| 55 |
+
"dreamina-3.1": "bytedance/dreamina-v3.1",
|
| 56 |
+
# Qwen (WaveSpeed optimized)
|
| 57 |
+
"qwen-image": "wavespeedai/qwen-image",
|
| 58 |
# Default
|
| 59 |
+
"default": "bytedance/seedream-v4.5",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Image-to-Video models
|
| 63 |
+
VIDEO_MODEL_MAP = {
|
| 64 |
+
# Kling (Kuaishou)
|
| 65 |
+
"kling-o1": "kuaishou/kling-o1",
|
| 66 |
+
"kling-o3": "kuaishou/kling-o3",
|
| 67 |
+
"kling-o3-pro": "kuaishou/kling-o3-pro",
|
| 68 |
+
# Veo (Google)
|
| 69 |
+
"veo-3": "google/veo-3",
|
| 70 |
+
"veo-3.1": "google/veo-3.1",
|
| 71 |
+
# WAN I2V (Alibaba)
|
| 72 |
+
"wan-2.2-i2v": "alibaba/wan-2.2-i2v-480p",
|
| 73 |
+
"wan-2.6-i2v": "alibaba/wan-2.6-i2v-720p",
|
| 74 |
+
# Seedance (ByteDance)
|
| 75 |
+
"seedance-1.5": "bytedance/seedance-1.5",
|
| 76 |
+
"seedance-1.5-pro": "bytedance/seedance-1.5-pro",
|
| 77 |
+
# Sora (OpenAI)
|
| 78 |
+
"sora-2": "openai/sora-2",
|
| 79 |
+
# Default
|
| 80 |
+
"default": "alibaba/wan-2.6-i2v-720p",
|
| 81 |
}
|
| 82 |
|
| 83 |
# Map friendly names to WaveSpeed edit model API paths
|