Download Models/Russian_CosyVoice3/prosody_profile.py from Random118/GLaDOS_TTS: direct link, hf CLI and curl.
- Browser
- Download file 4.94 kB
-
https://huggingface.co/Random118/GLaDOS_TTS/resolve/main/Models/Russian_CosyVoice3/prosody_profile.py
- Command line
-
hf download hf://Random118/GLaDOS_TTS/Models/Russian_CosyVoice3/prosody_profile.py
-
curl -L -o prosody_profile.py https://huggingface.co/Random118/GLaDOS_TTS/resolve/main/Models/Russian_CosyVoice3/prosody_profile.py
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() | |