Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,7 +15,6 @@ Jev-style 判別式決策互動 POC(繁體中文)
|
|
| 15 |
import json
|
| 16 |
import os
|
| 17 |
import subprocess
|
| 18 |
-
import sys
|
| 19 |
import threading
|
| 20 |
import time
|
| 21 |
import urllib.request
|
|
@@ -35,17 +34,71 @@ 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 |
-
|
| 43 |
tmp = dst + ".part"
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
os.rename(tmp, dst)
|
|
|
|
|
|
|
| 46 |
return dst
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def ensure_backend():
|
| 50 |
"""下載、解壓、啟動 jevos(冪等,可重複呼叫)。"""
|
| 51 |
global _server
|
|
@@ -58,45 +111,45 @@ def ensure_backend():
|
|
| 58 |
def _ensure_backend_locked():
|
| 59 |
global _server
|
| 60 |
# 先看是不是已經有 server 在跑(例如本機手動啟動的)→ 有就直接用,不下載
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
"questions": {"q": {"type": "noul", "instructions": "ok?"}}},
|
| 64 |
-
timeout=5)
|
| 65 |
-
print("[poc] jevos 已在運行", flush=True)
|
| 66 |
return
|
| 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 |
-
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 == "是非題":
|
|
@@ -118,7 +171,7 @@ def decide(state, mode, question, options_text):
|
|
| 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}}}
|
|
@@ -127,12 +180,12 @@ def decide(state, mode, question, options_text):
|
|
| 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"]
|
|
@@ -146,7 +199,13 @@ def decide(state, mode, question, options_text):
|
|
| 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 = {
|
|
@@ -173,9 +232,12 @@ CSS = """
|
|
| 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)
|
|
@@ -186,12 +248,14 @@ with gr.Blocks(title="Jev-style 判別式決策 POC") as demo:
|
|
| 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 |
-
|
| 196 |
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)),
|
| 197 |
css=CSS)
|
|
|
|
| 15 |
import json
|
| 16 |
import os
|
| 17 |
import subprocess
|
|
|
|
| 18 |
import threading
|
| 19 |
import time
|
| 20 |
import urllib.request
|
|
|
|
| 34 |
_server = None
|
| 35 |
_lock = threading.Lock()
|
| 36 |
|
| 37 |
+
# 後端狀態(給 UI 顯示用,避免首次下載時使用者以為當機)
|
| 38 |
+
_progress = {"phase": "idle", "pct": 0, "msg": ""}
|
| 39 |
+
_last_pct_shown = -1
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _set_phase(phase, msg="", pct=None):
|
| 43 |
+
global _last_pct_shown
|
| 44 |
+
_progress["phase"] = phase
|
| 45 |
+
_progress["msg"] = msg
|
| 46 |
+
if pct is not None:
|
| 47 |
+
_progress["pct"] = pct
|
| 48 |
+
print(f"[poc] {msg or phase}", flush=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def status_text():
|
| 52 |
+
ph = _progress["phase"]
|
| 53 |
+
if ph == "ready":
|
| 54 |
+
return "✅ 後端就緒(jevos 運行中)"
|
| 55 |
+
if ph == "downloading":
|
| 56 |
+
return f"⏳ 下載模型中 {_progress['pct']}%(首次啟動需下載約 660MB,請稍候)"
|
| 57 |
+
if ph == "extracting":
|
| 58 |
+
return "⏳ 解壓縮模型中…"
|
| 59 |
+
if ph == "starting":
|
| 60 |
+
return "⏳ 啟動 jevos 中(載入模型約需 10 秒)…"
|
| 61 |
+
if ph == "error":
|
| 62 |
+
return f"❌ 後端錯誤:{_progress['msg']}"
|
| 63 |
+
return "⏳ 後端準備中…"
|
| 64 |
+
|
| 65 |
|
| 66 |
def _dl(url, dst):
|
| 67 |
+
"""下載檔案,顯示進度百分比。"""
|
| 68 |
+
global _last_pct_shown
|
| 69 |
if os.path.exists(dst):
|
| 70 |
return dst
|
| 71 |
+
_set_phase("downloading", f"開始下載 {url.split('/')[-1]}")
|
| 72 |
tmp = dst + ".part"
|
| 73 |
+
_last_pct_shown = -1
|
| 74 |
+
|
| 75 |
+
def hook(blocks, block_size, total):
|
| 76 |
+
global _last_pct_shown
|
| 77 |
+
if total and total > 0:
|
| 78 |
+
pct = min(100, blocks * block_size * 100 // total)
|
| 79 |
+
_progress["pct"] = pct
|
| 80 |
+
if pct // 10 != _last_pct_shown // 10:
|
| 81 |
+
_last_pct_shown = pct
|
| 82 |
+
print(f"[poc] 下載進度 {pct}%", flush=True)
|
| 83 |
+
|
| 84 |
+
urllib.request.urlretrieve(url, tmp, reporthook=hook)
|
| 85 |
os.rename(tmp, dst)
|
| 86 |
+
_progress["pct"] = 100
|
| 87 |
+
print("[poc] 下載完成", flush=True)
|
| 88 |
return dst
|
| 89 |
|
| 90 |
|
| 91 |
+
def backend_ready():
|
| 92 |
+
"""不阻塞地檢查 jevos 是否已在服務。"""
|
| 93 |
+
try:
|
| 94 |
+
requests.post(API, json={"model": "jev-latest", "state": "ping",
|
| 95 |
+
"questions": {"q": {"type": "noul", "instructions": "ok?"}}},
|
| 96 |
+
timeout=3)
|
| 97 |
+
return True
|
| 98 |
+
except Exception:
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
|
| 102 |
def ensure_backend():
|
| 103 |
"""下載、解壓、啟動 jevos(冪等,可重複呼叫)。"""
|
| 104 |
global _server
|
|
|
|
| 111 |
def _ensure_backend_locked():
|
| 112 |
global _server
|
| 113 |
# 先看是不是已經有 server 在跑(例如本機手動啟動的)→ 有就直接用,不下載
|
| 114 |
+
if backend_ready():
|
| 115 |
+
_set_phase("ready", "jevos 已在運行")
|
|
|
|
|
|
|
|
|
|
| 116 |
return
|
| 117 |
+
try:
|
| 118 |
+
jev_dir = os.path.join(DATA, "jev")
|
| 119 |
+
binary = os.path.join(jev_dir, "jev")
|
| 120 |
+
model_dir = os.path.join(jev_dir, "model")
|
| 121 |
+
if not (os.path.exists(binary) and os.path.isdir(model_dir)):
|
| 122 |
+
os.makedirs(DATA, exist_ok=True)
|
| 123 |
+
tgz = _dl(BIN_URL, os.path.join(DATA, "jev-linux-x64.tar.gz"))
|
| 124 |
+
_set_phase("extracting", "解壓縮 jevos 主程式…")
|
| 125 |
+
with tarfile.open(tgz) as tf:
|
| 126 |
+
tf.extractall(DATA)
|
| 127 |
+
zf = _dl(MODEL_URL, os.path.join(DATA, "jevos-v3-openvino-int8.zip"))
|
| 128 |
+
_set_phase("extracting", "解壓縮模型(約 630MB)…")
|
| 129 |
+
with zipfile.ZipFile(zf) as z:
|
| 130 |
+
z.extractall(jev_dir) # 內含 model/
|
| 131 |
+
os.chmod(binary, 0o755)
|
| 132 |
+
_set_phase("starting", "啟動 jevos…")
|
| 133 |
+
_server = subprocess.Popen([binary, "serve"], cwd=jev_dir,
|
| 134 |
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 135 |
+
for _ in range(90):
|
| 136 |
+
time.sleep(1)
|
| 137 |
+
if backend_ready():
|
| 138 |
+
_set_phase("ready", "jevos 就緒")
|
| 139 |
+
return
|
| 140 |
+
raise RuntimeError("jevos 啟動逾時(90 秒)")
|
| 141 |
+
except Exception as e:
|
| 142 |
+
_set_phase("error", f"{type(e).__name__}: {e}")
|
| 143 |
+
raise
|
|
|
|
| 144 |
|
| 145 |
|
| 146 |
def decide(state, mode, question, options_text):
|
| 147 |
if not (state or "").strip() or not (question or "").strip():
|
| 148 |
+
return "請先輸入狀態文字與問題", "", "", status_text()
|
| 149 |
+
# 後端還沒好就不要卡住:直接告訴使用者目前進度
|
| 150 |
+
if not backend_ready():
|
| 151 |
+
ensure_backend_if_needed()
|
| 152 |
+
return status_text() + ",請稍後再按一次「送出判別」。", "", "", status_text()
|
| 153 |
ensure_backend()
|
| 154 |
t0 = time.perf_counter()
|
| 155 |
if mode == "是非題":
|
|
|
|
| 171 |
desc = desc.strip() or name # 無描述時用名稱代替(choice 需要 criteria 描述)
|
| 172 |
criteria[name] = desc
|
| 173 |
if not criteria:
|
| 174 |
+
return "請至少輸入一個候選選項", "", "", status_text()
|
| 175 |
payload = {"model": "jev-latest", "state": state,
|
| 176 |
"questions": {"q1": {"type": "choice", "instructions": question,
|
| 177 |
"criteria": criteria}}}
|
|
|
|
| 180 |
r.raise_for_status()
|
| 181 |
data = r.json()
|
| 182 |
except Exception as e:
|
| 183 |
+
return f"後端呼叫失敗:{type(e).__name__}({e})", "", "", status_text()
|
| 184 |
ms = (time.perf_counter() - t0) * 1000
|
| 185 |
try:
|
| 186 |
ans = data["answers"]["q1"]
|
| 187 |
except (KeyError, TypeError):
|
| 188 |
+
return f"後端回傳格式異常:{str(data)[:300]}", "", "", status_text()
|
| 189 |
usage = data.get("usage", {})
|
| 190 |
if ans["type"] == "noul":
|
| 191 |
p = ans["noul"]
|
|
|
|
| 199 |
f"各選項機率:{json.dumps(probs, ensure_ascii=False)}")
|
| 200 |
meta = (f"延遲 {ms:.0f} ms | input_tokens={usage.get('input_tokens','?')} | "
|
| 201 |
f"output_tokens={usage.get('output_tokens','?')}(零解碼)")
|
| 202 |
+
return summary, bars, meta, status_text()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def ensure_backend_if_needed():
|
| 206 |
+
"""確保背景下載執行緒有在跑(不阻塞)。"""
|
| 207 |
+
t = threading.Thread(target=ensure_backend, daemon=True)
|
| 208 |
+
t.start()
|
| 209 |
|
| 210 |
|
| 211 |
PRESETS = {
|
|
|
|
| 232 |
with gr.Blocks(title="Jev-style 判別式決策 POC") as demo:
|
| 233 |
gr.Markdown("# ⚡ Jev-style 判別式決策 POC\n"
|
| 234 |
"輸入一段**狀態文字**+**問題**,模型直接輸出**機率分佈**——不生成任何文字(`output_tokens=0`)。"
|
| 235 |
+
"這就是 System 1 判別引擎:傳統 LLM Agent 每步 1–3 秒還可能寫壞 JSON,這裡一次前向傳播就給出完整機率分佈。\n\n"
|
| 236 |
+
"> ⏰ **首次啟動**需要下載模型(約 660MB),請看下方「後端狀態」等它就緒再按送出,之後啟動都是秒開。")
|
| 237 |
+
status_box = gr.Textbox(label="後端狀態", value="⏳ 後端準備中…", interactive=False)
|
| 238 |
with gr.Row():
|
| 239 |
preset = gr.Dropdown(list(PRESETS.keys()), label="範例一鍵填入", value="客服訊息分類")
|
| 240 |
+
refresh_btn = gr.Button("重新整理狀態", size="sm")
|
| 241 |
state = gr.Textbox(label="狀態文字(state)", lines=3)
|
| 242 |
mode = gr.Radio(["是非題", "多選題"], label="題型", value="多選題")
|
| 243 |
question = gr.Textbox(label="問題(instructions)", lines=1)
|
|
|
|
| 248 |
out_meta = gr.Textbox(label="效能", lines=1)
|
| 249 |
|
| 250 |
preset.change(load_preset, preset, [state, mode, question, options])
|
| 251 |
+
refresh_btn.click(lambda: status_text(), outputs=status_box)
|
| 252 |
btn.click(decide, [state, mode, question, options],
|
| 253 |
+
[out_summary, out_bars, out_meta, status_box])
|
| 254 |
demo.load(load_preset, preset, [state, mode, question, options])
|
| 255 |
+
demo.load(lambda: status_text(), outputs=status_box)
|
| 256 |
|
| 257 |
if __name__ == "__main__":
|
| 258 |
# Spaces 啟動時先備好後端(背景執行,不擋 UI)
|
| 259 |
+
ensure_backend_if_needed()
|
| 260 |
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)),
|
| 261 |
css=CSS)
|