Shads229 commited on
Commit
6d18217
·
verified ·
1 Parent(s): 7254ed3

Upload 16 files

Browse files
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Configuration DeepSeek API
2
+ DEEPSEEK_API_URL=https://shads229-personnal-ai.hf.space/v1/chat/completions
3
+ DEEPSEEK_API_KEY=Shadobsh
4
+
5
+ # Optionnel : Port du serveur (défaut: 7860 pour Hugging Face)
6
+ PORT=7860
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,44 +1,44 @@
1
- # Utiliser une image Python légère
2
- FROM python:3.10-slim
3
-
4
- # Éviter les fichiers .pyc et activer le mode non-interactif
5
- ENV PYTHONDONTWRITEBYTECODE=1 \
6
- PYTHONUNBUFFERED=1 \
7
- DEBIAN_FRONTEND=noninteractive
8
-
9
- # Installer les dépendances système pour OpenCV, FFmpeg et l'audio
10
- RUN apt-get update && apt-get install -y \
11
- libgl1 \
12
- libglib2.0-0 \
13
- libsm6 \
14
- libxext6 \
15
- libxrender-dev \
16
- ffmpeg \
17
- gcc \
18
- python3-dev \
19
- && rm -rf /var/lib/apt/lists/*
20
-
21
- # Créer un utilisateur pour Hugging Face
22
- RUN useradd -m -u 1000 user
23
- USER user
24
- ENV HOME=/home/user \
25
- PATH=/home/user/.local/bin:$PATH
26
-
27
- WORKDIR $HOME/app
28
-
29
- # Copier et installer les dépendances Python
30
- COPY --chown=user requirements.txt .
31
- RUN pip install --no-cache-dir --upgrade pip && \
32
- pip install --no-cache-dir -r requirements.txt
33
-
34
- # Copier l'intégralité du code (backend, engine, app.py, .env)
35
- COPY --chown=user . .
36
-
37
- # Créer les dossiers de données nécessaires
38
- RUN mkdir -p video_analysis_pro/output video_analysis_pro/cache video_analysis_pro/reports
39
-
40
- # Exposer le port par défaut
41
- EXPOSE 7860
42
-
43
- # Démarrer l'application via le point d'entrée app.py
44
- CMD ["python", "app.py"]
 
1
+ # Utiliser une image Python légère
2
+ FROM python:3.10-slim
3
+
4
+ # Éviter les fichiers .pyc et activer le mode non-interactif
5
+ ENV PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1 \
7
+ DEBIAN_FRONTEND=noninteractive
8
+
9
+ # Installer les dépendances système pour OpenCV, FFmpeg et l'audio
10
+ RUN apt-get update && apt-get install -y \
11
+ libgl1 \
12
+ libglib2.0-0 \
13
+ libsm6 \
14
+ libxext6 \
15
+ libxrender-dev \
16
+ ffmpeg \
17
+ gcc \
18
+ python3-dev \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Créer un utilisateur pour Hugging Face
22
+ RUN useradd -m -u 1000 user
23
+ USER user
24
+ ENV HOME=/home/user \
25
+ PATH=/home/user/.local/bin:$PATH
26
+
27
+ WORKDIR $HOME/app
28
+
29
+ # Copier et installer les dépendances Python
30
+ COPY --chown=user requirements.txt .
31
+ RUN pip install --no-cache-dir --upgrade pip && \
32
+ pip install --no-cache-dir -r requirements.txt
33
+
34
+ # Copier l'intégralité du code (backend, engine, app.py, .env)
35
+ COPY --chown=user . .
36
+
37
+ # Créer les dossiers de données nécessaires
38
+ RUN mkdir -p video_analysis_pro/output video_analysis_pro/cache video_analysis_pro/reports
39
+
40
+ # Exposer le port par défaut
41
+ EXPOSE 7860
42
+
43
+ # Démarrer l'application via le point d'entrée app.py
44
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
- ---
2
- title: Zenith AI
3
- emoji: 📚
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Zenith AI
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,8 +1,8 @@
1
- from backend.main import app
2
- import uvicorn
3
- import os
4
-
5
- if __name__ == "__main__":
6
- # Hugging Face utilise le port 7860 par défaut
7
- port = int(os.environ.get("PORT", 7860))
8
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ from backend.main import app
2
+ import uvicorn
3
+ import os
4
+
5
+ if __name__ == "__main__":
6
+ # Hugging Face utilise le port 7860 par défaut
7
+ port = int(os.environ.get("PORT", 7860))
8
+ uvicorn.run(app, host="0.0.0.0", port=port)
backend/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (162 Bytes). View file
 
backend/__pycache__/main.cpython-314.pyc ADDED
Binary file (8.17 kB). View file
 
backend/core/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (167 Bytes). View file
 
backend/core/__pycache__/engine.cpython-314.pyc ADDED
Binary file (26.6 kB). View file
 
backend/core/engine.py CHANGED
@@ -1,392 +1,337 @@
1
- import os, json, logging, time, base64, gc, asyncio, concurrent.futures
2
- import cv2, numpy as np, torch
3
- from pathlib import Path
4
- from typing import List, Dict, Any, Optional, AsyncGenerator
5
- from collections import Counter
6
- from dataclasses import dataclass
7
- from dotenv import load_dotenv
8
-
9
- load_dotenv()
10
-
11
- # Configuration
12
- GEMINI_MODEL = "gemini-2.5-flash"
13
- BASE_DIR = Path("video_analysis_pro")
14
- OUTPUT_DIR, CACHE_DIR, REPORTS_DIR = BASE_DIR/"output", BASE_DIR/"cache", BASE_DIR/"reports"
15
- for d in [BASE_DIR, OUTPUT_DIR, CACHE_DIR, REPORTS_DIR]: d.mkdir(parents=True, exist_ok=True)
16
-
17
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
- logger = logging.getLogger("ZenithEngine")
19
-
20
- # Tools Availability
21
- try:
22
- from ultralytics import YOLO
23
- YOLO_AVAILABLE = True
24
- except ImportError:
25
- YOLO_AVAILABLE = False
26
-
27
- try:
28
- from faster_whisper import WhisperModel
29
- WHISPER_AVAILABLE = True
30
- except ImportError:
31
- WHISPER_AVAILABLE = False
32
-
33
- @dataclass
34
- class Frame:
35
- path: Path
36
- timestamp: float
37
- metrics: Dict[str, float] = None
38
- vision_content: str = ""
39
-
40
- class AuthManager:
41
- def __init__(self):
42
- self.refresh_url = "https://oauth2.googleapis.com/token"
43
- self.client_id = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
44
- self.client_secret = "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"
45
- self.access_token = None
46
- self.expiry_time = 0
47
-
48
- service_account_env = os.getenv("GCP_SERVICE_ACCOUNT")
49
- if service_account_env:
50
- try:
51
- cleaned_env = service_account_env.strip()
52
- # Supprimer les guillemets éventuels
53
- if (cleaned_env.startswith("'") and cleaned_env.endswith("'")) or \
54
- (cleaned_env.startswith('"') and cleaned_env.endswith('"')):
55
- cleaned_env = cleaned_env[1:-1]
56
-
57
- # Vérifier si c'est un chemin vers un fichier
58
- if os.path.isfile(cleaned_env):
59
- with open(cleaned_env, 'r') as f:
60
- self.creds = json.load(f)
61
- logger.info(f"✅ Credentials chargés depuis le fichier : {cleaned_env}")
62
- else:
63
- # Sinon, tenter de décoder comme du JSON brut
64
- self.creds = json.loads(cleaned_env)
65
- logger.info(" Credentials chargés depuis la variable d'environnement (JSON brut)")
66
- except Exception as e:
67
- logger.error(f"❌ Erreur critique lors du chargement de GCP_SERVICE_ACCOUNT : {e}")
68
- self.creds = {}
69
- else:
70
- self.creds = {}
71
-
72
- async def get_access_token(self) -> str:
73
- if self.access_token and time.time() < (self.expiry_time - 300):
74
- return self.access_token
75
-
76
- if not self.creds:
77
- logger.error("❌ GCP_SERVICE_ACCOUNT est vide ou mal configuré dans les secrets.")
78
- return ""
79
-
80
- refresh_token = self.creds.get("refresh_token")
81
- if not refresh_token:
82
- logger.error("❌ 'refresh_token' introuvable dans le JSON de GCP_SERVICE_ACCOUNT.")
83
- return ""
84
-
85
- payload = {
86
- "client_id": self.client_id, "client_secret": self.client_secret,
87
- "refresh_token": refresh_token, "grant_type": "refresh_token"
88
- }
89
- import httpx
90
- async with httpx.AsyncClient() as client:
91
- try:
92
- response = await client.post(self.refresh_url, data=payload)
93
- if response.status_code != 200:
94
- logger.error(f"❌ Échec du rafraîchissement du token (HTTP {response.status_code}): {response.text}")
95
- return ""
96
- data = response.json()
97
- self.access_token = data["access_token"]
98
- self.expiry_time = time.time() + data.get("expires_in", 3600)
99
- logger.info(" Nouveau jeton d'accès Gemini récupéré avec succès.")
100
- return self.access_token
101
- except Exception as e:
102
- logger.error(f"❌ Erreur réseau lors du rafraîchissement du token : {str(e)}")
103
- return ""
104
-
105
- class GeminiClient:
106
- def __init__(self, auth_manager: AuthManager):
107
- self.auth_manager = auth_manager
108
- self.base_url = "https://cloudcode-pa.googleapis.com/v1internal"
109
- self.project_id = os.getenv("GEMINI_PROJECT_ID")
110
-
111
- async def discover_project_id(self) -> str:
112
- if self.project_id: return self.project_id
113
- token = await self.auth_manager.get_access_token()
114
- if not token:
115
- logger.warning("⚠️ Pas de token disponible pour découvrir le project_id.")
116
- return "default-project"
117
- import httpx
118
- async with httpx.AsyncClient() as client:
119
- try:
120
- resp = await client.post(
121
- f"{self.base_url}:loadCodeAssist",
122
- headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
123
- json={"cloudaicompanionProject": "default-project", "metadata": {"duetProject": "default-project"}}
124
- )
125
- if resp.status_code == 200:
126
- data = resp.json()
127
- self.project_id = data.get("cloudaicompanionProject", "default-project")
128
- return self.project_id
129
- except Exception as e:
130
- logger.error(f"⚠️ Erreur lors de la découverte du projet ID: {e}")
131
- return "default-project"
132
-
133
- async def stream_content(self, model: str, messages: List[Dict[str, Any]], options: Dict[str, Any]) -> AsyncGenerator[Dict[str, Any], None]:
134
- token = await self.auth_manager.get_access_token()
135
- if not token:
136
- yield {"error": "Authentification échouée. Vérifiez votre secret GCP_SERVICE_ACCOUNT sur Hugging Face."}
137
- return
138
-
139
- project_id = await self.discover_project_id()
140
-
141
- # Format messages for API
142
- contents = []
143
- for msg in messages:
144
- role = "model" if msg["role"] == "assistant" else "user"
145
- parts = []
146
- content = msg.get("content", "")
147
- if isinstance(content, str): parts.append({"text": content})
148
- elif isinstance(content, list):
149
- for part in content:
150
- if part["type"] == "text": parts.append({"text": part["text"]})
151
- elif part["type"] == "image_url":
152
- url = part["image_url"]["url"]
153
- if url.startswith("data:"):
154
- mime, b64 = url.split(";base64,")
155
- parts.append({"inlineData": {"mimeType": mime.split(":")[1], "data": b64}})
156
- contents.append({"role": role, "parts": parts})
157
-
158
- payload = {
159
- "model": model, "project": project_id,
160
- "request": {
161
- "contents": contents,
162
- "generationConfig": {"temperature": options.get("temperature", 0.7)},
163
- "safetySettings": [{"category": c, "threshold": "BLOCK_NONE"} for c in [
164
- "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
165
- "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"
166
- ]]
167
- }
168
- }
169
-
170
- import httpx
171
- async with httpx.AsyncClient() as client:
172
- try:
173
- async with client.stream(
174
- "POST", f"{self.base_url}:streamGenerateContent?alt=sse",
175
- headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
176
- json=payload, timeout=None
177
- ) as response:
178
- async for line in response.aiter_lines():
179
- if line.startswith("data: "):
180
- yield json.loads(line[6:])
181
- except Exception as e:
182
- yield {"error": str(e)}
183
-
184
- class VideoProcessor:
185
- @staticmethod
186
- def get_frame_metrics(frame: np.ndarray) -> dict:
187
- try:
188
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
189
- hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
190
- return {"brightness": float(np.mean(gray)), "contrast": float(np.std(gray)),
191
- "saturation": float(np.mean(hsv[:, :, 1])), "sharpness": float(cv2.Laplacian(gray, cv2.CV_64F).var())}
192
- except: return {"brightness": 0, "contrast": 0, "saturation": 0, "sharpness": 0}
193
-
194
- def __init__(self, video_path: Path, output_dir: Path):
195
- self.video_path, self.output_dir = video_path, output_dir
196
- self.output_dir.mkdir(parents=True, exist_ok=True)
197
-
198
- def extract_keyframes(self, max_frames: int = 50) -> List[Frame]:
199
- try:
200
- from decord import VideoReader, cpu
201
- vr = VideoReader(str(self.video_path), ctx=cpu(0))
202
- total = len(vr)
203
- step = max(1, total // max_frames)
204
- indices = range(0, total, step)[:max_frames]
205
- frames_data = vr.get_batch(indices).asnumpy()
206
- fps = vr.get_avg_fps()
207
- extracted = []
208
- for i, idx in enumerate(indices):
209
- img = cv2.cvtColor(frames_data[i], cv2.COLOR_RGB2BGR)
210
- ts = idx / fps
211
- p = self.output_dir / f"f_{idx}.jpg"
212
- cv2.imwrite(str(p), img, [cv2.IMWRITE_JPEG_QUALITY, 85])
213
- extracted.append(Frame(path=p, timestamp=ts, metrics=self.get_frame_metrics(img)))
214
- return extracted
215
- except Exception as e:
216
- logger.warning(f"Decord failed, fallback to CV2: {e}")
217
- cap = cv2.VideoCapture(str(self.video_path))
218
- fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
219
- total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1000
220
- step = max(1, total // max_frames)
221
- extracted = []
222
- for idx in range(0, total, step):
223
- if len(extracted) >= max_frames: break
224
- cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
225
- ret, img = cap.read()
226
- if ret:
227
- ts = idx / fps
228
- p = self.output_dir / f"f_{idx}.jpg"
229
- cv2.imwrite(str(p), img, [cv2.IMWRITE_JPEG_QUALITY, 85])
230
- extracted.append(Frame(path=p, timestamp=ts, metrics=self.get_frame_metrics(img)))
231
- cap.release()
232
- return extracted
233
-
234
- class AudioProcessor:
235
- def __init__(self): self.model = None
236
- def initialize(self):
237
- if WHISPER_AVAILABLE and self.model is None:
238
- try:
239
- device = "cuda" if torch.cuda.is_available() else "cpu"
240
- self.model = WhisperModel("base", device=device, compute_type="int8")
241
- except: pass
242
- def transcribe(self, p: Path) -> str:
243
- self.initialize()
244
- if not self.model: return "Transcription indisponible"
245
- try:
246
- segments, info = self.model.transcribe(str(p), beam_size=5)
247
- transcript = " ".join([s.text for s in segments])
248
- return f"[Langue source détectée: {info.language.upper()}] {transcript}"
249
- except: return "Erreur transcription"
250
-
251
- class VideoDownloader:
252
- @staticmethod
253
- def download(url: str, output_dir: Path) -> Optional[Path]:
254
- import yt_dlp
255
- ydl_opts = {
256
- 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
257
- 'outtmpl': str(output_dir / 'downloaded_video.%(ext)s'),
258
- 'noplaylist': True, 'quiet': True, 'no_warnings': True, 'nocheckcertificate': True,
259
- 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
260
- 'referer': 'https://www.google.com/',
261
- 'http_headers': {'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9'}
262
- }
263
- try:
264
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
265
- info = ydl.extract_info(url, download=True)
266
- return Path(ydl.prepare_filename(info))
267
- except: return None
268
-
269
- class ZenithAnalyzer:
270
- def __init__(self):
271
- self.auth = AuthManager()
272
- self.gemini = GeminiClient(self.auth)
273
- self.audio_proc = AudioProcessor()
274
- self.yolo = YOLO("yolov8n.pt") if YOLO_AVAILABLE else None
275
-
276
- async def extract_frames_only(self, video_path: Path, session_id: str) -> List[str]:
277
- session_dir = OUTPUT_DIR / f"session_{session_id}"
278
- session_dir.mkdir(parents=True, exist_ok=True)
279
- proc = VideoProcessor(video_path, session_dir)
280
- frames = proc.extract_keyframes()
281
- return [f"/output/session_{session_id}/{f.path.name}" for f in frames[:12]]
282
-
283
- async def run_full_analysis(self, video_path: Path, session_id: str, custom_prompt: Optional[str] = None) -> AsyncGenerator[Dict[str, Any], None]:
284
- session_dir = OUTPUT_DIR / f"session_{session_id}"
285
- session_dir.mkdir(parents=True, exist_ok=True)
286
- cache_file = session_dir / "analysis_cache.json"
287
-
288
- # Optimisation : Ne pas ré-extraire si les frames existent déjà
289
- existing_frames = list(session_dir.glob("f_*.jpg"))
290
- if not existing_frames:
291
- yield {"status": "sampling", "message": "Analyse des séquences..."}
292
- proc = VideoProcessor(video_path, session_dir)
293
- frames = proc.extract_keyframes()
294
- else:
295
- def get_idx(p):
296
- try: return int(p.stem.split('_')[1])
297
- except: return 0
298
- existing_paths = sorted(existing_frames, key=get_idx)
299
- frames = []
300
- for p in existing_paths:
301
- img = cv2.imread(str(p))
302
- metrics = VideoProcessor.get_frame_metrics(img) if img is not None else {"brightness": 0, "contrast": 0, "saturation": 0, "sharpness": 0}
303
- frames.append(Frame(path=p, timestamp=0.0, metrics=metrics))
304
- yield {"status": "sampling", "message": "Récupération des séquences existantes..."}
305
-
306
- # Envoyer les chemins des images au frontend
307
- frame_urls = [f"/output/session_{session_id}/{f.path.name}" for f in frames[:12]]
308
- yield {"status": "frames_ready", "frames": frame_urls, "message": "Séquences prêtes."}
309
-
310
- # Vérifier si on a un cache pour l'audio et le visuel
311
- cached_data = {}
312
- if cache_file.exists():
313
- try:
314
- with open(cache_file, "r") as f:
315
- cached_data = json.load(f)
316
- logger.info(f"✅ Cache trouvé pour la session {session_id}")
317
- except: pass
318
-
319
- if "transcript" in cached_data and "vision_info" in cached_data:
320
- transcript = cached_data["transcript"]
321
- v_info = cached_data["vision_info"]
322
- yield {"status": "fusion", "message": "Utilisation des données en cache..."}
323
- else:
324
- yield {"status": "audio", "message": "Traitement audio & visuel..."}
325
- loop = asyncio.get_event_loop()
326
- with concurrent.futures.ThreadPoolExecutor() as executor:
327
- audio_task = loop.run_in_executor(executor, self.audio_proc.transcribe, video_path)
328
-
329
- if self.yolo:
330
- all_paths = [str(f.path) for f in frames]
331
- batch_size = 10
332
- for i in range(0, len(all_paths), batch_size):
333
- batch = all_paths[i:i+batch_size]
334
- results = await loop.run_in_executor(executor, lambda: self.yolo(batch, verbose=False, imgsz=320, stream=False))
335
- for j, res in enumerate(results):
336
- idx = i + j
337
- objs = [res.names[int(b.cls[0])] for b in res.boxes if b.conf > 0.25]
338
- ambiance = f"Ambiance: {'Sombre' if frames[idx].metrics['brightness'] < 50 else 'Lumineuse'}"
339
- frames[idx].vision_content = f"{ambiance}, Objets: " + ", ".join([f"{v}x {k}" for k,v in Counter(objs).items()])
340
-
341
- transcript = await audio_task
342
-
343
- v_info = "\n".join([f"[{f.timestamp:.1f}s] {f.vision_content}" for f in frames[:40]])
344
-
345
- # Sauvegarder dans le cache
346
- try:
347
- with open(cache_file, "w") as f:
348
- json.dump({"transcript": transcript, "vision_info": v_info}, f)
349
- except: pass
350
-
351
- yield {"status": "fusion", "message": "Intelligence Artificielle en action..."}
352
-
353
- # Utilisation du prompt personnalisé si fourni
354
- base_instruction = custom_prompt if custom_prompt else "Résumer et continuer l'analyse du média"
355
-
356
- prompt = f"""Tu es l'unité Zenith AI, un système d'analyse de données multimédias.
357
- INSTRUCTION UTILISATEUR : {base_instruction}
358
-
359
- DONNÉES D'ENTRÉE :
360
- - TRANSCRIPTION : {transcript}
361
- - DONNÉES VISUELLES : {v_info}
362
-
363
- Produis un rapport TECHNIQUE, FACTUEL et STRUCTURÉ en Markdown."""
364
-
365
- # Encodage parallèle des images
366
- selected_frames = [frames[i] for i in range(0, len(frames), max(1, len(frames)//10))][:10]
367
- def encode_f(f):
368
- img = cv2.imread(str(f.path))
369
- _, buf = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 70])
370
- return {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64.b64encode(buf).decode()}"}}
371
-
372
- with concurrent.futures.ThreadPoolExecutor() as executor:
373
- images = list(executor.map(encode_f, selected_frames))
374
-
375
- messages = [{"role": "user", "content": [{"type": "text", "text": prompt}] + images}]
376
-
377
- yield {"status": "generating", "message": "Génération du rapport par l'IA..."}
378
- async for chunk in self.gemini.stream_content(GEMINI_MODEL, messages, {"temperature": 0.7}):
379
- if "error" in chunk:
380
- yield {"error": chunk["error"]}
381
- break
382
- resp = chunk.get("response", {})
383
- candidates = resp.get("candidates", [])
384
- if candidates:
385
- for part in candidates[0].get("content", {}).get("parts", []):
386
- text = part.get("text", "")
387
- if text: yield {"status": "streaming", "text": text}
388
-
389
- # Cleanup
390
- gc.collect()
391
- if torch.cuda.is_available(): torch.cuda.empty_cache()
392
- yield {"status": "completed", "message": "Analyse terminée."}
 
1
+ import os, json, logging, time, base64, gc, asyncio, concurrent.futures
2
+ import cv2, numpy as np, torch
3
+ from pathlib import Path
4
+ from typing import List, Dict, Any, Optional, AsyncGenerator
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ # Configuration
12
+ DEEPSEEK_API_URL = "https://shads229-personnal-ai.hf.space/v1/chat/completions"
13
+ DEEPSEEK_API_KEY = "Shadobsh"
14
+ DEEPSEEK_MODEL = "deepseek-chat"
15
+ BASE_DIR = Path("video_analysis_pro")
16
+ OUTPUT_DIR, CACHE_DIR, REPORTS_DIR = BASE_DIR/"output", BASE_DIR/"cache", BASE_DIR/"reports"
17
+ for d in [BASE_DIR, OUTPUT_DIR, CACHE_DIR, REPORTS_DIR]: d.mkdir(parents=True, exist_ok=True)
18
+
19
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
20
+ logger = logging.getLogger("ZenithEngine")
21
+
22
+ # Tools Availability
23
+ try:
24
+ from ultralytics import YOLO
25
+ YOLO_AVAILABLE = True
26
+ except ImportError:
27
+ YOLO_AVAILABLE = False
28
+
29
+ try:
30
+ from faster_whisper import WhisperModel
31
+ WHISPER_AVAILABLE = True
32
+ except ImportError:
33
+ WHISPER_AVAILABLE = False
34
+
35
+ @dataclass
36
+ class Frame:
37
+ path: Path
38
+ timestamp: float
39
+ metrics: Dict[str, float] = None
40
+ vision_content: str = ""
41
+
42
+ class DeepSeekClient:
43
+ def __init__(self):
44
+ self.api_url = DEEPSEEK_API_URL
45
+ self.api_key = DEEPSEEK_API_KEY
46
+ logger.info(f"✅ DeepSeek Client initialisé avec l'URL : {self.api_url}")
47
+
48
+ async def stream_content(self, model: str, messages: List[Dict[str, Any]], options: Dict[str, Any]) -> AsyncGenerator[Dict[str, Any], None]:
49
+ # Convertir les messages au format OpenAI compatible
50
+ formatted_messages = []
51
+ for msg in messages:
52
+ role = msg["role"]
53
+ content = msg.get("content", "")
54
+
55
+ # Si le contenu contient des images, on les convertit en format texte + images
56
+ if isinstance(content, list):
57
+ text_parts = []
58
+ image_parts = []
59
+ for part in content:
60
+ if part["type"] == "text":
61
+ text_parts.append(part["text"])
62
+ elif part["type"] == "image_url":
63
+ url = part["image_url"]["url"]
64
+ if url.startswith("data:"):
65
+ image_parts.append({"type": "image_url", "image_url": {"url": url}})
66
+
67
+ # DeepSeek supporte le format OpenAI vision
68
+ if image_parts:
69
+ formatted_messages.append({
70
+ "role": role,
71
+ "content": [{"type": "text", "text": " ".join(text_parts)}] + image_parts
72
+ })
73
+ else:
74
+ formatted_messages.append({"role": role, "content": " ".join(text_parts)})
75
+ else:
76
+ formatted_messages.append({"role": role, "content": content})
77
+
78
+ payload = {
79
+ "model": model,
80
+ "messages": formatted_messages,
81
+ "temperature": options.get("temperature", 0.7),
82
+ "stream": True
83
+ }
84
+
85
+ import httpx
86
+ async with httpx.AsyncClient(timeout=None) as client:
87
+ try:
88
+ async with client.stream(
89
+ "POST", self.api_url,
90
+ headers={
91
+ "Authorization": f"Bearer {self.api_key}",
92
+ "Content-Type": "application/json"
93
+ },
94
+ json=payload
95
+ ) as response:
96
+ if response.status_code != 200:
97
+ error_text = await response.aread()
98
+ logger.error(f"❌ Erreur DeepSeek API (HTTP {response.status_code}): {error_text.decode()}")
99
+ yield {"error": f"Erreur API DeepSeek: {response.status_code}"}
100
+ return
101
+
102
+ async for line in response.aiter_lines():
103
+ if line.startswith("data: "):
104
+ data_str = line[6:]
105
+ if data_str.strip() == "[DONE]":
106
+ break
107
+ try:
108
+ data = json.loads(data_str)
109
+ # Format OpenAI streaming response
110
+ if "choices" in data and len(data["choices"]) > 0:
111
+ delta = data["choices"][0].get("delta", {})
112
+ content = delta.get("content", "")
113
+ if content:
114
+ # Convertir au format attendu par le frontend
115
+ yield {
116
+ "response": {
117
+ "candidates": [{
118
+ "content": {
119
+ "parts": [{"text": content}]
120
+ }
121
+ }]
122
+ }
123
+ }
124
+ except json.JSONDecodeError:
125
+ continue
126
+ except Exception as e:
127
+ logger.error(f"❌ Erreur lors du streaming DeepSeek : {str(e)}")
128
+ yield {"error": str(e)}
129
+
130
+ class VideoProcessor:
131
+ @staticmethod
132
+ def get_frame_metrics(frame: np.ndarray) -> dict:
133
+ try:
134
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
135
+ hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
136
+ return {"brightness": float(np.mean(gray)), "contrast": float(np.std(gray)),
137
+ "saturation": float(np.mean(hsv[:, :, 1])), "sharpness": float(cv2.Laplacian(gray, cv2.CV_64F).var())}
138
+ except: return {"brightness": 0, "contrast": 0, "saturation": 0, "sharpness": 0}
139
+
140
+ def __init__(self, video_path: Path, output_dir: Path):
141
+ self.video_path, self.output_dir = video_path, output_dir
142
+ self.output_dir.mkdir(parents=True, exist_ok=True)
143
+
144
+ def extract_keyframes(self, max_frames: int = 50) -> List[Frame]:
145
+ try:
146
+ from decord import VideoReader, cpu
147
+ vr = VideoReader(str(self.video_path), ctx=cpu(0))
148
+ total = len(vr)
149
+ step = max(1, total // max_frames)
150
+ indices = range(0, total, step)[:max_frames]
151
+ frames_data = vr.get_batch(indices).asnumpy()
152
+ fps = vr.get_avg_fps()
153
+ extracted = []
154
+ for i, idx in enumerate(indices):
155
+ img = cv2.cvtColor(frames_data[i], cv2.COLOR_RGB2BGR)
156
+ ts = idx / fps
157
+ p = self.output_dir / f"f_{idx}.jpg"
158
+ cv2.imwrite(str(p), img, [cv2.IMWRITE_JPEG_QUALITY, 85])
159
+ extracted.append(Frame(path=p, timestamp=ts, metrics=self.get_frame_metrics(img)))
160
+ return extracted
161
+ except Exception as e:
162
+ logger.warning(f"Decord failed, fallback to CV2: {e}")
163
+ cap = cv2.VideoCapture(str(self.video_path))
164
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
165
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1000
166
+ step = max(1, total // max_frames)
167
+ extracted = []
168
+ for idx in range(0, total, step):
169
+ if len(extracted) >= max_frames: break
170
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
171
+ ret, img = cap.read()
172
+ if ret:
173
+ ts = idx / fps
174
+ p = self.output_dir / f"f_{idx}.jpg"
175
+ cv2.imwrite(str(p), img, [cv2.IMWRITE_JPEG_QUALITY, 85])
176
+ extracted.append(Frame(path=p, timestamp=ts, metrics=self.get_frame_metrics(img)))
177
+ cap.release()
178
+ return extracted
179
+
180
+ class AudioProcessor:
181
+ def __init__(self): self.model = None
182
+ def initialize(self):
183
+ if WHISPER_AVAILABLE and self.model is None:
184
+ try:
185
+ device = "cuda" if torch.cuda.is_available() else "cpu"
186
+ self.model = WhisperModel("base", device=device, compute_type="int8")
187
+ except: pass
188
+ def transcribe(self, p: Path) -> str:
189
+ self.initialize()
190
+ if not self.model: return "Transcription indisponible"
191
+ try:
192
+ segments, info = self.model.transcribe(str(p), beam_size=5)
193
+ transcript = " ".join([s.text for s in segments])
194
+ return f"[Langue source détectée: {info.language.upper()}] {transcript}"
195
+ except: return "Erreur transcription"
196
+
197
+ class VideoDownloader:
198
+ @staticmethod
199
+ def download(url: str, output_dir: Path) -> Optional[Path]:
200
+ import yt_dlp
201
+ ydl_opts = {
202
+ 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
203
+ 'outtmpl': str(output_dir / 'downloaded_video.%(ext)s'),
204
+ 'noplaylist': True, 'quiet': True, 'no_warnings': True, 'nocheckcertificate': True,
205
+ 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
206
+ 'referer': 'https://www.google.com/',
207
+ 'http_headers': {'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9'}
208
+ }
209
+ try:
210
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
211
+ info = ydl.extract_info(url, download=True)
212
+ return Path(ydl.prepare_filename(info))
213
+ except: return None
214
+
215
+ class ZenithAnalyzer:
216
+ def __init__(self):
217
+ self.deepseek = DeepSeekClient()
218
+ self.audio_proc = AudioProcessor()
219
+ self.yolo = YOLO("yolov8n.pt") if YOLO_AVAILABLE else None
220
+
221
+ async def extract_frames_only(self, video_path: Path, session_id: str) -> List[str]:
222
+ session_dir = OUTPUT_DIR / f"session_{session_id}"
223
+ session_dir.mkdir(parents=True, exist_ok=True)
224
+ proc = VideoProcessor(video_path, session_dir)
225
+ frames = proc.extract_keyframes()
226
+ return [f"/output/session_{session_id}/{f.path.name}" for f in frames[:12]]
227
+
228
+ async def run_full_analysis(self, video_path: Path, session_id: str, custom_prompt: Optional[str] = None) -> AsyncGenerator[Dict[str, Any], None]:
229
+ session_dir = OUTPUT_DIR / f"session_{session_id}"
230
+ session_dir.mkdir(parents=True, exist_ok=True)
231
+ cache_file = session_dir / "analysis_cache.json"
232
+
233
+ # Optimisation : Ne pas ré-extraire si les frames existent déjà
234
+ existing_frames = list(session_dir.glob("f_*.jpg"))
235
+ if not existing_frames:
236
+ yield {"status": "sampling", "message": "Analyse des séquences..."}
237
+ proc = VideoProcessor(video_path, session_dir)
238
+ frames = proc.extract_keyframes()
239
+ else:
240
+ def get_idx(p):
241
+ try: return int(p.stem.split('_')[1])
242
+ except: return 0
243
+ existing_paths = sorted(existing_frames, key=get_idx)
244
+ frames = []
245
+ for p in existing_paths:
246
+ img = cv2.imread(str(p))
247
+ metrics = VideoProcessor.get_frame_metrics(img) if img is not None else {"brightness": 0, "contrast": 0, "saturation": 0, "sharpness": 0}
248
+ frames.append(Frame(path=p, timestamp=0.0, metrics=metrics))
249
+ yield {"status": "sampling", "message": "Récupération des séquences existantes..."}
250
+
251
+ # Envoyer les chemins des images au frontend
252
+ frame_urls = [f"/output/session_{session_id}/{f.path.name}" for f in frames[:12]]
253
+ yield {"status": "frames_ready", "frames": frame_urls, "message": "Séquences prêtes."}
254
+
255
+ # Vérifier si on a un cache pour l'audio et le visuel
256
+ cached_data = {}
257
+ if cache_file.exists():
258
+ try:
259
+ with open(cache_file, "r") as f:
260
+ cached_data = json.load(f)
261
+ logger.info(f"✅ Cache trouvé pour la session {session_id}")
262
+ except: pass
263
+
264
+ if "transcript" in cached_data and "vision_info" in cached_data:
265
+ transcript = cached_data["transcript"]
266
+ v_info = cached_data["vision_info"]
267
+ yield {"status": "fusion", "message": "Utilisation des données en cache..."}
268
+ else:
269
+ yield {"status": "audio", "message": "Traitement audio & visuel..."}
270
+ loop = asyncio.get_event_loop()
271
+ with concurrent.futures.ThreadPoolExecutor() as executor:
272
+ audio_task = loop.run_in_executor(executor, self.audio_proc.transcribe, video_path)
273
+
274
+ if self.yolo:
275
+ all_paths = [str(f.path) for f in frames]
276
+ batch_size = 10
277
+ for i in range(0, len(all_paths), batch_size):
278
+ batch = all_paths[i:i+batch_size]
279
+ results = await loop.run_in_executor(executor, lambda: self.yolo(batch, verbose=False, imgsz=320, stream=False))
280
+ for j, res in enumerate(results):
281
+ idx = i + j
282
+ objs = [res.names[int(b.cls[0])] for b in res.boxes if b.conf > 0.25]
283
+ ambiance = f"Ambiance: {'Sombre' if frames[idx].metrics['brightness'] < 50 else 'Lumineuse'}"
284
+ frames[idx].vision_content = f"{ambiance}, Objets: " + ", ".join([f"{v}x {k}" for k,v in Counter(objs).items()])
285
+
286
+ transcript = await audio_task
287
+
288
+ v_info = "\n".join([f"[{f.timestamp:.1f}s] {f.vision_content}" for f in frames[:40]])
289
+
290
+ # Sauvegarder dans le cache
291
+ try:
292
+ with open(cache_file, "w") as f:
293
+ json.dump({"transcript": transcript, "vision_info": v_info}, f)
294
+ except: pass
295
+
296
+ yield {"status": "fusion", "message": "Intelligence Artificielle en action..."}
297
+
298
+ # Utilisation du prompt personnalisé si fourni
299
+ base_instruction = custom_prompt if custom_prompt else "Résumer et continuer l'analyse du média"
300
+
301
+ prompt = f"""Tu es l'unité Zenith AI, un système d'analyse de données multimédias.
302
+ INSTRUCTION UTILISATEUR : {base_instruction}
303
+
304
+ DONNÉES D'ENTRÉE :
305
+ - TRANSCRIPTION : {transcript}
306
+ - DONNÉES VISUELLES : {v_info}
307
+
308
+ Produis un rapport TECHNIQUE, FACTUEL et STRUCTURÉ en Markdown."""
309
+
310
+ # Encodage parallèle des images
311
+ selected_frames = [frames[i] for i in range(0, len(frames), max(1, len(frames)//10))][:10]
312
+ def encode_f(f):
313
+ img = cv2.imread(str(f.path))
314
+ _, buf = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 70])
315
+ return {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64.b64encode(buf).decode()}"}}
316
+
317
+ with concurrent.futures.ThreadPoolExecutor() as executor:
318
+ images = list(executor.map(encode_f, selected_frames))
319
+
320
+ messages = [{"role": "user", "content": [{"type": "text", "text": prompt}] + images}]
321
+
322
+ yield {"status": "generating", "message": "Génération du rapport par l'IA..."}
323
+ async for chunk in self.deepseek.stream_content(DEEPSEEK_MODEL, messages, {"temperature": 0.7}):
324
+ if "error" in chunk:
325
+ yield {"error": chunk["error"]}
326
+ break
327
+ resp = chunk.get("response", {})
328
+ candidates = resp.get("candidates", [])
329
+ if candidates:
330
+ for part in candidates[0].get("content", {}).get("parts", []):
331
+ text = part.get("text", "")
332
+ if text: yield {"status": "streaming", "text": text}
333
+
334
+ # Cleanup
335
+ gc.collect()
336
+ if torch.cuda.is_available(): torch.cuda.empty_cache()
337
+ yield {"status": "completed", "message": "Analyse terminée."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py CHANGED
@@ -1,122 +1,121 @@
1
- import os, uuid, json, asyncio, time
2
- from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks, HTTPException
3
- from fastapi.responses import StreamingResponse
4
- from fastapi.middleware.cors import CORSMiddleware
5
- from fastapi.staticfiles import StaticFiles
6
- from pathlib import Path
7
- from backend.core.engine import ZenithAnalyzer, VideoDownloader, OUTPUT_DIR
8
-
9
- app = FastAPI(title="Zenith AI API")
10
-
11
- # Configuration CORS étendue pour permettre au frontend hébergé ailleurs de communiquer avec l'API
12
- app.add_middleware(
13
- CORSMiddleware,
14
- allow_origins=["*"], # Vous pourrez remplacer "*" par l'URL de votre site Vercel plus tard pour plus de sécurité
15
- allow_credentials=True,
16
- allow_methods=["*"],
17
- allow_headers=["*"],
18
- )
19
-
20
- analyzer = ZenithAnalyzer()
21
-
22
- def cleanup_old_sessions(max_age_hours=1):
23
- """Supprime les dossiers de session plus vieux que max_age_hours."""
24
- try:
25
- import shutil
26
- now = time.time()
27
- for path in OUTPUT_DIR.glob("session_*"):
28
- if path.is_dir():
29
- # Vérifier l'âge du dossier
30
- if (now - path.stat().st_mtime) > (max_age_hours * 3600):
31
- shutil.rmtree(path)
32
- except Exception as e:
33
- print(f"Erreur lors du nettoyage : {e}")
34
-
35
- @app.post("/analyze/url")
36
- async def analyze_url(url: str = Form(...), background_tasks: BackgroundTasks = BackgroundTasks()):
37
- background_tasks.add_task(cleanup_old_sessions)
38
- session_id = str(uuid.uuid4())
39
- session_dir = OUTPUT_DIR / f"session_{session_id}"
40
- session_dir.mkdir(parents=True, exist_ok=True)
41
-
42
- video_path = VideoDownloader.download(url, session_dir)
43
- if not video_path:
44
- raise HTTPException(status_code=400, detail="Échec du téléchargement de la vidéo")
45
-
46
- # Retourner l'URL relative pour le frontend
47
- video_url = f"/output/session_{session_id}/{video_path.name}"
48
- return {"session_id": session_id, "video_path": str(video_path), "video_url": video_url}
49
-
50
- @app.post("/analyze/upload")
51
- async def analyze_upload(file: UploadFile = File(...), background_tasks: BackgroundTasks = BackgroundTasks()):
52
- background_tasks.add_task(cleanup_old_sessions)
53
- session_id = str(uuid.uuid4())
54
- session_dir = OUTPUT_DIR / f"session_{session_id}"
55
- session_dir.mkdir(parents=True, exist_ok=True)
56
-
57
- file_path = session_dir / file.filename
58
- with open(file_path, "wb") as buffer:
59
- buffer.write(await file.read())
60
-
61
- # Retourner l'URL relative pour le frontend
62
- video_url = f"/output/session_{session_id}/{file.filename}"
63
- return {"session_id": session_id, "video_path": str(file_path), "video_url": video_url}
64
-
65
- @app.post("/analyze/extract-frames")
66
- async def extract_frames(session_id: str = Form(...), video_path: str = Form(...)):
67
- # Sécurité : Vérifier que le chemin du fichier est bien dans le dossier autorisé
68
- abs_video_path = Path(video_path).resolve()
69
- abs_output_dir = OUTPUT_DIR.resolve()
70
-
71
- if not str(abs_video_path).startswith(str(abs_output_dir)):
72
- raise HTTPException(status_code=403, detail="Accès au fichier non autorisé")
73
-
74
- try:
75
- frames = await analyzer.extract_frames_only(abs_video_path, session_id)
76
- return {"status": "success", "frames": frames}
77
- except Exception as e:
78
- raise HTTPException(status_code=500, detail=str(e))
79
-
80
- @app.get("/stream/{session_id}")
81
- async def stream_analysis(session_id: str, video_path: str, prompt: str = None):
82
- # Sécurité : Vérifier que le chemin du fichier est bien dans le dossier autorisé
83
- abs_video_path = Path(video_path).resolve()
84
- abs_output_dir = OUTPUT_DIR.resolve()
85
-
86
- if not str(abs_video_path).startswith(str(abs_output_dir)):
87
- raise HTTPException(status_code=403, detail="Accès au fichier non autorisé")
88
-
89
- async def event_generator():
90
- try:
91
- async for update in analyzer.run_full_analysis(abs_video_path, session_id, custom_prompt=prompt):
92
- yield f"data: {json.dumps(update)}\n\n"
93
- except Exception as e:
94
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
95
-
96
- return StreamingResponse(event_generator(), media_type="text/event-stream")
97
-
98
- # Servir le dossier de sortie pour les images extraites
99
- app.mount("/output", StaticFiles(directory=str(OUTPUT_DIR)), name="output")
100
-
101
- # Servir le frontend statique uniquement s'il existe
102
- frontend_path = Path("frontend")
103
- if frontend_path.exists():
104
- app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
105
- else:
106
- @app.get("/")
107
- async def root():
108
- import os
109
- gcp_configured = "OUI" if os.getenv("GCP_SERVICE_ACCOUNT") else "NON"
110
- return {
111
- "status": "Zenith AI API is running",
112
- "frontend": "hosted externally",
113
- "diagnostics": {
114
- "gcp_service_account": gcp_configured,
115
- "yolo_available": "YES" if analyzer.yolo else "NO",
116
- "whisper_available": "YES" if analyzer.audio_proc else "NO"
117
- }
118
- }
119
-
120
- if __name__ == "__main__":
121
- import uvicorn
122
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ import os, uuid, json, asyncio, time
2
+ from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks, HTTPException
3
+ from fastapi.responses import StreamingResponse
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.staticfiles import StaticFiles
6
+ from pathlib import Path
7
+ from backend.core.engine import ZenithAnalyzer, VideoDownloader, OUTPUT_DIR
8
+
9
+ app = FastAPI(title="Zenith AI API")
10
+
11
+ # Configuration CORS étendue pour permettre au frontend hébergé ailleurs de communiquer avec l'API
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"], # Vous pourrez remplacer "*" par l'URL de votre site Vercel plus tard pour plus de sécurité
15
+ allow_credentials=True,
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ analyzer = ZenithAnalyzer()
21
+
22
+ def cleanup_old_sessions(max_age_hours=1):
23
+ """Supprime les dossiers de session plus vieux que max_age_hours."""
24
+ try:
25
+ import shutil
26
+ now = time.time()
27
+ for path in OUTPUT_DIR.glob("session_*"):
28
+ if path.is_dir():
29
+ # Vérifier l'âge du dossier
30
+ if (now - path.stat().st_mtime) > (max_age_hours * 3600):
31
+ shutil.rmtree(path)
32
+ except Exception as e:
33
+ print(f"Erreur lors du nettoyage : {e}")
34
+
35
+ @app.post("/analyze/url")
36
+ async def analyze_url(url: str = Form(...), background_tasks: BackgroundTasks = BackgroundTasks()):
37
+ background_tasks.add_task(cleanup_old_sessions)
38
+ session_id = str(uuid.uuid4())
39
+ session_dir = OUTPUT_DIR / f"session_{session_id}"
40
+ session_dir.mkdir(parents=True, exist_ok=True)
41
+
42
+ video_path = VideoDownloader.download(url, session_dir)
43
+ if not video_path:
44
+ raise HTTPException(status_code=400, detail="Échec du téléchargement de la vidéo")
45
+
46
+ # Retourner l'URL relative pour le frontend
47
+ video_url = f"/output/session_{session_id}/{video_path.name}"
48
+ return {"session_id": session_id, "video_path": str(video_path), "video_url": video_url}
49
+
50
+ @app.post("/analyze/upload")
51
+ async def analyze_upload(file: UploadFile = File(...), background_tasks: BackgroundTasks = BackgroundTasks()):
52
+ background_tasks.add_task(cleanup_old_sessions)
53
+ session_id = str(uuid.uuid4())
54
+ session_dir = OUTPUT_DIR / f"session_{session_id}"
55
+ session_dir.mkdir(parents=True, exist_ok=True)
56
+
57
+ file_path = session_dir / file.filename
58
+ with open(file_path, "wb") as buffer:
59
+ buffer.write(await file.read())
60
+
61
+ # Retourner l'URL relative pour le frontend
62
+ video_url = f"/output/session_{session_id}/{file.filename}"
63
+ return {"session_id": session_id, "video_path": str(file_path), "video_url": video_url}
64
+
65
+ @app.post("/analyze/extract-frames")
66
+ async def extract_frames(session_id: str = Form(...), video_path: str = Form(...)):
67
+ # Sécurité : Vérifier que le chemin du fichier est bien dans le dossier autorisé
68
+ abs_video_path = Path(video_path).resolve()
69
+ abs_output_dir = OUTPUT_DIR.resolve()
70
+
71
+ if not str(abs_video_path).startswith(str(abs_output_dir)):
72
+ raise HTTPException(status_code=403, detail="Accès au fichier non autorisé")
73
+
74
+ try:
75
+ frames = await analyzer.extract_frames_only(abs_video_path, session_id)
76
+ return {"status": "success", "frames": frames}
77
+ except Exception as e:
78
+ raise HTTPException(status_code=500, detail=str(e))
79
+
80
+ @app.get("/stream/{session_id}")
81
+ async def stream_analysis(session_id: str, video_path: str, prompt: str = None):
82
+ # Sécurité : Vérifier que le chemin du fichier est bien dans le dossier autorisé
83
+ abs_video_path = Path(video_path).resolve()
84
+ abs_output_dir = OUTPUT_DIR.resolve()
85
+
86
+ if not str(abs_video_path).startswith(str(abs_output_dir)):
87
+ raise HTTPException(status_code=403, detail="Accès au fichier non autorisé")
88
+
89
+ async def event_generator():
90
+ try:
91
+ async for update in analyzer.run_full_analysis(abs_video_path, session_id, custom_prompt=prompt):
92
+ yield f"data: {json.dumps(update)}\n\n"
93
+ except Exception as e:
94
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
95
+
96
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
97
+
98
+ # Servir le dossier de sortie pour les images extraites
99
+ app.mount("/output", StaticFiles(directory=str(OUTPUT_DIR)), name="output")
100
+
101
+ # Servir le frontend statique uniquement s'il existe
102
+ frontend_path = Path("frontend")
103
+ if frontend_path.exists():
104
+ app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
105
+ else:
106
+ @app.get("/")
107
+ async def root():
108
+ import os
109
+ return {
110
+ "status": "Zenith AI API is running",
111
+ "frontend": "hosted externally",
112
+ "diagnostics": {
113
+ "deepseek_configured": "YES",
114
+ "yolo_available": "YES" if analyzer.yolo else "NO",
115
+ "whisper_available": "YES" if analyzer.audio_proc else "NO"
116
+ }
117
+ }
118
+
119
+ if __name__ == "__main__":
120
+ import uvicorn
121
+ uvicorn.run(app, host="0.0.0.0", port=8000)
 
requirements.txt CHANGED
@@ -1,15 +1,15 @@
1
- gradio
2
- requests
3
- numpy
4
- opencv-python
5
- ultralytics
6
- faster-whisper
7
- decord
8
- yt-dlp
9
- fastapi
10
- uvicorn
11
- httpx
12
- python-dotenv
13
- python-multipart
14
- python-magic; platform_system != 'Windows'
15
- python-magic-bin; platform_system == 'Windows'
 
1
+ gradio
2
+ requests
3
+ numpy
4
+ opencv-python
5
+ ultralytics
6
+ faster-whisper
7
+ decord
8
+ yt-dlp
9
+ fastapi
10
+ uvicorn
11
+ httpx
12
+ python-dotenv
13
+ python-multipart
14
+ python-magic; platform_system != 'Windows'
15
+ python-magic-bin; platform_system == 'Windows'
run.py CHANGED
@@ -1,16 +1,16 @@
1
- import uvicorn
2
- import os
3
- from dotenv import load_dotenv
4
-
5
- if __name__ == "__main__":
6
- load_dotenv()
7
-
8
- # Vérification des variables d'environnement critiques
9
- if not os.getenv("GCP_SERVICE_ACCOUNT"):
10
- print("⚠️ Attention : GCP_SERVICE_ACCOUNT n'est pas configuré dans le fichier .env")
11
-
12
- print("🚀 Démarrage de Zenith AI SaaS...")
13
- print("🌍 Interface disponible sur : http://localhost:8000")
14
-
15
- # Lancement du serveur FastAPI
16
- uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)
 
1
+ import uvicorn
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ if __name__ == "__main__":
6
+ load_dotenv()
7
+
8
+ # Vérification des variables d'environnement critiques
9
+ if not os.getenv("GCP_SERVICE_ACCOUNT"):
10
+ print("⚠️ Attention : GCP_SERVICE_ACCOUNT n'est pas configuré dans le fichier .env")
11
+
12
+ print("🚀 Démarrage de Zenith AI SaaS...")
13
+ print("🌍 Interface disponible sur : http://localhost:8000")
14
+
15
+ # Lancement du serveur FastAPI
16
+ uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)
test_deepseek.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script de test pour vérifier la connexion à l'API DeepSeek
4
+ """
5
+ import asyncio
6
+ import sys
7
+ from backend.core.engine import DeepSeekClient, DEEPSEEK_MODEL
8
+
9
+ async def test_deepseek():
10
+ print("🔍 Test de connexion à DeepSeek API...")
11
+ print(f"📡 URL: https://shads229-personnal-ai.hf.space/v1/chat/completions")
12
+ print(f"🤖 Modèle: {DEEPSEEK_MODEL}\n")
13
+
14
+ client = DeepSeekClient()
15
+
16
+ # Message de test simple
17
+ messages = [
18
+ {
19
+ "role": "user",
20
+ "content": "Réponds simplement 'OK' si tu me reçois."
21
+ }
22
+ ]
23
+
24
+ print("📤 Envoi du message de test...")
25
+
26
+ try:
27
+ response_received = False
28
+ async for chunk in client.stream_content(DEEPSEEK_MODEL, messages, {"temperature": 0.7}):
29
+ if "error" in chunk:
30
+ print(f"❌ Erreur: {chunk['error']}")
31
+ return False
32
+
33
+ if "response" in chunk:
34
+ candidates = chunk.get("response", {}).get("candidates", [])
35
+ if candidates:
36
+ for part in candidates[0].get("content", {}).get("parts", []):
37
+ text = part.get("text", "")
38
+ if text:
39
+ print(f"✅ Réponse reçue: {text}")
40
+ response_received = True
41
+
42
+ if response_received:
43
+ print("\n✅ Test réussi ! L'API DeepSeek fonctionne correctement.")
44
+ return True
45
+ else:
46
+ print("\n⚠️ Aucune réponse reçue de l'API.")
47
+ return False
48
+
49
+ except Exception as e:
50
+ print(f"\n❌ Erreur lors du test: {str(e)}")
51
+ return False
52
+
53
+ if __name__ == "__main__":
54
+ result = asyncio.run(test_deepseek())
55
+ sys.exit(0 if result else 1)