Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +197 -0
- requirements.txt +2 -0
app.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Jev-style 判別式決策互動 POC(繁體中文)
|
| 4 |
+
後端:jevos(feder-cr/jev,OpenVINO int8,CPU 可跑)
|
| 5 |
+
- 是非題 (noul):輸入狀態+問題,回傳 P(yes)
|
| 6 |
+
- 多選題 (choice):輸入狀態+問題+候選選項,回傳每選項機率
|
| 7 |
+
- 每次回應標示延遲與 output_tokens=0(零解碼,判別 vs 生成的核心差異)
|
| 8 |
+
|
| 9 |
+
免費部署:Hugging Face Spaces(CPU Basic 免費方案即可)
|
| 10 |
+
1. 在 huggingface.co/new-space 建一個 Gradio Space(公開)
|
| 11 |
+
2. 上傳 app.py+requirements.txt
|
| 12 |
+
3. 首次啟動會自動下載 jevos(約 660MB)並快取於 /data,之後秒啟
|
| 13 |
+
本地運行:見 README.md
|
| 14 |
+
"""
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import subprocess
|
| 18 |
+
import sys
|
| 19 |
+
import threading
|
| 20 |
+
import time
|
| 21 |
+
import urllib.request
|
| 22 |
+
import zipfile
|
| 23 |
+
import tarfile
|
| 24 |
+
|
| 25 |
+
import gradio as gr
|
| 26 |
+
import requests
|
| 27 |
+
|
| 28 |
+
BASE = os.path.dirname(os.path.abspath(__file__))
|
| 29 |
+
DATA = "/data" if os.path.isdir("/data") and os.access("/data", os.W_OK) else os.path.join(BASE, ".poc-data")
|
| 30 |
+
os.makedirs(DATA, exist_ok=True)
|
| 31 |
+
|
| 32 |
+
BIN_URL = "https://github.com/feder-cr/jev/releases/download/jevos-v3/jev-linux-x64.tar.gz"
|
| 33 |
+
MODEL_URL = "https://github.com/feder-cr/jev/releases/download/jevos-v3/jevos-v3-openvino-int8.zip"
|
| 34 |
+
API = "http://127.0.0.1:8017/v1/systemone"
|
| 35 |
+
_server = None
|
| 36 |
+
_lock = threading.Lock()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _dl(url, dst):
|
| 40 |
+
if os.path.exists(dst):
|
| 41 |
+
return dst
|
| 42 |
+
print(f"[poc] 下載 {url}", flush=True)
|
| 43 |
+
tmp = dst + ".part"
|
| 44 |
+
urllib.request.urlretrieve(url, tmp)
|
| 45 |
+
os.rename(tmp, dst)
|
| 46 |
+
return dst
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def ensure_backend():
|
| 50 |
+
"""下載、解壓、啟動 jevos(冪等,可重複呼叫)。"""
|
| 51 |
+
global _server
|
| 52 |
+
with _lock:
|
| 53 |
+
if _server is not None and _server.poll() is None:
|
| 54 |
+
return
|
| 55 |
+
_ensure_backend_locked()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _ensure_backend_locked():
|
| 59 |
+
global _server
|
| 60 |
+
# 先看是不是已經有 server 在跑(例如本機手動啟動的)→ 有就直接用,不下載
|
| 61 |
+
try:
|
| 62 |
+
requests.post(API, json={"model": "jev-latest", "state": "ping",
|
| 63 |
+
"questions": {"q": {"type": "noul", "instructions": "ok?"}}},
|
| 64 |
+
timeout=5)
|
| 65 |
+
print("[poc] jevos 已在運行", flush=True)
|
| 66 |
+
return
|
| 67 |
+
except Exception:
|
| 68 |
+
pass
|
| 69 |
+
jev_dir = os.path.join(DATA, "jev")
|
| 70 |
+
binary = os.path.join(jev_dir, "jev")
|
| 71 |
+
model_dir = os.path.join(jev_dir, "model")
|
| 72 |
+
if not (os.path.exists(binary) and os.path.isdir(model_dir)):
|
| 73 |
+
os.makedirs(DATA, exist_ok=True)
|
| 74 |
+
tgz = _dl(BIN_URL, os.path.join(DATA, "jev-linux-x64.tar.gz"))
|
| 75 |
+
with tarfile.open(tgz) as tf:
|
| 76 |
+
tf.extractall(DATA)
|
| 77 |
+
zf = _dl(MODEL_URL, os.path.join(DATA, "jevos-v3-openvino-int8.zip"))
|
| 78 |
+
with zipfile.ZipFile(zf) as z:
|
| 79 |
+
z.extractall(jev_dir) # 內含 model/
|
| 80 |
+
os.chmod(binary, 0o755)
|
| 81 |
+
print("[poc] 啟動 jevos…", flush=True)
|
| 82 |
+
_server = subprocess.Popen([binary, "serve"], cwd=jev_dir,
|
| 83 |
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 84 |
+
for _ in range(60):
|
| 85 |
+
time.sleep(1)
|
| 86 |
+
try:
|
| 87 |
+
requests.post(API, json={"model": "jev-latest", "state": "ping",
|
| 88 |
+
"questions": {"q": {"type": "noul", "instructions": "ok?"}}},
|
| 89 |
+
timeout=5)
|
| 90 |
+
print("[poc] jevos 就緒", flush=True)
|
| 91 |
+
return
|
| 92 |
+
except Exception:
|
| 93 |
+
continue
|
| 94 |
+
raise RuntimeError("jevos 啟動逾時")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def decide(state, mode, question, options_text):
|
| 98 |
+
if not (state or "").strip() or not (question or "").strip():
|
| 99 |
+
return "請先輸入狀態文字與問題", "", ""
|
| 100 |
+
ensure_backend()
|
| 101 |
+
t0 = time.perf_counter()
|
| 102 |
+
if mode == "是非題":
|
| 103 |
+
payload = {"model": "jev-latest", "state": state,
|
| 104 |
+
"questions": {"q1": {"type": "noul", "instructions": question}}}
|
| 105 |
+
else:
|
| 106 |
+
criteria = {}
|
| 107 |
+
for line in options_text.strip().split("\n"):
|
| 108 |
+
line = line.strip()
|
| 109 |
+
if not line:
|
| 110 |
+
continue
|
| 111 |
+
if ":" in line:
|
| 112 |
+
name, desc = line.split(":", 1)
|
| 113 |
+
elif ":" in line:
|
| 114 |
+
name, desc = line.split(":", 1)
|
| 115 |
+
else:
|
| 116 |
+
name, desc = line, ""
|
| 117 |
+
name = name.strip()
|
| 118 |
+
desc = desc.strip() or name # 無描述時用名稱代替(choice 需要 criteria 描述)
|
| 119 |
+
criteria[name] = desc
|
| 120 |
+
if not criteria:
|
| 121 |
+
return "請至少輸入一個候選選項", "", ""
|
| 122 |
+
payload = {"model": "jev-latest", "state": state,
|
| 123 |
+
"questions": {"q1": {"type": "choice", "instructions": question,
|
| 124 |
+
"criteria": criteria}}}
|
| 125 |
+
try:
|
| 126 |
+
r = requests.post(API, json=payload, timeout=120)
|
| 127 |
+
r.raise_for_status()
|
| 128 |
+
data = r.json()
|
| 129 |
+
except Exception as e:
|
| 130 |
+
return f"後端呼叫失敗:{type(e).__name__}({e})", "", ""
|
| 131 |
+
ms = (time.perf_counter() - t0) * 1000
|
| 132 |
+
try:
|
| 133 |
+
ans = data["answers"]["q1"]
|
| 134 |
+
except (KeyError, TypeError):
|
| 135 |
+
return f"後端回傳格式異常:{str(data)[:300]}", "", ""
|
| 136 |
+
usage = data.get("usage", {})
|
| 137 |
+
if ans["type"] == "noul":
|
| 138 |
+
p = ans["noul"]
|
| 139 |
+
bars = f"P(yes) = {p:.3f}\n" + "█" * int(p * 40) + f" {p*100:.1f}%"
|
| 140 |
+
summary = f"答案:{'是' if p >= 0.5 else '否'}(信心 {p if p>=0.5 else 1-p:.1%})"
|
| 141 |
+
else:
|
| 142 |
+
probs = ans["probabilities"]
|
| 143 |
+
bars = "\n".join(f"{k}: {'█' * int(v*40)} {v*100:.1f}%"
|
| 144 |
+
for k, v in sorted(probs.items(), key=lambda x: -x[1]))
|
| 145 |
+
summary = (f"答案:{ans['choice']}(信心 {ans.get('confidence', 0):.1%})\n"
|
| 146 |
+
f"各選項機率:{json.dumps(probs, ensure_ascii=False)}")
|
| 147 |
+
meta = (f"延遲 {ms:.0f} ms | input_tokens={usage.get('input_tokens','?')} | "
|
| 148 |
+
f"output_tokens={usage.get('output_tokens','?')}(零解碼)")
|
| 149 |
+
return summary, bars, meta
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
PRESETS = {
|
| 153 |
+
"客服訊息分類": ("客人說:「這個月帳單怎麼多了一筆我沒買的東西?」",
|
| 154 |
+
"多選題", "這封訊息應該派給哪個團隊?",
|
| 155 |
+
"billing: 帳單、付款、退款\nshipping: 寄送、包裹遺失損壞\ntech: 產品故障不會動"),
|
| 156 |
+
"是否為退款請求": ("I want my money back for the broken headphones.",
|
| 157 |
+
"是非題", "這是一則退款請求嗎?", ""),
|
| 158 |
+
"內容審核": ("這家餐廳難吃到我想給一星,老闆還敢嗆客人。",
|
| 159 |
+
"是非題", "這則評論含有針對店家的惡意攻擊嗎?", ""),
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def load_preset(name):
|
| 164 |
+
s, m, q, o = PRESETS[name]
|
| 165 |
+
return s, m, q, o
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
CSS = """
|
| 169 |
+
.gradio-container { max-width: 900px !important; }
|
| 170 |
+
.bar textarea { font-family: monospace !important; white-space: pre !important; }
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
with gr.Blocks(title="Jev-style 判別式決策 POC") as demo:
|
| 174 |
+
gr.Markdown("# ⚡ Jev-style 判別式決策 POC\n"
|
| 175 |
+
"輸入一段**狀態文字**+**問題**,模型直接輸出**機率分佈**——不生成任何文字(`output_tokens=0`)。"
|
| 176 |
+
"這就是 System 1 判別引擎:傳統 LLM Agent 每步 1–3 秒還可能寫壞 JSON,這裡一次前向傳播就給出完整機率分佈。")
|
| 177 |
+
with gr.Row():
|
| 178 |
+
preset = gr.Dropdown(list(PRESETS.keys()), label="範例一鍵填入", value="客服訊息分類")
|
| 179 |
+
state = gr.Textbox(label="狀態文字(state)", lines=3)
|
| 180 |
+
mode = gr.Radio(["是非題", "多選題"], label="題型", value="多選題")
|
| 181 |
+
question = gr.Textbox(label="問題(instructions)", lines=1)
|
| 182 |
+
options = gr.Textbox(label="候選選項(多選題用,每行「名稱: 描述」)", lines=4)
|
| 183 |
+
btn = gr.Button("送出判別", variant="primary")
|
| 184 |
+
out_summary = gr.Textbox(label="判別結果")
|
| 185 |
+
out_bars = gr.Textbox(label="機率分佈", elem_classes="bar", lines=6)
|
| 186 |
+
out_meta = gr.Textbox(label="效能", lines=1)
|
| 187 |
+
|
| 188 |
+
preset.change(load_preset, preset, [state, mode, question, options])
|
| 189 |
+
btn.click(decide, [state, mode, question, options],
|
| 190 |
+
[out_summary, out_bars, out_meta])
|
| 191 |
+
demo.load(load_preset, preset, [state, mode, question, options])
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
# Spaces 啟動時先備好後端(背景執行,不擋 UI)
|
| 195 |
+
threading.Thread(target=ensure_backend, daemon=True).start()
|
| 196 |
+
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)),
|
| 197 |
+
css=CSS)
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.0
|
| 2 |
+
requests>=2.31.0
|