Bladeren bron

test: 保存005的MOSS数字人讲解样片与音画复核报告

main
leiyun 6 uur geleden
bovenliggende
commit
1ecfd300c6
48 gewijzigde bestanden met toevoegingen van 564 en 0 verwijderingen
  1. BIN
     
  2. BIN
     
  3. BIN
     
  4. BIN
     
  5. BIN
     
  6. BIN
     
  7. BIN
     
  8. BIN
     
  9. BIN
     
  10. BIN
     
  11. +13
    -0
      reports/bridge-005-moss-junhao-20260919/index.html
  12. BIN
     
  13. BIN
     
  14. BIN
     
  15. BIN
     
  16. BIN
     
  17. +62
    -0
      reports/bridge-005-moss-junhao-20260919/result.json
  18. BIN
     
  19. BIN
     
  20. BIN
     
  21. BIN
     
  22. BIN
     
  23. BIN
     
  24. BIN
     
  25. BIN
     
  26. BIN
     
  27. BIN
     
  28. BIN
     
  29. +13
    -0
      reports/bridge-005-moss-junhao-v2-20260919/index.html
  30. BIN
     
  31. BIN
     
  32. BIN
     
  33. BIN
     
  34. BIN
     
  35. +37
    -0
      reports/bridge-005-moss-junhao-v2-20260919/playback-validation.json
  36. BIN
     
  37. +62
    -0
      reports/bridge-005-moss-junhao-v2-20260919/result.json
  38. +13
    -0
      reports/bridge-005-moss-refined-20260920/README.md
  39. BIN
     
  40. BIN
     
  41. +1
    -0
      reports/bridge-005-moss-refined-20260920/index.html
  42. BIN
     
  43. BIN
     
  44. BIN
     
  45. +160
    -0
      reports/bridge-005-moss-refined-20260920/result.json
  46. +26
    -0
      tools/bridge-005-moss-preview-check.cjs
  47. +127
    -0
      tools/bridge-005-moss-preview.py
  48. +50
    -0
      tools/bridge-005-refined-sample.py










+ 13
- 0
reports/bridge-005-moss-junhao-20260919/index.html Bestand weergeven

@@ -0,0 +1,13 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>005 · MOSS Junhao 讲解对比</title>
<style>body{margin:32px auto;max-width:1040px;padding:0 20px;background:#edf5f3;color:#193e38;font:16px/1.7 system-ui}section{background:white;border-radius:16px;padding:24px;margin:24px 0}h2{font-size:20px}.compare{display:flex;gap:28px;flex-wrap:wrap}.compare>div{flex:1;min-width:220px}video{width:100%;max-width:360px;aspect-ratio:3/4;background:#17342f;border-radius:12px}a{color:#008363}</style>
<p><strong>此目录为本机旧素材试渲染,请查看 <a href="../bridge-005-moss-junhao-v2-20260919/index.html">已校验 v2 素材的新旧对比预览</a>。</strong></p><h1>005 液压泵应急切换 · 新旧讲解对比</h1><p>本机旧素材试渲染(非验收版本) · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。</p><section><h2>第 1 步 · 根据故障现象确定是哪一个液压泵发生故障</h2><p>第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-01.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 2 步 · 断开架桥车取力器</h2><p>第2步,断开架桥车取力器。将架桥车取力器断开。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-02.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 3 步 · 将架桥车备胎斜上方链接两个红色滤油器的球阀打开</h2><p>第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-03.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 4 步 · 重新联通取力器</h2><p>第4步,重新接通取力器。将架桥车取力器重新接通。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-04.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 5 步 · 打开辅助控制盒内的“互救”开关</h2><p>第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-05.mp4" controls preload="metadata"></video></div></div></section><script>document.addEventListener('play',e=>{document.querySelectorAll('video,audio').forEach(m=>{if(m!==e.target)m.pause()})},true)</script></html>






+ 62
- 0
reports/bridge-005-moss-junhao-20260919/result.json Bestand weergeven

@@ -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
}












+ 13
- 0
reports/bridge-005-moss-junhao-v2-20260919/index.html Bestand weergeven

@@ -0,0 +1,13 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>005 · MOSS Junhao 讲解对比</title>
<style>body{margin:32px auto;max-width:1040px;padding:0 20px;background:#edf5f3;color:#193e38;font:16px/1.7 system-ui}section{background:white;border-radius:16px;padding:24px;margin:24px 0}h2{font-size:20px}.compare{display:flex;gap:28px;flex-wrap:wrap}.compare>div{flex:1;min-width:220px}video{width:100%;max-width:360px;aspect-ratio:3/4;background:#17342f;border-radius:12px}a{color:#008363}</style>
<h1>005 液压泵应急切换 · 新旧讲解对比</h1><p>教员1 · v2素材 · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。</p><section><h2>第 1 步 · 根据故障现象确定是哪一个液压泵发生故障</h2><p>第1步,确定故障液压泵。根据故障现象确定发生故障的液压泵。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-01-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-01-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-01.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 2 步 · 断开架桥车取力器</h2><p>第2步,断开架桥车取力器。将架桥车取力器断开。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-02-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-02-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-02.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 3 步 · 将架桥车备胎斜上方链接两个红色滤油器的球阀打开</h2><p>第3步,打开互救球阀。打开备胎上方两个红色滤油器之间的连通球阀,手柄由垂直管道转为平行管道。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-03-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-03-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-03.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 4 步 · 重新联通取力器</h2><p>第4步,重新接通取力器。将架桥车取力器重新接通。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-04-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-04-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-04.mp4" controls preload="metadata"></video></div></div></section><section><h2>第 5 步 · 打开辅助控制盒内的“互救”开关</h2><p>第5步,打开辅助控制盒互救开关。打开辅助控制盒盖,再打开互救开关,进入单泵作业状态。</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="TASK-VIRTUAL-005-step-05-MOSS-Junhao.mp4" controls preload="metadata"></video><p><a href="TASK-VIRTUAL-005-step-05-MOSS-Junhao.wav">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-05.mp4" controls preload="metadata"></video></div></div></section><script>document.addEventListener('play',e=>{document.querySelectorAll('video,audio').forEach(m=>{if(m!==e.target)m.pause()})},true)</script></html>






+ 37
- 0
reports/bridge-005-moss-junhao-v2-20260919/playback-validation.json Bestand weergeven

@@ -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
}
]


+ 62
- 0
reports/bridge-005-moss-junhao-v2-20260919/result.json Bestand weergeven

@@ -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
}

+ 13
- 0
reports/bridge-005-moss-refined-20260920/README.md Bestand weergeven

@@ -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 采样结果。



+ 1
- 0
reports/bridge-005-moss-refined-20260920/index.html Bestand weergeven

@@ -0,0 +1 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>005第一步讲解修正版</title><style>body{font:16px/1.8 system-ui;margin:32px auto;max-width:1000px;background:#eef5f3;color:#173e35}main{display:flex;gap:28px}section{background:white;padding:22px;flex:1;border-radius:14px}video{width:100%;aspect-ratio:3/4}audio{width:100%}a{color:#00856c}</style><h1>005 · 第一步讲解复核</h1><p>教员1 v2 · MOSS Junhao。修正版使用连贯句式、压缩长静音、统一16kHz口型输入。仍为独立试片,待人工确认口型和发音。</p><main><section><h2>本次修正版</h2><video controls src="TASK-VIRTUAL-005-step-01-Junhao-refined.mp4"></video><p>第一步,根据故障现象,确认是哪一个液压泵发生故障。</p><audio controls src="TASK-VIRTUAL-005-step-01-Junhao-refined.wav"></audio><p><a href="natural-unedited.wav">试听未处理的新语音</a></p></section><section><h2>上次版本</h2><video controls src="previous.mp4"></video></section></main><script>document.addEventListener('play',e=>{document.querySelectorAll('video,audio').forEach(a=>{if(a!==e.target)a.pause()})},true)</script></html>




+ 160
- 0
reports/bridge-005-moss-refined-20260920/result.json Bestand weergeven

@@ -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
}

+ 26
- 0
tools/bridge-005-moss-preview-check.cjs Bestand weergeven

@@ -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});

+ 127
- 0
tools/bridge-005-moss-preview.py Bestand weergeven

@@ -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'''<section><h2>第 {row['number']} 步 · {html.escape(row['title'])}</h2><p>{html.escape(row['text'])}</p>
<div class="compare"><div><h3>新版 · MOSS Junhao</h3><video src="{row['video']}" controls preload="metadata"></video><p><a href="{row['audio']}">单独试听新语音</a></p></div>
<div><h3>原版 · VITS</h3><video src="original-step-{row['number']:02}.mp4" controls preload="metadata"></video></div></div></section>''')
page = '''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>005 · MOSS Junhao 讲解对比</title>
<style>body{margin:32px auto;max-width:1040px;padding:0 20px;background:#edf5f3;color:#193e38;font:16px/1.7 system-ui}section{background:white;border-radius:16px;padding:24px;margin:24px 0}h2{font-size:20px}.compare{display:flex;gap:28px;flex-wrap:wrap}.compare>div{flex:1;min-width:220px}video{width:100%;max-width:360px;aspect-ratio:3/4;background:#17342f;border-radius:12px}a{color:#008363}</style>
<h1>005 液压泵应急切换 · 新旧讲解对比</h1><p>教员1 · v2素材 · MOSS-TTS-Nano / Junhao · 1.0倍速 · 3:4。五个步骤沿用原讲解文稿,当前为独立预览,线上讲解未替换。</p>''' + ''.join(cards) + '''<script>document.addEventListener('play',e=>{document.querySelectorAll('video,audio').forEach(m=>{if(m!==e.target)m.pause()})},true)</script></html>'''
(REPORT / 'index.html').write_text(page, 'utf-8')
print('Preview ready: '+str(REPORT / 'index.html'), flush=True)


if __name__ == '__main__':
main()

+ 50
- 0
tools/bridge-005-refined-sample.py Bestand weergeven

@@ -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('''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>005第一步讲解修正版</title><style>body{font:16px/1.8 system-ui;margin:32px auto;max-width:1000px;background:#eef5f3;color:#173e35}main{display:flex;gap:28px}section{background:white;padding:22px;flex:1;border-radius:14px}video{width:100%;aspect-ratio:3/4}audio{width:100%}a{color:#00856c}</style><h1>005 · 第一步讲解复核</h1><p>教员1 v2 · MOSS Junhao。修正版使用连贯句式、压缩长静音、统一16kHz口型输入。仍为独立试片,待人工确认口型和发音。</p><main><section><h2>本次修正版</h2><video controls src="TASK-VIRTUAL-005-step-01-Junhao-refined.mp4"></video><p>第一步,根据故障现象,确认是哪一个液压泵发生故障。</p><audio controls src="TASK-VIRTUAL-005-step-01-Junhao-refined.wav"></audio><p><a href="natural-unedited.wav">试听未处理的新语音</a></p></section><section><h2>上次版本</h2><video controls src="previous.mp4"></video></section></main><script>document.addEventListener('play',e=>{document.querySelectorAll('video,audio').forEach(a=>{if(a!==e.target)a.pause()})},true)</script></html>''','utf-8')
print('completed',video,flush=True)

Laden…
Annuleren
Opslaan