Signed-off-by: Mr.Dong <1042542469@qq.com> Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Mr.Dong <1042542469@qq.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""Metadata merge helpers for SoulX-Singer preprocess."""
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import soundfile as sf
|
|
|
|
|
|
@dataclass
|
|
class SegmentMetadata:
|
|
item_name: str
|
|
wav_fn: str
|
|
language: str
|
|
start_time_ms: int
|
|
end_time_ms: int
|
|
note_text: list[str]
|
|
note_dur: list[float]
|
|
note_pitch: list[int]
|
|
note_type: list[int]
|
|
origin_wav_fn: str | None = None
|
|
wav: np.ndarray | None = None
|
|
|
|
def get(self, name: str, default: Any = None) -> Any:
|
|
return getattr(self, name, default)
|
|
|
|
|
|
def _merge_group(
|
|
audio: np.ndarray,
|
|
sample_rate: int,
|
|
segments: list[SegmentMetadata | dict[str, Any]],
|
|
output_dir: Path | None,
|
|
*,
|
|
end_extension_ms: int = 0,
|
|
) -> SegmentMetadata:
|
|
if not segments:
|
|
raise ValueError("segments must not be empty")
|
|
|
|
words: list[str] = []
|
|
durs: list[float] = []
|
|
pitches: list[int] = []
|
|
types: list[int] = []
|
|
|
|
for i, seg in enumerate(segments):
|
|
if i > 0:
|
|
prev_seg = segments[i - 1]
|
|
gap_ms = seg.get("start_time_ms", 0) - prev_seg.get("end_time_ms", 0)
|
|
if gap_ms > 0:
|
|
words.append("<SP>")
|
|
durs.append(gap_ms / 1000.0)
|
|
pitches.append(0)
|
|
types.append(1)
|
|
|
|
words.extend(seg.get("note_text", []))
|
|
durs.extend(seg.get("note_dur", []))
|
|
pitches.extend(seg.get("note_pitch", []))
|
|
types.extend(seg.get("note_type", []))
|
|
|
|
if end_extension_ms > 0:
|
|
words.append("<SP>")
|
|
durs.append(end_extension_ms / 1000.0)
|
|
pitches.append(0)
|
|
types.append(1)
|
|
|
|
merged_words, merged_durs, merged_pitches, merged_types = [], [], [], []
|
|
for word, dur, pitch, note_type in zip(words, durs, pitches, types):
|
|
if merged_words and word == "<SP>" and merged_words[-1] == "<SP>":
|
|
merged_durs[-1] += dur
|
|
else:
|
|
merged_words.append(word)
|
|
merged_durs.append(dur)
|
|
merged_pitches.append(pitch)
|
|
merged_types.append(note_type)
|
|
|
|
languages = [s.get("language", "Mandarin") for s in segments if s.get("language")]
|
|
language = max(languages, key=languages.count) if languages else "Mandarin"
|
|
|
|
start_ms = int(segments[0].get("start_time_ms", 0))
|
|
end_ms = int(segments[-1].get("end_time_ms", 0)) + end_extension_ms
|
|
start_sample = int(start_ms * sample_rate // 1000)
|
|
end_sample = int(end_ms * sample_rate // 1000)
|
|
|
|
first_item_name = segments[0].get("item_name", "segment")
|
|
song_prefix = "_".join(str(first_item_name).split("_")[:-1])
|
|
item_name = f"{song_prefix}_{start_ms}_{end_ms}"
|
|
|
|
wav_fn = ""
|
|
segment_audio = audio[start_sample:end_sample]
|
|
if output_dir is not None:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
wav_path = output_dir / f"{item_name}.wav"
|
|
sf.write(wav_path, segment_audio, sample_rate)
|
|
wav_fn = str(wav_path)
|
|
|
|
return SegmentMetadata(
|
|
item_name=item_name,
|
|
wav_fn=wav_fn,
|
|
language=language,
|
|
start_time_ms=start_ms,
|
|
end_time_ms=end_ms,
|
|
note_text=merged_words,
|
|
note_dur=merged_durs,
|
|
note_pitch=merged_pitches,
|
|
note_type=merged_types,
|
|
origin_wav_fn=segments[0].get("origin_wav_fn", ""),
|
|
wav=segment_audio if output_dir is None else None,
|
|
)
|
|
|
|
|
|
def convert_metadata(item: SegmentMetadata, f0: np.ndarray | None = None) -> dict[str, Any]:
|
|
"""Convert merged segment metadata into SoulX JSON metadata format.
|
|
|
|
Matches upstream ``convert_metadata``: F0 always comes from ``*_f0.npy`` written
|
|
by ``extract_f0_file`` on the merged segment wav, not from an in-memory array.
|
|
"""
|
|
from vllm_omni.diffusion.models.soulx_singer.preprocess.g2p import g2p_transform
|
|
|
|
if item.wav_fn:
|
|
f0_path = str(Path(item.wav_fn).with_suffix("")) + "_f0.npy"
|
|
f0_arr = np.load(f0_path)
|
|
elif f0 is not None:
|
|
f0_arr = f0
|
|
else:
|
|
raise ValueError("convert_metadata requires item.wav_fn with sidecar f0 or an explicit f0 array")
|
|
|
|
return {
|
|
"index": item.item_name,
|
|
"language": item.language,
|
|
"time": [item.start_time_ms, item.end_time_ms],
|
|
"duration": " ".join(f"{d:.2f}" for d in item.note_dur),
|
|
"text": " ".join(item.note_text),
|
|
"phoneme": " ".join(g2p_transform(item.note_text, item.language)),
|
|
"note_pitch": " ".join(map(str, item.note_pitch)),
|
|
"note_type": " ".join(map(str, item.note_type)),
|
|
"f0": " ".join(f"{x:.1f}" for x in f0_arr),
|
|
}
|