From 06285b8d4f282db957e54de4700c02ef1abcdce5 Mon Sep 17 00:00:00 2001 From: Folzo Date: Sat, 1 Aug 2026 19:27:54 +0200 Subject: [PATCH] jean-router: streaming de messages d'attente pendant le WoL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Quand le PC est eteint/en veille, stream immediat de messages - 'PC de jeu se reveille...' → 'Magic packet envoye...' → 'Demarrage X%...' - Une fois pret, forward la vraie reponse en stream - Plus de blocage silencieux, l'utilisateur voit la progression --- jean-router/app.py | 90 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/jean-router/app.py b/jean-router/app.py index 52919e7..1ff74c5 100644 --- a/jean-router/app.py +++ b/jean-router/app.py @@ -13,7 +13,7 @@ import socket from typing import Optional import requests -from flask import Flask, request, Response, jsonify +from flask import Flask, request, Response, jsonify, stream_with_context JEAN_WEB_HOST = os.getenv("JEAN_WEB_HOST", "192.168.1.11") JEAN_WEB_PORT = int(os.getenv("JEAN_WEB_PORT", "8090")) @@ -34,7 +34,7 @@ OLLAMA_UNLOAD_TIMEOUT = int(os.getenv("OLLAMA_UNLOAD_TIMEOUT", "15")) ENABLE_WOL = os.getenv("ENABLE_WOL", "false").lower() == "true" TARGET_MAC = os.getenv("TARGET_MAC", os.getenv("PC_GAMING_MAC", "")) BROADCAST_IP = os.getenv("BROADCAST_IP", "192.168.1.255") -WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "120")) +WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "180")) wol_in_progress = threading.Lock() jean_alive_cache = {"alive": False, "timestamp": 0} @@ -152,6 +152,83 @@ def wait_for_jean(trigger_wol=True): log.error(f"[WoL] Jean ne repond pas apres {WAKE_TIMEOUT}s") +def stream_wake_then_forward(upstream_url, data, headers): + """Stream des messages d'attente pendant le reveil, puis forward la vraie reponse""" + import uuid + chat_id = f"chatcmpl-{uuid.uuid4().hex[:29]}" + + def wake_chunk(content): + return ( + f'data: {{"id":"{chat_id}","object":"chat.completion.chunk",' + f'"created":{int(time.time())},"model":"wake","choices":[' + f'{{"delta":{{"content":"{content} "}},"index":0}}]}}\n\n' + ).encode() + + def generate(): + yield wake_chunk("🖥️ Le PC de jeu se réveille...") + + if not jean_alive() and ENABLE_WOL: + with wol_in_progress: + if not jean_alive(): + log.info("[WoL-Stream] Envoi WoL + attente...") + send_wol() + yield wake_chunk("⚡ Magic packet envoyé, démarrage en cours...") + + deadline = time.time() + WAKE_TIMEOUT + last_pct = 0 + while time.time() < deadline: + if jean_alive(): + jean_alive_cache["timestamp"] = 0 + yield wake_chunk("✅ PC allumé, chargement du modèle...") + break + pct = int((1 - (deadline - time.time()) / WAKE_TIMEOUT) * 100) + if pct >= last_pct + 10: + yield wake_chunk(f"⏳ Démarrage... {min(pct, 95)}%") + last_pct = pct + time.sleep(CHECK_INTERVAL) + else: + yield wake_chunk("❌ Le PC n'a pas répondu, réessayez.") + yield b"data: [DONE]\n\n" + return + + # Attendre que Jean soit vraiment prêt (proxy + modele) + deadline = time.time() + SWITCH_TIMEOUT + while time.time() < deadline: + try: + r = requests.get(f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/health", timeout=2) + if r.ok and r.json().get("status") == "ok": + break + except Exception: + pass + yield wake_chunk(".") + time.sleep(2) + + yield wake_chunk("🚀 Modèle prêt, voici la réponse :\n\n") + + # Forward la vraie requête et streamer + try: + resp = requests.request( + method="POST", url=upstream_url, headers=headers, + json=data, stream=True, timeout=None, + ) + for line in resp.iter_lines(): + if line: + yield line + b"\n" + yield b"data: [DONE]\n\n" + except Exception as e: + yield wake_chunk(f"❌ Erreur: {str(e)[:100]}") + yield b"data: [DONE]\n\n" + + return Response( + stream_with_context(generate()), + content_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + ) + + def get_ollama_models(force_refresh=False): """Récupère la liste des modèles depuis Ollama""" if not ENABLE_OLLAMA: @@ -761,7 +838,14 @@ def chat_completions(): if model_id in jean_preset_ids or model_id in jean_names: internal_id = jean_name_to_id.get(model_id, model_id) - wait_for_jean(trigger_wol=True) + if not jean_alive() and ENABLE_WOL: + log.info("[WoL-Stream] PC down, demarrage stream d'attente...") + fwd_headers = dict(request.headers) + fwd_headers.pop("Host", None) + return stream_wake_then_forward( + f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/chat/completions", + data, fwd_headers + ) if not ensure_vram_for_backend("jean"): return jsonify({"error": "Failed to prepare VRAM for Jean"}), 503