LeaScript 是兰栖科技(Leagor)开源的动作脚本规范。它专为端侧离线视觉动作识别设计,底层支持 Mediapipe,全程无需联网,确保数据隐私安全。其完整工具链(包含 Launcher 编辑器与 kDesktop 跟练app)已向社区完全开源。
一、<workout>
<workout> 是一个 .cfg 文件的最外层标签,一个配置文件有且仅有一个 <workout> 块,它对应着一套完整的“操”(如俯卧撑、平板撑或肩颈操)。 在《LeaScript 的分层架构》中,<workout> 属于中间层(序列层)。
workout下存在三个字段:id、name、version。
1.1 动作脚本 ID(id)命名规范与逻辑定义
1.1.1 基础格式与字符限制
动作脚本中的 ID 字段是脚本的唯一标识符,须遵循以下硬性规则:
- 字符范围:仅允许使用英文字母(大小写不敏感,要以它命名文件,在windows,文件名是大小写不敏感)、数字(0-9)以及下划线 _。
- 长度限制:最大长度不得超过 24 字节。
1.1.2 文件关联
ID 直接映射脚本的物理文件名。引擎在加载脚本时,会在动作脚本文件名后附加 .cfg 后缀。即:动作脚本文件名的完整格式为 ID.cfg。
1.1.3 命名结构(防冲突机制)
为避免不同开发方或用户之间产生 ID 冲突(保证全局唯一性),ID 被强制要求由至少两段组成,段与段之间用下划线 _ 分隔:
- 第一段(必需):厂家标识(如 leagor)。代表该脚本的提供方或作者。
- 第二段(必需):产品/动作标识(如 pushup、plank)。代表该操的具体动作类型。
- 第三段(可选):差异化标识(如 90s、v2)。用于区分同一动作的不同时长、难度或版本。
示例:
- leagor_pushup(leagor 提供的标准俯卧撑)
- leagor_plank(leagor 提供的标准平板撑)
- leagor_plank_90s(leagor 提供的 90秒 平板撑进阶版)
1.1.4 “同一种操”的归并逻辑(核心历史统计规则)
在用户打卡记录、运动时长累计、历史次数统计等业务场景中,存在多个脚本属于“同一种操”的情况。为了解决这个问题,系统采用“前缀匹配”的判定规则:
- 归并规则:提取 ID 的前两段(即 厂家标识_动作标识)作为“操种核心键”。
- 判定标准:只要两个脚本的 前两段完全一致,即使第三段不同,系统在统计时也将其视为同一种操,相应的运动时长和次数将累加在一起。
场景举例:
- 脚本 A:leagor_plank(60秒)
- 脚本 B:leagor_plank_90s(90秒)
- 脚本 C:leagor_plank_v2(进阶版)
判定结果:上述三个脚本在底层数据库和统计引擎中,都将被视为 leagor_plank 这一种操。当用户完成一次 B 脚本(90秒),他的“平板撑”历史总时长将增加 90 秒,完成次数也相应增加 1 次。
1.2 版本号 (version)
版本号 version 并非动作脚本本身的“内容版本”,而是指 LeaScript 底层引擎源码的版本号,由脚本编辑器自动生成。launcher(编辑器)与 kdesktop(手机端跟练)共享同一套引擎源码,因此该字段用于标识解析和执行当前动作脚本所需的引擎版本。
- 格式示例:version="1.0.1-20260903"
- 字段解析:该值通常由“语义化版本号”和“发布/构建日期”拼接而成。例如,1.0.1 代表主版本.次版本.修订号,20260903 代表该引擎是 2026 年 9 月 3 日编译发布的构建版本。
- 工程意义:当不同设备上的引擎源码发生升级或变更时,通过读取此版本号,可以确保动作脚本与运行引擎的兼容性。如果动作脚本使用了新版引擎才支持的特性(如新增的 ang_range),而运行端引擎版本过旧,脚本将无法被正确解析。
二、LeaScript 的分层架构:从“原子”到“课程”
在深入 LeaScript 的语法细节之前,我们必须先了解它的顶层设计逻辑。LeaScript 不仅仅是一套描述姿态和动作的“脚本语言”,它更是一套面向内容工业化生产的标准化架构。
我们将系统的设计划分为三大层级,每一层都独立并服务于上一层:
2.1 系统架构总览
- 第一层(原子层):动作库 action_tpl2s.cfg
载体:标准动作模板 <action_tpl2>。
特性:纯粹复用。包含单个动作(如俯卧撑、平板撑、卷腹)的核心姿态阈值、计次/计时逻辑及固定的教学语音提示。它是系统中不可再拆分的“标准动作单元”。 - 第二层(中间层):健身操 .cfg
载体:运动脚本 <workout>(即目前 LeaScript 的核心内容)。
特性:序列编排。UP主在 Launcher 中通过“粘贴动作”功能,从动作库(原子层)中挑选动作,并按顺序排列、设置次数(如:俯卧撑8个 + 平板撑60秒),组合成一套完整的“操”或“跟练模板”。 - 第三层(顶层):课程 .course
载体:未来新增的课程计划文件。
特性:时间规划。UP主无需再面对复杂的姿态和阈值,只需直接引用已经编排好的“操”(.cfg),并按天(Day)顺序排列。例如:“15天瘦腰肚子计划”可由几个固定的“操”循环组合而成。
2.2 为什么采用这种“三层架构”?
这种分层架构拥有很好的扩展性和解耦性:
- 极大降低创作门槛:UP主无需深入骨骼点、角度阈值等底层算法,他们只需在做好的“动作库”里挑动作,就能拼出“操”;再挑“操”,就能拼出“课程”。
- 彻底分离关注点:
基础层负责“动作标不标准”(由算法工程师维护);
中间层负责“动作组合好不好”(由教练/UP主维护);
顶层负责“学习计划科不科学”(由课程策划维护)。 - 无缝对接应用生态:底层引擎只需运行 .cfg;顶层的 .course 仅作为调度器,读取并执行底层的 .cfg 文件。三层完全解耦,互不干扰。
本篇核心说明:本篇文章接下来的内容,将重点详解第一层(动作库)和第二层(<workout> / <state2> 语法)的具体实现。关于第三层(课程编排)的详细数据结构,将在后续的专项文档中进行阐述。
三、<state2>:状态与任务机制
3.1 状态机与 <state2> 标签

在图1,动作脚本是一个直观的“流程图”,这在底层的实现中被称为状态机(State Machine)。脚本由若干个 <state2>(状态)模块按顺序串联而成,每一个方框代表脚本在某一时刻要做的一件事。
以俯卧撑为例:脚本从“播报开场”开始,接着进入“定位”(需要摆放手机和站位),确认后播报口令“开始”,随后进入核心的“做8个俯卧撑”环节,最后播报“已结束”。
每个状态内部执行什么,以及完成后去往哪里,都是通过编写 <state2> 及其子标签来定义的。
命名说明:使用 <state2> 而非 <state> 作为标签名,是为了在 C++ 底层代码中能通过 class tstate2 直接映射该标签,避免与常见系统保留字冲突。
脚本执行引擎将按编写 <state2> 的出现顺序将其编入状态数组(索引从 0 开始)。状态机将按照索引顺序依次流转。
3.1.1 状态的两大分类
根据 <state2> 内部是否包含 <track_pose>(姿态判断块),状态分为两类:
- 姿态状态:包含 <track_pose> 块。这类状态用于执行实时的姿态跟踪与纠正。
- 非姿态状态:不包含 <track_pose> 块。目前此类状态仅用于执行纯语音播报任务。
3.2 核心任务块 <task>
每个 <state2> 内部必须且只能包含一个 <task> 块。<task> 定义了在该状态期间引擎需要执行的具体任务。根据 type 属性的不同,任务分为三种类型。
3.2.1 类型 speak:语音播报任务
适用状态:仅限非姿态状态。
作用:负责向用户播报指定的引导语音或结束语。
可用字段:
- msgstr (必填):指定要播报的语音文本字符串,不可为空。
- repeat_s (可选):若设置此字段,系统将每隔设定的秒数重复播报一次 msgstr。常用于静态引导或最终结束状态。
- min_state_duration_s (可选):若设置此字段,状态至少需要持续设定的秒数后,才会触发状态结束判断。
任务结束条件(必须同时满足):
- 语音播放器处于空闲状态(!is_speaking(),即语音已播完)。
- 字段 repeat_s 未被设置(若设置 repeat_s,任务视为无限循环任务,不会自动结束)。
- 若设置了 min_state_duration_s,当前状态已持续时长大于等于该设定值。
3.2.2 类型 time_counter:计时任务
适用状态:仅限姿态状态。
作用:在一个固定的总时长或满足时长内,持续检测用户姿态。
可用字段:
- max_count (必填):指定的总计时长,单位为秒。当计时达到此数值时,任务结束。
- rule (必填):计时规则。定义任务期间如何计时。
total:固定总时长。时间到即结束。
satisfied:纯满足时长。仅累计姿态达标的时长;不达标时暂停计时。
strict:严格满足时长。姿态不满足超过一定时间(如3秒),当前计时归零。待姿态再次满足后,从max_count秒重新开始倒计时。 - tone (必填):播报节奏。定义任务期间的时间播报风格。
full:全程密集报时。每秒尽力播报剩余秒数(受 is_speaking 防重叠保护)。
split:首尾密集报时。开头10秒、结尾10秒尽力播报;中间平稳期仅播报两次(如完成1/3和2/3时)。
silent:静默计时。任务全程及结束时均不发声,由后续 <state2> 负责播报。 - satisfied_threshold_s 与 satisfied_msgstr (配对可选):
当姿态判断持续满足 satisfied_threshold_s 秒(例如3秒)后,系统将播报 satisfied_msgstr(例如“好,保持住”)。
若 satisfied_msgstr 为空,则此阈值判定失效。
3.2.3 类型 rep_counter:计次任务
适用状态:仅限姿态状态。
作用:用于检测和统计用户按固定周期重复完成的动作次数。该任务不依赖单纯的时长(如秒数),而是依赖用户依次完成两个特定的动作阶段(例如俯卧撑的“向下”与“向上”,或腰椎操的“环臂”与“摆腰”)来累计计数。常见于俯卧撑、开合跳、腰椎操等周期性跟练动作。
详细语法说明:由于 rep_counter 内部包含 phase 子阶段、活跃期与冷却期的复杂状态机逻辑,具体语法配置及计数判定规则请参见本文“四、计次任务 type=‘rep_counter’”专章介绍。
3.3 状态流转控制 <next>
<next> 块定义了一个状态结束后,状态机的去向。
3.3.1 基本规则
- 若某个 <state2> 未包含 <next> 块,则将其视为最终状态。脚本将永久停在该状态(常用于语音播报“训练已结束”,直到用户手动退出)。
- 一个 <next> 块内部目前只支持一个 <branch>(分支)。
3.3.2 分支与判断 <branch> / <judge>
<branch> 用于定义状态结束后的跳转目标与触发条件。
字段说明:
- do_to_state:目标状态的索引(整数)。状态索引按脚本中 <state2> 出现的自然顺序从 0 开始自动编排。例如 do_to_state=4 表示跳转到第4个 <state2> 块。
- do_bool_set、do_str:附加操作,目前固定写为 do_bool_set="set_none" 和 do_str="",用于引擎内部变量复位。
- <judge>:判据块。
op="bool_equal":判断操作符,目前固定为布尔相等判断。
r_exp=true:预期结果的布尔值。
var_exp="$(is_wko_task_finished,)":调用 C++ 引擎底层的 is_wko_task_finished() 方法。当状态机任务(计时或计次)达到终点时,此方法返回 true。
结合图1中的俯卧撑状态机,串起<branch> 与 <judge> 判定逻辑:
- 准备阶段(索引0、1):脚本从“播报开场”开始,随后进入“定位”状态。定位状态是一个计时任务,引擎在此期间持续检测姿态,直到用户完成相机和站位调整。
- 播报开始(索引2):定位成功后,脚本进入“播报开始”状态,通过 speak 任务播报“定位完成,请做标准俯卧撑,开始”。
- 核心执行与跳转(索引3):脚本进入“做8个俯卧撑”状态。引擎每时间片(slice)调用 is_wko_task_finished() 判断计次任务是否完成。当用户完成 8 次后,<judge> 判断成立,引擎执行 do_to_state=4(即第5个状态“已结束”)。
- 收尾状态(索引4):引擎进入最后一个状态,循环播报结束语。
总结:正是由于 <judge> 与 <branch> 机制的存在,脚本才能像图1的箭头一样,在姿态达标、任务结束的瞬间,切换到下一环节(状态)。
3.4 is_setup:定位状态标记
类型:布尔值(true / false)
默认值:false
作用:用于标记当前状态是否为定位状态(Setup)。
核心作用:定义做操时长的起点
设置 is_setup 最根本的目的,是将用户进行相机摆放、站位对齐所花费的时间,排除在做操总时长的统计之外。
- 默认统计逻辑:系统的做操时长“0”时刻,定义为“进入首个姿态状态后,用户姿态首次满足姿态判断的那个时刻”。
- 为什么需要 is_setup:因为定位状态本质上也是姿态状态(包含 <track_pose>),如果不对其进行特殊标记,系统会把用户“慢慢摆手机、调整站位”的这段时间误算入做操时长。
- 偏移逻辑:有了 is_setup 后,引擎会将做操时长的“0”时刻往后顺延,即跳过所有带 is_setup 的状态,直到进入首个普通姿态状态且达标,才会开始正式计表,从而使最终统计出的“做操时长”更加纯粹和准确。
定义与规则:
- 语义:定位状态是系统强制用户在进入正式训练前,完成相机物理摆放和自身站位对齐的初始化准备阶段(如确保全身入镜、身体居中、朝向正确等)。
- 约束条件:
必须是姿态状态(包含 <track_pose>),且必须是脚本中的第一个姿态状态(通常是开场语音状态之后的第一个状态)。
推荐配置为计时任务(type="time_counter"),且计时规则设为严格满足时长(rule="strict"),播报节奏设为全程密集报时(tone="full")。
首次提醒延迟(unsatisfied_threshold_ms)建议不要超过 800 毫秒,以保证对相机的移动有足够灵敏的反馈。 - 引擎行为:在引擎中,标记了 is_setup=true 的状态通常会被渲染层(如 launcher 编辑器)以特殊的UI样式(如蓝框)标识,同时系统会将用户保持在定位状态,直到姿态达到设定标准,才会进入真正的训练阶段。
3.5 debug_skip:调试跳过标记
类型:布尔值(true / false)
默认值:false
作用:用于在开发调试阶段,强制让某状态瞬时完成(跳过),以快速进入后续状态进行参数验证。
定义与规则:
- 语义:当引擎加载到标记了 debug_skip=true 的状态时,会忽略该状态内部的 <task>、<track_pose> 等所有逻辑,直接执行 is_wko_task_finished() 并返回 true,从而立即触发 [next] 跳转。
- 适用场景:当动作脚本有长串状态机(如13个状态),需要频繁修改某个特定姿态判断参数,又不希望每次都从第1个状态开始完整跑一遍时。
- UI 提示:强烈建议配套工具(如 launcher)在渲染状态机图时,对标记了 debug_skip 的状态绘制显眼的标记(如红框、红色虚线或 [SKIP] 标签),以防止在发布正式版脚本时遗忘清除该字段。
- 约束:严禁在正式发布的脚本中保留 debug_skip=true。引擎底层或外部校验工具应在发布前强制扫描并清除该字段。
四、<state2>中的<track_pose>、<pose>
4.1 概述
<track_pose> 用于定义在某个 <state2>(状态)内,需要同时检测的一系列姿态判定条件。一个 <track_pose> 内部包含了若干个 <pose>(姿态判断)块,所有 <pose> 共同构成了该状态的“达标标准”。
在执行该状态时,系统会对这些 <pose> 进行检测。只有当这个 <track_pose> 内的所有 <pose> 姿态判定同时满足,系统的姿态检测引擎才会返回“达标”结果。
4.2 标签结构定义
4.2.1 <track_pose>标签(动作单元)
- 含义:定义操中的一个独立动作。
- 结构:该标签内可包含一个或多个 `<pose>` 子标签。
- 执行逻辑:内部的所有 `<pose>` 判定条件构成“与(AND)”逻辑关系。即只有当所有内部 `<pose>` 的判断结果均为 `true` 时,该 `<track_pose>` 才算达标,系统才会切换至下一个动作。
4.2.2 <pose>标签(姿态判断单元)
- 含义:定义单个具体的姿态约束条件。
- 结构:通过若干个配置属性(Attribute)定义计算类型、目标数值范围及提示语。
- 执行逻辑:根据 `type` 和 `operand` 计算出一个最终数值,并将其与 `min` 和 `max` 组成的闭区间 `[min, max]` 进行比较。若落在区间内,则判定成立。
4.3 <pose> 标签字段详细说明
| 字段名 | 必需 | 数据类型 | 说明 |
| landmarks | 是 | 字符串 | 指定参与计算的 Mediapipe 骨骼姿态点索引。根据 `type` 不同,格式有所区别(详见下文)。 |
| type | 是 | 枚举 | 判断类型。可选值:`angle3p`, `angle2p`, `diff`。 |
| operand | 是 | 枚举 | 运算单元类型。可选值:`point`, `x`, `y`, `dist_x`, `dist_y`。 |
| name | 是 | 字符串 | 该姿态判定的描述名称,用于日志记录或开发调试。 |
| min | 条件 | 浮点数 | 判定达标的下限阈值(包含该值)。如果不出现,视为 `-∞`(或根据算法默认),但 `min` 和 `max` 至少出现一个。 |
| max | 条件 | 浮点数 | 判定达标的上限阈值(包含该值)。如果不出现,视为 `+∞`(或根据算法默认),但 `min` 和 `max` 至少出现一个。 |
| unsatisfied_msgstr | 是 | 字符串 | 当未设置 unsatisfied_rmax_msgstr 时:只要姿态不满足范围(无论偏大还是偏小),均播报此提示语。 |
| unsatisfied_rmax_msgstr | 否 | 字符串 | 表示计算值大于 'max' 上限时的语音提示。与 'unsatisfied_msgstr'配合使用,实现差异化引导。 |
| abs | 否 | 布尔值 | 仅对 type="diff" 且 operand="x"或 operand="y"有效。true表示对计算结果取绝对值后再与 [min, max]比较;false(默认)或不填,则使用原始差值比较。 |
| phase_mask | 否 | 数字 | 标识当前姿态判断应当作用于计次任务中的哪一个或哪几个阶段。 |
| ang_range | 否 | 枚举 | 仅当 type="angle3p" 时有效。指定三点角度的取值范围。可选值:180(默认值,范围为 [0, 180])、360(范围为 [0, 360)) |
4.4 姿态点索引解析规则(包含伪姿态点)
4.4.1 真实姿态点索引(0 ~ 32)
Mediapipe 输出的 33 个骨骼姿态点索引范围固定为 0 到 32。脚本中直接使用这些数字即可(如 11 代表左肩,0 代表鼻子)。
4.4.2 伪姿态点索引(从 1000 起始)
为了解决画面绝对位置(如画面中心)的参考需求,系统专门预留了从 1000 开始的大数字作为伪姿态点索引。这些索引并非真实的骨骼,而是引擎内部硬编码的物理坐标。
当前系统预置的伪姿态点如下:
| 伪索引数字 | 语义名称 | 等效物理坐标(归一化 0~1) |
| 1000 | 画面正中心 (CENTER) | (0.5, 0.5) |
| 1001 | 画面左上角 (top-left) | (0.0, 0.0) |
| 1002 | 画面右上角 (top-right) | (1.0, 0.0) |
| 1003 | 画面右下角 (bottom-right) | (1.0, 1.0) |
| 1004 | 画面左下角 (bottom-left) | (0.0, 1.0) |
注:为了便于脚本编写者理解,允许在底层解析时用单词替换。但在动作配置文件中,必须使用数字 1000 进行配置,以保证结构体字段 int 类型的统一。
4.5 landmarks 字符串的解析法则
在解析 landmarks 字段时,系统将严格遵循以下优先级:
- 分隔符:使用 分号 (;) 将 landmarks 字符串切割为左侧和右侧两个运算单元。
- 左侧/右侧单元内部解析:
逗号 (,) 规则:如果某个运算单元内部出现逗号(如 "11,12"),则强制解析为“这两点求坐标平均值(中点)”。
单数字规则:如果只有一个数字(如 "0" 或 "16"),则直接提取该关键点的坐标。
伪姿态点规则:如果数字 >= 1000(如 1000),则系统不访问 Mediapipe 数据,而是直接返回预置的固定坐标(如 (0.5, 0.5))。 - 计算执行:根据 operand 类型,提取两个单元的 (x, y) 或单一维度进行计算。
4.6 数据类型(`type`)与 `landmarks` 格式详解
根据 `type` 的不同,`landmarks` 的解析方式及 `operand` 的取值范围均有明确规定。
4.6.1 类型:angle3p(三点角度)
- 功能:计算三点构成的内角,取值范围:[0, 180]。
- 几何定义:假设三点为 A、B、C,以 B 为顶点,计算射线 BA 与 BC 之间的夹角。
- landmarks 格式:`"A点索引; B点索引; C点索引"`(注意:必须是分号分隔的3个数字)。
- operand 限制:必须为 `point`。系统将提取这3个点的 `(x,y)` 坐标进行计算。
- 说明**:A、C互换位置(如 `11;13;15` 与 `15;13;11`)不影响计算结果。
4.6.2 类型:`angle2p`(两点逆时针角)
- 功能:计算两点连线相对于水平正X轴的逆时针旋转角,取值范围:[0, 360)。
- 几何定义:以第一个点 A 为原点,画一条水平向右的射线(极轴),计算该射线绕 A 点逆时针旋转到射线 AB 所经过的角度。
- `landmarks` 格式:`"A点索引, B点索引"`(**注意:必须是逗号分隔的2个数字**)。
- `operand` 限制:必须为 `point`。系统将提取这2个点的 `(x,y)` 坐标进行计算。
- 注意事项:图像坐标系中 Y 轴向下,但在 `angle2p` 计算中,角度严格按照数学坐标系(Y轴向上)的逆时针计算。
4.6.3 类型:`diff`(差值计算)
- 功能:计算两个运算单元(由 `operand` 定义)之间的数值差。
- `landmarks` 格式:`"单元1; 单元2"`(注意:必须是分号分隔)。
- 返回值:默认返回 `单元1计算结果 - 单元2计算结果`。结果可为负数、零或正数。
- `operand` 限制:由 `operand` 具体值决定计算细节。
4.7 运算单元(operand)类型详解
operand 指定了从 landmarks 中提取的数据形式,它决定了参与 diff 计算或 angle 计算的基础实体是什么。
| `operand` 值 | 适用 `type` | `landmarks` 格式说明 | 运算单元解析说明 |
| point | angle3p angle2p diff | 1. angle3p/angle2p:多个点 2. diff:"A点索引; B点索引" | 欧式距离:当用于 diff 时,表示计算两个姿态点 A 和 B 在图像上的直线距离。公式:sqrt((x_A-x_B)² + (y_A-y_B)²)。结果为非负数。 |
| x | diff | "A点索引; B点索引" | X轴坐标差:计算 `A点.x - B点.x`。 注意:若配置了 `abs="true"`,则结果为 `abs(A点.x - B点.x)`。 |
| y | diff | "A点索引; B点索引" | Y轴坐标差:计算 `A点.y - B点.y`。 注意:若配置了 `abs="true"`,则结果为 `abs(A点.y - B点.y)`。 |
| dist_x | diff | "A点索引; B点索引" | 水平距离:计算两点X轴坐标差的绝对值,即 `abs(A点.x - B点.x)`。结果为非负数。自带绝对值效果。 |
| dist_y | diff | "A点索引; B点索引" | 垂直距离:计算两点Y轴坐标差的绝对值,即 `abs(A点.y - B点.y)`。结果为非负数。自带绝对值效果。 |
特别说明:当 landmarks 中出现了伪姿态点索引(如 1000)时,系统在运行期取点时会自动将其替换为对应的固定坐标 (0.5, 0.5),上述所有 operand 的计算逻辑对该伪坐标完全适用。
4.8:phase_mask(阶段掩码)
- 字段类型:整数(位掩码)
- 适用场景:计次任务(rep_counter)、计时任务(time_counter)。
- 默认值:1
4.8.1 核心作用
phase_mask 用于标识当前 <pose>(姿态判断)应当作用于计次任务中的哪一个或哪几个阶段。
在计次任务中,一个 <track_pose> 数组内包含的 <pose> 判断是全局共享的。通过 phase_mask,脚本编写者可以精确控制每一个姿态判断在动作周期的哪个阶段被激活。
4.8.2 取值规则(位掩码机制)
为了实现“单阶段独用”和“多阶段共用”的灵活配置,phase_mask 采用二进制位掩码的方式进行编码:
| 取值(十进制) | 二进制 | 作用阶段 | 说明 |
| 1 | 01 | 第一阶段 | 该姿态判断仅在动作的第一阶段(如“向下”)被检测。 |
| 2 | 10 | 第二阶段 | 该姿态判断仅在动作的第二阶段(如“向上”)被检测。 |
| 3 | 11 | 第一、二阶段共用 | 该姿态判断在第一阶段和第二阶段同时生效。 |
(注:如果未来引入第三阶段,则第三阶段的掩码为 4(二进制 100)。如需要第一和第三阶段共用,则 phase_mask = 1 | 4 = 5。依此类推。)
4.8.3. 典型应用场景
- 场景一:阶段独立判定(值 = 1 或 2)
例如做俯卧撑时,阶段 1(向下)要求身体低点姿态正确;阶段 2(向上)要求手臂完全伸直。这两个姿态判断条件完全不同,且仅在各自阶段生效,因此分别设置 phase_mask="1" 和 phase_mask="2"。 - 场景二:阶段共用判定(值 = 3)
例如做“站姿踢腿”操时,无论是踢左脚还是踢右脚,上半身都必须保持“双肩中线与双髋中线尽量垂直”的直立姿态。此时,只需编写一条关于“身体歪斜角度”的 <pose>,并配置 phase_mask="3",系统就会在两个阶段自动复用这个判断。
4.8.4 计时任务中的特殊处理
对于计时任务(type="time_counter"),由于任务本身只有一个保持阶段,引擎在加载时默认将其视作第一阶段。
因此,引擎底层会直接使用 phase_mask=1 进行匹配。为了保持脚本的简洁性,在编写计时任务的脚本时,强烈建议省略 phase_mask 字段(让解析器使用默认值 1 即可)。只有明确需要区分动作阶段的计次任务时,才需要显式配置 phase_mask。
4.9 数值范围与角度坐标系规定(min / max)
在 <pose> 标签中,字段 min 和 max 定义了姿态判断达标的闭区间 [min, max]。引擎在计算判定时,会将实时计算值与这两个边界值进行比较。
对于角度类型,系统区分对待以下两种几何类型:
- 对于 angle3p(三点内角):
由于内角在数学上天然被限制在 0°~180° 之间,它不存在负角,也不存在跨越0°/360°边界的几何翻转问题。因此,angle3p 的 min/max 必须在 [0, 180] 内取值,且永远不需要进行坐标系切换。 - 对于 angle2p(两点逆时针有向角):
由于这是基于向量旋转的角度,可跨越 0°/360° 边界,系统内部支持两种坐标系表示方式:[0, 360)(标准360°平面)和 [-180, 180)(有符号角度)。
4.9.1 脚本编写规则(如何选择坐标系)
在脚本中填写 min 和 max 时,遵循以下明确的语法规范:
- 必须出现负数的,必须采用 [-180, 180)
如果角度范围包含负数(例如 [-30, 30]),在脚本中必须直接填写 min=-30,max=30。
注:即使视觉上看起来能换算成 [330, 360) ∪ [0, 30),为了避免歧义,也必须直接使用负数。 - 不出现负数的,采用 [0, 360)
如果角度范围完全在正半轴,则直接填写 0 到 360 之间的数字(如 min=175,max=252)。
硬性约束:当同时填写 min 和 max 时,max 的原始数值必须严格大于 min 的原始数值(即 max > min)。此比较是在原始数字层面进行的,引擎不会自动执行类似 max + 360 的溢出换算。
4.9.2 引擎的坐标切换逻辑(防误报机制)
由于360°是周期性的(即359°实际上非常接近0°),如果直接将绝对坐标系用于判定,极易出现判定方向的“边缘翻转”错误。
为了避免“本应报小于最小值的原因,却报成了大于最大值”的诡异情况,引擎会在以下三种特定情况中,自动将实时角度(以及内置的判定区间)从 [0, 360) 转换为 [-180, 180) 的等价表示:
- 最小值或最大值中出现了负数。
例:min=-30,max=30。此时实时值若为 -10°,直接判定达标;若为 35°,判定超出。 - 最小值和最大值均落在第一象限(约0°~90°之间)。
例:min=10,max=80。
触发原因:若不转换,当实时角度为 359°(物理上等同于 -1°)时,系统会被迫判定为“大于最大值(80)”。转换后,实时角度变为 -1°,系统会正确判定为“小于最小值(10)”,从而播报正确的错误原因(“角度太小”)。 - 最小值和最大值均落在第四象限(约270°~360°之间)。
例:min=280,max=350。
触发原因:若不转换,当实时角度为 1°(物理上等同于 361°)时,系统会将 1° 判定为“小于最小值(280)”,但实际物理语义是用户绕过了 360° 边界,进入了“极小正角”区域。切换后,实时角度变为 1°,系统会正确判定为“大于最大值(350)”,从而播报正确的“角度太大”提示。
注:在 angle3p(三点内角)中,由于其取值范围天然被锁定在 [0, 180] 且不存在跨越360°边界的问题,因此它只使用 [0, 180],不会出现上述坐标切换。
4.9.3 angle2p需要同时设置min、max
在编写 <pose> 姿态判断时,对于 angle2p(两点逆时针角) 类型,存在一个由数学特性和物理边界引起的特殊情况。为避免系统因单边约束导致语义误判,引擎强制要求:
严格规则:凡是 type="angle2p" 的 <pose>,必须同时提供 min 和 max 字段!禁止仅填写单一一个(如只设 max=10 或只设 min=100)。
为什么必须强制同时设置?
以“左上臂角度(左肩到左肘)”为例,假设人平躺在地上,动作要求该角度不能大于 10°。
如果脚本偷懒,只设置了 max=10,会发生什么?
- 在 [0, 360) 坐标系下:
Mediapipe 在捕捉平躺姿态时,极大概率会因为像素误差(肩比肘略高),得出一个微小的负角度,例如 -5°。
但在 [0, 360) 中,-5° 会被等价转换为 355°。此时,355 > max (10),系统误判为“大于最大值”,触发“角度过大”的语音报错。这显然是错的。 - 在 [-180, 180) 坐标系下:
引擎可能会根据物理直觉切换到这个坐标系。此时 -5° 会被正确识别为 < max (10),系统判定达标。这看似解决了问题。
但是! 假设用户的手臂极度异常,跨过了半圈垂到了身体的另一侧,角度变成了 -179°(或 181°)。在 [-180, 180) 下,系统会判断 -179 < 10,依然认为达标,从而漏报了极其严重的动作错误。
结论:只设单边,无论使用哪种坐标系,都无法完美覆盖所有奇异的物理姿态,都会导致“误报”或“漏报”。
解决策略:设置“不会出现的极值”兜底
为了防止上述歧义,编写 angle2p 时必须用 min 和 max 划定一条绝对安全的物理边界。
- 正确姿势:在要求“不超过 10°”的场景下,不仅要设 max=10,还要设一个在真实动作中永远不会触碰的极小值(如 min=-90)。
- 引擎判定逻辑:有了 min=-90 和 max=10 后:
实时角度 -5°:满足 [-90, 10],判定达标。
实时角度 -179°:小于 min (-90),系统判定为“小于最小值”,立即触发“角度太小/异常”的报错提示。
4.10 ang_range:三点角度的范围扩展(有向角)
字段类型:字符串(枚举)
默认值:180
适用场景:仅用于 type="angle3p" 的姿态判断中。
4.10.1 核心作用
ang_range 用于控制 angle3p 计算出的角度范围。普通的三点内角最大只能是 180°,但如果您需要对动作的旋转方向进行判断(例如区分“身体向前弓腰”和“向后塌腰”),就需要用到有向角度。
4.10.2 取值规则
- 180(默认值):计算由 A、B、C 三点构成的普通内角,取值范围 [0, 180]。A、C 互换位置不影响计算结果。受人体关节生理限制,绝大部分关节弯曲角(如手肘、膝盖、躯干平直度)都应使用此默认值。
- 360:计算以中间点 B 为原点,射线 BA 逆时针旋转至射线 BC 的有向角度,取值范围 [0, 360)。注意:由于引入了方向,A、C 互换位置会导致计算结果发生改变。
4.10.3 典型应用场景:臀桥(区分塌腰与弓腰)
在检测臀桥等动作时,如果只使用 [0, 180] 的普通内角,当用户将骨盆顶得过高(导致腰部严重弓起)时,其角度甚至可能接近或超过 180°。此时,系统无法区分“正确的微塌”和“错误的过顶弓腰”。
使用 ang_range="360" 后,系统可以捕捉到大于 180° 的异常角度。此时只需设置一个合理的上限(如 max=200),系统就能在用户弓腰时准确触发“弓腰顶过头了”的语音提示。
4.10.4 安全约束(强制双约束)
由于 [0, 360) 的角度具有方向性,突破了常规的角度物理限制,当设置 ang_range="360" 时,必须同时填写 min 和 max 字段,以划定安全边界。系统要求 max 必须严格大于 min。建议为阈值设置一个安全的容错范围,防止算法出现极端值。
4.11 编写规则与补充示例解析
规则强调:
- min 与 max:若只定义 min=145,则条件隐式为 [145, +∞);若只定义 max=0.16,则条件为 `(-∞, 0.16]`。
- 坐标归一化:Mediapipe 输出的坐标通常是归一化的相对坐标。因此 diff 的阈值通常是极小的浮点数(如 0.16、0.07),代表相对画面宽高比例的距离。
- 安全校验:引擎在执行时,若 landmarks 中的某个姿态点被 Mediapipe 判定为缺失(未识别到),或不在当前画面内,当前 pose 判断应直接视为失败。
- 伪姿态点(>=1000)始终有效,不受 Mediapipe 识别影响
示例 1:水平居中判定(利用伪姿态点 1000)
[pose] landmarks="11,12; 1000" <!-- 左单元:双肩中点;右单元:画面中心 (0.5,0.5) --> max=0.2 name="水平居中判定" operand="x" type="diff" abs=true <!-- 取绝对值,偏移量不分左右 --> unsatisfied_msgstr="请站在画面正中间" [/pose]
解析:计算 |双肩中点.x - 0.5|,只要偏移量小于0.2,判定成立。
补充示例 2:上下限差异化提示
[pose] landmarks="12; 14" max=280 min=260 name="右上臂角度" operand="point" type="angle2p" unsatisfied_msgstr="手肘弯曲太小,身体向后移一些" <!-- 当计算值 < 260时触发 --> unsatisfied_rmax_msgstr="手肘弯曲太大,身体向前移一些" <!-- 当计算值 > 280时触发 --> [/pose]
解析:这是一个 `angle2p` 角度判断。当角度低于 `min=260` 时,提示用户“向后移”;当角度高于 `max=280` 时,提示用户“向前移”。这提供了极高的用户容错引导能力。
补充示例 3:绝对差值判定(`abs`的应用)
[pose] abs=true landmarks="7; 8" min=0.015 name="abs(左耳.y-右耳.y)" operand="y" type="diff" unsatisfied_msgstr="让两耳更加倾斜" [/pose]
解析:计算 左耳.y - 右耳.y 后取绝对值。要求头部倾斜度至少 0.015,否则提示用户让两耳更加倾斜。。
五、动作库(Action Library)与工作流集成
在 LeaScript 中,我们提供了一套内置的动作库(Action Library),用于加速脚本编写、提升动作质量的一致性。
5.1. 动作库文件:action_tpl2s.cfg
动作库存储在一个独立的配置文件 action_tpl2s.cfg 中。文件内由多个 <action_tpl2> 块组成,每个块代表一个标准的、可复用的独立动作(如“死虫式”、“平板撑”、“交叉卷腹”等)。
相比于 <state2>,<action_tpl2> 额外包含以下三个核心字段:
| 字段名 | 必需 | 数据类型 | 说明 |
| id | 是 | 字符串 | 全局唯一标识。只能由小写英文字母、数字和下划线组成,最大长度不超过24个字符。(示例:dead_bug) |
| name | 是 | 字符串 | 动作的中文/本地化名称,用于在编辑器界面和语音播报中展示。(示例:死虫式) |
| msg | 是 | 字符串 | 动作开始前的标准开场/教学语音提示。(示例:屁股向脚尖方向微微滑动,把腰压平贴地,抬左脚,举右手。) |
5.2. 动作库在 Launcher 中的应用
当您在 launcher 编辑器中将 <action_tpl2> 应用到某一 <state2>(状态)时,遵循以下交互逻辑:
- 在工具栏中点击 “粘贴动作” 按钮,会弹出动作库列表。
- 选中其中一个动作后,根据当前状态类型,应用逻辑分为两种:
场景一:应用于姿态状态(如计时/计次状态)
- 行为:该动作的核心姿态判定(<track_pose>)、任务逻辑(<task>)以及纠错延迟等参数,会整体替换当前状态原有的逻辑。
- 状态名更新:当前状态的名称(state)会被自动更新为动作库中的 name。
场景二:应用于非姿态状态(即语音状态)
- 行为:当前语音状态无需替换姿态判断逻辑。系统会将动作库中的 msg 字段值,自动替换为该语音状态要播报的语音内容(msgstr)。
- 命名规则:最终播报的语音消息,由“动作名称”和“动作提示”组合而成。例如,选中“死虫式”后,语音状态将播报:“死虫式。屁股向脚尖方向微微滑动,把腰压平贴地,抬左脚,举右手。”
5.3. 动作库的工程价值
动作库的引入,让 LeaScript 的“复用性”达到了一个新的高度:
- 标准化动作质量:动作模板中的姿态阈值、阶段划分和纠错提示由高级教练或开发者编写好后,全项目统一使用,避免重复调参导致的“同一个动作,不同姿态”问题。
- 创建新操的效率倍增:脚本编写者不需要从零编写每一个姿态判断。只需从动作库中复制核心逻辑,再重新编排各个 <state2> 的跳转顺序(<next>)即可。
六、计次任务 type="rep_counter"
适用状态:仅限姿态状态。
作用:将一个完整动作拆解为两个独立的连续阶段,引导用户依次完成。当用户完成两个阶段一次后,计次 +1。常用于循环往复的跟练动作,如俯卧撑(向下向上)、腰椎操(环臂摆腰、侧踢迎掌等)。
6.1 任务结构:<phase> 块
每个 rep_counter 任务固定且必须包含两个 <phase>(阶段)。
这两个 <phase> 并不局限于“向下/向上”的物理含义,而是代表动作流程中的两个连续的动作要领。
- 示例(俯卧撑):
动作:阶段1 = “向下”,阶段2 = “向上” - 示例(腰椎操):
动作“环臂摆腰”:阶段1 = "环臂",阶段2 = "摆腰"。
动作“侧踢迎掌”:阶段1 = "踢右脚",阶段2 = "踢左脚"。
动作“挤腰空踏”:阶段1 = "挤腰",阶段2 = "空踏"。
【如何区分哪个是第二阶段?】
区分依据是:用户姿态在持续满足第二阶段的要求后,系统会自动播报当前累计次数(例如用户做完一次,系统报“1”),从而标志着单次动作的闭环。
以俯卧撑为例,一个完整动作是先“向下(蓄力)”再“向上(发力)”。因为当用户撑到最高点时,动作才真正完成,此时系统需要播报次数。因此,“向上”阶段必须配置在第二个 <phase> 中。
6.2 阶段内的两个子阶段(内部状态机)
在每个 <phase> 内部,系统会自动按照严格的顺序执行两个子阶段:
- 活跃期 (Active Period)
起止时间:从上一阶段冷却期结束(或任务刚开始时)开始,到本阶段姿态判断条件连续满足结束。
作用:系统持续进行姿态检测。用户必须在当前阶段的要求下保持姿态达标。 - 冷却期 (Cooldown Period)
起止时间:从本阶段活跃期结束的时刻开始,到进入下一阶段活跃期为止。冷却期属于当前阶段,是当前阶段的尾声,
作用:屏蔽姿态检测的窗口期。在冷却期内,系统暂停对下一阶段姿态的检测,防止动作转换过程中的瞬间姿态不达标触发错误的语音提示。
6.3 <phase> 可用字段说明
| 字段名 | 类型 | 必填 | 说明 |
| action_msg | 字符串 | 是 | 当前阶段的动作标准/引导提示语。例如:俯卧撑的 "向下"、腰椎操的 "环臂"、侧踢的 "踢右脚"。 核心触发机制:它不是用于报数,也不是达标后立刻播报。它仅在用户长时间不满足当前阶段姿态时,作为一种“归属指引”被播出,帮助用户明确自己当前应该做什么动作。 |
| min_duration_ms | 整数 | 是 | 活跃期最小时长(毫秒)。用户必须在此阶段的姿态判断下持续保持超过该时长,系统才认定当前阶段达标。 |
| cooldowned_ms | 整数 | 是 | 冷却期时长(毫秒)。阶段达标后,系统暂停检测的时间长度。 |
6.4. 完整的计次(计数)逻辑
单次动作的完整判定流程是一个严格的状态机序列:
- 第一阶段达标:引擎检测到姿态满足第一阶段(如“向下”)的活跃期,并连续保持达到 min_duration_ms。
- 进入冷却:第一阶段达标后,引擎自动进入该阶段的 cooldowned_ms 冷却期。在此期间,系统停止姿态检测。
- 进入第二阶段:冷却期结束后,引擎自动进入第二阶段的活跃期。
- 第二阶段达标(触发计数):引擎检测到姿态满足第二阶段(如“摆腰”)的活跃期,并连续保持达到 min_duration_ms。此时,系统执行 计数 + 1。
- 循环判断与动作分支:
如果 当前计数 < max_count:计数完成后,引擎进入第二阶段的 cooldowned_ms 冷却期。冷却期结束后,自动回退到第一阶段的活跃期,准备进行下一次循环(重复步骤1~4)。
如果 当前计数 == max_count:说明该状态的全部任务已按计划圆满完成。引擎直接跳过本次第二阶段的冷却期,立即结束当前计次任务,触发状态机跳转至下一状态(执行 <next> 中的逻辑)。

注:关于计数播报与阶段的关联:
引擎在判定第二阶段达标并触发计数(+1)时,会播报当前累计数字(如“8”)。这个计数播报本身不需要依赖 action_msg 的内容。
但是,由于 action_msg 必须按顺序写在两个 <phase> 中,引擎内部会默认将第二个 <phase> 的达标时刻作为计数的触发点,而 action_msg 会作为阶段名称的标识符传给前端Vlog显示。
6.5 关于“单次耗时”的统计定义
定义:单次耗时 = 本次计数完成时刻 - 上次计数完成时刻。
- 计数完成时刻:精确对应第二阶段活跃期结束的时刻(即进入第二阶段冷却期的瞬间)。
- 通过此定义,系统可以准确计算用户完成每个动作的节奏和平均用时,为后续的疲劳度分析或动作频率建议提供数据基础。
6.6 action_msg 与不满足原因的交替播报策略
在计次任务执行期间,当用户“姿态不满足”时,语音引导会采用 “先报纠错,后报指引” 的交替策略。action_msg 在此时扮演了极其关键的角色。
具体引擎逻辑如下:
- 首次不满足(触发 unsatisfied_threshold_ms,如 1200ms):
系统播报具体的不满足原因(即 <pose> 块中配置的 unsatisfied_msgstr)。
示例:如果用户“向下”没到位,系统报:“有塌腰,请向上顶腰”。 - 持续不满足(超过 unsatisfied_2th_threshold_s,如 4 秒):
如果用户在报错后仍未纠正,系统会立即播报当前阶段的 action_msg。
示例:系统报:“向下”。 - 长期不满足(1.2+4秒后仍然未恢复):
系统将进入 “交替循环播报” 模式,每隔设定间隔(如 4 秒)交替播报一条信息:
第 1 次:不满足的具体原因(指向姿势错误)。
第 2 次:当前阶段的 action_msg(指向动作身份)。
为什么加入 action_msg 的交替播报?
因为用户在做操时间久了,或者中途分神后,经常会“忘了自己当前做到哪一步了”(例如,用户以为自己在做“向上”撑起,但实际上系统在等“向下”沉底)。
如果只报“手臂没伸直”,用户可能会疯狂调整手臂,却不知道应该在“向下”阶段调整。action_msg 的介入,相当于告诉用户:“你刚刚手没伸直,而且你现在应该先去做‘向下’这个动作。” 这个机制解决了用户“丢失阶段”的问题。
七、姿态状态的通用延迟与纠错机制(unsatisfied_threshold_ms 和 unsatisfied_2th_threshold_s)
在包含 <track_pose> 的姿态状态(即计时任务 type="time_counter" 或计次任务 type="rep_counter"),系统在执行姿态检测时,都遵循一套统一的“不满足反馈与循环播报机制”。本章节将详解 unsatisfied_threshold_ms、unsatisfied_2th_threshold_s 以及针对计时任务专有的 unsatisfied_2th_msgstr 的底层逻辑。
它们都是<state2>下的顶层字段。
7.1 unsatisfied_threshold_ms(满足到不满足延迟 / 首次提醒延迟)
- 类型:整数(毫秒)
- 作用:定义系统从检测到姿态“不满足”到播报提醒声音之间的最短延时。
- 跨任务行为差异(非常重要):
计次任务(rep_counter)中:引擎的“满足/不满足”状态按单帧生效。即只要姿态算法(含防抖动)判定当前帧不合格,内部状态立刻转为“不满足”。但系统会等待达到 unsatisfied_threshold_ms 后,才首次播报不满足原因。这种“一帧判定、延迟播报”的设计避免了用户细微抖动引发的频繁报错。
计时任务(time_counter)中:引擎的状态变更存在“确认机制”。姿态必须连续不满足超过 unsatisfied_threshold_ms,内部状态才真正变成“不满足”。一旦变成不满足,系统会立即播报不满足原因。
7.2 unsatisfied_2th_threshold_s(不满足再次提醒延迟)
- 类型:整数(秒)
- 作用:在首次播报不满足原因后,如果用户姿态仍然处于不满足状态,系统会等待该字段定义的秒数,然后触发下一次播报。此后,若姿态仍不满足,系统将以此间隔(秒)循环播报。
7.3 unsatisfied_2th_msgstr(不满足再次提醒语音)
- 类型:字符串
- 适用状态:仅限计时任务(type="time_counter")。
- 作用:在计时任务中,当进入“再次提醒循环”时,系统不再仅播放单一的不满足原因,而是采用 “交替循环播报(交替策略)”:
第一轮:播报 <pose> 中配置的 unsatisfied_msgstr(具体姿态出错点,如“左侧手肘举得更直些”)。
第二轮:播报 unsatisfied_2th_msgstr(动作整体回述,如“请检查下动作。十指相扣,手臂向上延展”)。
第三轮:回到 unsatisfied_msgstr,以此类推。
为什么需要这个字段:用户做操时间久了或分神后,可能会忘记当前动作的要领。具体的纠错语音和动作总体提醒交替播报,能帮助用户自我调整,重新进入正确的动作轨道。
特殊说明(计次任务):在计次任务中,如果进入“再次提醒循环”,系统将采用不满足原因(unsatisfied_msgstr)与当前阶段动作提示(<phase> 中的 action_msg,如“向下”) 进行交替播报,确保用户不仅知道姿势错在哪,还能想起当前动作该做什么。
7.4 为什么计次任务采用“一帧判定”,而计时任务需要“累积判定”?
7.4.1 计次任务采用单帧判定
根本原因在于计次任务的动作特性与极短的停留时间。
1) 阶段满足时间极其短暂
在以重复动作为核心的计次任务中(如“向下”、“向上”、“环臂”、“空踏”),姿态必须处于标准范围内的时间往往非常短(通常仅为 500~1000 毫秒)。用户经常是“达标一瞬间,立刻进入下一阶段”。
2) 若增加“不满足判定延迟”将产生严重干扰
如果在计次任务中,也为“不满足”施加 unsatisfied_threshold_ms 的延迟保护,系统就会进入一种矛盾状态:
- 如果姿态在阶段切换的间隙短暂偏航,但因为没达到延迟时间,系统还要继续“硬挺”在满足状态;
- 这会导致当用户再次返回标准位置时,引擎的逻辑判定发生错乱,甚至无法正确识别阶段的开始和结束。
3) “短而灵敏”的设计定位
因此,计次任务采用 “单帧不满足即转态,单帧满足即转态” 的极简策略。它牺牲了对微小抖动的容忍度,但换来了对动作阶段性切换的最快响应速度。而单帧判定带来的“误报”风险,则交由 unsatisfied_threshold_ms(满足到不满足延迟)作为语音播报的“缓冲阀”:即在内部判定已为“不满足”后,等待数个毫秒,如果依然未恢复,才真正发声报警。
7.4.2 计时任务(如平板撑、肩颈拉伸)——追求“姿态的稳定性”
- 计时任务是连续保持类动作。用户需要在一个相对固定的姿态下维持 10 秒甚至 60 秒。
- 在此期间,用户的肢体难免会发生天然的、极短时间(如 100~500毫秒)的本能抖动或微调。如果我们采用“单帧不满足立即转态”的机制,那用户几乎全程都是“不满足”状态,根本无法积累有效时长。
- 设计依据:只有超过设定的容忍阈值(即 unsatisfied_threshold_ms),说明用户确实卸力或动作变形了。这种“累积超时转态”机制,完美地过滤了生理性抖动,只对真正的动作溃散做出响应。
7.5 安全性约束与校验规则
在脚本加载和解析阶段,引擎会对上述两个时间参数进行强制性一致性校验。脚本编写者必须遵守以下约束条件:
unsatisfied_2th_threshold_s * 1000 > unsatisfied_threshold_ms
说明:
- unsatisfied_2th_threshold_s 的单位是秒,必须乘以 1000 转换为毫秒,才能与 unsatisfied_threshold_ms 进行比较。
- 如果该条件不满足(即“二次提醒延迟”的时间小于或等于“首次提醒延迟”的时间),引擎会直接判定该脚本无效(编译/加载失败)。
为什么要设置这个硬性约束?
- 保证语音播报的间隔:系统需要确保首次播报语音(不满足原因)完全结束后,再触发第二次播报。如果二次延迟设得太短,会导致用户还在听第一句话时,第二句话就强行插入,造成严重的听觉重叠。
- 解决状态机的判定时差:计时任务中,“不满足”状态是在等待 unsatisfied_threshold_ms 后才会真正切换的。如果二次提醒小于或等于一次提醒,状态机可能刚变成“不满足”,立刻又触发二次提醒,导致逻辑死循环或瞬间触发两次播报。
- 用户体验的物理限制:人类听懂并消化一句纠错语音通常需要 1~2 秒。强制二次时长大于一次时长,能确保用户至少有充足的时间去理解并尝试纠正动作,而不是被连续不断的语音轰炸。