Spaces:
Running
Running
Download scripts/gen_tts.py from Ethscriptions/mx: direct link, hf CLI and curl.
- Browser
- Download file 4.85 kB
-
https://huggingface.co/spaces/Ethscriptions/mx/resolve/main/scripts/gen_tts.py
- Command line
-
hf download hf://spaces/Ethscriptions/mx/scripts/gen_tts.py
-
curl -L -o gen_tts.py https://huggingface.co/spaces/Ethscriptions/mx/resolve/main/scripts/gen_tts.py
4.85 kB
| """Generate narration clips (zh-CN neural TTS) + timeline.json. | |
| python3 scripts/gen_tts.py [rate] | |
| """ | |
| import asyncio, json, os, sys | |
| import edge_tts | |
| import miniaudio, numpy as np | |
| def measure(path): | |
| dec = miniaudio.decode_file(path, output_format=miniaudio.SampleFormat.FLOAT32, nchannels=1, sample_rate=24000) | |
| a = np.abs(np.array(dec.samples)); idx = np.where(a > 0.008)[0] | |
| st = max(0, idx[0] / 24000 - 0.03); en = min(len(a) / 24000, idx[-1] / 24000 + 0.06) | |
| return st, en - st | |
| VOICE = "zh-CN-YunyangNeural" | |
| RATE = sys.argv[1] if len(sys.argv) > 1 else "+30%" | |
| OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "public", "audio") | |
| SHOTS = [ | |
| ("疼痛开始", ["手指一弯,手腕一动,", "这里为什么突然疼?"]), | |
| ("腱鞘炎到底发生了什么", ["腱鞘,本来像一条小通道,", "让肌腱顺畅滑动。", "但长期反复摩擦,", "腱鞘可能出现肿胀、增厚。", "通道变得更紧。", "肌腱经过这里,就会越来越不顺。"]), | |
| ("为什么一动就疼", ["这时候,你越抓、越捏、越拧,", "肌腱移动越明显。", "疼痛,也可能越来越明显。"]), | |
| ("哪些人更容易出现", ["哪些人更容易出现?", "长期重复用手的人。", "经常抓握、捏、拧的人。", "还有部分需要频繁抱孩子的新手妈妈。"]), | |
| ("不要一上来就直接治疗", ["出现疼痛,不是马上做一种治疗。", "先判断位置。", "看活动时哪里疼。", "必要时结合检查,", "判断到底是哪一种腱鞘问题。"]), | |
| ("常见治疗方法", ["症状比较轻,可以先减少诱发动作。", "必要时使用支具,配合止痛、抗炎等处理。", "症状持续,还可以考虑局部注射等方法。", "少数长期不缓解的情况,可能需要手术松解。"]), | |
| ("重点进入小针刀", ["那小针刀,到底是在做什么?", "不是“扎一下就结束”。", "核心是:找到狭窄的位置,", "对增厚、卡压的区域进行精准松解。"]), | |
| ("小针刀治疗过程", ["先定位。", "再进行消毒。", "必要时进行局部麻醉。", "随后在影像或规范定位辅助下,", "让器械到达目标区域。", "进行松解。"]), | |
| ("松解前后对比", ["松解前,肌腱经过狭窄区域时,明显受限。", "松解后,通道空间改善。", "肌腱活动更顺畅。"]), | |
| ("从疼痛到恢复", ["疼痛改善,不代表马上就能过度使用。", "治疗之后,还要根据情况逐步恢复活动。", "减少再次刺激。", "让手部功能慢慢回来。"]), | |
| ("最后的记忆点", ["所以腱鞘炎,真正要解决的不是“忍住疼”。", "而是找到疼痛的原因,选择合适的治疗方式。", "记住一句话:", "手越疼,越不能只靠硬扛。"]), | |
| ("评论区互动", ["你是哪个动作一做,手腕或者手指就开始疼?"]), | |
| ] | |
| # extra pauses (seconds) after specific lines for dramatic effect / visual beats | |
| LINE_GAP = 0.1 | |
| SHOT_LEAD = {0: 2.3, 7: 0.1} # silence at shot start (visual intro) | |
| SHOT_TAIL = {0: 1.6, 1: 0.3, 2: 0.4, 3: 0.2, 4: 0.3, 5: 0.2, 6: 0.3, 7: 0.6, 8: 0.4, 9: 0.2, 10: 1.0, 11: 2.2} | |
| LINE_EXTRA = {(7, 0): 0.7, (7, 1): 0.8, (7, 2): 0.6, (7, 4): 0.4, (7, 5): 0.4, (10, 2): 0.25, (1, 1): 0.2} | |
| async def main(): | |
| os.makedirs(OUT, exist_ok=True) | |
| lines, shots = [], [] | |
| t = 0.0 | |
| for si, (title, texts) in enumerate(SHOTS): | |
| s_start = t | |
| t += SHOT_LEAD.get(si, 0.15) | |
| for li, text in enumerate(texts): | |
| fn = f"s{si+1:02d}_{li+1:02d}.mp3" | |
| path = os.path.join(OUT, fn) | |
| await edge_tts.Communicate(text, VOICE, rate=RATE).save(path) | |
| off, dur = measure(path) | |
| lines.append({"shot": si, "text": text, "file": fn, "start": round(t, 3), "off": round(off, 3), "dur": round(dur, 3)}) | |
| t += dur + LINE_GAP + LINE_EXTRA.get((si, li), 0) | |
| t += SHOT_TAIL.get(si, 0.4) | |
| shots.append({"index": si, "title": title, "start": round(s_start, 3), "end": round(t, 3)}) | |
| data = {"voice": VOICE, "rate": RATE, "total": round(t, 3), "shots": shots, "lines": lines} | |
| with open(os.path.join(OUT, "timeline.json"), "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=1) | |
| for s in shots: | |
| print(f"{s['index']+1:2d} {s['title']:<12} {s['start']:6.2f}-{s['end']:6.2f} ({s['end']-s['start']:.2f}s)") | |
| print("TOTAL", round(t, 2)) | |
| asyncio.run(main()) | |
| # also emit TS module for the frontend bundle | |
| ts = os.path.join(os.path.dirname(OUT), "..", "src", "video", "timelineData.ts") | |
| with open(ts, "w", encoding="utf-8") as f: | |
| f.write("// generated from public/audio/timeline.json by scripts/gen_tts.py\nexport const TL = " + json.dumps(data, ensure_ascii=False, indent=1) + " as const\n") | |