현재 작업 상태 저장

This commit is contained in:
Yun Chan 2026-06-27 11:20:24 +09:00
parent 07cc67761e
commit 6bd91b0d5e
674 changed files with 8726 additions and 298 deletions

View 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())