🔍 核心机关与修改指南
⚡ 30秒极速改台词 STEPS 元组
第 14~18 行的 STEPS 元组里放着三句充电台词,直接把文字改成你的段子、谜语或祝福词就能立刻打包!
🤖 双机型安全动作表 MOTIONS 字典
第 20~23 行定义了 Q1(3001/3008/3007,保持 7 秒)和 T1(26/43/26,保持 18 秒),choreography() 会自动校验 T1 形态。
🗣️ 工业级防吞字轮询 speak() 函数
第 35~55 行实现了带 4 秒起声监控与 0.7 秒防抖滤波的 speak(),彻底告别长句换气被截断的尴尬。
💻 完整源码与配置内容(共 78 行 · 支持行号定位与一键复制)
| 1 | """社交能量补给站:基于官方示例的 TTS 轮询与动作编排,纯标准库。""" |
| 2 | |
| 3 | import time |
| 4 | |
| 5 | ROBOT_MODEL = "q1" |
| 6 | POLL_SECONDS = 0.1 |
| 7 | IDLE_SECONDS = 0.7 |
| 8 | STEPS = ( |
| 9 | "第一格,勇气充电。今天你已经迈出了第一步,给自己加个油!", |
| 10 | "第二格,灵感充电。先让一个小点子跑起来,再慢慢把它变精彩!", |
| 11 | "第三格,快乐充电。请收下这份机械式认真,和百分之百的祝福!", |
| 12 | ) |
| 13 | # 等待时长为保守初值,现场按动作权威表、任务日志与实际观感调整。 |
| 14 | MOTIONS = { |
| 15 | "q1": ((3001, 7.0), (3008, 7.0), (3007, 7.0)), |
| 16 | "t1": ((26, 18.0), (43, 18.0), (26, 18.0)), |
| 17 | } |
| 18 | |
| 19 | |
| 20 | def audio_state() -> int: |
| 21 | snapshot = robot.get_audio_play_state() |
| 22 | if isinstance(snapshot, dict): |
| 23 | state = snapshot.get("state") |
| 24 | if isinstance(state, dict) and state.get("value") in (1, 2): |
| 25 | return state["value"] |
| 26 | raise RuntimeError("AUDIO_STATE_INVALID") |
| 27 | |
| 28 | |
| 29 | def speak(text: str) -> None: |
| 30 | """必须观测到起声及稳定空闲;超时终止,避免下一句截断当前音频。""" |
| 31 | print("SPEAK " + text, flush=True) |
| 32 | robot.play_tts(text) |
| 33 | start_deadline = time.monotonic() + 4.0 |
| 34 | while audio_state() != 1: |
| 35 | if time.monotonic() >= start_deadline: |
| 36 | raise RuntimeError("TTS_NOT_STARTED") |
| 37 | time.sleep(POLL_SECONDS) |
| 38 | |
| 39 | finish_deadline = time.monotonic() + max(len(text) * 0.45, 4.0) + 8.0 |
| 40 | idle_since = None |
| 41 | while time.monotonic() < finish_deadline: |
| 42 | if audio_state() == 1: |
| 43 | idle_since = None |
| 44 | elif idle_since is None: |
| 45 | idle_since = time.monotonic() |
| 46 | elif time.monotonic() - idle_since >= IDLE_SECONDS: |
| 47 | return |
| 48 | time.sleep(POLL_SECONDS) |
| 49 | raise RuntimeError("TTS_NOT_FINISHED") |
| 50 | |
| 51 | |
| 52 | def choreography() -> tuple: |
| 53 | if ROBOT_MODEL == "t1": |
| 54 | form = robot.get_t1_robot_form() |
| 55 | if form not in ("biped", "quadruped"): |
| 56 | raise RuntimeError("T1_FORM_UNKNOWN") |
| 57 | # 两种形态均使用 26/43,属于手册所列的四足允许集合。 |
| 58 | print("T1_FORM " + form, flush=True) |
| 59 | return MOTIONS[ROBOT_MODEL] |
| 60 | |
| 61 | |
| 62 | def main() -> None: |
| 63 | motions = choreography() |
| 64 | print("ENERGY_STATION_START model=" + ROBOT_MODEL, flush=True) |
| 65 | speak("欢迎来到社交能量补给站!不用扫码,送你三格好心情。") |
| 66 | for index, (text, (animation_id, hold)) in enumerate(zip(STEPS, motions), 1): |
| 67 | speak(text) |
| 68 | accepted = robot.play_animation(animation_id, interrupt=False) |
| 69 | print("ANIMATION step=%d id=%d accepted=%s" % (index, animation_id, accepted), flush=True) |
| 70 | if not accepted: |
| 71 | raise RuntimeError("ANIMATION_REJECTED id=%d" % animation_id) |
| 72 | time.sleep(hold) |
| 73 | speak("充电完成!愿你带着自己的点子出发。掌声送给每一个敢动手的人!") |
| 74 | print("ENERGY_STATION_DONE steps=%d" % len(STEPS), flush=True) |
| 75 | |
| 76 | |
| 77 | # 与官方示例一样,在运行时 exec 注入 robot 后直接执行。 |
| 78 | main() |