Files
blind/audio_1/scripts/05_gen_music_stableaudio.py
T
xslandClaude Sonnet 4.6 e5d3d44f9d 移除 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>
2026-05-20 23:12:01 +08:00

278 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()