移除 GPT-SoVITS 和 Fish Speech,整理 audio_1 脚本与文档

- 删除 03_gen_tts_gptsovits.py、00_transcribe_refs.py、ref_texts.json
- 删除 02_gen_tts_fishspeech.py 及 tts/v2_fishspeech/ 生成结果
- 保留 VoxCPM2 / ACE-Step / Stable Audio 三条链路
- 更新执行文档和技术报告,清除两款 TTS 的所有引用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-05-20 23:12:01 +08:00
co-authored by Claude Sonnet 4.6
parent 10ced5cb69
commit e5d3d44f9d
501 changed files with 960 additions and 37 deletions
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
步骤 1TTS v1 — VoxCPM2 批量生成
输出目录:audio_1/tts/v1_voxcpm2/
使用方法:
# 确认 VoxCPM2 服务已启动
python audio_1/scripts/01_gen_tts_voxcpm2.py
python audio_1/scripts/01_gen_tts_voxcpm2.py --dry-run
python audio_1/scripts/01_gen_tts_voxcpm2.py --role linxia
"""
import os, sys, glob, base64, json, tempfile, time, subprocess, argparse
import requests
PROJECT_DIR = "/home/xsl/blind"
VOICE_DIR = f"{PROJECT_DIR}/voice"
OUT_DIR = f"{PROJECT_DIR}/audio_1/tts/v1_voxcpm2"
HOST = "127.0.0.1"
PORT = "8000"
BASE_URL = f"http://{HOST}:{PORT}"
os.makedirs(OUT_DIR, exist_ok=True)
sys.path.insert(0, f"{PROJECT_DIR}/audio")
# 复用原始脚本的 ROLES 和 LINES 定义
from batch_tts_voxcpm import ROLES, LINES
# ──────────────────────────────────────────────────────────────
def find_ref_audio(role_key):
role_dir = os.path.join(VOICE_DIR, ROLES[role_key]["dir"])
for ext in ("*.wav", "*.mp3", "*.flac"):
files = sorted(glob.glob(os.path.join(role_dir, ext)))
if files:
return files[0]
return None
def wav_to_base64(path):
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
def convert_to_wav(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-ar", "16000", "-ac", "1",
"-acodec", "pcm_s16le", dst],
check=True, capture_output=True)
def apply_speed(src, speed, dst):
if abs(speed - 1.0) < 0.01:
import shutil; shutil.copy2(src, dst); return
subprocess.run(["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={speed}",
"-acodec", "pcm_s16le", dst],
check=True, capture_output=True)
def wav_to_mp3(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-codec:a", "libmp3lame",
"-b:a", "128k", "-ar", "24000", "-ac", "1", dst],
check=True, capture_output=True)
def check_server():
try:
return requests.get(f"{BASE_URL}/health", timeout=5).status_code == 200
except: return False
def register_voice(wav_path):
r = requests.post(f"{BASE_URL}/v1/voices",
json={"wav_base64": wav_to_base64(wav_path)}, timeout=60)
r.raise_for_status()
return r.json()["voice_id"]
def synthesize(voice_id, text):
r = requests.post(f"{BASE_URL}/v1/speech",
json={"text": text, "voice_id": voice_id,
"cfg_value": 2.0, "inference_timesteps": 10},
timeout=120)
r.raise_for_status()
return r.content
# ──────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--role", help="只合成指定角色")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--resume", action="store_true", help="跳过已存在的文件", default=True)
args = parser.parse_args()
lines = LINES
if args.role:
lines = [(f,r,t) for f,r,t in lines if r == args.role]
print(f"\n{'='*60}")
print(f" TTS v1 — VoxCPM2 ({len(lines)} 条)")
print(f" 输出: {OUT_DIR}")
print(f"{'='*60}\n")
if args.dry_run:
for f,r,t in lines:
out = os.path.join(OUT_DIR, f"{f}_wav_v1.mp3")
done = "" if os.path.exists(out) else "·"
print(f" [{done}] {f} [{r}] {t[:40]}")
return
if not check_server():
print(f"[错误] VoxCPM2 服务未响应: {BASE_URL}")
sys.exit(1)
# 预注册音色
voice_ids = {}
tmp = tempfile.mkdtemp(prefix="voxcpm_")
for role_key in set(r for _,r,_ in lines):
ref = find_ref_audio(role_key)
if not ref:
print(f" [跳过] {role_key}: 未找到参考音频"); continue
wav = ref if ref.endswith(".wav") else os.path.join(tmp, f"{role_key}.wav")
if not ref.endswith(".wav"):
convert_to_wav(ref, wav)
print(f" 注册音色: {role_key} ... ", end="", flush=True)
vid = register_voice(wav)
voice_ids[role_key] = vid
print(f"ok ({vid[:8]}...)")
# 批量合成
ok = fail = skip = 0
for i, (fname, role, text) in enumerate(lines, 1):
out_mp3 = os.path.join(OUT_DIR, f"{fname}_wav_v1.mp3")
if args.resume and os.path.exists(out_mp3):
print(f" [{i:3d}/{len(lines)}] SKIP {fname}")
skip += 1; continue
if role not in voice_ids:
print(f" [{i:3d}/{len(lines)}] MISS {fname} (无音色)")
fail += 1; continue
speed = ROLES[role]["speed"]
print(f" [{i:3d}/{len(lines)}] {fname} text={text[:30]}... ", end="", flush=True)
t0 = time.time()
try:
wav_bytes = synthesize(voice_ids[role], text)
raw_wav = os.path.join(tmp, f"{fname}_raw.wav")
spd_wav = os.path.join(tmp, f"{fname}_spd.wav")
with open(raw_wav, "wb") as f: f.write(wav_bytes)
apply_speed(raw_wav, speed, spd_wav)
wav_to_mp3(spd_wav, out_mp3)
print(f"ok ({time.time()-t0:.1f}s {os.path.getsize(out_mp3)//1024}KB)")
ok += 1
except Exception as e:
print(f"FAIL: {e}"); fail += 1
print(f"\n完成: {ok} ok, {skip} skip, {fail} fail → {OUT_DIR}")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
步骤 2TTS v2 — CosyVoice2-0.5B 批量生成
输出目录:audio_1/tts/v2_cosyvoice2/
使用方法:
conda activate cosyvoice
python audio_1/scripts/02_gen_tts_cosyvoice2.py
python audio_1/scripts/02_gen_tts_cosyvoice2.py --dry-run
"""
import os, sys, glob, json, time, tempfile, subprocess, argparse
import numpy as np
PROJECT_DIR = "/home/xsl/blind"
VOICE_DIR = f"{PROJECT_DIR}/voice"
OUT_DIR = f"{PROJECT_DIR}/audio_1/tts/v2_cosyvoice2"
REF_TEXTS = f"{PROJECT_DIR}/audio_1/ref_texts.json"
MODEL_DIR = "/home/xsl/tools/CosyVoice/pretrained_models/CosyVoice2-0.5B"
COSYVOICE_DIR = "/home/xsl/tools/CosyVoice"
os.makedirs(OUT_DIR, exist_ok=True)
sys.path.insert(0, COSYVOICE_DIR)
sys.path.insert(0, f"{PROJECT_DIR}/audio")
from batch_tts_voxcpm import ROLES, LINES
with open(REF_TEXTS, encoding="utf-8") as f:
REF_TEXT_MAP = json.load(f)
# ──────────────────────────────────────────────────────────────
def find_ref_audio(role_key):
role_dir = os.path.join(VOICE_DIR, ROLES[role_key]["dir"])
for ext in ("*.wav", "*.mp3", "*.flac"):
files = sorted(glob.glob(os.path.join(role_dir, ext)))
if files:
return files[0]
return None
def load_audio_16k(path):
"""加载音频,重采样到 16kHz,返回 numpy float32 array"""
import librosa
audio, _ = librosa.load(path, sr=16000, mono=True)
return audio
def wav_to_mp3(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-codec:a", "libmp3lame",
"-b:a", "128k", "-ar", "24000", "-ac", "1", dst],
check=True, capture_output=True)
def save_wav(audio_tensor, sr, path):
"""保存 torch tensor 或 numpy array 到 WAV"""
import soundfile as sf
if hasattr(audio_tensor, 'numpy'):
audio = audio_tensor.squeeze().numpy()
else:
audio = np.squeeze(audio_tensor)
sf.write(path, audio, sr)
# ──────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--role", help="只合成指定角色")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--resume", action="store_true", default=True)
args = parser.parse_args()
lines = LINES
if args.role:
lines = [(f,r,t) for f,r,t in lines if r == args.role]
print(f"\n{'='*60}")
print(f" TTS v2 — CosyVoice2-0.5B ({len(lines)} 条)")
print(f" 输出: {OUT_DIR}")
print(f"{'='*60}\n")
# 预检参考音频
ref_cache = {}
for role_key in ROLES:
ref_path = find_ref_audio(role_key)
if ref_path:
ref_cache[role_key] = {
"path": ref_path,
"audio16k": None, # 懒加载
"text": REF_TEXT_MAP.get(role_key, ""),
}
if args.dry_run:
for f,r,t in lines:
out = os.path.join(OUT_DIR, f"{f}_wav_v1.mp3")
done = "" if os.path.exists(out) else "·"
ref_ok = "" if r in ref_cache else ""
print(f" [{done}] ref={ref_ok} {f} [{r}] {t[:40]}")
return
# 加载模型
print("加载 CosyVoice2 模型...", end=" ", flush=True)
t0 = time.time()
from cosyvoice.cli.cosyvoice import CosyVoice2
cosyvoice = CosyVoice2(MODEL_DIR)
print(f"就绪 ({time.time()-t0:.1f}s) SR={cosyvoice.sample_rate}")
tmp = tempfile.mkdtemp(prefix="cosyvoice2_")
ok = fail = skip = 0
for i, (fname, role, text) in enumerate(lines, 1):
out_mp3 = os.path.join(OUT_DIR, f"{fname}_wav_v1.mp3")
if args.resume and os.path.exists(out_mp3):
print(f" [{i:3d}/{len(lines)}] SKIP {fname}")
skip += 1; continue
if role not in ref_cache:
print(f" [{i:3d}/{len(lines)}] MISS {fname} (无参考音频)")
fail += 1; continue
speed = ROLES[role]["speed"]
ref = ref_cache[role]
print(f" [{i:3d}/{len(lines)}] {fname} [{role}] {text[:30]}... ", end="", flush=True)
t0 = time.time()
try:
# 懒加载参考音频
if ref["audio16k"] is None:
ref["audio16k"] = load_audio_16k(ref["path"])
# 合成:zero-shot 语音克隆
import torch
prompt_speech = torch.from_numpy(ref["audio16k"]).unsqueeze(0)
audio_chunks = []
for result in cosyvoice.inference_zero_shot(
tts_text=text,
prompt_text=ref["text"],
prompt_speech_16k=prompt_speech,
speed=speed,
stream=False,
):
audio_chunks.append(result['tts_speech'].squeeze().numpy())
# 合并并保存
full_audio = np.concatenate(audio_chunks) if audio_chunks else np.array([])
out_wav = os.path.join(tmp, f"{fname}.wav")
import soundfile as sf
sf.write(out_wav, full_audio, cosyvoice.sample_rate)
wav_to_mp3(out_wav, out_mp3)
print(f"ok ({time.time()-t0:.1f}s {os.path.getsize(out_mp3)//1024}KB)")
ok += 1
except Exception as e:
print(f"FAIL: {e}"); fail += 1
print(f"\n完成: {ok} ok, {skip} skip, {fail} fail → {OUT_DIR}")
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
步骤 4:音乐 v1 — ACE-Step 1.5 批量生成
输出目录:audio_1/music/v1_acestep/{music,sfx,ambience}/
使用方法:
conda activate acestep
python audio_1/scripts/04_gen_music_acestep.py [--dry-run]
"""
import os, sys, time, argparse, subprocess
import torch
torch.backends.cuda.preferred_blas_library("cublaslt")
PROJECT_DIR = "/home/xsl/blind"
REPO_DIR = "/home/xsl/tools/ACE-Step-1.5"
OUT_MUSIC = f"{PROJECT_DIR}/audio_1/music/v1_acestep/music"
OUT_SFX = f"{PROJECT_DIR}/audio_1/music/v1_acestep/sfx"
OUT_AMB = f"{PROJECT_DIR}/audio_1/music/v1_acestep/ambience"
for d in (OUT_MUSIC, OUT_SFX, OUT_AMB):
os.makedirs(d, exist_ok=True)
sys.path.insert(0, REPO_DIR)
sys.path.insert(0, f"{PROJECT_DIR}/audio")
# 复用原脚本中的 TRACKS 和 SFX 定义
from gen_music_acestep import BGM_TRACKS, RINGTONES
from gen_sfx_acestep import SFX_TRACKS, AMBIENCE_TRACKS
def wav_to_mp3(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-codec:a", "libmp3lame",
"-b:a", "128k", "-ar", "44100", "-ac", "2", dst],
check=True, capture_output=True)
# ──────────────────────────────────────────────────────────────
def load_handler():
from acestep.handler import AceStepHandler
print("加载 ACE-Step 模型...", end=" ", flush=True)
t0 = time.time()
handler = AceStepHandler()
handler.initialize_service(project_root=REPO_DIR, config_path="acestep-v15-turbo", device="cuda")
print(f"就绪 ({time.time()-t0:.1f}s)")
return handler
def generate_one(handler, track, out_dir):
from acestep.inference import GenerationParams, GenerationConfig, generate_music
filename = track["filename"]
out_wav = os.path.join(out_dir, f"{filename}_v1.wav")
out_mp3 = os.path.join(out_dir, f"{filename}_v1.mp3")
if os.path.exists(out_mp3):
print(f" [SKIP] {filename}")
return "skip"
note = track.get("note", "")
print(f" [{filename}] {track['duration']}s {note} ...", end=" ", flush=True)
params = GenerationParams(
caption=track["caption"],
lyrics=track.get("lyrics", ""),
duration=track["duration"],
instrumental=True,
inference_steps=track.get("steps", 20),
seed=track.get("seed", 42),
)
config = GenerationConfig(batch_size=1, audio_format="wav")
t0 = time.time()
try:
result = generate_music(handler, None, params, config, save_dir=out_dir)
elapsed = time.time() - t0
if result.success:
gen_path = result.audios[0]["path"]
if gen_path != out_wav:
os.rename(gen_path, out_wav)
wav_to_mp3(out_wav, out_mp3)
size = os.path.getsize(out_mp3) // 1024
print(f"ok {elapsed:.1f}s {size}KB")
return "ok"
else:
print(f"FAIL: {result.error}"); return "fail"
except Exception as e:
print(f"FAIL: {e}"); return "fail"
# ──────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--category", choices=["music","sfx","ambience","all"], default="all")
args = parser.parse_args()
all_tasks = []
if args.category in ("music", "all"):
all_tasks += [(t, OUT_MUSIC) for t in BGM_TRACKS + RINGTONES]
if args.category in ("sfx", "all"):
all_tasks += [(t, OUT_SFX) for t in SFX_TRACKS]
if args.category in ("ambience", "all"):
all_tasks += [(t, OUT_AMB) for t in AMBIENCE_TRACKS]
print(f"\n{'='*60}")
print(f" 音乐 v1 — ACE-Step ({len(all_tasks)} 条)")
print(f"{'='*60}\n")
if args.dry_run:
for t, d in all_tasks:
mp3 = os.path.join(d, f"{t['filename']}_v1.mp3")
done = "" if os.path.exists(mp3) else "·"
print(f" [{done}] {t['filename']} {t['duration']}s → {os.path.basename(d)}/")
return
handler = load_handler()
ok = fail = skip = 0
for track, out_dir in all_tasks:
r = generate_one(handler, track, out_dir)
if r == "ok": ok += 1
elif r == "skip": skip += 1
else: fail += 1
print(f"\n完成: {ok} ok, {skip} skip, {fail} fail")
if __name__ == "__main__":
main()
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""
步骤 5:音乐 v2 — Stable Audio Open 1.0 批量生成
输出目录:audio_1/music/v2_stableaudio/{music,sfx,ambience}/
特点:44.1kHz 立体声,最长 47 秒,音效/氛围质量高,BGM 风格偏弥散
使用方法:
conda activate stableaudio
python audio_1/scripts/05_gen_music_stableaudio.py [--dry-run]
"""
import os, sys, time, subprocess, argparse
import torch, json
PROJECT_DIR = "/home/xsl/blind"
TOOLS_DIR = "/home/xsl/tools/stable-audio-tools"
MODEL_CONFIG = "/home/xsl/models/stable-audio-open/model_config.json"
MODEL_CKPT = "/home/xsl/models/stable-audio-open/model.safetensors"
OUT_MUSIC = f"{PROJECT_DIR}/audio_1/music/v2_stableaudio/music"
OUT_SFX = f"{PROJECT_DIR}/audio_1/music/v2_stableaudio/sfx"
OUT_AMB = f"{PROJECT_DIR}/audio_1/music/v2_stableaudio/ambience"
for d in (OUT_MUSIC, OUT_SFX, OUT_AMB):
os.makedirs(d, exist_ok=True)
sys.path.insert(0, TOOLS_DIR)
# ── 音乐 BGMStable Audio 最长 47s,生成片段)─────────────────
BGM_TRACKS = [
{
"filename": "lv11_bgm_m1_main_loop",
"duration": 47,
"prompt": (
"lo-fi midnight bedroom, soft piano, light vinyl crackle, "
"muted piano, subtle synth pad, occasional distant rain, "
"melancholy but warm, alone but not lonely, ambient, no drums, 70 bpm"
),
"steps": 200,
"seed": 2103,
"note": "M1 主题",
},
{
"filename": "lv11_bgm_m2_suspense",
"duration": 30,
"prompt": (
"subtle ambient suspense, cinematic tension, "
"low drone, single hanging piano note, sparse strings, "
"unsettling but not horror, builds slowly, unresolved chord, 60 bpm"
),
"steps": 200,
"seed": 2230,
"note": "M2 悬疑",
},
{
"filename": "lv11_bgm_m3_zhounan",
"duration": 47,
"prompt": (
"late night lo-fi, warm electric guitar fingerpicking, "
"light reverb, distant city sounds, nostalgic, gentle, 65 bpm"
),
"steps": 200,
"seed": 2345,
"note": "M3 周南",
},
{
"filename": "lv11_bgm_m4_ending",
"duration": 47,
"prompt": (
"gentle piano dawn, morning light, hopeful, sparse chords, "
"soft string pad, peaceful resolution, 60 bpm"
),
"steps": 200,
"seed": 2400,
"note": "M4 结局",
},
]
# ── 铃声(短促,~10-15s)────────────────────────────────────────
RINGTONES = [
{"filename": "lv11_ring_mom", "duration": 12, "seed": 3001, "steps": 150,
"prompt": "warm marimba ringtone, simple 4-note motif, old Nokia style, 70 bpm, loop, short"},
{"filename": "lv11_ring_azhe", "duration": 12, "seed": 3002, "steps": 150,
"prompt": "cold minimal electronic ringtone, 2 notes only, distant, 80 bpm, loop, short"},
{"filename": "lv11_ring_xiaomei", "duration": 12, "seed": 3003, "steps": 150,
"prompt": "cheerful pop ringtone, ukulele plucks, 4-bar phrase, energetic, 100 bpm, loop"},
{"filename": "lv11_ring_zhounan", "duration": 12, "seed": 3004, "steps": 150,
"prompt": "acoustic guitar single-string ringtone, contemplative, 75 bpm, loop, short"},
{"filename": "lv11_ring_hr", "duration": 12, "seed": 3005, "steps": 150,
"prompt": "corporate professional ringtone, neutral marimba, clean, 90 bpm, loop"},
{"filename": "lv11_ring_anan", "duration": 12, "seed": 3006, "steps": 150,
"prompt": "fun upbeat ringtone, synth pop, bright, 110 bpm, catchy, loop"},
{"filename": "lv11_ring_mom", "duration": 12, "seed": 3007, "steps": 150,
"prompt": "delivery notification ringtone, simple beep melody, neutral, 85 bpm"},
{"filename": "lv11_ring_delivery", "duration": 12, "seed": 3008, "steps": 150,
"prompt": "delivery notification ringtone, simple beep melody, neutral, 85 bpm"},
{"filename": "lv11_ring_unknown", "duration": 12, "seed": 3009, "steps": 150,
"prompt": "eerie unknown caller ringtone, distorted sine wave, unsettling, 60 bpm"},
{"filename": "lv11_ring_dorm_group","duration": 12, "seed": 3010, "steps": 150,
"prompt": "group chat notification sound, light percussion, cheerful, 95 bpm, loop"},
{"filename": "lv11_ring_zhounan", "duration": 12, "seed": 3011, "steps": 150,
"prompt": "acoustic guitar single-string ringtone, contemplative, 75 bpm, loop"},
]
# ── 音效 SFX ───────────────────────────────────────────────────
SFX_TRACKS = [
{"filename": "sfx_transition_chapter", "duration": 8, "seed": 4001, "steps": 200,
"prompt": "cinematic transition swell, soft orchestral rise and fall, gentle fade, film score"},
{"filename": "sfx_heartbeat_fast", "duration": 8, "seed": 4002, "steps": 200,
"prompt": "human heartbeat accelerating, close microphone, thumping, tension, anxiety, realistic"},
{"filename": "sfx_deep_breath", "duration": 6, "seed": 4003, "steps": 200,
"prompt": "single deep breath, calm, close microphone, soft exhale, meditative, realistic foley"},
{"filename": "sfx_rain_heavier", "duration": 8, "seed": 4004, "steps": 200,
"prompt": "rain suddenly intensifying on window glass, urban rain sound, dramatic, realistic"},
{"filename": "sfx_rain_lighter", "duration": 8, "seed": 4005, "steps": 200,
"prompt": "light rain on window, soft pattering, urban night, gentle, calming, realistic"},
{"filename": "sfx_glass_shatter_short","duration": 5, "seed": 4006, "steps": 200,
"prompt": "glass breaking, short, sharp, immediate, foley sound effect"},
{"filename": "sfx_door_knock_gentle", "duration": 5, "seed": 4007, "steps": 200,
"prompt": "gentle knocking on door, two light knocks, quiet, hesitant, foley"},
{"filename": "sfx_door_knock_firm", "duration": 5, "seed": 4008, "steps": 200,
"prompt": "firm confident door knock, three knocks, hollow door resonance, foley"},
{"filename": "sfx_phone_vibrate", "duration": 5, "seed": 4009, "steps": 200,
"prompt": "smartphone vibrating on hard surface, buzzing notification, realistic"},
{"filename": "sfx_keyboard_fast", "duration": 8, "seed": 4010, "steps": 200,
"prompt": "fast typing on mechanical keyboard, rapid keystrokes, focused, realistic"},
{"filename": "sfx_keyboard_slow", "duration": 8, "seed": 4011, "steps": 200,
"prompt": "slow hesitant typing, single key presses, pauses, uncertain typing, realistic"},
{"filename": "sfx_kettle_whistle", "duration": 8, "seed": 4012, "steps": 200,
"prompt": "electric kettle boiling and whistling, kitchen, steam, morning, realistic foley"},
{"filename": "sfx_water_boiling", "duration": 8, "seed": 4013, "steps": 200,
"prompt": "water boiling in pot, bubbling, kitchen ambience, realistic foley sound"},
{"filename": "sfx_paper_turning", "duration": 5, "seed": 4014, "steps": 200,
"prompt": "paper page turning, subtle rustling, quiet, realistic close-mic foley"},
{"filename": "sfx_night_bus", "duration": 10, "seed": 4015, "steps": 200,
"prompt": "night bus interior sound, engine rumble, gentle sway, city at night, ambient"},
{"filename": "sfx_cat_yowl", "duration": 5, "seed": 4016, "steps": 200,
"prompt": "cat yowling outside at night, distant, urban night sound, realistic"},
{"filename": "sfx_ending_chime", "duration": 6, "seed": 4017, "steps": 200,
"prompt": "soft ending chime, single bell tone, resonant, gentle, conclusive, musical"},
{"filename": "sfx_breathing_unknown", "duration": 8, "seed": 4018, "steps": 200,
"prompt": "quiet breathing, slightly nervous, close microphone, intimate, human, realistic"},
{"filename": "sfx_deep_breath", "duration": 6, "seed": 4019, "steps": 200,
"prompt": "slow deep calming breath, inhale and exhale, meditation, close mic, peaceful"},
]
# ── 环境氛围 ──────────────────────────────────────────────────
AMBIENCE_TRACKS = [
{"filename": "amb_apartment_night_01", "duration": 47, "seed": 5001, "steps": 200,
"prompt": "apartment at night, city sounds, distant traffic, quiet hum, urban isolation, ambient"},
{"filename": "amb_apartment_night_02", "duration": 47, "seed": 5002, "steps": 200,
"prompt": "late night apartment, occasional car passing, silence, urban night, subtle AC hum"},
{"filename": "amb_bar_loud_01", "duration": 30, "seed": 5003, "steps": 200,
"prompt": "busy bar ambience, crowd chatter, glasses clinking, background music, lively, indoor"},
{"filename": "amb_rain_window_01", "duration": 47, "seed": 5004, "steps": 200,
"prompt": "rain on window glass, cozy indoor, distant thunder rumble, peaceful, continuous"},
{"filename": "amb_kitchen_morning", "duration": 30, "seed": 5005, "steps": 200,
"prompt": "morning kitchen sounds, birds outside, soft light, gentle hum, peaceful domestic"},
{"filename": "amb_dawn_birds", "duration": 47, "seed": 5006, "steps": 200,
"prompt": "dawn birdsong, early morning outdoor, multiple bird species, peaceful, hopeful"},
]
# ──────────────────────────────────────────────────────────────
def wav_to_mp3(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-codec:a", "libmp3lame",
"-b:a", "128k", "-ar", "44100", "-ac", "2", dst],
check=True, capture_output=True)
def generate_one(track, out_dir, sa_model, sr):
"""使用 generate_diffusion_cond 直接生成音频"""
import numpy as np
from einops import rearrange
from stable_audio_tools.inference.generation import generate_diffusion_cond
import torchaudio
filename = track["filename"]
out_wav = os.path.join(out_dir, f"{filename}_v1.wav")
out_mp3 = os.path.join(out_dir, f"{filename}_v1.mp3")
if os.path.exists(out_mp3):
print(f" [SKIP] {filename}")
return "skip"
note = track.get("note", "")
duration = min(track["duration"], 47)
print(f" [{filename}] {duration}s {note}... ", end="", flush=True)
t0 = time.time()
try:
device = next(sa_model.parameters()).device
conditioning = [{"prompt": track["prompt"], "seconds_start": 0, "seconds_total": duration}]
audio = generate_diffusion_cond(
model=sa_model,
conditioning=conditioning,
steps=track.get("steps", 200),
cfg_scale=7.0,
batch_size=1,
sample_size=sr * duration,
seed=track.get("seed", 42),
device=device,
sampler_type="dpmpp-3m-sde",
sigma_min=0.03,
sigma_max=1000,
)
# audio shape: [batch, channels, samples]
audio = audio[:, :, :duration * sr]
audio = rearrange(audio, "b d n -> d (b n)")
audio = audio.to(torch.float32)
peak = torch.max(torch.abs(audio))
if peak > 0:
audio = audio / peak
audio = audio.clamp(-1, 1).mul(32767).to(torch.int16).cpu()
torchaudio.save(out_wav, audio, sr)
wav_to_mp3(out_wav, out_mp3)
size = os.path.getsize(out_mp3) // 1024
print(f"ok {time.time()-t0:.1f}s {size}KB")
return "ok"
except Exception as e:
import traceback; traceback.print_exc()
print(f"FAIL: {e}"); return "fail"
# ──────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--category", choices=["music","sfx","ambience","all"], default="all")
args = parser.parse_args()
all_tasks = []
if args.category in ("music", "all"):
all_tasks += [(t, OUT_MUSIC) for t in BGM_TRACKS + RINGTONES]
if args.category in ("sfx", "all"):
all_tasks += [(t, OUT_SFX) for t in SFX_TRACKS]
if args.category in ("ambience", "all"):
all_tasks += [(t, OUT_AMB) for t in AMBIENCE_TRACKS]
# 去重(RINGTONES 里有重复 filename
seen = set()
deduped = []
for t, d in all_tasks:
key = (t["filename"], d)
if key not in seen:
seen.add(key)
deduped.append((t, d))
all_tasks = deduped
print(f"\n{'='*60}")
print(f" 音乐 v2 — Stable Audio Open 1.0 ({len(all_tasks)} 条)")
print(f"{'='*60}\n")
if args.dry_run:
for t, d in all_tasks:
mp3 = os.path.join(d, f"{t['filename']}_v1.mp3")
done = "" if os.path.exists(mp3) else "·"
print(f" [{done}] {t['filename']} {t['duration']}s → {os.path.basename(d)}/")
return
# 加载模型
print(f"加载 Stable Audio 模型...", end=" ", flush=True)
t0 = time.time()
from stable_audio_tools.interface.gradio import load_model as sa_load_model
import json
with open(MODEL_CONFIG) as f:
model_config = json.load(f)
sa_model, _ = sa_load_model(model_config=model_config, model_ckpt_path=MODEL_CKPT, device="cuda", model_half=False)
print(f"就绪 ({time.time()-t0:.1f}s)")
ok = fail = skip = 0
for track, out_dir in all_tasks:
r = generate_one(track, out_dir, sa_model, model_config["sample_rate"])
if r == "ok": ok += 1
elif r == "skip": skip += 1
else: fail += 1
print(f"\n完成: {ok} ok, {skip} skip, {fail} fail")
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
步骤 6:格式整理 — 将 audio_1 目录下所有残留 WAV 转换为 MP3,并清理中间文件
输出:各子目录内的 *_v1.mp3
使用方法:
python audio_1/scripts/06_convert_all_to_mp3.py [--dry-run] [--clean-wav]
"""
import os, subprocess, argparse
AUDIO_1_DIR = "/home/xsl/blind/audio_1"
def wav_to_mp3(src, dst):
subprocess.run(["ffmpeg", "-y", "-i", src, "-codec:a", "libmp3lame",
"-b:a", "128k", "-ar", "44100", "-ac", "2", dst],
check=True, capture_output=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="只列出,不执行")
parser.add_argument("--clean-wav", action="store_true", help="转换后删除 WAV 源文件")
args = parser.parse_args()
wavs_found = []
for root, dirs, files in os.walk(AUDIO_1_DIR):
# 跳过 scripts 目录
if "scripts" in root:
continue
for fname in files:
if fname.endswith(".wav"):
wav_path = os.path.join(root, fname)
mp3_path = wav_path.replace(".wav", ".mp3")
wavs_found.append((wav_path, mp3_path))
if not wavs_found:
print("无需转换的 WAV 文件。")
return
print(f"\n找到 {len(wavs_found)} 个 WAV 文件\n")
ok = skip = fail = 0
for wav, mp3 in wavs_found:
rel = os.path.relpath(wav, AUDIO_1_DIR)
if os.path.exists(mp3):
print(f" [SKIP] {rel}")
skip += 1
continue
if args.dry_run:
print(f" [DRY] {rel}{os.path.basename(mp3)}")
continue
try:
wav_to_mp3(wav, mp3)
size = os.path.getsize(mp3) // 1024
print(f" [OK] {rel}{size}KB")
if args.clean_wav:
os.remove(wav)
ok += 1
except Exception as e:
print(f" [FAIL] {rel}: {e}")
fail += 1
print(f"\n完成: {ok} ok, {skip} skip, {fail} fail")
# 统计各目录 MP3 数量
print("\n─── 音频资源统计 ───")
subdirs = [
("tts/v1_voxcpm2", "TTS v1 VoxCPM2"),
("music/v1_acestep/music", "Music v1 ACE-Step BGM"),
("music/v1_acestep/sfx", "Music v1 ACE-Step SFX"),
("music/v1_acestep/ambience", "Music v1 ACE-Step Amb"),
("music/v2_stableaudio/music", "Music v2 StableAudio BGM"),
("music/v2_stableaudio/sfx", "Music v2 StableAudio SFX"),
("music/v2_stableaudio/ambience", "Music v2 StableAudio Amb"),
]
for rel_dir, label in subdirs:
full = os.path.join(AUDIO_1_DIR, rel_dir)
if not os.path.isdir(full):
count = 0
else:
count = len([f for f in os.listdir(full) if f.endswith(".mp3")])
print(f" {label:<30} {count:>4}")
if __name__ == "__main__":
main()