| |
| """ |
| Sahon AI - Gradio Bootstrap for Transformers.js (Node.js) |
| ========================================================= |
| This app starts a Node.js child process that runs Transformers.js. |
| Gradio acts as the UI proxy layer. |
| """ |
|
|
| import os |
| import sys |
| import json |
| import time |
| import subprocess |
| import threading |
| import urllib.request |
| import urllib.error |
| import atexit |
| import signal |
|
|
| |
| |
| |
| |
|
|
| try: |
| from spaces import GPU as spaces_gpu |
|
|
| @spaces_gpu |
| def _zerogpu_placeholder(): |
| """Satisfy ZeroGPU requirement. GPU not actually used.""" |
| return True |
|
|
| HAS_SPACES = True |
| print("[Sahon] β
ZeroGPU compatible (placeholder registered)") |
| except ImportError: |
| HAS_SPACES = False |
| print("[Sahon] β οΈ spaces module not available") |
|
|
| |
| NODE_SERVER_PORT = 8888 |
| NODE_SERVER_URL = f"http://127.0.0.1:{NODE_SERVER_PORT}" |
| NODE_SCRIPT = "server.mjs" |
|
|
| |
| node_process = None |
|
|
| def start_node_server(): |
| """Start the Node.js Transformers.js server as a subprocess.""" |
| global node_process |
| |
| |
| print("[Sahon] Installing npm packages...") |
| npm_install = subprocess.run( |
| ["npm", "install"], |
| capture_output=True, text=True, timeout=120 |
| ) |
| if npm_install.returncode != 0: |
| print(f"[Sahon] npm install stderr: {npm_install.stderr}") |
| |
| |
| print(f"[Sahon] Starting Node.js server on port {NODE_SERVER_PORT}...") |
| node_process = subprocess.Popen( |
| ["node", NODE_SCRIPT], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True, |
| bufsize=1, |
| ) |
| |
| |
| def log_output(): |
| for line in node_process.stdout: |
| print(f"[Node.js] {line}", end="") |
| |
| threading.Thread(target=log_output, daemon=True).start() |
| |
| print("[Sahon] Waiting for Node.js server to start...") |
| time.sleep(2) |
| |
| |
| for i in range(30): |
| try: |
| req = urllib.request.Request(f"{NODE_SERVER_URL}/health") |
| resp = urllib.request.urlopen(req, timeout=5) |
| data = json.loads(resp.read()) |
| if data.get("model_ready"): |
| print("[Sahon] β
Node.js server ready!") |
| return |
| print(f"[Sahon] Waiting for model... attempt {i+1}") |
| except Exception as e: |
| print(f"[Sahon] Waiting for server... attempt {i+1} ({e})") |
| time.sleep(5) |
| |
| print("[Sahon] β οΈ Node.js server started but model may not be ready yet") |
|
|
| def stop_node_server(): |
| """Stop the Node.js process.""" |
| global node_process |
| if node_process: |
| print("[Sahon] Stopping Node.js server...") |
| node_process.terminate() |
| try: |
| node_process.wait(timeout=10) |
| except: |
| node_process.kill() |
| node_process = None |
|
|
| atexit.register(stop_node_server) |
|
|
| |
| import gradio as gr |
| from gradio.routes import App |
| from fastapi import FastAPI, Request |
| from fastapi.responses import JSONResponse |
| import httpx |
|
|
| def chat_function(message: str, history: list) -> str: |
| """Send chat request to Node.js Transformers.js server.""" |
| |
| messages = [] |
| for user_msg, assistant_msg in history: |
| messages.append({"role": "user", "content": user_msg}) |
| if assistant_msg: |
| messages.append({"role": "assistant", "content": assistant_msg}) |
| messages.append({"role": "user", "content": message}) |
| |
| |
| try: |
| payload = json.dumps({ |
| "model": "phi-3-mini-4k-instruct", |
| "messages": messages, |
| "temperature": 0.7, |
| "max_tokens": 512, |
| }).encode() |
| |
| req = urllib.request.Request( |
| f"{NODE_SERVER_URL}/v1/chat/completions", |
| data=payload, |
| headers={"Content-Type": "application/json"}, |
| method="POST" |
| ) |
| resp = urllib.request.urlopen(req, timeout=120) |
| data = json.loads(resp.read()) |
| |
| content = data["choices"][0]["message"]["content"] |
| |
| |
| mission = data.get("_mission_barisal", {}) |
| if mission: |
| quality = mission.get("quality_score", 0) |
| content += f"\n\n---\n_Mission Barisal: {quality*100:.0f}% quality_" |
| |
| return content |
| except urllib.error.HTTPError as e: |
| return f"β Error {e.code}: {e.read().decode()[:200]}" |
| except urllib.error.URLError as e: |
| if "model_ready" not in str(e): |
| return "β³ Model is loading... Please wait and try again." |
| return f"β Connection error: {e.reason}" |
| except Exception as e: |
| return f"β Error: {str(e)[:200]}" |
|
|
| def run_app(): |
| """Main app function β wrapped with @spaces.GPU for ZeroGPU.""" |
| |
| |
| threading.Thread(target=start_node_server, daemon=True).start() |
| |
| |
| with gr.Blocks( |
| title="Sahon AI - Transformers.js + Mission Barisal", |
| theme=gr.themes.Soft(), |
| ) as demo: |
| gr.Markdown(""" |
| # π€ Sahon AI |
| ### Transformers.js (Node.js) + Gradio + ZeroGPU |
| |
| **JavaScript-powered LLM on Hugging Face Spaces!** |
| No Python ML dependencies β pure Transformers.js inference. |
| """) |
| |
| with gr.Row(): |
| status_box = gr.Textbox( |
| value="Starting Node.js server...", |
| label="π‘ Model Status", |
| interactive=False, |
| ) |
| |
| gr.ChatInterface( |
| fn=chat_function, |
| title="π¬ Chat", |
| description="Powered by Xenova/phi-3-mini-4k-instruct via Transformers.js", |
| examples=[ |
| "What is the capital of Bangladesh?", |
| "Explain AI hallucination simply", |
| "Write a Python prime function", |
| ], |
| ) |
| |
| with gr.Accordion("π API (OpenAI-Compatible)", open=False): |
| gr.Markdown(f""" |
| **API Base URL:** `https://bdzombie-sahon.hf.space` |
| |
| - `GET /v1/models` β List models |
| - `POST /v1/chat/completions` β Chat |
| - `POST /v1/completions` β Text |
| - `GET /health` β Health check |
| |
| All API endpoints served by the **Node.js Transformers.js** backend. |
| """) |
| |
| return demo |
|
|
|
|
| |
| |
| |
|
|
| def _start_server(): |
| """Build and start the entire Gradio + FastAPI + Node.js stack.""" |
| from gradio.routes import App |
| import uvicorn |
| |
| |
| demo = run_app() |
| |
| |
| fastapi_app = App.create_app(demo) |
| |
| |
| @fastapi_app.get("/health") |
| async def proxy_health(): |
| try: |
| async with httpx.AsyncClient() as client: |
| r = await client.get(f"{NODE_SERVER_URL}/health", timeout=5) |
| return JSONResponse(content=r.json()) |
| except: |
| return JSONResponse( |
| content={"status": "ok", "node_js": "loading", "progress": "Node.js server starting..."}, |
| status_code=200, |
| ) |
| |
| |
| @fastapi_app.get("/v1/models") |
| async def proxy_models(): |
| try: |
| async with httpx.AsyncClient() as client: |
| r = await client.get(f"{NODE_SERVER_URL}/v1/models", timeout=5) |
| return JSONResponse(content=r.json()) |
| except Exception as e: |
| return JSONResponse(content={"error": str(e)}, status_code=503) |
| |
| |
| @fastapi_app.post("/v1/chat/completions") |
| async def proxy_chat(request: Request): |
| try: |
| body = await request.json() |
| async with httpx.AsyncClient() as client: |
| r = await client.post(f"{NODE_SERVER_URL}/v1/chat/completions", json=body, timeout=120) |
| return JSONResponse(content=r.json(), status_code=r.status_code) |
| except httpx.TimeoutException: |
| return JSONResponse(content={"error": "Request timeout"}, status_code=504) |
| except Exception as e: |
| return JSONResponse(content={"error": str(e)}, status_code=500) |
| |
| |
| @fastapi_app.post("/v1/completions") |
| async def proxy_completions(request: Request): |
| try: |
| body = await request.json() |
| async with httpx.AsyncClient() as client: |
| r = await client.post(f"{NODE_SERVER_URL}/v1/completions", json=body, timeout=120) |
| return JSONResponse(content=r.json(), status_code=r.status_code) |
| except Exception as e: |
| return JSONResponse(content={"error": str(e)}, status_code=500) |
| |
| |
| port = int(os.environ.get("PORT", 7860)) |
| print(f"[Sahon] Starting unified server on 0.0.0.0:{port}") |
| uvicorn.run(fastapi_app, host="0.0.0.0", port=port, log_level="info") |
|
|
|
|
| |
| if HAS_SPACES: |
| main = spaces_gpu(_start_server) |
| else: |
| main = _start_server |
|
|
| if __name__ == "__main__": |
| main() |
|
|