diff --git a/modules/ai_media.py b/modules/ai_media.py index 401297b..d1b6ba0 100644 --- a/modules/ai_media.py +++ b/modules/ai_media.py @@ -6,7 +6,6 @@ import base64 from modules.logger import log_event def generer_image_cloudflare(prompt: str) -> bytes: - """Appel à l'API Cloudflare Workers AI avec décodage Base64""" account_id = os.getenv("CLOUDFLARE_ACCOUNT_ID") api_token = os.getenv("CLOUDFLARE_API_TOKEN") if not account_id or not api_token: return None @@ -27,35 +26,64 @@ def generer_image_cloudflare(prompt: str) -> bytes: log_event("WARNING", "MEDIA", f"Échec Cloudflare : {e}") return None -def generer_image_huggingface(prompt: str) -> bytes: - """Appel à l'API HuggingFace""" +def generer_image_openrouter(prompt: str) -> tuple: + api_key = os.getenv("OPENROUTER_API_KEY") + if not api_key: return None, None + + url = "https://openrouter.ai/api/v1/images" + headers = { + "Authorization": f"Bearer {api_key}", + "HTTP-Referer": "https://aethas38.duckdns.org", + "X-Title": "Aethas38" + } + modele = "bytedance-seed/seedream-4.5" + payload = {"prompt": prompt, "model": modele} + + try: + resp = requests.post(url, headers=headers, json=payload, timeout=120) + if resp.status_code == 200: + data = resp.json() + if "data" in data and len(data["data"]) > 0 and "b64_json" in data["data"][0]: + img_bytes = base64.b64decode(data["data"][0]["b64_json"]) + return img_bytes, modele + except Exception as e: + log_event("WARNING", "MEDIA", f"Échec OpenRouter Image : {e}") + return None, None + +def generer_image_huggingface(prompt: str) -> tuple: api_key = os.getenv("HUGGINGFACE_API_KEY") - if not api_key: return None + if not api_key: return None, "Clé API manquante" url = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0" headers = {"Authorization": f"Bearer {api_key}"} try: resp = requests.post(url, headers=headers, json={"inputs": prompt}, timeout=120) - if resp.status_code == 200: return resp.content + if resp.status_code == 200: + return resp.content, "Succès" + else: + return None, f"Erreur HTTP {resp.status_code} : {resp.text}" except Exception as e: log_event("WARNING", "MEDIA", f"Échec HF : {e}") - return None + return None, f"Exception système : {str(e)}" -def generer_les_deux_images(prompt: str, fichiers: list) -> tuple: +def generer_les_images(prompt: str, fichiers: list) -> tuple: trace = [] if fichiers: - trace.append("🟢 Mode Image-to-Image (Désactivé en multi-fournisseur)") - return f"*(BETA)* Le mode Image-to-Image nécessite une API spécifique. \n\nFichiers : {', '.join(fichiers)}", "#ff9800", "Système", "En dev", trace + trace.append("🟢 Mode Image-to-Image") + return f"*(BETA)* Le mode Image-to-Image est désactivé temporairement. \n\nFichiers : {', '.join(fichiers)}", "#ff9800", "Système", "En dev", trace - trace.append("🟢 Lancement Text-to-Image (HuggingFace + Cloudflare)") + trace.append("🟢 Lancement Text-to-Image (OR + CF + HF)") - # Exécution des deux API en parallèle pour gagner du temps + # Exécution des TROIS API en parallèle with concurrent.futures.ThreadPoolExecutor() as executor: - future_hf = executor.submit(generer_image_huggingface, prompt) + future_or = executor.submit(generer_image_openrouter, prompt) future_cf = executor.submit(generer_image_cloudflare, prompt) - img_hf = future_hf.result() + future_hf = executor.submit(generer_image_huggingface, prompt) + + img_or, nom_modele_or = future_or.result() img_cf = future_cf.result() + img_hf, msg_hf = future_hf.result() dossier_media = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Media" os.makedirs(dossier_media, exist_ok=True) @@ -63,38 +91,50 @@ def generer_les_deux_images(prompt: str, fichiers: list) -> tuple: reponse_md = f"**Prompt analysé :** {prompt}\n\n" fournisseurs_actifs = [] + # Traitement OpenRouter + if img_or: + nom_or = f"img_or_{int(time.time())}.png" + with open(os.path.join(dossier_media, nom_or), 'wb') as f: f.write(img_or) + reponse_md += f"### 🌐 OpenRouter ({nom_modele_or})\n![OpenRouter](/Media/{nom_or})\n" + reponse_md += f"**[📥 Télécharger l'image OR](/Media/{nom_or})**\n\n---\n\n" + fournisseurs_actifs.append("OpenRouter") + trace.append("🟢 OR : Succès") + else: + trace.append("🔴 OR : Échec") + # Traitement Cloudflare if img_cf: nom_cf = f"img_cf_{int(time.time())}.png" with open(os.path.join(dossier_media, nom_cf), 'wb') as f: f.write(img_cf) - reponse_md += f"### ☁️ Cloudflare Workers AI (SDXL)\n![Cloudflare](/Media/{nom_cf})\n\n" - reponse_md += f"**[📥 Cliquez ici pour télécharger l'image](/Media/{nom_cf})**\n\n---\n\n" + reponse_md += f"### ☁️ Cloudflare Workers AI\n![Cloudflare](/Media/{nom_cf})\n" + reponse_md += f"**[📥 Télécharger l'image CF](/Media/{nom_cf})**\n\n---\n\n" fournisseurs_actifs.append("Cloudflare") - trace.append("🟢 Cloudflare : Succès") + trace.append("🟢 CF : Succès") else: - trace.append("🔴 Cloudflare : Échec") + trace.append("🔴 CF : Échec") - # Traitement HuggingFace + # Traitement HuggingFace (Avec diagnostic intégré) if img_hf: nom_hf = f"img_hf_{int(time.time())}.png" with open(os.path.join(dossier_media, nom_hf), 'wb') as f: f.write(img_hf) - reponse_md += f"### 🤗 HuggingFace (SDXL)\n![HuggingFace](/Media/{nom_hf})\n\n" - reponse_md += f"**[📥 Cliquez ici pour télécharger l'image](/Media/{nom_hf})**\n\n" + reponse_md += f"### 🤗 HuggingFace (SDXL)\n![HuggingFace](/Media/{nom_hf})\n" + reponse_md += f"**[📥 Télécharger l'image HF](/Media/{nom_hf})**\n\n" fournisseurs_actifs.append("HuggingFace") trace.append("🟢 HF : Succès") else: - trace.append("🔴 HF : Échec") + erreur_courte = str(msg_hf)[:50] + "..." if len(str(msg_hf)) > 50 else str(msg_hf) + trace.append(f"🔴 HF : Échec ({erreur_courte})") + reponse_md += f"### 🤗 HuggingFace (SDXL)\n*⚠️ Échec de la génération : {msg_hf}*\n\n" - # Si les deux ont échoué - if not img_hf and not img_cf: - return "*Échec critique : HuggingFace et Cloudflare ont échoué.*", "#ff4444", "Système", "Erreur", trace + if not img_or and not img_cf and not img_hf: + return "*Échec critique : Aucune des 3 API n'a pu générer d'image.*", "#ff4444", "Système", "Erreur", trace - return reponse_md, "#00e5ff", " + ".join(fournisseurs_actifs), "Stable Diffusion XL", trace + return reponse_md, "#00e5ff", " + ".join(fournisseurs_actifs), "Moteur Triple-IA", trace def router_media(prompt: str, type_media: str, premium: bool, fichiers: list) -> tuple: trace = [f"🟢 Routage Média : {type_media.upper()}"] if type_media == "image": - return generer_les_deux_images(prompt, fichiers) + return generer_les_images(prompt, fichiers) elif type_media == "video": - return "*La génération vidéo nécessite une API payante (Luma, Kling).* \n\nPrompt : " + prompt, "#ff4444", "Système", "En attente API", trace + return "*La génération vidéo nécessite une API payante.* \n\nPrompt : " + prompt, "#ff4444", "Système", "En attente API", trace return "Type de média non reconnu.", "#333", "Erreur", "Aucun", trace \ No newline at end of file diff --git a/modules/theme.py b/modules/theme.py index e83a8df..dbfeee3 100644 --- a/modules/theme.py +++ b/modules/theme.py @@ -32,7 +32,7 @@ def get_theme_hdrs(): /* --- CONTENU CENTRAL & CONTRASTE MOBILE ACCRU --- */ .main-content { flex: 1; display: flex; flex-direction: column; height: 100vh; position: relative; box-sizing: border-box; } - .chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 160px; box-sizing: border-box; } + .chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 220px; box-sizing: border-box; } .user-bubble { background-color: #2a2a35; color: #ffffff; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #555; margin-left: auto; word-break: break-word; overflow-wrap: break-word; } /* BULLE IA : Contraste maximal forcé pour le confort de lecture mobile */ @@ -80,7 +80,7 @@ def get_theme_hdrs(): .sidebar { position: absolute; left: -100%; top: 0; z-index: 1000; width: 280px; height: 100vh; box-shadow: 5px 0 15px rgba(0,0,0,0.8); } .sidebar.sidebar-open { left: 0; } .main-content { flex: 1; height: calc(100vh - 60px); } - .chat-container { padding-bottom: 200px; padding-left: 10px; padding-right: 10px; } + .chat-container { padding-bottom: 260px; padding-left: 10px; padding-right: 10px; } .user-bubble, .ai-bubble { max-width: 95%; } .input-area { padding: 10px; } .options-row { justify-content: center; gap: 10px;}