Spaces:
Runtime error
Runtime error
| # file: app.py | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union | |
| # ---- Gradio<5 + hub>=1.0 shim (prevents HfFolder import error) ---- | |
| try: | |
| import huggingface_hub as _hub # noqa: F401 | |
| if not hasattr(_hub, "HfFolder"): | |
| class _HfFolder: | |
| def get_token() -> Optional[str]: | |
| return ( | |
| os.getenv("HUGGINGFACEHUB_API_TOKEN") | |
| or os.getenv("HF_TOKEN") | |
| or os.getenv("HF_HOME_TOKEN") | |
| ) | |
| _hub.HfFolder = _HfFolder # type: ignore[attr-defined] | |
| if not hasattr(_hub, "whoami"): | |
| def _whoami(token: Optional[str] = None) -> Dict[str, str]: | |
| return {} | |
| _hub.whoami = _whoami # type: ignore[attr-defined] | |
| except Exception: | |
| pass | |
| import httpx | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient, __version__ as _hub_ver | |
| # Optional: show versions once (helps debugging in hosted envs) | |
| try: | |
| import importlib.metadata as _md | |
| _gr_ver = _md.version("gradio") | |
| print(f"[startup] gradio={_gr_ver} huggingface_hub={_hub_ver}", file=sys.stderr) | |
| except Exception: | |
| pass | |
| ROUTER_BASE = "https://router.huggingface.co" | |
| # ---- CSS (FIXED: do not override .gradio-container layout; make panel scroll correctly) ---- | |
| css = """ | |
| body { | |
| background-image: url('https://cdn-uploads.huggingface.co/production/uploads/67351c643fe51cb1aa28f2e5/YcsJnPk8HJvXiB5WkVmf1.jpeg'); | |
| background-size: cover; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| } | |
| /* ✅ FIX: Do NOT set display:flex / justify-content on .gradio-container */ | |
| .gradio-container { | |
| min-height: 100vh; | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| } | |
| #title-container { | |
| background-color: rgba(255, 255, 255, 0.85); | |
| border-radius: 16px; | |
| padding: 1.5rem 2rem; | |
| margin: 2rem 0; | |
| width: fit-content; | |
| max-width: 500px; | |
| text-align: left; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| margin-left: 0rem; | |
| } | |
| #title-container h1 { | |
| color: #222 !important; | |
| font-size: 4rem; | |
| font-family: 'Noto Sans JP', sans-serif; | |
| margin: 0; | |
| } | |
| #title-container .subtitle { | |
| font-size: 1.1rem; | |
| font-family: 'Noto Sans', sans-serif; | |
| color: #222 !important; | |
| margin-top: .5rem; | |
| margin-bottom: 0; | |
| width: 100%; | |
| display: block; | |
| } | |
| /* ✅ FIX: Make the chat panel a vertical flex container and let the chatbot scroll */ | |
| #chat-panel { | |
| background-color: rgba(255,255,255,.85); | |
| padding: 2rem; | |
| border-radius: 12px; | |
| width: 100%; | |
| max-width: 700px; | |
| height: 70vh; | |
| box-shadow: 0 0 12px rgba(0,0,0,.3); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| overflow: hidden; /* important */ | |
| } | |
| /* ✅ FIX: chat transcript scrolls */ | |
| #chat-panel .chatbot { | |
| flex: 1; | |
| overflow-y: auto; | |
| min-height: 0; /* crucial for flex children scrolling */ | |
| } | |
| .gradio-container .chatbot h1 { | |
| color: var(--custom-title-color) !important; | |
| font-family: 'Noto Sans', serif !important; | |
| font-size: 5rem !important; | |
| font-weight: bold !important; | |
| text-align: center !important; | |
| margin-bottom: 1.5rem !important; | |
| width: 100%; | |
| } | |
| """ | |
| # ---- Prompting ---- | |
| def level_to_prompt(level: str) -> str: | |
| return { | |
| "A1": "You are a friendly French tutor. Focus on the user's specific question. Use simple French and explain in English. If helpful, you may include word origins or cultural notes, but avoid unrelated tangents and voice features.", | |
| "A2": "You are a patient French tutor. Respond to the user's question clearly. You may include brief relevant background such as word origin, common mistakes, or cultural usage — but only if directly related to the question. Do not mention or suggest voice interaction.", | |
| "B1": "You are a helpful French tutor. Use mostly French and minimal English. You can add short on-topic insights (like grammar tips or usage context) but avoid unrelated vocabulary or tools.", | |
| "B2": "You are a French tutor. Respond primarily in French and include only concise, relevant elaborations. Avoid suggesting voice interaction or unrelated content.", | |
| "C1": "You are a native French tutor. Use fluent French and address only what was asked, but you may include brief cultural or historical context if directly relevant.", | |
| "C2": "You are a French language professor. Use sophisticated French to answer only the question. You may include historical or linguistic nuance but avoid speculation or tool suggestions.", | |
| }.get(level, "You are a helpful French tutor.") | |
| # ---- Types & shaping ---- | |
| GradioMessage = Dict[str, str] | |
| HistoryItem = Union[Tuple[str, str], GradioMessage] | |
| History = Optional[Sequence[HistoryItem]] | |
| def _history_to_messages(history: History) -> List[GradioMessage]: | |
| msgs: List[GradioMessage] = [] | |
| if not history: | |
| return msgs | |
| for item in history: | |
| if isinstance(item, tuple) and len(item) == 2: | |
| u, a = item | |
| if u: | |
| msgs.append({"role": "user", "content": u}) | |
| if a: | |
| msgs.append({"role": "assistant", "content": a}) | |
| elif isinstance(item, dict) and "role" in item and "content" in item: | |
| if item["role"] != "system": | |
| msgs.append({"role": item["role"], "content": item["content"]}) | |
| return msgs | |
| def coerce_to_messages( | |
| message: Union[str, List[GradioMessage]], | |
| history: History, | |
| system_text: str | |
| ) -> List[GradioMessage]: | |
| if isinstance(message, list) and all(isinstance(m, dict) and "role" in m for m in message): | |
| cleaned = [m for m in message if m.get("role") != "system"] | |
| return [{"role": "system", "content": system_text}, *cleaned] | |
| msgs = [{"role": "system", "content": system_text}, *_history_to_messages(history)] | |
| if isinstance(message, str) and message: | |
| msgs.append({"role": "user", "content": message}) | |
| return msgs | |
| # ---- Token & model helpers ---- | |
| def get_token(user_token: Optional[str]) -> str: | |
| tok = user_token or os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN") | |
| if not tok: | |
| raise RuntimeError("Missing Hugging Face token (needs 'Make calls to Inference Providers').") | |
| return tok | |
| def list_models(token: str) -> List[str]: | |
| r = httpx.get( | |
| f"{ROUTER_BASE}/v1/models", | |
| headers={"Authorization": f"Bearer {token}"}, | |
| timeout=30 | |
| ) | |
| r.raise_for_status() | |
| data = r.json() | |
| return [m["id"] for m in data.get("data", []) if isinstance(m, dict) and "id" in m] | |
| def choose_model(token: str, requested: Optional[str]) -> tuple[str, Optional[str]]: | |
| prefs = [ | |
| "meta-llama/Llama-3.1-8B-Instruct", | |
| "mistralai/Mixtral-8x7B-Instruct-v0.1", | |
| "Qwen/Qwen2.5-7B-Instruct", | |
| "deepseek-ai/DeepSeek-R1", | |
| ] | |
| note: Optional[str] = None | |
| try: | |
| available = list_models(token) | |
| except Exception: | |
| return (requested or "meta-llama/Llama-3.1-8B-Instruct"), None | |
| if requested and requested in available: | |
| return requested, None | |
| for mid in prefs: | |
| if mid in available: | |
| if requested and requested not in available: | |
| note = f"Requested model '{requested}' not available. Using '{mid}'." | |
| elif not requested: | |
| note = f"No model specified. Using '{mid}'." | |
| return mid, note | |
| if available: | |
| chosen = available[0] | |
| note = ( | |
| f"Requested '{requested}' not available. Using '{chosen}'." | |
| if requested else f"No model specified. Using '{chosen}'." | |
| ) | |
| return chosen, note | |
| return (requested or "meta-llama/Llama-3.1-8B-Instruct"), "No models visible to your token on Inference Providers." | |
| # ---- Chunk extractor (robust against empty choices / non-content deltas) ---- | |
| def _extract_piece(chunk) -> Optional[str]: | |
| """Return incremental text; tolerate empty/non-content chunks.""" | |
| try: | |
| choices = getattr(chunk, "choices", None) | |
| if choices is None and isinstance(chunk, dict): | |
| choices = chunk.get("choices") | |
| if not choices: | |
| return None | |
| choice0 = choices[0] if len(choices) > 0 else None | |
| if not choice0: | |
| return None | |
| delta = getattr(choice0, "delta", None) | |
| if delta is None and isinstance(choice0, dict): | |
| delta = choice0.get("delta") | |
| if not delta: | |
| return None | |
| piece = getattr(delta, "content", None) | |
| if piece is None and isinstance(delta, dict): | |
| piece = delta.get("content") | |
| return piece | |
| except Exception: | |
| return None | |
| # ---- Streaming via HF Router (OpenAI-style chat.completions) ---- | |
| def stream_chat( | |
| client: InferenceClient, | |
| *, | |
| model_id: str, | |
| messages: List[GradioMessage], | |
| max_tokens: int, | |
| temperature: float, | |
| top_p: float | |
| ) -> Iterable[str]: | |
| buf = "" | |
| try: | |
| events = client.chat.completions.create( | |
| model=model_id, | |
| messages=messages, | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| top_p=top_p, | |
| stream=True, | |
| ) | |
| for ev in events: | |
| piece = _extract_piece(ev) | |
| if piece: | |
| buf += piece | |
| yield buf | |
| except Exception as e: | |
| # keep as a single stream output | |
| yield f"Désolé ! {str(e)}" | |
| # ---- Gradio callback (FIXED: single continuous stream; no separate note yield) ---- | |
| def respond( | |
| message: Union[str, List[GradioMessage]], | |
| history: History, | |
| user_level: str, | |
| max_tokens: int, | |
| temperature: float, | |
| top_p: float, | |
| model_id: str, | |
| hf_token: Optional[str] | |
| ): | |
| system_message = level_to_prompt(user_level) | |
| msgs = coerce_to_messages(message, history, system_message) | |
| max_tokens = int(max(1, min(int(max_tokens), 4096))) | |
| temperature = float(max(0.0, min(float(temperature), 4.0))) | |
| top_p = float(max(0.0, min(float(top_p), 1.0))) | |
| try: | |
| token = get_token(hf_token) | |
| except Exception as e: | |
| yield f"Désolé ! {e}" | |
| return | |
| chosen_model, note = choose_model(token, (model_id or "").strip() or None) | |
| prefix = f"ℹ️ {note}\n\n" if note else "" | |
| client = InferenceClient(api_key=token) | |
| first = True | |
| for chunk in stream_chat( | |
| client, | |
| model_id=chosen_model, | |
| messages=msgs, | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| top_p=top_p | |
| ): | |
| if first and prefix: | |
| yield prefix + chunk | |
| first = False | |
| else: | |
| yield chunk | |
| # ---- Extra UI action ---- | |
| def list_models_ui(hf_token: Optional[str]) -> str: | |
| try: | |
| token = get_token(hf_token) | |
| models = list_models(token) | |
| if not models: | |
| return "No models visible to your token on Inference Providers." | |
| return "\n".join(models) | |
| except Exception as e: | |
| return f"Error listing models: {e}" | |
| # ---- UI ---- | |
| with gr.Blocks(css=css) as demo: | |
| gr.HTML(""" | |
| <div id="title-container"> | |
| <h1>LE PROFESSEUR</h1> | |
| <p class="subtitle">French Tutor</p> | |
| </div> | |
| """) | |
| with gr.Column(elem_id="chat-panel"): | |
| with gr.Accordion("Advanced Settings", open=False): | |
| user_level = gr.Dropdown( | |
| choices=["A1","A2","B1","B2","C1","C2"], | |
| value="A1", | |
| label="Your French Level (CEFR)" | |
| ) | |
| max_tokens = gr.Slider(1, 4096, value=400, step=1, label="Response Length") | |
| temperature = gr.Slider(0.0, 4.0, value=0.5, step=0.1, label="Creativity") | |
| top_p = gr.Slider(0.0, 1.0, value=0.85, step=0.05, label="Dynamic Text Sampling") | |
| model_id = gr.Textbox( | |
| label="Model (HF Inference Providers)", | |
| value="meta-llama/Llama-3.1-8B-Instruct", | |
| placeholder="Leave blank to auto-pick; e.g. mistralai/Mixtral-8x7B-Instruct-v0.1" | |
| ) | |
| hf_token = gr.Textbox( | |
| label="Hugging Face Token", | |
| type="password", | |
| placeholder="hf_xxx — must allow 'Make calls to Inference Providers'" | |
| ) | |
| with gr.Row(): | |
| list_btn = gr.Button("List models (visible to my token)") | |
| models_box = gr.Textbox(label="Available models", lines=10) | |
| list_btn.click(list_models_ui, inputs=[hf_token], outputs=[models_box]) | |
| gr.ChatInterface( | |
| fn=respond, | |
| additional_inputs=[user_level, max_tokens, temperature, top_p, model_id, hf_token], | |
| type="messages", | |
| ) | |
| # ✅ Recommended on Spaces for stable streaming behind proxies | |
| demo = demo.queue() | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) | |