DeepLearning101 commited on
Commit
548611a
·
verified ·
1 Parent(s): 4ca5f81

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +197 -0
  2. 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