- 删除 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>
278 lines
14 KiB
Python
278 lines
14 KiB
Python
#!/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()
|