GLaDOS_TTS / Models /Russian_CosyVoice3 /prosody_profile.py
Random118's picture
Add Russian CosyVoice 3 dubbing pack and audio gallery
8c2913a verified
Raw History Blame Contribute Delete
4.94 kB
"""Extract an auditable prosody profile from a GLaDOS reference clip."""
import argparse
import json
import subprocess
from pathlib import Path
import librosa
import numpy as np
import soundfile as sf
def finite_percentiles(values, percentiles):
values = np.asarray(values)
values = values[np.isfinite(values)]
if len(values) == 0:
return [None for _ in percentiles]
return [round(float(x), 3) for x in np.percentile(values, percentiles)]
def analyze(path: Path, text: str | None = None) -> dict:
audio, sr = librosa.load(str(path), sr=16000, mono=True)
duration = len(audio) / sr
hop = 160
f0, voiced, _ = librosa.pyin(
audio,
fmin=65,
fmax=500,
sr=sr,
frame_length=1024,
hop_length=hop,
)
rms = librosa.feature.rms(y=audio, frame_length=1024, hop_length=hop)[0]
rms_db = librosa.amplitude_to_db(np.maximum(rms, 1e-8), ref=1.0)
centroid = librosa.feature.spectral_centroid(y=audio, sr=sr, hop_length=hop)[0]
flatness = librosa.feature.spectral_flatness(y=audio, hop_length=hop)[0]
intervals = librosa.effects.split(audio, top_db=32, frame_length=1024, hop_length=hop)
pauses = []
for left, right in zip(intervals, intervals[1:]):
start = right[0] / sr
end = left[1] / sr
if start - end >= 0.08:
pauses.append({"start": round(end, 3), "end": round(start, 3), "duration": round(start - end, 3)})
voiced_f0 = f0[np.isfinite(f0) & voiced]
times = np.arange(len(f0)) * hop / sr
valid = np.isfinite(f0)
contour_times = np.linspace(0, duration, 20)
if valid.any():
contour_f0 = np.interp(contour_times, times[valid], f0[valid])
else:
contour_f0 = np.full_like(contour_times, np.nan)
energy_times = np.arange(len(rms_db)) * hop / sr
contour_energy = np.interp(contour_times, energy_times, rms_db)
profile = {
"file": str(path),
"sample_rate_for_analysis": sr,
"duration_sec": round(duration, 3),
"voiced_fraction": round(float(np.mean(voiced)), 3),
"pause_map": pauses,
"f0_hz": {
"median": finite_percentiles(voiced_f0, [50])[0],
"p10_p90": finite_percentiles(voiced_f0, [10, 90]),
"contour_20_points": [round(float(x), 2) for x in contour_f0],
},
"energy_db": {
"p10_p50_p90": finite_percentiles(rms_db, [10, 50, 90]),
"dynamic_range_db": round(float(np.percentile(rms_db, 90) - np.percentile(rms_db, 10)), 3),
"contour_20_points": [round(float(x), 2) for x in contour_energy],
},
"timbral_proxies": {
"spectral_centroid_hz_median": finite_percentiles(centroid, [50])[0],
"spectral_flatness_median": finite_percentiles(flatness, [50])[0],
},
"interpretation": {
"target": "soft, close-mic, restrained mechanical delivery",
"sarcasm": "low-to-moderate pitch movement with deliberate clause-final timing",
"smile": "look for local energy/brightness lift around emphasized words; do not synthesize a broad cheerful smile",
},
}
if text:
profile["text"] = text
profile["text_chars_per_sec"] = round(len(text) / duration, 3)
return profile
def warp_duration(source: Path, reference: Path, output: Path) -> None:
reference_duration = librosa.get_duration(path=str(reference))
output.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
"rubberband",
"-3",
"-D",
f"{reference_duration:.6f}",
str(source),
str(output),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
audio, sr = sf.read(output, always_2d=False)
peak = float(np.max(np.abs(audio)))
if peak > 0.95:
audio = audio * (0.95 / peak)
sf.write(output, audio, sr, subtype="PCM_16")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--reference", type=Path, required=True)
parser.add_argument("--source", type=Path)
parser.add_argument("--text")
parser.add_argument("--output-profile", type=Path, required=True)
parser.add_argument("--warp-source", type=Path)
args = parser.parse_args()
profile = analyze(args.reference, args.text)
if args.source:
profile["source"] = analyze(args.source, args.text)
args.output_profile.parent.mkdir(parents=True, exist_ok=True)
args.output_profile.write_text(json.dumps(profile, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if args.warp_source:
if not args.source:
raise SystemExit("--warp-source requires --source")
warp_duration(args.source, args.reference, args.warp_source)
print(json.dumps(profile, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()