File size: 20,266 Bytes
d99ae44
1ecaafa
 
d99ae44
cdf96a9
77f08ac
 
 
 
 
 
 
cdf96a9
cdf70d4
cdf96a9
d99ae44
 
77f08ac
d99ae44
 
 
 
 
 
 
 
 
20cb253
 
 
 
cdf70d4
20cb253
 
d99ae44
 
cdf96a9
 
d99ae44
cdf96a9
d99ae44
cdf96a9
77f08ac
 
cdf96a9
 
77f08ac
 
d99ae44
 
cdf96a9
d99ae44
cdf70d4
20cb253
 
cdf96a9
 
d99ae44
cdf96a9
77f08ac
 
cdf96a9
 
77f08ac
 
 
 
cdf96a9
77f08ac
cdf70d4
20cb253
 
d99ae44
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf70d4
20cb253
 
d99ae44
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf70d4
20cb253
 
cdf96a9
 
 
 
20cb253
ab34c21
20cb253
77f08ac
 
cdf96a9
 
77f08ac
cdf96a9
77f08ac
 
20cb253
 
cdf96a9
 
77f08ac
 
 
 
 
 
d99ae44
 
 
 
 
 
 
 
 
 
77f08ac
cdf96a9
77f08ac
 
 
 
 
cdf96a9
 
ab34c21
cdf96a9
 
77f08ac
 
 
 
 
 
 
d99ae44
77f08ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d99ae44
77f08ac
 
d99ae44
20cb253
 
77f08ac
 
 
 
 
 
 
 
 
 
20cb253
 
77f08ac
cdf96a9
20cb253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab34c21
20cb253
cdf96a9
77f08ac
 
 
 
cdf96a9
ab34c21
20cb253
cdf96a9
 
77f08ac
cdf96a9
 
 
 
 
 
 
 
 
20cb253
 
 
 
 
 
 
 
 
 
 
 
 
 
77f08ac
20cb253
 
cdf96a9
 
 
 
20cb253
 
 
 
 
 
 
 
 
 
 
 
 
cdf70d4
20cb253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96a9
1ecaafa
 
 
cdf96a9
1ecaafa
 
c6e4036
1ecaafa
cdf96a9
20cb253
 
 
 
 
 
 
 
 
ab34c21
cdf96a9
 
 
 
ab34c21
20cb253
ab34c21
 
 
20cb253
 
 
 
 
 
 
 
ab34c21
 
 
20cb253
 
 
 
 
ab34c21
 
cdf96a9
ab34c21
20cb253
ab34c21
 
 
 
 
 
 
 
 
 
 
cdf96a9
 
ab34c21
 
 
20cb253
 
 
 
 
 
 
 
 
 
ab34c21
20cb253
ab34c21
20cb253
 
 
ab34c21
 
20cb253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab34c21
 
 
 
 
 
 
 
 
 
20cb253
ab34c21
20cb253
 
 
ab34c21
 
 
 
 
 
 
 
 
 
 
20cb253
 
 
 
 
 
 
 
 
 
 
ab34c21
20cb253
cdf96a9
20cb253
 
 
 
 
1ecaafa
 
 
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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
import os, json, html, requests, re, random
import gradio as gr

APP_TITLE = "孙子你好(DeepSeek 多样化·诊断版)"

# 可通过 Secrets 覆盖:
#   DEEPSEEK_API_KEY   = sk-xxxxxxxx
#   DEEPSEEK_MODEL     = deepseek-chat(默认)
#   DEEPSEEK_BASE_URL  = https://api.deepseek.com/v1/chat/completions(多数账号)
#                        或 https://api.deepseek.com/chat/completions(少量旧线路)

MODEL_NAME = os.getenv("DEEPSEEK_MODEL", "deepseek-chat").strip() or "deepseek-chat"

# ===================== 系统提示词(增加 explain_cn 字段 + 做法语气要求) =====================
SYS_PROMPT = (
    "你是中文军师,擅长将《孙子兵法》的智慧用于现实问题。\n"
    "任务:针对用户的问题,从《孙子兵法》十三篇中找出最贴合的一章,引用原文(若能确定),并转化为现代生活策略。\n\n"
    "规则:\n"
    "1. 优先根据问题性质挑选不同篇章:\n"
    "   - 计划/评估:〈始计篇〉〈作战篇〉\n"
    "   - 谈判/攻防:〈谋攻篇〉〈虚实篇〉\n"
    "   - 节奏/风险:〈军形篇〉〈兵势篇〉\n"
    "   - 主动出击/竞争:〈军争篇〉〈九变篇〉〈地形篇〉\n"
    "   - 困境/沟通:〈行军篇〉〈九地篇〉〈用间篇〉〈火攻篇〉\n"
    "2. 若问题重复,尽量换不同篇章或换角度回答,保持多样性。\n"
    "3. 若无法百分之百确认原文或篇名,不要硬编,改为思想意译并标记 unsure=true。\n"
    "4. 严格仅输出 JSON,固定字段:\n"
    "   {\"chapter\":\"<篇名>\","
    "    \"quote\":\"<原文或思想>\","
    "    \"unsure\":false/true,"
    "    \"insight\":\"<一句人话启示≤24字>\","
    "    \"action\":\"<一步可执行的做法≤100字,用轻松幽默的口吻表达,但不要改变建议本身的含义>\","
    "    \"speech\":\"<直接可说的话术≤120字>\","
    "    \"explain_cn\":\"<用非常简单的话解释这句名言的大意≤60字>\"}\n"
    "5. 语气简洁、机智、委婉、有边界;不得人身攻击、威胁或违法建议。\n"
    "6. 禁止输出 JSON 以外的任何文字。"
)

# few-shot:覆盖多篇章,减少模型只用某一章的倾向
FEWSHOT = [
    # 加班 → 作战篇
    {
        "role": "user",
        "content": "问题:老板临时要我通宵加班,我状态很差,怎么体面拒绝?只输出JSON。"
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "chapter": "作战篇",
            "quote": "夫兵久而国利者,未之有也。",
            "unsure": False,
            "insight": "避免持久消耗,保整体战力。",
            "action": "跟老板提一个“明早高质量交付”的方案,比硬熬一整晚靠谱多了,也能保证你明天还是个清醒的人。",
            "speech": "今晚赶出来质量可能不稳,我建议明早整理成终版,保证更可靠,您看可以吗?",
            "explain_cn": "意思是战争拖太久没有好处,打仗要快准狠,不能一直消耗。"
        }, ensure_ascii=False)
    },
    # 借钱 → 始计篇
    {
        "role": "user",
        "content": "问题:朋友借钱不还,怎样体面要回?只输出JSON。"
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "chapter": "始计篇",
            "quote": "兵者,诡道也。",
            "unsure": False,
            "insight": "绕开硬催,设局顺势收回。",
            "action": "就说自己在清账、要安排资金,先把一部分或者分期要回来,既能把钱收回,又不至于把气氛搞得像开战。",
            "speech": "我这边在清账需要安排下,能否先转一部分?分期也行,别影响咱们交情。",
            "explain_cn": "打仗要讲策略,不是直来直去,要懂得绕弯子、用计谋。"
        }, ensure_ascii=False)
    },
    # 邻居噪音 → 行军篇
    {
        "role": "user",
        "content": "问题:邻居太吵,怎么友好沟通?只输出JSON。"
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "chapter": "行军篇",
            "quote": "知彼知己,百战不殆。(通用于沟通前的情势判断)",
            "unsure": True,  # 行军篇原文无此句,标不确定,演示 unsure 机制
            "insight": "先辨来源时机,再择机沟通。",
            "action": "挑对方不忙不生气的时候,先表示理解,再轻轻说出自己的困扰,让TA自己不好意思再把家当KTV用。",
            "speech": "最近晚上声音有点明显,我怕打扰到楼上楼下。能否帮忙晚间稍微注意下?谢谢啦~",
            "explain_cn": "先搞清楚对方情况和自己底牌,这样做事比较稳,不容易吃亏。"
        }, ensure_ascii=False)
    },
    # 职场竞争 → 军争篇
    {
        "role": "user",
        "content": "问题:同事跟我抢功劳,怎么应对?只输出JSON。"
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "chapter": "军争篇",
            "quote": "以迂为直,以患为利。",
            "unsure": False,
            "insight": "不硬碰,转势为己用。",
            "action": "开会前先发一份“小而精”的纪要,把自己负责的部分写清楚,让功劳自动写进流程里,对方想抢也得先面对记录。",
            "speech": "我把这次我的负责点和关键产出提前发一份纪要,方便会里对齐,避免遗漏。",
            "explain_cn": "看起来绕远路,其实是走捷径;把麻烦事变成自己有利的机会。"
        }, ensure_ascii=False)
    }
]

# ===================== 调用 DeepSeek:主分析 =====================
def call_deepseek(user_question: str) -> dict:
    """自动尝试两个端点,返回结构化兵法解析结果。"""
    key = os.getenv("DEEPSEEK_API_KEY", "").strip()
    if not key:
        return {
            "chapter": "",
            "quote": "未配置 DEEPSEEK_API_KEY",
            "unsure": True,
            "insight": "请先在 Settings → Secrets 配置密钥。",
            "action": "键名必须是 DEEPSEEK_API_KEY,值不要带引号或空格;配置后重启。",
            "speech": "密钥配好我就能正常回答。",
            "explain_cn": "当前还无法解释名言,因为没有配置密钥。"
        }

    endpoints = [
        (os.getenv("DEEPSEEK_BASE_URL", "").strip()
         or "https://api.deepseek.com/v1/chat/completions"),
        "https://api.deepseek.com/chat/completions",
    ]

    # 多样化提示:鼓励换篇章/换角度
    diversity_hint = random.choice([
        "尽量避免重复使用同一篇章,尝试换篇章回答。",
        "优先考虑《军形篇》《兵势篇》《九变篇》《地形篇》的角度。",
        "如果问题可多解,换一个不同篇章来解释。",
        "引用少见但贴合的篇章,保持回答多样性。",
        "可从节奏、态势、资源、时机等不同维度选篇章。"
    ])

    msgs = [{"role": "system", "content": SYS_PROMPT + "\n" + diversity_hint}] + FEWSHOT + [
        {"role": "user", "content": f"问题:{user_question} 只输出JSON。"}
    ]

    last_err = None
    for ep in endpoints:
        try:
            payload = {
                "model": MODEL_NAME,
                "messages": msgs,
                "temperature": 0.7,
                "top_p": 0.9,
                "max_tokens": 450
            }
            r = requests.post(
                ep,
                headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
                json=payload,
                timeout=60
            )
            print(f"[DeepSeek] endpoint={ep} status={r.status_code} model={MODEL_NAME}")  # Runtime logs

            if r.status_code // 100 != 2:
                try:
                    err_detail = r.json()
                except Exception:
                    err_detail = {"raw": r.text[:500]}
                last_err = f"HTTP {r.status_code} @ {ep} (model={MODEL_NAME}) → {err_detail}"
                continue

            data = r.json()
            content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
            m = re.search(r"\{.*\}", content, flags=re.S)
            js = m.group(0) if m else content
            result = json.loads(js)

            return {
                "chapter": result.get("chapter", "")[:20],
                "quote": result.get("quote", "")[:200],
                "unsure": bool(result.get("unsure", False)),
                "insight": result.get("insight", "")[:40],
                "action": result.get("action", "")[:120],
                "speech": result.get("speech", "")[:140],
                "explain_cn": result.get("explain_cn", "")[:80],
            }
        except Exception as e:
            last_err = f"{type(e).__name__}: {e}"

    return {
        "chapter": "",
        "quote": "为避免误引,已隐藏原文与篇名(模型不确定出典)。",
        "unsure": True,
        "insight": "DeepSeek 请求未成功。",
        "action": f"诊断信息:{last_err or '未知错误'}",
        "speech": "请按诊断检查:①密钥②端点③模型名④配额/限速,然后重启。",
        "explain_cn": "当前无法给出名言解释,请检查配置。"
    }

# ===================== 调用 DeepSeek:中译英翻译 =====================
def translate_to_en(text_cn: str) -> str:
    """使用 DeepSeek 将简单中文解释翻译成英文。"""
    key = os.getenv("DEEPSEEK_API_KEY", "").strip()
    if not key:
        return "⚠️ 未配置 DEEPSEEK_API_KEY,暂时无法翻译成英文。"

    endpoints = [
        (os.getenv("DEEPSEEK_BASE_URL", "").strip()
         or "https://api.deepseek.com/v1/chat/completions"),
        "https://api.deepseek.com/chat/completions",
    ]

    msgs = [
        {
            "role": "system",
            "content": "你是专业的中译英助手,只把用户提供的中文翻译成自然简洁的英文,不要添加解释或多余内容。"
        },
        {
            "role": "user",
            "content": text_cn
        }
    ]

    last_err = None
    for ep in endpoints:
        try:
            payload = {
                "model": MODEL_NAME,
                "messages": msgs,
                "temperature": 0.3,
                "top_p": 0.9,
                "max_tokens": 200
            }
            r = requests.post(
                ep,
                headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
                json=payload,
                timeout=60
            )
            print(f"[DeepSeek-Translate] endpoint={ep} status={r.status_code} model={MODEL_NAME}")

            if r.status_code // 100 != 2:
                try:
                    err_detail = r.json()
                except Exception:
                    err_detail = {"raw": r.text[:500]}
                last_err = f"HTTP {r.status_code} @ {ep} (model={MODEL_NAME}) → {err_detail}"
                continue

            data = r.json()
            content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
            return html.escape(content)
        except Exception as e:
            last_err = f"{type(e).__name__}: {e}"

    return f"⚠️ 翻译失败:{last_err or '未知错误'}"

# ===================== 渲染函数 =====================
def render_main(result: dict) -> str:
    """
    主体渲染(不包含话术示例、不包含“我是老外”解释)。
    默认严格校验:若 unsure=True 则隐藏原文与篇名。
    """
    unsure = bool(result.get("unsure"))
    chap = html.escape(result.get("chapter", ""))
    quote = html.escape(result.get("quote", ""))
    insight = html.escape(result.get("insight", ""))
    action = html.escape(result.get("action", ""))

    # 默认“严格出处校验”:模型不确定时,隐藏原文与篇名
    show_quote = not unsure

    parts = ["<div style='font-size:22px;line-height:1.7em;'>"]
    if show_quote:
        badge = "📜" if not unsure else "❓"
        chap_line = f"《{chap}》" if chap else "(未标明篇名)"
        parts.append(f"{badge} {chap_line}<br><br>")
        parts.append(f"「{quote}」<br><br>")
    else:
        parts.append("⚠️ 为避免误引,已隐藏原文与篇名(模型不确定出典)。<br><br>")

    parts.append(f"<b>启示:</b>{insight}<br><br>")
    parts.append(f"<b>做法:</b>{action}<br><br>")
    parts.append("</div>")
    return "".join(parts)


def render_speech(result: dict) -> str:
    """单独渲染话术示例区域。"""
    speech = html.escape(result.get("speech", "").strip())
    if not speech:
        return (
            "<div style='margin-top:8px;font-size:16px;color:#777;'>"
            "当前没有可用的「话术示例」,请先点击「生成兵法」。"
            "</div>"
        )
    return (
        "<div style='margin-top:12px;padding:12px 14px;border:1px dashed #e2d7f2;"
        "border-radius:10px;background:#fbf8ff;font-size:18px;line-height:1.7em;'>"
        "<div style='font-weight:600;margin-bottom:6px;'>💬 话术示例:</div>"
        f"<div>{speech}</div>"
        "</div>"
    )


def render_explain_cn(result: dict) -> str:
    """为“我是老外”按钮渲染特别简单的中文解释。"""
    explain_cn = html.escape(result.get("explain_cn", "").strip())
    if not explain_cn:
        return (
            "<div style='margin-top:8px;font-size:16px;color:#777;'>"
            "暂时没有找到这句名言的简单解释,请先生成一次兵法再试。"
            "</div>"
        )
    return (
        "<div style='margin-top:12px;padding:12px 14px;border:1px solid #d7e6ff;"
        "border-radius:10px;background:#f5f8ff;font-size:18px;line-height:1.7em;'>"
        "<div style='font-weight:600;margin-bottom:6px;'>🌏 给外国朋友解释:</div>"
        f"<div>{explain_cn}</div>"
        "</div>"
    )


def render_explain_en(text_en: str) -> str:
    """渲染英文翻译结果。"""
    return (
        "<div style='margin-top:8px;padding:12px 14px;border:1px dashed #cddfea;"
        "border-radius:10px;background:#f6f9ff;font-size:18px;line-height:1.7em;'>"
        "<div style='font-weight:600;margin-bottom:6px;'>🔁 英文解释:</div>"
        f"<div>{text_en}</div>"
        "</div>"
    )

# ===================== UI =====================
theme = gr.themes.Soft(primary_hue="pink", neutral_hue="slate")
CSS = """
body,.gradio-container{background:#f7f6fb!important;}
.header{font-weight:900;font-size:28px;color:#111;text-align:center;padding:24px 0;}
.card{background:#fff;border:1px solid #eddcf3;border-radius:14px;box-shadow:0 4px 18px rgba(0,0,0,.05);padding:16px;}
"""

with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS) as demo:
    gr.HTML("<div class='header'>孙子你好</div>")
    with gr.Group(elem_classes=["card"]):
        q = gr.Textbox(
            label="你的问题",
            placeholder="例如:朋友借钱不还;老板让我加班;邻居太吵;甲方拖款…",
            lines=3
        )

        # 保存最近一次 DeepSeek 返回的结构化结果,用于“话术示例”和“我是老外”
        state_result = gr.State({})

        # 首页:只显示两个按钮
        with gr.Row():
            b1 = gr.Button("🎯 生成兵法", variant="primary")
            b3 = gr.Button("🧹 清空")

        # 下面这些按钮一开始不显示,生成兵法后再出现
        with gr.Row():
            btn_speech = gr.Button("💬 话术示例", visible=False)
            btn_foreign = gr.Button("🌏 我是老外", visible=False)
            btn_translate = gr.Button("🔁 翻译成英文", visible=False)

        out_main = gr.HTML()
        out_speech = gr.HTML()
        out_explain_cn = gr.HTML()
        out_explain_en = gr.HTML()

        # ---------- 回调函数 ----------
        def on_gen(user_q):
            # 返回顺序:
            # out_main, out_speech, out_explain_cn, out_explain_en, state_result,
            # btn_speech_update, btn_foreign_update, btn_translate_update
            if not (user_q or "").strip():
                empty_msg = (
                    "<div style='font-size:16px;color:#777;'>"
                    "请先在上面输入你的问题,比如:朋友借钱不还怎么办?</div>"
                )
                hide = gr.update(visible=False)
                return empty_msg, "", "", "", {}, hide, hide, hide

            res = call_deepseek(user_q)
            main_html = render_main(res)
            # 生成新结果时,显示「话术示例」「我是老外」,但先不显示「翻译成英文」
            return (
                main_html,
                "",
                "",
                "",
                res,
                gr.update(visible=True),   # btn_speech
                gr.update(visible=True),   # btn_foreign
                gr.update(visible=False),  # btn_translate
            )

        def on_clear():
            # 清空输入框和所有输出 & 状态,并隐藏所有二级按钮
            hide = gr.update(visible=False)
            return "", "", "", "", "", {}, hide, hide, hide

        def on_show_speech(res):
            if not isinstance(res, dict) or not res:
                return (
                    "<div style='margin-top:8px;font-size:16px;color:#777;'>"
                    "请先点击「生成兵法」,再查看话术示例。</div>"
                )
            return render_speech(res)

        def on_show_foreign(res):
            # 返回:out_explain_cn, btn_translate_update
            if not isinstance(res, dict) or not res:
                msg = (
                    "<div style='margin-top:8px;font-size:16px;color:#777;'>"
                    "请先点击「生成兵法」,再点「我是老外」。</div>"
                )
                return msg, gr.update(visible=False)
            return render_explain_cn(res), gr.update(visible=True)

        def on_translate(res):
            if not isinstance(res, dict) or not res:
                return (
                    "<div style='margin-top:8px;font-size:16px;color:#777;'>"
                    "请先生成兵法并点击「我是老外」后,再翻译成英文。</div>"
                )
            text_cn = res.get("explain_cn", "").strip()
            if not text_cn:
                return (
                    "<div style='margin-top:8px;font-size:16px;color:#777;'>"
                    "当前没有可以翻译的中文解释,请先点「我是老外」。</div>"
                )
            text_en = translate_to_en(text_cn)
            return render_explain_en(text_en)

        # ---------- 事件绑定 ----------
        b1.click(
            on_gen,
            inputs=[q],
            outputs=[
                out_main,
                out_speech,
                out_explain_cn,
                out_explain_en,
                state_result,
                btn_speech,
                btn_foreign,
                btn_translate,
            ]
        )

        b3.click(
            on_clear,
            inputs=None,
            outputs=[
                q,
                out_main,
                out_speech,
                out_explain_cn,
                out_explain_en,
                state_result,
                btn_speech,
                btn_foreign,
                btn_translate,
            ]
        )

        btn_speech.click(
            on_show_speech,
            inputs=[state_result],
            outputs=[out_speech]
        )

        btn_foreign.click(
            on_show_foreign,
            inputs=[state_result],
            outputs=[out_explain_cn, btn_translate]
        )

        btn_translate.click(
            on_translate,
            inputs=[state_result],
            outputs=[out_explain_en]
        )

if __name__ == "__main__":
    demo.launch()