File size: 4,852 Bytes
f8c63df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213d393
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""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")