Rengoku / app.py
wy-wu's picture
Update app.py
5fa3493 verified
import gradio as gr
import random, re, datetime
# ===== 基本口號(備援) =====
CANON_ZH = [
"讓心燃燒起來!","竟然有此等之事!","我會履行我的職責!","向前看!","要堅強地活下去!","美味!",
"堂堂正正地活著!","我會守護大家!","立場不動搖!","弱者值得被守護!","絕不後退!","勇氣就在此刻!",
"把恐懼丟在身後!","正面迎上去!","把背打直!","光芒就在前方!","不必比較,只管前進!",
"即使受傷,也別讓心後退!","我相信你!","別害怕失敗!","努力不會白費!","堂堂正正的一擊!",
"挺起胸膛活下去吧!","這樣的事不會讓我的熱情消逝!我心中的火燄從未消失!",
"我的內心火焰從不曾熄滅,絕對不會因此受挫!","母親,我能成為這樣的人,是我的榮幸。",
]
OPENERS_SHORT = ["精神百倍!!","燃起來了!!","向陽而立!!","我在此!!","站穩立場!!","出發!!"]
# ===== 呼吸(招式+訓練) =====
BREATH_FORMS = [
"炎之呼吸・壹之型・不知火!","炎之呼吸・弐之型・昇炎斬!",
"炎之呼吸・参之型・氣炎萬象!","炎之呼吸・肆之型・盛炎之漩渦!",
"炎之呼吸・伍之型・炎虎!","炎之呼吸・奧義・煉獄!",
]
BREATH_TRAIN_ZH = [
"全集中・常中!","別讓呼吸亂掉。","用腹部吸氣,穩穩吐出。","站穩中軸,心不動搖。",
"肩放鬆,氣沉丹田。","節奏統一,像火焰一樣有規律。","吸四拍、停一拍、吐四拍。","意志領先,呼吸跟上。",
"把痛楚交給呼吸,讓心保持清明。","集中到指尖與腳跟,整個身體一起發力。",
]
# ===== 你的「劇情實句」:最高優先 =====
USER_DEMON = ["無論是老去還是死亡,都是人類這種短暫生物的美麗之處。"]
USER_FIGHTCALL = [
"我會履行我的職責!這裡的任何一個人都不會死去!",
"燃燒心靈,超越自我極限,我是炎柱煉獄杏壽郎!",
"不管是哪一場戰鬥,鬼殺隊永遠都在!",
]
USER_ENCOURAGE = [
"你絕對可以比昨天的自己更強大。",
"你要繼續成長下去,將來就由你成為鬼殺隊的柱;我相信你。",
"我相信著,打從心底相信你們。",
"這樣的事不會讓我的熱情消逝!我心中的火燄從未消失!",
]
USER_PRAISE = ["我相信著,打從心底相信你們。"]
USER_CHAT = [
"我的內心火焰從不曾熄滅,絕對不會因此受挫!",
"母親,我能成為這樣的人,是我的榮幸。",
]
USER_TAIL = [
"挺起胸膛活下去吧!",
"我會履行我的職責!這裡的任何一個人都不會死去!",
"不管是哪一場戰鬥,鬼殺隊永遠都在!",
]
# ===== 角色知識庫(lore)與劇透開關 =====
SPOILER_ON = True
LORE = [
{"triggers": ["你是誰","自我介紹","名字","稱號","炎柱","柱","鬼殺隊"],"spoiler": False,
"reply": ["我是炎柱・煉獄杏壽郎,使用炎之呼吸!","守護人們,履行職責——這就是我的道路!"]},
{"triggers": ["年齡","幾歲","歲"],"spoiler": False,"reply": ["我二十歲。","年齡只是數字,氣勢要永遠向前!"]},
{"triggers": ["個性","性格","特色","笑","熱血","開朗"],"spoiler": False,
"reply": ["我性格爽朗直率,時刻保持熱情與笑容!","堂堂正正地活著,讓心燃燒起來!"]},
{"triggers": ["父親","老爸","槙壽郎","爸爸"],"spoiler": False,
"reply": ["父親曾是炎柱,後來一度消沉。","無論如何,我會以自己的方式守護大家!"]},
{"triggers": ["母親","媽媽","瑠火","琉火"],"spoiler": False,
"reply": ["母親教我:『成為強者,就用力量去幫助弱者。』","我一直把這句話刻在心上!"]},
{"triggers": ["弟弟","千壽郎","弟"],"spoiler": False,"reply": ["我有個弟弟叫千壽郎。","我希望他筆直而堅強地活著!"]},
{"triggers": ["日輪刀","刀","武器","鍔"],"spoiler": False,"reply": ["我的日輪刀呈火紅色,護手像火焰。","外型熱烈,內心更要熾熱!"]},
{"triggers": ["無限列車","列車任務","乘客","猗窩座","上弦三","黎明"],"spoiler": True,
"reply": ["無限列車任務上,我選擇守住所有乘客。","即使面對上弦之三,也絕不讓任何人殞落!"]},
{"triggers": ["炭治郎","竈門","禰豆子","祢豆子"],"spoiler": False,
"reply": ["我認可竈門兄妹的意志與人性。","我相信他們會筆直地走在光明之中!"]},
]
# ===== 備援宣言池 =====
LINES_INSULT = [
"惡意到此為止;我以守護之火回應!","語言會灼傷人,但我選擇成為保護眾人的火焰!",
"把憤怒留在我這裡;你不必被它吞沒!","不讓誰再被言語踐踏!","正面與善意才會留下真正的力量!",
"用勇氣終止惡意的連鎖!","冷酷無益;我以熱度還擊!","這裡只容得下光明與正直!",
"我的內心火焰從不曾熄滅,絕對不會因此受挫!",
]
LINES_DEMON = [
"我選擇做人,永遠站在黎明的一方!","人類雖短暫,正因此更珍貴!",
"以人的姿態前進,這就是我的答案!","夜再長,也會迎來曙光!","不動搖;我的道路只有守護!",
"拒絕誘惑;把意志烙印在心上!","我會證明人心的火焰能照亮黑暗!","善良不是弱點,而是榮耀!",
]
LINES_FOOD = [
"美味讓心也發光,補給完畢,繼續前進!","好味道化成火力,路就打開了!",
"吃飽就能再戰一百回合!","把溫度吞進肚裡,讓步伐更穩!","一口下去,士氣滿格!",
"食物是夥伴的禮物;好好接住它!","把味道存進心裡,成為明天的燃料!","好料理,連陰影都被照亮!",
]
LINES_GREET = [
"把背打直,向前看!","夜晚有夜晚的任務;休息,明天再燃燒!",
"新的一天就在眼前,精神百倍!","把疲憊交給風,留下熱度給自己!","問候收下了;我會更熱血!",
"日出在即;腳步要有氣勢!","睡吧,直到心恢復成火焰!","早晨是勝利的開始!","挺起胸膛活下去吧!",
]
LINES_ENCOURAGE = [
"困難只是柴火,點燃就是推進力!","向前踏出,堂堂正正地走!",
"跌倒沒關係,站起來就贏過昨天!","恐懼是陰影,不是鐵牆!","把焦慮交給呼吸,讓意志領路!",
"一刀劈開猶豫;行動才是答案!","把眼神放亮,腳步自然跟上!","你擁有足以燃燒黑夜的火!",
"不求一次到位,只求不退一步!","把今天的你打磨得比昨天更亮!",
]
LINES_PRAISE = [
"你的光芒清楚可見!","做得很好,氣勢十足!","我看見你的努力,值得驕傲!",
"你比想像中更強!","把這份熱度保存好,路會自己出現!","你的堅定,像火柱一樣筆直!",
"好樣的!再多一寸就能突破!","你的步伐,是能鼓舞他人的節奏!",
]
LINES_FIGHTCALL = [
"就緒!炎之呼吸・壹之型・不知火!","膽怯退散;堂堂正正迎上去!",
"意志在前,刀隨之!","把破綻封死,只留正面突擊!","火勢抬高到頂點,貫穿!",
"對敵不退,對己不苟!","把每一口氣都變成力量!","眼神向前,讓手先到!",
]
LINES_QUESTION = [
"用火把猶豫燒盡,定下心來!","答案從立場誕生,別動搖地前進!",
"讓意志說話;行動就會跟上!","先穩住呼吸,再下判斷!","把答案握在手裡,而不是天上!",
"從你相信的開始做起!","當下就是最好時機!","把雜訊清除,只留方向!",
]
LINES_CHAT = [
"立場明確,路在前方!","只管讓心燃燒著前進!","今天也要堂堂正正!",
"我在這裡,熱度不滅!","把影子甩在身後!","不必猶豫;向光走!",
"心要像火一樣筆直!","每一步都算數!","勝利感是自己點燃的!","信念先行,萬事可破!",
]
# ===== emoji 與限制 =====
EMOJI = ["🔥","💥","✨","🌅","💪","🍱"]
EMOJI_RATE = 0.25
MAX_LINES = 2
# ===== 小工具(含強化防重複) =====
AVOID_LAST_N = 5 # 避免近 N 輪重複
def now_tpe(): return datetime.datetime.utcnow() + datetime.timedelta(hours=8)
def exclaim(s: str, intensity: int) -> str:
return s.rstrip("!") + ("!" * max(1, min(3, intensity // 3 + 1)))
def add_maybe_emoji(s: str) -> str:
return s + (" " + random.choice(EMOJI) if random.random() < EMOJI_RATE else "")
def recent_bots(history, n=AVOID_LAST_N):
return [(b or "") for _, b in (history or [])][-n:]
def pick_no_repeat(cands, history):
recents = "\n".join(recent_bots(history))
if not cands: return ""
random.shuffle(cands)
for c in cands:
if c not in recents:
return c
# 都用過就選出「近輪出現次數最少」的
counts = {c: recents.count(c) for c in cands}
return min(cands, key=lambda x: counts.get(x, 0))
def pick_user_first(user_pool, default_pool, history):
cand = pick_no_repeat(user_pool, history) if user_pool else ""
return cand or pick_no_repeat(default_pool, history)
# ===== 安全 =====
BLOCKED = ["未成年","國小","國中","高中","小學生","未滿18","16歲","17歲"]
def is_safe(text: str) -> bool:
if any(w in text for w in BLOCKED): return False
ages = re.findall(r"(\d{1,2})\s*歲", text)
return not any(int(a) < 18 for a in ages)
# ===== 意圖偵測(修正「成為鬼/我是鬼」等變體) =====
FOOD = ["吃","餐","飯","麵","拉麵","壽司","燒肉","咖哩","便當","甜點","奶茶","咖啡","好吃","美味"]
GREET = ["早安","晚安","安安","嗨","哈囉","你好","妳好","您好","午安"]
FIGHT = ["加油","打氣","我要努力","要衝","害怕","緊張","焦慮","我做不到","不會","怎麼辦","挑戰","面試","比賽","挫折","沮喪","低落"]
PRAISE = ["誇","稱讚","可愛","漂亮","帥","讚","鼓勵我","安慰我","辛苦了"]
INSULT = ["去死","滾","垃圾","閉嘴","笨","白癡","弱","廢物"]
DEMON_REGEX = re.compile(
r"(?:我.*是.*鬼(?:喔|哦)?|"
r"(?:變|成|當|做|成為|一起|邀|拉).*?鬼)"
)
LORE_WORDS = ["背景","故事","身世","家庭","父親","母親","弟弟","年齡","歲","名字","稱號",
"炎柱","柱","鬼殺隊","日輪刀","武器","鍔","無限列車","列車","猗窩座","上弦三",
"炭治郎","竈門","禰豆子","祢豆子","個性","性格","笑","熱血","開朗"]
def detect_intent(msg: str):
m = msg.strip()
if any(w in m for w in ["炎之呼吸","呼吸","全集中","出招","型"]): return "breath"
if m.startswith("/spoiler"): return "command"
if any(w in m for w in LORE_WORDS): return "lore"
if m.startswith("/quote"): return "quote"
if DEMON_REGEX.search(m): return "demon_invite"
if any(w in m for w in INSULT): return "insult"
if any(w in m for w in FOOD): return "food"
if any(w in m for w in GREET): return "greet"
if any(w in m for w in FIGHT): return "encourage"
if any(w in m for w in PRAISE): return "praise"
if "戰鬥" in m: return "fight_call"
if m.endswith("嗎") or "?" in m or "?" in m: return "question"
return "chat"
def infer_intensity(message, history):
msg = message or ""
intensity = 5 + min(5, msg.count("!") + msg.count("!") + (len(re.findall(r"[🔥💥✨]", msg)) * 2))
if any(w in msg for w in ["累","想睡","疲倦","受傷","難過","QQ","..."]): intensity = max(3, intensity - 2)
last_bot = (history[-1][1] if history else "") or ""
if last_bot.count("!") >= 6: intensity = max(intensity, 6)
return max(2, min(10, intensity))
# ===== 指令處理 =====
def handle_command(msg):
global SPOILER_ON
m = msg.strip().lower()
if m.startswith("/spoiler"):
if "on" in m: SPOILER_ON = True; return "劇情資訊已開啟。"
if "off" in m: SPOILER_ON = False; return "劇情資訊已關閉。"
return "用法:/spoiler on 或 /spoiler off"
return ""
# ===== lore 回覆 =====
def lore_reply(msg, history, intensity):
hits = []
for item in LORE:
if any(key in msg for key in item["triggers"]):
if item["spoiler"] and not SPOILER_ON: continue
hits.append(item)
if not hits:
return exclaim("我是一名以『守護』為信念的炎柱;家學炎之呼吸,自修也不曾懈怠!", intensity)
pick = random.choice(hits)
lines = pick["reply"][:2]
return "\n".join(exclaim(x, intensity) for x in lines)
# ===== 宣言式單句(你的台詞優先) =====
def line_by_intent(intent, intensity, history):
user_map = {
"demon_invite": USER_DEMON, "fight_call": USER_FIGHTCALL,
"encourage": USER_ENCOURAGE, "praise": USER_PRAISE, "chat": USER_CHAT,
}
default_map = {
"insult": LINES_INSULT, "demon_invite": LINES_DEMON, "food": LINES_FOOD,
"greet": LINES_GREET, "encourage": LINES_ENCOURAGE, "praise": LINES_PRAISE,
"fight_call": LINES_FIGHTCALL, "question": LINES_QUESTION, "chat": LINES_CHAT,
}
s = pick_user_first(user_map.get(intent, []), default_map[intent], history)
return exclaim(s, intensity)
# ===== 主回覆(≤2 句、無追問) =====
def line_breath(intensity, history):
form = pick_no_repeat(BREATH_FORMS, history)
guide = pick_no_repeat(BREATH_TRAIN_ZH, history)
a = exclaim(form, intensity)
b = exclaim(guide, max(3, intensity))
if random.random() < 0.30: b += " 讓心燃燒起來!"
return a + "\n" + b
def rengoku_reply(message, history):
msg = (message or "").strip()
if not is_safe(msg): return "⚠️ 這裡只進行成年且友善的對話。讓我們換個安全的主題吧!"
intent = detect_intent(msg)
intensity = infer_intensity(msg, history)
if intent == "command": return handle_command(msg)
if msg.startswith("/quote"):
user_all = USER_DEMON + USER_FIGHTCALL + USER_ENCOURAGE + USER_PRAISE + USER_CHAT
pool = user_all + CANON_ZH
picks = []
for _ in range(min(2, len(pool))):
cand = pick_no_repeat(pool, history)
while cand in picks and len(pool) > 1:
cand = pick_no_repeat(pool, history)
picks.append(cand)
return "\n".join(picks)
if intent == "breath": return line_breath(intensity, history)
if intent == "lore": return lore_reply(msg, history, intensity)
lines = []
if random.random() < 0.30:
lines.append(exclaim(pick_no_repeat(OPENERS_SHORT, history), intensity))
core = line_by_intent(intent, intensity, history)
core = add_maybe_emoji(core)
if random.random() < 0.20:
tail = pick_user_first(USER_TAIL, CANON_ZH, history)
if tail and tail not in core: core += " " + tail
lines.append(core)
tpe = now_tpe()
if tpe.weekday() == 4 and tpe.hour >= 17 and lines:
lines[-1] += " 週末也要讓心燃燒!"
return "\n".join(lines[:MAX_LINES])
# ===== Gradio ChatInterface =====
def chat(message, history):
return rengoku_reply(message, history)
demo = gr.ChatInterface(
fn=chat,
title="🔥鬼滅之刃 煉獄杏壽郎 風格機器人",
description="大哥沒有輸!來跟大哥學習「炎之呼吸」的精神吧!"
)
if __name__ == "__main__":
demo.launch()