From 519a42107fad4f6f48b67432c9cb10efd6e8f0c9 Mon Sep 17 00:00:00 2001 From: Xavier Date: Mon, 24 Aug 2026 19:25:05 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20module=20multim=C3=A9dia=20(Text-t?= =?UTF-8?q?o-Image=20via=20HuggingFace=20et=20Cloudflare)=20et=20s=C3=A9cu?= =?UTF-8?q?risation=20du=20routeur=20Multi-IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++- aethas_ip_logs.json | 60 ++++++++++++++++++ main.py | 3 + modules/ai_media.py | 91 ++++++++++++++++++++++++++++ modules/ai_openrouter.py | 18 +++++- modules/chat_handler.py | 127 +++++++++++++++++++++------------------ modules/model_updater.py | 14 ++--- modules/ui_components.py | 43 ++++++++++--- 8 files changed, 284 insertions(+), 82 deletions(-) create mode 100644 modules/ai_media.py diff --git a/README.md b/README.md index e958867..b950a38 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,13 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim - [x] Continuité de service : IA de secours automatique et Timeout à 300s. ### Phase 3 : Refonte UI & Expérience Utilisateur ✅ -- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive) +- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive / Contraste OLED élevé) - [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session - [x] Zone de saisie intelligente (Drag & Drop, Collage multi-fichiers) - [x] Export modulaire et propre (PDF, DOC, TXT, JSON, ZIP) - [x] Boutons de partage direct (WhatsApp, Email) et Déconnexion. - [x] Indicateur d'avancement dynamique (Loader Néon) pendant le traitement des IA. +- [x] Transparence du Routage : Affichage visuel (Badges) du parcours multi-IA (Succès, Échecs, Erreurs 404/503) pour chaque requête. ### Phase 4 : Gestion des Utilisateurs & Sécurité Légale - [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur (création PDF, DOCX, etc.). @@ -49,6 +50,7 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim ### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖 *L'infrastructure bascule sur un catalogue dynamique mis à jour toutes les 2h pour pallier les décommissionnements.* +*Filtrage actif des modèles OpenRouter restreints (ex: Politiques de confidentialité strictes ignorées).* | Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal | | :--- | :--- | :--- | :--- | @@ -61,6 +63,8 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim - [ ] File d'attente asynchrone (Queue List) et Timer en temps réel pour gérer la charge serveur. - [ ] Sauvegarde automatique : Compression du site toutes les 6 heures vers un GDrive. - [x] Hébergement Gitea Docker. Injection des clés via `env_file`. Volumes persistants configurés. +- [x] Persistance absolue et sécurité : Fichiers d'historique, logs, facturation et dossiers RAG isolés sur volume RAID mdadm externe. +- [x] Synchronisation temporelle : Conteneurs Docker synchronisés sur l'horloge système du serveur Debian hôte (NTP via montage en lecture seule). ### Phase 9 : Modération Automatisée & Conformité Pénale 🚨 - [x] Système hiérarchique : `Super Admin`, `Admin`, `VIP` (Famille) et `Standard`. Quotas de ressources (60% CPU/RAM) applicables selon le rang. @@ -69,6 +73,6 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim ### Phase 10 : Base de Connaissances Intelligente (RAG) 📚 - [x] Initialisation de l'arborescence dynamique (`RAG/`) sur le stockage RAID. -- [x] Injection asynchrone des documents Markdown et Texte directement dans la fenêtre de contexte des LLMs. -- [x] Auto-Apprentissage : Le système génère automatiquement des fichiers `.md` de son propre savoir après validation. +- [x] Injection dynamique des documents Markdown et Texte directement dans la fenêtre de contexte des LLMs. +- [x] Auto-Apprentissage Infaillible : Le système génère automatiquement des fichiers `.md` de son propre savoir après validation. Inclusion d'un dossier "Connaissances_Generales" pour capter 100% des requêtes. - [ ] Interface UI : Ajout d'une vue Administrateur pour téléverser de nouveaux documents RAG. \ No newline at end of file diff --git a/aethas_ip_logs.json b/aethas_ip_logs.json index a37d7ad..0316446 100644 --- a/aethas_ip_logs.json +++ b/aethas_ip_logs.json @@ -11860,5 +11860,65 @@ "action": "Acc\u00e8s \u00e0 /", "date": "2026-08-23 15:08:53", "timestamp": 1787490533.299349 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 19:50:11", + "timestamp": 1787507411.7223423 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 20:27:43", + "timestamp": 1787509663.9815109 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-24 18:25:30", + "timestamp": 1787588730.6111038 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-24 18:54:02", + "timestamp": 1787590442.176149 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-24 18:54:18", + "timestamp": 1787590458.311077 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-24 19:08:18", + "timestamp": 1787591298.5431492 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /chat", + "date": "2026-08-24 19:14:32", + "timestamp": 1787591672.8711567 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /Media/img_cf_1787591691.png", + "date": "2026-08-24 19:14:51", + "timestamp": 1787591691.4614098 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-24 19:21:20", + "timestamp": 1787592080.4595678 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /Media/img_cf_1787591691.png", + "date": "2026-08-24 19:21:22", + "timestamp": 1787592082.4804988 } ] \ No newline at end of file diff --git a/main.py b/main.py index 8b47c29..5a01e86 100644 --- a/main.py +++ b/main.py @@ -44,7 +44,10 @@ app, rt = fast_app( middleware=[Middleware(SecurityMiddleware)] ) +dossier_media = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Media" +os.makedirs(dossier_media, exist_ok=True) app.mount("/Logo", StaticFiles(directory="Logo"), name="Logo") +app.mount("/Media", StaticFiles(directory="/DATA/AppData/MULTI-IA-AETHAS38/RAG/Media"), name="Media") # --- ROUTES D'AUTHENTIFICATION --- @rt('/login') diff --git a/modules/ai_media.py b/modules/ai_media.py new file mode 100644 index 0000000..5041cdb --- /dev/null +++ b/modules/ai_media.py @@ -0,0 +1,91 @@ +import os +import requests +import time +import concurrent.futures +from modules.logger import log_event + +def generer_image_cloudflare(prompt: str) -> bytes: + """Appel à l'API Cloudflare Workers AI""" + account_id = os.getenv("CLOUDFLARE_ACCOUNT_ID") + api_token = os.getenv("CLOUDFLARE_API_TOKEN") + if not account_id or not api_token: return None + + url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/stabilityai/stable-diffusion-xl-base-1.0" + headers = {"Authorization": f"Bearer {api_token}"} + + try: + resp = requests.post(url, headers=headers, json={"prompt": prompt}, timeout=120) + if resp.status_code == 200: return resp.content + except Exception as e: + log_event("WARNING", "MEDIA", f"Échec Cloudflare : {e}") + return None + +def generer_image_huggingface(prompt: str) -> bytes: + """Appel à l'API HuggingFace""" + api_key = os.getenv("HUGGINGFACE_API_KEY") + if not api_key: return None + + 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 + except Exception as e: + log_event("WARNING", "MEDIA", f"Échec HF : {e}") + return None + +def generer_les_deux_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("🟢 Lancement Text-to-Image (HuggingFace + Cloudflare)") + + # Exécution des deux API en parallèle pour gagner du temps + with concurrent.futures.ThreadPoolExecutor() as executor: + future_hf = executor.submit(generer_image_huggingface, prompt) + future_cf = executor.submit(generer_image_cloudflare, prompt) + img_hf = future_hf.result() + img_cf = future_cf.result() + + dossier_media = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Media" + os.makedirs(dossier_media, exist_ok=True) + + reponse_md = f"**Prompt analysé :** {prompt}\n\n" + fournisseurs_actifs = [] + + # 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---\n\n" + fournisseurs_actifs.append("Cloudflare") + trace.append("🟢 Cloudflare : Succès") + else: + trace.append("🔴 Cloudflare : Échec") + + # Traitement HuggingFace + 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" + fournisseurs_actifs.append("HuggingFace") + trace.append("🟢 HF : Succès") + else: + trace.append("🔴 HF : Échec") + + # Si les deux ont échoué + if not img_hf and not img_cf: + return "*Échec critique : HuggingFace et Cloudflare ont échoué (ou clés manquantes).* \nVérifiez les logs du serveur.", "#ff4444", "Système", "Erreur", trace + + return reponse_md, "#00e5ff", " + ".join(fournisseurs_actifs), "Stable Diffusion XL", 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) + 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 "Type de média non reconnu.", "#333", "Erreur", "Aucun", trace \ No newline at end of file diff --git a/modules/ai_openrouter.py b/modules/ai_openrouter.py index 601b44d..adfff05 100644 --- a/modules/ai_openrouter.py +++ b/modules/ai_openrouter.py @@ -83,11 +83,23 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code verifier_cle_api() extra_body_data = {"session_id": chat_id} if chat_id else {} - modeles = [obtenir_modele_dynamique(premium, prompt)] + # --- LA LISTE BLANCHE DES 5 MEILLEURS MODÈLES --- if premium: - modeles.append("openai/gpt-4o-mini") + modeles = [ + "anthropic/claude-3.5-sonnet", # Le meilleur en code/logique + "openai/gpt-4o", # Ultra polyvalent + "google/gemini-1.5-pro", # Excellent contexte + "mistralai/mistral-large-2407", # Top pour le français + "meta-llama/llama-3.1-405b-instruct" # Le titan OpenSource + ] else: - modeles.append("nvidia/nemotron-3-nano-30b-a3b:free") + modeles = [ + "google/gemma-2-9b-it:free", + "meta-llama/llama-3.1-8b-instruct:free", + "microsoft/phi-3-mini-128k-instruct:free", + "mistralai/mistral-nemo:free", + "qwen/qwen-2-7b-instruct:free" + ] reponses = [] for modele in modeles: diff --git a/modules/chat_handler.py b/modules/chat_handler.py index 682fb8b..123392d 100644 --- a/modules/chat_handler.py +++ b/modules/chat_handler.py @@ -16,6 +16,7 @@ from modules.logger import log_event from modules.billing_manager import verifier_et_valider_budget, obtenir_solde from modules.rag_manager import rechercher_contexte_rag, auto_apprendre_rag from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver +from modules.ai_media import router_media # <--- IMPORT DU NOUVEAU MODULE def check_and_update_resource_quota(utilisateur_nom, role_utilisateur): quota_file = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs/quotas_ressources.json" @@ -45,7 +46,9 @@ async def handle_chat_request(request, session): mode_payant = form.get("mode_payant") == "on" mode_multi_ia = form.get("mode_multi_ia") == "on" - mode_education = form.get("mode_education") == "on" # INTERCEPTION DU MODE EDU + mode_education = form.get("mode_education") == "on" + media_mode = form.get("media_mode", "texte") + validation_acquise = form.get("validation_acquise") == "true" choix_utilisateur = form.get("choix_utilisateur", "aucun") @@ -81,69 +84,75 @@ async def handle_chat_request(request, session): file_summary.append(f"\n--- Fichier : {f.filename} ---\n{content.decode('utf-8', errors='ignore')}") liste_fichiers_str = ", ".join(file_names) if file_names else "Aucun fichier" - log_event("INFO", "SYSTEM", f"Requête reçue (Edu: {mode_education} / Payant: {mode_payant})") + log_event("INFO", "SYSTEM", f"Requête reçue (Mode: {media_mode.upper()} / Payant: {mode_payant})") - sec_prompt = f"""Tu es l'Orchestrateur système. Analyse la requête et choisis le meilleur fournisseur IA...""" - try: - texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt) - decision = nettoyer_decision_router(texte_routeur) - except: - decision = {"intention": "Analyse", "fournisseur": "openrouter_chat", "theme_rag": "aucun"} + # --- AIGUILLAGE MULTIMÉDIA --- + if media_mode in ["image", "video"]: + reponse, couleur, fournisseur_nom, modele, trace_route = router_media(prompt, type_media=media_mode, premium=mode_payant, fichiers=file_names) + intention = f"Génération {media_mode.capitalize()}" + thematique_rag = "aucun" + + # --- AIGUILLAGE TEXTE (CLASSIQUE) --- + else: + sec_prompt = f"""Tu es l'Orchestrateur système. Analyse la requête et choisis le meilleur fournisseur IA...""" + try: + texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt) + decision = nettoyer_decision_router(texte_routeur) + except: + decision = {"intention": "Analyse", "fournisseur": "openrouter_chat", "theme_rag": "aucun"} + + intention = decision.get("intention", "Analyse") + fournisseur = decision.get("fournisseur", "openrouter_chat").lower() + thematique_rag = decision.get("theme_rag", "aucun").lower() + + if mode_payant or mode_multi_ia: + fournisseur = "openrouter_code" + intention += " (API Premium/Multi)" + + if file_names: + fournisseur = "gemini" + + if mode_education: + fournisseur = "openrouter_chat" + thematique_rag = "Education_Nationale" + intention = "Éducation Nationale " + ("(Payant)" if mode_payant else "(IA Gratuite)") + instruction_edu = "[SYSTÈME] RÈGLE ÉDUCATION NATIONALE : Agis comme un expert de l'Éducation Nationale française. Pédagogie stricte, respect du B.O.\n\n" + prompt = instruction_edu + prompt + + contexte_rag = "" + if thematique_rag != "aucun": + contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag) + + urls = re.findall(r"https?://[^\s]+", prompt) + if any(m in prompt.lower() for m in ["cherche", "recherche", "sur le web"]): + urls.extend(rechercher_sur_google(prompt)) + web_text = extraire_contenu_urls(list(set(urls))) if urls else "" - intention = decision.get("intention", "Analyse") - fournisseur = decision.get("fournisseur", "openrouter_chat").lower() - thematique_rag = decision.get("theme_rag", "aucun").lower() + full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "") + if contexte_rag: full_prompt += f"\n\n[Contexte RAG :\n{contexte_rag}]" + if web_text: full_prompt += f"\n\n[Contexte web :\n{web_text}]" - if mode_payant or mode_multi_ia: - fournisseur = "openrouter_code" - intention += " (API Premium/Multi)" + if "openrouter" in fournisseur: + modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free" + + if mode_payant and not validation_acquise: + from modules.billing_manager import estimer_cout_facture + _, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) + if prix_estime > 0: + return render_popup_paiement(prix_estime, prompt, chat_id, mode_multi_ia, mode_education) - if file_names: - fournisseur = "gemini" + if mode_payant and role_utilisateur.lower() not in ["super admin", "admin", "vip", "famille"]: + from modules.billing_manager import estimer_cout_facture + _, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) + if prix_estime > obtenir_solde(utilisateur_nom): + return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Rechargez votre compte."), style="background:#300; padding:15px;") - # --- LOGIQUE EDUCATION NATIONALE --- - if mode_education: - fournisseur = "openrouter_chat" - thematique_rag = "Education_Nationale" - intention = "Éducation Nationale " + ("(Payant)" if mode_payant else "(IA Gratuite)") - # Marqueur secret pour ai_openrouter.py - instruction_edu = "[SYSTÈME] RÈGLE ÉDUCATION NATIONALE : Agis comme un expert de l'Éducation Nationale française. Pédagogie stricte, respect du B.O.\n\n" - prompt = instruction_edu + prompt - - contexte_rag = "" - if thematique_rag != "aucun": - contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag) - - urls = re.findall(r"https?://[^\s]+", prompt) - if any(m in prompt.lower() for m in ["cherche", "recherche", "sur le web"]): - urls.extend(rechercher_sur_google(prompt)) - web_text = extraire_contenu_urls(list(set(urls))) if urls else "" - - full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "") - if contexte_rag: full_prompt += f"\n\n[Contexte RAG :\n{contexte_rag}]" - if web_text: full_prompt += f"\n\n[Contexte web :\n{web_text}]" - - if "openrouter" in fournisseur: - modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free" + reponse, couleur, fournisseur_nom, modele, trace_route = executer_requete_ia( + fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id + ) - if mode_payant and not validation_acquise: - from modules.billing_manager import estimer_cout_facture - _, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) - if prix_estime > 0: - return render_popup_paiement(prix_estime, prompt, chat_id, mode_multi_ia, mode_education) - - if mode_payant and role_utilisateur.lower() not in ["super admin", "admin", "vip", "famille"]: - from modules.billing_manager import estimer_cout_facture - _, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) - if prix_estime > obtenir_solde(utilisateur_nom): - return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Rechargez votre compte."), style="background:#300; padding:15px;") - - reponse, couleur, fournisseur_nom, modele, trace_route = executer_requete_ia( - fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id - ) - - if "openrouter" in fournisseur_nom.lower() and mode_payant: - verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) + if "openrouter" in fournisseur_nom.lower() and mode_payant: + verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) if thematique_rag != "aucun" and reponse: auto_apprendre_rag(prompt, reponse, thematique_rag) @@ -164,7 +173,7 @@ async def handle_chat_request(request, session): "modele": modele, "couleur": couleur, "execution_time": execution_time, - "trace": trace_route # <--- Ajout de la traçabilité + "trace": trace_route }) save_history(history) diff --git a/modules/model_updater.py b/modules/model_updater.py index 88e30b1..54175a3 100644 --- a/modules/model_updater.py +++ b/modules/model_updater.py @@ -19,7 +19,7 @@ def _do_update(): if resp_local.status_code == 200: log_event("INFO", "UPDATER", "Modèle local Abliterated vérifié/mis à jour.") - # MAJ du catalogue OpenRouter (Gratuits ET Payants) + # MAJ DU CATALOGUE COMPLET OPENROUTER resp_or = requests.get("https://openrouter.ai/api/v1/models", timeout=30) if resp_or.status_code == 200: data = resp_or.json().get("data", []) @@ -28,23 +28,23 @@ def _do_update(): for m in data: pricing = m.get("pricing", {}) - # Gratuits purs + + # S'il est 100% gratuit if pricing.get("prompt") == "0" and pricing.get("completion") == "0": - if "nvidia/nemotron" not in m["id"].lower(): + if "nvidia/nemotron" not in m["id"].lower(): # On garde juste cette exclusion sécurité modeles_gratuits.append(m["id"]) - # Ciblage des payants ultra-performants (Claude & OpenAI) - elif "claude-3.5-sonnet" in m["id"].lower() or "gpt-4o" in m["id"].lower(): + else: + # On intègre TOUS les modèles payants (Texte, Vision, 3D, etc.) modeles_payants.append(m["id"]) with open(CATALOGUE_FILE, "w", encoding="utf-8") as f: json.dump({"free_models": modeles_gratuits, "paid_models": modeles_payants}, f) - log_event("INFO", "UPDATER", f"Catalogue dynamique mis à jour ({len(modeles_gratuits)} gratuits, {len(modeles_payants)} payants).") + log_event("INFO", "UPDATER", f"Catalogue Global mis à jour : {len(modeles_gratuits)} gratuits, {len(modeles_payants)} payants détectés.") except Exception as e: log_event("ERROR", "UPDATER", f"Erreur lors de la MAJ auto : {e}") def trigger_update_now(): - """Déclenchement forcé du scan en cas d'erreur de modèle.""" threading.Thread(target=_do_update, daemon=True).start() def update_models_background(): diff --git a/modules/ui_components.py b/modules/ui_components.py index 47a44f9..883d2d5 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -20,7 +20,6 @@ def render_user_msg(text): def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None, trace=None): time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else "" - # Construction de la trace visuelle si elle existe trace_html = "" if trace and isinstance(trace, list): badges = [Span(t, style="font-size:0.75em; background:#222; padding:2px 6px; border-radius:4px; border:1px solid #444;") for t in trace] @@ -43,8 +42,11 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi cls="msg-actions" ) + # Si le texte contient une image générée (détection basique Markdown ou HTML) + # L'affichage natif s'occupera du rendu via marked.js + return Div( - trace_html, # Affichage du chemin de routage + trace_html, encart_info, Div(text, cls="markdown-raw", style="display:none;"), Div(cls="markdown-rendered", style="line-height: 1.6;"), @@ -77,7 +79,7 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', ''), - msg.get('trace', []) # <--- C'est ici qu'on transmet la trace aux anciens messages ! + msg.get('trace', []) )) if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) @@ -88,6 +90,23 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): .then(response => { if (serverDown && response.ok) { window.location.reload(); } }) .catch(() => { serverDown = true; console.log("Serveur hors ligne..."); }); }, 5000); + + // Script pour gérer l'avertissement de gratuité sur les médias + function checkMediaPaymentWarning() { + const mediaMode = document.querySelector('input[name="media_mode"]:checked').value; + const isPayant = document.getElementById('mode-payant-toggle').checked; + const warningDiv = document.getElementById('media-warning'); + + if (mediaMode === 'video' && !isPayant) { + warningDiv.style.display = 'block'; + warningDiv.innerHTML = '⚠️ La génération vidéo gratuite est très limitée ou inexistante. Cochez "Payant" pour de meilleurs résultats.'; + } else if (mediaMode === 'image' && !isPayant) { + warningDiv.style.display = 'block'; + warningDiv.innerHTML = 'ℹ️ Mode image gratuit : Qualité standard (HuggingFace). Pour la HD, cochez "Payant".'; + } else { + warningDiv.style.display = 'none'; + } + } ''') solde_perso = obtenir_solde(utilisateur_nom) @@ -131,6 +150,15 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): Form( Input(type="hidden", name="chat_id", value=chat_id), Div( + # --- NOUVEAU SÉLECTEUR DE MODE MÉDIA --- + Div( + Label(Input(type="radio", name="media_mode", value="texte", checked=True, onchange="checkMediaPaymentWarning()"), "📝 Texte", cls="media-btn"), + Label(Input(type="radio", name="media_mode", value="image", onchange="checkMediaPaymentWarning()"), "🎨 Image", cls="media-btn"), + Label(Input(type="radio", name="media_mode", value="video", onchange="checkMediaPaymentWarning()"), "🎬 Vidéo", cls="media-btn"), + style="display: flex; gap: 15px; justify-content: center; margin-bottom: 10px;" + ), + Div(id="media-warning", style="display:none; color: #ff9800; font-size: 0.8em; text-align: center; margin-bottom: 10px;"), + Div(id="file-preview-list"), Div( Label("+", _for="file-upload", cls="file-btn"), @@ -140,11 +168,11 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): ), Div( Label(Input(type="checkbox", name="mode_education", id="mode-education-toggle", style="margin-right: 6px; accent-color: #8bc34a; cursor: pointer;"), "🎓 Éducation", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), - Label(Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"), "👑 Payant", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), + Label(Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;", onchange="checkMediaPaymentWarning()"), "👑 Payant", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), Label(Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"), "⚡ Multi-IA", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), cls="options-row" ), cls="input-container-box" - ), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px';", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" + ), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px'; checkMediaPaymentWarning();", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" ), cls="input-area" ), cls="main-content" ) @@ -178,10 +206,6 @@ def render_login_page(error=None): ) def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False, edu_state=False): - """ - Modale de rejet financier. - Version sans système de paiement (en attendant la structure juridique). - """ edu_val = "on" if edu_state else "off" return Div( @@ -193,7 +217,6 @@ def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_stat Div( Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 48%; font-size: 0.85em; border-radius: 8px;"), - # Bouton de secours pour forcer un modèle gratuit local (Ollama) Form( Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id),