File size: 9,864 Bytes
06e1656 3fbf9d5 06e1656 060e6d9 06e1656 3fbf9d5 06e1656 3fbf9d5 06e1656 3fbf9d5 06e1656 3fbf9d5 06e1656 3fbf9d5 060e6d9 06e1656 060e6d9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | #!/usr/bin/env python3
"""
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
# βββ ZeroGPU Activation βββ
# ZeroGPU requires at least one @spaces.GPU decorated function.
# Even if we don't use GPU (Transformers.js runs on CPU),
# the decorator must be present for ZeroGPU to activate.
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")
# βββ Config βββ
NODE_SERVER_PORT = 8888
NODE_SERVER_URL = f"http://127.0.0.1:{NODE_SERVER_PORT}"
NODE_SCRIPT = "server.mjs"
# βββ Node.js Process Management βββ
node_process = None
def start_node_server():
"""Start the Node.js Transformers.js server as a subprocess."""
global node_process
# Install npm packages first
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}")
# Start Node.js server
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,
)
# Log output in background
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)
# Wait for health check to pass
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)
# βββ Gradio UI βββ
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."""
# Build messages from history
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})
# Call Node.js server
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"]
# Add Mission Barisal quality info if available
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."""
# Start Node.js server in background
threading.Thread(target=start_node_server, daemon=True).start()
# Create Gradio UI
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
# βββ Main Entry Point (wrapped with @spaces.GPU) βββ
# The @spaces.GPU decorator keeps GPU allocated for the entire
# lifetime of the server, since uvicorn.run() blocks forever.
def _start_server():
"""Build and start the entire Gradio + FastAPI + Node.js stack."""
from gradio.routes import App
import uvicorn
# Build Gradio demo
demo = run_app()
# Create FastAPI app from Gradio
fastapi_app = App.create_app(demo)
# ββ Proxy: GET /health ββ
@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,
)
# ββ Proxy: GET /v1/models ββ
@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)
# ββ Proxy: POST /v1/chat/completions ββ
@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)
# ββ Proxy: POST /v1/completions ββ
@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)
# Start!
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")
# Apply @spaces.GPU if available β this keeps GPU alive while uvicorn runs
if HAS_SPACES:
main = spaces_gpu(_start_server)
else:
main = _start_server
if __name__ == "__main__":
main()
|