Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload folder using huggingface_hub
Browse files- Dockerfile +6 -4
- app/engines.py +68 -2
- app/feedback.py +37 -3
- app/pipeline.py +126 -15
- app/server.py +44 -2
- app/static/icon-192.png +0 -0
- app/static/icon-512.png +0 -0
- app/static/index.html +113 -13
- app/static/manifest.webmanifest +19 -0
- app/static/sw.js +4 -0
- app/voiceclone/__init__.py +86 -0
- app/voiceclone/ov/LICENSE +7 -0
- app/voiceclone/ov/__init__.py +1 -0
- app/voiceclone/ov/attentions.py +465 -0
- app/voiceclone/ov/commons.py +160 -0
- app/voiceclone/ov/mel_processing.py +183 -0
- app/voiceclone/ov/models.py +499 -0
- app/voiceclone/ov/modules.py +598 -0
- app/voiceclone/ov/transforms.py +209 -0
- app/voiceclone/ov/utils.py +194 -0
- requirements.txt +3 -0
- train/finetune_nllb.py +117 -0
Dockerfile
CHANGED
|
@@ -1,17 +1,19 @@
|
|
| 1 |
-
#
|
| 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 -
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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">
|
| 73 |
-
<div id="pLink"><input id="url" type="url" placeholder="Facebook, Instagram, YouTube,
|
| 74 |
<div id="pFile" class="hidden">
|
| 75 |
-
<div id="drop" class="drop">
|
| 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.
|
| 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 => ({"&":"&","<":"<",">":">",'"':"""}[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;
|
| 147 |
-
$("#
|
| 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
|
|
|
|
| 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 => ({"&":"&","<":"<",">":">",'"':"""}[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()
|