Spaces:
Runtime error
Runtime error
Download app.py from sofiamak/BingfaGenerator: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/spaces/sofiamak/BingfaGenerator/resolve/main/app.py
- Command line
-
hf download hf://spaces/sofiamak/BingfaGenerator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sofiamak/BingfaGenerator/resolve/main/app.py
20.3 kB
| 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() |