"""Illustrations contextuelles Wikipédia déclenchées APRÈS une réponse Hermes. Pas de modification des prompts Hermes ou de /api/render. Échec silencieux. """ from __future__ import annotations import re import threading import time import unicodedata from urllib.parse import urlencode, quote, urlparse import requests CACHE = {} LOCK = threading.Lock() def _norm(s): s = unicodedata.normalize('NFKD', str(s)).casefold() return re.sub(r'[^a-z0-9 ]+', '', ''.join(c for c in s if not unicodedata.combining(c))) def entity_question(s): text = ' '.join(str(s or '').split()) m = re.match(r'^(?:alfred[ ,.:!?]*)?(?:qui\s+(?:est|etait|était|sont)\s+|c.?est\s+qui\s+)([^?!.]{3,95})', text, re.I) if not m: return None entity = m.group(1).strip(' «»\'".,?!') entity = re.sub(r'\s+(?:au juste|exactement|s.il te plait)$', '', entity, flags=re.I) if len(entity.split()) > 7 or len(entity) < 3: return None return entity def _query_wikipedia(name): # PageImages retourne une illustration publique et l'URL de la page source. params = {'action':'query','generator':'search','gsrsearch':name,'gsrlimit':1, 'prop':'pageimages','piprop':'thumbnail','pithumbsize':500, 'format':'json','formatversion':'2','redirects':'1'} headers = {'User-Agent':'AlfredAssistant/1.0 (personal home assistant; image source: Wikipedia)'} response = requests.get('https://fr.wikipedia.org/w/api.php', params=params, headers=headers, timeout=4, allow_redirects=False) response.raise_for_status() pages = response.json().get('query', {}).get('pages', []) if not pages: return None page = pages[0] title = str(page.get('title','')) if not title or _norm(name) not in _norm(title) and _norm(title) not in _norm(name): return None url = str(page.get('thumbnail', {}).get('source', '')) parsed = urlparse(url) if parsed.scheme != 'https' or parsed.hostname not in ('upload.wikimedia.org','commons.wikimedia.org'): return None return {'title': title, 'image_url':url, 'source_url':'https://fr.wikipedia.org/wiki/'+quote(title.replace(' ','_')), 'credit':'Image : Wikipédia / Wikimedia Commons'} def image_for_question(name): key = _norm(name) with LOCK: old = CACHE.get(key) if old and time.monotonic()-old[0] < 4*3600: return old[1] try: result = _query_wikipedia(name) except (requests.RequestException, ValueError, KeyError): result = None with LOCK: CACHE[key] = (time.monotonic(), result) return result def schedule_context_image(question, answer, socketio, still_current): name = entity_question(question) if not name: return def worker(): info = image_for_question(name) if info and still_current(): info['text'] = str(answer or '')[:500] socketio.emit('alfred_context_image', info) threading.Thread(target=worker, name='alfred-context-image', daemon=True).start()