| 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 = [ |
| "挺起胸膛活下去吧!", |
| "我會履行我的職責!這裡的任何一個人都不會死去!", |
| "不管是哪一場戰鬥,鬼殺隊永遠都在!", |
| ] |
|
|
| |
| 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_RATE = 0.25 |
| MAX_LINES = 2 |
|
|
| |
| AVOID_LAST_N = 5 |
| 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 "" |
|
|
| |
| 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) |
|
|
| |
| 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]) |
|
|
| |
| def chat(message, history): |
| return rengoku_reply(message, history) |
|
|
| demo = gr.ChatInterface( |
| fn=chat, |
| title="🔥鬼滅之刃 煉獄杏壽郎 風格機器人", |
| description="大哥沒有輸!來跟大哥學習「炎之呼吸」的精神吧!" |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|