diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4
new file mode 100644
index 0000000..56268f4
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav
new file mode 100644
index 0000000..2fa6b22
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4
new file mode 100644
index 0000000..92c9205
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav
new file mode 100644
index 0000000..6c2f396
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4
new file mode 100644
index 0000000..5967eb4
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav
new file mode 100644
index 0000000..51d23f7
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4
new file mode 100644
index 0000000..6161d07
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav
new file mode 100644
index 0000000..690ccda
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4
new file mode 100644
index 0000000..d04afd3
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav
new file mode 100644
index 0000000..09bca9d
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-20260919/index.html b/reports/bridge-005-moss-junhao-20260919/index.html
new file mode 100644
index 0000000..7f585a0
--- /dev/null
+++ b/reports/bridge-005-moss-junhao-20260919/index.html
@@ -0,0 +1,13 @@
+
005 · MOSS Junhao 讲解对比
+
+ 此目录为本机旧素材试渲染,请查看 已校验 v2 素材的新旧对比预览。
005 液压泵应急切换 · 新旧讲解对比
本机旧素材试渲染(非验收版本) · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。
第 1 步 · 根据故障现象确定是哪一个液压泵发生故障
第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。
+ 第 2 步 · 断开架桥车取力器
第2步,断开架桥车取力器。将架桥车取力器断开。
+ 第 3 步 · 将架桥车备胎斜上方链接两个红色滤油器的球阀打开
第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。
+ 第 4 步 · 重新联通取力器
第4步,重新接通取力器。将架桥车取力器重新接通。
+ 第 5 步 · 打开辅助控制盒内的“互救”开关
第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。
+
\ No newline at end of file
diff --git a/reports/bridge-005-moss-junhao-20260919/original-step-01.mp4 b/reports/bridge-005-moss-junhao-20260919/original-step-01.mp4
new file mode 100644
index 0000000..f627c15
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/original-step-01.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/original-step-02.mp4 b/reports/bridge-005-moss-junhao-20260919/original-step-02.mp4
new file mode 100644
index 0000000..b79c9d4
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/original-step-02.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/original-step-03.mp4 b/reports/bridge-005-moss-junhao-20260919/original-step-03.mp4
new file mode 100644
index 0000000..323db9e
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/original-step-03.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/original-step-04.mp4 b/reports/bridge-005-moss-junhao-20260919/original-step-04.mp4
new file mode 100644
index 0000000..2d0aea9
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/original-step-04.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/original-step-05.mp4 b/reports/bridge-005-moss-junhao-20260919/original-step-05.mp4
new file mode 100644
index 0000000..8499d40
Binary files /dev/null and b/reports/bridge-005-moss-junhao-20260919/original-step-05.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-20260919/result.json b/reports/bridge-005-moss-junhao-20260919/result.json
new file mode 100644
index 0000000..7719fe8
--- /dev/null
+++ b/reports/bridge-005-moss-junhao-20260919/result.json
@@ -0,0 +1,62 @@
+{
+ "profile": {
+ "agent": "测试虚拟教员",
+ "slug": "dh-ffcaecf8b65d",
+ "avatar": "教员1",
+ "providerAvatarId": "a1000000000000000000000000000005",
+ "voice": "MOSS · Junhao",
+ "engine": "MOSS-TTS-Nano",
+ "speed": 1.0,
+ "width": 480,
+ "height": 640,
+ "avatarManifestSha256": "7bcdedf502015b52ec230d404cbd6be53b7a40ed74e20fbe618de74ed2b38dc8"
+ },
+ "steps": [
+ {
+ "number": 1,
+ "title": "根据故障现象确定是哪一个液压泵发生故障",
+ "text": "第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。",
+ "video": "TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav",
+ "duration": 9.2,
+ "videoSha256": "c9417a84696b07def80a4d33644f7cd13c4273724ded495bc1c98bed72bf6778"
+ },
+ {
+ "number": 2,
+ "title": "断开架桥车取力器",
+ "text": "第2步,断开架桥车取力器。将架桥车取力器断开。",
+ "video": "TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav",
+ "duration": 6.4,
+ "videoSha256": "b098e2d007f5eea7f0d9aed66697bd643f24c55b76409982183a01a7bec1f039"
+ },
+ {
+ "number": 3,
+ "title": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开",
+ "text": "第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。",
+ "video": "TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav",
+ "duration": 11.4,
+ "videoSha256": "ae6e3b218a449edd334a3904cc75f4db28ee24d6aee5b925ec93779be64230e7"
+ },
+ {
+ "number": 4,
+ "title": "重新联通取力器",
+ "text": "第4步,重新接通取力器。将架桥车取力器重新接通。",
+ "video": "TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav",
+ "duration": 7.0,
+ "videoSha256": "b17425fc91befc848f3396fba19f290760bcf3b8778830fe425197a2f584360a"
+ },
+ {
+ "number": 5,
+ "title": "打开辅助控制盒内的“互救”开关",
+ "text": "第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。",
+ "video": "TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav",
+ "duration": 9.88,
+ "videoSha256": "5a4d2cbe5b26012349154636dafcb3aef1d65798671faf3eef53a7d63d492071"
+ }
+ ],
+ "publishedBindingsChanged": false
+}
\ No newline at end of file
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4
new file mode 100644
index 0000000..eefd2dd
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav
new file mode 100644
index 0000000..2fa6b22
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4
new file mode 100644
index 0000000..e370eb1
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav
new file mode 100644
index 0000000..6c2f396
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4
new file mode 100644
index 0000000..6eee046
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav
new file mode 100644
index 0000000..51d23f7
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4
new file mode 100644
index 0000000..a6f4b07
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav
new file mode 100644
index 0000000..690ccda
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4
new file mode 100644
index 0000000..7d905a1
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav
new file mode 100644
index 0000000..09bca9d
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/frame-step-01.jpg b/reports/bridge-005-moss-junhao-v2-20260919/frame-step-01.jpg
new file mode 100644
index 0000000..0084b2d
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/frame-step-01.jpg differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/index.html b/reports/bridge-005-moss-junhao-v2-20260919/index.html
new file mode 100644
index 0000000..1bc9536
--- /dev/null
+++ b/reports/bridge-005-moss-junhao-v2-20260919/index.html
@@ -0,0 +1,13 @@
+005 · MOSS Junhao 讲解对比
+
+ 005 液压泵应急切换 · 新旧讲解对比
教员1 · v2素材 · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。
第 1 步 · 根据故障现象确定是哪一个液压泵发生故障
第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。
+ 第 2 步 · 断开架桥车取力器
第2步,断开架桥车取力器。将架桥车取力器断开。
+ 第 3 步 · 将架桥车备胎斜上方链接两个红色滤油器的球阀打开
第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。
+ 第 4 步 · 重新联通取力器
第4步,重新接通取力器。将架桥车取力器重新接通。
+ 第 5 步 · 打开辅助控制盒内的“互救”开关
第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。
+
\ No newline at end of file
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/original-step-01.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/original-step-01.mp4
new file mode 100644
index 0000000..f627c15
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/original-step-01.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/original-step-02.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/original-step-02.mp4
new file mode 100644
index 0000000..b79c9d4
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/original-step-02.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/original-step-03.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/original-step-03.mp4
new file mode 100644
index 0000000..323db9e
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/original-step-03.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/original-step-04.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/original-step-04.mp4
new file mode 100644
index 0000000..2d0aea9
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/original-step-04.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/original-step-05.mp4 b/reports/bridge-005-moss-junhao-v2-20260919/original-step-05.mp4
new file mode 100644
index 0000000..8499d40
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/original-step-05.mp4 differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/playback-validation.json b/reports/bridge-005-moss-junhao-v2-20260919/playback-validation.json
new file mode 100644
index 0000000..7353a73
--- /dev/null
+++ b/reports/bridge-005-moss-junhao-v2-20260919/playback-validation.json
@@ -0,0 +1,37 @@
+[
+ {
+ "width": 480,
+ "height": 640,
+ "duration": 9.24,
+ "playing": true,
+ "error": null
+ },
+ {
+ "width": 480,
+ "height": 640,
+ "duration": 6.48,
+ "playing": true,
+ "error": null
+ },
+ {
+ "width": 480,
+ "height": 640,
+ "duration": 11.4,
+ "playing": true,
+ "error": null
+ },
+ {
+ "width": 480,
+ "height": 640,
+ "duration": 6.96,
+ "playing": true,
+ "error": null
+ },
+ {
+ "width": 480,
+ "height": 640,
+ "duration": 9.96,
+ "playing": true,
+ "error": null
+ }
+]
\ No newline at end of file
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/preview-page.png b/reports/bridge-005-moss-junhao-v2-20260919/preview-page.png
new file mode 100644
index 0000000..96568eb
Binary files /dev/null and b/reports/bridge-005-moss-junhao-v2-20260919/preview-page.png differ
diff --git a/reports/bridge-005-moss-junhao-v2-20260919/result.json b/reports/bridge-005-moss-junhao-v2-20260919/result.json
new file mode 100644
index 0000000..21d6db9
--- /dev/null
+++ b/reports/bridge-005-moss-junhao-v2-20260919/result.json
@@ -0,0 +1,62 @@
+{
+ "profile": {
+ "agent": "测试虚拟教员",
+ "slug": "dh-ffcaecf8b65d",
+ "avatar": "教员1",
+ "providerAvatarId": "a1000000000000000000000000000005",
+ "voice": "MOSS · Junhao",
+ "engine": "MOSS-TTS-Nano",
+ "speed": 1.0,
+ "width": 480,
+ "height": 640,
+ "avatarManifestSha256": "8155a21af68b60ea024c7591c8ad454aa9884a2c7d22cb069c16bb192bc4e122"
+ },
+ "steps": [
+ {
+ "number": 1,
+ "title": "根据故障现象确定是哪一个液压泵发生故障",
+ "text": "第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。",
+ "video": "TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav",
+ "duration": 9.24,
+ "videoSha256": "39fe0da650f4ee162c6106073509415bbbb869a3657e145d69acd1b32fdf8195"
+ },
+ {
+ "number": 2,
+ "title": "断开架桥车取力器",
+ "text": "第2步,断开架桥车取力器。将架桥车取力器断开。",
+ "video": "TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav",
+ "duration": 6.48,
+ "videoSha256": "74ea5f713a855136d7152dcadc7d97523556e229181e58a593a975a5f5550af6"
+ },
+ {
+ "number": 3,
+ "title": "将架桥车备胎斜上方链接两个红色滤油器的球阀打开",
+ "text": "第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。",
+ "video": "TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav",
+ "duration": 11.4,
+ "videoSha256": "ad2495509abf63328e7832fc471059e66b37f93d37276499e97682af1238163c"
+ },
+ {
+ "number": 4,
+ "title": "重新联通取力器",
+ "text": "第4步,重新接通取力器。将架桥车取力器重新接通。",
+ "video": "TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav",
+ "duration": 6.96,
+ "videoSha256": "e91ea2f247d9ceadb2c4a5c5e0e516609891be19572a4f08271b7a9aad611095"
+ },
+ {
+ "number": 5,
+ "title": "打开辅助控制盒内的“互救”开关",
+ "text": "第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。",
+ "video": "TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4",
+ "audio": "TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav",
+ "duration": 9.96,
+ "videoSha256": "2663e8ec475e2b12eaa9daa4d5cce94fe6360d3ea7f14f3a6fef3ad36073c1ab"
+ }
+ ],
+ "publishedBindingsChanged": false
+}
\ No newline at end of file
diff --git a/reports/bridge-005-moss-refined-20260920/README.md b/reports/bridge-005-moss-refined-20260920/README.md
new file mode 100644
index 0000000..8c11492
--- /dev/null
+++ b/reports/bridge-005-moss-refined-20260920/README.md
@@ -0,0 +1,13 @@
+
+
+### 2026-09-20 讲解连贯性与口型复核
+
+005 第一步旧 MOSS 原始 WAV 在 -38dB 阈值下存在约 1.69s、2.77s 长静音。成片音轨与源 WAV 的估计整体偏移约 29ms;尚无证据表明双播放时钟是主要原因。相同音频按 48kHz 与 16kHz 输入得到不同的 WASM blendshape 轨迹,录制时采样帧率约 25fps。生成入口现统一转换为此前验证样片使用的 16kHz、单声道、PCM16 WAV,播放和口型使用同一转换结果;重采样不改变音高和时长。
+
+独立第一步样片改用连贯文稿,在 -48dB、至少 0.65s 的长静音内部缩短留白至 0.32s,非静音区间保留;处理规则尚未全局应用到 MOSS 或其他脚本。浏览器播放、解码和重采样单测通过,不代表逐字口型和发音已通过人工验收。未更改已发布任务视频。
+
+样片:TASK-VIRTUAL-005-step-01-Junhao-refined.mp4;对比页 index.html。音频 5.582s,成片 5.76s,480×640。
+
+ASR 辅助复核识别出完整操作句,存在“故障现象”同音字误识别;ASR 不能代替人工听感和口型评估。
+
+额外 greedy 采样实验触发上游 codec 内存分配失败及降级,不采用;当前样片使用正常完成的 seed42 采样结果。
diff --git a/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.mp4 b/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.mp4
new file mode 100644
index 0000000..b2d558c
Binary files /dev/null and b/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.mp4 differ
diff --git a/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.wav b/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.wav
new file mode 100644
index 0000000..aeb3f53
Binary files /dev/null and b/reports/bridge-005-moss-refined-20260920/TASK-VIRTUAL-005-step-01-Junhao-refined.wav differ
diff --git a/reports/bridge-005-moss-refined-20260920/index.html b/reports/bridge-005-moss-refined-20260920/index.html
new file mode 100644
index 0000000..d059a5f
--- /dev/null
+++ b/reports/bridge-005-moss-refined-20260920/index.html
@@ -0,0 +1 @@
+005第一步讲解修正版005 · 第一步讲解复核
教员1 v2 · MOSS Junhao。修正版使用连贯句式、压缩长静音、统一16kHz口型输入。仍为独立试片,待人工确认口型和发音。
\ No newline at end of file
diff --git a/reports/bridge-005-moss-refined-20260920/natural-unedited.wav b/reports/bridge-005-moss-refined-20260920/natural-unedited.wav
new file mode 100644
index 0000000..325d702
Binary files /dev/null and b/reports/bridge-005-moss-refined-20260920/natural-unedited.wav differ
diff --git a/reports/bridge-005-moss-refined-20260920/preview.png b/reports/bridge-005-moss-refined-20260920/preview.png
new file mode 100644
index 0000000..980bb2e
Binary files /dev/null and b/reports/bridge-005-moss-refined-20260920/preview.png differ
diff --git a/reports/bridge-005-moss-refined-20260920/previous.mp4 b/reports/bridge-005-moss-refined-20260920/previous.mp4
new file mode 100644
index 0000000..eefd2dd
Binary files /dev/null and b/reports/bridge-005-moss-refined-20260920/previous.mp4 differ
diff --git a/reports/bridge-005-moss-refined-20260920/result.json b/reports/bridge-005-moss-refined-20260920/result.json
new file mode 100644
index 0000000..2141f1e
--- /dev/null
+++ b/reports/bridge-005-moss-refined-20260920/result.json
@@ -0,0 +1,160 @@
+{
+ "text": "第一步,根据故障现象,确认是哪一个液压泵发生故障。",
+ "voice": "MOSS Junhao",
+ "seed": 42,
+ "cuts": [
+ {
+ "start": 0.581562,
+ "end": 1.949187,
+ "keptSeconds": 0.32
+ },
+ {
+ "start": 3.084229,
+ "end": 4.454417,
+ "keptSeconds": 0.32
+ }
+ ],
+ "inputDuration": 7.68,
+ "preparedDuration": 5.5821875,
+ "lipInputRate": 16000,
+ "metadata": {
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 480,
+ "height": 640,
+ "coded_width": 480,
+ "coded_height": 640,
+ "closed_captions": 0,
+ "film_grain": 0,
+ "has_b_frames": 2,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "3:4",
+ "pix_fmt": "yuv420p",
+ "level": 30,
+ "color_range": "tv",
+ "color_space": "bt470bg",
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "id": "0x1",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/12800",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 73728,
+ "duration": "5.760000",
+ "bit_rate": "336540",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "144",
+ "extradata_size": 49,
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "non_diegetic": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng",
+ "handler_name": "VideoHandler",
+ "vendor_id": "[0][0][0][0]",
+ "encoder": "Lavc61.3.100 libx264"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_tag_string": "mp4a",
+ "codec_tag": "0x6134706d",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "initial_padding": 0,
+ "id": "0x2",
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 1008,
+ "start_time": "0.021000",
+ "duration_ts": 268888,
+ "duration": "5.601833",
+ "bit_rate": "128656",
+ "nb_frames": "263",
+ "extradata_size": 5,
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "non_diegetic": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng",
+ "handler_name": "SoundHandler",
+ "vendor_id": "[0][0][0][0]"
+ }
+ }
+ ],
+ "format": {
+ "filename": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\bridge-005-moss-refined-20260920\\TASK-VIRTUAL-005-step-01-Junhao-refined.mp4",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "nb_stream_groups": 0,
+ "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+ "format_long_name": "QuickTime / MOV",
+ "start_time": "0.000000",
+ "duration": "5.760000",
+ "size": "339220",
+ "bit_rate": "471138",
+ "probe_score": 100,
+ "tags": {
+ "major_brand": "isom",
+ "minor_version": "512",
+ "compatible_brands": "isomiso2avc1mp41",
+ "encoder": "Lavf61.1.100"
+ }
+ }
+ },
+ "publishedBindingsChanged": false
+}
\ No newline at end of file
diff --git a/tools/bridge-005-moss-preview-check.cjs b/tools/bridge-005-moss-preview-check.cjs
new file mode 100644
index 0000000..8e1e0e6
--- /dev/null
+++ b/tools/bridge-005-moss-preview-check.cjs
@@ -0,0 +1,26 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const { pathToFileURL } = require('node:url');
+const { chromium } = require('@playwright/test');
+(async()=>{
+ const dir=path.resolve(__dirname,'../reports/bridge-005-moss-junhao-v2-20260919');
+ const browser=await chromium.launch({headless:true,channel:'chrome'});
+ try {
+ const page=await browser.newPage({viewport:{width:1280,height:1000}});
+ await page.goto(pathToFileURL(path.join(dir,'index.html')).href);
+ if(await page.locator('video').count()!==10) throw new Error('Expected five pairs of videos');
+ const results=[];
+ for(let step=0;step<5;step++){
+ const video=page.locator('video').nth(step*2);
+ await video.scrollIntoViewIfNeeded();
+ await video.evaluate(v=>v.play());
+ await page.waitForFunction(index=>{const v=document.querySelectorAll('video')[index];return v.currentTime>.15&&!v.paused&&!v.error},step*2);
+ results.push(await video.evaluate(v=>({width:v.videoWidth,height:v.videoHeight,duration:v.duration,playing:!v.paused,error:v.error?.code||null})));
+ await video.evaluate(v=>v.pause());
+ }
+ await page.evaluate(()=>scrollTo(0,0));
+ await page.screenshot({path:path.join(dir,'preview-page.png')});
+ fs.writeFileSync(path.join(dir,'playback-validation.json'),JSON.stringify(results,null,2));
+ console.log('PASS: five new videos played, five original comparisons present',results);
+ } finally {await browser.close()}
+})().catch(e=>{console.error(e.message);process.exitCode=1});
diff --git a/tools/bridge-005-moss-preview.py b/tools/bridge-005-moss-preview.py
new file mode 100644
index 0000000..6dbd9e2
--- /dev/null
+++ b/tools/bridge-005-moss-preview.py
@@ -0,0 +1,127 @@
+"""Generate an isolated five-step preview using the workshop's avatar and Junhao."""
+from pathlib import Path
+import hashlib
+import html
+import json
+import shutil
+import subprocess
+import sys
+import threading
+import time
+import argparse
+
+import httpx
+from sqlalchemy import select
+
+ROOT = Path(__file__).resolve().parents[3]
+sys.path.insert(0, str(ROOT / 'ai_person/ai_person_api'))
+from app.config import get_settings
+from app.persistence.database import get_session_factory
+from app.persistence.agent_models import Agent, AgentAvatar, AgentVoice
+from app.persistence.asset_models import Avatar, CapabilityAsset
+from app.schemas.open_platform import GenerationCommand
+from app.services.moss_tts_service import MossTtsService
+from app.services.open_generation_worker import OpenGenerationWorker
+
+REPORT = ROOT / 'unreal_tran/ute2e/reports/bridge-005-moss-junhao-v2-20260919'
+WEB = ROOT / 'unreal_tran/unreal_tran_web'
+
+
+def read_manifest(name):
+ source = (WEB / 'src/features/virtual-training-scripts' / name).read_text('utf-8')
+ return json.loads(source.split('export default', 1)[1].strip().rstrip(';'))
+
+
+def digest(path):
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def main():
+ REPORT.mkdir(parents=True, exist_ok=True)
+ settings = get_settings()
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--service-url', required=True, help='Renderer with the validated v2 avatar')
+ args = parser.parse_args()
+ settings = settings.model_copy(update={'service_base_url': args.service_url})
+ with httpx.Client(trust_env=False, timeout=30) as client:
+ widget = client.get('http://127.0.0.1:6180/api/auth/v1/system-config/public').json()['data']['digitalHumanWidget']
+ with get_session_factory()() as db:
+ agent = db.scalar(select(Agent).where(Agent.slug == widget['agentSlug'], Agent.is_delete == 0))
+ relation = db.scalar(select(AgentAvatar).where(AgentAvatar.agent_id == agent.id).order_by(AgentAvatar.is_default.desc(), AgentAvatar.sort_order))
+ avatar = db.get(Avatar, relation.avatar_id)
+ voice = db.scalar(select(CapabilityAsset).where(CapabilityAsset.asset_code == 'builtin-voice-moss-junhao'))
+ assert db.scalar(select(AgentVoice).where(AgentVoice.agent_id == agent.id, AgentVoice.capability_id == voice.id)), 'Junhao is not selected in the linked agent'
+ avatar_id, voice_id = str(avatar.id), str(voice.id)
+ provider_avatar = avatar.provider_avatar_id
+ profile = {'agent': agent.agent_name, 'slug': agent.slug, 'avatar': avatar.avatar_name,
+ 'providerAvatarId': provider_avatar, 'voice': voice.asset_name, 'engine': 'MOSS-TTS-Nano',
+ 'speed': float(agent.voice_speed), 'width': 480, 'height': 640}
+ manifest = client.get(str(settings.service_base_url).rstrip('/')+f'/api/v1/avatars/{provider_avatar}/assets/manifest.json')
+ manifest.raise_for_status()
+ profile['avatarManifestSha256'] = hashlib.sha256(manifest.content).hexdigest()
+ expected = json.loads((ROOT / 'ai_person/ai_person_service/builtin_avatars/instructors-v2.json').read_text('utf-8'))['instructors'][0]
+ assert profile['avatarManifestSha256'] == expected['bundle_sha256']['manifest.json'], 'Renderer does not have the approved v2 avatar'
+ pack = next(p for p in read_manifest('narration-manifest.js')['scripts'] if p['code'] == 'TASK-VIRTUAL-005')
+ old_pack = next(p for p in read_manifest('narration-video-manifest.js')['scripts'] if p['code'] == 'TASK-VIRTUAL-005')
+ renderer = OpenGenerationWorker(None, settings, None, None)
+ engine = MossTtsService(settings)
+ rows = []
+ try:
+ for step in pack['steps']:
+ number = step['number']
+ stem = f'TASK-VIRTUAL-005-step-{number:02}-MOSS-Junhao'
+ audio, video = REPORT / f'{stem}.wav', REPORT / f'{stem}.mp4'
+ capture = REPORT / f'{stem}.webm'
+ if video.exists():
+ raise RuntimeError('Output exists; use a new report folder to preserve comparisons')
+ print(f'Step {number}: synthesizing Junhao', flush=True)
+ previous_audio = REPORT.parent / 'bridge-005-moss-junhao-20260919' / audio.name
+ if previous_audio.exists():
+ import wave
+ shutil.copyfile(previous_audio, audio)
+ with wave.open(str(audio)) as wav:
+ generated = {'duration': wav.getnframes() / wav.getframerate()}
+ else:
+ generated = engine.synthesize(step['text'], voice_code='Junhao', speed=profile['speed'])
+ source = engine.output_path(generated['id'])
+ shutil.copyfile(source, audio)
+ source.unlink()
+ print(f'Step {number}: rendering avatar, audio {generated["duration"]:.2f}s', flush=True)
+ command = GenerationCommand(title=f'005 第{number}步 MOSS Junhao', text=step['text'],
+ avatarId=avatar_id, voiceId=voice_id, speed=profile['speed'], width=480, height=640, subtitles=False)
+ renderer.capture(command, provider_avatar, audio, None, capture, threading.Event())
+ subprocess.run([settings.ffmpeg_binary, '-hide_banner', '-loglevel', 'error', '-y', '-i', str(capture),
+ '-r', '25', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p',
+ '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', str(video)], check=True)
+ info = json.loads(subprocess.run([settings.ffprobe_binary, '-v', 'error', '-show_streams', '-show_format',
+ '-of', 'json', str(video)], capture_output=True, text=True, check=True).stdout)
+ v = next(s for s in info['streams'] if s['codec_type'] == 'video')
+ a = next(s for s in info['streams'] if s['codec_type'] == 'audio')
+ assert (v['width'], v['height'], v['codec_name']) == (480, 640, 'h264')
+ assert a['codec_name'] == 'aac'
+ assert abs(float(info['format']['duration']) - generated['duration']) < 0.7
+ subprocess.run([settings.ffmpeg_binary, '-v', 'error', '-i', str(video), '-f', 'null', '-'], check=True)
+ previous = next(s for s in old_pack['steps'] if s['number'] == number)
+ old = WEB / 'public' / previous['url'].lstrip('/')
+ if old.is_file(): shutil.copyfile(old, REPORT / f'original-step-{number:02}.mp4')
+ row = {'number': number, 'title': step['expectedTitle'], 'text': step['text'], 'video': video.name,
+ 'audio': audio.name, 'duration': float(info['format']['duration']), 'videoSha256': digest(video)}
+ rows.append(row)
+ (REPORT / 'result.json').write_text(json.dumps({'profile': profile, 'steps': rows, 'publishedBindingsChanged': False}, ensure_ascii=False, indent=2), 'utf-8')
+ print(f'Step {number}: complete ({row["duration"]:.2f}s)', flush=True)
+ finally:
+ engine.close()
+ cards = []
+ for row in rows:
+ cards.append(f'''第 {row['number']} 步 · {html.escape(row['title'])}
{html.escape(row['text'])}
+ ''')
+ page = '''005 · MOSS Junhao 讲解对比
+
+ 005 液压泵应急切换 · 新旧讲解对比
教员1 · v2素材 · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。
''' + ''.join(cards) + ''''''
+ (REPORT / 'index.html').write_text(page, 'utf-8')
+ print('Preview ready: '+str(REPORT / 'index.html'), flush=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/bridge-005-refined-sample.py b/tools/bridge-005-refined-sample.py
new file mode 100644
index 0000000..6388630
--- /dev/null
+++ b/tools/bridge-005-refined-sample.py
@@ -0,0 +1,50 @@
+from pathlib import Path
+import sys,json,subprocess,re,threading,hashlib,shutil
+import numpy as np
+import soundfile as sf
+import httpx
+
+ROOT=Path(__file__).resolve().parents[3]
+sys.path.insert(0,str(ROOT/'ai_person/ai_person_api'))
+from app.config import get_settings
+from app.schemas.open_platform import GenerationCommand
+from app.services.open_generation_worker import OpenGenerationWorker
+
+out=ROOT/'unreal_tran/ute2e/reports/bridge-005-moss-refined-20260920'
+out.mkdir(parents=True,exist_ok=True)
+source=ROOT/'ai_person/outputs/moss-integration-check/step1-refined/natural-42.wav'
+audio,sr=sf.read(source)
+detect=subprocess.run(['ffmpeg','-hide_banner','-i',str(source),'-af','silencedetect=noise=-48dB:d=0.65','-f','null','-'],capture_output=True,text=True,check=True)
+starts=re.findall(r'silence_start: ([0-9.]+)',detect.stderr)
+ends=re.findall(r'silence_end: ([0-9.]+)',detect.stderr)
+pieces=[];last=0;cuts=[]
+for start,end in zip(starts,ends):
+ start,end=float(start),float(end)
+ a,b=round((start+.16)*sr),round((end-.16)*sr)
+ if b<=a:continue
+ pieces.append(audio[last:a]);last=b;cuts.append({'start':start,'end':end,'keptSeconds':.32})
+pieces.append(audio[last:])
+prepared=np.concatenate(pieces)
+wav=out/'TASK-VIRTUAL-005-step-01-Junhao-refined.wav'
+sf.write(wav,prepared,sr,subtype='PCM_16')
+shutil.copyfile(source,out/'natural-unedited.wav')
+settings=get_settings().model_copy(update={'service_base_url':'http://1.14.103.234:8123'})
+avatar='a1000000000000000000000000000005'
+with httpx.Client(trust_env=False,timeout=30) as client:
+ response=client.get(str(settings.service_base_url)+f'/api/v1/avatars/{avatar}/assets/manifest.json');response.raise_for_status()
+ expected=json.loads((ROOT/'ai_person/ai_person_service/builtin_avatars/instructors-v2.json').read_text('utf-8'))['instructors'][0]['bundle_sha256']['manifest.json']
+ assert hashlib.sha256(response.content).hexdigest()==expected
+text='第一步,根据故障现象,确认是哪一个液压泵发生故障。'
+command=GenerationCommand(title='005 第一步 Junhao 连贯性修正版',text=text,avatarId='55',voiceId='49',speed=1,width=480,height=640,subtitles=False)
+worker=OpenGenerationWorker(None,settings,None,None)
+capture=out/'capture.webm';video=out/'TASK-VIRTUAL-005-step-01-Junhao-refined.mp4'
+print('pause cleanup:',cuts,'duration:',len(prepared)/sr,flush=True)
+worker.capture(command,avatar,wav,None,capture,threading.Event())
+subprocess.run(['ffmpeg','-v','error','-y','-i',str(capture),'-r','25','-c:v','libx264','-crf','20','-preset','veryfast','-pix_fmt','yuv420p','-c:a','aac','-b:a','128k','-movflags','+faststart',str(video)],check=True)
+subprocess.run(['ffmpeg','-v','error','-i',str(video),'-f','null','-'],check=True)
+probe=json.loads(subprocess.run(['ffprobe','-v','error','-show_streams','-show_format','-of','json',str(video)],capture_output=True,text=True,check=True).stdout)
+assert abs(float(probe['format']['duration'])-len(prepared)/sr)<.7
+shutil.copyfile(out.parent/'bridge-005-moss-junhao-v2-20260919/TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4',out/'previous.mp4')
+(out/'result.json').write_text(json.dumps({'text':text,'voice':'MOSS Junhao','seed':42,'cuts':cuts,'inputDuration':len(audio)/sr,'preparedDuration':len(prepared)/sr,'lipInputRate':16000,'metadata':probe,'publishedBindingsChanged':False},ensure_ascii=False,indent=2),'utf-8')
+(out/'index.html').write_text('''005第一步讲解修正版005 · 第一步讲解复核
教员1 v2 · MOSS Junhao。修正版使用连贯句式、压缩长静音、统一16kHz口型输入。仍为独立试片,待人工确认口型和发音。
''','utf-8')
+print('completed',video,flush=True)