PrimeBot 启元 Q1 & T1 技能整活工作台
PYTHON SKILL SOURCE · C位全能王剧本

《启元百变互动官》核心源码精讲

集齐「语义化动作表 + 随机好运盲盒 + 联网金句(带断网保底) + 呼吸灯 + 0.7s防抖播音」的万能模板

😜 调皮课代表大白话划重点(30 秒读懂核心)
这可是咱们工作台的当家花旦!全篇代码被切成了像乐高一样清楚的三块:第一块(第 15~65 行)是「傻瓜式积木配置区」——你想改台词、换算命签文、开联网或者关灯光,只管在这儿改几行中文字典;第二块是「防炸机硬件底座」——帮你盯紧嘴巴防吞字、查验 T1 是趴着还是站着、断网时 0.01 秒自动切回保底词;第三块是「三幕主演出流水线」!

🔍 核心机关与修改指南

🎨 第一块:积木配置区 改这里就行!

包含 ENABLE_NET_QUOTE 联网开关、LUCKY_POOL 随机签文池、OPENING_LINE 开场白以及 ACTION_TABLE 动作字典。不用懂底层硬件也能 3 分钟改出新技能!

🛡️ 第二块:0.7秒防抖与断网护盾 别乱动它!

speak() 负责盯着机器人开口出声且连续安静满 0.7 秒才放行;fetch_quote() 负责 2.5 秒限时联网,一旦断网立刻优雅返回 FALLBACK_QUOTE!

🎬 第三块:三幕剧主循环 丝滑演出

严格按照「先说完话 ➔ 再触发 play_animation(id, interrupt=False) ➔ 再 time.sleep(hold) 等动作做完」的黄金节奏推进,最后打印 UNIVERSAL_SKILL_DONE steps=3。

💻 完整源码与配置内容(共 184 行 · 支持行号定位与一键复制)

📄 共 184 行
1"""启元百变互动官(Q1 / T1 通用全能技能模板,纯 Python 3.10 标准库)。
2
3设计目标:
41. 开箱即用:可直接打包部署到 Q1 或 T1 真机运行,完成「迎宾 → 每日金句(联网+断网兜底) → 随机抽签互动 → 谢幕」完整闭环。
52. 极易改编:只需修改顶部的配置区(台词、动作别名、抽签池、是否开启联网/灯光),即可快速改成迎宾、算命、播报、小剧场等任意技能。
63. 严守契约:不导入 robot / ROS2,内蔵 TTS 0.7 秒防抖轮询与超时保护,自动适配 Q1 upper_body 与 T1 双足/四足动作白名单。
7"""
8
9import json
10import random
11import time
12import urllib.request
13
14# ==============================================================================
15# 【第一部分:积木配置区 —— 改这里就能变成你的新技能!】
16# ==============================================================================
17
18# 构建脚本 build.py 会在打包时自动将此值替换为 "q1" 或 "t1"
19ROBOT_MODEL = "q1"
20
21# 功能开关
22ENABLE_NET_QUOTE = False # True=尝试联网拉取每日一言(失败自动用离线兜底);False=纯离线最稳模式
23ENABLE_RANDOM_DRAW = True # True=从 LUCKY_POOL 随机抽签;False=固定使用第一支签
24ENABLE_MOOD_EFFECTS = True # True=尝试联动呼吸灯带与面部表情(自带安全容错)
25
26# 联网 API 配置与断网兜底金句
27QUOTE_API_URL = "https://v1.hitokoto.cn/?c=i&encode=json"
28FALLBACK_QUOTE = "代码写得稳,真机跑得准,每一次动手都是超酷的创造!"
29
30# 随机互动签文池:(签文台词, 动作语义名)
31LUCKY_POOL = (
32 ("叮!为你抽到「真机丝滑上上签」!今天你的所有动作指令全部一次通过!", "cheer"),
33 ("叮!为你抽到「脑洞大开幸运签」!你的创意全场最亮眼,评委看了都点赞!", "heart"),
34 ("叮!为你抽到「零Bug欧皇签」!编译不报错,网络不超时,好运直接拉满!", "greet"),
35)
36
37# 主流程固定剧本:(动作语义名, 台词文本, 备注说明)
38# 支持占位符:{quote} 会自动填入联网/兜底金句,{lucky_text} 会填入抽签结果
39OPENING_LINE = "哈喽大家好!我是启元百变互动官,很高兴在活动现场见到你!"
40CLOSING_LINE = "今日互动圆满完成!祝你玩得开心,作品大放异彩,我们下次见!"
41
42# 语义化动作映射表:把好记的名字映射到 Q1 / T1 的安全动作 ID 和保持秒数 (动作ID, 等待秒数)
43# - Q1 严格只用室内模式允许的 upper_body 动作(3000 系)
44# - T1 四足/双足均默认使用四足白名单兼容动作(26 喜悦 / 43 律动),也可在 biped 下扩展
45ACTION_TABLE = {
46 "q1": {
47 "greet": (3001, 6.5), # 招手
48 "cheer": (3008, 6.5), # 加油
49 "heart": (3007, 6.5), # 双手比心
50 },
51 "t1_quadruped": {
52 "greet": (26, 16.0), # 喜悦(四足安全白名单)
53 "cheer": (43, 16.0), # 律动(四足安全白名单)
54 "heart": (26, 16.0), # 喜悦(四足安全白名单)
55 },
56 "t1_biped": {
57 "greet": (26, 16.0), # 喜悦(双足同样安全可用)
58 "cheer": (43, 16.0), # 律动
59 "heart": (26, 16.0), # 喜悦
60 },
61}
62
63POLL_SECONDS = 0.1
64IDLE_SECONDS = 0.7
65
66
67# ==============================================================================
68# 【第二部分:安全硬件底座 —— 防吞字轮询、形态校验、网络兜底(一般无需修改)】
69# ==============================================================================
70
71def audio_state() -> int:
72 """读取并校验音频状态:1=播放中,2=空闲。"""
73 snapshot = robot.get_audio_play_state()
74 if isinstance(snapshot, dict):
75 state = snapshot.get("state")
76 if isinstance(state, dict) and state.get("value") in (1, 2):
77 return state["value"]
78 raise RuntimeError("AUDIO_STATE_INVALID")
79
80
81def speak(text: str) -> None:
82 """说话并等待真实播完(含 4 秒起声监控 + 0.7 秒防抖滤波 + 超时保护)。"""
83 print("SPEAK " + text, flush=True)
84 robot.play_tts(text)
85 start_deadline = time.monotonic() + 4.0
86 while audio_state() != 1:
87 if time.monotonic() >= start_deadline:
88 raise RuntimeError("TTS_NOT_STARTED")
89 time.sleep(POLL_SECONDS)
90
91 finish_deadline = time.monotonic() + max(len(text) * 0.45, 4.0) + 8.0
92 idle_since = None
93 while time.monotonic() < finish_deadline:
94 if audio_state() == 1:
95 idle_since = None
96 elif idle_since is None:
97 idle_since = time.monotonic()
98 elif time.monotonic() - idle_since >= IDLE_SECONDS:
99 return
100 time.sleep(POLL_SECONDS)
101 raise RuntimeError("TTS_NOT_FINISHED")
102
103
104def trigger_mood(color: str = "#FF6B2B", mode: int = 3) -> None:
105 """可选的灯带氛围触发(带安全容错,不影响主流程与离线替身测试)。"""
106 if not ENABLE_MOOD_EFFECTS:
107 return
108 play_light = getattr(robot, "play_light", None)
109 if callable(play_light):
110 try:
111 play_light(color, mode)
112 except Exception as exc:
113 print("LIGHT_SKIP " + str(exc), flush=True)
114
115
116def fetch_quote() -> str:
117 """获取今日金句:若开启联网则请求 API,超时或断网时 100% 自动回退到离线金句。"""
118 if not ENABLE_NET_QUOTE:
119 return FALLBACK_QUOTE
120 try:
121 req = urllib.request.Request(QUOTE_API_URL, headers={"User-Agent": "PrimeBot-Skill/1.0"})
122 with urllib.request.urlopen(req, timeout=2.5) as resp:
123 data = json.loads(resp.read().decode("utf-8"))
124 hitokoto = data.get("hitokoto")
125 if isinstance(hitokoto, str) and hitokoto.strip():
126 return hitokoto.strip()
127 except Exception as exc:
128 print("NET_FALLBACK " + str(exc), flush=True)
129 return FALLBACK_QUOTE
130
131
132def resolve_action_profile() -> dict:
133 """根据机型(Q1 / T1)与实时形态(biped / quadruped)选择安全动作表。"""
134 if ROBOT_MODEL == "q1":
135 return ACTION_TABLE["q1"]
136 if ROBOT_MODEL == "t1":
137 form = robot.get_t1_robot_form()
138 if form not in ("biped", "quadruped"):
139 raise RuntimeError("T1_FORM_UNKNOWN")
140 print("T1_FORM " + form, flush=True)
141 return ACTION_TABLE["t1_" + form]
142 raise RuntimeError("ROBOT_MODEL_UNKNOWN")
143
144
145# ==============================================================================
146# 【第三部分:主演出流程】
147# ==============================================================================
148
149def main() -> None:
150 profile = resolve_action_profile()
151 print("UNIVERSAL_SKILL_START model=" + ROBOT_MODEL, flush=True)
152 trigger_mood("#FF6B2B", 3)
153
154 # 1. 开场问候
155 speak(OPENING_LINE)
156
157 # 2. 组装三幕互动内容(迎宾问候 → 今日灵感金句 → 幸运抽签)
158 quote_text = fetch_quote()
159 lucky_line, lucky_action = (
160 random.choice(LUCKY_POOL) if ENABLE_RANDOM_DRAW else LUCKY_POOL[0]
161 )
162 steps = (
163 ("greet", "第一幕,元气问候。先挥个手,给你打满今日份的开发者Buff!"),
164 ("cheer", "第二幕,灵感广播。送你一句话:" + quote_text),
165 (lucky_action, "第三幕,好运盲盒。" + lucky_line),
166 )
167
168 # 3. 逐幕执行:先说话播完 → 再触发安全动作 → 等待动作完成
169 for index, (action_key, line) in enumerate(steps, 1):
170 speak(line)
171 animation_id, hold = profile[action_key]
172 accepted = robot.play_animation(animation_id, interrupt=False)
173 print("ANIMATION step=%d key=%s id=%d accepted=%s" % (index, action_key, animation_id, accepted), flush=True)
174 if not accepted:
175 raise RuntimeError("ANIMATION_REJECTED id=%d" % animation_id)
176 time.sleep(hold)
177
178 # 4. 收场谢幕与完成标记
179 speak(CLOSING_LINE)
180 print("UNIVERSAL_SKILL_DONE steps=%d" % len(steps), flush=True)
181
182
183# 机器人运行时在注入全局 robot 对象后直接执行
184main()