Spaces:
Running
Running
Download app.py from bsalvan/Valdeuck-final: direct link, hf CLI and curl.
- Browser
- Download file 23.6 kB
-
https://huggingface.co/spaces/bsalvan/Valdeuck-final/resolve/main/app.py
- Command line
-
hf download hf://spaces/bsalvan/Valdeuck-final/app.py
-
curl -L -o app.py https://huggingface.co/spaces/bsalvan/Valdeuck-final/resolve/main/app.py
23.6 kB
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from openai import OpenAI | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| from langchain_chroma import Chroma | |
| import os | |
| import json | |
| import re | |
| import time | |
| import io | |
| import asyncio | |
| import edge_tts | |
| import requests | |
| app = FastAPI(title="Valdeuck Maître Deuck V13") | |
| # ========================= | |
| # VOICE — voix masculine française Microsoft | |
| # ========================= | |
| TTS_VOICE = "fr-FR-RemyMultilingualNeural" | |
| # FREE CHAT MODELS (for valdeuck.fr) | |
| # model="HuggingFaceH4/zephyr-7b-gemma-v0.1", # Modèle Chat natif et gratuit | |
| # model="meta-llama/Llama-3.2-3B-Instruct", # Modèle Chat natif | |
| # ========================= | |
| # HUGGING FACE INFERENCE PROVIDERS | |
| # ========================= | |
| # Dans les Secrets du Space, définir HF_TOKEN. | |
| # HF_CHAT_MODEL peut être changé sans modifier le code. | |
| # :preferred demande au routeur HF le premier fournisseur disponible. | |
| HF_TOKEN_VALUE = ( | |
| os.getenv("HF_TOKEN") | |
| or os.getenv("HUGGINGFACEHUB_API_TOKEN") | |
| or "" | |
| ).strip() | |
| #• Qwen/Qwen2.5-Coder-7B-Instruct (Le choix idéal : très intelligent, rapide, gère parfaitement le français) | |
| #• deepseek-ai/DeepSeek-R1-Distill-Qwen-7B (Excellent modèle de raisonnement logique basé sur l'architecture ouverte de DeepSeek) | |
| #• Qwen/Qwen2.5-Coder-3B-Instruct (Ultra-léger, s'exécute instantanément sur les serveurs CPU de Hugging Face) | |
| ############################# Ne fonctionne plus sur huggingface, version avec abonnement uniquement | |
| #HF_CHAT_MODEL = os.getenv( | |
| # "HF_CHAT_MODEL", | |
| # "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", | |
| #) | |
| # | |
| #if not HF_TOKEN_VALUE: | |
| # print("ERREUR : aucun jeton HF_TOKEN / HUGGINGFACEHUB_API_TOKEN détecté.") | |
| #else: | |
| # print("Jeton Hugging Face détecté :", True) | |
| # print("Modèle configuré :", HF_CHAT_MODEL) | |
| # | |
| #client = OpenAI( | |
| # base_url="https://router.huggingface.co/v1", | |
| # api_key=HF_TOKEN_VALUE or "missing-token" | |
| #) | |
| # | |
| # ========================================================================================== | |
| # CONFIGURATION DE L'IA (Contournement gratuit via Pollinations AI) | |
| # via interface (bibliothèque) openai versus bibliothgèque standard : exclusif avec standard | |
| # ========================================================================================== | |
| # On utilise 'qwen' qui est performant, gratuit et gère parfaitement le français | |
| #HF_CHAT_MODEL = os.getenv("HF_CHAT_MODEL", "qwen") | |
| #print("Jeton d'authentification : Non requis (API Libre)") | |
| #print("Modèle configuré :", HF_CHAT_MODEL) | |
| # CORRECTION : On utilise la racine de l'URL sans le /v1 pour éviter l'erreur 405 | |
| #client = OpenAI( | |
| # base_url="https://pollinations.ai", | |
| # api_key="not-needed" | |
| #) | |
| #=========================================================================================== | |
| #################################################################### | |
| # DEBUG - Liste des modeles disponibles aucune garantie de gratuité | |
| #try: | |
| # models = client.models.list() | |
| # print("=== MODELES DISPONIBLES VIA LE ROUTEUR HF ===") | |
| # for model in models.data: | |
| # print(model.id) | |
| #except Exception as e: | |
| # print("ERREUR LISTE DES MODELES :", repr(e)) | |
| ##################################################################### | |
| ########################## Si Utilisation bibliothèque standard vs openai : exclusif avec openai | |
| HF_CHAT_MODEL = os.getenv("HF_CHAT_MODEL", "openai-fast") | |
| print("Jeton d'authentification : Non requis (API Libre)") | |
| print("Modèle configuré :", HF_CHAT_MODEL) | |
| # Utilisation de l'endpoint OpenAI officiel de Pollinations | |
| client = OpenAI( | |
| base_url="https://text.pollinations.ai/openai", | |
| api_key="not-needed" | |
| ) | |
| # ========================= | |
| # VECTOR DB | |
| # ========================= | |
| embeddings = HuggingFaceEmbeddings( | |
| model_name="intfloat/multilingual-e5-base" | |
| ) | |
| beer_db = Chroma( | |
| persist_directory="vectorstore_beers", | |
| embedding_function=embeddings | |
| ) | |
| knowledge_db = Chroma( | |
| persist_directory="vectorstore_knowledge", | |
| embedding_function=embeddings | |
| ) | |
| beer_retriever = beer_db.as_retriever(search_kwargs={"k": 6}) | |
| knowledge_retriever = knowledge_db.as_retriever(search_kwargs={"k": 3}) | |
| # ========================= | |
| # REQUEST | |
| # ========================= | |
| class Query(BaseModel): | |
| query: str | |
| # ========================= | |
| # CLEAN | |
| # ========================= | |
| def clean(t): | |
| if not t: | |
| return "" | |
| return re.sub(r"\s+", " ", t).strip() | |
| # ========================= | |
| # NETTOYAGE STRICT ANTI-ASTÉRISQUE ET ACCENT US | |
| # ========================= | |
| def clean_tts_text(text: str) -> str: | |
| if not text: | |
| return "" | |
| # 1. Supprime définitivement la syntaxe Markdown (astérisques, tirets, accents graves) | |
| t = text.replace("**", "").replace("*", "").replace("`", "") | |
| # 2. Remplacer les noms anglais par leur équivalent phonétique français | |
| # évite que le moteur vocal Remy ne bascule sur un accent américain forcé | |
| phonetic_map = { | |
| "Bongo Fury": "Bongo Fiouri", | |
| "Concrete Jungle": "Concrète Jeunguel", | |
| "China Girl": "Tchaïna Geurl", | |
| "Red Hot Kampot Pepper": "Red Hot Kampotte Pèpeur", | |
| "Elder Foot Beat": "Elder Foute Bite", | |
| "Melody Nelson": "Mélodi Nelson", | |
| "White Rabbit": "Ouaïte Rabitte", | |
| "IPA": "I P A", | |
| "Stout": "Stoute", | |
| "Sour": "Safeur", | |
| "Craft Beer": "Crafte Bière" | |
| } | |
| for eng, fr in phonetic_map.items(): | |
| t = re.sub(r'\b' + re.escape(eng) + r'\b', fr, t, flags=re.IGNORECASE) | |
| return t | |
| # ========================= | |
| # TTS ENDPOINT SÉCURISÉ CONTRE LES ERREURS RÉSEAU | |
| # Génère un fichier audio MP3 côté serveur avec edge-tts | |
| # ========================= | |
| async def tts(text: str): | |
| try: | |
| # On applique le filtre anti-astérisque et phonétique avant l'envoi de la voix | |
| text_clean = clean_tts_text(text) | |
| if not text_clean.strip(): | |
| return StreamingResponse(io.BytesIO(), media_type="audio/mpeg") | |
| communicate = edge_tts.Communicate(text_clean, TTS_VOICE) | |
| buf = io.BytesIO() | |
| # Flux sécurisé asynchrone par morceaux (evite les coupures de proxy) | |
| async for chunk in communicate.stream(): | |
| if chunk["type"] == "audio": | |
| buf.write(chunk["data"]) | |
| buf.seek(0) | |
| # Validation du contenu généré | |
| if buf.getbuffer().nbytes == 0: | |
| return StreamingResponse(io.BytesIO(), media_type="audio/mpeg") | |
| return StreamingResponse(buf, media_type="audio/mpeg") | |
| except Exception as e: | |
| # Capture l'erreur réseau proprement sans faire crasher l'interface utilisateur | |
| print(f"❌ Erreur réseau interceptée sur le module Voix (edge-tts): {e}") | |
| return StreamingResponse(io.BytesIO(), media_type="audio/mpeg") | |
| # ========================= | |
| # UI | |
| # ========================= | |
| def home(): | |
| return """<!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Maître Deuck</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --orange: #ff6b35; | |
| --dark: #0d0d0d; | |
| --card: #1a1a1a; | |
| --border: #2a2a2a; | |
| --text: #f0e6d3; | |
| --muted: #777; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--dark); | |
| color: var(--text); | |
| font-family: 'Lato', sans-serif; | |
| font-size: 13px; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| } | |
| header { | |
| text-align: center; | |
| padding: 14px 12px 10px; | |
| border-bottom: 1px solid var(--border); | |
| background: #111; | |
| } | |
| header .logo { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| color: var(--orange); | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| } | |
| header .sub { | |
| font-size: 0.65rem; | |
| color: var(--muted); | |
| letter-spacing: 3px; | |
| text-transform: uppercase; | |
| margin-top: 2px; | |
| } | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 12px; | |
| gap: 10px; | |
| overflow-y: auto; | |
| } | |
| .welcome-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--orange); | |
| border-radius: 8px; | |
| padding: 12px 14px; | |
| animation: fadeIn 0.6s ease; | |
| } | |
| .welcome-card .name { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 0.85rem; | |
| color: var(--orange); | |
| font-weight: 700; | |
| margin-bottom: 4px; | |
| } | |
| .welcome-card .msg { | |
| font-size: 0.8rem; | |
| color: var(--text); | |
| line-height: 1.5; | |
| } | |
| #chat { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .bubble { | |
| max-width: 88%; | |
| padding: 9px 13px; | |
| border-radius: 10px; | |
| font-size: 0.8rem; | |
| line-height: 1.5; | |
| animation: slideUp 0.25s ease; | |
| } | |
| .bubble.user { | |
| align-self: flex-end; | |
| background: var(--orange); | |
| color: #fff; | |
| border-bottom-right-radius: 3px; | |
| } | |
| .bubble.bot { | |
| align-self: flex-start; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| border-bottom-left-radius: 3px; | |
| } | |
| .bubble.bot .bubble-name { | |
| font-size: 0.65rem; | |
| color: var(--orange); | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .thinking { | |
| align-self: flex-start; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| border-bottom-left-radius: 3px; | |
| padding: 10px 14px; | |
| display: none; | |
| } | |
| .dots span { | |
| display: inline-block; | |
| width: 6px; height: 6px; | |
| background: var(--orange); | |
| border-radius: 50%; | |
| margin: 0 2px; | |
| animation: bounce 1.2s infinite; | |
| } | |
| .dots span:nth-child(2) { animation-delay: 0.2s; } | |
| .dots span:nth-child(3) { animation-delay: 0.4s; } | |
| .input-zone { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 10px 12px; | |
| display: flex; | |
| gap: 8px; | |
| align-items: flex-end; | |
| } | |
| textarea { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--text); | |
| font-family: 'Lato', sans-serif; | |
| font-size: 0.8rem; | |
| resize: none; | |
| min-height: 36px; | |
| max-height: 90px; | |
| line-height: 1.4; | |
| } | |
| textarea::placeholder { color: var(--muted); } | |
| #btn-mic { | |
| width: 34px; height: 34px; | |
| border-radius: 50%; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--muted); | |
| cursor: pointer; | |
| font-size: 1rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| flex-shrink: 0; | |
| } | |
| #btn-mic:hover { border-color: var(--orange); color: var(--orange); } | |
| #btn-mic.active { background: var(--orange); border-color: var(--orange); color: #fff; animation: pulse 1s infinite; } | |
| #btn-send { | |
| padding: 8px 18px; | |
| background: var(--orange); | |
| border: none; | |
| border-radius: 6px; | |
| color: #fff; | |
| font-family: 'Lato', sans-serif; | |
| font-size: 0.78rem; | |
| font-weight: 700; | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| flex-shrink: 0; | |
| height: 34px; | |
| } | |
| #btn-send:hover { background: #e05520; } | |
| #btn-send:disabled { background: #444; cursor: not-allowed; } | |
| @keyframes fadeIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} } | |
| @keyframes slideUp { from{opacity:0;transform:translateY(5px)} to{opacity:1;transform:translateY(0)} } | |
| @keyframes bounce { 0%,80%,100%{transform:scale(0)} 40%{transform:scale(1)} } | |
| @keyframes pulse { 0%,100%{box-shadow:0 0 0 0 rgba(255,107,53,.4)} 50%{box-shadow:0 0 0 6px rgba(255,107,53,0)} } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">🍺 Valdeuck</div> | |
| <div class="sub">Bincherie Fabrik — Salon-de-Provence</div> | |
| </header> | |
| <main> | |
| <div class="welcome-card"> | |
| <div class="name">Maître Deuck</div> | |
| <div class="msg"> | |
| Bonjour ! Je suis Maître Deuck, l'expert brassicole de la Valdeuck.<br> | |
| Comment puis-je vous aider ? | |
| </div> | |
| </div> | |
| <div id="chat"></div> | |
| <div class="thinking" id="thinking"> | |
| <div class="dots"><span></span><span></span><span></span></div> | |
| </div> | |
| </main> | |
| <div style="padding:8px 12px 12px; background:var(--dark); border-top:1px solid var(--border);"> | |
| <div class="input-zone"> | |
| <textarea | |
| id="input" | |
| placeholder="Votre question… ou 🎤 pour parler" | |
| rows="1" | |
| ></textarea> | |
| <button id="btn-mic" title="Parler">🎤</button> | |
| <button id="btn-send">Envoyer</button> | |
| </div> | |
| </div> | |
| <script> | |
| let currentAudio = null; | |
| async function speak(text) { | |
| try { | |
| if (currentAudio) { | |
| currentAudio.pause(); | |
| currentAudio = null; | |
| } | |
| const url = "/tts?text=" + encodeURIComponent(text); | |
| const audio = new Audio(url); | |
| currentAudio = audio; | |
| await audio.play(); | |
| } catch (e) { | |
| console.warn("TTS playback error:", e); | |
| } | |
| } | |
| window.addEventListener("load", () => { | |
| setTimeout(() => speak(WELCOME), 700); | |
| }); | |
| function addBubble(text, who) { | |
| const chat = document.getElementById("chat"); | |
| const div = document.createElement("div"); | |
| div.className = `bubble ${who}`; | |
| if (who === "bot") { | |
| div.innerHTML = `<div class="bubble-name">Maître Deuck</div>${text}`; | |
| } else { | |
| div.textContent = text; | |
| } | |
| chat.appendChild(div); | |
| div.scrollIntoView({ behavior: "smooth", block: "end" }); | |
| } | |
| async function send(queryText) { | |
| const input = document.getElementById("input"); | |
| const btnSend = document.getElementById("btn-send"); | |
| const thinking = document.getElementById("thinking"); | |
| const q = (queryText || input.value).trim(); | |
| if (!q) return; | |
| addBubble(q, "user"); | |
| input.value = ""; | |
| btnSend.disabled = true; | |
| thinking.style.display = "block"; | |
| thinking.scrollIntoView({ behavior: "smooth", block: "end" }); | |
| try { | |
| const res = await fetch("/ask", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ query: q }) | |
| }); | |
| const data = await res.json(); | |
| const answer = data.response || "Information non disponible."; | |
| thinking.style.display = "none"; | |
| addBubble(answer, "bot"); | |
| speak(answer); | |
| } catch { | |
| thinking.style.display = "none"; | |
| addBubble("Souci technique… Reformulez votre question. 🍺", "bot"); | |
| } | |
| btnSend.disabled = false; | |
| } | |
| document.getElementById("btn-send").addEventListener("click", () => send()); | |
| document.getElementById("input").addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } | |
| }); | |
| let recognition = null; | |
| document.getElementById("btn-mic").addEventListener("click", async () => { | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SR) { | |
| alert("Reconnaissance vocale non supportée sur ce navigateur (utilisez Chrome)."); | |
| return; | |
| } | |
| if (recognition) { | |
| recognition.stop(); | |
| return; | |
| } | |
| try { | |
| await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| } catch (err) { | |
| alert("Accès au microphone refusé. Autorisez le micro dans les paramètres du navigateur."); | |
| return; | |
| } | |
| const btn = document.getElementById("btn-mic"); | |
| const input = document.getElementById("input"); | |
| recognition = new SR(); | |
| recognition.lang = "fr-FR"; | |
| recognition.interimResults = true; | |
| recognition.maxAlternatives = 1; | |
| recognition.continuous = false; | |
| btn.classList.add("active"); | |
| input.value = ""; | |
| input.placeholder = "🎤 Parlez maintenant…"; | |
| recognition.onresult = e => { | |
| let interim = ""; | |
| let final = ""; | |
| for (let i = e.resultIndex; i < e.results.length; i++) { | |
| const t = e.results[i][0].transcript; | |
| if (e.results[i].isFinal) final += t; | |
| else interim += t; | |
| } | |
| input.value = final || interim; | |
| if (final) { | |
| setTimeout(() => { | |
| input.placeholder = "Votre question… ou 🎤 pour parler"; | |
| send(final.trim()); | |
| }, 400); | |
| } | |
| }; | |
| recognition.onerror = e => { | |
| console.error("Mic error:", e.error); | |
| btn.classList.remove("active"); | |
| input.placeholder = "Votre question… ou 🎤 pour parler"; | |
| recognition = null; | |
| if (e.error === "not-allowed") alert("Microphone non autorisé."); | |
| if (e.error === "no-speech") input.value = ""; | |
| if (e.error === "network") alert("Erreur réseau sur la reconnaissance vocale."); | |
| }; | |
| recognition.onend = () => { | |
| btn.classList.remove("active"); | |
| input.placeholder = "Votre question… ou 🎤 pour parler"; | |
| recognition = null; | |
| }; | |
| recognition.start(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ========================= | |
| # HEALTH - | |
| # ========================= | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "beer": beer_db._collection.count(), | |
| "knowledge": knowledge_db._collection.count(), | |
| "tts_voice": TTS_VOICE, | |
| "time": time.time() | |
| } | |
| # ========================= | |
| # ASK | |
| # ========================= | |
| # Si openai sans Pollinations | |
| #@app.post("/ask") | |
| #def ask(q: Query): | |
| # try: | |
| # beer_docs = beer_retriever.invoke(q.query) | |
| # know_docs = knowledge_retriever.invoke(q.query) | |
| # context = "\n".join( | |
| # clean(d.page_content) for d in beer_docs + know_docs | |
| # ) | |
| # system = """Tu es Maître Deuck, expert de la Valdeuck. | |
| #Règles : réponds en 2-3 phrases maximum. Sois direct et précis. | |
| #Si l'info est absente du contexte : "Information non disponible." | |
| #Pas de marques externes. Français uniquement. | |
| #Remplace systématiquement "alc" et "d'alc" par "alcool" et "d'alcool" dans tes réponses.""" | |
| # user = f"CONTEXTE :\n{context}\n\nQUESTION : {q.query}" | |
| # if not HF_TOKEN_VALUE: | |
| # raise RuntimeError( | |
| # "Jeton absent : configurez HF_TOKEN dans les Secrets du Space." | |
| # ) | |
| # res = client.chat.completions.create( | |
| # model=HF_CHAT_MODEL, | |
| # messages=[ | |
| # {"role": "system", "content": system}, | |
| # {"role": "user", "content": user} | |
| # ], | |
| # temperature=0.2, | |
| # max_tokens=300 | |
| # ) | |
| # answer = (res.choices[0].message.content or "").strip() | |
| # if not answer: | |
| # raise RuntimeError("Le fournisseur a renvoyé une réponse vide.") | |
| # answer = answer.replace("vol d'alc/", "vol d'alcool/") | |
| # answer = answer.replace("% alc", "% alcool") | |
| # answer = answer.replace("% d'alc", "% d'alcool") | |
| # answer = answer.replace(" alc.", " alcool") | |
| # answer = answer.replace(" alc/", " alcool/") | |
| # answer = answer.replace("Valdeuck Bincherie Fabrik", "Valdeuck") | |
| # return {"response": answer} | |
| # except Exception as e: | |
| # print(f"❌ Erreur critique dans /ask ({type(e).__name__}) : {str(e)}") | |
| # return { | |
| # "response": ( | |
| # "Maître Deuck est momentanément indisponible. " | |
| # "Réessayez dans quelques instants." | |
| # ) | |
| # } | |
| def ask(q: Query): | |
| try: | |
| beer_docs = beer_retriever.invoke(q.query) | |
| know_docs = knowledge_retriever.invoke(q.query) | |
| context = "\n".join( | |
| clean(d.page_content) for d in beer_docs + know_docs | |
| ) | |
| system = """Tu es Maître Deuck, expert de la Valdeuck. | |
| Règles : réponds en 2-3 phrases maximum. Sois direct et précis. | |
| Si l'info est absente du contexte : "Information non disponible." | |
| Pas de marques externes. Français uniquement. | |
| Remplace systématiquement "alc" et "d'alc" par "alcool" et "d'alcool" dans tes réponses.""" | |
| user = f"CONTEXTE :\n{context}\n\nQUESTION : {q.query}" | |
| # Appel direct à Pollinations.ai via son endpoint text compatible OpenAI | |
| url = "https://text.pollinations.ai/" | |
| payload = { | |
| "messages": [ | |
| {"role": "system", "content": system}, | |
| {"role": "user", "content": user} | |
| ], | |
| "model": HF_CHAT_MODEL, | |
| "json": False | |
| } | |
| print(f"Envoi de la requête à Pollinations pour : {q.query}") | |
| res = requests.post(url, json=payload, timeout=30) | |
| if res.status_code == 200: | |
| answer = res.text.strip() | |
| print("Réponse reçue avec succès !") | |
| else: | |
| raise Exception(f"Erreur serveur Pollinations code {res.status_code}: {res.text}") | |
| if not answer: | |
| raise RuntimeError("Le fournisseur a renvoyé une réponse vide.") | |
| # Post-traitements de nettoyage | |
| answer = answer.replace("vol d'alc/", "vol d'alcool/") | |
| answer = answer.replace("% alc", "% alcool") | |
| answer = answer.replace("% d'alc", "% d'alcool") | |
| answer = answer.replace(" alc.", " alcool") | |
| answer = answer.replace(" alc/", " alcool/") | |
| answer = answer.replace("Valdeuck Bincherie Fabrik", "Valdeuck") | |
| return {"response": answer} | |
| except Exception as e: | |
| print(f"❌ Erreur critique dans /ask ({type(e).__name__}) : {str(e)}") | |
| return { | |
| "response": ( | |
| "Maître Deuck est momentanément indisponible. " | |
| "Réessayez dans quelques instants." | |
| ) | |
| } |