File size: 13,334 Bytes
8f86b84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  SUPREMEAI — API REST FastAPI complète
  Endpoints: gΓ©nΓ©ration, statut, historique, fine-tuning, streaming
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""

from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from typing import Optional, List
import asyncio, uuid, os, time, json, logging
from pathlib import Path
from datetime import datetime

logger = logging.getLogger(__name__)
OUTPUT_DIR = Path(os.getenv("SUPREMEAI_OUTPUT", "/tmp/supremeai_output"))
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# ── Γ‰tat global des jobs ─────────────────────────────────────────────────────
JOBS: dict = {}   # job_id β†’ {status, progress, result, created_at}

app = FastAPI(
    title="SupremeAI Video Engine API",
    description="API REST pour la gΓ©nΓ©ration vidΓ©o IA la plus avancΓ©e",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── SchΓ©mas Pydantic ─────────────────────────────────────────────────────────

class GenerateRequest(BaseModel):
    prompt:           str   = Field(..., min_length=3, max_length=1000, description="Description de la vidΓ©o")
    negative_prompt:  str   = Field("", description="Ce Γ  Γ©viter dans la vidΓ©o")
    style:            str   = Field("cinematic", description="Style vidΓ©o")
    mode:             str   = Field("balanced", description="Mode: quality | balanced | speed")
    width:            int   = Field(1280, ge=256, le=3840)
    height:           int   = Field(720,  ge=144, le=2160)
    fps:              int   = Field(24,   ge=12,  le=120)
    duration:         float = Field(5.0,  ge=1.0, le=60.0)
    num_steps:        int   = Field(20,   ge=1,   le=100)
    guidance_scale:   float = Field(7.5,  ge=1.0, le=20.0)
    seed:             int   = Field(-1)
    upscale_4k:       bool  = Field(False)
    interpolate_fps:  int   = Field(0)
    color_grading:    str   = Field("none")
    add_voiceover:    bool  = Field(False)
    voiceover_text:   str   = Field("")
    voice_lang:       str   = Field("fr")
    storyboard:       List[str] = Field([], description="Scènes pour le mode Director")

class GenerateResponse(BaseModel):
    job_id:    str
    status:    str
    message:   str
    eta_seconds: Optional[float] = None

class JobStatus(BaseModel):
    job_id:    str
    status:    str   # pending | processing | done | error
    progress:  int   # 0-100
    message:   str
    video_url: Optional[str] = None
    error:     Optional[str] = None
    model:     Optional[str] = None
    duration:  Optional[float] = None
    created_at: str

class GPUInfoResponse(BaseModel):
    has_cuda:   bool
    gpu_name:   str
    vram_gb:    float
    recommended_model: str
    recommended_mode:  str

class CacheStatsResponse(BaseModel):
    entries:    int
    size_mb:    float
    total_hits: int

# ── Initialisation pipeline ──────────────────────────────────────────────────

_pipeline = None
_cache     = None

def get_pipeline():
    global _pipeline
    if _pipeline is None:
        from pipeline.generator import VideoGenerationPipeline
        _pipeline = VideoGenerationPipeline()
    return _pipeline

def get_cache():
    global _cache
    if _cache is None:
        from optimizers.speed import GenerationCache
        _cache = GenerationCache()
    return _cache

# ── Background task de gΓ©nΓ©ration ────────────────────────────────────────────

async def run_generation(job_id: str, req: GenerateRequest):
    """Exécute la génération en arrière-plan."""
    import asyncio

    JOBS[job_id]["status"]   = "processing"
    JOBS[job_id]["progress"] = 5
    JOBS[job_id]["message"]  = "Initialisation..."

    def progress_cb(pct: int, msg: str):
        JOBS[job_id]["progress"] = pct
        JOBS[job_id]["message"]  = msg

    try:
        from core.architecture import VideoGenerationConfig, VideoStyle, GenerationMode

        style_map = {s.value: s for s in VideoStyle}
        mode_map  = {m.value: m for m in GenerationMode}

        config = VideoGenerationConfig(
            prompt=req.prompt,
            negative_prompt=req.negative_prompt,
            style=style_map.get(req.style, VideoStyle.CINEMATIC),
            mode=mode_map.get(req.mode, GenerationMode.BALANCED),
            width=req.width, height=req.height,
            fps=req.fps, duration=req.duration,
            num_inference_steps=req.num_steps,
            guidance_scale=req.guidance_scale,
            seed=req.seed,
            upscale_to_4k=req.upscale_4k,
            interpolate_fps=req.interpolate_fps,
            color_grading=req.color_grading,
            add_voiceover=req.add_voiceover,
            voiceover_text=req.voiceover_text,
            voice_language=req.voice_lang,
            storyboard=req.storyboard,
        )

        # VΓ©rif cache
        cache = get_cache()
        cached = cache.get(config)
        if cached:
            JOBS[job_id].update({
                "status":    "done",
                "progress":  100,
                "message":   "βœ… VidΓ©o servie depuis le cache",
                "video_url": f"/video/{os.path.basename(cached)}",
                "model":     "cache",
            })
            return

        pipe = get_pipeline()

        # Mode Director si storyboard fourni
        if req.storyboard and len(req.storyboard) > 1:
            result = await asyncio.get_event_loop().run_in_executor(
                None, lambda: pipe.generate_director_mode(
                    topic=req.prompt, n_scenes=len(req.storyboard),
                    config=config, progress_cb=progress_cb
                )
            )
        else:
            result = await asyncio.get_event_loop().run_in_executor(
                None, lambda: pipe.generate(config, progress_cb=progress_cb)
            )

        if result.success:
            cache.put(config, result.video_path)
            JOBS[job_id].update({
                "status":    "done",
                "progress":  100,
                "message":   "βœ… VidΓ©o gΓ©nΓ©rΓ©e avec succΓ¨s",
                "video_url": f"/video/{os.path.basename(result.video_path)}",
                "model":     result.model_used,
                "duration":  result.generation_time,
            })
        else:
            JOBS[job_id].update({
                "status":  "error",
                "message": result.error or "Erreur inconnue",
                "error":   result.error,
            })

    except Exception as e:
        logger.exception(f"Erreur job {job_id}")
        JOBS[job_id].update({"status": "error", "error": str(e), "message": str(e)})

# ── Endpoints ────────────────────────────────────────────────────────────────

@app.get("/")
async def root():
    return {
        "name":    "SupremeAI Video Engine",
        "version": "1.0.0",
        "status":  "online",
        "docs":    "/docs",
    }

@app.get("/gpu", response_model=GPUInfoResponse)
async def gpu_info():
    """Retourne les informations GPU et le modèle recommandé."""
    from pipeline.generator import GPUProfiler
    info = GPUProfiler.detect()
    return GPUInfoResponse(**info)

@app.post("/generate", response_model=GenerateResponse)
async def generate_video(req: GenerateRequest, bg: BackgroundTasks):
    """Lance une gΓ©nΓ©ration vidΓ©o asynchrone. Retourne un job_id."""
    job_id = str(uuid.uuid4())[:8]
    JOBS[job_id] = {
        "status":     "pending",
        "progress":   0,
        "message":    "En attente...",
        "created_at": datetime.now().isoformat(),
    }
    bg.add_task(run_generation, job_id, req)

    # Estimation ETA
    eta = {"quality": 60, "balanced": 25, "speed": 8}.get(req.mode, 25)
    return GenerateResponse(
        job_id=job_id, status="pending",
        message="GΓ©nΓ©ration lancΓ©e", eta_seconds=eta
    )

@app.get("/status/{job_id}", response_model=JobStatus)
async def job_status(job_id: str):
    """Retourne le statut d'un job de gΓ©nΓ©ration."""
    if job_id not in JOBS:
        raise HTTPException(404, detail="Job non trouvΓ©")
    j = JOBS[job_id]
    return JobStatus(
        job_id=job_id, status=j["status"],
        progress=j.get("progress", 0), message=j.get("message", ""),
        video_url=j.get("video_url"), error=j.get("error"),
        model=j.get("model"), duration=j.get("duration"),
        created_at=j["created_at"],
    )

@app.get("/video/{filename}")
async def get_video(filename: str):
    """TΓ©lΓ©charge une vidΓ©o gΓ©nΓ©rΓ©e."""
    path = OUTPUT_DIR / filename
    if not path.exists():
        raise HTTPException(404, detail="VidΓ©o non trouvΓ©e")
    return FileResponse(str(path), media_type="video/mp4",
                        headers={"Content-Disposition": f"attachment; filename={filename}"})

@app.get("/history")
async def get_history(limit: int = 20):
    """Retourne l'historique des jobs rΓ©cents."""
    jobs = sorted(JOBS.items(), key=lambda x: x[1]["created_at"], reverse=True)
    return [{"job_id": k, **v} for k, v in jobs[:limit]]

@app.delete("/history")
async def clear_history():
    """Efface l'historique des jobs."""
    JOBS.clear()
    return {"message": "Historique effacΓ©"}

@app.get("/cache/stats", response_model=CacheStatsResponse)
async def cache_stats():
    """Statistiques du cache de gΓ©nΓ©ration."""
    cache = get_cache()
    stats = cache.stats()
    return CacheStatsResponse(**stats)

@app.delete("/cache")
async def clear_cache():
    """Vide le cache de gΓ©nΓ©ration."""
    cache = get_cache()
    cache.index = {}
    cache._save_index()
    return {"message": "Cache vidΓ©"}

@app.post("/generate/sync")
async def generate_sync(req: GenerateRequest):
    """
    GΓ©nΓ©ration synchrone (attend le rΓ©sultat).
    Pour les vidΓ©os courtes (< 10s) uniquement.
    """
    if req.duration > 10:
        raise HTTPException(400, detail="Mode synchrone limitΓ© Γ  10 secondes. Utilisez /generate pour les vidΓ©os plus longues.")

    job_id = str(uuid.uuid4())[:8]
    JOBS[job_id] = {"status": "processing", "progress": 0, "message": "", "created_at": datetime.now().isoformat()}
    await run_generation(job_id, req)
    j = JOBS[job_id]

    if j["status"] == "done":
        return {"success": True, "video_url": j["video_url"], "model": j.get("model"), "duration": j.get("duration")}
    else:
        raise HTTPException(500, detail=j.get("error", "Erreur gΓ©nΓ©ration"))

@app.get("/styles")
async def list_styles():
    """Retourne tous les styles vidΓ©o disponibles."""
    from core.architecture import VideoStyle, STYLE_ENHANCERS
    return [
        {"id": s.value, "name": s.value.replace("_", " ").title(),
         "enhancer": STYLE_ENHANCERS.get(s, "")[:80]}
        for s in VideoStyle
    ]

@app.get("/models")
async def list_models():
    """Retourne les modèles disponibles et leur statut."""
    from pipeline.generator import GPUProfiler
    gpu = GPUProfiler.detect()
    vram = gpu.get("vram_gb", 0)
    return [
        {"id": "wan2.1",       "name": "Wan 2.1 (14B)",           "min_vram": 24, "available": vram >= 24, "quality": "⭐⭐⭐⭐⭐", "speed": "Lente"},
        {"id": "cogvideox-5b", "name": "CogVideoX-5B",            "min_vram": 16, "available": vram >= 16, "quality": "⭐⭐⭐⭐",  "speed": "Moyenne"},
        {"id": "cogvideox-2b", "name": "CogVideoX-2B",            "min_vram": 8,  "available": vram >= 8,  "quality": "⭐⭐⭐",   "speed": "Rapide"},
        {"id": "animatediff",  "name": "AnimateDiff-Lightning",   "min_vram": 4,  "available": vram >= 4,  "quality": "⭐⭐⭐",   "speed": "Ultra rapide (4 steps)"},
        {"id": "enhanced_moviepy", "name": "Enhanced MoviePy CPU","min_vram": 0,  "available": True,       "quality": "⭐⭐",    "speed": "CPU uniquement"},
    ]

# ── Lancement ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000, reload=False, workers=1)