第二批反馈:引擎改造(限幅/SoundTouch不变调变速/长按快进/跳句/iOS闪光降级) + 6游戏状态BGM与试玩测试全绿
This commit is contained in:
+15
-14
@@ -1,34 +1,35 @@
|
||||
# web/rpg 音频资产清单与再生成方法(R4)
|
||||
# web/rpg 音频资产清单与再生成方法(R4;R6 更新)
|
||||
|
||||
设计依据:`docs/design/05-rpg-ch1-voice-ids.md`(台词)、`05-rpg-chapter1.md`(场景/战斗/资产清单)、`05-rpg-addendum.md`(层叠修正)。
|
||||
设计依据:`docs/design/05-rpg-ch1-voice-ids.md`(台词)、`05-rpg-chapter1.md`(场景/战斗/资产清单)、`05-rpg-addendum.md`(层叠修正)、`docs/design/11-rpg-battle-rebalance.md`(R6 战斗重构缺口资产)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
web/rpg/audio/
|
||||
├── voice/
|
||||
│ ├── male/ # 旁白 107 id(Uncle_Fu,说书人)×{ogg,m4a}
|
||||
│ ├── female/ # 旁白 107 id(Serena,讲武侠故事)×{ogg,m4a}
|
||||
│ ├── male/ # 旁白 118 id(Uncle_Fu,说书人)×{ogg,m4a}
|
||||
│ ├── female/ # 旁白 118 id(Serena,讲武侠故事)×{ogg,m4a}
|
||||
│ └── npc_*. # NPC 14 id 单音色平铺(引擎平铺回退,同 storyline 角色模式)
|
||||
├── bgm/ # village / battle / boss / ending(90s 无缝循环,loudnorm -20)
|
||||
└── sfx/ # 35 件(tel_* 9 / 战斗 6+SYN2 / 探索 4+SYN3 / amb_* 11)
|
||||
├── bgm/ # village / battle / boss / ending(R4)+ menu / explore(R6);90s 无缝循环
|
||||
└── sfx/ # 37 件(tel_* 9 / 战斗 7 / 探索 4 / SYN 6 / amb_* 11)
|
||||
```
|
||||
|
||||
## voice(107 旁白 id + 14 NPC id)
|
||||
## voice(118 旁白 id + 14 NPC id)
|
||||
|
||||
- 旁白 id 分组:A 序章 `intro_1..3`;S 系统 `sys_* help_* settings_hint st_*`(28);D 场景 `desc_*`(16);O 选项 `opt_*`(18);E 事件 `evt_*`(6);B 战斗 `btl_* cmd_* tut_*`(26);SH 商店 `shop_*`(7);addendum 补录敌方档位 `foe_hp_high/mid/low`(3)。
|
||||
- 旁白 id 分组:A 序章 `intro_1..3`;S 系统 `sys_* help_* settings_hint st_*`(28)+ R6 `skip_hint`;D 场景 `desc_*`(16,gate/boss1 按设计无 _re 回访句);O 选项 `opt_*`(18);E 事件 `evt_*`(6);B 战斗 `btl_* cmd_* tut_*`(32,含 R5 补录 `btl_half_bandit`、R6 首战教学 `tut_battle_1..5` 与 11 号文档补录 `tut_b0 tut_b6 btl_flee_fail boss1_warn`);SH 商店 `shop_*`(7);addendum 补录敌方档位 `foe_hp_high/mid/low`(3)。
|
||||
- NPC(平铺,speaker+instruct 恒定):村长 `npc_headman_1..4`(Uncle_Fu·慈祥和缓的老者);老猎人 `npc_hunter_1..4`(Eric·沙哑粗犷豪爽);寨丁头目 `npc_elite_1/half/die`(Dylan·凶狠粗鲁);黑风寨主 `npc_boss1_1/2/die`(Uncle_Fu·低沉阴狠——与村长同 speaker 不同 instruct,请试听区分度)。
|
||||
- 再生成:`source env.d/tts.sh && flock .gpu.lock .venv-tts/bin/python tools/gen_voice_dual.py --game rpg --gender both`(staging 在 `tools/gen_voice_dual_wav/rpg/`,断点续跑;`--only id1,id2` 单句重录;`--check` 两性时长离群检查;`--convert` 转 ogg+m4a)。
|
||||
- R6 缺口 4 句(`tut_b0 tut_b6 btl_flee_fail boss1_warn`):`flock .gpu.lock .venv-tts/bin/python web/rpg/gen/gen_gap_voice.py`,再 `--check` / `--convert`(staging 在 `web/rpg/gen/staging/voice/`,音色/instruct/质控同主管线)。
|
||||
|
||||
## bgm(4 首,Mureka)
|
||||
## bgm(6 首,Mureka)
|
||||
|
||||
`village`(村庄/安全区)`battle`(普通战斗)`boss`(Boss 战)`ending`(章结算)。探索期**无音乐**(addendum §3),`amb_*` 由游戏层手动 loop 接 sfx 总线。再生成:`source env.d/mureka.sh && python3 tools/mureka_gen.py --prompt ... --out tools/mureka_out/rpg_<id>`(prompt 见 `tools/mureka_log.md` R4 节与本目录设计文档)。
|
||||
`village`(村庄/安全区)`battle`(普通战斗)`boss`(Boss 战)`ending`(章结算)+ R6 新增 `menu`(模态菜单 pad,-24 LUFS)与 `explore`(探索轻垫,引擎 volume 0.35,10 文档 §7 恢复)。`amb_*` 仍由游戏层手动 loop 接 sfx 总线(不变)。再生成:`source env.d/mureka.sh && python3 tools/mureka_gen.py --prompt ... --out tools/mureka_out/rpg_<id>`(prompt 见 `tools/mureka_log.md`;R6 两首的 task id 见 docs/audio/round6-report.md §2)。
|
||||
|
||||
## sfx(35 件)
|
||||
## sfx(37 件)
|
||||
|
||||
- SA3 生成 29 件(`tools/gen_rpg_sfx.sh`,staging `tools/gen_rpg_sfx_wav/`):前摇 `tel_wolf tel_bandit tel_spider tel_elite tel_boss1 tel_croc tel_shaman tel_puppet tel_boss2`(裁 1.0s 格挡窗口);战斗 `atk_swing atk_hit parry skill_slash enemy_die potion`;探索 `chest drink_water rest door_heavy`;环境 `amb_village amb_path amb_fork amb_chest amb_stream amb_hunter amb_deepwood amb_gate amb_hall amb_swamp amb_peak`(6s→1s 首尾交叉淡化成 5s 循环,均值 -25dB)。
|
||||
- 程序合成 5 件(numpy,`tools/gen_rpg_syn.py`):`hurt`(低频闷击)`flee`(渐远脚步)`step`(单步)`coin`(硬币叮当)`levelup`(A 小调五声上行钟声琶音)。
|
||||
- one-shot 统一峰值 -1dBFS;`ui_deny` 按设计不做(反馈走震动)。
|
||||
- SA3 生成 30 件(R4 批 `tools/gen_rpg_sfx.sh`,staging `tools/gen_rpg_sfx_wav/`;R6 `victory` 用 `web/rpg/gen/gen_gap_sfx.sh`,staging `web/rpg/gen/staging/sfx/`):前摇 `tel_wolf tel_bandit tel_spider tel_elite tel_boss1 tel_croc tel_shaman tel_puppet tel_boss2`(裁 1.0s 格挡窗口);战斗 `atk_swing atk_hit parry skill_slash enemy_die potion victory`(胜利号角 1.2s);探索 `chest drink_water rest door_heavy`;环境 `amb_village amb_path amb_fork amb_chest amb_stream amb_hunter amb_deepwood amb_gate amb_hall amb_swamp amb_peak`(6s→1s 首尾交叉淡化成 5s 循环,均值 -25dB)。
|
||||
- 程序合成 6 件(numpy;R4 五件 `tools/gen_rpg_syn.py`,R6 `turn_player` 由 `web/rpg/gen/gen_gap_sfx.py` 合成):`hurt`(低频闷击)`flee`(渐远脚步)`step`(单步)`coin`(硬币叮当)`levelup`(A 小调五声上行钟声琶音)`turn_player`(E5→A5 上行双音拨弦 0.36s,"轮到你"听觉标点)。
|
||||
- one-shot 统一 R6 标准:去直流 + 首尾 8ms cos² 淡入淡出 + 峰值 -3dBFS(ogg 解码回读真峰值校验);`ui_deny` 按设计不做(反馈走震动)。
|
||||
|
||||
## 双格式约定
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""R6 缺口 sfx(docs/design/11-rpg-battle-rebalance.md §5):turn_player / victory。
|
||||
|
||||
- turn_player:程序合成(numpy)。上行双音(五声 E5→A5 拨弦质感),≤0.4s,
|
||||
"轮到你"的听觉标点,每回合出现,必须极短不烦。
|
||||
- victory:Stable Audio 3 生成(见 gen_gap_sfx.sh,英文 prompt 照录 11 文档:
|
||||
"triumphant short brass fanfare, 3 rising notes, wuxia style, under 1.5 seconds"),
|
||||
本脚本负责后处理:去头静音 + 定长 1.2s。
|
||||
|
||||
用法:
|
||||
bash web/rpg/gen/gen_gap_sfx.sh # SA3 生成 victory staging(flock GPU)
|
||||
.venv-tts/bin/python web/rpg/gen/gen_gap_sfx.py # 合成 turn_player + 两者后处理落盘
|
||||
|
||||
后处理标准(docs/audio/asset-manifest.md R6 起):one-shot 去直流 → 首尾 8ms cos²
|
||||
淡入淡出 → 峰值 -3dBFS(ogg 解码回读真峰值校验,超 -3dB 降增益重编,首轮附带
|
||||
18kHz 低通,最多 3 次迭代)。落盘 web/rpg/audio/sfx/<id>.{ogg,m4a}(q3 / aac 128k)。
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
|
||||
ROOT = "/home/ubuntu/SoundGame"
|
||||
STAGING = os.path.join(ROOT, "web", "rpg", "gen", "staging", "sfx")
|
||||
OUT = os.path.join(ROOT, "web", "rpg", "audio", "sfx")
|
||||
FFMPEG = os.path.join(ROOT, "tools", "bin", "ffmpeg")
|
||||
|
||||
FADE_MS = 8
|
||||
PEAK_TARGET = 10 ** (-3.0 / 20) # -3dBFS
|
||||
TRIM = {"victory": 1.2} # 去头后定长(秒);turn_player 合成即定长不裁
|
||||
|
||||
|
||||
def synth_turn_player(path, sr=44100):
|
||||
"""上行双音拨弦:E5(659.25) → A5(880),各 0.16s,总 0.36s。
|
||||
基波 + 2/3 次谐波指数衰减模拟拨弦亮度,避免纯正弦的"提示音"廉价感。"""
|
||||
def pluck(freq, dur, sr):
|
||||
t = np.arange(int(sr * dur)) / sr
|
||||
env = np.exp(-t * 18)
|
||||
y = (np.sin(2 * np.pi * freq * t)
|
||||
+ 0.45 * np.sin(2 * np.pi * freq * 2 * t)
|
||||
+ 0.18 * np.sin(2 * np.pi * freq * 3 * t)) * env
|
||||
return y
|
||||
n1 = pluck(659.25, 0.16, sr)
|
||||
n2 = pluck(880.00, 0.20, sr)
|
||||
y = np.concatenate([n1, n2]).astype(np.float32)
|
||||
sf.write(path, y, sr)
|
||||
print(f"[synth] turn_player {len(y) / sr:.2f}s")
|
||||
|
||||
|
||||
def head_trim(y, sr, thresh_db=-45):
|
||||
"""去头静音(5ms 包络,阈值 -45dB,同 R4/R6 工艺)。"""
|
||||
mono = np.abs(y.mean(axis=1))
|
||||
win = int(sr * 0.005)
|
||||
env = np.convolve(mono, np.ones(win) / win, mode="same")
|
||||
start = int(np.argmax(env > 10 ** (thresh_db / 20)))
|
||||
return y[start:]
|
||||
|
||||
|
||||
def fix_array(y, sr):
|
||||
"""one-shot 标准:首尾 8ms cos² 淡入淡出 → 峰值归一 -3dBFS → 去直流。"""
|
||||
y = y.astype(np.float64)
|
||||
n = int(sr * FADE_MS / 1000)
|
||||
ramp = np.sin(np.linspace(0, np.pi / 2, n)) ** 2
|
||||
y[:n] *= ramp[:, None] if y.ndim > 1 else ramp
|
||||
y[-n:] *= ramp[::-1][:, None] if y.ndim > 1 else ramp[::-1]
|
||||
peak = np.max(np.abs(y))
|
||||
if peak > 0:
|
||||
y = y / peak * PEAK_TARGET
|
||||
y = y - y.mean(axis=0)
|
||||
return y.astype(np.float32)
|
||||
|
||||
|
||||
def encode(wav_path, base):
|
||||
"""编码 ogg+m4a;ogg 解码回读真峰值校验(MDCT 过冲根因见 round6-report §1),
|
||||
过冲则降增益重编(首轮附带 18kHz 低通),最多 3 次。返回解码实测峰值 dB。"""
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", wav_path, "-ar", "44100",
|
||||
"-c:a", "aac", "-b:a", "128k", base + ".m4a"], check=True)
|
||||
cur = wav_path
|
||||
peak_db = None
|
||||
for attempt in range(3):
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", cur, "-ar", "44100",
|
||||
"-c:a", "libvorbis", "-q:a", "3", base + ".ogg"], check=True)
|
||||
dec, _sr = sf.read(base + ".ogg", always_2d=True)
|
||||
peak_db = 20 * np.log10(max(float(np.max(np.abs(dec))), 1e-9))
|
||||
if peak_db <= -2.7:
|
||||
break
|
||||
gain_db = -3.2 - peak_db
|
||||
nxt = wav_path + f".tp{attempt}.wav"
|
||||
af = ("lowpass=f=18000," if attempt == 0 else "") + f"volume={gain_db:.2f}dB"
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", cur,
|
||||
"-af", af, "-c:a", "pcm_f32le", nxt], check=True)
|
||||
cur = nxt
|
||||
return peak_db
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(STAGING, exist_ok=True)
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
tp_staging = os.path.join(STAGING, "turn_player.wav")
|
||||
if not os.path.exists(tp_staging):
|
||||
synth_turn_player(tp_staging)
|
||||
|
||||
for fid in ("turn_player", "victory"):
|
||||
src = os.path.join(STAGING, fid + ".wav")
|
||||
if not os.path.exists(src):
|
||||
print(f"[WARN] 缺 staging {fid}.wav(victory 需先跑 gen_gap_sfx.sh 生成)")
|
||||
continue
|
||||
y, sr = sf.read(src, always_2d=True)
|
||||
y = head_trim(y, sr)
|
||||
if fid in TRIM:
|
||||
y = y[:int(sr * TRIM[fid])]
|
||||
y = fix_array(y, sr)
|
||||
fixed = os.path.join(STAGING, fid + ".fixed.wav")
|
||||
sf.write(fixed, y, sr)
|
||||
base = os.path.join(OUT, fid)
|
||||
peak_db = encode(fixed, base)
|
||||
dur = len(y) / sr
|
||||
mono = y.mean(axis=1)
|
||||
print(f"[ok] {fid} dur={dur:.2f}s true_peak={peak_db:.2f}dBFS "
|
||||
f"dc={float(np.mean(mono)):.5f} -> {base}.{{ogg,m4a}}")
|
||||
assert dur <= 0.4 if fid == "turn_player" else dur <= 1.2, f"{fid} 超长 {dur:.2f}s"
|
||||
assert peak_db <= -2.7, f"{fid} 真峰值超标 {peak_db:.2f}dB"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# R6 缺口 sfx:victory(Stable Audio 3,英文 prompt 照录 docs/design/11 §5)。
|
||||
# 用法: bash web/rpg/gen/gen_gap_sfx.sh (内部 flock 串行 GPU;输出 staging)
|
||||
set -u
|
||||
source /home/ubuntu/SoundGame/env.d/sfx.sh
|
||||
cd /home/ubuntu/SoundGame/tools/stable-audio-3
|
||||
OUT=/home/ubuntu/SoundGame/web/rpg/gen/staging/sfx
|
||||
mkdir -p "$OUT"
|
||||
|
||||
flock /home/ubuntu/SoundGame/.gpu.lock bash -c '
|
||||
if [ -s "'"$OUT"'/victory.wav" ]; then echo "skip victory"; else
|
||||
uv run stable-audio --model small-sfx \
|
||||
-p "triumphant short brass fanfare, 3 rising notes, wuxia style, under 1.5 seconds" \
|
||||
--duration 2 -o "'"$OUT"'/victory.wav" 2>&1 | grep -i "saved\|error"
|
||||
fi'
|
||||
echo "done: $(ls "$OUT")"
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""R6 缺口语音生成(docs/design/11-rpg-battle-rebalance.md §6):tut_b0 / tut_b6 /
|
||||
btl_flee_fail / boss1_warn 四句,双性别(male=Uncle_Fu / female=Serena),
|
||||
instruct 沿用 tools/gen_voice_dual.py 的 rpg 旁白常量。
|
||||
|
||||
用法(GPU 串行):
|
||||
source env.d/tts.sh
|
||||
flock .gpu.lock .venv-tts/bin/python web/rpg/gen/gen_gap_voice.py # 生成 staging wav
|
||||
.venv-tts/bin/python web/rpg/gen/gen_gap_voice.py --convert # 转 ogg+m4a 落盘
|
||||
.venv-tts/bin/python web/rpg/gen/gen_gap_voice.py --check # 两性时长离群检查
|
||||
|
||||
staging: web/rpg/gen/staging/voice/<gender>/<id>.wav(断点续跑,删 wav 即重录该句)
|
||||
落盘: web/rpg/audio/voice/<gender>/<id>.{ogg,m4a}
|
||||
质量控制同主管线:时长 >0.3s、峰值 >0.05、峰值归一 0.89、>40 字拆句拼接(句间 0.12s)。
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("HF_HOME", "/home/ubuntu/SoundGame/models/hf")
|
||||
os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
|
||||
|
||||
ROOT = "/home/ubuntu/SoundGame"
|
||||
sys.path.insert(0, os.path.join(ROOT, "tools"))
|
||||
from gen_voice_dual import split_text, valid_wav, PEAK_NORM, GAP_SEC # noqa: E402 只读复用主管线
|
||||
|
||||
GAME = "rpg"
|
||||
STAGING = os.path.join(ROOT, "web", "rpg", "gen", "staging", "voice")
|
||||
OUT = os.path.join(ROOT, "web", GAME, "audio", "voice")
|
||||
FFMPEG = os.path.join(ROOT, "tools", "bin", "ffmpeg")
|
||||
|
||||
SPEAKERS = {"male": "Uncle_Fu", "female": "Serena"}
|
||||
INSTRUCT = {
|
||||
"male": "低沉醇厚的男声,像说书人,平静有韵味",
|
||||
"female": "平静清晰的年轻女声,像在讲武侠故事",
|
||||
}
|
||||
|
||||
# 台词全文照录 11 号文档 §6
|
||||
LINES = [
|
||||
("tut_b0", "战斗里,左右滑选招式,单击出手。听到这个声音,就是你的回合。先滑一滑,试试。"),
|
||||
("tut_b6", "来不及格挡,就选防御。左右滑,选防御,单击。"),
|
||||
("btl_flee_fail", "没甩掉!它追上来了。"),
|
||||
("boss1_warn", "里面就是黑风寨主。你现在回去练练再来,也来得及。"),
|
||||
]
|
||||
|
||||
|
||||
def staging_path(gender, fid):
|
||||
return os.path.join(STAGING, gender, fid + ".wav")
|
||||
|
||||
|
||||
def generate():
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from qwen_tts import Qwen3TTSModel
|
||||
|
||||
model = Qwen3TTSModel.from_pretrained(
|
||||
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||||
device_map="cuda:0",
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
print("model loaded", flush=True)
|
||||
|
||||
for gender in ("male", "female"):
|
||||
speaker = SPEAKERS[gender]
|
||||
instruct = INSTRUCT[gender]
|
||||
os.makedirs(os.path.join(STAGING, gender), exist_ok=True)
|
||||
|
||||
# 展开成 (fid, chunk_idx, text),跳过已完成
|
||||
items = []
|
||||
for fid, text in LINES:
|
||||
if valid_wav(staging_path(gender, fid)):
|
||||
continue
|
||||
for ci, ctext in enumerate(split_text(text)):
|
||||
items.append((fid, ci, ctext))
|
||||
if not items:
|
||||
print(f"[{gender}] nothing to do", flush=True)
|
||||
continue
|
||||
|
||||
results = {}
|
||||
for i in range(0, len(items), 6):
|
||||
batch = items[i:i + 6]
|
||||
wavs, sr = model.generate_custom_voice(
|
||||
text=[b[2] for b in batch],
|
||||
language=["Chinese"] * len(batch),
|
||||
speaker=[speaker] * len(batch),
|
||||
instruct=[instruct] * len(batch),
|
||||
max_new_tokens=2048,
|
||||
)
|
||||
for (fid, ci, _t), wav in zip(batch, wavs):
|
||||
results.setdefault(fid, {})[ci] = np.asarray(wav, dtype=np.float32)
|
||||
print(f"[{gender}] batch {i // 6 + 1} (n={len(batch)})", flush=True)
|
||||
|
||||
gap = np.zeros(int(GAP_SEC * sr), dtype=np.float32)
|
||||
for fid, text in LINES:
|
||||
path = staging_path(gender, fid)
|
||||
if valid_wav(path):
|
||||
continue
|
||||
got = results.get(fid, {})
|
||||
n_chunks = len(split_text(text))
|
||||
assert len(got) == n_chunks, f"{gender}/{fid}: 缺 chunk {len(got)}/{n_chunks}"
|
||||
wav = got[0]
|
||||
for ci in range(1, n_chunks):
|
||||
wav = np.concatenate([wav, gap, got[ci]])
|
||||
peak = float(np.max(np.abs(wav)))
|
||||
assert peak > 0.05, f"{gender}/{fid}: peak {peak:.3f}"
|
||||
wav = wav / peak * PEAK_NORM
|
||||
dur = len(wav) / sr
|
||||
assert dur > 0.3, f"{gender}/{fid}: dur {dur:.2f}s"
|
||||
sf.write(path, wav, sr)
|
||||
print(f"[ok] {gender}/{fid} {dur:.2f}s chunks={n_chunks}", flush=True)
|
||||
|
||||
|
||||
def check():
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
|
||||
problems = []
|
||||
for fid, _text in LINES:
|
||||
durs = {}
|
||||
for gender in ("male", "female"):
|
||||
path = staging_path(gender, fid)
|
||||
if not os.path.exists(path):
|
||||
durs[gender] = None
|
||||
continue
|
||||
data, sr = sf.read(path)
|
||||
peak = float(np.max(np.abs(data)))
|
||||
durs[gender] = len(data) / sr
|
||||
if durs[gender] <= 0.3 or peak <= 0.05:
|
||||
problems.append(f"{gender}/{fid}: 质量不达标 dur={durs[gender]:.2f} peak={peak:.3f}")
|
||||
m, f = durs["male"], durs["female"]
|
||||
if m is None or f is None:
|
||||
problems.append(f"{fid}: 缺 {'male' if m is None else 'female'}")
|
||||
continue
|
||||
ratio = max(m, f) / min(m, f)
|
||||
flag = " <-- OUTLIER" if ratio > 3 else ""
|
||||
if ratio > 3:
|
||||
problems.append(f"{fid}: 时长离群 m={m:.2f}s f={f:.2f}s ratio={ratio:.1f}")
|
||||
print(f" {fid:16s} m={m:6.2f}s f={f:6.2f}s{flag}")
|
||||
if problems:
|
||||
print("PROBLEMS:")
|
||||
for p in problems:
|
||||
print(" ", p)
|
||||
return 1
|
||||
print("ALL OK")
|
||||
return 0
|
||||
|
||||
|
||||
def convert():
|
||||
total = 0
|
||||
for gender in ("male", "female"):
|
||||
dst_dir = os.path.join(OUT, gender)
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
for fid, _text in LINES:
|
||||
src = staging_path(gender, fid)
|
||||
if not os.path.exists(src):
|
||||
print(f"[convert] WARN missing {gender}/{fid}.wav")
|
||||
continue
|
||||
base = os.path.join(dst_dir, fid)
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", src,
|
||||
"-ar", "44100", "-c:a", "libvorbis", "-q:a", "3", base + ".ogg"], check=True)
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-i", src,
|
||||
"-ar", "44100", "-c:a", "aac", "-b:a", "128k", base + ".m4a"], check=True)
|
||||
total += 1
|
||||
print(f"converted {total} files (ogg+m4a each)")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--check", action="store_true")
|
||||
ap.add_argument("--convert", action="store_true")
|
||||
args = ap.parse_args()
|
||||
if args.check:
|
||||
sys.exit(check())
|
||||
if args.convert:
|
||||
convert()
|
||||
return
|
||||
generate()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user