Spaces:
Running on Zero
Running on Zero
File size: 17,506 Bytes
548611a d17d584 548611a 722fa5b 548611a 394c2c6 548611a 03f0180 548611a 5cf37a5 03f0180 548611a 5cf37a5 548611a 03f0180 5cf37a5 548611a 03f0180 548611a 03f0180 548611a 03f0180 d17d584 5cf37a5 03f0180 5cf37a5 d17d584 03f0180 548611a d17d584 5cf37a5 d17d584 5cf37a5 d17d584 5cf37a5 d17d584 5cf37a5 d17d584 5cf37a5 d17d584 5cf37a5 548611a 03f0180 548611a 03f0180 548611a 03f0180 548611a 03f0180 548611a 03f0180 548611a 722fa5b 548611a 03f0180 722fa5b 548611a 03f0180 722fa5b 548611a 03f0180 722fa5b 548611a 03f0180 548611a 03f0180 548611a 722fa5b 394c2c6 548611a 03f0180 548611a | 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 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | # -*- coding: utf-8 -*-
"""
Jev-style 判別式決策互動 POC(繁體中文)
後端:jevos(feder-cr/jev,OpenVINO int8,CPU 可跑)
- 是非題 (noul):輸入狀態+問題,回傳 P(yes)
- 多選題 (choice):輸入狀態+問題+候選選項,回傳每選項機率
- 每次回應標示延遲與 output_tokens=0(零解碼,判別 vs 生成的核心差異)
免費部署:Hugging Face Spaces(CPU Basic 免費方案即可)
1. 在 huggingface.co/new-space 建一個 Gradio Space(公開)
2. 上傳 app.py+requirements.txt
3. 首次啟動會自動下載 jevos(約 660MB)並快取於 /data,之後秒啟
本地運行:見 README.md
"""
import json
import os
import shutil
import subprocess
import threading
import time
import zipfile
import tarfile
import gradio as gr
import requests
import spaces # ZeroGPU 執行期內建;不可寫進 requirements.txt(會蓋掉內建模組)
BASE = os.path.dirname(os.path.abspath(__file__))
if os.path.isdir("/data") and os.access("/data", os.W_OK):
DATA = "/data"
_DATA_PERSISTENT = True
else:
DATA = os.path.join(BASE, ".poc-data")
_DATA_PERSISTENT = False
os.makedirs(DATA, exist_ok=True)
BIN_URL = "https://github.com/feder-cr/jev/releases/download/jevos-v3/jev-linux-x64.tar.gz"
MODEL_URL = "https://github.com/feder-cr/jev/releases/download/jevos-v3/jevos-v3-openvino-int8.zip"
API = "http://127.0.0.1:8017/v1/systemone"
_server = None
_lock = threading.Lock()
# 後端狀態(給 UI 顯示用,避免首次下載時使用者以為當機)
_progress = {"phase": "idle", "pct": 0, "msg": ""}
_last_pct_shown = -1
def _set_phase(phase, msg="", pct=None):
global _last_pct_shown
_progress["phase"] = phase
_progress["msg"] = msg
if pct is not None:
_progress["pct"] = pct
print(f"[poc] {msg or phase}", flush=True)
def status_text():
ph = _progress["phase"]
if ph == "ready":
return "✅ 後端就緒(jevos 運行中)"
if ph == "downloading":
return f"⏳ 下載模型中 {_progress['pct']}%(首次啟動需下載約 660MB,請稍候)"
if ph == "extracting":
return "⏳ 解壓縮模型中…"
if ph == "starting":
return "⏳ 啟動 jevos 中(載入模型約需 10 秒)…"
if ph == "error":
return f"❌ 後端錯誤:{_progress['msg']}"
return "⏳ 後端準備中…"
def _dl(url, dst):
"""下載檔案(支援斷點續傳+重試),顯示進度百分比。"""
global _last_pct_shown
if os.path.exists(dst):
return dst
name = url.split("/")[-1]
tmp = dst + ".part"
_last_pct_shown = -1
for attempt in range(1, 4):
try:
start = os.path.getsize(tmp) if os.path.exists(tmp) else 0
if start > 0:
print(f"[poc] 續傳 {name}(從 {start // 1024 // 1024}MB 繼續)", flush=True)
else:
_set_phase("downloading", f"開始下載 {name}")
headers = {"Range": f"bytes={start}-"} if start else {}
with requests.get(url, headers=headers, stream=True, timeout=30) as r:
r.raise_for_status()
total = int(r.headers.get("content-length", 0) or 0)
if start and r.status_code != 206:
# 伺服器不支援續傳,從頭來
print("[poc] 伺服器不支援續傳,從頭下載", flush=True)
start, total, mode = 0, total, "wb"
else:
mode = "ab" if start else "wb"
full_total = total + start
downloaded = start
with open(tmp, mode) as f:
for chunk in r.iter_content(chunk_size=1024 * 256):
if not chunk:
continue
f.write(chunk)
downloaded += len(chunk)
if full_total:
pct = min(100, downloaded * 100 // full_total)
_progress["pct"] = pct
if pct // 10 != _last_pct_shown // 10:
_last_pct_shown = pct
print(f"[poc] 下載進度 {pct}%", flush=True)
os.rename(tmp, dst)
_progress["pct"] = 100
print("[poc] 下載完成", flush=True)
return dst
except (requests.RequestException, OSError) as e:
print(f"[poc] 下載中斷(嘗試 {attempt}/3):{type(e).__name__},5 秒後續傳重試", flush=True)
time.sleep(5)
raise RuntimeError(f"下載失敗,已重試 3 次:{name}")
def backend_ready():
"""不阻塞地檢查 jevos 是否已在服務。"""
try:
requests.post(API, json={"model": "jev-latest", "state": "ping",
"questions": {"q": {"type": "noul", "instructions": "ok?"}}},
timeout=3)
return True
except Exception:
return False
def ensure_backend():
"""下載、解壓、啟動 jevos(冪等,可重複呼叫)。"""
global _server
with _lock:
if _server is not None and _server.poll() is None:
return
_ensure_backend_locked()
def _ensure_backend_locked():
global _server
# 先看是不是已經有 server 在跑(例如本機手動啟動的)→ 有就直接用,不下載
if backend_ready():
_set_phase("ready", "jevos 已在運行")
return
try:
jev_dir, binary = _find_or_prepare_backend()
# 保險:確保可執行(git/網頁上傳可能掉執行權限)
try:
os.chmod(binary, 0o755)
except Exception:
pass
_ensure_model_weights(os.path.join(jev_dir, "model"))
_set_phase("starting", "啟動 jevos…")
_server = subprocess.Popen([binary, "serve"], cwd=jev_dir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(90):
time.sleep(1)
if backend_ready():
_set_phase("ready", "jevos 就緒")
return
raise RuntimeError("jevos 啟動逾時(90 秒)")
except Exception as e:
_set_phase("error", f"{type(e).__name__}: {e}")
raise
def _ensure_model_weights(model_dir):
"""確保 openvino_model.bin 存在;缺失時從 release 下載 zip 並只解出該檔(支援續傳)。"""
bin_path = os.path.join(model_dir, "openvino_model.bin")
if os.path.isfile(bin_path):
return
print("[poc] 缺少 openvino_model.bin,從 release 下載模型包並解出該檔…", flush=True)
_set_phase("downloading", "下載模型包(約 630MB,只取權重檔,支援續傳)…")
zf = _dl(MODEL_URL, os.path.join(DATA, "jevos-v3-openvino-int8.zip"))
_set_phase("extracting", "解出 openvino_model.bin…")
os.makedirs(model_dir, exist_ok=True)
with zipfile.ZipFile(zf) as z:
for name in z.namelist():
if name.endswith("openvino_model.bin"):
with z.open(name) as src, open(bin_path, "wb") as dst:
shutil.copyfileobj(src, dst, length=1024 * 1024)
print("[poc] openvino_model.bin 就緒", flush=True)
return
raise RuntimeError("模型包內找不到 openvino_model.bin")
def _find_or_prepare_backend():
"""找後端檔案,順序:repo 內已解壓 → 快取 → repo 內壓縮檔 → 線上下載。
回傳 (jev_dir, binary)。jev_dir 下保證有執行檔與 model/(巢狀佈局會建連結)。"""
# 1. 已解壓好的(手動上傳到 repo 根目錄,或之前快取的)
for base in (BASE, DATA):
# 1a. 標準佈局:<base>/jev/{jev(執行檔), model/, *.so}
d = os.path.join(base, "jev")
b = os.path.join(d, "jev")
if os.path.isfile(b) and os.path.isdir(os.path.join(d, "model")):
print(f"[poc] 使用現成後端:{b}", flush=True)
return d, b
# 1b. 巢狀佈局:<base>/jev/jev/{jev(執行檔), *.so},model/ 在 <base>/jev/model/
# (tarball 內層目錄被原樣上傳時會長這樣;執行檔在內層,./model 要能被找到)
inner = os.path.join(d, "jev")
b2 = os.path.join(inner, "jev")
outer_model = os.path.join(d, "model")
if os.path.isfile(b2) and os.path.isdir(outer_model):
link = os.path.join(inner, "model")
if not os.path.exists(link):
try:
os.symlink(outer_model, link)
print("[poc] 巢狀佈局:已建立 model 連結", flush=True)
except Exception as e:
print(f"[poc] 建 model 連結失敗:{e}", flush=True)
print(f"[poc] 使用現成後端(巢狀):{b2}", flush=True)
return inner, b2
# 2. repo 根目錄有壓縮檔(手動上傳)→ 解到 DATA
jev_dir = os.path.join(DATA, "jev")
tgz = os.path.join(BASE, "jev-linux-x64.tar.gz")
zf = os.path.join(BASE, "jevos-v3-openvino-int8.zip")
if os.path.exists(tgz) and os.path.exists(zf):
print("[poc] 使用 repo 內附壓縮檔解壓…", flush=True)
_set_phase("extracting", "解壓縮 repo 內附檔案…")
os.makedirs(DATA, exist_ok=True)
with tarfile.open(tgz) as tf:
tf.extractall(DATA, filter="data")
print("[poc] 主程式解壓完成,解壓縮模型(約 630MB,需要幾分鐘)…", flush=True)
_extract_zip_progress(zf, jev_dir)
print("[poc] 模型解壓完成", flush=True)
return jev_dir, os.path.join(jev_dir, "jev")
# 3. 線上下載(Spaces 執行期可連外網)
os.makedirs(DATA, exist_ok=True)
tgz = _dl(BIN_URL, os.path.join(DATA, "jev-linux-x64.tar.gz"))
_set_phase("extracting", "解壓縮 jevos 主程式…")
with tarfile.open(tgz) as tf:
tf.extractall(DATA, filter="data")
zf = _dl(MODEL_URL, os.path.join(DATA, "jevos-v3-openvino-int8.zip"))
_set_phase("extracting", "解壓縮模型(約 630MB,需要幾分鐘)…")
_extract_zip_progress(zf, jev_dir)
print("[poc] 模型解壓完成", flush=True)
return jev_dir, os.path.join(jev_dir, "jev")
def _extract_zip_progress(zf, jev_dir):
"""解壓 zip 並印進度(避免長時間無輸出被誤判卡死)。"""
with zipfile.ZipFile(zf) as z:
members = z.namelist()
total = len(members)
for i, m in enumerate(members):
z.extract(m, jev_dir)
if (i + 1) % 25 == 0 or (i + 1) == total:
print(f"[poc] 模型解壓縮 {i + 1}/{total}", flush=True)
def decide(state, mode, question, options_text):
if not (state or "").strip() or not (question or "").strip():
return "請先輸入狀態文字與問題", "", "", status_text()
# 後端還沒好就不要卡住:直接告訴使用者目前進度
if not backend_ready():
ensure_backend_if_needed()
return status_text() + ",請稍後再按一次「送出判別」。", "", "", status_text()
ensure_backend()
t0 = time.perf_counter()
if mode == "是非題":
payload = {"model": "jev-latest", "state": state,
"questions": {"q1": {"type": "noul", "instructions": question}}}
else:
criteria = {}
for line in options_text.strip().split("\n"):
line = line.strip()
if not line:
continue
if ":" in line:
name, desc = line.split(":", 1)
elif ":" in line:
name, desc = line.split(":", 1)
else:
name, desc = line, ""
name = name.strip()
desc = desc.strip() or name # 無描述時用名稱代替(choice 需要 criteria 描述)
criteria[name] = desc
if not criteria:
return "請至少輸入一個候選選項", "", "", status_text()
payload = {"model": "jev-latest", "state": state,
"questions": {"q1": {"type": "choice", "instructions": question,
"criteria": criteria}}}
try:
r = requests.post(API, json=payload, timeout=120)
r.raise_for_status()
data = r.json()
except Exception as e:
return f"後端呼叫失敗:{type(e).__name__}({e})", "", "", status_text()
ms = (time.perf_counter() - t0) * 1000
try:
ans = data["answers"]["q1"]
except (KeyError, TypeError):
return f"後端回傳格式異常:{str(data)[:300]}", "", "", status_text()
usage = data.get("usage", {})
if ans["type"] == "noul":
p = ans["noul"]
bars = f"P(yes) = {p:.3f}\n" + "█" * int(p * 40) + f" {p*100:.1f}%"
summary = f"答案:{'是' if p >= 0.5 else '否'}(信心 {p if p>=0.5 else 1-p:.1%})"
else:
probs = ans["probabilities"]
bars = "\n".join(f"{k}: {'█' * int(v*40)} {v*100:.1f}%"
for k, v in sorted(probs.items(), key=lambda x: -x[1]))
summary = (f"答案:{ans['choice']}(信心 {ans.get('confidence', 0):.1%})\n"
f"各選項機率:{json.dumps(probs, ensure_ascii=False)}")
meta = (f"延遲 {ms:.0f} ms | input_tokens={usage.get('input_tokens','?')} | "
f"output_tokens={usage.get('output_tokens','?')}(零解碼)")
return summary, bars, meta, status_text()
def ensure_backend_if_needed():
"""確保背景下載執行緒有在跑(不阻塞)。"""
t = threading.Thread(target=ensure_backend, daemon=True)
t.start()
PRESETS = {
"客服訊息分類": ("客人說:「這個月帳單怎麼多了一筆我沒買的東西?」",
"多選題", "這封訊息應該派給哪個團隊?",
"billing: 帳單、付款、退款\nshipping: 寄送、包裹遺失損壞\ntech: 產品故障不會動"),
"是否為退款請求": ("I want my money back for the broken headphones.",
"是非題", "這是一則退款請求嗎?", ""),
"內容審核": ("這家餐廳難吃到我想給一星,老闆還敢嗆客人。",
"是非題", "這則評論含有針對店家的惡意攻擊嗎?", ""),
}
def load_preset(name):
s, m, q, o = PRESETS[name]
return s, m, q, o
@spaces.GPU(duration=10)
def gpu_compat_check():
"""ZeroGPU 硬體要求至少一個 @spaces.GPU 函數才能啟動。
jevos 為純 CPU 運算(OpenVINO),此函數僅為通過啟動檢查的佔位,實際不參與判別、不消耗有意義的 GPU 配額。"""
return "ZeroGPU 相容檢查通過(判別運算實際於 CPU 執行)"
CSS = """
.gradio-container { max-width: 900px !important; }
.bar textarea { font-family: monospace !important; white-space: pre !important; }
"""
with gr.Blocks(title="Jev-style 判別式決策 POC") as demo:
gr.Markdown("# ⚡ Jev-style 判別式決策 POC\n"
"輸入一段**狀態文字**+**問題**,模型直接輸出**機率分佈**——不生成任何文字(`output_tokens=0`)。"
"這就是 System 1 判別引擎:傳統 LLM Agent 每步 1–3 秒還可能寫壞 JSON,這裡一次前向傳播就給出完整機率分佈。\n\n"
"> ⏰ **首次啟動**需要下載模型(約 660MB),請看下方「後端狀態」等它就緒再按送出,之後啟動都是秒開。")
status_box = gr.Textbox(label="後端狀態", value="⏳ 後端準備中…", interactive=False)
gpu_out = gr.Textbox(label="ZeroGPU 相容檢查", value="", interactive=False)
with gr.Row():
preset = gr.Dropdown(list(PRESETS.keys()), label="範例一鍵填入", value="客服訊息分類")
refresh_btn = gr.Button("重新整理狀態", size="sm")
gpu_btn = gr.Button("GPU 相容檢查", size="sm")
state = gr.Textbox(label="狀態文字(state)", lines=3)
mode = gr.Radio(["是非題", "多選題"], label="題型", value="多選題")
question = gr.Textbox(label="問題(instructions)", lines=1)
options = gr.Textbox(label="候選選項(多選題用,每行「名稱: 描述」)", lines=4)
btn = gr.Button("送出判別", variant="primary")
out_summary = gr.Textbox(label="判別結果")
out_bars = gr.Textbox(label="機率分佈", elem_classes="bar", lines=6)
out_meta = gr.Textbox(label="效能", lines=1)
preset.change(load_preset, preset, [state, mode, question, options])
refresh_btn.click(lambda: status_text(), outputs=status_box)
gpu_btn.click(gpu_compat_check, outputs=gpu_out)
btn.click(decide, [state, mode, question, options],
[out_summary, out_bars, out_meta, status_box])
demo.load(load_preset, preset, [state, mode, question, options])
demo.load(lambda: status_text(), outputs=status_box)
if __name__ == "__main__":
print("[poc] app.py v20261003-zerogpu (multi-source backend + @spaces.GPU)", flush=True)
print(f"[poc] 資料目錄:{DATA}({'重啟後保留,可斷點續傳' if _DATA_PERSISTENT else '重啟後消失,請勿重啟'})", flush=True)
# Spaces 啟動時先備好後端(背景執行,不擋 UI)
ensure_backend_if_needed()
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)),
css=CSS)
|