移除 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:
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
步骤 1:TTS 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()
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
步骤 2:TTS 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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
# ── 音乐 BGM(Stable 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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user