feat(mtc): .mt 脚本编译器落地 + pybot 扬声器静音语义

mtc 新模块:命令式动作脚本(Python DSL)→ .mt 矩阵编译器,非阻塞时间轴 IR(重叠截断/rest 静止值)+ 10ms 栅格化,产物可由 pybot/bot4cj 直接执行;34 测试全绿(含 pybot parser e2e)。

pybot speaker.py:frequency<=0 改为静音节拍(仅 sleep 保时序,不再钳到 min_f 低鸣),对齐仓颉版 sleep 占位语义;mtc Beep 通道 rest=0,段前/段间/段后回落静音,两侧语义闭环。

修复 test_actpred_compatibility 过时断言(99c1bc1 首行分解为重复微步 19→20 帧时漏改,14→15)。

文档三处同步:根 README 回填 memdb/pybot/actpred 并新增 mtc(模块表+端口一览+章节),MODULES/ARCHITECTURE 同步 mtc,新建 mtc/README.md。
This commit is contained in:
qing committed 2026-08-15 13:54:47 +08:00
1 parent 205a60f039
commit 01126a5786
18 files changed
+1125 -7

No files matched your search

+14 -2
View File
@@ -7,7 +7,8 @@
dry_run 模式下仅记日志立即返回(不 sleep、不出声)。
时序约定:play() 阻塞总耗时对齐 delay_ms(与仓颉版 sleep(delay) 节拍一致),
播放提前结束则补足剩余时间。
播放提前结束则补足剩余时间。frequency <= 0 为静音节拍(不出声仅保节拍,
对齐仓颉版 sleep 占位语义),正频率仍按 [min_f, max_f] 钳制。
"""
import logging
@@ -44,7 +45,18 @@ class Speaker:
log.warning("aplay not found, speaker degrades to silent sleep")
def play(self, frequency, delay_ms):
"""播放 frequency Hz 正弦波 delay_ms 毫秒(阻塞)"""
"""播放 frequency Hz 正弦波 delay_ms 毫秒(阻塞)
frequency <= 0 为静音节拍:不出声、仅保持 delay 时序(对齐
仓颉版 sleep 占位语义)。此前 0Hz 会被钳制到 min_f 发出低鸣,
破坏了 .mt 首行 freq=0(jump.mt/std.mt)的静默意图。
"""
if frequency <= 0: # 静音节拍
if self.dry_run:
log.info("speaker(dry-run): silence %dms", delay_ms)
return
time.sleep(delay_ms / 1000.0)
return
# 频率钳制(与仓颉版一致)
if frequency > self.max_f:
frequency = self.max_f
+3 -2
View File
@@ -439,8 +439,9 @@ def test_actpred_compatibility(tmp_path, mock_i2c):
from dataset import ActionDataset
ds = ActionDataset(str(tmp_path), history_len=5,
action_dim=18, frame_len=3)
# 19 帧,start=max(5,2)=5,samples: t=5..18 → 14 个样本
assert len(ds) == 14
# 20 帧(10 行 × 2 微步,99c1bc1 起首行也分解为重复步),
# start=max(5,2)=5,samples: t=5..19 → 15 个样本
assert len(ds) == 15
sample = ds[0]
# ActionDataset 默认 resize 到 640×480,与相机原始尺寸无关
assert sample["images"].shape == (3, 3, 480, 640) # (F, C, H, W)
+82
View File
@@ -0,0 +1,82 @@
"""Speaker 测试:静音语义 / 频率钳制 / SpeakerVar 参数透传
静音语义(2026-08-15 修复):frequency <= 0 不出声、仅保持 delay 节拍
(对齐仓颉版 sleep 占位语义);此前 0Hz 被钳到 min_f 发出 20Hz 低鸣,
破坏了 .mt 首行 freq=0(jump.mt/std.mt)的静默意图。
"""
import logging
import pytest
from pybot.hardvars import SpeakerVar
from pybot.hd.speaker import Speaker
class _FakeSpeaker:
"""记录 play 调用的桩(SpeakerVar 透传测试用)"""
def __init__(self):
self.calls = []
def play(self, freq, delay_ms):
self.calls.append((freq, delay_ms))
# ---------------- 静音语义 ----------------
def test_zero_frequency_is_silent_and_keeps_delay(monkeypatch):
"""freq=0:不合成波形、不播放,仅 sleep 保持节拍"""
spk = Speaker(dry_run=False)
slept, synths = [], []
monkeypatch.setattr("pybot.hd.speaker.time.sleep", lambda s: slept.append(s))
monkeypatch.setattr(spk, "_synth", lambda *a: synths.append(a) or b"")
spk.play(0, 100)
assert slept == [0.1]
assert synths == [] # 未走波形合成路径
def test_negative_frequency_is_silent(monkeypatch):
spk = Speaker(dry_run=False)
slept = []
monkeypatch.setattr("pybot.hd.speaker.time.sleep", lambda s: slept.append(s))
spk.play(-5, 50)
assert slept == [0.05]
def test_zero_frequency_dry_run_logs_silence(caplog):
"""dry_run:仅记日志立即返回(不 sleep),日志标识 silence"""
spk = Speaker(dry_run=True)
with caplog.at_level(logging.INFO, logger="pybot.speaker"):
spk.play(0, 100)
assert "silence 100ms" in caplog.text
assert "Hz" not in caplog.text
# ---------------- 正频率行为不破坏 ----------------
def test_positive_frequency_still_clamped(caplog):
"""正频率钳制不变:5Hz → min_f(20),25000Hz → max_f"""
spk = Speaker(dry_run=True, min_f=20.0, max_f=5000.0)
with caplog.at_level(logging.INFO, logger="pybot.speaker"):
spk.play(5, 10)
spk.play(25000, 10)
assert "20.0Hz 10ms" in caplog.text
assert "5000.0Hz 10ms" in caplog.text
# ---------------- SpeakerVar 透传 ----------------
def test_speakervar_passes_zero_frequency_through():
""".mt 行 freq=0 经 SpeakerVar 原样透传到 Speaker.play(进入静音分支)"""
fake = _FakeSpeaker()
var = SpeakerVar("spk", fake)
assert var.play({"frequency": "0", "delay": "100"}) == {"status": "OK"}
assert fake.calls == [(0.0, 100)]
def test_speakervar_requires_frequency_key():
"""缺 frequency 键仍 KeyError(契约不变:delay 列必须与 frequency 成对)"""
var = SpeakerVar("spk", _FakeSpeaker())
with pytest.raises(KeyError):
var.play({"delay": "100"})