| @@ -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> | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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> | |||||
| @@ -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 | |||||
| } | |||||
| ] | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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 采样结果。 | |||||
| @@ -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> | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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}); | |||||
| @@ -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() | |||||
| @@ -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) | |||||