현재 작업 상태 저장
This commit is contained in:
parent
07cc67761e
commit
6bd91b0d5e
674 changed files with 8726 additions and 298 deletions
236
docs/voice-art/scripts/generate_p1_seoyeon_higgs_samples.py
Normal file
236
docs/voice-art/scripts/generate_p1_seoyeon_higgs_samples.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""P1 서연 Higgs v3 무참조 synthetic voice 샘플 생성기.
|
||||
|
||||
실존 인물 reference voice를 쓰지 않는다. 먼저 Higgs 무참조 생성으로
|
||||
synthetic seed voice를 만들고, 그 seed audio만 재참조해 같은 페르소나
|
||||
화색의 정서/속도 변주를 만든다.
|
||||
|
||||
권장 실행:
|
||||
cd D:/workspace/vignette
|
||||
C:/Users/encep/Tools/ComfyUI_windows_portable/python_embeded/python.exe \
|
||||
docs/voice-art/scripts/generate_p1_seoyeon_higgs_samples.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
|
||||
BASE = Path(r"C:\Users\encep\Tools\ComfyUI_windows_portable")
|
||||
NODE = BASE / "ComfyUI" / "custom_nodes"
|
||||
FFMPEG = Path(
|
||||
r"C:\Users\encep\AppData\Local\Microsoft\WinGet\Packages"
|
||||
r"\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe"
|
||||
r"\ffmpeg-8.1-full_build\bin\ffmpeg.exe"
|
||||
)
|
||||
OUT_DIR = Path(__file__).resolve().parents[1] / "p1-seoyeon-higgs-v3-20260627"
|
||||
|
||||
SYNTHETIC_SEED_TEXT = (
|
||||
"안녕하세요. 저는 서연이에요. 오늘은 천천히, 너무 밝지 않게, "
|
||||
"하지만 또렷하게 말해볼게요."
|
||||
)
|
||||
|
||||
SAMPLES = [
|
||||
{
|
||||
"id": "p1_seoyeon_01_depressed_slow",
|
||||
"label": "우울·무기력·느린 말",
|
||||
"intent": "초기 비자발 내담자. 낮은 에너지, 늘어짐, 긴 침묵.",
|
||||
"seed": 3101,
|
||||
"text": (
|
||||
"음... <|emotion:helplessness|><|prosody:speed_very_slow|>"
|
||||
"그냥 학교 가도 아무 의미 없는 것 같아요."
|
||||
"<|prosody:long_pause|> 엄마는 제가 이상한 줄 알아요."
|
||||
"<|prosody:pause|> 그냥 좀, 다 귀찮아요."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "p1_seoyeon_02_tired_flat",
|
||||
"label": "피로·수면부족·평평한 톤",
|
||||
"intent": "잠을 못 잔 상태. 감정 표현이 납작하고 문장이 짧음.",
|
||||
"seed": 3102,
|
||||
"text": (
|
||||
"어제도 <|emotion:sadness|><|prosody:expressive_low|>"
|
||||
"잠을 거의 못 잤어요. 새벽까지 그냥 누워 있었고요."
|
||||
"<|prosody:pause|> 아침 되니까 몸이 너무 무거워서,"
|
||||
" 아무것도 하기 싫었어요."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "p1_seoyeon_03_anxious_guarded",
|
||||
"label": "불안·경계·말 빨라짐",
|
||||
"intent": "비밀보장 걱정으로 경계가 올라간 상태. 속도와 음높이가 상승.",
|
||||
"seed": 3103,
|
||||
"text": (
|
||||
"근데 선생님, <|emotion:fear|><|prosody:speed_fast|>"
|
||||
"<|prosody:pitch_high|>이거 엄마한테 다 말하는 거 아니죠?"
|
||||
" 그런 거면 저 그냥 갈래요. 진짜로요."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "p1_seoyeon_04_rapport_relief",
|
||||
"label": "라포 형성·조심스러운 완화",
|
||||
"intent": "상담자가 버텨준 뒤 처음으로 조금 풀리는 목소리.",
|
||||
"seed": 3104,
|
||||
"text": (
|
||||
"아까는 말하기 싫었는데..."
|
||||
"<|emotion:relief|><|prosody:speed_slow|>"
|
||||
"그래도 선생님은 그냥 듣고 계신 것 같아서요."
|
||||
"<|prosody:pause|> 그건 조금, 괜찮았어요."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "p1_seoyeon_05_recovered_lively",
|
||||
"label": "회복감·조금 활기찬 톤",
|
||||
"intent": "라포와 작은 성공 경험 후. 과장 없는 밝아짐.",
|
||||
"seed": 3105,
|
||||
"text": (
|
||||
"오늘은 그래도 <|emotion:contentment|><|prosody:speed_fast|>"
|
||||
"학교 끝나고 친구랑 잠깐 웃었어요."
|
||||
"<|emotion:relief|>진짜 별거 아닌데,"
|
||||
" 조금 괜찮았어요."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def import_higgs_modules():
|
||||
if not NODE.exists():
|
||||
raise RuntimeError(f"Higgs custom node 경로가 없다: {NODE}")
|
||||
sys.path.insert(0, str(NODE))
|
||||
loader = importlib.import_module("Higgs_v3-TTS-ComfyUI.loader")
|
||||
native = importlib.import_module("Higgs_v3-TTS-ComfyUI.native")
|
||||
return loader, native
|
||||
|
||||
|
||||
def load_bundle(loader):
|
||||
choices = loader.get_model_choices()
|
||||
if not choices:
|
||||
raise RuntimeError("Higgs 모델 선택지가 없다.")
|
||||
choice = choices[0]
|
||||
print(f"[load] {choice}", flush=True)
|
||||
return loader.load_higgs_bundle(
|
||||
model_choice=choice,
|
||||
dtype_name="auto",
|
||||
device_name="auto",
|
||||
attention="auto",
|
||||
download_if_missing=False,
|
||||
)
|
||||
|
||||
|
||||
def synth(native, bundle, *, text: str, seed: int, reference_audio=None, reference_text: str = "") -> dict:
|
||||
return native.generate_higgs_audio(
|
||||
bundle,
|
||||
text=text,
|
||||
reference_audio=reference_audio,
|
||||
reference_audio_path="",
|
||||
reference_text=reference_text,
|
||||
max_new_tokens=2048,
|
||||
temperature=0.8,
|
||||
top_p=0.95,
|
||||
top_k=50,
|
||||
seed=seed,
|
||||
trim_reference_audio=True,
|
||||
silence_threshold_db=-42.0,
|
||||
max_reference_seconds=12.0,
|
||||
progress_callback=None,
|
||||
)
|
||||
|
||||
|
||||
def write_wav(audio: dict, path: Path) -> None:
|
||||
waveform = audio["waveform"]
|
||||
if not isinstance(waveform, torch.Tensor):
|
||||
waveform = torch.as_tensor(waveform)
|
||||
data = waveform.detach().float().cpu()
|
||||
if data.ndim == 3:
|
||||
data = data[0]
|
||||
if data.ndim == 2:
|
||||
data = data.numpy().T
|
||||
else:
|
||||
data = data.numpy()
|
||||
sf.write(str(path), data, int(audio["sample_rate"]))
|
||||
|
||||
|
||||
def wav_duration(path: Path) -> float:
|
||||
with wave.open(str(path), "rb") as wf:
|
||||
return wf.getnframes() / float(wf.getframerate())
|
||||
|
||||
|
||||
def convert_mp3(wav_path: Path, mp3_path: Path) -> None:
|
||||
if not FFMPEG.exists():
|
||||
raise RuntimeError(f"ffmpeg를 찾을 수 없다: {FFMPEG}")
|
||||
subprocess.run(
|
||||
[str(FFMPEG), "-y", "-i", str(wav_path), "-codec:a", "libmp3lame", "-q:a", "2", str(mp3_path)],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
loader, native = import_higgs_modules()
|
||||
bundle = load_bundle(loader)
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("[synth] synthetic_seed_no_external_reference", flush=True)
|
||||
seed_audio = synth(native, bundle, text=SYNTHETIC_SEED_TEXT, seed=2701)
|
||||
seed_wav = OUT_DIR / "_synthetic_seed_no_external_reference.wav"
|
||||
seed_mp3 = OUT_DIR / "_synthetic_seed_no_external_reference.mp3"
|
||||
write_wav(seed_audio, seed_wav)
|
||||
convert_mp3(seed_wav, seed_mp3)
|
||||
|
||||
manifest = {
|
||||
"persona": "P1 서연(가명) · 고2 · 우울/자살사고",
|
||||
"model": "Higgs Audio v3 TTS local runtime",
|
||||
"reference_policy": "실존 인물 음성 금지. Higgs 무참조 생성 seed만 재참조한 synthetic voice.",
|
||||
"license_gate": "Higgs Audio v3 상업 사용 가능 여부 확정 전 서비스 배포 금지.",
|
||||
"synthetic_seed": {
|
||||
"text": SYNTHETIC_SEED_TEXT,
|
||||
"wav": seed_wav.name,
|
||||
"mp3": seed_mp3.name,
|
||||
"duration_sec": round(wav_duration(seed_wav), 2),
|
||||
},
|
||||
"samples": [],
|
||||
}
|
||||
|
||||
for sample in SAMPLES:
|
||||
wav_path = OUT_DIR / f"{sample['id']}.wav"
|
||||
mp3_path = OUT_DIR / f"{sample['id']}.mp3"
|
||||
print(f"[synth] {sample['id']} - {sample['label']}", flush=True)
|
||||
audio = synth(
|
||||
native,
|
||||
bundle,
|
||||
text=sample["text"],
|
||||
seed=sample["seed"],
|
||||
reference_audio=seed_audio,
|
||||
reference_text=SYNTHETIC_SEED_TEXT,
|
||||
)
|
||||
write_wav(audio, wav_path)
|
||||
convert_mp3(wav_path, mp3_path)
|
||||
manifest["samples"].append(
|
||||
{
|
||||
**sample,
|
||||
"wav": wav_path.name,
|
||||
"mp3": mp3_path.name,
|
||||
"duration_sec": round(wav_duration(wav_path), 2),
|
||||
"bytes_mp3": mp3_path.stat().st_size,
|
||||
}
|
||||
)
|
||||
|
||||
(OUT_DIR / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"[done] {OUT_DIR}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue