MsThai commited on
Commit
e0cd25a
·
verified ·
1 Parent(s): 34c7570

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -1,17 +1,19 @@
1
- # Ox Translate - container for Hugging Face Spaces (or any Docker host)
2
  FROM python:3.12-slim
3
- RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg fonts-dejavu-core && rm -rf /var/lib/apt/lists/*
4
  RUN useradd -m -u 1000 user
5
  USER user
6
  ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH HF_HOME=/home/user/.cache/huggingface \
7
  OXT_DATA=/home/user/data PYTHONUNBUFFERED=1
8
  WORKDIR /home/user/app
9
  COPY --chown=user requirements.txt .
10
- RUN pip install --no-cache-dir --user -r requirements.txt
 
11
  # Pre-download the speech + translation models so the first visitor doesn't wait
12
  RUN python -c "from faster_whisper import WhisperModel; WhisperModel('small', device='cpu', compute_type='int8')" && \
13
  python -c "from huggingface_hub import snapshot_download; snapshot_download('JustFrederik/nllb-200-distilled-600M-ct2-int8')" && \
14
- python -c "import transformers; transformers.AutoTokenizer.from_pretrained('facebook/nllb-200-distilled-600M')"
 
15
  COPY --chown=user . .
16
  EXPOSE 7860
17
  CMD ["python", "-m", "uvicorn", "app.server:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "*"]
 
1
+ # SwitchLingo - container for Hugging Face Spaces (or any Docker host)
2
  FROM python:3.12-slim
3
+ RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg fonts-dejavu-core fonts-noto-core fonts-noto-cjk && rm -rf /var/lib/apt/lists/*
4
  RUN useradd -m -u 1000 user
5
  USER user
6
  ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH HF_HOME=/home/user/.cache/huggingface \
7
  OXT_DATA=/home/user/data PYTHONUNBUFFERED=1
8
  WORKDIR /home/user/app
9
  COPY --chown=user requirements.txt .
10
+ RUN pip install --no-cache-dir --user torch --index-url https://download.pytorch.org/whl/cpu && \
11
+ pip install --no-cache-dir --user -r requirements.txt
12
  # Pre-download the speech + translation models so the first visitor doesn't wait
13
  RUN python -c "from faster_whisper import WhisperModel; WhisperModel('small', device='cpu', compute_type='int8')" && \
14
  python -c "from huggingface_hub import snapshot_download; snapshot_download('JustFrederik/nllb-200-distilled-600M-ct2-int8')" && \
15
+ python -c "import transformers; transformers.AutoTokenizer.from_pretrained('facebook/nllb-200-distilled-600M')" && \
16
+ python -c "from huggingface_hub import hf_hub_download as d; [d('myshell-ai/OpenVoiceV2', f) for f in ('converter/config.json', 'converter/checkpoint.pth')]"
17
  COPY --chown=user . .
18
  EXPOSE 7860
19
  CMD ["python", "-m", "uvicorn", "app.server:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "*"]
app/engines.py CHANGED
@@ -175,11 +175,33 @@ def available_voices() -> list[str]:
175
  return out
176
 
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  def speak(text: str, lang: str, out: Path, gender: str = "Female", engine: str = "edge",
179
- rate: str = "+15%") -> None:
180
  """Write speech for `text` to `out` (mp3)."""
181
  if engine == "elevenlabs":
182
- vid = os.environ.get(f"OXT_ELEVEN_VOICE_{gender.upper()}", ELEVEN_DEFAULT[gender])
183
  _eleven_tts(text, vid, out)
184
  return
185
  voice = pick_edge_voice(lang, gender)
@@ -190,3 +212,47 @@ def speak(text: str, lang: str, out: Path, gender: str = "Female", engine: str =
190
 
191
  def clean(text: str) -> str:
192
  return re.sub(r"\s+", " ", text).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  return out
176
 
177
 
178
+ def eleven_clone(sample: Path) -> str:
179
+ """Instant voice clone from a speech sample; returns a temporary voice_id."""
180
+ import requests
181
+ with open(sample, "rb") as f:
182
+ r = requests.post("https://api.elevenlabs.io/v1/voices/add",
183
+ headers={"xi-api-key": os.environ["ELEVENLABS_API_KEY"]},
184
+ data={"name": f"switchlingo-tmp-{sample.parent.name}",
185
+ "remove_background_noise": "true"},
186
+ files={"files": (sample.name, f, "audio/mpeg")}, timeout=180)
187
+ r.raise_for_status()
188
+ return r.json()["voice_id"]
189
+
190
+
191
+ def eleven_delete(voice_id: str) -> None:
192
+ import requests
193
+ try:
194
+ requests.delete(f"https://api.elevenlabs.io/v1/voices/{voice_id}",
195
+ headers={"xi-api-key": os.environ["ELEVENLABS_API_KEY"]}, timeout=60)
196
+ except Exception: # noqa: BLE001 - best effort cleanup
197
+ pass
198
+
199
+
200
  def speak(text: str, lang: str, out: Path, gender: str = "Female", engine: str = "edge",
201
+ rate: str = "+15%", voice_id: str | None = None) -> None:
202
  """Write speech for `text` to `out` (mp3)."""
203
  if engine == "elevenlabs":
204
+ vid = voice_id or os.environ.get(f"OXT_ELEVEN_VOICE_{gender.upper()}", ELEVEN_DEFAULT[gender])
205
  _eleven_tts(text, vid, out)
206
  return
207
  voice = pick_edge_voice(lang, gender)
 
212
 
213
  def clean(text: str) -> str:
214
  return re.sub(r"\s+", " ", text).strip()
215
+
216
+
217
+ # ------------------------------------------------------------------ lip-sync (GPU, pay-per-video)
218
+ def lipsync_available() -> bool:
219
+ return bool(os.environ.get("REPLICATE_API_TOKEN"))
220
+
221
+
222
+ def lipsync(video: Path, audio: Path, out: Path, progress=lambda m: None) -> None:
223
+ """Re-render mouths to match the new voice with ByteDance LatentSync on Replicate (~$0.10/run).
224
+ Model can be swapped with OXT_LIPSYNC_MODEL (owner/name)."""
225
+ import time as _t
226
+ import requests
227
+ tok = os.environ["REPLICATE_API_TOKEN"]
228
+ h = {"Authorization": f"Bearer {tok}"}
229
+ api = "https://api.replicate.com/v1"
230
+ model = os.environ.get("OXT_LIPSYNC_MODEL", "bytedance/latentsync")
231
+
232
+ def upload(p: Path) -> str:
233
+ with open(p, "rb") as f:
234
+ r = requests.post(f"{api}/files", headers=h, files={"content": (p.name, f)}, timeout=600)
235
+ r.raise_for_status()
236
+ return r.json()["urls"]["get"]
237
+
238
+ version = requests.get(f"{api}/models/{model}", headers=h, timeout=60).json()["latest_version"]["id"]
239
+ vurl, aurl = upload(video), upload(audio)
240
+ r = requests.post(f"{api}/predictions", headers={**h, "Content-Type": "application/json"},
241
+ json={"version": version, "input": {"video": vurl, "audio": aurl}}, timeout=60)
242
+ r.raise_for_status()
243
+ pred = r.json()
244
+ t0 = _t.time()
245
+ while pred["status"] not in ("succeeded", "failed", "canceled"):
246
+ if _t.time() - t0 > 1800:
247
+ raise RuntimeError("Lip-sync took too long.")
248
+ progress(f"Lip-syncing on GPU ({int(_t.time() - t0)}s)")
249
+ _t.sleep(5)
250
+ pred = requests.get(pred["urls"]["get"], headers=h, timeout=60).json()
251
+ if pred["status"] != "succeeded":
252
+ raise RuntimeError(f"Lip-sync failed: {pred.get('error')}")
253
+ url = pred["output"] if isinstance(pred["output"], str) else pred["output"][0]
254
+ with requests.get(url, stream=True, timeout=600) as d:
255
+ d.raise_for_status()
256
+ with open(out, "wb") as f:
257
+ for chunk in d.iter_content(1 << 20):
258
+ f.write(chunk)
app/feedback.py CHANGED
@@ -55,6 +55,18 @@ def voter_id(ip: str) -> str:
55
 
56
 
57
  def _learn(rec: dict) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
58
  fix = (rec.get("correction") or "").strip()
59
  if not fix:
60
  return
@@ -91,7 +103,7 @@ def start() -> None:
91
  from huggingface_hub import CommitScheduler
92
  _scheduler = CommitScheduler(repo_id=REPO, repo_type="dataset", folder_path=FB_DIR,
93
  path_in_repo=".", every=5, private=True,
94
- allow_patterns=["*.jsonl"])
95
  except Exception as e: # noqa: BLE001
96
  _sync_error = f"{type(e).__name__}: {str(e)[:200]}"
97
  print("feedback sync disabled:", e)
@@ -126,19 +138,41 @@ def examples(src: str, tgt: str, n: int = 15) -> list[dict]:
126
  return out[-n:]
127
 
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  def stats() -> dict:
130
- n = good = bad = fixes = 0
 
131
  for p in FB_DIR.glob("*.jsonl"):
132
  for line in p.read_text(encoding="utf-8").splitlines():
133
  try:
134
  r = json.loads(line)
135
  except ValueError:
136
  continue
 
 
 
 
137
  n += 1
138
  good += r.get("rating") == 1
139
  bad += r.get("rating") == -1
140
  fixes += bool(r.get("correction"))
141
- return {"ratings": n, "thumbs_up": good, "thumbs_down": bad, "corrections": fixes,
142
  "approved_fixes": len(_trusted) + sum(1 for k in _votes if lookup(*k)),
143
  "synced_to": REPO or None, "sync_active": _scheduler is not None,
144
  "sync_error": _sync_error or None}
 
55
 
56
 
57
  def _learn(rec: dict) -> None:
58
+ if rec.get("kind") == "pair": # contributed sentence pair counts as a vote both ways
59
+ if rec.get("dialect"): # dialects are kept for training only, not mixed into the parent language
60
+ return
61
+ a, b = rec["en"].strip(), rec["text"].strip()
62
+ lang = rec["lang"]
63
+ if a and b:
64
+ _votes[_key("en", lang, a)][b].add(rec.get("voter", "?"))
65
+ _votes[_key(lang, "en", b)][a].add(rec.get("voter", "?"))
66
+ if rec.get("trusted"):
67
+ _trusted[_key("en", lang, a)] = b
68
+ _trusted[_key(lang, "en", b)] = a
69
+ return
70
  fix = (rec.get("correction") or "").strip()
71
  if not fix:
72
  return
 
103
  from huggingface_hub import CommitScheduler
104
  _scheduler = CommitScheduler(repo_id=REPO, repo_type="dataset", folder_path=FB_DIR,
105
  path_in_repo=".", every=5, private=True,
106
+ allow_patterns=["*.jsonl", "audio/*"])
107
  except Exception as e: # noqa: BLE001
108
  _sync_error = f"{type(e).__name__}: {str(e)[:200]}"
109
  print("feedback sync disabled:", e)
 
138
  return out[-n:]
139
 
140
 
141
+ AUDIO_DIR = FB_DIR / "audio"
142
+ AUDIO_DIR.mkdir(exist_ok=True)
143
+
144
+
145
+ def add_pair(lang: str, en: str, text: str, voter: str, trusted: bool, dialect: str = "",
146
+ audio: bytes | None = None, audio_ext: str = "webm") -> None:
147
+ rec = {"kind": "pair", "lang": lang, "dialect": dialect, "en": en, "text": text,
148
+ "voter": voter, "trusted": trusted}
149
+ if audio:
150
+ name = f"{uuid.uuid4().hex[:12]}.{audio_ext}"
151
+ lock = _scheduler.lock if _scheduler else _lock
152
+ with lock:
153
+ (AUDIO_DIR / name).write_bytes(audio)
154
+ rec["audio"] = f"audio/{name}"
155
+ add(rec)
156
+
157
+
158
  def stats() -> dict:
159
+ n = good = bad = fixes = pairs = 0
160
+ by_lang: dict[str, int] = defaultdict(int)
161
  for p in FB_DIR.glob("*.jsonl"):
162
  for line in p.read_text(encoding="utf-8").splitlines():
163
  try:
164
  r = json.loads(line)
165
  except ValueError:
166
  continue
167
+ if r.get("kind") == "pair":
168
+ pairs += 1
169
+ by_lang[r.get("dialect") or r.get("lang")] += 1
170
+ continue
171
  n += 1
172
  good += r.get("rating") == 1
173
  bad += r.get("rating") == -1
174
  fixes += bool(r.get("correction"))
175
+ return {"ratings": n, "sentence_pairs": pairs, "pairs_by_language": dict(by_lang), "thumbs_up": good, "thumbs_down": bad, "corrections": fixes,
176
  "approved_fixes": len(_trusted) + sum(1 for k in _votes if lookup(*k)),
177
  "synced_to": REPO or None, "sync_active": _scheduler is not None,
178
  "sync_error": _sync_error or None}
app/pipeline.py CHANGED
@@ -39,7 +39,10 @@ def pcm(path: Path, rate: int, tempo: float = 1.0) -> np.ndarray:
39
  return np.frombuffer(raw, np.int16).astype(np.float32)
40
 
41
 
42
- def fetch_url(url: str, folder: Path) -> Path:
 
 
 
43
  import yt_dlp
44
  opts = {"outtmpl": str(folder / "source.%(ext)s"), "quiet": True, "no_warnings": True,
45
  "noprogress": True, "noplaylist": True,
@@ -48,9 +51,42 @@ def fetch_url(url: str, folder: Path) -> Path:
48
  with yt_dlp.YoutubeDL(opts) as ydl:
49
  info = ydl.extract_info(url, download=True)
50
  p = Path(ydl.prepare_filename(info))
51
- if not p.exists():
52
- p = next(folder.glob("source.*"))
53
- return p
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
  # ------------------------------------------------------------------ transcription
@@ -135,22 +171,78 @@ def burn_subs(video: Path, srt: Path, out: Path) -> None:
135
  "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", str(out)], check=True)
136
 
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  def dub(media: Path, segs: list[dict], lang: str, out: Path, has_video: bool, duration: float,
139
- gender: str, voice_engine: str, work: Path, progress: Progress) -> None:
140
  parts = work / "voice"
141
  parts.mkdir(exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  track = np.zeros(int((duration + 15) * SR), np.float32)
143
  prev_end = 0
144
- for k, s in enumerate(segs):
145
- progress(f"Voicing line {k + 1} of {len(segs)}", 0.70 + 0.25 * k / max(len(segs), 1))
146
- f = parts / f"{k:04}.mp3"
147
- engines.speak(s["tr"], lang, f, gender=gender, engine=voice_engine)
148
  nxt = segs[k + 1]["s"] if k + 1 < len(segs) else duration
149
  window = max(nxt - s["s"] - 0.05, 0.4)
150
- a = pcm(f, SR)
151
- if len(a) / SR > window: # speed up a little to stay in sync (max 1.2x)
152
- a = pcm(f, SR, min(len(a) / SR / window, 1.2))
153
- st = max(int(s["s"] * SR), prev_end + int(0.08 * SR)) # never overlap
 
 
 
 
 
154
  prev_end = st + len(a)
155
  track[st:st + len(a)] += a[:len(track) - st]
156
  raw = work / "dub.raw"
@@ -171,7 +263,8 @@ def dub(media: Path, segs: list[dict], lang: str, out: Path, has_video: bool, du
171
  def run(job_dir: Path, source_path: Path | None, url: str | None, target: str,
172
  source_lang: str | None = None, engine: str = "nllb", outputs: tuple = ("text", "subs", "dub"),
173
  quality: str = "fast", gender: str = "Female", voice_engine: str = "edge",
174
- max_minutes: float = 0, progress: Progress = lambda m, p: None) -> dict:
 
175
  languages.get(target)
176
  progress("Getting the media", 0.02)
177
  media = fetch_url(url, job_dir) if url else source_path
@@ -220,12 +313,30 @@ def run(job_dir: Path, source_path: Path | None, url: str | None, target: str,
220
  progress("Creating the voice-over", 0.70)
221
  name = "voiceover.mp4" if meta["has_video"] else "voiceover.m4a"
222
  dub(base, segs, target, job_dir / name, meta["has_video"], meta["duration"],
223
- gender, voice_engine, job_dir, progress)
224
  results["files"]["voiceover"] = name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  if meta["has_video"] or url:
227
  results["files"]["original"] = media.name
228
  shutil.rmtree(job_dir / "voice", ignore_errors=True)
 
 
229
  (job_dir / "dub.raw").unlink(missing_ok=True)
230
  progress("Done", 1.0)
231
  return results
 
39
  return np.frombuffer(raw, np.int16).astype(np.float32)
40
 
41
 
42
+ MEDIA_EXT = r"(?:mp4|m4v|mov|webm|mkv|m3u8|mp3|m4a|aac|wav|ogg|oga|opus|flac|amr|3gp)"
43
+
44
+
45
+ def _ydl(url: str, folder: Path) -> Path:
46
  import yt_dlp
47
  opts = {"outtmpl": str(folder / "source.%(ext)s"), "quiet": True, "no_warnings": True,
48
  "noprogress": True, "noplaylist": True,
 
51
  with yt_dlp.YoutubeDL(opts) as ydl:
52
  info = ydl.extract_info(url, download=True)
53
  p = Path(ydl.prepare_filename(info))
54
+ return p if p.exists() else next(folder.glob("source.*"))
55
+
56
+
57
+ def _find_media_in_page(url: str) -> list[str]:
58
+ """For ordinary websites: look for <video>/<audio> sources, og:video tags and media file links."""
59
+ import html
60
+ import re as _re
61
+ from urllib.parse import urljoin
62
+ import requests
63
+ page = requests.get(url, timeout=30, headers={"User-Agent": "Mozilla/5.0 SwitchLingo"}).text
64
+ found = []
65
+ pats = [r'<(?:video|audio|source)[^>]+src=["\']([^"\']+)',
66
+ r'<meta[^>]+(?:property|name)=["\'](?:og:video(?::secure_url|:url)?|og:audio|twitter:player:stream)["\'][^>]+content=["\']([^"\']+)',
67
+ r'["\'(]((?:https?:)?//[^"\'()\s]+?\.' + MEDIA_EXT + r'(?:\?[^"\'()\s]*)?)["\')]',
68
+ r'(?:youtube\.com/embed/|player\.vimeo\.com/video/)[\w-]+']
69
+ for pat in pats:
70
+ for m in _re.finditer(pat, page, _re.I):
71
+ u = html.unescape(m.group(1) if m.groups() else "https://" + m.group(0))
72
+ u = urljoin(url, u)
73
+ if u not in found:
74
+ found.append(u)
75
+ return found
76
+
77
+
78
+ def fetch_url(url: str, folder: Path) -> Path:
79
+ """Social media and 1000+ video sites via yt-dlp; direct file links; any web page with a video."""
80
+ try:
81
+ return _ydl(url, folder)
82
+ except Exception as first: # noqa: BLE001
83
+ for cand in _find_media_in_page(url)[:5]:
84
+ try:
85
+ return _ydl(cand, folder)
86
+ except Exception: # noqa: BLE001
87
+ continue
88
+ raise RuntimeError("Couldn't find a video or audio on that link. If it's private or behind a "
89
+ "login, download it and upload the file instead.") from first
90
 
91
 
92
  # ------------------------------------------------------------------ transcription
 
171
  "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", str(out)], check=True)
172
 
173
 
174
+ def _speech_sample(media: Path, segs: list[dict], out: Path, max_sec: float = 75) -> Path:
175
+ """The speaker's own voice only (no silences), for voice cloning."""
176
+ keep, total = [], 0.0
177
+ for s in segs:
178
+ if total >= max_sec:
179
+ break
180
+ keep.append(f"between(t,{s['s']:.2f},{s['e']:.2f})")
181
+ total += s["e"] - s["s"]
182
+ subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(media), "-vn",
183
+ "-af", f"aselect='{'+'.join(keep)}',asetpts=N/SR/TB", "-ac", "1",
184
+ "-b:a", "128k", str(out)], check=True)
185
+ return out
186
+
187
+
188
  def dub(media: Path, segs: list[dict], lang: str, out: Path, has_video: bool, duration: float,
189
+ gender: str, voice_engine: str, work: Path, progress: Progress, clone: bool = False) -> None:
190
  parts = work / "voice"
191
  parts.mkdir(exist_ok=True)
192
+ n = max(len(segs), 1)
193
+
194
+ # 1) speak every line (ElevenLabs can clone the speaker directly)
195
+ voice_id = None
196
+ if clone and voice_engine == "elevenlabs":
197
+ progress("Cloning the speaker's voice", 0.70)
198
+ voice_id = engines.eleven_clone(_speech_sample(media, segs, work / "sample.mp3"))
199
+ try:
200
+ files = []
201
+ for k, s in enumerate(segs):
202
+ progress(f"Voicing line {k + 1} of {len(segs)}", 0.70 + 0.15 * k / n)
203
+ f = parts / f"{k:04}.mp3"
204
+ engines.speak(s["tr"], lang, f, gender=gender, engine=voice_engine, voice_id=voice_id)
205
+ files.append(f)
206
+ finally:
207
+ if voice_id:
208
+ engines.eleven_delete(voice_id)
209
+ clips = [pcm(f, SR) for f in files]
210
+
211
+ # 2) free cloning for every language: re-color each line to sound like the original speaker
212
+ if clone and voice_engine != "elevenlabs":
213
+ from . import voiceclone as vc
214
+ progress("Matching the speaker's voice", 0.86)
215
+ orig = pcm(media, vc.SR) / 32768.0
216
+ tgts = vc.speaker_embeddings(orig, [(s["s"], s["e"]) for s in segs])
217
+ tts22 = [pcm(f, vc.SR) / 32768.0 for f in files]
218
+ src_se = vc.embed(np.concatenate(tts22[:12]))
219
+ for k, (y, t) in enumerate(zip(tts22, tgts)):
220
+ progress(f"Matching voice, line {k + 1} of {len(segs)}", 0.86 + 0.09 * k / n)
221
+ if t is None:
222
+ continue
223
+ conv = vc.convert(y.astype(np.float32), src_se, t)
224
+ raw = work / "vc.raw"
225
+ (np.clip(conv, -1, 1) * 32767).astype(np.int16).tofile(raw)
226
+ clips[k] = np.frombuffer(subprocess.run(
227
+ ["ffmpeg", "-loglevel", "error", "-f", "s16le", "-ar", str(vc.SR), "-ac", "1", "-i", str(raw),
228
+ "-f", "s16le", "-ar", str(SR), "-ac", "1", "-"], capture_output=True, check=True).stdout,
229
+ np.int16).astype(np.float32)
230
+
231
+ # 3) place lines at the speaker's timing (speed up max 1.2x, never overlap)
232
  track = np.zeros(int((duration + 15) * SR), np.float32)
233
  prev_end = 0
234
+ for k, (s, a) in enumerate(zip(segs, clips)):
 
 
 
235
  nxt = segs[k + 1]["s"] if k + 1 < len(segs) else duration
236
  window = max(nxt - s["s"] - 0.05, 0.4)
237
+ if len(a) / SR > window:
238
+ tempo = min(len(a) / SR / window, 1.2)
239
+ raw = work / "line.raw"
240
+ np.clip(a, -32768, 32767).astype(np.int16).tofile(raw)
241
+ a = np.frombuffer(subprocess.run(
242
+ ["ffmpeg", "-loglevel", "error", "-f", "s16le", "-ar", str(SR), "-ac", "1", "-i", str(raw),
243
+ "-af", f"atempo={tempo:.3f}", "-f", "s16le", "-"], capture_output=True, check=True).stdout,
244
+ np.int16).astype(np.float32)
245
+ st = max(int(s["s"] * SR), prev_end + int(0.08 * SR))
246
  prev_end = st + len(a)
247
  track[st:st + len(a)] += a[:len(track) - st]
248
  raw = work / "dub.raw"
 
263
  def run(job_dir: Path, source_path: Path | None, url: str | None, target: str,
264
  source_lang: str | None = None, engine: str = "nllb", outputs: tuple = ("text", "subs", "dub"),
265
  quality: str = "fast", gender: str = "Female", voice_engine: str = "edge",
266
+ max_minutes: float = 0, progress: Progress = lambda m, p: None,
267
+ clone: bool = True, lipsync: bool = False) -> dict:
268
  languages.get(target)
269
  progress("Getting the media", 0.02)
270
  media = fetch_url(url, job_dir) if url else source_path
 
313
  progress("Creating the voice-over", 0.70)
314
  name = "voiceover.mp4" if meta["has_video"] else "voiceover.m4a"
315
  dub(base, segs, target, job_dir / name, meta["has_video"], meta["duration"],
316
+ gender, voice_engine, job_dir, progress, clone=clone)
317
  results["files"]["voiceover"] = name
318
+ results["voice_cloned"] = clone
319
+ if lipsync and meta["has_video"] and engines.lipsync_available():
320
+ progress("Lip-syncing mouths to the new voice (GPU)", 0.96)
321
+ vo = job_dir / name
322
+ aud = job_dir / "dub_audio.m4a"
323
+ subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(vo), "-vn", "-c:a", "copy", str(aud)],
324
+ check=True)
325
+ synced = job_dir / "lipsync_raw.mp4"
326
+ engines.lipsync(vo, aud, synced, progress=lambda m: progress(m, 0.97))
327
+ subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(synced), "-i", str(aud),
328
+ "-map", "0:v:0", "-map", "1:a:0", "-c:v", "libx264", "-preset", "veryfast",
329
+ "-crf", "21", "-c:a", "copy", "-shortest", "-movflags", "+faststart",
330
+ str(job_dir / "lipsync.mp4")], check=True)
331
+ results["files"]["lipsync"] = "lipsync.mp4"
332
+ for junk in (aud, synced):
333
+ junk.unlink(missing_ok=True)
334
 
335
  if meta["has_video"] or url:
336
  results["files"]["original"] = media.name
337
  shutil.rmtree(job_dir / "voice", ignore_errors=True)
338
+ for junk in ("sample.mp3", "vc.raw", "line.raw"):
339
+ (job_dir / junk).unlink(missing_ok=True)
340
  (job_dir / "dub.raw").unlink(missing_ok=True)
341
  progress("Done", 1.0)
342
  return results
app/server.py CHANGED
@@ -83,7 +83,7 @@ def get_languages():
83
  return {"languages": languages.public_list(), "popular": languages.POPULAR_US,
84
  "limits": {"max_minutes": MAX_MINUTES, "max_mb": MAX_MB},
85
  "translators": engines.available_translators(),
86
- "voices": engines.available_voices()}
87
 
88
 
89
  @app.post("/api/jobs")
@@ -97,6 +97,8 @@ async def create_job(
97
  quality: str = Form("fast"),
98
  gender: str = Form("Female"),
99
  voice_engine: str = Form("edge"),
 
 
100
  file: UploadFile | None = File(None),
101
  ):
102
  if target not in languages.LANGS:
@@ -137,7 +139,8 @@ async def create_job(
137
  kwargs = dict(source_path=src_path, url=url.strip() or None, target=target,
138
  source_lang=source or None, engine=engine,
139
  outputs=tuple(o for o in outputs.split(",") if o), quality=quality,
140
- gender=gender, voice_engine=voice_engine, max_minutes=MAX_MINUTES)
 
141
  pool.submit(_work, job_id, kwargs)
142
  return {"id": job_id}
143
 
@@ -206,6 +209,45 @@ def post_feedback(request: Request, job_id: str = Form(...), seg: int = Form(...
206
  return {"ok": True}
207
 
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  @app.get("/api/feedback/stats")
210
  def feedback_stats():
211
  return feedback.stats()
 
83
  return {"languages": languages.public_list(), "popular": languages.POPULAR_US,
84
  "limits": {"max_minutes": MAX_MINUTES, "max_mb": MAX_MB},
85
  "translators": engines.available_translators(),
86
+ "voices": engines.available_voices(), "lipsync": engines.lipsync_available()}
87
 
88
 
89
  @app.post("/api/jobs")
 
97
  quality: str = Form("fast"),
98
  gender: str = Form("Female"),
99
  voice_engine: str = Form("edge"),
100
+ clone: str = Form("1"),
101
+ lipsync: str = Form("0"),
102
  file: UploadFile | None = File(None),
103
  ):
104
  if target not in languages.LANGS:
 
139
  kwargs = dict(source_path=src_path, url=url.strip() or None, target=target,
140
  source_lang=source or None, engine=engine,
141
  outputs=tuple(o for o in outputs.split(",") if o), quality=quality,
142
+ gender=gender, voice_engine=voice_engine, max_minutes=MAX_MINUTES,
143
+ clone=clone == "1", lipsync=lipsync == "1" and engines.lipsync_available())
144
  pool.submit(_work, job_id, kwargs)
145
  return {"id": job_id}
146
 
 
209
  return {"ok": True}
210
 
211
 
212
+ TEACH_PROMPTS = [
213
+ "Where is the nearest hospital?", "How much does this cost?", "Thank you for helping my family.",
214
+ "What time does the store close?", "I don't understand, please speak slowly.",
215
+ "My child is sick and needs a doctor.", "Can you help me fill out this form?",
216
+ "The food is delicious.", "I am looking for work.", "Please call me back tomorrow.",
217
+ "Grandma is coming to visit next week.", "Where do I pay my bill?",
218
+ "I need a translator for my appointment.", "Happy New Year to everyone!",
219
+ "Be careful, the road is slippery.", "How do you say this in your language?",
220
+ ]
221
+ TEACH_LANGS = {"lo": "Lao", "lo-isan": "Isan (Northeast Thai)", "hmn": "Hmong (White)",
222
+ "hmn-green": "Hmong (Green)", "km": "Khmer", "so": "Somali", "th": "Thai", "vi": "Vietnamese"}
223
+
224
+
225
+ @app.get("/api/teach/languages")
226
+ def teach_languages():
227
+ return {"languages": TEACH_LANGS, "prompts": TEACH_PROMPTS}
228
+
229
+
230
+ @app.post("/api/teach")
231
+ async def teach(request: Request, lang: str = Form(...), en: str = Form(...), text: str = Form(...),
232
+ token: str = Form(""), audio: UploadFile | None = File(None)):
233
+ if lang not in TEACH_LANGS:
234
+ raise HTTPException(400, "Unknown language")
235
+ en, text = en.strip()[:500], text.strip()[:500]
236
+ if not en or not text:
237
+ raise HTTPException(400, "Both sentences are needed")
238
+ data, ext = None, "webm"
239
+ if audio and audio.filename:
240
+ data = await audio.read(3 * 1024 * 1024 + 1)
241
+ if len(data) > 3 * 1024 * 1024:
242
+ raise HTTPException(413, "Recording is too long (max ~1 minute)")
243
+ ext = (Path(audio.filename).suffix.lstrip(".") or "webm")[:5]
244
+ base = lang.split("-")[0] # dialects feed the parent language's memory
245
+ feedback.add_pair(base, en, text, feedback.voter_id(_client_ip(request)),
246
+ bool(feedback.ADMIN_TOKEN) and token == feedback.ADMIN_TOKEN,
247
+ dialect=TEACH_LANGS[lang] if "-" in lang else "", audio=data, audio_ext=ext)
248
+ return {"ok": True}
249
+
250
+
251
  @app.get("/api/feedback/stats")
252
  def feedback_stats():
253
  return feedback.stats()
app/static/icon-192.png ADDED
app/static/icon-512.png ADDED
app/static/index.html CHANGED
@@ -5,6 +5,10 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <title>SwitchLingo — Any video, your language</title>
7
  <meta name="description" content="SwitchLingo: AI that translates any video or link into your language with subtitles and voice-over in 100+ languages, and gets better from your corrections.">
 
 
 
 
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
  <style>
@@ -55,11 +59,30 @@ td.t{white-space:nowrap;color:var(--muted);font-variant-numeric:tabular-nums;wid
55
  .fix{display:flex;gap:6px;margin-top:6px}.fix textarea{min-height:44px;font-size:14px}.fix button{border:0;background:var(--accent);color:var(--accent-ink);border-radius:8px;padding:0 12px;font-weight:600;cursor:pointer}
56
  .human{display:inline-block;font-size:11px;background:var(--soft);color:var(--ok);border-radius:99px;padding:1px 8px;margin-left:6px}
57
  .ok{color:var(--ok);font-size:12px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  </style>
59
  </head>
60
  <body>
61
  <div class="wrap">
62
- <header><h1>Switch<span>Lingo</span></h1><div class="tag">AI video translation. Subtitles and voice-over in 100+ languages, and it learns from your fixes.</div></header>
63
 
64
  <section class="card demo">
65
  <h2>See it switch</h2>
@@ -69,11 +92,15 @@ td.t{white-space:nowrap;color:var(--muted);font-variant-numeric:tabular-nums;wid
69
 
70
  <section class="card">
71
  <h2>1. Add your video or audio</h2>
72
- <div class="tabs"><button id="tLink" class="on">Paste a link</button><button id="tFile">Upload a file</button></div>
73
- <div id="pLink"><input id="url" type="url" placeholder="Facebook, Instagram, YouTube, TikTok… link"></div>
74
  <div id="pFile" class="hidden">
75
- <div id="drop" class="drop">Drop a video/audio file here or <u>browse</u><br><small id="fname"></small></div>
76
- <input id="file" type="file" accept="video/*,audio/*" hidden>
 
 
 
 
77
  </div>
78
 
79
  <div class="row">
@@ -86,6 +113,8 @@ td.t{white-space:nowrap;color:var(--muted);font-variant-numeric:tabular-nums;wid
86
  <label class="chk"><input type="checkbox" class="out" value="subs" checked> Subtitles on video</label>
87
  <label class="chk"><input type="checkbox" class="out" value="dub" checked> Voice-over</label>
88
  </div>
 
 
89
 
90
  <details>
91
  <summary>Options</summary>
@@ -98,7 +127,7 @@ td.t{white-space:nowrap;color:var(--muted);font-variant-numeric:tabular-nums;wid
98
  </div>
99
  </details>
100
  <button id="go" class="btn">Translate</button>
101
- <p class="muted" style="font-size:13px;margin:10px 0 0"><span id="limits"></span> Only use videos you have the right to share. Voice-overs are AI voices, not the original speaker.</p>
102
  <div id="formErr" class="err"></div>
103
  </section>
104
 
@@ -123,6 +152,22 @@ td.t{white-space:nowrap;color:var(--muted);font-variant-numeric:tabular-nums;wid
123
  <div id="jobs" class="jobs muted">Nothing yet.</div>
124
  </section>
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  <section class="card">
127
  <h2>Don't see your language?</h2>
128
  <p class="muted" style="margin-top:-6px">Tell us what to add. Requests help decide which languages and dialects get trained next.</p>
@@ -143,9 +188,22 @@ let mode = "link", file = null, LANG = {};
143
  const esc = s => (s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
144
  const fmt = s => { s = Math.floor(s); return `${Math.floor(s/60)}:${String(s%60).padStart(2,"0")}`; };
145
 
146
- function setMode(m){ mode=m; $("#tLink").classList.toggle("on",m==="link"); $("#tFile").classList.toggle("on",m==="file");
147
- $("#pLink").classList.toggle("hidden",m!=="link"); $("#pFile").classList.toggle("hidden",m!=="file"); }
148
- $("#tLink").onclick=()=>setMode("link"); $("#tFile").onclick=()=>setMode("file");
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  const drop=$("#drop");
150
  drop.onclick=()=>$("#file").click();
151
  $("#file").onchange=e=>{file=e.target.files[0]; $("#fname").textContent=file?file.name:"";};
@@ -169,6 +227,10 @@ async function init(){
169
  r.voices.forEach(v=>$("#voice").insertAdjacentHTML("beforeend",`<option value="${v}">${vn[v]||v}</option>`));
170
  if(r.voices.includes("elevenlabs")) $("#voice").value="elevenlabs";
171
  if(r.translators.includes("claude")) $("#engine").value="claude";
 
 
 
 
172
  loadJobs();
173
  const id=new URLSearchParams(location.search).get("job"); if(id) watch(id);
174
  }
@@ -177,10 +239,12 @@ $("#go").onclick=async()=>{
177
  $("#formErr").textContent="";
178
  const fd=new FormData();
179
  if(mode==="link"){ if(!$("#url").value.trim()) return $("#formErr").textContent="Paste a link first."; fd.append("url",$("#url").value.trim()); }
180
- else { if(!file) return $("#formErr").textContent="Choose a file first."; fd.append("file",file); }
 
181
  const outs=["text",...[...document.querySelectorAll(".out:checked")].map(c=>c.value)];
182
  Object.entries({target:$("#tgt").value,source:$("#src").value,engine:$("#engine").value,outputs:outs.join(","),
183
- quality:$("#quality").value,gender:$("#gender").value,voice_engine:$("#voice").value}).forEach(([k,v])=>fd.append(k,v));
 
184
  $("#go").disabled=true; $("#go").textContent="Uploading…";
185
  try{
186
  const r=await fetch("/api/jobs",{method:"POST",body:fd}); const j=await r.json();
@@ -205,10 +269,11 @@ async function watch(id){
205
  function show(j){
206
  const r=j.result, f=r.files, url=n=>`/api/files/${j.id}/${encodeURIComponent(n)}`;
207
  $("#langs").innerHTML=`<span class="pill">${esc(r.source_name)} → ${esc(r.target_name)}</span><span class="pill">${fmt(r.duration)}</span>`
 
208
  + (r.auto_detected && r.detect_confidence < 0.85 ? `<p class="err">Heard ${esc(r.source_name)}, but only ${Math.round(r.detect_confidence*100)}% sure. If that's wrong, set "Spoken language" above and translate again. Mixed-language speech (e.g. Somali with English words) often fools auto-detect.</p>` : "");
209
- const main=f.voiceover||f.subtitled;
210
  $("#player").innerHTML = main ? (main.endsWith(".m4a")?`<audio controls src="${url(main)}"></audio>`:`<video controls playsinline src="${url(main)}"></video>`) : "";
211
- const labels={voiceover:"Voice-over",subtitled:"Subtitled video",translated_srt:"Translated subtitles (.srt)",original_srt:"Original subtitles (.srt)",transcript:"Side-by-side transcript (.txt)",original:"Original media"};
212
  $("#downloads").innerHTML=Object.entries(labels).filter(([k])=>f[k]).map(([k,l])=>`<a class="btn ghost" href="${url(f[k])}?download=true">${l}</a>`).join("");
213
  $("#thO").textContent="Before — "+r.source_name; $("#thT").textContent="After — "+r.target_name;
214
  CUR=j.id;
@@ -241,6 +306,41 @@ async function loadJobs(){
241
  $("#reqForm").onsubmit=async e=>{ e.preventDefault();
242
  const r=await fetch("/api/language-requests",{method:"POST",body:new FormData(e.target)});
243
  $("#reqMsg").textContent=r.ok?"Thanks, request saved.":"Couldn't save, try again."; if(r.ok) e.target.reset(); };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  init();
245
  </script>
246
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <title>SwitchLingo — Any video, your language</title>
7
  <meta name="description" content="SwitchLingo: AI that translates any video or link into your language with subtitles and voice-over in 100+ languages, and gets better from your corrections.">
8
+ <link rel="manifest" href="manifest.webmanifest">
9
+ <meta name="theme-color" content="#1d1628">
10
+ <link rel="apple-touch-icon" href="icon-192.png">
11
+ <link rel="icon" href="icon-192.png">
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
14
  <style>
 
59
  .fix{display:flex;gap:6px;margin-top:6px}.fix textarea{min-height:44px;font-size:14px}.fix button{border:0;background:var(--accent);color:var(--accent-ink);border-radius:8px;padding:0 12px;font-weight:600;cursor:pointer}
60
  .human{display:inline-block;font-size:11px;background:var(--soft);color:var(--ok);border-radius:99px;padding:1px 8px;margin-left:6px}
61
  .ok{color:var(--ok);font-size:12px}
62
+ .sw{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--soft);padding:12px 14px;border-radius:12px;margin-top:10px;cursor:pointer}
63
+ .sw b{display:block;font-weight:600}.sw small{color:var(--muted)}
64
+ .sw input{appearance:none;width:46px;height:28px;border-radius:99px;background:var(--line);position:relative;flex:none;cursor:pointer;transition:.2s}
65
+ .sw input:checked{background:var(--accent)}
66
+ .sw input::after{content:"";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:#fff;transition:.2s}
67
+ .sw input:checked::after{left:21px}
68
+ .badge{display:inline-block;font-size:11px;font-weight:600;background:var(--accent);color:var(--accent-ink);border-radius:99px;padding:1px 8px;margin-left:6px;vertical-align:middle}
69
+ .rec{background:var(--soft);color:var(--ink)}.rec.on{background:var(--bad);color:#fff}
70
+ .prompt{font:600 18px Fraunces,Georgia,serif;margin:6px 0 10px}
71
+ #install{display:none}
72
+ .sw.hidden{display:none}
73
+ @media (max-width:640px){
74
+ .wrap{padding:18px 12px 90px}h1{font-size:30px}.card{padding:16px;border-radius:14px}
75
+ .btn{width:100%;justify-content:center;padding:15px}.btn.ghost{width:auto}
76
+ input[type=text],input[type=url],select,textarea{font-size:16px;padding:13px}
77
+ table thead{display:none}table,tbody,tr,td{display:block;width:100%}
78
+ tr{border-bottom:1px solid var(--line);padding:10px 0}td{border:0;padding:3px 0}
79
+ td.t{width:auto}.fb{width:auto}.fb button{padding:8px 14px;font-size:17px;margin:6px 6px 0 0}
80
+ }
81
  </style>
82
  </head>
83
  <body>
84
  <div class="wrap">
85
+ <header><h1>Switch<span>Lingo</span></h1><button id="install" class="btn ghost" style="margin:0 0 0 auto">📲 Add to phone</button><div class="tag">AI video translation. Subtitles and voice-over in 100+ languages, and it learns from your fixes.</div></header>
86
 
87
  <section class="card demo">
88
  <h2>See it switch</h2>
 
92
 
93
  <section class="card">
94
  <h2>1. Add your video or audio</h2>
95
+ <div class="tabs"><button id="tLink" class="on">🔗 Link</button><button id="tFile">📁 Upload</button><button id="tRec">🎤 Record</button></div>
96
+ <div id="pLink"><input id="url" type="url" inputmode="url" placeholder="Facebook, Instagram, TikTok, YouTube, X, or any website with a video"></div>
97
  <div id="pFile" class="hidden">
98
+ <div id="drop" class="drop">Tap to choose a video or voice recording<br><small class="muted">mp4, mov, m4a, mp3, wav, amr, 3gp, ogg, opus…</small><br><small id="fname"></small></div>
99
+ <input id="file" type="file" accept="video/*,audio/*,.m4a,.mp3,.wav,.amr,.3gp,.3ga,.aac,.ogg,.oga,.opus,.flac,.wma,.webm,.mkv,.mov,.mp4" hidden>
100
+ </div>
101
+ <div id="pRec" class="hidden">
102
+ <div class="drop" id="recMain">🎤 Tap to start recording<br><small class="muted">Speak or hold your phone near the speaker. Up to 10 minutes.</small></div>
103
+ <audio id="recMainPlay" controls class="hidden"></audio>
104
  </div>
105
 
106
  <div class="row">
 
113
  <label class="chk"><input type="checkbox" class="out" value="subs" checked> Subtitles on video</label>
114
  <label class="chk"><input type="checkbox" class="out" value="dub" checked> Voice-over</label>
115
  </div>
116
+ <label class="sw"><span><b>🎙️ Keep the speaker's voice</b><small>Voice-over sounds like the person in the video</small></span><input type="checkbox" id="clone" checked></label>
117
+ <label class="sw hidden" id="lipWrap"><span><b>👄 Lip-sync <span class="badge">PRO</span></b><small>Mouths move with the new language (takes ~2 min longer)</small></span><input type="checkbox" id="lipsync"></label>
118
 
119
  <details>
120
  <summary>Options</summary>
 
127
  </div>
128
  </details>
129
  <button id="go" class="btn">Translate</button>
130
+ <p class="muted" style="font-size:13px;margin:10px 0 0"><span id="limits"></span> Only use videos you have the right to share. AI voices, even cloned ones, are never the real person speaking. Get permission before reposting.</p>
131
  <div id="formErr" class="err"></div>
132
  </section>
133
 
 
152
  <div id="jobs" class="jobs muted">Nothing yet.</div>
153
  </section>
154
 
155
+ <section class="card" id="teach">
156
+ <h2>🇱🇦 Teach SwitchLingo your language</h2>
157
+ <p class="muted" style="margin-top:-6px">Big apps barely know Lao, Isan or Hmong. Every sentence you add makes SwitchLingo better for your family and community. Takes 20 seconds.</p>
158
+ <form id="teachForm">
159
+ <label class="lb" for="tlang">Language</label><select id="tlang" name="lang"></select>
160
+ <div style="margin-top:12px"><label class="lb">Say this in your language <a href="#" id="nextP" style="float:right">another one ↻</a></label>
161
+ <div class="prompt" id="tprompt"></div><input type="hidden" name="en" id="ten"></div>
162
+ <label class="lb" for="ttext">Type it (your own spelling is fine)</label><textarea id="ttext" name="text" required></textarea>
163
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px">
164
+ <button type="button" class="btn ghost rec" id="recBtn">🎤 Record yourself saying it</button><audio id="recPlay" controls class="hidden" style="max-width:260px;margin:0"></audio>
165
+ </div>
166
+ <button class="btn" type="submit">Add my sentence</button> <span id="teachMsg" class="muted"></span>
167
+ </form>
168
+ <p class="muted" style="font-size:13px" id="teachStats"></p>
169
+ </section>
170
+
171
  <section class="card">
172
  <h2>Don't see your language?</h2>
173
  <p class="muted" style="margin-top:-6px">Tell us what to add. Requests help decide which languages and dialects get trained next.</p>
 
188
  const esc = s => (s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
189
  const fmt = s => { s = Math.floor(s); return `${Math.floor(s/60)}:${String(s%60).padStart(2,"0")}`; };
190
 
191
+ function setMode(m){ mode=m; [["link","Link"],["file","File"],["rec","Rec"]].forEach(([k,n])=>{
192
+ $("#t"+n).classList.toggle("on",m===k); $("#p"+n).classList.toggle("hidden",m!==k); }); }
193
+ $("#tLink").onclick=()=>setMode("link"); $("#tFile").onclick=()=>setMode("file"); $("#tRec").onclick=()=>setMode("rec");
194
+ let mainRec=null, mainBlob=null, recTimer=null;
195
+ $("#recMain").onclick=async()=>{
196
+ if(mainRec&&mainRec.state==="recording"){ mainRec.stop(); return; }
197
+ try{
198
+ const st=await navigator.mediaDevices.getUserMedia({audio:true}); const ch=[]; const t0=Date.now();
199
+ mainRec=new MediaRecorder(st); mainRec.ondataavailable=e=>ch.push(e.data);
200
+ mainRec.onstop=()=>{ clearInterval(recTimer); st.getTracks().forEach(t=>t.stop()); mainBlob=new Blob(ch,{type:mainRec.mimeType});
201
+ $("#recMainPlay").src=URL.createObjectURL(mainBlob); $("#recMainPlay").classList.remove("hidden");
202
+ $("#recMain").innerHTML="✅ Recorded. Tap to record again"; };
203
+ mainRec.start(); recTimer=setInterval(()=>{ const s=Math.floor((Date.now()-t0)/1000);
204
+ $("#recMain").innerHTML=`⏹ Recording ${fmt(s)}… tap to stop`; if(s>=600) mainRec.stop(); },500);
205
+ }catch(e){ $("#formErr").textContent="Microphone not allowed. Check your browser settings."; }
206
+ };
207
  const drop=$("#drop");
208
  drop.onclick=()=>$("#file").click();
209
  $("#file").onchange=e=>{file=e.target.files[0]; $("#fname").textContent=file?file.name:"";};
 
227
  r.voices.forEach(v=>$("#voice").insertAdjacentHTML("beforeend",`<option value="${v}">${vn[v]||v}</option>`));
228
  if(r.voices.includes("elevenlabs")) $("#voice").value="elevenlabs";
229
  if(r.translators.includes("claude")) $("#engine").value="claude";
230
+ if(r.lipsync) $("#lipWrap").classList.remove("hidden");
231
+ const qs=new URLSearchParams(location.search), shared=qs.get("url")||qs.get("text")||"";
232
+ const m=shared.match(/https?:\/\/\S+/); if(m){ setMode("link"); $("#url").value=m[0]; $("#go").scrollIntoView({behavior:"smooth",block:"center"}); }
233
+ initTeach();
234
  loadJobs();
235
  const id=new URLSearchParams(location.search).get("job"); if(id) watch(id);
236
  }
 
239
  $("#formErr").textContent="";
240
  const fd=new FormData();
241
  if(mode==="link"){ if(!$("#url").value.trim()) return $("#formErr").textContent="Paste a link first."; fd.append("url",$("#url").value.trim()); }
242
+ else if(mode==="file"){ if(!file) return $("#formErr").textContent="Choose a file first."; fd.append("file",file); }
243
+ else { if(!mainBlob) return $("#formErr").textContent="Record something first."; fd.append("file",mainBlob,"recording."+((mainBlob.type.split("/")[1]||"webm").split(";")[0])); }
244
  const outs=["text",...[...document.querySelectorAll(".out:checked")].map(c=>c.value)];
245
  Object.entries({target:$("#tgt").value,source:$("#src").value,engine:$("#engine").value,outputs:outs.join(","),
246
+ quality:$("#quality").value,gender:$("#gender").value,voice_engine:$("#voice").value,
247
+ clone:$("#clone").checked?"1":"0",lipsync:$("#lipsync").checked?"1":"0"}).forEach(([k,v])=>fd.append(k,v));
248
  $("#go").disabled=true; $("#go").textContent="Uploading…";
249
  try{
250
  const r=await fetch("/api/jobs",{method:"POST",body:fd}); const j=await r.json();
 
269
  function show(j){
270
  const r=j.result, f=r.files, url=n=>`/api/files/${j.id}/${encodeURIComponent(n)}`;
271
  $("#langs").innerHTML=`<span class="pill">${esc(r.source_name)} → ${esc(r.target_name)}</span><span class="pill">${fmt(r.duration)}</span>`
272
+ + (r.voice_cloned?'<span class="pill">🎙️ speaker\'s voice</span>':'') + (f.lipsync?'<span class="pill">👄 lip-synced</span>':'')
273
  + (r.auto_detected && r.detect_confidence < 0.85 ? `<p class="err">Heard ${esc(r.source_name)}, but only ${Math.round(r.detect_confidence*100)}% sure. If that's wrong, set "Spoken language" above and translate again. Mixed-language speech (e.g. Somali with English words) often fools auto-detect.</p>` : "");
274
+ const main=f.lipsync||f.voiceover||f.subtitled;
275
  $("#player").innerHTML = main ? (main.endsWith(".m4a")?`<audio controls src="${url(main)}"></audio>`:`<video controls playsinline src="${url(main)}"></video>`) : "";
276
+ const labels={lipsync:"Lip-synced video",voiceover:"Voice-over",subtitled:"Subtitled video",translated_srt:"Translated subtitles (.srt)",original_srt:"Original subtitles (.srt)",transcript:"Side-by-side transcript (.txt)",original:"Original media"};
277
  $("#downloads").innerHTML=Object.entries(labels).filter(([k])=>f[k]).map(([k,l])=>`<a class="btn ghost" href="${url(f[k])}?download=true">${l}</a>`).join("");
278
  $("#thO").textContent="Before — "+r.source_name; $("#thT").textContent="After — "+r.target_name;
279
  CUR=j.id;
 
306
  $("#reqForm").onsubmit=async e=>{ e.preventDefault();
307
  const r=await fetch("/api/language-requests",{method:"POST",body:new FormData(e.target)});
308
  $("#reqMsg").textContent=r.ok?"Thanks, request saved.":"Couldn't save, try again."; if(r.ok) e.target.reset(); };
309
+ let recBlob=null, recorder=null, PROMPTS=[];
310
+ async function initTeach(){
311
+ const r=await (await fetch("/api/teach/languages")).json(); PROMPTS=r.prompts;
312
+ $("#tlang").innerHTML=Object.entries(r.languages).map(([k,v])=>`<option value="${k}">${esc(v)}</option>`).join("");
313
+ nextPrompt(); teachStats();
314
+ }
315
+ function nextPrompt(){ const p=PROMPTS[Math.floor(Math.random()*PROMPTS.length)]; $("#tprompt").textContent="“"+p+"”"; $("#ten").value=p; }
316
+ $("#nextP").onclick=e=>{e.preventDefault(); nextPrompt();};
317
+ async function teachStats(){ try{ const s=await (await fetch("/api/feedback/stats")).json();
318
+ const by=Object.entries(s.pairs_by_language||{}).map(([k,v])=>`${esc(k)} ${v}`).join(" · ");
319
+ $("#teachStats").textContent=`${s.sentence_pairs||0} sentences taught so far${by?" ("+by+")":""} · ${s.corrections||0} fixes from viewers.`; }catch(e){} }
320
+ $("#recBtn").onclick=async()=>{
321
+ if(recorder&&recorder.state==="recording"){ recorder.stop(); return; }
322
+ try{
323
+ const st=await navigator.mediaDevices.getUserMedia({audio:true}); const ch=[];
324
+ recorder=new MediaRecorder(st); recorder.ondataavailable=e=>ch.push(e.data);
325
+ recorder.onstop=()=>{ st.getTracks().forEach(t=>t.stop()); recBlob=new Blob(ch,{type:recorder.mimeType});
326
+ $("#recPlay").src=URL.createObjectURL(recBlob); $("#recPlay").classList.remove("hidden");
327
+ $("#recBtn").classList.remove("on"); $("#recBtn").textContent="🎤 Record again"; };
328
+ recorder.start(); $("#recBtn").classList.add("on"); $("#recBtn").textContent="⏹ Stop recording";
329
+ setTimeout(()=>recorder&&recorder.state==="recording"&&recorder.stop(),30000);
330
+ }catch(e){ $("#teachMsg").textContent="Microphone not allowed. You can still type it."; }
331
+ };
332
+ $("#teachForm").onsubmit=async e=>{ e.preventDefault();
333
+ const fd=new FormData(e.target); fd.append("token",ADMIN);
334
+ if(recBlob) fd.append("audio",recBlob,"rec."+((recBlob.type.split("/")[1]||"webm").split(";")[0]));
335
+ const r=await fetch("/api/teach",{method:"POST",body:fd});
336
+ $("#teachMsg").textContent=r.ok?"Thank you! 🙏 Added.":"Couldn't save, try again.";
337
+ if(r.ok){ $("#ttext").value=""; recBlob=null; $("#recPlay").classList.add("hidden"); $("#recBtn").textContent="🎤 Record yourself saying it"; nextPrompt(); teachStats(); } };
338
+
339
+ // phone: installable app + share-to-SwitchLingo
340
+ if("serviceWorker" in navigator) navigator.serviceWorker.register("sw.js").catch(()=>{});
341
+ let deferred=null;
342
+ window.addEventListener("beforeinstallprompt",e=>{ e.preventDefault(); deferred=e; $("#install").style.display="inline-flex"; });
343
+ $("#install").onclick=async()=>{ if(!deferred) return; deferred.prompt(); await deferred.userChoice; deferred=null; $("#install").style.display="none"; };
344
  init();
345
  </script>
346
  </body>
app/static/manifest.webmanifest ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "SwitchLingo — any video, your language",
3
+ "short_name": "SwitchLingo",
4
+ "description": "Translate any video into your language with subtitles and a voice that sounds like the speaker.",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#1d1628",
9
+ "theme_color": "#1d1628",
10
+ "icons": [
11
+ {"src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable"},
12
+ {"src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
13
+ ],
14
+ "share_target": {
15
+ "action": "/",
16
+ "method": "GET",
17
+ "params": {"title": "title", "text": "text", "url": "url"}
18
+ }
19
+ }
app/static/sw.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ // Minimal service worker: makes SwitchLingo installable; always uses the live site (no stale cache).
2
+ self.addEventListener("install", () => self.skipWaiting());
3
+ self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
4
+ self.addEventListener("fetch", () => {});
app/voiceclone/__init__.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Free voice cloning for every language.
2
+
3
+ The voice-over is first spoken by a normal neural voice in the target language, then OpenVoice V2's
4
+ tone-color converter (MIT, runs on CPU) re-colors it so it sounds like the person in the original
5
+ video. Because it converts audio instead of generating speech, it works for any language,
6
+ including Lao, Somali and Khmer.
7
+
8
+ Each line takes its voice from what the speaker sounded like at that moment, smoothed with nearby
9
+ lines that sound like the same person, so videos with two or three speakers keep them apart.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ from functools import lru_cache
15
+
16
+ import numpy as np
17
+
18
+ SR = 22050
19
+ _lock = threading.Lock()
20
+
21
+
22
+ @lru_cache(maxsize=1)
23
+ def _model():
24
+ import torch
25
+ from huggingface_hub import hf_hub_download
26
+ from .ov import utils
27
+ from .ov.models import SynthesizerTrn
28
+ cfg = hf_hub_download("myshell-ai/OpenVoiceV2", "converter/config.json")
29
+ ckpt = hf_hub_download("myshell-ai/OpenVoiceV2", "converter/checkpoint.pth")
30
+ hps = utils.get_hparams_from_file(cfg)
31
+ m = SynthesizerTrn(len(getattr(hps, "symbols", [])), hps.data.filter_length // 2 + 1,
32
+ n_speakers=hps.data.n_speakers, **hps.model)
33
+ m.load_state_dict(torch.load(ckpt, map_location="cpu")["model"], strict=False)
34
+ m.eval()
35
+ torch.set_num_threads(max(1, min(8, torch.get_num_threads())))
36
+ return m, hps
37
+
38
+
39
+ def _spec(y: np.ndarray):
40
+ import torch
41
+ from .ov.mel_processing import spectrogram_torch
42
+ _, hps = _model()
43
+ t = torch.FloatTensor(y).unsqueeze(0)
44
+ return spectrogram_torch(t, hps.data.filter_length, hps.data.sampling_rate, hps.data.hop_length,
45
+ hps.data.win_length, center=False)
46
+
47
+
48
+ def embed(y: np.ndarray):
49
+ """Speaker embedding of a mono float clip at 22.05 kHz."""
50
+ import torch
51
+ m, _ = _model()
52
+ with torch.no_grad():
53
+ return m.ref_enc(_spec(y).transpose(1, 2)).unsqueeze(-1)
54
+
55
+
56
+ def convert(y: np.ndarray, src_se, tgt_se, tau: float = 0.3) -> np.ndarray:
57
+ import torch
58
+ m, _ = _model()
59
+ with _lock, torch.no_grad():
60
+ spec = _spec(y)
61
+ out = m.voice_conversion(spec, torch.LongTensor([spec.size(-1)]), sid_src=src_se,
62
+ sid_tgt=tgt_se, tau=tau)[0][0, 0]
63
+ return out.float().numpy()
64
+
65
+
66
+ def speaker_embeddings(original: np.ndarray, spans: list[tuple[float, float]], min_len: float = 3.0):
67
+ """One target voice per line: that moment of the original, widened to >=3s, then averaged
68
+ with other lines that sound like the same speaker (cosine > 0.85)."""
69
+ import torch
70
+ raw = []
71
+ for s, e in spans:
72
+ if e - s < min_len:
73
+ pad = (min_len - (e - s)) / 2
74
+ s, e = max(0.0, s - pad), e + pad
75
+ clip = original[int(s * SR): int(e * SR)]
76
+ raw.append(embed(clip) if len(clip) > SR // 2 else None)
77
+ good = [r for r in raw if r is not None]
78
+ if not good:
79
+ return [None] * len(spans)
80
+ glob = torch.stack(good).mean(0)
81
+ raw = [r if r is not None else glob for r in raw]
82
+ flat = torch.stack([r.flatten() for r in raw])
83
+ flat = flat / flat.norm(dim=1, keepdim=True)
84
+ sim = flat @ flat.T
85
+ return [torch.stack([raw[j] for j in range(len(raw)) if sim[i, j] > 0.85]).mean(0)
86
+ for i in range(len(raw))]
app/voiceclone/ov/LICENSE ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Copyright 2024 MyShell.ai
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
app/voiceclone/ov/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Vendored from myshell-ai/OpenVoice (MIT). Only the tone-color converter parts."""
app/voiceclone/ov/attentions.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from torch import nn
4
+ from torch.nn import functional as F
5
+
6
+ from . import commons
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LayerNorm(nn.Module):
13
+ def __init__(self, channels, eps=1e-5):
14
+ super().__init__()
15
+ self.channels = channels
16
+ self.eps = eps
17
+
18
+ self.gamma = nn.Parameter(torch.ones(channels))
19
+ self.beta = nn.Parameter(torch.zeros(channels))
20
+
21
+ def forward(self, x):
22
+ x = x.transpose(1, -1)
23
+ x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
24
+ return x.transpose(1, -1)
25
+
26
+
27
+ @torch.jit.script
28
+ def fused_add_tanh_sigmoid_multiply(input_a, input_b, n_channels):
29
+ n_channels_int = n_channels[0]
30
+ in_act = input_a + input_b
31
+ t_act = torch.tanh(in_act[:, :n_channels_int, :])
32
+ s_act = torch.sigmoid(in_act[:, n_channels_int:, :])
33
+ acts = t_act * s_act
34
+ return acts
35
+
36
+
37
+ class Encoder(nn.Module):
38
+ def __init__(
39
+ self,
40
+ hidden_channels,
41
+ filter_channels,
42
+ n_heads,
43
+ n_layers,
44
+ kernel_size=1,
45
+ p_dropout=0.0,
46
+ window_size=4,
47
+ isflow=True,
48
+ **kwargs
49
+ ):
50
+ super().__init__()
51
+ self.hidden_channels = hidden_channels
52
+ self.filter_channels = filter_channels
53
+ self.n_heads = n_heads
54
+ self.n_layers = n_layers
55
+ self.kernel_size = kernel_size
56
+ self.p_dropout = p_dropout
57
+ self.window_size = window_size
58
+ # if isflow:
59
+ # cond_layer = torch.nn.Conv1d(256, 2*hidden_channels*n_layers, 1)
60
+ # self.cond_pre = torch.nn.Conv1d(hidden_channels, 2*hidden_channels, 1)
61
+ # self.cond_layer = weight_norm(cond_layer, name='weight')
62
+ # self.gin_channels = 256
63
+ self.cond_layer_idx = self.n_layers
64
+ if "gin_channels" in kwargs:
65
+ self.gin_channels = kwargs["gin_channels"]
66
+ if self.gin_channels != 0:
67
+ self.spk_emb_linear = nn.Linear(self.gin_channels, self.hidden_channels)
68
+ # vits2 says 3rd block, so idx is 2 by default
69
+ self.cond_layer_idx = (
70
+ kwargs["cond_layer_idx"] if "cond_layer_idx" in kwargs else 2
71
+ )
72
+ # logging.debug(self.gin_channels, self.cond_layer_idx)
73
+ assert (
74
+ self.cond_layer_idx < self.n_layers
75
+ ), "cond_layer_idx should be less than n_layers"
76
+ self.drop = nn.Dropout(p_dropout)
77
+ self.attn_layers = nn.ModuleList()
78
+ self.norm_layers_1 = nn.ModuleList()
79
+ self.ffn_layers = nn.ModuleList()
80
+ self.norm_layers_2 = nn.ModuleList()
81
+
82
+ for i in range(self.n_layers):
83
+ self.attn_layers.append(
84
+ MultiHeadAttention(
85
+ hidden_channels,
86
+ hidden_channels,
87
+ n_heads,
88
+ p_dropout=p_dropout,
89
+ window_size=window_size,
90
+ )
91
+ )
92
+ self.norm_layers_1.append(LayerNorm(hidden_channels))
93
+ self.ffn_layers.append(
94
+ FFN(
95
+ hidden_channels,
96
+ hidden_channels,
97
+ filter_channels,
98
+ kernel_size,
99
+ p_dropout=p_dropout,
100
+ )
101
+ )
102
+ self.norm_layers_2.append(LayerNorm(hidden_channels))
103
+
104
+ def forward(self, x, x_mask, g=None):
105
+ attn_mask = x_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
106
+ x = x * x_mask
107
+ for i in range(self.n_layers):
108
+ if i == self.cond_layer_idx and g is not None:
109
+ g = self.spk_emb_linear(g.transpose(1, 2))
110
+ g = g.transpose(1, 2)
111
+ x = x + g
112
+ x = x * x_mask
113
+ y = self.attn_layers[i](x, x, attn_mask)
114
+ y = self.drop(y)
115
+ x = self.norm_layers_1[i](x + y)
116
+
117
+ y = self.ffn_layers[i](x, x_mask)
118
+ y = self.drop(y)
119
+ x = self.norm_layers_2[i](x + y)
120
+ x = x * x_mask
121
+ return x
122
+
123
+
124
+ class Decoder(nn.Module):
125
+ def __init__(
126
+ self,
127
+ hidden_channels,
128
+ filter_channels,
129
+ n_heads,
130
+ n_layers,
131
+ kernel_size=1,
132
+ p_dropout=0.0,
133
+ proximal_bias=False,
134
+ proximal_init=True,
135
+ **kwargs
136
+ ):
137
+ super().__init__()
138
+ self.hidden_channels = hidden_channels
139
+ self.filter_channels = filter_channels
140
+ self.n_heads = n_heads
141
+ self.n_layers = n_layers
142
+ self.kernel_size = kernel_size
143
+ self.p_dropout = p_dropout
144
+ self.proximal_bias = proximal_bias
145
+ self.proximal_init = proximal_init
146
+
147
+ self.drop = nn.Dropout(p_dropout)
148
+ self.self_attn_layers = nn.ModuleList()
149
+ self.norm_layers_0 = nn.ModuleList()
150
+ self.encdec_attn_layers = nn.ModuleList()
151
+ self.norm_layers_1 = nn.ModuleList()
152
+ self.ffn_layers = nn.ModuleList()
153
+ self.norm_layers_2 = nn.ModuleList()
154
+ for i in range(self.n_layers):
155
+ self.self_attn_layers.append(
156
+ MultiHeadAttention(
157
+ hidden_channels,
158
+ hidden_channels,
159
+ n_heads,
160
+ p_dropout=p_dropout,
161
+ proximal_bias=proximal_bias,
162
+ proximal_init=proximal_init,
163
+ )
164
+ )
165
+ self.norm_layers_0.append(LayerNorm(hidden_channels))
166
+ self.encdec_attn_layers.append(
167
+ MultiHeadAttention(
168
+ hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout
169
+ )
170
+ )
171
+ self.norm_layers_1.append(LayerNorm(hidden_channels))
172
+ self.ffn_layers.append(
173
+ FFN(
174
+ hidden_channels,
175
+ hidden_channels,
176
+ filter_channels,
177
+ kernel_size,
178
+ p_dropout=p_dropout,
179
+ causal=True,
180
+ )
181
+ )
182
+ self.norm_layers_2.append(LayerNorm(hidden_channels))
183
+
184
+ def forward(self, x, x_mask, h, h_mask):
185
+ """
186
+ x: decoder input
187
+ h: encoder output
188
+ """
189
+ self_attn_mask = commons.subsequent_mask(x_mask.size(2)).to(
190
+ device=x.device, dtype=x.dtype
191
+ )
192
+ encdec_attn_mask = h_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
193
+ x = x * x_mask
194
+ for i in range(self.n_layers):
195
+ y = self.self_attn_layers[i](x, x, self_attn_mask)
196
+ y = self.drop(y)
197
+ x = self.norm_layers_0[i](x + y)
198
+
199
+ y = self.encdec_attn_layers[i](x, h, encdec_attn_mask)
200
+ y = self.drop(y)
201
+ x = self.norm_layers_1[i](x + y)
202
+
203
+ y = self.ffn_layers[i](x, x_mask)
204
+ y = self.drop(y)
205
+ x = self.norm_layers_2[i](x + y)
206
+ x = x * x_mask
207
+ return x
208
+
209
+
210
+ class MultiHeadAttention(nn.Module):
211
+ def __init__(
212
+ self,
213
+ channels,
214
+ out_channels,
215
+ n_heads,
216
+ p_dropout=0.0,
217
+ window_size=None,
218
+ heads_share=True,
219
+ block_length=None,
220
+ proximal_bias=False,
221
+ proximal_init=False,
222
+ ):
223
+ super().__init__()
224
+ assert channels % n_heads == 0
225
+
226
+ self.channels = channels
227
+ self.out_channels = out_channels
228
+ self.n_heads = n_heads
229
+ self.p_dropout = p_dropout
230
+ self.window_size = window_size
231
+ self.heads_share = heads_share
232
+ self.block_length = block_length
233
+ self.proximal_bias = proximal_bias
234
+ self.proximal_init = proximal_init
235
+ self.attn = None
236
+
237
+ self.k_channels = channels // n_heads
238
+ self.conv_q = nn.Conv1d(channels, channels, 1)
239
+ self.conv_k = nn.Conv1d(channels, channels, 1)
240
+ self.conv_v = nn.Conv1d(channels, channels, 1)
241
+ self.conv_o = nn.Conv1d(channels, out_channels, 1)
242
+ self.drop = nn.Dropout(p_dropout)
243
+
244
+ if window_size is not None:
245
+ n_heads_rel = 1 if heads_share else n_heads
246
+ rel_stddev = self.k_channels**-0.5
247
+ self.emb_rel_k = nn.Parameter(
248
+ torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels)
249
+ * rel_stddev
250
+ )
251
+ self.emb_rel_v = nn.Parameter(
252
+ torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels)
253
+ * rel_stddev
254
+ )
255
+
256
+ nn.init.xavier_uniform_(self.conv_q.weight)
257
+ nn.init.xavier_uniform_(self.conv_k.weight)
258
+ nn.init.xavier_uniform_(self.conv_v.weight)
259
+ if proximal_init:
260
+ with torch.no_grad():
261
+ self.conv_k.weight.copy_(self.conv_q.weight)
262
+ self.conv_k.bias.copy_(self.conv_q.bias)
263
+
264
+ def forward(self, x, c, attn_mask=None):
265
+ q = self.conv_q(x)
266
+ k = self.conv_k(c)
267
+ v = self.conv_v(c)
268
+
269
+ x, self.attn = self.attention(q, k, v, mask=attn_mask)
270
+
271
+ x = self.conv_o(x)
272
+ return x
273
+
274
+ def attention(self, query, key, value, mask=None):
275
+ # reshape [b, d, t] -> [b, n_h, t, d_k]
276
+ b, d, t_s, t_t = (*key.size(), query.size(2))
277
+ query = query.view(b, self.n_heads, self.k_channels, t_t).transpose(2, 3)
278
+ key = key.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
279
+ value = value.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
280
+
281
+ scores = torch.matmul(query / math.sqrt(self.k_channels), key.transpose(-2, -1))
282
+ if self.window_size is not None:
283
+ assert (
284
+ t_s == t_t
285
+ ), "Relative attention is only available for self-attention."
286
+ key_relative_embeddings = self._get_relative_embeddings(self.emb_rel_k, t_s)
287
+ rel_logits = self._matmul_with_relative_keys(
288
+ query / math.sqrt(self.k_channels), key_relative_embeddings
289
+ )
290
+ scores_local = self._relative_position_to_absolute_position(rel_logits)
291
+ scores = scores + scores_local
292
+ if self.proximal_bias:
293
+ assert t_s == t_t, "Proximal bias is only available for self-attention."
294
+ scores = scores + self._attention_bias_proximal(t_s).to(
295
+ device=scores.device, dtype=scores.dtype
296
+ )
297
+ if mask is not None:
298
+ scores = scores.masked_fill(mask == 0, -1e4)
299
+ if self.block_length is not None:
300
+ assert (
301
+ t_s == t_t
302
+ ), "Local attention is only available for self-attention."
303
+ block_mask = (
304
+ torch.ones_like(scores)
305
+ .triu(-self.block_length)
306
+ .tril(self.block_length)
307
+ )
308
+ scores = scores.masked_fill(block_mask == 0, -1e4)
309
+ p_attn = F.softmax(scores, dim=-1) # [b, n_h, t_t, t_s]
310
+ p_attn = self.drop(p_attn)
311
+ output = torch.matmul(p_attn, value)
312
+ if self.window_size is not None:
313
+ relative_weights = self._absolute_position_to_relative_position(p_attn)
314
+ value_relative_embeddings = self._get_relative_embeddings(
315
+ self.emb_rel_v, t_s
316
+ )
317
+ output = output + self._matmul_with_relative_values(
318
+ relative_weights, value_relative_embeddings
319
+ )
320
+ output = (
321
+ output.transpose(2, 3).contiguous().view(b, d, t_t)
322
+ ) # [b, n_h, t_t, d_k] -> [b, d, t_t]
323
+ return output, p_attn
324
+
325
+ def _matmul_with_relative_values(self, x, y):
326
+ """
327
+ x: [b, h, l, m]
328
+ y: [h or 1, m, d]
329
+ ret: [b, h, l, d]
330
+ """
331
+ ret = torch.matmul(x, y.unsqueeze(0))
332
+ return ret
333
+
334
+ def _matmul_with_relative_keys(self, x, y):
335
+ """
336
+ x: [b, h, l, d]
337
+ y: [h or 1, m, d]
338
+ ret: [b, h, l, m]
339
+ """
340
+ ret = torch.matmul(x, y.unsqueeze(0).transpose(-2, -1))
341
+ return ret
342
+
343
+ def _get_relative_embeddings(self, relative_embeddings, length):
344
+ 2 * self.window_size + 1
345
+ # Pad first before slice to avoid using cond ops.
346
+ pad_length = max(length - (self.window_size + 1), 0)
347
+ slice_start_position = max((self.window_size + 1) - length, 0)
348
+ slice_end_position = slice_start_position + 2 * length - 1
349
+ if pad_length > 0:
350
+ padded_relative_embeddings = F.pad(
351
+ relative_embeddings,
352
+ commons.convert_pad_shape([[0, 0], [pad_length, pad_length], [0, 0]]),
353
+ )
354
+ else:
355
+ padded_relative_embeddings = relative_embeddings
356
+ used_relative_embeddings = padded_relative_embeddings[
357
+ :, slice_start_position:slice_end_position
358
+ ]
359
+ return used_relative_embeddings
360
+
361
+ def _relative_position_to_absolute_position(self, x):
362
+ """
363
+ x: [b, h, l, 2*l-1]
364
+ ret: [b, h, l, l]
365
+ """
366
+ batch, heads, length, _ = x.size()
367
+ # Concat columns of pad to shift from relative to absolute indexing.
368
+ x = F.pad(x, commons.convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, 1]]))
369
+
370
+ # Concat extra elements so to add up to shape (len+1, 2*len-1).
371
+ x_flat = x.view([batch, heads, length * 2 * length])
372
+ x_flat = F.pad(
373
+ x_flat, commons.convert_pad_shape([[0, 0], [0, 0], [0, length - 1]])
374
+ )
375
+
376
+ # Reshape and slice out the padded elements.
377
+ x_final = x_flat.view([batch, heads, length + 1, 2 * length - 1])[
378
+ :, :, :length, length - 1 :
379
+ ]
380
+ return x_final
381
+
382
+ def _absolute_position_to_relative_position(self, x):
383
+ """
384
+ x: [b, h, l, l]
385
+ ret: [b, h, l, 2*l-1]
386
+ """
387
+ batch, heads, length, _ = x.size()
388
+ # pad along column
389
+ x = F.pad(
390
+ x, commons.convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, length - 1]])
391
+ )
392
+ x_flat = x.view([batch, heads, length**2 + length * (length - 1)])
393
+ # add 0's in the beginning that will skew the elements after reshape
394
+ x_flat = F.pad(x_flat, commons.convert_pad_shape([[0, 0], [0, 0], [length, 0]]))
395
+ x_final = x_flat.view([batch, heads, length, 2 * length])[:, :, :, 1:]
396
+ return x_final
397
+
398
+ def _attention_bias_proximal(self, length):
399
+ """Bias for self-attention to encourage attention to close positions.
400
+ Args:
401
+ length: an integer scalar.
402
+ Returns:
403
+ a Tensor with shape [1, 1, length, length]
404
+ """
405
+ r = torch.arange(length, dtype=torch.float32)
406
+ diff = torch.unsqueeze(r, 0) - torch.unsqueeze(r, 1)
407
+ return torch.unsqueeze(torch.unsqueeze(-torch.log1p(torch.abs(diff)), 0), 0)
408
+
409
+
410
+ class FFN(nn.Module):
411
+ def __init__(
412
+ self,
413
+ in_channels,
414
+ out_channels,
415
+ filter_channels,
416
+ kernel_size,
417
+ p_dropout=0.0,
418
+ activation=None,
419
+ causal=False,
420
+ ):
421
+ super().__init__()
422
+ self.in_channels = in_channels
423
+ self.out_channels = out_channels
424
+ self.filter_channels = filter_channels
425
+ self.kernel_size = kernel_size
426
+ self.p_dropout = p_dropout
427
+ self.activation = activation
428
+ self.causal = causal
429
+
430
+ if causal:
431
+ self.padding = self._causal_padding
432
+ else:
433
+ self.padding = self._same_padding
434
+
435
+ self.conv_1 = nn.Conv1d(in_channels, filter_channels, kernel_size)
436
+ self.conv_2 = nn.Conv1d(filter_channels, out_channels, kernel_size)
437
+ self.drop = nn.Dropout(p_dropout)
438
+
439
+ def forward(self, x, x_mask):
440
+ x = self.conv_1(self.padding(x * x_mask))
441
+ if self.activation == "gelu":
442
+ x = x * torch.sigmoid(1.702 * x)
443
+ else:
444
+ x = torch.relu(x)
445
+ x = self.drop(x)
446
+ x = self.conv_2(self.padding(x * x_mask))
447
+ return x * x_mask
448
+
449
+ def _causal_padding(self, x):
450
+ if self.kernel_size == 1:
451
+ return x
452
+ pad_l = self.kernel_size - 1
453
+ pad_r = 0
454
+ padding = [[0, 0], [0, 0], [pad_l, pad_r]]
455
+ x = F.pad(x, commons.convert_pad_shape(padding))
456
+ return x
457
+
458
+ def _same_padding(self, x):
459
+ if self.kernel_size == 1:
460
+ return x
461
+ pad_l = (self.kernel_size - 1) // 2
462
+ pad_r = self.kernel_size // 2
463
+ padding = [[0, 0], [0, 0], [pad_l, pad_r]]
464
+ x = F.pad(x, commons.convert_pad_shape(padding))
465
+ return x
app/voiceclone/ov/commons.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from torch.nn import functional as F
4
+
5
+
6
+ def init_weights(m, mean=0.0, std=0.01):
7
+ classname = m.__class__.__name__
8
+ if classname.find("Conv") != -1:
9
+ m.weight.data.normal_(mean, std)
10
+
11
+
12
+ def get_padding(kernel_size, dilation=1):
13
+ return int((kernel_size * dilation - dilation) / 2)
14
+
15
+
16
+ def convert_pad_shape(pad_shape):
17
+ layer = pad_shape[::-1]
18
+ pad_shape = [item for sublist in layer for item in sublist]
19
+ return pad_shape
20
+
21
+
22
+ def intersperse(lst, item):
23
+ result = [item] * (len(lst) * 2 + 1)
24
+ result[1::2] = lst
25
+ return result
26
+
27
+
28
+ def kl_divergence(m_p, logs_p, m_q, logs_q):
29
+ """KL(P||Q)"""
30
+ kl = (logs_q - logs_p) - 0.5
31
+ kl += (
32
+ 0.5 * (torch.exp(2.0 * logs_p) + ((m_p - m_q) ** 2)) * torch.exp(-2.0 * logs_q)
33
+ )
34
+ return kl
35
+
36
+
37
+ def rand_gumbel(shape):
38
+ """Sample from the Gumbel distribution, protect from overflows."""
39
+ uniform_samples = torch.rand(shape) * 0.99998 + 0.00001
40
+ return -torch.log(-torch.log(uniform_samples))
41
+
42
+
43
+ def rand_gumbel_like(x):
44
+ g = rand_gumbel(x.size()).to(dtype=x.dtype, device=x.device)
45
+ return g
46
+
47
+
48
+ def slice_segments(x, ids_str, segment_size=4):
49
+ ret = torch.zeros_like(x[:, :, :segment_size])
50
+ for i in range(x.size(0)):
51
+ idx_str = ids_str[i]
52
+ idx_end = idx_str + segment_size
53
+ ret[i] = x[i, :, idx_str:idx_end]
54
+ return ret
55
+
56
+
57
+ def rand_slice_segments(x, x_lengths=None, segment_size=4):
58
+ b, d, t = x.size()
59
+ if x_lengths is None:
60
+ x_lengths = t
61
+ ids_str_max = x_lengths - segment_size + 1
62
+ ids_str = (torch.rand([b]).to(device=x.device) * ids_str_max).to(dtype=torch.long)
63
+ ret = slice_segments(x, ids_str, segment_size)
64
+ return ret, ids_str
65
+
66
+
67
+ def get_timing_signal_1d(length, channels, min_timescale=1.0, max_timescale=1.0e4):
68
+ position = torch.arange(length, dtype=torch.float)
69
+ num_timescales = channels // 2
70
+ log_timescale_increment = math.log(float(max_timescale) / float(min_timescale)) / (
71
+ num_timescales - 1
72
+ )
73
+ inv_timescales = min_timescale * torch.exp(
74
+ torch.arange(num_timescales, dtype=torch.float) * -log_timescale_increment
75
+ )
76
+ scaled_time = position.unsqueeze(0) * inv_timescales.unsqueeze(1)
77
+ signal = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], 0)
78
+ signal = F.pad(signal, [0, 0, 0, channels % 2])
79
+ signal = signal.view(1, channels, length)
80
+ return signal
81
+
82
+
83
+ def add_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4):
84
+ b, channels, length = x.size()
85
+ signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
86
+ return x + signal.to(dtype=x.dtype, device=x.device)
87
+
88
+
89
+ def cat_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4, axis=1):
90
+ b, channels, length = x.size()
91
+ signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
92
+ return torch.cat([x, signal.to(dtype=x.dtype, device=x.device)], axis)
93
+
94
+
95
+ def subsequent_mask(length):
96
+ mask = torch.tril(torch.ones(length, length)).unsqueeze(0).unsqueeze(0)
97
+ return mask
98
+
99
+
100
+ @torch.jit.script
101
+ def fused_add_tanh_sigmoid_multiply(input_a, input_b, n_channels):
102
+ n_channels_int = n_channels[0]
103
+ in_act = input_a + input_b
104
+ t_act = torch.tanh(in_act[:, :n_channels_int, :])
105
+ s_act = torch.sigmoid(in_act[:, n_channels_int:, :])
106
+ acts = t_act * s_act
107
+ return acts
108
+
109
+
110
+ def convert_pad_shape(pad_shape):
111
+ layer = pad_shape[::-1]
112
+ pad_shape = [item for sublist in layer for item in sublist]
113
+ return pad_shape
114
+
115
+
116
+ def shift_1d(x):
117
+ x = F.pad(x, convert_pad_shape([[0, 0], [0, 0], [1, 0]]))[:, :, :-1]
118
+ return x
119
+
120
+
121
+ def sequence_mask(length, max_length=None):
122
+ if max_length is None:
123
+ max_length = length.max()
124
+ x = torch.arange(max_length, dtype=length.dtype, device=length.device)
125
+ return x.unsqueeze(0) < length.unsqueeze(1)
126
+
127
+
128
+ def generate_path(duration, mask):
129
+ """
130
+ duration: [b, 1, t_x]
131
+ mask: [b, 1, t_y, t_x]
132
+ """
133
+
134
+ b, _, t_y, t_x = mask.shape
135
+ cum_duration = torch.cumsum(duration, -1)
136
+
137
+ cum_duration_flat = cum_duration.view(b * t_x)
138
+ path = sequence_mask(cum_duration_flat, t_y).to(mask.dtype)
139
+ path = path.view(b, t_x, t_y)
140
+ path = path - F.pad(path, convert_pad_shape([[0, 0], [1, 0], [0, 0]]))[:, :-1]
141
+ path = path.unsqueeze(1).transpose(2, 3) * mask
142
+ return path
143
+
144
+
145
+ def clip_grad_value_(parameters, clip_value, norm_type=2):
146
+ if isinstance(parameters, torch.Tensor):
147
+ parameters = [parameters]
148
+ parameters = list(filter(lambda p: p.grad is not None, parameters))
149
+ norm_type = float(norm_type)
150
+ if clip_value is not None:
151
+ clip_value = float(clip_value)
152
+
153
+ total_norm = 0
154
+ for p in parameters:
155
+ param_norm = p.grad.data.norm(norm_type)
156
+ total_norm += param_norm.item() ** norm_type
157
+ if clip_value is not None:
158
+ p.grad.data.clamp_(min=-clip_value, max=clip_value)
159
+ total_norm = total_norm ** (1.0 / norm_type)
160
+ return total_norm
app/voiceclone/ov/mel_processing.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.utils.data
3
+ from librosa.filters import mel as librosa_mel_fn
4
+
5
+ MAX_WAV_VALUE = 32768.0
6
+
7
+
8
+ def dynamic_range_compression_torch(x, C=1, clip_val=1e-5):
9
+ """
10
+ PARAMS
11
+ ------
12
+ C: compression factor
13
+ """
14
+ return torch.log(torch.clamp(x, min=clip_val) * C)
15
+
16
+
17
+ def dynamic_range_decompression_torch(x, C=1):
18
+ """
19
+ PARAMS
20
+ ------
21
+ C: compression factor used to compress
22
+ """
23
+ return torch.exp(x) / C
24
+
25
+
26
+ def spectral_normalize_torch(magnitudes):
27
+ output = dynamic_range_compression_torch(magnitudes)
28
+ return output
29
+
30
+
31
+ def spectral_de_normalize_torch(magnitudes):
32
+ output = dynamic_range_decompression_torch(magnitudes)
33
+ return output
34
+
35
+
36
+ mel_basis = {}
37
+ hann_window = {}
38
+
39
+
40
+ def spectrogram_torch(y, n_fft, sampling_rate, hop_size, win_size, center=False):
41
+ if torch.min(y) < -1.1:
42
+ print("min value is ", torch.min(y))
43
+ if torch.max(y) > 1.1:
44
+ print("max value is ", torch.max(y))
45
+
46
+ global hann_window
47
+ dtype_device = str(y.dtype) + "_" + str(y.device)
48
+ wnsize_dtype_device = str(win_size) + "_" + dtype_device
49
+ if wnsize_dtype_device not in hann_window:
50
+ hann_window[wnsize_dtype_device] = torch.hann_window(win_size).to(
51
+ dtype=y.dtype, device=y.device
52
+ )
53
+
54
+ y = torch.nn.functional.pad(
55
+ y.unsqueeze(1),
56
+ (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)),
57
+ mode="reflect",
58
+ )
59
+ y = y.squeeze(1)
60
+
61
+ spec = torch.stft(
62
+ y,
63
+ n_fft,
64
+ hop_length=hop_size,
65
+ win_length=win_size,
66
+ window=hann_window[wnsize_dtype_device],
67
+ center=center,
68
+ pad_mode="reflect",
69
+ normalized=False,
70
+ onesided=True,
71
+ return_complex=False,
72
+ )
73
+
74
+ spec = torch.sqrt(spec.pow(2).sum(-1) + 1e-6)
75
+ return spec
76
+
77
+
78
+ def spectrogram_torch_conv(y, n_fft, sampling_rate, hop_size, win_size, center=False):
79
+ # if torch.min(y) < -1.:
80
+ # print('min value is ', torch.min(y))
81
+ # if torch.max(y) > 1.:
82
+ # print('max value is ', torch.max(y))
83
+
84
+ global hann_window
85
+ dtype_device = str(y.dtype) + '_' + str(y.device)
86
+ wnsize_dtype_device = str(win_size) + '_' + dtype_device
87
+ if wnsize_dtype_device not in hann_window:
88
+ hann_window[wnsize_dtype_device] = torch.hann_window(win_size).to(dtype=y.dtype, device=y.device)
89
+
90
+ y = torch.nn.functional.pad(y.unsqueeze(1), (int((n_fft-hop_size)/2), int((n_fft-hop_size)/2)), mode='reflect')
91
+
92
+ # ******************** original ************************#
93
+ # y = y.squeeze(1)
94
+ # spec1 = torch.stft(y, n_fft, hop_length=hop_size, win_length=win_size, window=hann_window[wnsize_dtype_device],
95
+ # center=center, pad_mode='reflect', normalized=False, onesided=True, return_complex=False)
96
+
97
+ # ******************** ConvSTFT ************************#
98
+ freq_cutoff = n_fft // 2 + 1
99
+ fourier_basis = torch.view_as_real(torch.fft.fft(torch.eye(n_fft)))
100
+ forward_basis = fourier_basis[:freq_cutoff].permute(2, 0, 1).reshape(-1, 1, fourier_basis.shape[1])
101
+ forward_basis = forward_basis * torch.as_tensor(librosa.util.pad_center(torch.hann_window(win_size), size=n_fft)).float()
102
+
103
+ import torch.nn.functional as F
104
+
105
+ # if center:
106
+ # signal = F.pad(y[:, None, None, :], (n_fft // 2, n_fft // 2, 0, 0), mode = 'reflect').squeeze(1)
107
+ assert center is False
108
+
109
+ forward_transform_squared = F.conv1d(y, forward_basis.to(y.device), stride = hop_size)
110
+ spec2 = torch.stack([forward_transform_squared[:, :freq_cutoff, :], forward_transform_squared[:, freq_cutoff:, :]], dim = -1)
111
+
112
+
113
+ # ******************** Verification ************************#
114
+ spec1 = torch.stft(y.squeeze(1), n_fft, hop_length=hop_size, win_length=win_size, window=hann_window[wnsize_dtype_device],
115
+ center=center, pad_mode='reflect', normalized=False, onesided=True, return_complex=False)
116
+ assert torch.allclose(spec1, spec2, atol=1e-4)
117
+
118
+ spec = torch.sqrt(spec2.pow(2).sum(-1) + 1e-6)
119
+ return spec
120
+
121
+
122
+ def spec_to_mel_torch(spec, n_fft, num_mels, sampling_rate, fmin, fmax):
123
+ global mel_basis
124
+ dtype_device = str(spec.dtype) + "_" + str(spec.device)
125
+ fmax_dtype_device = str(fmax) + "_" + dtype_device
126
+ if fmax_dtype_device not in mel_basis:
127
+ mel = librosa_mel_fn(sampling_rate, n_fft, num_mels, fmin, fmax)
128
+ mel_basis[fmax_dtype_device] = torch.from_numpy(mel).to(
129
+ dtype=spec.dtype, device=spec.device
130
+ )
131
+ spec = torch.matmul(mel_basis[fmax_dtype_device], spec)
132
+ spec = spectral_normalize_torch(spec)
133
+ return spec
134
+
135
+
136
+ def mel_spectrogram_torch(
137
+ y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False
138
+ ):
139
+ if torch.min(y) < -1.0:
140
+ print("min value is ", torch.min(y))
141
+ if torch.max(y) > 1.0:
142
+ print("max value is ", torch.max(y))
143
+
144
+ global mel_basis, hann_window
145
+ dtype_device = str(y.dtype) + "_" + str(y.device)
146
+ fmax_dtype_device = str(fmax) + "_" + dtype_device
147
+ wnsize_dtype_device = str(win_size) + "_" + dtype_device
148
+ if fmax_dtype_device not in mel_basis:
149
+ mel = librosa_mel_fn(sampling_rate, n_fft, num_mels, fmin, fmax)
150
+ mel_basis[fmax_dtype_device] = torch.from_numpy(mel).to(
151
+ dtype=y.dtype, device=y.device
152
+ )
153
+ if wnsize_dtype_device not in hann_window:
154
+ hann_window[wnsize_dtype_device] = torch.hann_window(win_size).to(
155
+ dtype=y.dtype, device=y.device
156
+ )
157
+
158
+ y = torch.nn.functional.pad(
159
+ y.unsqueeze(1),
160
+ (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)),
161
+ mode="reflect",
162
+ )
163
+ y = y.squeeze(1)
164
+
165
+ spec = torch.stft(
166
+ y,
167
+ n_fft,
168
+ hop_length=hop_size,
169
+ win_length=win_size,
170
+ window=hann_window[wnsize_dtype_device],
171
+ center=center,
172
+ pad_mode="reflect",
173
+ normalized=False,
174
+ onesided=True,
175
+ return_complex=False,
176
+ )
177
+
178
+ spec = torch.sqrt(spec.pow(2).sum(-1) + 1e-6)
179
+
180
+ spec = torch.matmul(mel_basis[fmax_dtype_device], spec)
181
+ spec = spectral_normalize_torch(spec)
182
+
183
+ return spec
app/voiceclone/ov/models.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from torch import nn
4
+ from torch.nn import functional as F
5
+
6
+ from . import commons
7
+ from . import modules
8
+ from . import attentions
9
+
10
+ from torch.nn import Conv1d, ConvTranspose1d, Conv2d
11
+ from torch.nn.utils import weight_norm, remove_weight_norm, spectral_norm
12
+
13
+ from .commons import init_weights, get_padding
14
+
15
+
16
+ class TextEncoder(nn.Module):
17
+ def __init__(self,
18
+ n_vocab,
19
+ out_channels,
20
+ hidden_channels,
21
+ filter_channels,
22
+ n_heads,
23
+ n_layers,
24
+ kernel_size,
25
+ p_dropout):
26
+ super().__init__()
27
+ self.n_vocab = n_vocab
28
+ self.out_channels = out_channels
29
+ self.hidden_channels = hidden_channels
30
+ self.filter_channels = filter_channels
31
+ self.n_heads = n_heads
32
+ self.n_layers = n_layers
33
+ self.kernel_size = kernel_size
34
+ self.p_dropout = p_dropout
35
+
36
+ self.emb = nn.Embedding(n_vocab, hidden_channels)
37
+ nn.init.normal_(self.emb.weight, 0.0, hidden_channels**-0.5)
38
+
39
+ self.encoder = attentions.Encoder(
40
+ hidden_channels,
41
+ filter_channels,
42
+ n_heads,
43
+ n_layers,
44
+ kernel_size,
45
+ p_dropout)
46
+ self.proj= nn.Conv1d(hidden_channels, out_channels * 2, 1)
47
+
48
+ def forward(self, x, x_lengths):
49
+ x = self.emb(x) * math.sqrt(self.hidden_channels) # [b, t, h]
50
+ x = torch.transpose(x, 1, -1) # [b, h, t]
51
+ x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype)
52
+
53
+ x = self.encoder(x * x_mask, x_mask)
54
+ stats = self.proj(x) * x_mask
55
+
56
+ m, logs = torch.split(stats, self.out_channels, dim=1)
57
+ return x, m, logs, x_mask
58
+
59
+
60
+ class DurationPredictor(nn.Module):
61
+ def __init__(
62
+ self, in_channels, filter_channels, kernel_size, p_dropout, gin_channels=0
63
+ ):
64
+ super().__init__()
65
+
66
+ self.in_channels = in_channels
67
+ self.filter_channels = filter_channels
68
+ self.kernel_size = kernel_size
69
+ self.p_dropout = p_dropout
70
+ self.gin_channels = gin_channels
71
+
72
+ self.drop = nn.Dropout(p_dropout)
73
+ self.conv_1 = nn.Conv1d(
74
+ in_channels, filter_channels, kernel_size, padding=kernel_size // 2
75
+ )
76
+ self.norm_1 = modules.LayerNorm(filter_channels)
77
+ self.conv_2 = nn.Conv1d(
78
+ filter_channels, filter_channels, kernel_size, padding=kernel_size // 2
79
+ )
80
+ self.norm_2 = modules.LayerNorm(filter_channels)
81
+ self.proj = nn.Conv1d(filter_channels, 1, 1)
82
+
83
+ if gin_channels != 0:
84
+ self.cond = nn.Conv1d(gin_channels, in_channels, 1)
85
+
86
+ def forward(self, x, x_mask, g=None):
87
+ x = torch.detach(x)
88
+ if g is not None:
89
+ g = torch.detach(g)
90
+ x = x + self.cond(g)
91
+ x = self.conv_1(x * x_mask)
92
+ x = torch.relu(x)
93
+ x = self.norm_1(x)
94
+ x = self.drop(x)
95
+ x = self.conv_2(x * x_mask)
96
+ x = torch.relu(x)
97
+ x = self.norm_2(x)
98
+ x = self.drop(x)
99
+ x = self.proj(x * x_mask)
100
+ return x * x_mask
101
+
102
+ class StochasticDurationPredictor(nn.Module):
103
+ def __init__(self, in_channels, filter_channels, kernel_size, p_dropout, n_flows=4, gin_channels=0):
104
+ super().__init__()
105
+ filter_channels = in_channels # it needs to be removed from future version.
106
+ self.in_channels = in_channels
107
+ self.filter_channels = filter_channels
108
+ self.kernel_size = kernel_size
109
+ self.p_dropout = p_dropout
110
+ self.n_flows = n_flows
111
+ self.gin_channels = gin_channels
112
+
113
+ self.log_flow = modules.Log()
114
+ self.flows = nn.ModuleList()
115
+ self.flows.append(modules.ElementwiseAffine(2))
116
+ for i in range(n_flows):
117
+ self.flows.append(modules.ConvFlow(2, filter_channels, kernel_size, n_layers=3))
118
+ self.flows.append(modules.Flip())
119
+
120
+ self.post_pre = nn.Conv1d(1, filter_channels, 1)
121
+ self.post_proj = nn.Conv1d(filter_channels, filter_channels, 1)
122
+ self.post_convs = modules.DDSConv(filter_channels, kernel_size, n_layers=3, p_dropout=p_dropout)
123
+ self.post_flows = nn.ModuleList()
124
+ self.post_flows.append(modules.ElementwiseAffine(2))
125
+ for i in range(4):
126
+ self.post_flows.append(modules.ConvFlow(2, filter_channels, kernel_size, n_layers=3))
127
+ self.post_flows.append(modules.Flip())
128
+
129
+ self.pre = nn.Conv1d(in_channels, filter_channels, 1)
130
+ self.proj = nn.Conv1d(filter_channels, filter_channels, 1)
131
+ self.convs = modules.DDSConv(filter_channels, kernel_size, n_layers=3, p_dropout=p_dropout)
132
+ if gin_channels != 0:
133
+ self.cond = nn.Conv1d(gin_channels, filter_channels, 1)
134
+
135
+ def forward(self, x, x_mask, w=None, g=None, reverse=False, noise_scale=1.0):
136
+ x = torch.detach(x)
137
+ x = self.pre(x)
138
+ if g is not None:
139
+ g = torch.detach(g)
140
+ x = x + self.cond(g)
141
+ x = self.convs(x, x_mask)
142
+ x = self.proj(x) * x_mask
143
+
144
+ if not reverse:
145
+ flows = self.flows
146
+ assert w is not None
147
+
148
+ logdet_tot_q = 0
149
+ h_w = self.post_pre(w)
150
+ h_w = self.post_convs(h_w, x_mask)
151
+ h_w = self.post_proj(h_w) * x_mask
152
+ e_q = torch.randn(w.size(0), 2, w.size(2)).to(device=x.device, dtype=x.dtype) * x_mask
153
+ z_q = e_q
154
+ for flow in self.post_flows:
155
+ z_q, logdet_q = flow(z_q, x_mask, g=(x + h_w))
156
+ logdet_tot_q += logdet_q
157
+ z_u, z1 = torch.split(z_q, [1, 1], 1)
158
+ u = torch.sigmoid(z_u) * x_mask
159
+ z0 = (w - u) * x_mask
160
+ logdet_tot_q += torch.sum((F.logsigmoid(z_u) + F.logsigmoid(-z_u)) * x_mask, [1,2])
161
+ logq = torch.sum(-0.5 * (math.log(2*math.pi) + (e_q**2)) * x_mask, [1,2]) - logdet_tot_q
162
+
163
+ logdet_tot = 0
164
+ z0, logdet = self.log_flow(z0, x_mask)
165
+ logdet_tot += logdet
166
+ z = torch.cat([z0, z1], 1)
167
+ for flow in flows:
168
+ z, logdet = flow(z, x_mask, g=x, reverse=reverse)
169
+ logdet_tot = logdet_tot + logdet
170
+ nll = torch.sum(0.5 * (math.log(2*math.pi) + (z**2)) * x_mask, [1,2]) - logdet_tot
171
+ return nll + logq # [b]
172
+ else:
173
+ flows = list(reversed(self.flows))
174
+ flows = flows[:-2] + [flows[-1]] # remove a useless vflow
175
+ z = torch.randn(x.size(0), 2, x.size(2)).to(device=x.device, dtype=x.dtype) * noise_scale
176
+ for flow in flows:
177
+ z = flow(z, x_mask, g=x, reverse=reverse)
178
+ z0, z1 = torch.split(z, [1, 1], 1)
179
+ logw = z0
180
+ return logw
181
+
182
+ class PosteriorEncoder(nn.Module):
183
+ def __init__(
184
+ self,
185
+ in_channels,
186
+ out_channels,
187
+ hidden_channels,
188
+ kernel_size,
189
+ dilation_rate,
190
+ n_layers,
191
+ gin_channels=0,
192
+ ):
193
+ super().__init__()
194
+ self.in_channels = in_channels
195
+ self.out_channels = out_channels
196
+ self.hidden_channels = hidden_channels
197
+ self.kernel_size = kernel_size
198
+ self.dilation_rate = dilation_rate
199
+ self.n_layers = n_layers
200
+ self.gin_channels = gin_channels
201
+
202
+ self.pre = nn.Conv1d(in_channels, hidden_channels, 1)
203
+ self.enc = modules.WN(
204
+ hidden_channels,
205
+ kernel_size,
206
+ dilation_rate,
207
+ n_layers,
208
+ gin_channels=gin_channels,
209
+ )
210
+ self.proj = nn.Conv1d(hidden_channels, out_channels * 2, 1)
211
+
212
+ def forward(self, x, x_lengths, g=None, tau=1.0):
213
+ x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(
214
+ x.dtype
215
+ )
216
+ x = self.pre(x) * x_mask
217
+ x = self.enc(x, x_mask, g=g)
218
+ stats = self.proj(x) * x_mask
219
+ m, logs = torch.split(stats, self.out_channels, dim=1)
220
+ z = (m + torch.randn_like(m) * tau * torch.exp(logs)) * x_mask
221
+ return z, m, logs, x_mask
222
+
223
+
224
+ class Generator(torch.nn.Module):
225
+ def __init__(
226
+ self,
227
+ initial_channel,
228
+ resblock,
229
+ resblock_kernel_sizes,
230
+ resblock_dilation_sizes,
231
+ upsample_rates,
232
+ upsample_initial_channel,
233
+ upsample_kernel_sizes,
234
+ gin_channels=0,
235
+ ):
236
+ super(Generator, self).__init__()
237
+ self.num_kernels = len(resblock_kernel_sizes)
238
+ self.num_upsamples = len(upsample_rates)
239
+ self.conv_pre = Conv1d(
240
+ initial_channel, upsample_initial_channel, 7, 1, padding=3
241
+ )
242
+ resblock = modules.ResBlock1 if resblock == "1" else modules.ResBlock2
243
+
244
+ self.ups = nn.ModuleList()
245
+ for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
246
+ self.ups.append(
247
+ weight_norm(
248
+ ConvTranspose1d(
249
+ upsample_initial_channel // (2**i),
250
+ upsample_initial_channel // (2 ** (i + 1)),
251
+ k,
252
+ u,
253
+ padding=(k - u) // 2,
254
+ )
255
+ )
256
+ )
257
+
258
+ self.resblocks = nn.ModuleList()
259
+ for i in range(len(self.ups)):
260
+ ch = upsample_initial_channel // (2 ** (i + 1))
261
+ for j, (k, d) in enumerate(
262
+ zip(resblock_kernel_sizes, resblock_dilation_sizes)
263
+ ):
264
+ self.resblocks.append(resblock(ch, k, d))
265
+
266
+ self.conv_post = Conv1d(ch, 1, 7, 1, padding=3, bias=False)
267
+ self.ups.apply(init_weights)
268
+
269
+ if gin_channels != 0:
270
+ self.cond = nn.Conv1d(gin_channels, upsample_initial_channel, 1)
271
+
272
+ def forward(self, x, g=None):
273
+ x = self.conv_pre(x)
274
+ if g is not None:
275
+ x = x + self.cond(g)
276
+
277
+ for i in range(self.num_upsamples):
278
+ x = F.leaky_relu(x, modules.LRELU_SLOPE)
279
+ x = self.ups[i](x)
280
+ xs = None
281
+ for j in range(self.num_kernels):
282
+ if xs is None:
283
+ xs = self.resblocks[i * self.num_kernels + j](x)
284
+ else:
285
+ xs += self.resblocks[i * self.num_kernels + j](x)
286
+ x = xs / self.num_kernels
287
+ x = F.leaky_relu(x)
288
+ x = self.conv_post(x)
289
+ x = torch.tanh(x)
290
+
291
+ return x
292
+
293
+ def remove_weight_norm(self):
294
+ print("Removing weight norm...")
295
+ for layer in self.ups:
296
+ remove_weight_norm(layer)
297
+ for layer in self.resblocks:
298
+ layer.remove_weight_norm()
299
+
300
+
301
+ class ReferenceEncoder(nn.Module):
302
+ """
303
+ inputs --- [N, Ty/r, n_mels*r] mels
304
+ outputs --- [N, ref_enc_gru_size]
305
+ """
306
+
307
+ def __init__(self, spec_channels, gin_channels=0, layernorm=True):
308
+ super().__init__()
309
+ self.spec_channels = spec_channels
310
+ ref_enc_filters = [32, 32, 64, 64, 128, 128]
311
+ K = len(ref_enc_filters)
312
+ filters = [1] + ref_enc_filters
313
+ convs = [
314
+ weight_norm(
315
+ nn.Conv2d(
316
+ in_channels=filters[i],
317
+ out_channels=filters[i + 1],
318
+ kernel_size=(3, 3),
319
+ stride=(2, 2),
320
+ padding=(1, 1),
321
+ )
322
+ )
323
+ for i in range(K)
324
+ ]
325
+ self.convs = nn.ModuleList(convs)
326
+
327
+ out_channels = self.calculate_channels(spec_channels, 3, 2, 1, K)
328
+ self.gru = nn.GRU(
329
+ input_size=ref_enc_filters[-1] * out_channels,
330
+ hidden_size=256 // 2,
331
+ batch_first=True,
332
+ )
333
+ self.proj = nn.Linear(128, gin_channels)
334
+ if layernorm:
335
+ self.layernorm = nn.LayerNorm(self.spec_channels)
336
+ else:
337
+ self.layernorm = None
338
+
339
+ def forward(self, inputs, mask=None):
340
+ N = inputs.size(0)
341
+
342
+ out = inputs.view(N, 1, -1, self.spec_channels) # [N, 1, Ty, n_freqs]
343
+ if self.layernorm is not None:
344
+ out = self.layernorm(out)
345
+
346
+ for conv in self.convs:
347
+ out = conv(out)
348
+ # out = wn(out)
349
+ out = F.relu(out) # [N, 128, Ty//2^K, n_mels//2^K]
350
+
351
+ out = out.transpose(1, 2) # [N, Ty//2^K, 128, n_mels//2^K]
352
+ T = out.size(1)
353
+ N = out.size(0)
354
+ out = out.contiguous().view(N, T, -1) # [N, Ty//2^K, 128*n_mels//2^K]
355
+
356
+ self.gru.flatten_parameters()
357
+ memory, out = self.gru(out) # out --- [1, N, 128]
358
+
359
+ return self.proj(out.squeeze(0))
360
+
361
+ def calculate_channels(self, L, kernel_size, stride, pad, n_convs):
362
+ for i in range(n_convs):
363
+ L = (L - kernel_size + 2 * pad) // stride + 1
364
+ return L
365
+
366
+
367
+ class ResidualCouplingBlock(nn.Module):
368
+ def __init__(self,
369
+ channels,
370
+ hidden_channels,
371
+ kernel_size,
372
+ dilation_rate,
373
+ n_layers,
374
+ n_flows=4,
375
+ gin_channels=0):
376
+ super().__init__()
377
+ self.channels = channels
378
+ self.hidden_channels = hidden_channels
379
+ self.kernel_size = kernel_size
380
+ self.dilation_rate = dilation_rate
381
+ self.n_layers = n_layers
382
+ self.n_flows = n_flows
383
+ self.gin_channels = gin_channels
384
+
385
+ self.flows = nn.ModuleList()
386
+ for i in range(n_flows):
387
+ self.flows.append(modules.ResidualCouplingLayer(channels, hidden_channels, kernel_size, dilation_rate, n_layers, gin_channels=gin_channels, mean_only=True))
388
+ self.flows.append(modules.Flip())
389
+
390
+ def forward(self, x, x_mask, g=None, reverse=False):
391
+ if not reverse:
392
+ for flow in self.flows:
393
+ x, _ = flow(x, x_mask, g=g, reverse=reverse)
394
+ else:
395
+ for flow in reversed(self.flows):
396
+ x = flow(x, x_mask, g=g, reverse=reverse)
397
+ return x
398
+
399
+ class SynthesizerTrn(nn.Module):
400
+ """
401
+ Synthesizer for Training
402
+ """
403
+
404
+ def __init__(
405
+ self,
406
+ n_vocab,
407
+ spec_channels,
408
+ inter_channels,
409
+ hidden_channels,
410
+ filter_channels,
411
+ n_heads,
412
+ n_layers,
413
+ kernel_size,
414
+ p_dropout,
415
+ resblock,
416
+ resblock_kernel_sizes,
417
+ resblock_dilation_sizes,
418
+ upsample_rates,
419
+ upsample_initial_channel,
420
+ upsample_kernel_sizes,
421
+ n_speakers=256,
422
+ gin_channels=256,
423
+ zero_g=False,
424
+ **kwargs
425
+ ):
426
+ super().__init__()
427
+
428
+ self.dec = Generator(
429
+ inter_channels,
430
+ resblock,
431
+ resblock_kernel_sizes,
432
+ resblock_dilation_sizes,
433
+ upsample_rates,
434
+ upsample_initial_channel,
435
+ upsample_kernel_sizes,
436
+ gin_channels=gin_channels,
437
+ )
438
+ self.enc_q = PosteriorEncoder(
439
+ spec_channels,
440
+ inter_channels,
441
+ hidden_channels,
442
+ 5,
443
+ 1,
444
+ 16,
445
+ gin_channels=gin_channels,
446
+ )
447
+
448
+ self.flow = ResidualCouplingBlock(inter_channels, hidden_channels, 5, 1, 4, gin_channels=gin_channels)
449
+
450
+ self.n_speakers = n_speakers
451
+ if n_speakers == 0:
452
+ self.ref_enc = ReferenceEncoder(spec_channels, gin_channels)
453
+ else:
454
+ self.enc_p = TextEncoder(n_vocab,
455
+ inter_channels,
456
+ hidden_channels,
457
+ filter_channels,
458
+ n_heads,
459
+ n_layers,
460
+ kernel_size,
461
+ p_dropout)
462
+ self.sdp = StochasticDurationPredictor(hidden_channels, 192, 3, 0.5, 4, gin_channels=gin_channels)
463
+ self.dp = DurationPredictor(hidden_channels, 256, 3, 0.5, gin_channels=gin_channels)
464
+ self.emb_g = nn.Embedding(n_speakers, gin_channels)
465
+ self.zero_g = zero_g
466
+
467
+ def infer(self, x, x_lengths, sid=None, noise_scale=1, length_scale=1, noise_scale_w=1., sdp_ratio=0.2, max_len=None):
468
+ x, m_p, logs_p, x_mask = self.enc_p(x, x_lengths)
469
+ if self.n_speakers > 0:
470
+ g = self.emb_g(sid).unsqueeze(-1) # [b, h, 1]
471
+ else:
472
+ g = None
473
+
474
+ logw = self.sdp(x, x_mask, g=g, reverse=True, noise_scale=noise_scale_w) * sdp_ratio \
475
+ + self.dp(x, x_mask, g=g) * (1 - sdp_ratio)
476
+
477
+ w = torch.exp(logw) * x_mask * length_scale
478
+ w_ceil = torch.ceil(w)
479
+ y_lengths = torch.clamp_min(torch.sum(w_ceil, [1, 2]), 1).long()
480
+ y_mask = torch.unsqueeze(commons.sequence_mask(y_lengths, None), 1).to(x_mask.dtype)
481
+ attn_mask = torch.unsqueeze(x_mask, 2) * torch.unsqueeze(y_mask, -1)
482
+ attn = commons.generate_path(w_ceil, attn_mask)
483
+
484
+ m_p = torch.matmul(attn.squeeze(1), m_p.transpose(1, 2)).transpose(1, 2) # [b, t', t], [b, t, d] -> [b, d, t']
485
+ logs_p = torch.matmul(attn.squeeze(1), logs_p.transpose(1, 2)).transpose(1, 2) # [b, t', t], [b, t, d] -> [b, d, t']
486
+
487
+ z_p = m_p + torch.randn_like(m_p) * torch.exp(logs_p) * noise_scale
488
+ z = self.flow(z_p, y_mask, g=g, reverse=True)
489
+ o = self.dec((z * y_mask)[:,:,:max_len], g=g)
490
+ return o, attn, y_mask, (z, z_p, m_p, logs_p)
491
+
492
+ def voice_conversion(self, y, y_lengths, sid_src, sid_tgt, tau=1.0):
493
+ g_src = sid_src
494
+ g_tgt = sid_tgt
495
+ z, m_q, logs_q, y_mask = self.enc_q(y, y_lengths, g=g_src if not self.zero_g else torch.zeros_like(g_src), tau=tau)
496
+ z_p = self.flow(z, y_mask, g=g_src)
497
+ z_hat = self.flow(z_p, y_mask, g=g_tgt, reverse=True)
498
+ o_hat = self.dec(z_hat * y_mask, g=g_tgt if not self.zero_g else torch.zeros_like(g_tgt))
499
+ return o_hat, y_mask, (z, z_p, z_hat)
app/voiceclone/ov/modules.py ADDED
@@ -0,0 +1,598 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from torch import nn
4
+ from torch.nn import functional as F
5
+
6
+ from torch.nn import Conv1d
7
+ from torch.nn.utils import weight_norm, remove_weight_norm
8
+
9
+ from . import commons
10
+ from .commons import init_weights, get_padding
11
+ from .transforms import piecewise_rational_quadratic_transform
12
+ from .attentions import Encoder
13
+
14
+ LRELU_SLOPE = 0.1
15
+
16
+
17
+ class LayerNorm(nn.Module):
18
+ def __init__(self, channels, eps=1e-5):
19
+ super().__init__()
20
+ self.channels = channels
21
+ self.eps = eps
22
+
23
+ self.gamma = nn.Parameter(torch.ones(channels))
24
+ self.beta = nn.Parameter(torch.zeros(channels))
25
+
26
+ def forward(self, x):
27
+ x = x.transpose(1, -1)
28
+ x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
29
+ return x.transpose(1, -1)
30
+
31
+
32
+ class ConvReluNorm(nn.Module):
33
+ def __init__(
34
+ self,
35
+ in_channels,
36
+ hidden_channels,
37
+ out_channels,
38
+ kernel_size,
39
+ n_layers,
40
+ p_dropout,
41
+ ):
42
+ super().__init__()
43
+ self.in_channels = in_channels
44
+ self.hidden_channels = hidden_channels
45
+ self.out_channels = out_channels
46
+ self.kernel_size = kernel_size
47
+ self.n_layers = n_layers
48
+ self.p_dropout = p_dropout
49
+ assert n_layers > 1, "Number of layers should be larger than 0."
50
+
51
+ self.conv_layers = nn.ModuleList()
52
+ self.norm_layers = nn.ModuleList()
53
+ self.conv_layers.append(
54
+ nn.Conv1d(
55
+ in_channels, hidden_channels, kernel_size, padding=kernel_size // 2
56
+ )
57
+ )
58
+ self.norm_layers.append(LayerNorm(hidden_channels))
59
+ self.relu_drop = nn.Sequential(nn.ReLU(), nn.Dropout(p_dropout))
60
+ for _ in range(n_layers - 1):
61
+ self.conv_layers.append(
62
+ nn.Conv1d(
63
+ hidden_channels,
64
+ hidden_channels,
65
+ kernel_size,
66
+ padding=kernel_size // 2,
67
+ )
68
+ )
69
+ self.norm_layers.append(LayerNorm(hidden_channels))
70
+ self.proj = nn.Conv1d(hidden_channels, out_channels, 1)
71
+ self.proj.weight.data.zero_()
72
+ self.proj.bias.data.zero_()
73
+
74
+ def forward(self, x, x_mask):
75
+ x_org = x
76
+ for i in range(self.n_layers):
77
+ x = self.conv_layers[i](x * x_mask)
78
+ x = self.norm_layers[i](x)
79
+ x = self.relu_drop(x)
80
+ x = x_org + self.proj(x)
81
+ return x * x_mask
82
+
83
+
84
+ class DDSConv(nn.Module):
85
+ """
86
+ Dilated and Depth-Separable Convolution
87
+ """
88
+
89
+ def __init__(self, channels, kernel_size, n_layers, p_dropout=0.0):
90
+ super().__init__()
91
+ self.channels = channels
92
+ self.kernel_size = kernel_size
93
+ self.n_layers = n_layers
94
+ self.p_dropout = p_dropout
95
+
96
+ self.drop = nn.Dropout(p_dropout)
97
+ self.convs_sep = nn.ModuleList()
98
+ self.convs_1x1 = nn.ModuleList()
99
+ self.norms_1 = nn.ModuleList()
100
+ self.norms_2 = nn.ModuleList()
101
+ for i in range(n_layers):
102
+ dilation = kernel_size**i
103
+ padding = (kernel_size * dilation - dilation) // 2
104
+ self.convs_sep.append(
105
+ nn.Conv1d(
106
+ channels,
107
+ channels,
108
+ kernel_size,
109
+ groups=channels,
110
+ dilation=dilation,
111
+ padding=padding,
112
+ )
113
+ )
114
+ self.convs_1x1.append(nn.Conv1d(channels, channels, 1))
115
+ self.norms_1.append(LayerNorm(channels))
116
+ self.norms_2.append(LayerNorm(channels))
117
+
118
+ def forward(self, x, x_mask, g=None):
119
+ if g is not None:
120
+ x = x + g
121
+ for i in range(self.n_layers):
122
+ y = self.convs_sep[i](x * x_mask)
123
+ y = self.norms_1[i](y)
124
+ y = F.gelu(y)
125
+ y = self.convs_1x1[i](y)
126
+ y = self.norms_2[i](y)
127
+ y = F.gelu(y)
128
+ y = self.drop(y)
129
+ x = x + y
130
+ return x * x_mask
131
+
132
+
133
+ class WN(torch.nn.Module):
134
+ def __init__(
135
+ self,
136
+ hidden_channels,
137
+ kernel_size,
138
+ dilation_rate,
139
+ n_layers,
140
+ gin_channels=0,
141
+ p_dropout=0,
142
+ ):
143
+ super(WN, self).__init__()
144
+ assert kernel_size % 2 == 1
145
+ self.hidden_channels = hidden_channels
146
+ self.kernel_size = (kernel_size,)
147
+ self.dilation_rate = dilation_rate
148
+ self.n_layers = n_layers
149
+ self.gin_channels = gin_channels
150
+ self.p_dropout = p_dropout
151
+
152
+ self.in_layers = torch.nn.ModuleList()
153
+ self.res_skip_layers = torch.nn.ModuleList()
154
+ self.drop = nn.Dropout(p_dropout)
155
+
156
+ if gin_channels != 0:
157
+ cond_layer = torch.nn.Conv1d(
158
+ gin_channels, 2 * hidden_channels * n_layers, 1
159
+ )
160
+ self.cond_layer = torch.nn.utils.weight_norm(cond_layer, name="weight")
161
+
162
+ for i in range(n_layers):
163
+ dilation = dilation_rate**i
164
+ padding = int((kernel_size * dilation - dilation) / 2)
165
+ in_layer = torch.nn.Conv1d(
166
+ hidden_channels,
167
+ 2 * hidden_channels,
168
+ kernel_size,
169
+ dilation=dilation,
170
+ padding=padding,
171
+ )
172
+ in_layer = torch.nn.utils.weight_norm(in_layer, name="weight")
173
+ self.in_layers.append(in_layer)
174
+
175
+ # last one is not necessary
176
+ if i < n_layers - 1:
177
+ res_skip_channels = 2 * hidden_channels
178
+ else:
179
+ res_skip_channels = hidden_channels
180
+
181
+ res_skip_layer = torch.nn.Conv1d(hidden_channels, res_skip_channels, 1)
182
+ res_skip_layer = torch.nn.utils.weight_norm(res_skip_layer, name="weight")
183
+ self.res_skip_layers.append(res_skip_layer)
184
+
185
+ def forward(self, x, x_mask, g=None, **kwargs):
186
+ output = torch.zeros_like(x)
187
+ n_channels_tensor = torch.IntTensor([self.hidden_channels])
188
+
189
+ if g is not None:
190
+ g = self.cond_layer(g)
191
+
192
+ for i in range(self.n_layers):
193
+ x_in = self.in_layers[i](x)
194
+ if g is not None:
195
+ cond_offset = i * 2 * self.hidden_channels
196
+ g_l = g[:, cond_offset : cond_offset + 2 * self.hidden_channels, :]
197
+ else:
198
+ g_l = torch.zeros_like(x_in)
199
+
200
+ acts = commons.fused_add_tanh_sigmoid_multiply(x_in, g_l, n_channels_tensor)
201
+ acts = self.drop(acts)
202
+
203
+ res_skip_acts = self.res_skip_layers[i](acts)
204
+ if i < self.n_layers - 1:
205
+ res_acts = res_skip_acts[:, : self.hidden_channels, :]
206
+ x = (x + res_acts) * x_mask
207
+ output = output + res_skip_acts[:, self.hidden_channels :, :]
208
+ else:
209
+ output = output + res_skip_acts
210
+ return output * x_mask
211
+
212
+ def remove_weight_norm(self):
213
+ if self.gin_channels != 0:
214
+ torch.nn.utils.remove_weight_norm(self.cond_layer)
215
+ for l in self.in_layers:
216
+ torch.nn.utils.remove_weight_norm(l)
217
+ for l in self.res_skip_layers:
218
+ torch.nn.utils.remove_weight_norm(l)
219
+
220
+
221
+ class ResBlock1(torch.nn.Module):
222
+ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)):
223
+ super(ResBlock1, self).__init__()
224
+ self.convs1 = nn.ModuleList(
225
+ [
226
+ weight_norm(
227
+ Conv1d(
228
+ channels,
229
+ channels,
230
+ kernel_size,
231
+ 1,
232
+ dilation=dilation[0],
233
+ padding=get_padding(kernel_size, dilation[0]),
234
+ )
235
+ ),
236
+ weight_norm(
237
+ Conv1d(
238
+ channels,
239
+ channels,
240
+ kernel_size,
241
+ 1,
242
+ dilation=dilation[1],
243
+ padding=get_padding(kernel_size, dilation[1]),
244
+ )
245
+ ),
246
+ weight_norm(
247
+ Conv1d(
248
+ channels,
249
+ channels,
250
+ kernel_size,
251
+ 1,
252
+ dilation=dilation[2],
253
+ padding=get_padding(kernel_size, dilation[2]),
254
+ )
255
+ ),
256
+ ]
257
+ )
258
+ self.convs1.apply(init_weights)
259
+
260
+ self.convs2 = nn.ModuleList(
261
+ [
262
+ weight_norm(
263
+ Conv1d(
264
+ channels,
265
+ channels,
266
+ kernel_size,
267
+ 1,
268
+ dilation=1,
269
+ padding=get_padding(kernel_size, 1),
270
+ )
271
+ ),
272
+ weight_norm(
273
+ Conv1d(
274
+ channels,
275
+ channels,
276
+ kernel_size,
277
+ 1,
278
+ dilation=1,
279
+ padding=get_padding(kernel_size, 1),
280
+ )
281
+ ),
282
+ weight_norm(
283
+ Conv1d(
284
+ channels,
285
+ channels,
286
+ kernel_size,
287
+ 1,
288
+ dilation=1,
289
+ padding=get_padding(kernel_size, 1),
290
+ )
291
+ ),
292
+ ]
293
+ )
294
+ self.convs2.apply(init_weights)
295
+
296
+ def forward(self, x, x_mask=None):
297
+ for c1, c2 in zip(self.convs1, self.convs2):
298
+ xt = F.leaky_relu(x, LRELU_SLOPE)
299
+ if x_mask is not None:
300
+ xt = xt * x_mask
301
+ xt = c1(xt)
302
+ xt = F.leaky_relu(xt, LRELU_SLOPE)
303
+ if x_mask is not None:
304
+ xt = xt * x_mask
305
+ xt = c2(xt)
306
+ x = xt + x
307
+ if x_mask is not None:
308
+ x = x * x_mask
309
+ return x
310
+
311
+ def remove_weight_norm(self):
312
+ for l in self.convs1:
313
+ remove_weight_norm(l)
314
+ for l in self.convs2:
315
+ remove_weight_norm(l)
316
+
317
+
318
+ class ResBlock2(torch.nn.Module):
319
+ def __init__(self, channels, kernel_size=3, dilation=(1, 3)):
320
+ super(ResBlock2, self).__init__()
321
+ self.convs = nn.ModuleList(
322
+ [
323
+ weight_norm(
324
+ Conv1d(
325
+ channels,
326
+ channels,
327
+ kernel_size,
328
+ 1,
329
+ dilation=dilation[0],
330
+ padding=get_padding(kernel_size, dilation[0]),
331
+ )
332
+ ),
333
+ weight_norm(
334
+ Conv1d(
335
+ channels,
336
+ channels,
337
+ kernel_size,
338
+ 1,
339
+ dilation=dilation[1],
340
+ padding=get_padding(kernel_size, dilation[1]),
341
+ )
342
+ ),
343
+ ]
344
+ )
345
+ self.convs.apply(init_weights)
346
+
347
+ def forward(self, x, x_mask=None):
348
+ for c in self.convs:
349
+ xt = F.leaky_relu(x, LRELU_SLOPE)
350
+ if x_mask is not None:
351
+ xt = xt * x_mask
352
+ xt = c(xt)
353
+ x = xt + x
354
+ if x_mask is not None:
355
+ x = x * x_mask
356
+ return x
357
+
358
+ def remove_weight_norm(self):
359
+ for l in self.convs:
360
+ remove_weight_norm(l)
361
+
362
+
363
+ class Log(nn.Module):
364
+ def forward(self, x, x_mask, reverse=False, **kwargs):
365
+ if not reverse:
366
+ y = torch.log(torch.clamp_min(x, 1e-5)) * x_mask
367
+ logdet = torch.sum(-y, [1, 2])
368
+ return y, logdet
369
+ else:
370
+ x = torch.exp(x) * x_mask
371
+ return x
372
+
373
+
374
+ class Flip(nn.Module):
375
+ def forward(self, x, *args, reverse=False, **kwargs):
376
+ x = torch.flip(x, [1])
377
+ if not reverse:
378
+ logdet = torch.zeros(x.size(0)).to(dtype=x.dtype, device=x.device)
379
+ return x, logdet
380
+ else:
381
+ return x
382
+
383
+
384
+ class ElementwiseAffine(nn.Module):
385
+ def __init__(self, channels):
386
+ super().__init__()
387
+ self.channels = channels
388
+ self.m = nn.Parameter(torch.zeros(channels, 1))
389
+ self.logs = nn.Parameter(torch.zeros(channels, 1))
390
+
391
+ def forward(self, x, x_mask, reverse=False, **kwargs):
392
+ if not reverse:
393
+ y = self.m + torch.exp(self.logs) * x
394
+ y = y * x_mask
395
+ logdet = torch.sum(self.logs * x_mask, [1, 2])
396
+ return y, logdet
397
+ else:
398
+ x = (x - self.m) * torch.exp(-self.logs) * x_mask
399
+ return x
400
+
401
+
402
+ class ResidualCouplingLayer(nn.Module):
403
+ def __init__(
404
+ self,
405
+ channels,
406
+ hidden_channels,
407
+ kernel_size,
408
+ dilation_rate,
409
+ n_layers,
410
+ p_dropout=0,
411
+ gin_channels=0,
412
+ mean_only=False,
413
+ ):
414
+ assert channels % 2 == 0, "channels should be divisible by 2"
415
+ super().__init__()
416
+ self.channels = channels
417
+ self.hidden_channels = hidden_channels
418
+ self.kernel_size = kernel_size
419
+ self.dilation_rate = dilation_rate
420
+ self.n_layers = n_layers
421
+ self.half_channels = channels // 2
422
+ self.mean_only = mean_only
423
+
424
+ self.pre = nn.Conv1d(self.half_channels, hidden_channels, 1)
425
+ self.enc = WN(
426
+ hidden_channels,
427
+ kernel_size,
428
+ dilation_rate,
429
+ n_layers,
430
+ p_dropout=p_dropout,
431
+ gin_channels=gin_channels,
432
+ )
433
+ self.post = nn.Conv1d(hidden_channels, self.half_channels * (2 - mean_only), 1)
434
+ self.post.weight.data.zero_()
435
+ self.post.bias.data.zero_()
436
+
437
+ def forward(self, x, x_mask, g=None, reverse=False):
438
+ x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
439
+ h = self.pre(x0) * x_mask
440
+ h = self.enc(h, x_mask, g=g)
441
+ stats = self.post(h) * x_mask
442
+ if not self.mean_only:
443
+ m, logs = torch.split(stats, [self.half_channels] * 2, 1)
444
+ else:
445
+ m = stats
446
+ logs = torch.zeros_like(m)
447
+
448
+ if not reverse:
449
+ x1 = m + x1 * torch.exp(logs) * x_mask
450
+ x = torch.cat([x0, x1], 1)
451
+ logdet = torch.sum(logs, [1, 2])
452
+ return x, logdet
453
+ else:
454
+ x1 = (x1 - m) * torch.exp(-logs) * x_mask
455
+ x = torch.cat([x0, x1], 1)
456
+ return x
457
+
458
+
459
+ class ConvFlow(nn.Module):
460
+ def __init__(
461
+ self,
462
+ in_channels,
463
+ filter_channels,
464
+ kernel_size,
465
+ n_layers,
466
+ num_bins=10,
467
+ tail_bound=5.0,
468
+ ):
469
+ super().__init__()
470
+ self.in_channels = in_channels
471
+ self.filter_channels = filter_channels
472
+ self.kernel_size = kernel_size
473
+ self.n_layers = n_layers
474
+ self.num_bins = num_bins
475
+ self.tail_bound = tail_bound
476
+ self.half_channels = in_channels // 2
477
+
478
+ self.pre = nn.Conv1d(self.half_channels, filter_channels, 1)
479
+ self.convs = DDSConv(filter_channels, kernel_size, n_layers, p_dropout=0.0)
480
+ self.proj = nn.Conv1d(
481
+ filter_channels, self.half_channels * (num_bins * 3 - 1), 1
482
+ )
483
+ self.proj.weight.data.zero_()
484
+ self.proj.bias.data.zero_()
485
+
486
+ def forward(self, x, x_mask, g=None, reverse=False):
487
+ x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
488
+ h = self.pre(x0)
489
+ h = self.convs(h, x_mask, g=g)
490
+ h = self.proj(h) * x_mask
491
+
492
+ b, c, t = x0.shape
493
+ h = h.reshape(b, c, -1, t).permute(0, 1, 3, 2) # [b, cx?, t] -> [b, c, t, ?]
494
+
495
+ unnormalized_widths = h[..., : self.num_bins] / math.sqrt(self.filter_channels)
496
+ unnormalized_heights = h[..., self.num_bins : 2 * self.num_bins] / math.sqrt(
497
+ self.filter_channels
498
+ )
499
+ unnormalized_derivatives = h[..., 2 * self.num_bins :]
500
+
501
+ x1, logabsdet = piecewise_rational_quadratic_transform(
502
+ x1,
503
+ unnormalized_widths,
504
+ unnormalized_heights,
505
+ unnormalized_derivatives,
506
+ inverse=reverse,
507
+ tails="linear",
508
+ tail_bound=self.tail_bound,
509
+ )
510
+
511
+ x = torch.cat([x0, x1], 1) * x_mask
512
+ logdet = torch.sum(logabsdet * x_mask, [1, 2])
513
+ if not reverse:
514
+ return x, logdet
515
+ else:
516
+ return x
517
+
518
+
519
+ class TransformerCouplingLayer(nn.Module):
520
+ def __init__(
521
+ self,
522
+ channels,
523
+ hidden_channels,
524
+ kernel_size,
525
+ n_layers,
526
+ n_heads,
527
+ p_dropout=0,
528
+ filter_channels=0,
529
+ mean_only=False,
530
+ wn_sharing_parameter=None,
531
+ gin_channels=0,
532
+ ):
533
+ assert n_layers == 3, n_layers
534
+ assert channels % 2 == 0, "channels should be divisible by 2"
535
+ super().__init__()
536
+ self.channels = channels
537
+ self.hidden_channels = hidden_channels
538
+ self.kernel_size = kernel_size
539
+ self.n_layers = n_layers
540
+ self.half_channels = channels // 2
541
+ self.mean_only = mean_only
542
+
543
+ self.pre = nn.Conv1d(self.half_channels, hidden_channels, 1)
544
+ self.enc = (
545
+ Encoder(
546
+ hidden_channels,
547
+ filter_channels,
548
+ n_heads,
549
+ n_layers,
550
+ kernel_size,
551
+ p_dropout,
552
+ isflow=True,
553
+ gin_channels=gin_channels,
554
+ )
555
+ if wn_sharing_parameter is None
556
+ else wn_sharing_parameter
557
+ )
558
+ self.post = nn.Conv1d(hidden_channels, self.half_channels * (2 - mean_only), 1)
559
+ self.post.weight.data.zero_()
560
+ self.post.bias.data.zero_()
561
+
562
+ def forward(self, x, x_mask, g=None, reverse=False):
563
+ x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
564
+ h = self.pre(x0) * x_mask
565
+ h = self.enc(h, x_mask, g=g)
566
+ stats = self.post(h) * x_mask
567
+ if not self.mean_only:
568
+ m, logs = torch.split(stats, [self.half_channels] * 2, 1)
569
+ else:
570
+ m = stats
571
+ logs = torch.zeros_like(m)
572
+
573
+ if not reverse:
574
+ x1 = m + x1 * torch.exp(logs) * x_mask
575
+ x = torch.cat([x0, x1], 1)
576
+ logdet = torch.sum(logs, [1, 2])
577
+ return x, logdet
578
+ else:
579
+ x1 = (x1 - m) * torch.exp(-logs) * x_mask
580
+ x = torch.cat([x0, x1], 1)
581
+ return x
582
+
583
+ x1, logabsdet = piecewise_rational_quadratic_transform(
584
+ x1,
585
+ unnormalized_widths,
586
+ unnormalized_heights,
587
+ unnormalized_derivatives,
588
+ inverse=reverse,
589
+ tails="linear",
590
+ tail_bound=self.tail_bound,
591
+ )
592
+
593
+ x = torch.cat([x0, x1], 1) * x_mask
594
+ logdet = torch.sum(logabsdet * x_mask, [1, 2])
595
+ if not reverse:
596
+ return x, logdet
597
+ else:
598
+ return x
app/voiceclone/ov/transforms.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch.nn import functional as F
3
+
4
+ import numpy as np
5
+
6
+
7
+ DEFAULT_MIN_BIN_WIDTH = 1e-3
8
+ DEFAULT_MIN_BIN_HEIGHT = 1e-3
9
+ DEFAULT_MIN_DERIVATIVE = 1e-3
10
+
11
+
12
+ def piecewise_rational_quadratic_transform(
13
+ inputs,
14
+ unnormalized_widths,
15
+ unnormalized_heights,
16
+ unnormalized_derivatives,
17
+ inverse=False,
18
+ tails=None,
19
+ tail_bound=1.0,
20
+ min_bin_width=DEFAULT_MIN_BIN_WIDTH,
21
+ min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
22
+ min_derivative=DEFAULT_MIN_DERIVATIVE,
23
+ ):
24
+ if tails is None:
25
+ spline_fn = rational_quadratic_spline
26
+ spline_kwargs = {}
27
+ else:
28
+ spline_fn = unconstrained_rational_quadratic_spline
29
+ spline_kwargs = {"tails": tails, "tail_bound": tail_bound}
30
+
31
+ outputs, logabsdet = spline_fn(
32
+ inputs=inputs,
33
+ unnormalized_widths=unnormalized_widths,
34
+ unnormalized_heights=unnormalized_heights,
35
+ unnormalized_derivatives=unnormalized_derivatives,
36
+ inverse=inverse,
37
+ min_bin_width=min_bin_width,
38
+ min_bin_height=min_bin_height,
39
+ min_derivative=min_derivative,
40
+ **spline_kwargs
41
+ )
42
+ return outputs, logabsdet
43
+
44
+
45
+ def searchsorted(bin_locations, inputs, eps=1e-6):
46
+ bin_locations[..., -1] += eps
47
+ return torch.sum(inputs[..., None] >= bin_locations, dim=-1) - 1
48
+
49
+
50
+ def unconstrained_rational_quadratic_spline(
51
+ inputs,
52
+ unnormalized_widths,
53
+ unnormalized_heights,
54
+ unnormalized_derivatives,
55
+ inverse=False,
56
+ tails="linear",
57
+ tail_bound=1.0,
58
+ min_bin_width=DEFAULT_MIN_BIN_WIDTH,
59
+ min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
60
+ min_derivative=DEFAULT_MIN_DERIVATIVE,
61
+ ):
62
+ inside_interval_mask = (inputs >= -tail_bound) & (inputs <= tail_bound)
63
+ outside_interval_mask = ~inside_interval_mask
64
+
65
+ outputs = torch.zeros_like(inputs)
66
+ logabsdet = torch.zeros_like(inputs)
67
+
68
+ if tails == "linear":
69
+ unnormalized_derivatives = F.pad(unnormalized_derivatives, pad=(1, 1))
70
+ constant = np.log(np.exp(1 - min_derivative) - 1)
71
+ unnormalized_derivatives[..., 0] = constant
72
+ unnormalized_derivatives[..., -1] = constant
73
+
74
+ outputs[outside_interval_mask] = inputs[outside_interval_mask]
75
+ logabsdet[outside_interval_mask] = 0
76
+ else:
77
+ raise RuntimeError("{} tails are not implemented.".format(tails))
78
+
79
+ (
80
+ outputs[inside_interval_mask],
81
+ logabsdet[inside_interval_mask],
82
+ ) = rational_quadratic_spline(
83
+ inputs=inputs[inside_interval_mask],
84
+ unnormalized_widths=unnormalized_widths[inside_interval_mask, :],
85
+ unnormalized_heights=unnormalized_heights[inside_interval_mask, :],
86
+ unnormalized_derivatives=unnormalized_derivatives[inside_interval_mask, :],
87
+ inverse=inverse,
88
+ left=-tail_bound,
89
+ right=tail_bound,
90
+ bottom=-tail_bound,
91
+ top=tail_bound,
92
+ min_bin_width=min_bin_width,
93
+ min_bin_height=min_bin_height,
94
+ min_derivative=min_derivative,
95
+ )
96
+
97
+ return outputs, logabsdet
98
+
99
+
100
+ def rational_quadratic_spline(
101
+ inputs,
102
+ unnormalized_widths,
103
+ unnormalized_heights,
104
+ unnormalized_derivatives,
105
+ inverse=False,
106
+ left=0.0,
107
+ right=1.0,
108
+ bottom=0.0,
109
+ top=1.0,
110
+ min_bin_width=DEFAULT_MIN_BIN_WIDTH,
111
+ min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
112
+ min_derivative=DEFAULT_MIN_DERIVATIVE,
113
+ ):
114
+ if torch.min(inputs) < left or torch.max(inputs) > right:
115
+ raise ValueError("Input to a transform is not within its domain")
116
+
117
+ num_bins = unnormalized_widths.shape[-1]
118
+
119
+ if min_bin_width * num_bins > 1.0:
120
+ raise ValueError("Minimal bin width too large for the number of bins")
121
+ if min_bin_height * num_bins > 1.0:
122
+ raise ValueError("Minimal bin height too large for the number of bins")
123
+
124
+ widths = F.softmax(unnormalized_widths, dim=-1)
125
+ widths = min_bin_width + (1 - min_bin_width * num_bins) * widths
126
+ cumwidths = torch.cumsum(widths, dim=-1)
127
+ cumwidths = F.pad(cumwidths, pad=(1, 0), mode="constant", value=0.0)
128
+ cumwidths = (right - left) * cumwidths + left
129
+ cumwidths[..., 0] = left
130
+ cumwidths[..., -1] = right
131
+ widths = cumwidths[..., 1:] - cumwidths[..., :-1]
132
+
133
+ derivatives = min_derivative + F.softplus(unnormalized_derivatives)
134
+
135
+ heights = F.softmax(unnormalized_heights, dim=-1)
136
+ heights = min_bin_height + (1 - min_bin_height * num_bins) * heights
137
+ cumheights = torch.cumsum(heights, dim=-1)
138
+ cumheights = F.pad(cumheights, pad=(1, 0), mode="constant", value=0.0)
139
+ cumheights = (top - bottom) * cumheights + bottom
140
+ cumheights[..., 0] = bottom
141
+ cumheights[..., -1] = top
142
+ heights = cumheights[..., 1:] - cumheights[..., :-1]
143
+
144
+ if inverse:
145
+ bin_idx = searchsorted(cumheights, inputs)[..., None]
146
+ else:
147
+ bin_idx = searchsorted(cumwidths, inputs)[..., None]
148
+
149
+ input_cumwidths = cumwidths.gather(-1, bin_idx)[..., 0]
150
+ input_bin_widths = widths.gather(-1, bin_idx)[..., 0]
151
+
152
+ input_cumheights = cumheights.gather(-1, bin_idx)[..., 0]
153
+ delta = heights / widths
154
+ input_delta = delta.gather(-1, bin_idx)[..., 0]
155
+
156
+ input_derivatives = derivatives.gather(-1, bin_idx)[..., 0]
157
+ input_derivatives_plus_one = derivatives[..., 1:].gather(-1, bin_idx)[..., 0]
158
+
159
+ input_heights = heights.gather(-1, bin_idx)[..., 0]
160
+
161
+ if inverse:
162
+ a = (inputs - input_cumheights) * (
163
+ input_derivatives + input_derivatives_plus_one - 2 * input_delta
164
+ ) + input_heights * (input_delta - input_derivatives)
165
+ b = input_heights * input_derivatives - (inputs - input_cumheights) * (
166
+ input_derivatives + input_derivatives_plus_one - 2 * input_delta
167
+ )
168
+ c = -input_delta * (inputs - input_cumheights)
169
+
170
+ discriminant = b.pow(2) - 4 * a * c
171
+ assert (discriminant >= 0).all()
172
+
173
+ root = (2 * c) / (-b - torch.sqrt(discriminant))
174
+ outputs = root * input_bin_widths + input_cumwidths
175
+
176
+ theta_one_minus_theta = root * (1 - root)
177
+ denominator = input_delta + (
178
+ (input_derivatives + input_derivatives_plus_one - 2 * input_delta)
179
+ * theta_one_minus_theta
180
+ )
181
+ derivative_numerator = input_delta.pow(2) * (
182
+ input_derivatives_plus_one * root.pow(2)
183
+ + 2 * input_delta * theta_one_minus_theta
184
+ + input_derivatives * (1 - root).pow(2)
185
+ )
186
+ logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
187
+
188
+ return outputs, -logabsdet
189
+ else:
190
+ theta = (inputs - input_cumwidths) / input_bin_widths
191
+ theta_one_minus_theta = theta * (1 - theta)
192
+
193
+ numerator = input_heights * (
194
+ input_delta * theta.pow(2) + input_derivatives * theta_one_minus_theta
195
+ )
196
+ denominator = input_delta + (
197
+ (input_derivatives + input_derivatives_plus_one - 2 * input_delta)
198
+ * theta_one_minus_theta
199
+ )
200
+ outputs = input_cumheights + numerator / denominator
201
+
202
+ derivative_numerator = input_delta.pow(2) * (
203
+ input_derivatives_plus_one * theta.pow(2)
204
+ + 2 * input_delta * theta_one_minus_theta
205
+ + input_derivatives * (1 - theta).pow(2)
206
+ )
207
+ logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
208
+
209
+ return outputs, logabsdet
app/voiceclone/ov/utils.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ import numpy as np
4
+
5
+
6
+ def get_hparams_from_file(config_path):
7
+ with open(config_path, "r", encoding="utf-8") as f:
8
+ data = f.read()
9
+ config = json.loads(data)
10
+
11
+ hparams = HParams(**config)
12
+ return hparams
13
+
14
+ class HParams:
15
+ def __init__(self, **kwargs):
16
+ for k, v in kwargs.items():
17
+ if type(v) == dict:
18
+ v = HParams(**v)
19
+ self[k] = v
20
+
21
+ def keys(self):
22
+ return self.__dict__.keys()
23
+
24
+ def items(self):
25
+ return self.__dict__.items()
26
+
27
+ def values(self):
28
+ return self.__dict__.values()
29
+
30
+ def __len__(self):
31
+ return len(self.__dict__)
32
+
33
+ def __getitem__(self, key):
34
+ return getattr(self, key)
35
+
36
+ def __setitem__(self, key, value):
37
+ return setattr(self, key, value)
38
+
39
+ def __contains__(self, key):
40
+ return key in self.__dict__
41
+
42
+ def __repr__(self):
43
+ return self.__dict__.__repr__()
44
+
45
+
46
+ def string_to_bits(string, pad_len=8):
47
+ # Convert each character to its ASCII value
48
+ ascii_values = [ord(char) for char in string]
49
+
50
+ # Convert ASCII values to binary representation
51
+ binary_values = [bin(value)[2:].zfill(8) for value in ascii_values]
52
+
53
+ # Convert binary strings to integer arrays
54
+ bit_arrays = [[int(bit) for bit in binary] for binary in binary_values]
55
+
56
+ # Convert list of arrays to NumPy array
57
+ numpy_array = np.array(bit_arrays)
58
+ numpy_array_full = np.zeros((pad_len, 8), dtype=numpy_array.dtype)
59
+ numpy_array_full[:, 2] = 1
60
+ max_len = min(pad_len, len(numpy_array))
61
+ numpy_array_full[:max_len] = numpy_array[:max_len]
62
+ return numpy_array_full
63
+
64
+
65
+ def bits_to_string(bits_array):
66
+ # Convert each row of the array to a binary string
67
+ binary_values = [''.join(str(bit) for bit in row) for row in bits_array]
68
+
69
+ # Convert binary strings to ASCII values
70
+ ascii_values = [int(binary, 2) for binary in binary_values]
71
+
72
+ # Convert ASCII values to characters
73
+ output_string = ''.join(chr(value) for value in ascii_values)
74
+
75
+ return output_string
76
+
77
+
78
+ def split_sentence(text, min_len=10, language_str='[EN]'):
79
+ if language_str in ['EN']:
80
+ sentences = split_sentences_latin(text, min_len=min_len)
81
+ else:
82
+ sentences = split_sentences_zh(text, min_len=min_len)
83
+ return sentences
84
+
85
+ def split_sentences_latin(text, min_len=10):
86
+ """Split Long sentences into list of short ones
87
+
88
+ Args:
89
+ str: Input sentences.
90
+
91
+ Returns:
92
+ List[str]: list of output sentences.
93
+ """
94
+ # deal with dirty sentences
95
+ text = re.sub('[。!?;]', '.', text)
96
+ text = re.sub('[,]', ',', text)
97
+ text = re.sub('[“”]', '"', text)
98
+ text = re.sub('[‘’]', "'", text)
99
+ text = re.sub(r"[\<\>\(\)\[\]\"\«\»]+", "", text)
100
+ text = re.sub('[\n\t ]+', ' ', text)
101
+ text = re.sub('([,.!?;])', r'\1 $#!', text)
102
+ # split
103
+ sentences = [s.strip() for s in text.split('$#!')]
104
+ if len(sentences[-1]) == 0: del sentences[-1]
105
+
106
+ new_sentences = []
107
+ new_sent = []
108
+ count_len = 0
109
+ for ind, sent in enumerate(sentences):
110
+ # print(sent)
111
+ new_sent.append(sent)
112
+ count_len += len(sent.split(" "))
113
+ if count_len > min_len or ind == len(sentences) - 1:
114
+ count_len = 0
115
+ new_sentences.append(' '.join(new_sent))
116
+ new_sent = []
117
+ return merge_short_sentences_latin(new_sentences)
118
+
119
+
120
+ def merge_short_sentences_latin(sens):
121
+ """Avoid short sentences by merging them with the following sentence.
122
+
123
+ Args:
124
+ List[str]: list of input sentences.
125
+
126
+ Returns:
127
+ List[str]: list of output sentences.
128
+ """
129
+ sens_out = []
130
+ for s in sens:
131
+ # If the previous sentence is too short, merge them with
132
+ # the current sentence.
133
+ if len(sens_out) > 0 and len(sens_out[-1].split(" ")) <= 2:
134
+ sens_out[-1] = sens_out[-1] + " " + s
135
+ else:
136
+ sens_out.append(s)
137
+ try:
138
+ if len(sens_out[-1].split(" ")) <= 2:
139
+ sens_out[-2] = sens_out[-2] + " " + sens_out[-1]
140
+ sens_out.pop(-1)
141
+ except:
142
+ pass
143
+ return sens_out
144
+
145
+ def split_sentences_zh(text, min_len=10):
146
+ text = re.sub('[。!?;]', '.', text)
147
+ text = re.sub('[,]', ',', text)
148
+ # 将文本中的换行符、空格和制表符替换为空格
149
+ text = re.sub('[\n\t ]+', ' ', text)
150
+ # 在标点符号后添加一个空格
151
+ text = re.sub('([,.!?;])', r'\1 $#!', text)
152
+ # 分隔句子并去除前后空格
153
+ # sentences = [s.strip() for s in re.split('(。|!|?|;)', text)]
154
+ sentences = [s.strip() for s in text.split('$#!')]
155
+ if len(sentences[-1]) == 0: del sentences[-1]
156
+
157
+ new_sentences = []
158
+ new_sent = []
159
+ count_len = 0
160
+ for ind, sent in enumerate(sentences):
161
+ new_sent.append(sent)
162
+ count_len += len(sent)
163
+ if count_len > min_len or ind == len(sentences) - 1:
164
+ count_len = 0
165
+ new_sentences.append(' '.join(new_sent))
166
+ new_sent = []
167
+ return merge_short_sentences_zh(new_sentences)
168
+
169
+
170
+ def merge_short_sentences_zh(sens):
171
+ # return sens
172
+ """Avoid short sentences by merging them with the following sentence.
173
+
174
+ Args:
175
+ List[str]: list of input sentences.
176
+
177
+ Returns:
178
+ List[str]: list of output sentences.
179
+ """
180
+ sens_out = []
181
+ for s in sens:
182
+ # If the previous sentense is too short, merge them with
183
+ # the current sentence.
184
+ if len(sens_out) > 0 and len(sens_out[-1]) <= 2:
185
+ sens_out[-1] = sens_out[-1] + " " + s
186
+ else:
187
+ sens_out.append(s)
188
+ try:
189
+ if len(sens_out[-1]) <= 2:
190
+ sens_out[-2] = sens_out[-2] + " " + sens_out[-1]
191
+ sens_out.pop(-1)
192
+ except:
193
+ pass
194
+ return sens_out
requirements.txt CHANGED
@@ -11,5 +11,8 @@ numpy
11
  edge-tts>=7.0
12
  deep-translator>=1.11
13
  requests
 
 
 
14
  # optional, only if you set ANTHROPIC_API_KEY
15
  anthropic
 
11
  edge-tts>=7.0
12
  deep-translator>=1.11
13
  requests
14
+ # voice cloning (OpenVoice V2 converter; torch CPU is installed in the Dockerfile)
15
+ librosa>=0.10
16
+ soundfile
17
  # optional, only if you set ANTHROPIC_API_KEY
18
  anthropic
train/finetune_nllb.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fine-tune the translation model on what people taught SwitchLingo.
2
+
3
+ Input: the feedback export (GET /api/feedback/export?token=...), which contains
4
+ - viewer corrections {original, correction, src, tgt}
5
+ - taught sentences {kind: "pair", en, text, lang, dialect}
6
+ Output: a fine-tuned NLLB model, converted to CTranslate2 int8 and (optionally) pushed to the Hub.
7
+ Point the site at it with the Space variable OXT_NLLB_MODEL=<your-repo>.
8
+
9
+ Run on a GPU (Google Colab free T4 or Hugging Face Jobs). A few thousand sentences train in ~15 minutes.
10
+
11
+ pip install torch transformers datasets sentencepiece ctranslate2 huggingface_hub accelerate
12
+ python train/finetune_nllb.py feedback.jsonl --langs lo hmn --push MsThai/switchlingo-nllb-lo
13
+ python train/finetune_nllb.py feedback.jsonl --dry-run # just show what would be trained
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import random
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
24
+ from app.languages import LANGS # noqa: E402
25
+
26
+ BASE = "facebook/nllb-200-distilled-600M"
27
+
28
+
29
+ def load_pairs(path: str, langs: set[str] | None, min_votes: int) -> list[tuple[str, str, str, str]]:
30
+ """(src_lang, tgt_lang, source_text, target_text). Corrections need min_votes distinct voters
31
+ unless trusted; taught pairs are used both directions."""
32
+ votes: dict[tuple, set] = {}
33
+ trusted: set[tuple] = set()
34
+ for line in open(path, encoding="utf-8"):
35
+ try:
36
+ r = json.loads(line)
37
+ except ValueError:
38
+ continue
39
+ if r.get("kind") == "pair":
40
+ if r.get("dialect"):
41
+ continue # dialects get their own model later
42
+ items = [("en", r["lang"], r["en"], r["text"]), (r["lang"], "en", r["text"], r["en"])]
43
+ elif r.get("correction"):
44
+ items = [(r["src"], r["tgt"], r["original"], r["correction"])]
45
+ else:
46
+ continue
47
+ for it in items:
48
+ it = tuple(x.strip() for x in it)
49
+ if not all(it):
50
+ continue
51
+ votes.setdefault(it, set()).add(r.get("voter", "?"))
52
+ if r.get("trusted"):
53
+ trusted.add(it)
54
+ out = [k for k, v in votes.items() if k in trusted or len(v) >= min_votes]
55
+ if langs:
56
+ out = [p for p in out if p[0] in langs or p[1] in langs]
57
+ return [p for p in out if LANGS.get(p[0], {}).get("nllb") and LANGS.get(p[1], {}).get("nllb")]
58
+
59
+
60
+ def main() -> None:
61
+ ap = argparse.ArgumentParser()
62
+ ap.add_argument("export")
63
+ ap.add_argument("--langs", nargs="*", help="only pairs touching these codes, e.g. lo hmn")
64
+ ap.add_argument("--min-votes", type=int, default=2)
65
+ ap.add_argument("--epochs", type=float, default=3)
66
+ ap.add_argument("--lr", type=float, default=5e-5)
67
+ ap.add_argument("--out", default="nllb-switchlingo")
68
+ ap.add_argument("--push", help="Hub repo for the CTranslate2 model")
69
+ ap.add_argument("--dry-run", action="store_true")
70
+ a = ap.parse_args()
71
+
72
+ pairs = load_pairs(a.export, set(a.langs) if a.langs else None, a.min_votes)
73
+ by = {}
74
+ for p in pairs:
75
+ by[f"{p[0]}→{p[1]}"] = by.get(f"{p[0]}→{p[1]}", 0) + 1
76
+ print(f"{len(pairs)} training pairs:", by)
77
+ if a.dry_run or not pairs:
78
+ for p in pairs[:5]:
79
+ print(" ", p)
80
+ return
81
+
82
+ import torch
83
+ from datasets import Dataset
84
+ from transformers import (AutoModelForSeq2SeqLM, AutoTokenizer, DataCollatorForSeq2Seq,
85
+ Seq2SeqTrainer, Seq2SeqTrainingArguments)
86
+ tok = AutoTokenizer.from_pretrained(BASE)
87
+ model = AutoModelForSeq2SeqLM.from_pretrained(BASE)
88
+ random.shuffle(pairs)
89
+
90
+ def enc(ex):
91
+ tok.src_lang, tok.tgt_lang = LANGS[ex["s"]]["nllb"], LANGS[ex["t"]]["nllb"]
92
+ return tok(ex["x"], text_target=ex["y"], truncation=True, max_length=200)
93
+
94
+ ds = Dataset.from_list([{"s": s, "t": t, "x": x, "y": y} for s, t, x, y in pairs]).map(
95
+ enc, remove_columns=["s", "t", "x", "y"])
96
+ args = Seq2SeqTrainingArguments(a.out, num_train_epochs=a.epochs, learning_rate=a.lr,
97
+ per_device_train_batch_size=8, gradient_accumulation_steps=2,
98
+ warmup_ratio=0.1, weight_decay=0.01, save_strategy="no",
99
+ fp16=torch.cuda.is_available(), logging_steps=20, report_to=[])
100
+ Seq2SeqTrainer(model=model, args=args, train_dataset=ds,
101
+ data_collator=DataCollatorForSeq2Seq(tok, model)).train()
102
+ model.save_pretrained(a.out)
103
+ tok.save_pretrained(a.out)
104
+
105
+ import ctranslate2.converters
106
+ ct2 = a.out + "-ct2-int8"
107
+ ctranslate2.converters.TransformersConverter(a.out).convert(ct2, quantization="int8", force=True)
108
+ print("CTranslate2 model:", ct2)
109
+ if a.push:
110
+ from huggingface_hub import HfApi
111
+ HfApi().create_repo(a.push, exist_ok=True, private=True)
112
+ HfApi().upload_folder(folder_path=ct2, repo_id=a.push)
113
+ print(f"Pushed. Set the Space variable OXT_NLLB_MODEL={a.push}")
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()