Delete tts_ad.py
Browse files
tts_ad.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
import os, re, tempfile, subprocess
|
| 2 |
-
from dataclasses import dataclass
|
| 3 |
-
from typing import List, Optional, Tuple
|
| 4 |
-
import numpy as np
|
| 5 |
-
import soundfile as sf
|
| 6 |
-
|
| 7 |
-
from ovos_tts_plugin_matxa_multispeaker_cat import MatxaCatalanTTSPlugin
|
| 8 |
-
from pydub import AudioSegment
|
| 9 |
-
|
| 10 |
-
@dataclass
|
| 11 |
-
class Segment:
|
| 12 |
-
idx: int
|
| 13 |
-
start_s: float
|
| 14 |
-
end_s: float
|
| 15 |
-
text: str
|
| 16 |
-
|
| 17 |
-
SRT_TS = re.compile(
|
| 18 |
-
r"(?P<h1>\d{2}):(?P<m1>\d{2}):(?P<s1>\d{2}),(?P<ms1>\d{3})\s*-->\s*"
|
| 19 |
-
r"(?P<h2>\d{2}):(?P<m2>\d{2}):(?P<s2>\d{2}),(?P<ms2>\d{3})"
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
def _ts_to_seconds(h: str, m: str, s: str, ms: str) -> float:
|
| 23 |
-
return int(h)*3600 + int(m)*60 + int(s) + int(ms)/1000.0
|
| 24 |
-
|
| 25 |
-
def _is_empty_ad_text(t: str) -> bool:
|
| 26 |
-
cleaned = re.sub(r"[^\wÀ-ÿ]", "", t, flags=re.UNICODE)
|
| 27 |
-
return len(cleaned.strip()) == 0
|
| 28 |
-
|
| 29 |
-
def parse_srt_ad_only(srt_text: str) -> List[Segment]:
|
| 30 |
-
content = srt_text.replace("\r\n","\n").replace("\r","\n")
|
| 31 |
-
blocks = [b.strip() for b in re.split(r"\n\s*\n", content) if b.strip()]
|
| 32 |
-
segs: List[Segment] = []
|
| 33 |
-
for block in blocks:
|
| 34 |
-
lines = block.split("\n")
|
| 35 |
-
if len(lines) < 2: continue
|
| 36 |
-
try:
|
| 37 |
-
idx = int(lines[0].strip())
|
| 38 |
-
m = SRT_TS.match(lines[1].strip())
|
| 39 |
-
if not m: continue
|
| 40 |
-
start_s = _ts_to_seconds(m["h1"],m["m1"],m["s1"],m["ms1"])
|
| 41 |
-
end_s = _ts_to_seconds(m["h2"],m["m2"],m["s2"],m["ms2"])
|
| 42 |
-
ad_texts=[]
|
| 43 |
-
for t in lines[2:]:
|
| 44 |
-
t=t.strip()
|
| 45 |
-
if t.startswith("(AD):"):
|
| 46 |
-
t=t[len("(AD):"):].lstrip()
|
| 47 |
-
if t and not _is_empty_ad_text(t):
|
| 48 |
-
ad_texts.append(t)
|
| 49 |
-
if not ad_texts: continue
|
| 50 |
-
segs.append(Segment(idx=idx, start_s=start_s, end_s=end_s, text=" ".join(ad_texts)))
|
| 51 |
-
except:
|
| 52 |
-
continue
|
| 53 |
-
segs.sort(key=lambda s:(s.start_s, s.idx))
|
| 54 |
-
return segs
|
| 55 |
-
|
| 56 |
-
def _resample_np(x: np.ndarray, sr_from: int, sr_to: int) -> np.ndarray:
|
| 57 |
-
if sr_from == sr_to: return x
|
| 58 |
-
ratio = sr_to / sr_from
|
| 59 |
-
new_len = int(round(len(x)*ratio))
|
| 60 |
-
xp = np.linspace(0,1,num=len(x),endpoint=False)
|
| 61 |
-
xq = np.linspace(0,1,num=new_len,endpoint=False)
|
| 62 |
-
return np.interp(xq, xp, x).astype(np.float32)
|
| 63 |
-
|
| 64 |
-
def trim_or_pad(data: np.ndarray, sr: int, target_sec: float) -> np.ndarray:
|
| 65 |
-
target_len = int(round(target_sec * sr))
|
| 66 |
-
if len(data) > target_len: return data[:target_len]
|
| 67 |
-
if len(data) < target_len:
|
| 68 |
-
pad = np.zeros(target_len-len(data), dtype=data.dtype)
|
| 69 |
-
return np.concatenate([data,pad])
|
| 70 |
-
return data
|
| 71 |
-
|
| 72 |
-
def synth_text_to_wav(text: str, out_path: str, voice="central/grau", tts=None) -> Tuple[int, np.ndarray]:
|
| 73 |
-
created = False
|
| 74 |
-
if tts is None:
|
| 75 |
-
tts = MatxaCatalanTTSPlugin()
|
| 76 |
-
created = True
|
| 77 |
-
# asegura carpeta
|
| 78 |
-
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
| 79 |
-
tts.get_tts(text, out_path, voice=voice)
|
| 80 |
-
data, sr = sf.read(out_path, dtype="float32", always_2d=False)
|
| 81 |
-
if data.ndim == 2: data = data.mean(axis=1)
|
| 82 |
-
if created: del tts
|
| 83 |
-
return sr, data
|
| 84 |
-
|
| 85 |
-
def build_ad_track_from_srt_text(srt_text: str, out_audio_path: str, voice="central/grau") -> str:
|
| 86 |
-
segs = parse_srt_ad_only(srt_text)
|
| 87 |
-
if not segs:
|
| 88 |
-
raise ValueError("No se encontraron bloques (AD) con contenido en el SRT.")
|
| 89 |
-
# generar cada clip y montar timeline
|
| 90 |
-
tts = MatxaCatalanTTSPlugin()
|
| 91 |
-
tmpdir = tempfile.mkdtemp(prefix="matxa_ad_")
|
| 92 |
-
clips=[]
|
| 93 |
-
for seg in segs:
|
| 94 |
-
seg_wav = os.path.join(tmpdir, f"ad_{seg.idx}.wav")
|
| 95 |
-
sr, data = synth_text_to_wav(seg.text, seg_wav, voice=voice, tts=tts)
|
| 96 |
-
data = trim_or_pad(data, sr, seg.end_s - seg.start_s)
|
| 97 |
-
clips.append((sr, data, seg.start_s, seg.end_s))
|
| 98 |
-
master_sr = clips[0][0]
|
| 99 |
-
total_dur = max(s.end_s for s in segs)
|
| 100 |
-
master = np.zeros(int(round(total_dur*master_sr)), dtype=np.float32)
|
| 101 |
-
for sr, data, start_s, _ in clips:
|
| 102 |
-
d = _resample_np(data, sr, master_sr)
|
| 103 |
-
i = int(round(start_s*master_sr))
|
| 104 |
-
j = min(i+len(d), len(master))
|
| 105 |
-
master[i:j] += d[:j-i]
|
| 106 |
-
# normalización suave
|
| 107 |
-
peak = float(np.max(np.abs(master))) if master.size else 0.0
|
| 108 |
-
if peak > 0.999: master = (master/peak*0.98).astype(np.float32)
|
| 109 |
-
# escribe mp3 o wav
|
| 110 |
-
base, ext = os.path.splitext(out_audio_path)
|
| 111 |
-
os.makedirs(os.path.dirname(out_audio_path) or ".", exist_ok=True)
|
| 112 |
-
if ext.lower()==".mp3":
|
| 113 |
-
tmp_wav = base + ".__tmp__.wav"
|
| 114 |
-
sf.write(tmp_wav, master, master_sr, subtype="PCM_16")
|
| 115 |
-
AudioSegment.from_wav(tmp_wav).export(out_audio_path, format="mp3")
|
| 116 |
-
os.remove(tmp_wav)
|
| 117 |
-
else:
|
| 118 |
-
if ext.lower()!=".wav":
|
| 119 |
-
out_audio_path = base + ".wav"
|
| 120 |
-
sf.write(out_audio_path, master, master_sr, subtype="PCM_16")
|
| 121 |
-
return out_audio_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|