multimodalart HF Staff commited on
Commit
4b7b1a9
·
1 Parent(s): 6715980

[Admin maintenance] Support new ZeroGPU hardware (#2)

Browse files

- [Admin maintenance] Support new ZeroGPU hardware (49b55aad1900a1ebf92fab0de2db7601d43e0f1f)

README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 😻
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 4.42.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
app.py CHANGED
@@ -1,6 +1,7 @@
 
 
1
  import gradio as gr
2
  import torch
3
- import spaces
4
  import torchaudio
5
  from whisperspeech.vq_stoks import RQBottleneckTransformer
6
  from encodec.utils import convert_audio
@@ -8,19 +9,18 @@ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
8
  from transformers import StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
9
  from threading import Thread
10
  import logging
11
- import os
12
  from generate_audio import (
13
  TTSProcessor,
14
- )
15
  import uuid
16
 
17
 
18
- device = "cuda" if torch.cuda.is_available() else "cpu"
19
  vq_model = RQBottleneckTransformer.load_model(
20
  "whisper-vq-stoks-medium-en+pl-fixed.model"
21
  ).to(device)
22
  # tts = TTSProcessor('cpu')
23
- use_8bit = False
24
  llm_path = "homebrewltd/Llama3.1-s-instruct-2024-08-19-epoch-3"
25
  tokenizer = AutoTokenizer.from_pretrained(llm_path)
26
  model_kwargs = {}
 
1
+ import os
2
+ import spaces
3
  import gradio as gr
4
  import torch
 
5
  import torchaudio
6
  from whisperspeech.vq_stoks import RQBottleneckTransformer
7
  from encodec.utils import convert_audio
 
9
  from transformers import StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
10
  from threading import Thread
11
  import logging
 
12
  from generate_audio import (
13
  TTSProcessor,
14
+ )
15
  import uuid
16
 
17
 
18
+ device = "cuda"
19
  vq_model = RQBottleneckTransformer.load_model(
20
  "whisper-vq-stoks-medium-en+pl-fixed.model"
21
  ).to(device)
22
  # tts = TTSProcessor('cpu')
23
+ use_8bit = False
24
  llm_path = "homebrewltd/Llama3.1-s-instruct-2024-08-19-epoch-3"
25
  tokenizer = AutoTokenizer.from_pretrained(llm_path)
26
  model_kwargs = {}
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- openai-whisper==20231117
2
  IPython
3
  peft
4
  huggingface_hub
@@ -7,15 +7,14 @@ pyarrow
7
  datasets
8
  encodec
9
  soundfile
10
- gradio==4.39.0
11
  transformers
12
  bitsandbytes
13
- torchvision
 
 
14
  vector_quantize_pytorch
15
  webdataset
16
- git+https://github.com/homebrewltd/WhisperSpeech.git
17
- --extra-index-url https://download.pytorch.org/whl/cu121
18
- torch==2.2.0
19
- torchaudio==2.2.0
20
- fsspec==2024.6.1
21
- anyio==4.4.0
 
1
+ openai-whisper
2
  IPython
3
  peft
4
  huggingface_hub
 
7
  datasets
8
  encodec
9
  soundfile
 
10
  transformers
11
  bitsandbytes
12
+ torch==2.8.0
13
+ torchaudio==2.8.0
14
+ torchvision==0.23.0
15
  vector_quantize_pytorch
16
  webdataset
17
+ vocos
18
+ speechbrain<1.0
19
+ fastprogress
20
+ fastcore
 
 
whisperspeech/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "0.8"
whisperspeech/a2wav.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/6. Quality-boosting vocoder.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['Vocoder']
5
+
6
+ # %% ../nbs/6. Quality-boosting vocoder.ipynb 1
7
+ from vocos import Vocos
8
+ from whisperspeech import inference
9
+ import torch
10
+ import torchaudio
11
+
12
+ # %% ../nbs/6. Quality-boosting vocoder.ipynb 2
13
+ class Vocoder:
14
+ def __init__(self, repo_id="charactr/vocos-encodec-24khz", device=None):
15
+ if device is None: device = inference.get_compute_device()
16
+ if device == 'mps': device = 'cpu' # mps does not currently work with vocos, thus only cuda or cpu
17
+ self.device = device
18
+ self.vocos = Vocos.from_pretrained(repo_id).to(device)
19
+
20
+ def is_notebook(self):
21
+ try:
22
+ return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
23
+ except:
24
+ return False
25
+
26
+ @torch.no_grad()
27
+ def decode(self, atoks):
28
+ if len(atoks.shape) == 3:
29
+ b,q,t = atoks.shape
30
+
31
+ atoks = atoks.permute(1,0,2)
32
+ else:
33
+ q,t = atoks.shape
34
+ # on mps we run Vocos on the CPU, make sure it's input is on the correct device
35
+ atoks = atoks.to(self.device)
36
+ # print(atoks.dtype, atoks.device) # uncomment to check dtype and compute_device
37
+ features = self.vocos.codes_to_features(atoks)
38
+ bandwidth_id = torch.tensor({2: 0, 4: 1, 8: 2}[q]).to(self.device) # Move tensor to the same device as model
39
+ return self.vocos.decode(features, bandwidth_id=bandwidth_id)
40
+
41
+ def decode_to_file(self, fname, atoks):
42
+ audio = self.decode(atoks)
43
+ torchaudio.save(fname, audio.cpu(), 24000)
44
+ if self.is_notebook():
45
+ from IPython.display import display, HTML, Audio
46
+ display(HTML(f'<a href="{fname}" target="_blank">Listen to {fname}</a>'))
47
+
48
+ def decode_to_notebook(self, atoks):
49
+ from IPython.display import display, HTML, Audio
50
+
51
+ audio = self.decode(atoks)
52
+ display(Audio(audio.cpu().numpy(), rate=24000))
whisperspeech/benchmark.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/C. Benchmark.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/C. Benchmark.ipynb 2
7
+ import time
8
+ import torch
9
+ from fastcore.script import call_parse
10
+ from whisperspeech.pipeline import Pipeline
11
+ from whisperspeech.inference import get_compute_device
12
+
13
+ # %% ../nbs/C. Benchmark.ipynb 3
14
+ def measure(fun, iterations = 10):
15
+ ts = []
16
+ for x in range(iterations):
17
+ start = time.time()
18
+ fun()
19
+ getattr(torch, get_compute_device()).synchronize()
20
+ ts.append(time.time() - start)
21
+ ts = torch.tensor(ts)
22
+ return ts.mean(), ts.std()
23
+
24
+ @call_parse
25
+ def benchmark(
26
+ t2s_ref='collabora/whisperspeech:t2s-small-en+pl.model',
27
+ s2a_ref='collabora/whisperspeech:s2a-q4-tiny-en+pl.model',
28
+ batch_size : int = 1,
29
+ max_batch_size : int = None,
30
+ no_torch_compile : bool = False,
31
+ s2a_ctx_n : int = None,
32
+ t2s_ctx_n : int = None,
33
+ iterations = 10,
34
+ ):
35
+ max_batch_size = max_batch_size or batch_size
36
+
37
+ pipe = Pipeline(t2s_ref=t2s_ref, s2a_ref=s2a_ref, optimize=False)
38
+
39
+ if t2s_ctx_n:
40
+ pipe.t2s.stoks_len = t2s_ctx_n
41
+ pipe.t2s.decoder.mask = torch.empty(t2s_ctx_n, t2s_ctx_n).fill_(-torch.inf).triu_(1).to(get_compute_device())
42
+
43
+ pipe.t2s.optimize(max_batch_size=max_batch_size, torch_compile=not no_torch_compile)
44
+
45
+ if s2a_ctx_n:
46
+ pipe.s2a.ctx_n = s2a_ctx_n
47
+ pipe.s2a.decoder.mask = torch.empty(s2a_ctx_n, s2a_ctx_n).fill_(-torch.inf).triu_(1).to(get_compute_device())
48
+
49
+ pipe.s2a.optimize(max_batch_size=max_batch_size, torch_compile=not no_torch_compile)
50
+
51
+ txt = "This is the first demo of Whisper Speech, a fully open source text-to-speech model trained by Collabora and Lion on the Juwels supercomputer."
52
+ stoks = torch.zeros(250)
53
+ t = len(stoks)/25
54
+
55
+ def t2s():
56
+ return pipe.t2s.generate(txt, bs=batch_size, show_progress_bar=False)
57
+ def s2a():
58
+ return pipe.s2a.generate(stoks, pipe.default_speaker.unsqueeze(0), bs=batch_size, show_progress_bar=False)
59
+
60
+ # warmup
61
+ t2s()
62
+ s2a()
63
+
64
+ t2s_mean, t2s_std = measure(t2s, iterations=iterations)
65
+ s2a_mean, s2a_std = measure(s2a, iterations=iterations)
66
+ print(f"T2S: {t2s_mean:.3f} ± {t2s_std:.3f} s S2A: {s2a_mean:.3f} ± {s2a_std:.3f} s Total: {t2s_mean+s2a_mean:.3f} s")
67
+ print(f" {t/t2s_mean:.2f}x {t/s2a_mean:.2f}x {t/(t2s_mean+s2a_mean):.2f}x")
whisperspeech/extract_metrics.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/3B. Speech quality metrics extraction.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/3B. Speech quality metrics extraction.ipynb 2
7
+ import sys
8
+ import os
9
+ from os.path import expanduser
10
+ import itertools
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import torch
15
+ import torchaudio
16
+ import torch.nn.functional as F
17
+ from torch.profiler import profile, record_function, ProfilerActivity
18
+
19
+ from fastprogress import progress_bar
20
+ from fastcore.script import *
21
+
22
+ from pyannote.audio import Model
23
+ from brouhaha.pipeline import RegressiveActivityDetectionPipeline
24
+ from . import vq_stoks, utils, vad_merge
25
+ import webdataset as wds
26
+
27
+ from .inference import get_compute_device
28
+
29
+ # %% ../nbs/3B. Speech quality metrics extraction.ipynb 4
30
+ @call_parse
31
+ def prepare_metrics(
32
+ input:str, # audio file webdataset file path
33
+ output:str, # output shard path
34
+ n_samples:int=None, # process a limited amount of samples
35
+
36
+ ):
37
+ device = get_compute_device()
38
+
39
+ model = Model.from_pretrained(expanduser('~/.cache/brouhaha.ckpt'), strict=False)
40
+ snr_pipeline = RegressiveActivityDetectionPipeline(segmentation=model).to(torch.device(device))
41
+
42
+ total = n_samples if n_samples else 'noinfer'
43
+
44
+ if total == 'noinfer':
45
+ import math, time
46
+ start = time.time()
47
+ ds = wds.WebDataset([utils.derived_name(input, 'mvad')]).decode()
48
+ total = math.ceil(sum([len(x[f'max.spk_emb.npy']) for x in ds]))
49
+ print(f"Counting {total} batches: {time.time()-start:.2f}")
50
+
51
+ ds = vad_merge.chunked_audio_dataset([input], 'max').compose(
52
+ wds.to_tuple('__key__', 'rpad', 'gain_shift.npy', 'samples', 'sample_rate'),
53
+ )
54
+
55
+ dl = wds.WebLoader(ds, num_workers=1, batch_size=None)
56
+
57
+ with utils.AtomicTarWriter(output, throwaway=n_samples is not None) as sink:
58
+ for keys, rpad, gain_shift, samples, sr in progress_bar(dl, total=total):
59
+ with torch.no_grad():
60
+ snd = samples
61
+ if rpad > 0: snd = snd[:-rpad]
62
+ snd = (snd - gain_shift[1]) * gain_shift[0]
63
+ snd = snd.unsqueeze(0).to(device)
64
+
65
+ res = snr_pipeline({
66
+ "sample_rate": sr, "waveform": snd
67
+ })
68
+
69
+ s = {
70
+ "__key__": keys,
71
+ "snr_c50.npy": np.array([res['snr'].mean(), res['c50'].mean()])
72
+ }
73
+ sink.write(s)
74
+ sys.stdout.write("\n")
whisperspeech/extract_spk_emb.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/2A. Speaker Embeddings.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/2A. Speaker Embeddings.ipynb 3
7
+ import os
8
+ from os.path import expanduser
9
+ import sys
10
+
11
+ from fastprogress import progress_bar
12
+ from fastcore.script import *
13
+ import webdataset as wds
14
+ import torch
15
+ import torch.nn.functional as F
16
+ from torch.utils.data.dataloader import DataLoader
17
+
18
+ from . import vad, utils
19
+
20
+ from speechbrain.pretrained import EncoderClassifier
21
+ from .inference import get_compute_device
22
+
23
+ # %% ../nbs/2A. Speaker Embeddings.ipynb 5
24
+ def calc_len(x):
25
+ x['seconds'] = torch.tensor(x['tend'] - x['tstart'])
26
+ return x
27
+
28
+ def chunked_dataset(input, bs=16):
29
+ ds = utils.vad_dataset([input]).compose(
30
+ utils.resampler(16000, 'samples_16k'),
31
+ wds.map(calc_len),
32
+ wds.to_tuple('__key__', 'samples_16k', 'seconds'),
33
+ wds.batched(bs),
34
+ )
35
+ dl = DataLoader(ds, num_workers=1, batch_size=None)
36
+ return dl
37
+
38
+ # %% ../nbs/2A. Speaker Embeddings.ipynb 13
39
+ @call_parse
40
+ def process_shard(
41
+ input:str, # input shard URL/path
42
+ output:str, # output shard URL/path
43
+ batch_size:int=16, # batch size
44
+ n_samples:int=None, # limit the number of samples (useful for quick benchmarking)
45
+ ):
46
+ device = get_compute_device()
47
+ if n_samples is None: total = 'noinfer'
48
+ else: total = n_samples // batch_size
49
+
50
+ dl = chunked_dataset(input, bs=batch_size)
51
+
52
+ classifier = EncoderClassifier.from_hparams("speechbrain/spkrec-ecapa-voxceleb",
53
+ savedir=expanduser("~/.cache/speechbrain/"),
54
+ run_opts = {"device": device})
55
+
56
+ with utils.AtomicTarWriter(output) as sink:
57
+ for keys, samples, seconds in progress_bar(dl, total=total):
58
+ with torch.no_grad():
59
+ embs = classifier.encode_batch(samples, wav_lens=seconds/30).squeeze(1)
60
+ for key, emb in zip(keys, embs):
61
+ sink.write({
62
+ "__key__": key,
63
+ "spk_emb.npy": emb.cpu().numpy(),
64
+ })
65
+ if n_samples is not None:
66
+ sink.abort = True
67
+ sys.stdout.write("\n")
whisperspeech/extract_stoks.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/3B. Semantic token extraction.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/3B. Semantic token extraction.ipynb 2
7
+ import sys
8
+ import os
9
+ from os.path import expanduser
10
+ import itertools
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import torch
15
+ import torchaudio
16
+ import torch.nn.functional as F
17
+ from torch.profiler import profile, record_function, ProfilerActivity
18
+
19
+ from fastprogress import progress_bar
20
+ from fastcore.script import *
21
+
22
+ from speechbrain.pretrained import EncoderClassifier
23
+ from . import vq_stoks, utils, vad_merge
24
+ import webdataset as wds
25
+
26
+ from .inference import get_compute_device
27
+
28
+ # %% ../nbs/3B. Semantic token extraction.ipynb 7
29
+ @call_parse
30
+ def prepare_stoks(
31
+ input:str, # audio file webdataset file path
32
+ output:str, # output shard path
33
+ vq_model:str="collabora/spear-tts-pytorch:whisper-vq-stoks-v2.model", # the model path (use repo_id:filename to download it from hugginface)
34
+ n_samples:int=None, # process a limited amount of samples
35
+ batch_size:int=64, # process several segments at once
36
+ kind:str="max", # could be eqvad to get more uniform chunk lengths
37
+
38
+ ):
39
+ device = get_compute_device()
40
+ vq_model = vq_stoks.RQBottleneckTransformer.load_model(vq_model).to(device)
41
+ vq_model.ensure_whisper()
42
+
43
+ spk_classifier = EncoderClassifier.from_hparams("speechbrain/spkrec-ecapa-voxceleb",
44
+ savedir=expanduser("~/.cache/speechbrain/"),
45
+ run_opts = {"device": device})
46
+
47
+ total = n_samples//batch_size if n_samples else 'noinfer'
48
+
49
+ if total == 'noinfer':
50
+ import math, time
51
+ start = time.time()
52
+ ds = wds.WebDataset([utils.derived_name(input, 'mvad')]).decode()
53
+ total = math.ceil(sum([len(x[f'{kind}.spk_emb.npy']) for x in ds])/batch_size)
54
+ print(f"Counting {total} batches: {time.time()-start:.2f}")
55
+
56
+ ds = vad_merge.chunked_audio_dataset([input], kind).compose(
57
+ utils.resampler(16000, 'samples_16k'),
58
+ wds.to_tuple('__key__', 'rpad_s', 'samples_16k'),
59
+ wds.batched(64),
60
+ )
61
+
62
+ dl = wds.WebLoader(ds, num_workers=1, batch_size=None).unbatched().batched(batch_size)
63
+
64
+ with utils.AtomicTarWriter(output, throwaway=n_samples is not None) as sink:
65
+ for keys, rpad_ss, samples16k in progress_bar(dl, total=total):
66
+ with torch.no_grad():
67
+ samples16k = samples16k.to(device).to(torch.float16)
68
+ stoks = vq_model.encode_audio(samples16k).cpu().numpy().astype(np.int16)
69
+ spk_embs = spk_classifier.encode_batch(
70
+ samples16k, wav_lens=torch.tensor(30 - rpad_ss, dtype=torch.float)/30)[:,0,:].cpu().numpy()
71
+ for key, rpad_s, _stoks, spk_emb in zip(keys, rpad_ss, stoks, spk_embs):
72
+ _stoks = _stoks[:int((30-rpad_s) * 25 + .5)]
73
+ s = {
74
+ "__key__": key,
75
+ "stoks.npy": _stoks,
76
+ }
77
+ if spk_emb is not None: s["spk_emb.npy"] = spk_emb
78
+ sink.write(s)
79
+ sys.stdout.write("\n")
whisperspeech/fetch_models.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/0. Download models.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/0. Download models.ipynb 1
7
+ from encodec.model import EncodecModel
8
+ from whisperspeech import utils
9
+ from fastcore.script import call_parse
10
+ import whisperx
11
+ import whisper
12
+ from speechbrain.pretrained import EncoderClassifier
13
+ from os.path import expanduser
14
+ import urllib.request
15
+
16
+ # %% ../nbs/0. Download models.ipynb 3
17
+ def load_whisperx(model, lang):
18
+ try:
19
+ whisperx.asr.load_model(model, "cpu", compute_type="float16", language=lang)
20
+ except ValueError as exc:
21
+ print(exc.args[0])
22
+ if exc.args[0] != "Requested float16 compute type, but the target device or backend do not support efficient float16 computation.":
23
+ raise
24
+
25
+ @call_parse
26
+ def main():
27
+ EncodecModel.encodec_model_24khz()
28
+ whisper.load_model('base.en')
29
+ whisper.load_model('small.en')
30
+ whisper.load_model('medium')
31
+ whisperx.vad.load_vad_model('cpu')
32
+ load_whisperx('small.en', 'en')
33
+ load_whisperx('medium.en', 'en')
34
+ load_whisperx('medium', 'en')
35
+ load_whisperx('large-v3', 'en')
36
+ EncoderClassifier.from_hparams(source="speechbrain/spkrec-ecapa-voxceleb",
37
+ savedir=expanduser("~/.cache/speechbrain/"))
38
+ urllib.request.urlretrieve('https://github.com/marianne-m/brouhaha-vad/raw/main/models/best/checkpoints/best.ckpt',
39
+ expanduser('~/.cache/brouhaha.ckpt'))
whisperspeech/inference.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/D. Common inference utilities.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['get_compute_device']
5
+
6
+ # %% ../nbs/D. Common inference utilities.ipynb 1
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from huggingface_hub import hf_hub_download
10
+
11
+ from contextlib import nullcontext
12
+
13
+ # %% ../nbs/D. Common inference utilities.ipynb 2
14
+ def get_default_compute_device():
15
+ if torch.cuda.is_available() and (torch.version.cuda or torch.version.hip):
16
+ return 'cuda'
17
+ elif torch.backends.mps.is_available():
18
+ return 'mps'
19
+ else:
20
+ return 'cpu'
21
+
22
+ preferred_device = None
23
+
24
+ # %% ../nbs/D. Common inference utilities.ipynb 3
25
+ def get_compute_device():
26
+ global preferred_device
27
+ if preferred_device is None: preferred_device = get_default_compute_device()
28
+ return preferred_device
29
+
30
+ # %% ../nbs/D. Common inference utilities.ipynb 4
31
+ def load_model(ref=None, spec=None, device='cpu'):
32
+ if spec is not None: return spec
33
+ if ":" in ref:
34
+ repo_id, filename = ref.split(":", 1)
35
+ local_filename = hf_hub_download(repo_id=repo_id, filename=filename)
36
+ else:
37
+ local_filename = ref
38
+ return torch.load(local_filename, map_location=device)
39
+
40
+ # %% ../nbs/D. Common inference utilities.ipynb 5
41
+ def inference_context():
42
+ if torch.cuda.is_available():
43
+ return torch.backends.cuda.sdp_kernel(enable_flash=False, enable_mem_efficient=False, enable_math=True)
44
+ else:
45
+ return nullcontext()
46
+
47
+ # from https://github.com/pytorch-labs/gpt-fast/blob/main/generate.py
48
+ def multinomial_sample_one_no_sync(probs_sort): # Does multinomial sampling without a cuda synchronization
49
+ q = torch.empty_like(probs_sort).exponential_(1)
50
+ return torch.argmax(probs_sort / q, dim=-1, keepdim=True).to(dtype=torch.int)
51
+
52
+ def logits_to_probs(logits, T=1.0, top_k=None):
53
+ logits = logits / max(T, 1e-5)
54
+
55
+ if top_k is not None:
56
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
57
+ pivot = v.select(-1, -1).unsqueeze(-1)
58
+ logits = torch.where(logits < pivot, -float("Inf"), logits)
59
+
60
+ probs = torch.nn.functional.softmax(logits, dim=-1)
61
+ return probs
62
+
63
+ def sample(logits, T=1.0, top_k=None):
64
+ probs = logits_to_probs(logits, T, top_k)
65
+ idx_next = multinomial_sample_one_no_sync(probs)
66
+ return idx_next
whisperspeech/languages.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/B. Languages.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['to_id']
5
+
6
+ # %% ../nbs/B. Languages.ipynb 3
7
+ LANGUAGES = {
8
+ "en": "english",
9
+ "zh": "chinese",
10
+ "de": "german",
11
+ "es": "spanish",
12
+ "ru": "russian",
13
+ "ko": "korean",
14
+ "fr": "french",
15
+ "ja": "japanese",
16
+ "pt": "portuguese",
17
+ "tr": "turkish",
18
+ "pl": "polish",
19
+ "ca": "catalan",
20
+ "nl": "dutch",
21
+ "ar": "arabic",
22
+ "sv": "swedish",
23
+ "it": "italian",
24
+ "id": "indonesian",
25
+ "hi": "hindi",
26
+ "fi": "finnish",
27
+ "vi": "vietnamese",
28
+ "he": "hebrew",
29
+ "uk": "ukrainian",
30
+ "el": "greek",
31
+ "ms": "malay",
32
+ "cs": "czech",
33
+ "ro": "romanian",
34
+ "da": "danish",
35
+ "hu": "hungarian",
36
+ "ta": "tamil",
37
+ "no": "norwegian",
38
+ "th": "thai",
39
+ "ur": "urdu",
40
+ "hr": "croatian",
41
+ "bg": "bulgarian",
42
+ "lt": "lithuanian",
43
+ "la": "latin",
44
+ "mi": "maori",
45
+ "ml": "malayalam",
46
+ "cy": "welsh",
47
+ "sk": "slovak",
48
+ "te": "telugu",
49
+ "fa": "persian",
50
+ "lv": "latvian",
51
+ "bn": "bengali",
52
+ "sr": "serbian",
53
+ "az": "azerbaijani",
54
+ "sl": "slovenian",
55
+ "kn": "kannada",
56
+ "et": "estonian",
57
+ "mk": "macedonian",
58
+ "br": "breton",
59
+ "eu": "basque",
60
+ "is": "icelandic",
61
+ "hy": "armenian",
62
+ "ne": "nepali",
63
+ "mn": "mongolian",
64
+ "bs": "bosnian",
65
+ "kk": "kazakh",
66
+ "sq": "albanian",
67
+ "sw": "swahili",
68
+ "gl": "galician",
69
+ "mr": "marathi",
70
+ "pa": "punjabi",
71
+ "si": "sinhala",
72
+ "km": "khmer",
73
+ "sn": "shona",
74
+ "yo": "yoruba",
75
+ "so": "somali",
76
+ "af": "afrikaans",
77
+ "oc": "occitan",
78
+ "ka": "georgian",
79
+ "be": "belarusian",
80
+ "tg": "tajik",
81
+ "sd": "sindhi",
82
+ "gu": "gujarati",
83
+ "am": "amharic",
84
+ "yi": "yiddish",
85
+ "lo": "lao",
86
+ "uz": "uzbek",
87
+ "fo": "faroese",
88
+ "ht": "haitian creole",
89
+ "ps": "pashto",
90
+ "tk": "turkmen",
91
+ "nn": "nynorsk",
92
+ "mt": "maltese",
93
+ "sa": "sanskrit",
94
+ "lb": "luxembourgish",
95
+ "my": "myanmar",
96
+ "bo": "tibetan",
97
+ "tl": "tagalog",
98
+ "mg": "malagasy",
99
+ "as": "assamese",
100
+ "tt": "tatar",
101
+ "haw": "hawaiian",
102
+ "ln": "lingala",
103
+ "ha": "hausa",
104
+ "ba": "bashkir",
105
+ "jw": "javanese",
106
+ "su": "sundanese",
107
+ }
108
+
109
+ # %% ../nbs/B. Languages.ipynb 4
110
+ # language code lookup by name, with a few language aliases
111
+ TO_LANGUAGE_CODE = {
112
+ **{language: code for code, language in LANGUAGES.items()},
113
+ "burmese": "my",
114
+ "valencian": "ca",
115
+ "flemish": "nl",
116
+ "haitian": "ht",
117
+ "letzeburgesch": "lb",
118
+ "pushto": "ps",
119
+ "panjabi": "pa",
120
+ "moldavian": "ro",
121
+ "moldovan": "ro",
122
+ "sinhalese": "si",
123
+ "castilian": "es",
124
+ }
125
+
126
+ # %% ../nbs/B. Languages.ipynb 5
127
+ languages = tuple(LANGUAGES.keys())
128
+
129
+ # %% ../nbs/B. Languages.ipynb 6
130
+ def to_id(lang):
131
+ return languages.index(TO_LANGUAGE_CODE.get(lang, lang))
whisperspeech/modules.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/A. Neural modules.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['LayerNorm', 'LinearHead', 'QueryHead', 'init_transformer', 'sinusoids', 'MultiHeadAttention',
5
+ 'ResidualAttentionBlock', 'BaseDecoder', 'EmbeddingProjector', 'FlexEmbeddings']
6
+
7
+ # %% ../nbs/A. Neural modules.ipynb 2
8
+ import torch
9
+ import numpy as np
10
+ import math
11
+
12
+ from torch import Tensor, nn
13
+ import torch.nn.functional as F
14
+ from typing import Dict, Iterable, Optional
15
+
16
+ # import xformers.ops as xops
17
+
18
+ # %% ../nbs/A. Neural modules.ipynb 3
19
+ # Code in this file is mostly borrowed from
20
+ # https://github.com/openai/whisper/blob/main/whisper/model.py
21
+ # and is under the MIT License
22
+
23
+ class LayerNorm(nn.LayerNorm):
24
+ def forward(self, x):
25
+ return super().forward(x.float()).type(x.dtype)
26
+
27
+ # Used in μP to initialize the weights and configure the optimizer
28
+ # These two layers map the transformer width into a fixed dimension
29
+ class LinearHead(nn.Linear):
30
+ pass
31
+
32
+ class QueryHead(nn.Linear):
33
+ pass
34
+
35
+ # based on https://github.com/karpathy/minGPT/blob/master/mingpt/model.py#L163
36
+ def init_transformer(m):
37
+ if isinstance(m, (nn.Linear, nn.Embedding)):
38
+ torch.nn.init.trunc_normal_(m.weight, std=.02)
39
+ if isinstance(m, nn.Linear) and m.bias is not None:
40
+ torch.nn.init.constant_(m.bias, 0)
41
+ elif isinstance(m, nn.LayerNorm):
42
+ torch.nn.init.constant_(m.bias, 0)
43
+ torch.nn.init.constant_(m.weight, 1.0)
44
+
45
+ # %% ../nbs/A. Neural modules.ipynb 4
46
+ def sinusoids(length, channels, max_timescale=10000):
47
+ """Returns sinusoids for positional embedding"""
48
+ assert channels % 2 == 0
49
+ log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
50
+ inv_timescales = torch.exp(-log_timescale_increment * torch.arange(channels // 2))
51
+ scaled_time = torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
52
+ return torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1)
53
+
54
+ # %% ../nbs/A. Neural modules.ipynb 5
55
+ class MultiHeadAttention(nn.Module):
56
+ def __init__(self, n_state: int, n_head: int, qk_scale: float = 1, rope: bool = False, cross=False):
57
+ super().__init__()
58
+ self.n_state = n_state
59
+ self.n_head = n_head
60
+ self.sqrt_qk_scale = math.sqrt(qk_scale)
61
+ self.query = QueryHead(n_state, n_state)
62
+ self.key = nn.Linear(n_state, n_state, bias=False)
63
+ self.value = nn.Linear(n_state, n_state)
64
+ self.out = nn.Linear(n_state, n_state)
65
+ self.cross = cross
66
+ self.query_subsampling = 1
67
+ self.key_subsampling = 1
68
+
69
+ self.cached_kvx = None
70
+ self.register_buffer('k_cache', None)
71
+ self.register_buffer('v_cache', None)
72
+
73
+ self.rotary = None
74
+ if rope:
75
+ self.rotary = Rotary(n_state // n_head)
76
+ self.qkv = None
77
+ self.kv = None
78
+
79
+ def setup_kv_cache(self, max_batch_size, max_seq_len, dtype=torch.float32):
80
+ cache_shape = (max_batch_size, self.n_head, max_seq_len, self.n_state//self.n_head)
81
+ self.k_cache = torch.zeros(cache_shape, dtype=dtype, device=self.key.weight.device)
82
+ self.v_cache = torch.zeros(cache_shape, dtype=dtype, device=self.value.weight.device)
83
+
84
+ def merge_linears(self, layers, mults):
85
+ bias = [x.bias for x in layers if x.bias is not None][0]
86
+ din, dout = layers[0].weight.shape
87
+ new = nn.Linear(din, len(layers) * dout).to(layers[0].weight.device)
88
+ with torch.no_grad():
89
+ new.weight[:] = torch.cat([x.weight * m for x,m in zip(layers, mults)])
90
+ new.bias[:] = torch.cat([torch.zeros_like(bias) if x.bias is None else x.bias * m for x, m in zip(layers, mults)])
91
+ return new
92
+
93
+ def convert_for_eval(self):
94
+ if self.qkv or self.kv: raise AttributeError("already converted")
95
+
96
+ self.odim = self.key.weight.shape[1]
97
+ if self.cross:
98
+ self.q = self.merge_linears([self.query], [self.sqrt_qk_scale])
99
+ self.kv = self.merge_linears([self.key, self.value],
100
+ [self.sqrt_qk_scale, 1])
101
+ else:
102
+ self.qkv = self.merge_linears([self.query, self.key, self.value],
103
+ [self.sqrt_qk_scale, self.sqrt_qk_scale, 1])
104
+
105
+ def split_heads(self, x, x_positions, rope=False, subsampling=1):
106
+ x = x.view(*x.shape[:2], self.n_head, -1)
107
+ if rope:
108
+ x = rope_rotate(x, x_positions * subsampling, *self.rotary(x))
109
+ return x.permute(0, 2, 1, 3)
110
+
111
+ def forward(
112
+ self,
113
+ qx,
114
+ q_positions,
115
+ kvx,
116
+ kv_positions,
117
+ causal = False,
118
+ mask=None,
119
+ ):
120
+ if self.k_cache is not None:
121
+ assert qx.shape[0] <= self.k_cache.shape[0], "please pass in a larger max_batch_size to setup_kv_cache"
122
+ if self.qkv:
123
+ q,k,v = self.qkv(qx).split(self.odim, dim=-1)
124
+ elif self.kv:
125
+ q = self.q(qx)
126
+ k,v = self.kv(kvx).split(self.odim, dim=-1)
127
+ else:
128
+ q,k,v = None,None,None
129
+
130
+ if q is None: q = self.query(qx) * self.sqrt_qk_scale
131
+ q = self.split_heads(q, q_positions, rope = self.rotary, subsampling = self.query_subsampling)
132
+
133
+ if kvx is not self.cached_kvx:
134
+ if k is None: k = self.key(kvx) * self.sqrt_qk_scale
135
+ k = self.split_heads(k, kv_positions, rope = self.rotary, subsampling = self.key_subsampling)
136
+ if v is None: v = self.value(kvx)
137
+ v = self.split_heads(v, kv_positions)
138
+ if self.k_cache is not None:
139
+ self.k_cache[:k.shape[0],:,kv_positions] = k
140
+ self.v_cache[:v.shape[0],:,kv_positions] = v
141
+
142
+ if self.k_cache is not None:
143
+ k, v = self.k_cache[:k.shape[0]], self.v_cache[:v.shape[0]]
144
+
145
+ if mask is not None:
146
+ mask = mask[q_positions,:k.shape[-2]]
147
+
148
+ wv = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0, is_causal=causal)
149
+
150
+ return self.out(wv.permute(0, 2, 1, 3).flatten(start_dim=2))
151
+
152
+ # %% ../nbs/A. Neural modules.ipynb 6
153
+ # modified from https://blog.eleuther.ai/rotary-embeddings/
154
+
155
+ import torch
156
+
157
+ class Rotary(torch.nn.Module):
158
+ def __init__(self, dim, base=10000):
159
+ super().__init__()
160
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
161
+ self.register_buffer("inv_freq", inv_freq)
162
+ self.seq_len_cached = None
163
+ self.cos_cached = None
164
+ self.sin_cached = None
165
+
166
+ def forward(self, x, seq_dim=1):
167
+ seq_len = x.shape[seq_dim]
168
+ if not self.seq_len_cached or seq_len > self.seq_len_cached:
169
+ self.seq_len_cached = 2500
170
+ # self.seq_len_cached = seq_len
171
+
172
+ t = torch.arange(self.seq_len_cached, device=x.device).type_as(self.inv_freq)
173
+ freqs = torch.einsum("i,j->ij", t, self.inv_freq)
174
+ emb = torch.cat((freqs, freqs), dim=-1).to(x.device)
175
+ self.cos_cached = emb.cos()[None, :, None, :]
176
+ self.sin_cached = emb.sin()[None, :, None, :]
177
+ return self.cos_cached, self.sin_cached
178
+
179
+
180
+ # rotary pos emb helpers:
181
+ def rotate_half(x):
182
+ x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :]
183
+ return torch.cat(
184
+ (-x2, x1), dim=len(x.shape)-1
185
+ )
186
+
187
+ def rope_rotate(x, positions, cos, sin):
188
+ return x * cos[:,positions] + rotate_half(x) * sin[:,positions]
189
+
190
+ # %% ../nbs/A. Neural modules.ipynb 7
191
+ class ResidualAttentionBlock(nn.Module):
192
+ def __init__(self, n_state: int, n_head: int, cross_attention: bool = False, rope: bool = False,
193
+ qk_scale: float = 1, ffn_mult: int = 4):
194
+ super().__init__()
195
+ self.attn = MultiHeadAttention(n_state, n_head, qk_scale=qk_scale, rope=rope)
196
+ self.attn_ln = LayerNorm(n_state)
197
+
198
+ self.cross_attn = (
199
+ MultiHeadAttention(n_state, n_head, qk_scale=qk_scale, rope=rope, cross=True) if cross_attention else None
200
+ )
201
+ self.cross_attn_ln = LayerNorm(n_state) if cross_attention else None
202
+
203
+ n_mlp = n_state * ffn_mult
204
+ self.mlp = nn.Sequential(
205
+ nn.Linear(n_state, n_mlp), nn.GELU(), nn.Linear(n_mlp, n_state)
206
+ )
207
+ self.mlp_ln = LayerNorm(n_state)
208
+
209
+ def setup_kv_cache(self, max_batch_size, max_seq_len, max_cross_seq_len=None):
210
+ self.attn.setup_kv_cache(max_batch_size, max_seq_len)
211
+ if self.cross_attn:
212
+ self.cross_attn.setup_kv_cache(max_batch_size, max_cross_seq_len)
213
+
214
+ def forward(
215
+ self,
216
+ x: Tensor,
217
+ x_positions: Tensor = None,
218
+ xa: Optional[Tensor] = None,
219
+ xa_positions: Optional[Tensor] = None,
220
+ causal = False,
221
+ mask=None,
222
+ ):
223
+ lnx = self.attn_ln(x)
224
+ x = x + self.attn(lnx, x_positions, lnx, x_positions, causal=causal, mask=mask)
225
+ if self.cross_attn:
226
+ lnx = self.cross_attn_ln(x)
227
+ x = x + self.cross_attn(lnx, x_positions, xa, xa_positions)
228
+ x = x + self.mlp(self.mlp_ln(x))
229
+ return x
230
+
231
+ # %% ../nbs/A. Neural modules.ipynb 8
232
+ class BaseDecoder(nn.Module):
233
+ def __init__(self, depth=6, n_head=6, width=384, qk_scale=1, ffn_mult=4, length=2250, rope=False):
234
+ super().__init__()
235
+ self.length = length
236
+ self.width = width
237
+ self.layers = nn.ModuleList([
238
+ ResidualAttentionBlock(
239
+ self.width, n_head, qk_scale=qk_scale, ffn_mult=ffn_mult, cross_attention=True, rope=rope
240
+ ) for _ in range(math.floor(depth))
241
+ ])
242
+
243
+ self.ln_post = LayerNorm(width)
244
+
245
+ mask = torch.empty(length, length).fill_(-torch.inf).triu_(1)
246
+ self.register_buffer("mask", mask, persistent=False)
247
+
248
+ def forward(self, x, x_positions, xenc, xenc_positions):
249
+ for i,l in enumerate(self.layers):
250
+ x = l(x, x_positions, xenc, xenc_positions, causal=self.training, mask=self.mask if not self.training else None)
251
+
252
+ x = self.ln_post(x)
253
+
254
+ return x
255
+
256
+ # %% ../nbs/A. Neural modules.ipynb 9
257
+ class EmbeddingProjector(nn.Linear):
258
+ pass
259
+
260
+ class FlexEmbeddings(nn.Module):
261
+ def __init__(self, codes, width, special_codes=None, frozen_width=None, special_embedding=None, unembed=True):
262
+ super().__init__()
263
+ self.codes = codes
264
+ self.special_codes = special_codes
265
+ if frozen_width is None: frozen_width = width
266
+
267
+ self.main = nn.Embedding(codes, frozen_width or width)
268
+ self.emb_to_hidden = EmbeddingProjector(frozen_width, width) if frozen_width != width else None
269
+ self.hidden_to_emb = EmbeddingProjector(width, frozen_width) if unembed and frozen_width != width else None
270
+ if special_codes:
271
+ self.special = special_embedding or nn.Embedding(special_codes, width)
272
+
273
+ self.register_buffer('merged_in', None)
274
+ self.register_buffer('merged_out', None)
275
+ self.register_buffer('bias_out', None)
276
+
277
+ def set_frozen_embeddings(self, values):
278
+ with torch.no_grad():
279
+ self.main.weight[:] = values
280
+ self.main.lr_scale = 0
281
+
282
+ @torch.no_grad()
283
+ def convert_for_eval(self):
284
+ if not self.special_codes: return
285
+ # in
286
+ main_w = self.main.weight
287
+ if self.emb_to_hidden is not None: main_w = self.emb_to_hidden(main_w)
288
+ weight = torch.cat([main_w, self.special.weight], dim=0)
289
+ self.merged_in = nn.Embedding(*weight.shape, _weight=weight)
290
+
291
+ # out
292
+ weight = self.main.weight
293
+ if self.hidden_to_emb: weight = weight @ self.hidden_to_emb.weight
294
+ self.merged_out = torch.cat([weight.T, self.special.weight.T], dim=1).T.contiguous() # T is for F.linear
295
+ if self.hidden_to_emb:
296
+ self.bias_out = torch.cat([
297
+ self.hidden_to_emb.bias @ self.main.weight.T,
298
+ torch.zeros(self.special.weight.shape[0], device=weight.device, dtype=weight.dtype)
299
+ ], dim=0)
300
+ else:
301
+ self.bias_out = None
302
+
303
+ def forward(self, toks):
304
+ if not self.training and self.merged_in is not None:
305
+ return self.merged_in(toks)
306
+
307
+ if self.special_codes:
308
+ special_mask = toks >= self.codes
309
+ embs = self.main(torch.where(special_mask, 0, toks))
310
+ else:
311
+ embs = self.main(toks)
312
+
313
+ if self.emb_to_hidden: embs = self.emb_to_hidden(embs)
314
+
315
+ if self.special_codes:
316
+ embs[special_mask] = self.special(toks[special_mask] - self.codes).to(embs.dtype)
317
+
318
+ return embs
319
+
320
+ def unembed(self, embs):
321
+ if not self.training and self.merged_out is not None:
322
+ return F.linear(embs, self.merged_out, self.bias_out) # embs @ self.merged_out + self.bias_out
323
+
324
+ orig_embs = embs
325
+ if self.hidden_to_emb: embs = self.hidden_to_emb(embs)
326
+
327
+ main_logits = (embs @ self.main.weight.to(embs.dtype).T).float()
328
+
329
+ if not self.special_codes:
330
+ return main_logits
331
+
332
+ special_logits = (orig_embs @ self.special.weight.to(orig_embs.dtype).T).float()
333
+ return torch.cat([main_logits, special_logits], dim=-1)
whisperspeech/pipeline.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/7. Pipeline.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['Pipeline']
5
+
6
+ # %% ../nbs/7. Pipeline.ipynb 1
7
+ from os.path import expanduser
8
+ import torch
9
+ from whisperspeech.t2s_up_wds_mlang_enclm import TSARTransformer
10
+ from whisperspeech.s2a_delar_mup_wds_mlang import SADelARTransformer
11
+ from whisperspeech.a2wav import Vocoder
12
+ from whisperspeech import inference, s2a_delar_mup_wds_mlang_cond
13
+ import traceback
14
+ from pathlib import Path
15
+
16
+ # %% ../nbs/7. Pipeline.ipynb 2
17
+ class Pipeline:
18
+ default_speaker = torch.tensor(
19
+ [-0.2929, -0.4503, 0.4155, -0.1417, 0.0473, -0.1624, -0.2322, 0.7071,
20
+ 0.4800, 0.5496, 0.0410, 0.6236, 0.4729, 0.0587, 0.2194, -0.0466,
21
+ -0.3036, 0.0497, 0.5028, -0.1703, 0.5039, -0.6464, 0.3857, -0.7350,
22
+ -0.1605, 0.4808, 0.5397, -0.4851, 0.1774, -0.8712, 0.5789, 0.1785,
23
+ -0.1417, 0.3039, 0.4232, -0.0186, 0.2685, 0.6153, -0.3103, -0.5706,
24
+ -0.4494, 0.3394, -0.6184, -0.3617, 1.1041, -0.1178, -0.1885, 0.1997,
25
+ 0.5571, -0.2906, -0.0477, -0.4048, -0.1062, 1.4779, 0.1639, -0.3712,
26
+ -0.1776, -0.0568, -0.6162, 0.0110, -0.0207, -0.1319, -0.3854, 0.7248,
27
+ 0.0343, 0.5724, 0.0670, 0.0486, -0.3813, 0.1738, 0.3017, 1.0502,
28
+ 0.1550, 0.5708, 0.0366, 0.5093, 0.0294, -0.7091, -0.8220, -0.1583,
29
+ -0.2343, 0.1366, 0.7372, -0.0631, 0.1505, 0.4600, -0.1252, -0.5245,
30
+ 0.7523, -0.0386, -0.2587, 1.0066, -0.2037, 0.1617, -0.3800, 0.2790,
31
+ 0.0184, -0.5111, -0.7291, 0.1627, 0.2367, -0.0192, 0.4822, -0.4458,
32
+ 0.1457, -0.5884, 0.1909, 0.2563, -0.2035, -0.0377, 0.7771, 0.2139,
33
+ 0.3801, 0.6047, -0.6043, -0.2563, -0.0726, 0.3856, 0.3217, 0.0823,
34
+ -0.1302, 0.3287, 0.5693, 0.2453, 0.8231, 0.0072, 1.0327, 0.6065,
35
+ -0.0620, -0.5572, 0.5220, 0.2485, 0.1520, 0.0222, -0.2179, -0.7392,
36
+ -0.3855, 0.1822, 0.1042, 0.7133, 0.3583, 0.0606, -0.0424, -0.9189,
37
+ -0.4882, -0.5480, -0.5719, -0.1660, -0.3439, -0.5814, -0.2542, 0.0197,
38
+ 0.4942, 0.0915, -0.0420, -0.0035, 0.5578, 0.1051, -0.0891, 0.2348,
39
+ 0.6876, -0.6685, 0.8215, -0.3692, -0.3150, -0.0462, -0.6806, -0.2661,
40
+ -0.0308, -0.0050, 0.6756, -0.1647, 1.0734, 0.0049, 0.4969, 0.0259,
41
+ -0.8949, 0.0731, 0.0886, 0.3442, -0.1433, -0.6804, 0.2204, 0.1859,
42
+ 0.2702, 0.1699, -0.1443, -0.9614, 0.3261, 0.1718, 0.3545, -0.0686]
43
+ )
44
+
45
+ def __init__(self, t2s_ref=None, s2a_ref=None, optimize=True, torch_compile=False, device=None):
46
+ if device is None: device = inference.get_compute_device()
47
+ self.device = device
48
+ args = dict(device = device)
49
+ try:
50
+ if t2s_ref:
51
+ args["ref"] = t2s_ref
52
+ self.t2s = TSARTransformer.load_model(**args) # use obtained compute device
53
+ if optimize: self.t2s.optimize(torch_compile=torch_compile)
54
+ except:
55
+ print("Failed to load the T2S model:")
56
+ print(traceback.format_exc())
57
+ args = dict(device = device)
58
+ try:
59
+ if s2a_ref:
60
+ spec = inference.load_model(ref=s2a_ref, device=device)
61
+ if [x for x in spec['state_dict'].keys() if x.startswith('cond_embeddings.')]:
62
+ cls = s2a_delar_mup_wds_mlang_cond.SADelARTransformer
63
+ args['spec'] = spec
64
+ else:
65
+ cls = SADelARTransformer
66
+ args['spec'] = spec
67
+ else:
68
+ cls = SADelARTransformer
69
+ self.s2a = cls.load_model(**args) # use obtained compute device
70
+ if optimize: self.s2a.optimize(torch_compile=torch_compile)
71
+ except:
72
+ print("Failed to load the S2A model:")
73
+ print(traceback.format_exc())
74
+
75
+ self.vocoder = Vocoder(device=device)
76
+ self.encoder = None
77
+
78
+ def extract_spk_emb(self, fname):
79
+ """Extracts a speaker embedding from the first 30 seconds of the give audio file.
80
+ """
81
+ import torchaudio
82
+ if self.encoder is None:
83
+ device = self.device
84
+ if device == 'mps': device = 'cpu' # operator 'aten::_fft_r2c' is not currently implemented for the MPS device
85
+ from speechbrain.pretrained import EncoderClassifier
86
+ self.encoder = EncoderClassifier.from_hparams("speechbrain/spkrec-ecapa-voxceleb",
87
+ savedir=expanduser("~/.cache/speechbrain/"),
88
+ run_opts={"device": device})
89
+ audio_info = torchaudio.info(fname)
90
+ actual_sample_rate = audio_info.sample_rate
91
+ num_frames = actual_sample_rate * 30 # specify 30 seconds worth of frames
92
+ samples, sr = torchaudio.load(fname, num_frames=num_frames)
93
+ samples = samples[:, :num_frames]
94
+ samples = self.encoder.audio_normalizer(samples[0], sr)
95
+ spk_emb = self.encoder.encode_batch(samples.unsqueeze(0))
96
+
97
+ return spk_emb[0,0].to(self.device)
98
+
99
+ def generate_atoks(self, text, speaker=None, lang='en', cps=15, step_callback=None):
100
+ if speaker is None: speaker = self.default_speaker
101
+ elif isinstance(speaker, (str, Path)): speaker = self.extract_spk_emb(speaker)
102
+ text = text.replace("\n", " ")
103
+ stoks = self.t2s.generate(text, cps=cps, lang=lang, step=step_callback)[0]
104
+ atoks = self.s2a.generate(stoks, speaker.unsqueeze(0), step=step_callback)
105
+ return atoks
106
+
107
+ def generate(self, text, speaker=None, lang='en', cps=15, step_callback=None):
108
+ return self.vocoder.decode(self.generate_atoks(text, speaker, lang=lang, cps=cps, step_callback=step_callback))
109
+
110
+ def generate_to_file(self, fname, text, speaker=None, lang='en', cps=15, step_callback=None):
111
+ self.vocoder.decode_to_file(fname, self.generate_atoks(text, speaker, lang=lang, cps=cps, step_callback=None))
112
+
113
+ def generate_to_notebook(self, text, speaker=None, lang='en', cps=15, step_callback=None):
114
+ self.vocoder.decode_to_notebook(self.generate_atoks(text, speaker, lang=lang, cps=cps, step_callback=None))
whisperspeech/prepare_s2a_atoks.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/3C. S2A acoustic tokens preparation.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/3C. S2A acoustic tokens preparation.ipynb 2
7
+ import sys
8
+ import os
9
+ import itertools
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+ from fastprogress import progress_bar
17
+ from fastcore.script import *
18
+
19
+ import webdataset as wds
20
+ from . import utils, vad_merge
21
+ from .inference import get_compute_device
22
+
23
+ # %% ../nbs/3C. S2A acoustic tokens preparation.ipynb 4
24
+ def load_model():
25
+ "Load the pretrained EnCodec model"
26
+ from encodec.model import EncodecModel
27
+ model = EncodecModel.encodec_model_24khz()
28
+ model.set_target_bandwidth(1.5)
29
+ model.to(get_compute_device()).eval()
30
+ return model
31
+
32
+ # %% ../nbs/3C. S2A acoustic tokens preparation.ipynb 5
33
+ @call_parse
34
+ def prepare_atoks(
35
+ input:str, # audio file webdataset file path
36
+ output:str, # output shard path
37
+ n_samples:int=None, # process a limited amount of samples
38
+ batch_size:int=4, # process several segments at once
39
+ bandwidth:float=3,
40
+ ):
41
+ device = get_compute_device()
42
+ amodel = load_model().to(device) # Move model to computed device
43
+ amodel.set_target_bandwidth(bandwidth)
44
+
45
+ total = n_samples//batch_size if n_samples else 'noinfer'
46
+ if n_samples: print(f"Benchmarking run of {n_samples} samples ({total} batches)")
47
+
48
+ if total == 'noinfer':
49
+ import math, time
50
+ start = time.time()
51
+ ds = wds.WebDataset([utils.derived_name(input, 'mvad')]).decode()
52
+ total = math.ceil(sum([len(x['max.spk_emb.npy']) for x in ds])/batch_size)
53
+ print(f"Counting {total} batches: {time.time()-start:.2f}")
54
+
55
+ ds = vad_merge.chunked_audio_dataset([input], 'max').compose(
56
+ utils.resampler(24000, 'samples_24k'),
57
+ wds.to_tuple('__key__', 'rpad_s', 'samples_24k'),
58
+ wds.batched(64),
59
+ )
60
+
61
+ dl = wds.WebLoader(ds, num_workers=1, batch_size=None).unbatched().batched(batch_size)
62
+
63
+ with utils.AtomicTarWriter(output, throwaway=n_samples is not None) as sink:
64
+ for keys, rpad_ss, samples in progress_bar(dl, total=total):
65
+ csamples = samples.to(device).unsqueeze(1) # Move tensors to computed device
66
+ atokss = amodel.encode(csamples)[0][0]
67
+ atokss = atokss.cpu().numpy().astype(np.int16)
68
+ for key, rpad_s, atoks in zip(keys, rpad_ss, atokss):
69
+ atoks = atoks[:,:int((30-rpad_s) * 75 + 0.5)]
70
+ sink.write({
71
+ "__key__": key,
72
+ "atoks.npy": atoks,
73
+ })
whisperspeech/prepare_t2s_txts.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/3A. T2S transcripts preparation.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/3A. T2S transcripts preparation.ipynb 2
7
+ import sys
8
+ import os
9
+ import itertools
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+ from fastprogress import progress_bar
17
+ from fastcore.script import *
18
+
19
+ import whisper, whisperx
20
+ from . import utils, vad_merge
21
+ import webdataset as wds
22
+
23
+ from .inference import get_compute_device
24
+
25
+ # %% ../nbs/3A. T2S transcripts preparation.ipynb 4
26
+ class Transcriber:
27
+ """
28
+ A helper class to transcribe a batch of 30 second audio chunks.
29
+ """
30
+ def __init__(self, model_size, lang=False):
31
+ self.model_size = model_size
32
+ # try to translate long language names to codes
33
+ lang = whisper.tokenizer.TO_LANGUAGE_CODE.get(lang, lang)
34
+ self.model = whisperx.asr.load_model(
35
+ model_size, get_compute_device(), compute_type="float16", language=lang,
36
+ asr_options=dict(repetition_penalty=1, no_repeat_ngram_size=0, prompt_reset_on_temperature=0.5,
37
+ max_new_tokens=500, clip_timestamps=None, hallucination_silence_threshold=None))
38
+ # without calling vad_model at least once the rest segfaults for some reason...
39
+ self.model.vad_model({"waveform": torch.zeros(1, 16000), "sample_rate": 16000})
40
+
41
+ def transcribe(self, batch):
42
+ batch = whisper.log_mel_spectrogram(batch, 128 if self.model_size == 'large-v3' else 80)
43
+ embs = self.model.model.encode(batch.cpu().numpy())
44
+ return self.model.tokenizer.tokenizer.decode_batch([x.sequences_ids[0] for x in
45
+ self.model.model.model.generate(
46
+ embs,
47
+ [self.model.model.get_prompt(self.model.tokenizer, [], without_timestamps=True)]*len(batch),
48
+ )])
49
+
50
+ # %% ../nbs/3A. T2S transcripts preparation.ipynb 5
51
+ @call_parse
52
+ def prepare_txt(
53
+ input:str, # input shard URL/path
54
+ output:str, # output shard path
55
+ n_samples:int=None, # process a limited amount of samples
56
+ batch_size:int=16, # process several segments at once
57
+ transcription_model:str="medium",
58
+ language:str="en",
59
+ ):
60
+ transcriber = Transcriber(transcription_model, lang=language)
61
+
62
+ total = n_samples//batch_size if n_samples else 'noinfer'
63
+ if n_samples: print(f"Benchmarking run of {n_samples} samples ({total} batches)")
64
+
65
+ import math, time
66
+ start = time.time()
67
+ ds = wds.WebDataset([utils.derived_name(input, 'mvad')]).decode()
68
+ total = math.ceil(sum([len(x['raw.spk_emb.npy']) for x in ds])/batch_size)
69
+ print(f"Counting {total} batches: {time.time()-start:.2f}")
70
+
71
+ ds = vad_merge.chunked_audio_dataset([input], 'raw').compose(
72
+ utils.resampler(16000, 'samples_16k'),
73
+ )
74
+
75
+ ds = ds.compose(
76
+ wds.to_tuple('__key__', 'rpad', 'samples_16k'),
77
+ wds.batched(64),
78
+ )
79
+
80
+ dl = wds.WebLoader(ds, num_workers=1, batch_size=None).unbatched().batched(batch_size)
81
+
82
+ with utils.AtomicTarWriter(output, throwaway=n_samples is not None) as sink:
83
+ for keys, rpads, samples in progress_bar(dl, total=total):
84
+ csamples = samples.to(get_compute_device())
85
+ txts = transcriber.transcribe(csamples)
86
+
87
+ for key, rpad, txt in zip(keys, rpads, txts):
88
+ sink.write({
89
+ "__key__": key,
90
+ "txt": txt,
91
+ })
whisperspeech/s2a_delar_mup_wds_mlang.py ADDED
@@ -0,0 +1,569 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['load_dataset', 'DelSumEmbedding', 'DelSumHead', 'rand', 'Tunables', 'SADelARTransformer']
5
+
6
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 1
7
+ import io
8
+ import time
9
+ import math
10
+ import random
11
+ import dataclasses
12
+
13
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 2
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ import numpy as np
18
+ from torch.profiler import profile, record_function, ProfilerActivity, schedule
19
+ from fastcore.basics import store_attr
20
+ from huggingface_hub import hf_hub_download
21
+
22
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 3
23
+ from pathlib import Path
24
+ import json
25
+ from fastprogress import progress_bar, master_bar
26
+
27
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 4
28
+ from . import inference
29
+ from .modules import *
30
+
31
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 8
32
+ def rand(start, end):
33
+ return random.random() * (end - start) + start
34
+
35
+ def logrand(start, end):
36
+ return 10**rand(math.log10(start), math.log10(end))
37
+
38
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 9
39
+ def random_trunc(random_trunc_p, atoks_len = 2250, stoks_len = 750):
40
+ atoks_per_second = atoks_len / 30
41
+ def _trunc(samples):
42
+ for s in samples:
43
+ if random.random() < random_trunc_p:
44
+ seconds = rand(0.3, 30)
45
+ s['atoks.npy'] = s['atoks.npy'][:,:math.ceil(seconds * atoks_per_second)]
46
+ s['stoks.npy'] = s['stoks.npy'][:math.ceil(s['atoks.npy'].shape[-1]/atoks_len*stoks_len)]
47
+ yield s
48
+ return _trunc
49
+
50
+ def pad_samples(atoks_len = 2250, stoks_len = 750, stoks_pad_token = 4096):
51
+ def _pad(samples):
52
+ for s in samples:
53
+ stoks = torch.tensor(s['stoks.npy'])
54
+ atoks = torch.tensor(s['atoks.npy'])
55
+ s['in_stoks'] = F.pad(stoks, (1, stoks_len - stoks.shape[-1]-1), value=stoks_pad_token)
56
+ q,n = atoks.shape
57
+ padatoks = [F.pad( atoks[i], (i + 1, 0 ), value=1025) for i in range(q)]
58
+ padatoks = [F.pad(padatoks[i], (0, atoks_len - n - i - 1), value=1024) for i in range(q)]
59
+ s['in_atoks'] = torch.stack(padatoks)
60
+ yield s
61
+ return _pad
62
+
63
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 10
64
+ def load_dataset(
65
+ dataset_dir:Path,
66
+ stoks_dir:str="stoks",
67
+ random_trunc_p:float=0,# probability of truncating the input to less than 30 seconds
68
+ vq_codes:int=4096,
69
+ weight:float=1,
70
+ validation:bool=False,
71
+ exclude_datasets:str="atoks-random-valid",
72
+ randomize_speakers:bool=False,
73
+ ):
74
+ import webdataset as wds
75
+ from whisperspeech import utils, languages
76
+
77
+ dataset_dir = Path(dataset_dir)
78
+ shards = utils.shard_glob(dataset_dir/'encodec-3kbps/*.tar.gz')
79
+ with open(dataset_dir/'atoks-samples.list') as f: samples = len(f.readlines())
80
+ language = utils.readlines(dataset_dir/'language')[0]
81
+
82
+ excludes = {x
83
+ for dir in exclude_datasets.split()
84
+ for x in utils.readlines(dataset_dir/Path(dir)/"atoks-samples.list")
85
+ } if not validation and exclude_datasets else set()
86
+
87
+ def check_for_nan(s):
88
+ if torch.tensor(s['spk_emb.npy']).isnan().any(): print("found NaN:", s['__key__'])
89
+ return s
90
+
91
+ def set_language(x):
92
+ x['language'] = languages.to_id(language)
93
+ return x
94
+
95
+ same_on_all_nodes = lambda urls: urls # will only be used for validation
96
+ ds = wds.WebDataset(shards, resampled=not validation, nodesplitter=same_on_all_nodes).compose(
97
+ wds.decode(),
98
+ utils.merge_in(utils.derived_dataset(stoks_dir)),
99
+ wds.map(check_for_nan),
100
+ wds.select(lambda s: s['__key__'] not in excludes),
101
+ wds.map_dict(**{'spk_emb.npy':np.nan_to_num}), # remove nans from the speaker embedding model
102
+ random_trunc(random_trunc_p) if random_trunc_p > 0 else lambda x: x,
103
+ pad_samples(stoks_pad_token=vq_codes-1),
104
+ wds.map(set_language),
105
+ wds.to_tuple('in_stoks', 'in_atoks', 'spk_emb.npy', 'language'),
106
+ wds.shuffle(20000, initial=20000),
107
+ wds.batched(64),
108
+ )
109
+ if randomize_speakers:
110
+ rng = np.random.default_rng()
111
+ ds = ds.compose(
112
+ wds.map_tuple(None, None, lambda x: rng.permutation(x), None),
113
+ )
114
+ if validation:
115
+ ds = ds.slice(samples // 64)
116
+ ds.total_samples = samples
117
+ ds.weight = weight
118
+
119
+ return ds
120
+
121
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 13
122
+ class DelSumEmbedding(nn.Module):
123
+ def __init__(self, n_head=6, head_width=64, atoks_width=None, length=2250, codes=1024, quantizers=8, pos_embs=None):
124
+ super().__init__()
125
+ self.length = length
126
+ width = n_head * head_width
127
+ if atoks_width is None: atoks_width = width
128
+ self.width = width
129
+ self.quantizers = quantizers
130
+
131
+ emb = None
132
+ embs = []
133
+ for _ in range(quantizers):
134
+ emb = FlexEmbeddings(codes, width, special_codes=2, frozen_width=atoks_width,
135
+ special_embedding=emb and emb.special)
136
+ embs.append(emb)
137
+ self.embeddings = nn.ModuleList(embs)
138
+ if pos_embs is not None:
139
+ self.register_buffer("positional_embedding", pos_embs)
140
+
141
+ def forward(self, toks, xenc):
142
+ with record_function("embeddings"):
143
+ b,_,n = toks.shape
144
+ newn = min(n, self.length)
145
+
146
+ embs = torch.zeros((b,newn,self.width), dtype=xenc.dtype, device=xenc.device)
147
+ for i in range(self.quantizers):
148
+ embs[:, :] += self.embeddings[i](toks[:,i,:])
149
+
150
+ x = embs.to(xenc.dtype)
151
+ return x
152
+
153
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 14
154
+ class DelSumHead(nn.Module):
155
+ def __init__(self, quantizers=8, n_head=6, head_width=64):
156
+ super().__init__()
157
+ self.width = n_head * head_width
158
+ self.quantizers = quantizers
159
+ self.splitter = nn.Sequential(
160
+ nn.Linear(self.width, self.width * quantizers),
161
+ nn.GELU(),
162
+ )
163
+
164
+ def forward(self, x, embeddings=None):
165
+ b, newn, _ = x.shape
166
+ with record_function("splitter"):
167
+ split = self.splitter(x).view(b,newn,self.quantizers,self.width)
168
+ with record_function("unembed"):
169
+ logits = torch.stack([embeddings[q].unembed(split[:,:,q]) for q in range(self.quantizers)], dim=1)
170
+ return logits
171
+
172
+ def rand(start, end):
173
+ return random.random() * (end - start) + start
174
+
175
+ @dataclasses.dataclass
176
+ class Tunables:
177
+ init_std :float = 9
178
+ embeddings_std :float = 0.2
179
+ embeddings_lr_scale: float = 10
180
+ output_mult :float = 5.6
181
+ # FIXME: try separate mults for self and cross attention
182
+ query_mult :float = .3
183
+ encoder_depth_ratio :float = 0.25
184
+ linear_heads :bool = False
185
+ rope :bool = True
186
+ q0_loss_mult: float = 1
187
+ causal_encoder :bool = False
188
+
189
+ lr0 :float = 3e-3
190
+ clip_gradient_norm :float = 2
191
+ weight_decay :float = 1e-3
192
+ warmup_steps :float = 2000
193
+
194
+ random :bool = False
195
+ random_finetune :bool = False
196
+
197
+ # backwards compat
198
+ force_hidden_to_emb: bool = False
199
+
200
+ def __post_init__(self):
201
+ # randomize the hyperparams if requested
202
+ if self.random:
203
+ self.init_std = 2*10**rand(0,1)
204
+ self.embeddings_std = 10**rand(-1.7,-0.22)
205
+ self.embeddings_lr_scale = 2**rand(2,4)
206
+ self.output_mult = 2**rand(1.5,3)
207
+ self.query_mult = 2**rand(-3,-1.3)
208
+ self.encoder_depth_ratio = random.choice([0.25,0.5])
209
+ self.linear_heads = False
210
+ self.rope = True
211
+
212
+ self.lr0 = 3e-3
213
+ self.clip_gradient_norm = 10**rand(-1,1)
214
+ self.warmup_steps = 100*(10**rand(1.18,1.3))
215
+ if self.random_finetune:
216
+ self.lr0 = logrand(1e-5,1e-3)
217
+ self.clip_gradient_norm = logrand(1e-2,2e-1)
218
+ self.weight_decay = logrand(1e-5,1e-1)
219
+ self.warmup_steps = logrand(20,500)
220
+
221
+ @staticmethod
222
+ def upgrade(args):
223
+ args = {k:v for k,v in args.items()}
224
+ def old_default(name, value):
225
+ if name not in args: args[name] = value
226
+ old_default('rope', False)
227
+ old_default('linear_heads', True)
228
+ old_default('causal_encoder', False)
229
+ old_default('force_hidden_to_emb', True)
230
+ return args
231
+
232
+ class SADelARTransformer(nn.Module):
233
+ def __init__(self, depth=3, ctx_n=2250,
234
+ stoks_len=750, stoks_codes=4097, stoks_width=None,
235
+ spk_width=None,
236
+ atoks_width=None,
237
+ n_head=3, head_width=64, ffn_mult=4,
238
+ quantizers=8, speaker_map={"1":0}, tunables=Tunables()):
239
+ super().__init__()
240
+ self.quantizers = quantizers
241
+ self.codes = 1024
242
+ width = n_head * head_width
243
+ store_attr("depth,ctx_n,stoks_len,stoks_codes,stoks_width,spk_width,atoks_width,n_head,head_width,ffn_mult,quantizers,speaker_map")
244
+ self.width = width
245
+ self.base_width = 3 * head_width
246
+ self.tunables = tunables
247
+
248
+ if stoks_width is None: stoks_width = width
249
+ if spk_width is None: spk_width = width
250
+ self.emb_factor = width != stoks_width
251
+ self.spk_factor = width != spk_width
252
+
253
+ if tunables.rope:
254
+ self.positional_embeddings = None
255
+ else:
256
+ self.register_buffer('positional_embeddings', sinusoids(ctx_n, width))
257
+
258
+ self.semantic_embedding = nn.Embedding(stoks_codes, stoks_width)
259
+ if self.emb_factor:
260
+ self.emb_to_hidden = nn.Linear(stoks_width, width)
261
+ if self.tunables.causal_encoder or self.tunables.force_hidden_to_emb:
262
+ self.hidden_to_emb = nn.Linear(width, stoks_width)
263
+
264
+ if self.spk_factor:
265
+ self.spk_to_hidden = nn.Linear(spk_width, width)
266
+
267
+ qk_scale = self.tunables.query_mult * 8 / math.sqrt(head_width)
268
+
269
+ encoder_depth = int(depth * 2 * tunables.encoder_depth_ratio)
270
+ decoder_depth = depth * 2 - encoder_depth
271
+ self.encoder = nn.Sequential(*[
272
+ ResidualAttentionBlock(width, n_head, qk_scale=qk_scale, ffn_mult=ffn_mult, rope=tunables.rope) for _ in range(encoder_depth)
273
+ ])
274
+ self.ln_post = LayerNorm(width)
275
+
276
+ self.embds = DelSumEmbedding(
277
+ pos_embs=self.positional_embeddings, length=ctx_n,
278
+ n_head=n_head, head_width=head_width, atoks_width=atoks_width,
279
+ quantizers=quantizers,
280
+ )
281
+ self.decoder = BaseDecoder(qk_scale=qk_scale, length=ctx_n,
282
+ n_head=n_head, width=n_head * head_width,
283
+ ffn_mult=ffn_mult, depth=decoder_depth,
284
+ rope=tunables.rope)
285
+ self.head = DelSumHead(n_head=n_head, head_width=head_width, quantizers=quantizers)
286
+ for l in self.decoder.layers:
287
+ l.cross_attn.key_subsampling = 3
288
+
289
+ self.register_buffer('val_true', torch.zeros(self.quantizers))
290
+ self.register_buffer('val_total', torch.zeros(self.quantizers))
291
+ self.apply(self.init_transformer)
292
+
293
+ def setup(self, device):
294
+ pass
295
+
296
+ def load_frozen_semantic_embeddings(self, vqmodel):
297
+ with torch.no_grad():
298
+ self.semantic_embedding.weight[:] = vqmodel.rq.layers[0]._codebook.embed[0]
299
+ self.semantic_embedding.lr_scale = 0
300
+
301
+ def load_frozen_acoustic_embeddings(self, amodel):
302
+ for i in range(self.quantizers):
303
+ self.decoder.embeddings[i].set_frozen_embeddings(amodel.quantizer.vq.layers[i].codebook)
304
+
305
+ def init_transformer(self, m):
306
+ if isinstance(m, LinearHead):
307
+ m.no_weight_decay = True
308
+ torch.nn.init.constant_(m.weight, 0)
309
+ elif isinstance(m, QueryHead):
310
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
311
+ torch.nn.init.constant_(m.weight, 0)
312
+ elif isinstance(m, nn.Embedding):
313
+ m.no_weight_decay = True
314
+ m.lr_scale = self.tunables.embeddings_lr_scale
315
+ std = self.tunables.embeddings_std
316
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
317
+ elif isinstance(m, nn.Linear):
318
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
319
+ std = self.tunables.init_std / m.weight.shape[1]
320
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
321
+ if m.bias is not None:
322
+ torch.nn.init.trunc_normal_(m.bias, std=std, a=-3*std, b=3*std)
323
+ elif isinstance(m, nn.LayerNorm):
324
+ m.no_weight_decay = True
325
+ torch.nn.init.constant_(m.bias, 0)
326
+ torch.nn.init.constant_(m.weight, 1)
327
+
328
+ def embed_stoks(self, Stoks):
329
+ b,n = Stoks.shape
330
+ if self.stoks_len == 1500:
331
+ # converts 50 toks/s to 75 toks/s by adding padding between every two tokens
332
+ x = Stoks.reshape(b,n//2,2)
333
+ x = x.repeat_interleave(2, -1)[:,:,:3]
334
+ x[:,:,1] = 1024
335
+ x = x.reshape(b,n//2*3)
336
+ else:
337
+ # it's a lot easier with 25 toks/s
338
+ x = Stoks
339
+ # embed semantic tokens
340
+ Sembs = self.semantic_embedding(x.to(torch.long))
341
+ if self.emb_factor:
342
+ Sembs = self.emb_to_hidden(Sembs)
343
+ return Sembs
344
+
345
+ def _encoder(self, semb, positions):
346
+ x = semb
347
+ for l in self.encoder: x = l(x, positions, causal=self.tunables.causal_encoder)
348
+ return self.ln_post(x)
349
+
350
+ def run_encoder(self, Stoks, speakers):
351
+ semb = self.embed_stoks(Stoks)
352
+ with record_function("encoder"):
353
+ if self.positional_embeddings is not None: semb = semb + self.positional_embeddings
354
+ positions = torch.arange(0, semb.shape[1], device=semb.device)
355
+ xenc = self._encoder(semb, positions)
356
+ if self.training and self.tunables.causal_encoder:
357
+ enc_logits = (self.hidden_to_emb(xenc) @ self.semantic_embedding.weight.to(xenc.dtype).T).float()
358
+ enc_logits = enc_logits * self.tunables.output_mult / (self.width / self.base_width)
359
+ else:
360
+ enc_logits = None
361
+
362
+ spk_embs = F.normalize(speakers, dim=-1) # use extracted embeddings
363
+ if self.spk_factor: spk_embs = self.spk_to_hidden(spk_embs)
364
+ return xenc + spk_embs.unsqueeze(1), positions, enc_logits
365
+
366
+ def forward(self, Stoks, Atoks, speakers, langs=None, out_stoks=None, out_atoks=None, noloss=False, xenc=None, xenc_positions=None, atoks_positions=None):
367
+ if xenc is None:
368
+ Stoks, Atoks = [x.to(dtype=torch.long) for x in (Stoks, Atoks)]
369
+ xenc, xenc_positions, enc_logits = self.run_encoder(Stoks, speakers)
370
+ with record_function("decoder"):
371
+ embs = self.embds(Atoks, xenc)
372
+ if atoks_positions is None: atoks_positions = torch.arange(0, embs.shape[1], device=embs.device)
373
+ x = self.decoder(embs, atoks_positions, xenc, xenc_positions)
374
+ logits = self.head(x, embeddings=self.embds.embeddings)
375
+ logits *= self.tunables.output_mult / (self.width / self.base_width)
376
+
377
+ if noloss:
378
+ return logits
379
+
380
+ with record_function("loss"):
381
+ loss = 0
382
+ for i in range(self.quantizers):
383
+ loss += F.cross_entropy(logits[:,i,:-1].reshape(-1,logits.shape[-1]), Atoks[:,i,1:].reshape(-1), ignore_index=1024)
384
+ if self.training and i == 0:
385
+ loss *= self.tunables.q0_loss_mult
386
+ loss_denom = self.quantizers
387
+ if self.training: loss_denom += - 1 + self.tunables.q0_loss_mult
388
+ loss /= loss_denom
389
+ if self.training and self.tunables.causal_encoder:
390
+ loss += 0.1 * F.cross_entropy(enc_logits[:,:-1].transpose(-1,-2), Stoks[:,1:])
391
+
392
+ if not self.training:
393
+ for i in range(self.quantizers):
394
+ Atoks_i = Atoks[:,i,1:]
395
+ valid_Atoks = Atoks_i != 1024
396
+ self.val_true[i] += (logits[:,i,:-1].argmax(-1)[valid_Atoks] == Atoks_i[valid_Atoks]).float().sum()
397
+ self.val_total[i] += valid_Atoks.float().sum()
398
+
399
+ return logits, loss
400
+
401
+ def get_metrics(self):
402
+ metrics = {
403
+ f'acc_{i}':x.item() for i,x in enumerate(self.val_true / self.val_total)
404
+ }
405
+ self.val_true[:] = 0
406
+ self.val_total[:] = 0
407
+ return metrics
408
+
409
+ #
410
+ # inference
411
+ #
412
+ @classmethod
413
+ def load_model(cls, ref="collabora/whisperspeech:s2a-q4-small-en+pl.model",
414
+ repo_id=None, filename=None, local_filename=None, spec=None, device=None):
415
+ if repo_id is None and filename is None and local_filename is None and spec is None:
416
+ if ":" in ref:
417
+ repo_id, filename = ref.split(":", 1)
418
+ else:
419
+ local_filename = ref
420
+ if not local_filename and spec is None:
421
+ local_filename = hf_hub_download(repo_id=repo_id, filename=filename)
422
+ if spec is None:
423
+ spec = torch.load(local_filename, map_location=device)
424
+ if '_extra_state' not in spec['state_dict'] and 'speaker_map' in spec['config']: spec['state_dict']['_extra_state'] = { 'speaker_map': spec['config']['speaker_map'] }
425
+ model = cls(**spec['config'], tunables=Tunables(**Tunables.upgrade(spec['tunables'])))
426
+ model.load_state_dict(spec['state_dict'])
427
+ model.eval().to(device)
428
+ return model
429
+
430
+ def get_extra_state(self):
431
+ return { 'speaker_map': self.speaker_map }
432
+
433
+ def set_extra_state(self, st):
434
+ self.speaker_map = st['speaker_map']
435
+
436
+ def load_checkpoint(self, local_filename_or_obj):
437
+ if isinstance(local_filename_or_obj, (str, Path)):
438
+ spec = torch.load(local_filename_or_obj, map_location='cpu')
439
+ else:
440
+ spec = local_filename_or_obj
441
+ assert 'pytorch-lightning_version' in spec, 'not a valid PyTorch Lightning checkpoint'
442
+ state_dict = {k.replace('model.', ''):v
443
+ for k,v in spec['state_dict'].items()}
444
+ self.load_state_dict(state_dict)
445
+ return self
446
+
447
+ def save_model(self, fname):
448
+ torch.save(dict(config = self.__stored_args__,
449
+ tunables = dataclasses.asdict(self.tunables),
450
+ state_dict = self.state_dict()), fname)
451
+
452
+ def switch_dtypes(self, dtype=torch.float16):
453
+ self.dtype = dtype
454
+ for n,m in self.named_modules():
455
+ # convert every leaf layer apart from the LayerNorms
456
+ if isinstance(m, (nn.Linear, nn.Embedding)):
457
+ m.to(dtype)
458
+ # take care of buffers ([kv]_cache, masks) that are not in the leaf layers
459
+ for bn,b in m.named_buffers(recurse=False):
460
+ setattr(m,bn,b.to(dtype))
461
+
462
+ def optimize(self, max_batch_size=1, dtype=torch.float16, torch_compile=True):
463
+ for emb in self.embds.embeddings:
464
+ emb.convert_for_eval()
465
+ for l in self.encoder:
466
+ l.attn.convert_for_eval()
467
+ for l in self.decoder.layers:
468
+ l.attn.convert_for_eval()
469
+ l.cross_attn.convert_for_eval()
470
+ l.setup_kv_cache(max_batch_size, self.ctx_n, self.stoks_len)
471
+ self.switch_dtypes(dtype)
472
+ if torch_compile:
473
+ self.generate_next = torch.compile(self.generate_next, mode="reduce-overhead", fullgraph=True)
474
+
475
+ def optimize_training(self):
476
+ self.decoder = torch.compile(self.decoder, fullgraph=True, mode="reduce-overhead")
477
+ self._encoder = torch.compile(self._encoder, fullgraph=True, mode="reduce-overhead")
478
+
479
+ @property
480
+ def device(self):
481
+ return next(self.parameters()).device
482
+
483
+ def generate_one(self, toks, positions, langs, xenc, xenc_positions, T, top_k):
484
+ probs = self(None, toks, None, langs, noloss=True, xenc=xenc, xenc_positions=xenc_positions, atoks_positions=positions)
485
+ probs = probs[:,:,-1]
486
+ return inference.sample(probs, T, top_k)
487
+
488
+ def generate_next(self, *args, **kwargs):
489
+ return self.generate_one(*args, **kwargs)
490
+
491
+ @torch.no_grad()
492
+ def generate(self, stoks, speakers, langs=None, atoks_prompt=None, N=None, bs=1, T=0.7, top_k=None, show_progress_bar=True, step=None, subsample_enc=False):
493
+ dev = self.device
494
+ N = N or len(stoks) * 3
495
+ stoks = F.pad(stoks.to(dev), (1, self.stoks_len - len(stoks) - 1), value=self.stoks_codes-1).unsqueeze(0)
496
+ speakers = speakers.to(device=dev, dtype=self.dtype)
497
+ toks = torch.full((bs,self.quantizers,self.ctx_n), self.codes+1, dtype=torch.long, device=dev)
498
+ T = torch.tensor(T, device=dev)
499
+
500
+ start = 0 # number of valid tokens or the index of first empty spot
501
+ if atoks_prompt is not None:
502
+ start = atoks_prompt.shape[-1]
503
+ for i in range(self.quantizers):
504
+ toks[:,i,1+i:start+i+1] = atoks_prompt[:,i]
505
+ start += 1 # we always start with at least an SOT
506
+
507
+ with record_function("encode"):
508
+ stoks, speakers = [x.repeat(bs, 1) for x in (stoks, speakers)]
509
+ xenc, xenc_positions, _ = self.run_encoder(stoks, speakers)
510
+ toks_positions = torch.arange(N, device=dev)
511
+ with record_function("prefill"):
512
+ initial = self.generate_one(toks[:,:,:start], toks_positions[:start], langs, xenc, xenc_positions, T, top_k)
513
+ toks[:,:start,start:start+1] = initial[:,:start]
514
+ start += 1
515
+
516
+ with inference.inference_context():
517
+ it = range(start,min(N,self.ctx_n-1))
518
+ if show_progress_bar: it = progress_bar(it)
519
+
520
+ for i in it:
521
+ with record_function("generate_one"):
522
+ toks[:,:i,i:i+1] = self.generate_next(toks[:,:,i-1:i], toks_positions[i-1:i], langs, xenc, xenc_positions, T, top_k)[:,:i]
523
+
524
+ # for profiling, debugging or early exit
525
+ if step is not None: step()
526
+ # shift tokens
527
+ toks = toks[:,:,1:N]
528
+ for j in range(self.quantizers):
529
+ toks[:, j] = torch.roll(toks[:, j], -j)
530
+ return toks[:,:,:N-4]
531
+
532
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling.ipynb 15
533
+ def _make_model(size:str, quantizers:int=4, tunables:Tunables=Tunables(), **kwargs):
534
+ kwargs = dict(quantizers=quantizers, tunables=tunables, **kwargs)
535
+ if size == 'micro':
536
+ return SADelARTransformer(depth=4, n_head=3, ffn_mult=2, **kwargs)
537
+ if size == 'tiny-narrow':
538
+ return SADelARTransformer(depth=4, n_head=6, ffn_mult=1, **kwargs)
539
+ if size == 'tiny':
540
+ return SADelARTransformer(depth=4, n_head=6, **kwargs)
541
+ if size == 'base':
542
+ return SADelARTransformer(depth=6, n_head=8, **kwargs)
543
+ if size == 'base-deep':
544
+ return SADelARTransformer(depth=9, n_head=8, **kwargs)
545
+ if size == 'base-wide':
546
+ return SADelARTransformer(depth=6, n_head=12, **kwargs)
547
+ if size == 'small/2':
548
+ return SADelARTransformer(depth=9, n_head=12, **kwargs)
549
+ if size == 'small':
550
+ return SADelARTransformer(depth=12, n_head=12, **kwargs)
551
+ if size == 'medium':
552
+ return SADelARTransformer(depth=24, n_head=16, **kwargs)
553
+
554
+ def make_model(size:str, quantizers:int=4, frozen_embeddings_model:str=None, frozen_acoustic_embeddings:bool=False, spk_width:int=None, tunables:Tunables=Tunables(), dataset=None):
555
+ from encodec.model import EncodecModel
556
+ from whisperspeech import vq_stoks
557
+
558
+ amodel = EncodecModel.encodec_model_24khz() if frozen_acoustic_embeddings else None
559
+ vqmodel = vq_stoks.RQBottleneckTransformer.load_model(frozen_embeddings_model) if frozen_embeddings_model else None
560
+ model = _make_model(size, quantizers, tunables,
561
+ spk_width=spk_width,
562
+ atoks_width=amodel and amodel.quantizer.vq.layers[0]._codebook.embed.shape[-1],
563
+ stoks_codes=vqmodel.vq_codes+1, stoks_width=vqmodel.rq.layers[0]._codebook.embed[0].shape[-1])
564
+ if vqmodel: model.load_frozen_semantic_embeddings(vqmodel)
565
+ if amodel: model.load_frozen_acoustic_embeddings(amodel)
566
+ return model
567
+
568
+ def load_model(*args, **kwargs):
569
+ return SADelARTransformer.load_model(*args, **kwargs)
whisperspeech/s2a_delar_mup_wds_mlang_cond.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['load_dataset', 'DelSumEmbedding', 'DelSumHead', 'rand', 'Tunables', 'CategoricalEmbedding', 'BinnedEmbedding',
5
+ 'SpeakerEmbedding', 'SADelARTransformer']
6
+
7
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 1
8
+ import io
9
+ import time
10
+ import math
11
+ import random
12
+ import dataclasses
13
+
14
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 2
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ import numpy as np
19
+ from torch.profiler import profile, record_function, ProfilerActivity, schedule
20
+ from fastcore.basics import store_attr
21
+ from huggingface_hub import hf_hub_download
22
+
23
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 3
24
+ from pathlib import Path
25
+ import json
26
+ from fastprogress import progress_bar, master_bar
27
+
28
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 4
29
+ from . import inference, languages
30
+ from .modules import *
31
+
32
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 8
33
+ def rand(start, end):
34
+ return random.random() * (end - start) + start
35
+
36
+ def logrand(start, end):
37
+ return 10**rand(math.log10(start), math.log10(end))
38
+
39
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 9
40
+ def random_trunc(random_trunc_p, atoks_len = 2250, stoks_len = 750):
41
+ atoks_per_second = atoks_len / 30
42
+ def _trunc(samples):
43
+ for s in samples:
44
+ if random.random() < random_trunc_p:
45
+ seconds = rand(0.3, 30)
46
+ s['atoks.npy'] = s['atoks.npy'][:,:math.ceil(seconds * atoks_per_second)]
47
+ s['stoks.npy'] = s['stoks.npy'][:math.ceil(s['atoks.npy'].shape[-1]/atoks_len*stoks_len)]
48
+ yield s
49
+ return _trunc
50
+
51
+ def pad_samples(atoks_len = 2250, stoks_len = 750, stoks_pad_token = 4096):
52
+ def _pad(samples):
53
+ for s in samples:
54
+ stoks = torch.tensor(s['stoks.npy'])
55
+ atoks = torch.tensor(s['atoks.npy'])
56
+ s['in_stoks'] = F.pad(stoks, (1, stoks_len - stoks.shape[-1]-1), value=stoks_pad_token)
57
+ q,n = atoks.shape
58
+ padatoks = [F.pad( atoks[i], (i + 1, 0 ), value=1025) for i in range(q)]
59
+ padatoks = [F.pad(padatoks[i], (0, atoks_len - n - i - 1), value=1024) for i in range(q)]
60
+ s['in_atoks'] = torch.stack(padatoks)
61
+ yield s
62
+ return _pad
63
+
64
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 10
65
+ def load_dataset(
66
+ dataset_dir:Path,
67
+ stoks_dir:str="stoks",
68
+ random_trunc_p:float=0,# probability of truncating the input to less than 30 seconds
69
+ vq_codes:int=4096,
70
+ weight:float=1,
71
+ validation:bool=False,
72
+ exclude_datasets:str="atoks-random-valid",
73
+ randomized_conditionings:str="",
74
+ conditionings="lang,speaker,snr,c50",
75
+ conditinings_dropout="speaker:.5,snr:.5,c50:.5"
76
+ ):
77
+ import webdataset as wds
78
+ from whisperspeech import utils, languages
79
+
80
+ dataset_dir = Path(dataset_dir)
81
+ shards = utils.shard_glob(dataset_dir/'encodec-3kbps/*.tar.gz')
82
+ with open(dataset_dir/'atoks-samples.list') as f: samples = len(f.readlines())
83
+ language = utils.readlines(dataset_dir/'language')[0]
84
+
85
+ excludes = {x
86
+ for dir in exclude_datasets.split()
87
+ for x in utils.readlines(dataset_dir/Path(dir)/"atoks-samples.list")
88
+ } if not validation and exclude_datasets else set()
89
+
90
+ def check_for_nan(s):
91
+ if torch.tensor(s['spk_emb.npy']).isnan().any(): print("found NaN:", s['__key__'])
92
+ return s
93
+
94
+ def set_language(x):
95
+ x['language'] = languages.to_id(language)
96
+ return x
97
+
98
+ conditionings = set(conditionings.split(','))
99
+ def merge_conditionings(x):
100
+ conds = {
101
+ 'lang': x['language'],
102
+ 'speaker': x['spk_emb.npy'],
103
+ 'snr': float(x['snr_c50.npy'][0]),
104
+ 'c50': float(x['snr_c50.npy'][1]),
105
+ }
106
+ x['conds'] = conds
107
+ for k in [k for k in conds.keys() if k not in conditionings]:
108
+ del conds[k]
109
+ # ks, ps = zip([x.split(':') for x in conditinings_dropout.split(",")])
110
+ return x
111
+
112
+ randomized_conditionings = set(randomized_conditionings.split(',')) if randomized_conditionings else set()
113
+ rng = np.random.default_rng()
114
+ def randomize_conditionings(batch):
115
+ if not randomized_conditionings: return batch
116
+ bs = len(batch)
117
+ for k in randomized_conditionings:
118
+ orig = [x.get(k, None) for x in batch]
119
+ for i,j in enumerate(rng.permutation(range(bs))):
120
+ if orig[j] is not None:
121
+ batch[i][k] = orig[j]
122
+ else:
123
+ del batch[i][k]
124
+ return batch
125
+
126
+ same_on_all_nodes = lambda urls: urls # will only be used for validation
127
+ ds = wds.WebDataset(shards, resampled=not validation, nodesplitter=same_on_all_nodes).compose(
128
+ wds.decode(),
129
+ utils.merge_in(utils.derived_dataset(stoks_dir)),
130
+ utils.merge_in(utils.derived_dataset('snr-c50')),
131
+ wds.map(check_for_nan),
132
+ wds.select(lambda s: s['__key__'] not in excludes),
133
+ wds.map_dict(**{'spk_emb.npy':np.nan_to_num}), # remove nans from the speaker embedding model
134
+ random_trunc(random_trunc_p) if random_trunc_p > 0 else lambda x: x,
135
+ pad_samples(stoks_pad_token=vq_codes-1),
136
+ wds.map(set_language),
137
+ wds.map(merge_conditionings),
138
+ wds.to_tuple('in_stoks', 'in_atoks', 'conds'),
139
+ wds.shuffle(20000, initial=20000),
140
+ wds.batched(64),
141
+ wds.map_tuple(None, None, randomize_conditionings),
142
+ )
143
+ if validation:
144
+ ds = ds.slice(samples // 64)
145
+ ds.total_samples = samples
146
+ ds.weight = weight
147
+
148
+ return ds
149
+
150
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 13
151
+ class DelSumEmbedding(nn.Module):
152
+ def __init__(self, n_head=6, head_width=64, atoks_width=None, length=2250, codes=1024, quantizers=8, pos_embs=None):
153
+ super().__init__()
154
+ self.length = length
155
+ width = n_head * head_width
156
+ if atoks_width is None: atoks_width = width
157
+ self.width = width
158
+ self.quantizers = quantizers
159
+
160
+ emb = None
161
+ embs = []
162
+ for _ in range(quantizers):
163
+ emb = FlexEmbeddings(codes, width, special_codes=2, frozen_width=atoks_width,
164
+ special_embedding=emb and emb.special)
165
+ embs.append(emb)
166
+ self.embeddings = nn.ModuleList(embs)
167
+ if pos_embs is not None:
168
+ self.register_buffer("positional_embedding", pos_embs)
169
+
170
+ def forward(self, toks, xenc):
171
+ with record_function("embeddings"):
172
+ b,_,n = toks.shape
173
+ newn = min(n, self.length)
174
+
175
+ embs = torch.zeros((b,newn,self.width), dtype=xenc.dtype, device=xenc.device)
176
+ for i in range(self.quantizers):
177
+ embs[:, :] += self.embeddings[i](toks[:,i,:])
178
+
179
+ x = embs.to(xenc.dtype)
180
+ return x
181
+
182
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 14
183
+ class DelSumHead(nn.Module):
184
+ def __init__(self, quantizers=8, n_head=6, head_width=64):
185
+ super().__init__()
186
+ self.width = n_head * head_width
187
+ self.quantizers = quantizers
188
+ self.splitter = nn.Sequential(
189
+ nn.Linear(self.width, self.width * quantizers),
190
+ nn.GELU(),
191
+ )
192
+
193
+ def forward(self, x, embeddings=None):
194
+ b, newn, _ = x.shape
195
+ with record_function("splitter"):
196
+ split = self.splitter(x).view(b,newn,self.quantizers,self.width)
197
+ with record_function("unembed"):
198
+ logits = torch.stack([embeddings[q].unembed(split[:,:,q]) for q in range(self.quantizers)], dim=1)
199
+ return logits
200
+
201
+ def rand(start, end):
202
+ return random.random() * (end - start) + start
203
+
204
+ @dataclasses.dataclass
205
+ class Tunables:
206
+ init_std :float = 9
207
+ embeddings_std :float = 0.2
208
+ embeddings_lr_scale: float = 10
209
+ output_mult :float = 5.6
210
+ # FIXME: try separate mults for self and cross attention
211
+ query_mult :float = .3
212
+ encoder_depth_ratio :float = 0.25
213
+ linear_heads :bool = False
214
+ rope :bool = True
215
+ q0_loss_mult: float = 1
216
+ causal_encoder :bool = False
217
+
218
+ lr0 :float = 3e-3
219
+ clip_gradient_norm :float = 2
220
+ weight_decay :float = 1e-3
221
+ warmup_steps :float = 2000
222
+
223
+ random :bool = False
224
+ random_finetune :bool = False
225
+
226
+ # backwards compat
227
+ force_hidden_to_emb: bool = False
228
+
229
+ def __post_init__(self):
230
+ # randomize the hyperparams if requested
231
+ if self.random:
232
+ self.init_std = 2*10**rand(0,1)
233
+ self.embeddings_std = 10**rand(-1.7,-0.22)
234
+ self.embeddings_lr_scale = 2**rand(2,4)
235
+ self.output_mult = 2**rand(1.5,3)
236
+ self.query_mult = 2**rand(-3,-1.3)
237
+ self.encoder_depth_ratio = random.choice([0.25,0.5])
238
+ self.linear_heads = False
239
+ self.rope = True
240
+
241
+ self.lr0 = 3e-3
242
+ self.clip_gradient_norm = 10**rand(-1,1)
243
+ self.warmup_steps = 100*(10**rand(1.18,1.3))
244
+ if self.random_finetune:
245
+ self.lr0 = logrand(1e-5,1e-3)
246
+ self.clip_gradient_norm = logrand(1e-2,2e-1)
247
+ self.weight_decay = logrand(1e-5,1e-1)
248
+ self.warmup_steps = logrand(20,500)
249
+
250
+ @staticmethod
251
+ def upgrade(args):
252
+ args = {k:v for k,v in args.items()}
253
+ def old_default(name, value):
254
+ if name not in args: args[name] = value
255
+ old_default('rope', False)
256
+ old_default('linear_heads', True)
257
+ old_default('causal_encoder', False)
258
+ old_default('force_hidden_to_emb', True)
259
+ return args
260
+
261
+ class CategoricalEmbedding(nn.Module):
262
+ default = torch.nan
263
+
264
+ def __init__(self, codes, width=512):
265
+ super().__init__()
266
+ store_attr('codes,width')
267
+ self.embed = nn.Embedding(codes+1, width)
268
+
269
+ def forward(self, x):
270
+ x[torch.isnan(x)] = self.codes # separate code for NaNs which represent missing conditioning
271
+ return self.embed(x.to(torch.long))
272
+
273
+
274
+ class BinnedEmbedding(nn.Module):
275
+ default = torch.nan
276
+
277
+ def __init__(self, vmin=0, vmax=1, bins=32, width=512):
278
+ super().__init__()
279
+ store_attr('vmin,vmax,bins')
280
+ self.embed = nn.Embedding(bins+1, width)
281
+
282
+ def forward(self, x):
283
+ # calculate the bin index
284
+ qx = ((x - self.vmin) / (self.vmax - self.vmin) * self.bins).to(torch.long)
285
+ qx.clamp_(0,self.bins-1)
286
+ qx[torch.isnan(x)] = self.bins # separate bin for NaNs which represent missing conditioning
287
+ return self.embed(qx.to(torch.long))
288
+
289
+ class SpeakerEmbedding(nn.Module):
290
+ def __init__(self, spk_width, width):
291
+ super().__init__()
292
+ store_attr('spk_width,width')
293
+
294
+ self.default = torch.full((spk_width,), 0, dtype=torch.float16)
295
+ self.spk_to_hidden = nn.Linear(spk_width, width) if spk_width != width else None
296
+
297
+ def forward(self, x):
298
+ x = F.normalize(x, dim=-1)
299
+ if self.spk_to_hidden: x = self.spk_to_hidden(x.to(self.spk_to_hidden.weight.dtype))
300
+ return x
301
+
302
+ from webdataset.filters import default_collation_fn
303
+
304
+ class SADelARTransformer(nn.Module):
305
+ def __init__(self, depth=3, ctx_n=2250,
306
+ stoks_len=750, stoks_codes=4097, stoks_width=None,
307
+ spk_width=None,
308
+ atoks_width=None,
309
+ n_head=3, head_width=64, ffn_mult=4,
310
+ quantizers=8, speaker_map={"1":0}, tunables=Tunables()):
311
+ super().__init__()
312
+ self.quantizers = quantizers
313
+ self.codes = 1024
314
+ width = n_head * head_width
315
+ store_attr("depth,ctx_n,stoks_len,stoks_codes,stoks_width,spk_width,atoks_width,n_head,head_width,ffn_mult,quantizers,speaker_map")
316
+ self.width = width
317
+ self.base_width = 3 * head_width
318
+ self.tunables = tunables
319
+
320
+ if stoks_width is None: stoks_width = width
321
+ if spk_width is None: spk_width = width
322
+ self.emb_factor = width != stoks_width
323
+
324
+ if tunables.rope:
325
+ self.positional_embeddings = None
326
+ else:
327
+ self.register_buffer('positional_embeddings', sinusoids(ctx_n, width))
328
+
329
+ self.cond_embeddings = nn.ModuleDict({
330
+ 'lang': CategoricalEmbedding(len(languages.languages), width),
331
+ 'snr': BinnedEmbedding(vmin=-10, vmax=70, bins=10, width=width),
332
+ 'c50': BinnedEmbedding(vmin=0, vmax=60, bins=6, width=width),
333
+ 'speaker': SpeakerEmbedding(spk_width, width),
334
+ })
335
+
336
+ self.semantic_embedding = nn.Embedding(stoks_codes, stoks_width)
337
+ if self.emb_factor:
338
+ self.emb_to_hidden = nn.Linear(stoks_width, width)
339
+ if self.tunables.causal_encoder or self.tunables.force_hidden_to_emb:
340
+ self.hidden_to_emb = nn.Linear(width, stoks_width)
341
+
342
+ qk_scale = self.tunables.query_mult * 8 / math.sqrt(head_width)
343
+
344
+ encoder_depth = int(depth * 2 * tunables.encoder_depth_ratio)
345
+ decoder_depth = depth * 2 - encoder_depth
346
+ self.encoder = nn.Sequential(*[
347
+ ResidualAttentionBlock(width, n_head, qk_scale=qk_scale, ffn_mult=ffn_mult, rope=tunables.rope) for _ in range(encoder_depth)
348
+ ])
349
+ self.ln_post = LayerNorm(width)
350
+
351
+ self.embds = DelSumEmbedding(
352
+ pos_embs=self.positional_embeddings, length=ctx_n,
353
+ n_head=n_head, head_width=head_width, atoks_width=atoks_width,
354
+ quantizers=quantizers,
355
+ )
356
+ self.decoder = BaseDecoder(qk_scale=qk_scale, length=ctx_n,
357
+ n_head=n_head, width=n_head * head_width,
358
+ ffn_mult=ffn_mult, depth=decoder_depth,
359
+ rope=tunables.rope)
360
+ self.head = DelSumHead(n_head=n_head, head_width=head_width, quantizers=quantizers)
361
+ for l in self.decoder.layers:
362
+ l.cross_attn.key_subsampling = 3
363
+
364
+ self.register_buffer('val_true', torch.zeros(self.quantizers))
365
+ self.register_buffer('val_total', torch.zeros(self.quantizers))
366
+ self.apply(self.init_transformer)
367
+
368
+ def setup(self, device):
369
+ pass
370
+
371
+ def load_frozen_semantic_embeddings(self, vqmodel):
372
+ with torch.no_grad():
373
+ self.semantic_embedding.weight[:] = vqmodel.rq.layers[0]._codebook.embed[0]
374
+ self.semantic_embedding.lr_scale = 0
375
+
376
+ def load_frozen_acoustic_embeddings(self, amodel):
377
+ for i in range(self.quantizers):
378
+ self.decoder.embeddings[i].set_frozen_embeddings(amodel.quantizer.vq.layers[i].codebook)
379
+
380
+ def init_transformer(self, m):
381
+ if isinstance(m, LinearHead):
382
+ m.no_weight_decay = True
383
+ torch.nn.init.constant_(m.weight, 0)
384
+ elif isinstance(m, QueryHead):
385
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
386
+ torch.nn.init.constant_(m.weight, 0)
387
+ elif isinstance(m, nn.Embedding):
388
+ m.no_weight_decay = True
389
+ m.lr_scale = self.tunables.embeddings_lr_scale
390
+ std = self.tunables.embeddings_std
391
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
392
+ elif isinstance(m, nn.Linear):
393
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
394
+ std = self.tunables.init_std / m.weight.shape[1]
395
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
396
+ if m.bias is not None:
397
+ torch.nn.init.trunc_normal_(m.bias, std=std, a=-3*std, b=3*std)
398
+ elif isinstance(m, nn.LayerNorm):
399
+ m.no_weight_decay = True
400
+ torch.nn.init.constant_(m.bias, 0)
401
+ torch.nn.init.constant_(m.weight, 1)
402
+
403
+ def embed_stoks(self, Stoks):
404
+ b,n = Stoks.shape
405
+ if self.stoks_len == 1500:
406
+ # converts 50 toks/s to 75 toks/s by adding padding between every two tokens
407
+ x = Stoks.reshape(b,n//2,2)
408
+ x = x.repeat_interleave(2, -1)[:,:,:3]
409
+ x[:,:,1] = 1024
410
+ x = x.reshape(b,n//2*3)
411
+ else:
412
+ # it's a lot easier with 25 toks/s
413
+ x = Stoks
414
+ # embed semantic tokens
415
+ Sembs = self.semantic_embedding(x.to(torch.long))
416
+ if self.emb_factor:
417
+ Sembs = self.emb_to_hidden(Sembs)
418
+ return Sembs
419
+
420
+ def _encoder(self, semb, positions):
421
+ x = semb
422
+ for l in self.encoder: x = l(x, positions, causal=self.tunables.causal_encoder)
423
+ return self.ln_post(x)
424
+
425
+ def run_encoder(self, Stoks, conds):
426
+ bs = Stoks.shape[0]
427
+
428
+ semb = self.embed_stoks(Stoks)
429
+ with record_function("encoder"):
430
+ if self.positional_embeddings is not None: semb = semb + self.positional_embeddings
431
+ positions = torch.arange(0, semb.shape[1], device=semb.device)
432
+ xenc = self._encoder(semb, positions)
433
+ if self.training and self.tunables.causal_encoder:
434
+ enc_logits = (self.hidden_to_emb(xenc) @ self.semantic_embedding.weight.to(xenc.dtype).T).float()
435
+ enc_logits = enc_logits * self.tunables.output_mult / (self.width / self.base_width)
436
+ else:
437
+ enc_logits = None
438
+
439
+ cond_embs = torch.zeros((bs,semb.shape[-1]), dtype=semb.dtype, device=semb.device)
440
+ for k in self.cond_embeddings.keys():
441
+ samples = [(x.get(k, self.cond_embeddings[k].default),) for x in conds]
442
+ c = default_collation_fn(samples)[0]
443
+ # print(c.shape, type(c), getattr(c, 'device', None))
444
+ if isinstance(c, np.ndarray): c = torch.tensor(c, device=Stoks.device)
445
+ if isinstance(c, torch.Tensor): c = c.to(device=Stoks.device)
446
+ cond_embs += self.cond_embeddings[k](c)
447
+
448
+ return xenc + cond_embs.unsqueeze(1), positions, enc_logits
449
+
450
+ def forward(self, Stoks, Atoks, conds, out_stoks=None, out_atoks=None, noloss=False, xenc=None, xenc_positions=None, atoks_positions=None):
451
+ if xenc is None:
452
+ Stoks, Atoks = [x.to(dtype=torch.long) for x in (Stoks, Atoks)]
453
+ xenc, xenc_positions, enc_logits = self.run_encoder(Stoks, conds)
454
+ with record_function("decoder"):
455
+ embs = self.embds(Atoks, xenc)
456
+ if atoks_positions is None: atoks_positions = torch.arange(0, embs.shape[1], device=embs.device)
457
+ x = self.decoder(embs, atoks_positions, xenc, xenc_positions)
458
+ logits = self.head(x, embeddings=self.embds.embeddings)
459
+ logits *= self.tunables.output_mult / (self.width / self.base_width)
460
+
461
+ if noloss:
462
+ return logits
463
+
464
+ with record_function("loss"):
465
+ loss = 0
466
+ for i in range(self.quantizers):
467
+ loss += F.cross_entropy(logits[:,i,:-1].reshape(-1,logits.shape[-1]), Atoks[:,i,1:].reshape(-1), ignore_index=1024)
468
+ if self.training and i == 0:
469
+ loss *= self.tunables.q0_loss_mult
470
+ loss_denom = self.quantizers
471
+ if self.training: loss_denom += - 1 + self.tunables.q0_loss_mult
472
+ loss /= loss_denom
473
+ if self.training and self.tunables.causal_encoder:
474
+ loss += 0.1 * F.cross_entropy(enc_logits[:,:-1].transpose(-1,-2), Stoks[:,1:])
475
+
476
+ if not self.training:
477
+ for i in range(self.quantizers):
478
+ Atoks_i = Atoks[:,i,1:]
479
+ valid_Atoks = Atoks_i != 1024
480
+ self.val_true[i] += (logits[:,i,:-1].argmax(-1)[valid_Atoks] == Atoks_i[valid_Atoks]).float().sum()
481
+ self.val_total[i] += valid_Atoks.float().sum()
482
+
483
+ return logits, loss
484
+
485
+ def get_metrics(self):
486
+ metrics = {
487
+ f'acc_{i}':x.item() for i,x in enumerate(self.val_true / self.val_total)
488
+ }
489
+ self.val_true[:] = 0
490
+ self.val_total[:] = 0
491
+ return metrics
492
+
493
+ #
494
+ # inference
495
+ #
496
+ @classmethod
497
+ def load_model(cls, ref="collabora/whisperspeech:s2a-q4-small-en+pl.model", spec=None, device=None):
498
+ spec = inference.load_model(ref=ref, spec=spec, device=device)
499
+ if '_extra_state' not in spec['state_dict'] and 'speaker_map' in spec['config']: spec['state_dict']['_extra_state'] = { 'speaker_map': spec['config']['speaker_map'] }
500
+ model = cls(**spec['config'], tunables=Tunables(**Tunables.upgrade(spec['tunables'])))
501
+ model.load_state_dict(spec['state_dict'])
502
+ model.eval().to(device)
503
+ return model
504
+
505
+ def get_extra_state(self):
506
+ return { 'speaker_map': self.speaker_map }
507
+
508
+ def set_extra_state(self, st):
509
+ self.speaker_map = st['speaker_map']
510
+
511
+ def load_checkpoint(self, local_filename_or_obj):
512
+ if isinstance(local_filename_or_obj, (str, Path)):
513
+ spec = torch.load(local_filename_or_obj, map_location='cpu')
514
+ else:
515
+ spec = local_filename_or_obj
516
+ assert 'pytorch-lightning_version' in spec, 'not a valid PyTorch Lightning checkpoint'
517
+ state_dict = {k.replace('model.', ''):v
518
+ for k,v in spec['state_dict'].items()}
519
+ self.load_state_dict(state_dict)
520
+ return self
521
+
522
+ def save_model(self, fname):
523
+ torch.save(dict(config = self.__stored_args__,
524
+ tunables = dataclasses.asdict(self.tunables),
525
+ state_dict = self.state_dict()), fname)
526
+
527
+ def switch_dtypes(self, dtype=torch.float16):
528
+ self.dtype = dtype
529
+ for n,m in self.named_modules():
530
+ # convert every leaf layer apart from the LayerNorms
531
+ if isinstance(m, (nn.Linear, nn.Embedding)):
532
+ m.to(dtype)
533
+ # take care of buffers ([kv]_cache, masks) that are not in the leaf layers
534
+ for bn,b in m.named_buffers(recurse=False):
535
+ setattr(m,bn,b.to(dtype))
536
+
537
+ def optimize(self, max_batch_size=1, dtype=torch.float16, torch_compile=True):
538
+ for emb in self.embds.embeddings:
539
+ emb.convert_for_eval()
540
+ for l in self.encoder:
541
+ l.attn.convert_for_eval()
542
+ for l in self.decoder.layers:
543
+ l.attn.convert_for_eval()
544
+ l.cross_attn.convert_for_eval()
545
+ l.setup_kv_cache(max_batch_size, self.ctx_n, self.stoks_len)
546
+ self.switch_dtypes(dtype)
547
+ if torch_compile:
548
+ self.generate_next = torch.compile(self.generate_next, mode="reduce-overhead", fullgraph=True)
549
+
550
+ def optimize_training(self):
551
+ self.decoder = torch.compile(self.decoder, fullgraph=True, mode="reduce-overhead")
552
+ self._encoder = torch.compile(self._encoder, fullgraph=True, mode="reduce-overhead")
553
+
554
+ @property
555
+ def device(self):
556
+ return next(self.parameters()).device
557
+
558
+ def generate_one(self, toks, positions, langs, xenc, xenc_positions, T, top_k):
559
+ probs = self(None, toks, None, langs, noloss=True, xenc=xenc, xenc_positions=xenc_positions, atoks_positions=positions)
560
+ probs = probs[:,:,-1]
561
+ return inference.sample(probs, T, top_k)
562
+
563
+ def generate_next(self, *args, **kwargs):
564
+ return self.generate_one(*args, **kwargs)
565
+
566
+ @torch.no_grad()
567
+ def generate(self, stoks, speakers, langs=None, atoks_prompt=None, N=None, bs=1, T=0.7, top_k=None, show_progress_bar=True, step=None, subsample_enc=False):
568
+ dev = self.device
569
+ N = N or len(stoks) * 3
570
+ stoks = F.pad(stoks.to(dev), (1, self.stoks_len - len(stoks) - 1), value=self.stoks_codes-1).unsqueeze(0)
571
+ speakers = speakers.to(device=dev, dtype=self.dtype)
572
+ toks = torch.full((bs,self.quantizers,self.ctx_n), self.codes+1, dtype=torch.long, device=dev)
573
+ T = torch.tensor(T, device=dev)
574
+
575
+ start = 0 # number of valid tokens or the index of first empty spot
576
+ if atoks_prompt is not None:
577
+ start = atoks_prompt.shape[-1]
578
+ for i in range(self.quantizers):
579
+ toks[:,i,1+i:start+i+1] = atoks_prompt[:,i]
580
+ start += 1 # we always start with at least an SOT
581
+
582
+ with record_function("encode"):
583
+ stoks, speakers = [x.repeat(bs, 1) for x in (stoks, speakers)]
584
+ xenc, xenc_positions, _ = self.run_encoder(stoks, [dict(speaker = s, snr=60, c50=60) for s in speakers])
585
+ toks_positions = torch.arange(N, device=dev)
586
+ with record_function("prefill"):
587
+ initial = self.generate_one(toks[:,:,:start], toks_positions[:start], langs, xenc, xenc_positions, T, top_k)
588
+ toks[:,:start,start:start+1] = initial[:,:start]
589
+ start += 1
590
+
591
+ with inference.inference_context():
592
+ it = range(start,min(N,self.ctx_n-1))
593
+ if show_progress_bar: it = progress_bar(it)
594
+
595
+ for i in it:
596
+ with record_function("generate_one"):
597
+ toks[:,:i,i:i+1] = self.generate_next(toks[:,:,i-1:i], toks_positions[i-1:i], langs, xenc, xenc_positions, T, top_k)[:,:i]
598
+
599
+ # for profiling, debugging or early exit
600
+ if step is not None: step()
601
+ # shift tokens
602
+ toks = toks[:,:,1:N]
603
+ for j in range(self.quantizers):
604
+ toks[:, j] = torch.roll(toks[:, j], -j)
605
+ return toks[:,:,:N-4]
606
+
607
+ # %% ../nbs/4B. Multi-language semantic to acoustic token modeling-Copy2.ipynb 15
608
+ def _make_model(size:str, quantizers:int=4, tunables:Tunables=Tunables(), **kwargs):
609
+ kwargs = dict(quantizers=quantizers, tunables=tunables, **kwargs)
610
+ if size == 'micro':
611
+ return SADelARTransformer(depth=4, n_head=3, ffn_mult=2, **kwargs)
612
+ if size == 'tiny-narrow':
613
+ return SADelARTransformer(depth=4, n_head=6, ffn_mult=1, **kwargs)
614
+ if size == 'tiny':
615
+ return SADelARTransformer(depth=4, n_head=6, **kwargs)
616
+ if size == 'base':
617
+ return SADelARTransformer(depth=6, n_head=8, **kwargs)
618
+ if size == 'base-deep':
619
+ return SADelARTransformer(depth=9, n_head=8, **kwargs)
620
+ if size == 'base-wide':
621
+ return SADelARTransformer(depth=6, n_head=12, **kwargs)
622
+ if size == 'small/2':
623
+ return SADelARTransformer(depth=9, n_head=12, **kwargs)
624
+ if size == 'small':
625
+ return SADelARTransformer(depth=12, n_head=12, **kwargs)
626
+ if size == 'medium':
627
+ return SADelARTransformer(depth=24, n_head=16, **kwargs)
628
+
629
+ def make_model(size:str, quantizers:int=4, frozen_embeddings_model:str=None, frozen_acoustic_embeddings:bool=False, spk_width:int=None, tunables:Tunables=Tunables(), dataset=None):
630
+ from encodec.model import EncodecModel
631
+ from whisperspeech import vq_stoks
632
+
633
+ amodel = EncodecModel.encodec_model_24khz() if frozen_acoustic_embeddings else None
634
+ vqmodel = vq_stoks.RQBottleneckTransformer.load_model(frozen_embeddings_model) if frozen_embeddings_model else None
635
+ model = _make_model(size, quantizers, tunables,
636
+ spk_width=spk_width,
637
+ atoks_width=amodel and amodel.quantizer.vq.layers[0]._codebook.embed.shape[-1],
638
+ stoks_codes=vqmodel.vq_codes+1, stoks_width=vqmodel.rq.layers[0]._codebook.embed[0].shape[-1])
639
+ if vqmodel: model.load_frozen_semantic_embeddings(vqmodel)
640
+ if amodel: model.load_frozen_acoustic_embeddings(amodel)
641
+ return model
642
+
643
+ def load_model(*args, **kwargs):
644
+ return SADelARTransformer.load_model(*args, **kwargs)
whisperspeech/split_out_val_datasets.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/3D. Split out validation.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/3D. Split out validation.ipynb 1
7
+ import os
8
+ import webdataset as wds
9
+ from pathlib import Path
10
+ import torch
11
+ from fastprogress import progress_bar
12
+ from fastcore.script import call_parse
13
+ import numpy as np
14
+ import random
15
+ from collections import Counter, defaultdict
16
+ from whisperspeech import utils, vad_merge
17
+ import sys
18
+ import copy
19
+
20
+ # %% ../nbs/3D. Split out validation.ipynb 2
21
+ @call_parse
22
+ def split_dataset(
23
+ shard_dir:str,
24
+ splits:str,
25
+ mvad_kind:str=None,
26
+ ):
27
+ mode = Path(shard_dir).name
28
+
29
+ if mode == "audio":
30
+ shards = utils.shard_glob(shard_dir+'/*.tar')
31
+ else:
32
+ shards = utils.shard_glob(shard_dir+'/*.tar.gz')
33
+
34
+ splits = splits.split()
35
+
36
+ # unpacks sample id 'src_key_001' into 'src_key', '001'
37
+ def unpack_id(x):
38
+ return x.rsplit('_', 1)
39
+
40
+ def make_tar_writer(name):
41
+ name.parent.mkdir(parents=True, exist_ok=True)
42
+ return wds.TarWriter(str(name))
43
+
44
+ suffix = ".tar.gz" if mode != 'audio' else ".tar"
45
+
46
+ bufs = {k:[] for k in splits}
47
+ outputs = {k:make_tar_writer(Path(k).parent/mode/(Path(k).name+suffix)) for k in splits}
48
+
49
+ if mode == "audio" or mode == "mvad":
50
+ needles = {}
51
+ chunks = defaultdict(lambda: [])
52
+ for split in splits:
53
+ for k in utils.readlines(split):
54
+ file_id, chunk_id = unpack_id(k)
55
+ needles[file_id] = bufs[split]
56
+ chunks[file_id].append(int(chunk_id))
57
+ else:
58
+ needles = {k:bufs[split] for split in splits for k in utils.readlines(split)}
59
+ chunks = None
60
+
61
+ print(f"Generating splits: {' '.join(outputs.keys())}, looking for {len(needles)} {mode} samples...")
62
+
63
+ ds = wds.WebDataset(shards).compose(
64
+ wds.select(lambda x: x['__key__'] in needles),
65
+ )
66
+ if mode == 'mvad': ds = ds.decode()
67
+
68
+ dl = wds.WebLoader(ds, num_workers=0 if len(shards) > 10 else 16, batch_size=None)
69
+
70
+ for s in progress_bar(dl, total='noinfer'):
71
+ if mode == "mvad":
72
+ mask = np.zeros(s[mvad_kind+'.vad.npy'].shape[0], dtype=np.bool_)
73
+ for i in chunks[s['__key__']]: mask[i] = True
74
+ new = {}
75
+ for k in ['__key__', mvad_kind+'.vad.npy', mvad_kind+'.spk_emb.npy', mvad_kind+'.subvads.pyd', 'gain_shift.npy']:
76
+ v = s[k]
77
+ if isinstance(v, torch.Tensor): v = v.numpy()
78
+ new[k] = v
79
+ new['mask.npy'] = mask
80
+ s = new
81
+ needles[s['__key__']].append(copy.deepcopy(s))
82
+ del needles[s['__key__']]
83
+ pass
84
+ print()
85
+
86
+ for split,buf in bufs.items():
87
+ for s in sorted(buf, key=lambda x: x['__key__']):
88
+ outputs[split].write(s)
89
+
90
+ if len(needles) > 0:
91
+ print(f"Missed {len(needles)} samples!")
92
+ sys.exit(1)
whisperspeech/t2s_up_wds_mlang_enclm.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/5B. Multi-lang text to semantic token modeling.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['load_dataset', 'rand', 'Tunables', 'T2SEmbedding', 'Encoder', 'TSARTransformer', 'make_model']
5
+
6
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 1
7
+ import dataclasses
8
+ import random
9
+ import math
10
+ import itertools
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ from torch.profiler import record_function
15
+
16
+ from huggingface_hub import hf_hub_download
17
+ from fastcore.basics import store_attr
18
+ from fastprogress import progress_bar
19
+
20
+ from pathlib import Path
21
+
22
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 2
23
+ from whisperspeech.modules import *
24
+ from whisperspeech import languages, inference
25
+
26
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 6
27
+ import re
28
+
29
+ class CharTokenizer:
30
+ """Trivial tokenizer – just use UTF-8 bytes"""
31
+ eot = 0
32
+
33
+ def encode(self, txt):
34
+ return list(bytes(txt.strip(), 'utf-8'))
35
+
36
+ def decode(self, tokens):
37
+ return bytes(tokens).decode('utf-8')
38
+
39
+ def tokenizer(ikey, okey, length):
40
+ """Tokenizes a transcript"""
41
+ tok = CharTokenizer()
42
+ def _tokenizer(samples):
43
+ for s in samples:
44
+ toks = torch.tensor(tok.encode(s[ikey]))
45
+ s[okey] = F.pad(toks, (0, length - toks.shape[-1]), value=tok.eot)
46
+ yield s
47
+ return _tokenizer
48
+
49
+ def ar_padder(ikey, okey, length, pad_token):
50
+ """Pads the tokens for autoregresive training"""
51
+ import numpy as np
52
+
53
+ def _ar_padder(samples):
54
+ for s in samples:
55
+ toks = s[ikey]
56
+ if isinstance(toks, (list, np.ndarray)): toks = torch.tensor(toks)
57
+ toks = toks.to(torch.long)
58
+ s['in_' +okey] = F.pad(toks, (1, length - toks.shape[-1] - 1), value=pad_token)
59
+ s['out_'+okey] = F.pad(toks, (0, length - toks.shape[-1]), value=pad_token)
60
+ yield s
61
+ return _ar_padder
62
+
63
+ def char_per_seconder(txt_key, stoks_key, cps_key, stoks_per_second=25):
64
+ """Adds the characters per second metric to the input data"""
65
+ def _char_per_seconder(samples):
66
+ for s in samples:
67
+ secs = s[stoks_key].shape[-1] / stoks_per_second
68
+ s[cps_key] = len(s[txt_key]) / secs
69
+ yield s
70
+ return _char_per_seconder
71
+
72
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 7
73
+ def load_dataset(
74
+ dataset_dir:Path,
75
+ stoks_dir:str=None,
76
+ txt_dir:str=None,
77
+ vq_codes:int=4096,
78
+ weight:float=1,
79
+ validation:bool=False,
80
+ exclude_datasets:str="txt-random-valid",
81
+ ):
82
+ import webdataset as wds
83
+ from whisperspeech import utils, languages
84
+
85
+ dataset_dir = Path(dataset_dir)
86
+
87
+ if txt_dir is None:
88
+ for name in ['small.en-txt', 'medium-txt']:
89
+ if (dataset_dir/name).exists():
90
+ txt_dir = name
91
+ break
92
+ assert txt_dir is not None, f"No transcripts found in {dataset_dir}"
93
+
94
+ txt_path = dataset_dir/f'{txt_dir}/*.tar.gz'
95
+ shards = utils.shard_glob(txt_path)
96
+ assert len(shards), f"No data shards found in {txt_path}."
97
+
98
+ with open(dataset_dir/'txt-samples.list') as f: samples = len(f.readlines())
99
+ language = utils.readlines(dataset_dir/'language')[0]
100
+ language = languages.to_id(language)
101
+
102
+ excludes = {x
103
+ for dir in exclude_datasets.split()
104
+ for x in utils.readlines(dataset_dir/Path(dir)/"txt-samples.list")
105
+ } if not validation and exclude_datasets else set()
106
+
107
+ def set_language(x):
108
+ x['language'] = language
109
+ return x
110
+
111
+ same_on_all_nodes = lambda urls: urls # will only be used for validation
112
+ ds = wds.WebDataset(shards, resampled=not validation, nodesplitter=same_on_all_nodes).compose(
113
+ wds.decode(),
114
+ utils.merge_in(utils.derived_dataset(stoks_dir)),
115
+ wds.select(lambda s: s['__key__'] not in excludes and len(s['stoks.npy']) > 0), # discard validation samples
116
+ tokenizer('txt', 'ttoks', length=550),
117
+ ar_padder('stoks.npy', 'stoks', length=750, pad_token=vq_codes-1),
118
+ ar_padder('ttoks', 'ttoks', length=550, pad_token=CharTokenizer.eot),
119
+ char_per_seconder('txt', 'stoks.npy', 'cps', stoks_per_second=25),
120
+ wds.map(set_language),
121
+ wds.to_tuple('in_ttoks', 'out_ttoks', 'language', 'cps', 'in_stoks', 'out_stoks'),
122
+ )
123
+ if validation:
124
+ ds = ds.compose(
125
+ wds.batched(samples)
126
+ ).slice(1)
127
+ else:
128
+ ds = ds.compose(
129
+ wds.shuffle(20000, initial=20000),
130
+ wds.batched(2048)
131
+ )
132
+ ds.total_samples = samples
133
+ ds.stoks_len = 750
134
+ ds.stoks_codes = vq_codes
135
+ ds.ttoks_len = 550
136
+ ds.weight = weight
137
+
138
+ return ds
139
+
140
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 12
141
+ def rand(start, end):
142
+ return random.random() * (end - start) + start
143
+
144
+ @dataclasses.dataclass
145
+ class Tunables:
146
+ init_std :float = 1
147
+ embeddings_std :float = .01
148
+ embeddings_lr_scale: float = 5
149
+ embedding_projector_lr_scale: float = 2.5
150
+ output_mult :float = .35
151
+ query_mult :float = 1
152
+ encoder_depth_ratio :float = 0.25
153
+ causal_encoder: bool = True
154
+ eot_dropout_p :float = .5
155
+ cps_input: bool = True
156
+ cps_bins: int = 32
157
+ padding_token_offset: int = 0
158
+
159
+ lr0 :float = 1.5e-3
160
+ clip_gradient_norm :float = .2
161
+ weight_decay :float = 1e-1
162
+ warmup_steps :float = 4000
163
+
164
+ random :bool = False
165
+
166
+ def __post_init__(self):
167
+ # randomize the hyperparams if requested
168
+ if self.random:
169
+ self.init_std = 10**rand(-1,1)
170
+ self.embeddings_std = 10**rand(-3,-.7)
171
+ self.embeddings_lr_scale = rand(2,6)
172
+ self.output_mult = rand(0.25,0.65)
173
+ self.query_mult = 2**rand(-2,3)
174
+ self.encoder_depth_ratio = 0.25
175
+
176
+ self.lr0 = rand(1,5)*1e-3
177
+ self.clip_gradient_norm = 10**rand(-3,0)
178
+ self.warmup_steps = 100*(10**rand(1,1.85))
179
+
180
+ @staticmethod
181
+ def upgrade(args):
182
+ args = {k:v for k,v in args.items()}
183
+ def old_default(name, value):
184
+ if name not in args: args[name] = value
185
+ old_default('padding_token_offset', -1)
186
+ return args
187
+
188
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 13
189
+ class T2SEmbedding(nn.Module):
190
+ def __init__(self, length=1500, codes=1024, width=384, pos_embs=None, stoks_width=384):
191
+ super().__init__()
192
+ self.embedding = FlexEmbeddings(codes, width, special_codes=1, frozen_width=stoks_width)
193
+ if pos_embs is None: pos_embs = sinusoids(length, width)
194
+ self.register_buffer("positional_embedding", pos_embs)
195
+
196
+ def forward(self, Stoks, xenc, cps=None, offset=0):
197
+ Sembs = self.embedding(Stoks)
198
+ xin = (Sembs + self.positional_embedding[offset : offset + Sembs.shape[1]]).to(xenc.dtype)
199
+ if cps is not None: xin = xin + cps
200
+ return xin, offset
201
+
202
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 14
203
+ class Encoder(nn.Module):
204
+ def __init__(self, depth=6, width=384, n_head=6, length=1500, codes=1024, emb_width=384, ffn_mult=4, pos_embs=None, tunables=Tunables()):
205
+ super().__init__()
206
+ self.emb_width = emb_width
207
+ self.tunables = tunables
208
+
209
+ self.embedding = FlexEmbeddings(codes, width, frozen_width=emb_width)
210
+
211
+ if pos_embs is None: pos_embs = sinusoids(length, width)
212
+ self.register_buffer("positional_embedding", pos_embs)
213
+
214
+ self.layers = nn.ModuleList([
215
+ ResidualAttentionBlock(width, n_head,
216
+ qk_scale=tunables.query_mult*8/math.sqrt(width/n_head), ffn_mult=ffn_mult) for _ in range(depth)
217
+ ])
218
+
219
+ self.ln_post = LayerNorm(width)
220
+
221
+ mask = torch.empty(length, length).fill_(-torch.inf).triu_(1)
222
+ self.register_buffer("mask", mask, persistent=False)
223
+
224
+ def forward(self, Stoks, positions, lang_emb=None):
225
+ xin = self.embedding(Stoks)
226
+
227
+ if lang_emb is not None: xin = xin + lang_emb
228
+
229
+ x = (xin +
230
+ self.positional_embedding[positions]).to(xin.dtype)
231
+
232
+ for l in self.layers: x = l(x, positions,
233
+ causal=self.tunables.causal_encoder and self.training,
234
+ mask=self.mask if self.tunables.causal_encoder and not self.training else None)
235
+
236
+ return self.ln_post(x)
237
+
238
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 15
239
+ class TSARTransformer(nn.Module):
240
+ def __init__(self, depth=6, n_head=6, head_width=64, ffn_mult=4,
241
+ ttoks_len=200, ttoks_codes=256, ttoks_width=None,
242
+ stoks_len=1500, stoks_codes=1024, stoks_width=None,
243
+ tunables=Tunables()):
244
+ super().__init__()
245
+ store_attr("depth,n_head,head_width,ffn_mult,stoks_width,ttoks_width,ttoks_len,stoks_len,ttoks_codes,stoks_codes")
246
+
247
+ width = n_head * head_width
248
+ self.width = width
249
+ self.base_width = 3 * head_width
250
+ self.tunables = tunables
251
+ if self.stoks_width is None: self.stoks_width = self.width
252
+ if self.ttoks_width is None: self.ttoks_width = self.width
253
+
254
+ self.lang_embeddings = nn.Embedding(len(languages.languages), width)
255
+ if tunables.cps_input:
256
+ self.cps_embeddings = nn.Embedding(tunables.cps_bins, self.width)
257
+ else:
258
+ self.cps_embeddings = None
259
+
260
+ encoder_depth = int(depth * 2 * tunables.encoder_depth_ratio)
261
+ decoder_depth = depth * 2 - encoder_depth
262
+ tformer_args = dict(width=width, n_head=n_head, ffn_mult=ffn_mult, tunables=tunables)
263
+ self.encoder = Encoder(length=ttoks_len, codes=ttoks_codes, emb_width=self.ttoks_width, depth=encoder_depth, **tformer_args)
264
+ self.embeddings = T2SEmbedding(length=stoks_len, codes=stoks_codes, width=width, stoks_width=self.stoks_width)
265
+
266
+ self.decoder = BaseDecoder(
267
+ length=stoks_len,
268
+ depth=decoder_depth,
269
+ qk_scale=tunables.query_mult*8/math.sqrt(width/n_head),
270
+ width=width, n_head=n_head, ffn_mult=ffn_mult,
271
+ )
272
+ self.tokenizer = None
273
+
274
+ self.apply(self.init_transformer)
275
+
276
+ def load_frozen_semantic_embeddings(self, vqmodel):
277
+ self.embeddings.embedding.set_frozen_embeddings(vqmodel.rq.layers[0]._codebook.embed[0])
278
+
279
+ def setup(self, device):
280
+ pass
281
+
282
+ def init_transformer(self, m):
283
+ if isinstance(m, LinearHead):
284
+ m.no_weight_decay = True
285
+ torch.nn.init.constant_(m.weight, 0)
286
+ elif isinstance(m, QueryHead):
287
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
288
+ torch.nn.init.constant_(m.weight, 0)
289
+ elif isinstance(m, nn.Embedding):
290
+ m.no_weight_decay = True
291
+ m.lr_scale = self.tunables.embeddings_lr_scale
292
+ std = self.tunables.embeddings_std
293
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
294
+ elif isinstance(m, EmbeddingProjector):
295
+ m.lr_scale = self.tunables.embedding_projector_lr_scale
296
+ std = self.tunables.init_std
297
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
298
+ elif isinstance(m, nn.Linear):
299
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
300
+ std = self.tunables.init_std / m.weight.shape[1]
301
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
302
+ if m.bias is not None:
303
+ torch.nn.init.trunc_normal_(m.bias, std=std, a=-3*std, b=3*std)
304
+ elif isinstance(m, nn.LayerNorm):
305
+ m.no_weight_decay = True
306
+ torch.nn.init.constant_(m.bias, 0)
307
+ torch.nn.init.constant_(m.weight, 1)
308
+
309
+ def _embed_cps(self, cpss):
310
+ if self.cps_embeddings is None: return None
311
+
312
+ cps_bin = (cpss / 20 * self.tunables.cps_bins).to(torch.long)
313
+ cps_bin[cps_bin >= self.tunables.cps_bins] = self.tunables.cps_bins-1
314
+ return self.cps_embeddings(cps_bin).unsqueeze(1)
315
+
316
+ def run_encoder(self, in_ttoks, languages, cpss):
317
+ if len(languages.shape) != 3: lang_embs = self.lang_embeddings(languages)
318
+ else: lang_embs = languages
319
+ if len(lang_embs.shape) == 2: lang_embs = lang_embs.unsqueeze(1)
320
+
321
+ cps_emb = self._embed_cps(cpss)
322
+
323
+ with record_function("encoder"):
324
+ positions = torch.arange(0, in_ttoks.shape[1], device=in_ttoks.device)
325
+ xenc = self.encoder(in_ttoks.to(torch.long), positions, lang_emb=lang_embs)
326
+
327
+ return xenc, positions, cps_emb
328
+
329
+ def forward(self, in_ttoks, out_ttoks, languages, cpss, in_stoks, out_stoks=None, in_stoks_positions=None, loss=True, offset=None, xenc=None, xenc_positions=None, cps_emb=None):
330
+ if xenc is None:
331
+ xenc, xenc_positions, cps_emb = self.run_encoder(in_ttoks, languages, cpss)
332
+
333
+ with record_function("decoder"):
334
+ x = (self.embeddings.embedding(in_stoks) +
335
+ self.embeddings.positional_embedding[in_stoks_positions] +
336
+ cps_emb).to(xenc[0].dtype)
337
+ x = self.decoder(x, in_stoks_positions, xenc.clone(), xenc_positions)
338
+ logits = self.embeddings.embedding.unembed(x)
339
+ logits = logits * self.tunables.output_mult / (self.width / self.base_width)
340
+
341
+ if loss is not None:
342
+ with record_function("loss"):
343
+ loss = F.cross_entropy(logits.transpose(-1,-2), out_stoks)
344
+ if self.training and self.tunables.causal_encoder:
345
+ enc_logits = self.encoder.embedding.unembed(xenc)
346
+ enc_logits = enc_logits * self.tunables.output_mult / (self.width / self.base_width)
347
+ loss += 0.1 * F.cross_entropy(enc_logits.transpose(-1,-2), out_ttoks)
348
+
349
+ return logits, loss
350
+
351
+ #
352
+ # inference
353
+ #
354
+ @classmethod
355
+ def load_model(cls, ref="collabora/whisperspeech:t2s-small-en+pl.model",
356
+ repo_id=None, filename=None, local_filename=None, spec=None, device=None):
357
+ if repo_id is None and filename is None and local_filename is None and spec is None:
358
+ if ":" in ref:
359
+ repo_id, filename = ref.split(":", 1)
360
+ else:
361
+ local_filename = ref
362
+ if not local_filename and spec is None:
363
+ local_filename = hf_hub_download(repo_id=repo_id, filename=filename)
364
+ if spec is None:
365
+ spec = torch.load(local_filename, map_location=device)
366
+ model = cls(**spec['config'], tunables=Tunables(**Tunables.upgrade(spec['tunables'])))
367
+ model.load_state_dict(spec['state_dict'])
368
+ model.eval().to(device)
369
+ return model
370
+
371
+ def load_checkpoint(self, local_filename_or_obj):
372
+ if isinstance(local_filename_or_obj, (str, Path)):
373
+ spec = torch.load(local_filename, map_location='cpu')
374
+ else:
375
+ spec = local_filename_or_obj
376
+ assert 'pytorch-lightning_version' in spec, 'not a valid PyTorch Lightning checkpoint'
377
+ state_dict = {k.replace('model.', ''):v
378
+ for k,v in spec['state_dict'].items()}
379
+ self.load_state_dict(state_dict)
380
+ return self
381
+
382
+ def save_model(self, fname):
383
+ torch.save(dict(config = self.__stored_args__,
384
+ tunables = dataclasses.asdict(self.tunables),
385
+ state_dict = self.state_dict()), fname)
386
+
387
+ def ensure_tokenizer(self):
388
+ assert not self.training
389
+ if self.tokenizer is None: self.tokenizer = CharTokenizer()
390
+
391
+ def switch_dtypes(self, dtype=torch.float16):
392
+ self.dtype = dtype
393
+ for n,m in self.named_modules():
394
+ # convert every leaf layer apart from the LayerNorms
395
+ if isinstance(m, (nn.Linear, nn.Embedding)):
396
+ m.to(dtype)
397
+ # take care of buffers ([kv]_cache, masks) that are not in the leaf layers
398
+ for bn,b in m.named_buffers(recurse=False):
399
+ setattr(m,bn,b.to(dtype))
400
+
401
+ def optimize(self, max_batch_size=1, dtype=torch.float16, torch_compile=True):
402
+ for emb in [self.embeddings.embedding, self.embeddings.embedding]:
403
+ emb.convert_for_eval()
404
+ for l in self.encoder.layers:
405
+ l.attn.convert_for_eval()
406
+ for l in self.decoder.layers:
407
+ l.attn.convert_for_eval()
408
+ l.cross_attn.convert_for_eval()
409
+ l.setup_kv_cache(max_batch_size, self.stoks_len, self.ttoks_len)
410
+ self.switch_dtypes(dtype)
411
+ if torch_compile:
412
+ self.generate_next = torch.compile(self.generate_next, mode="reduce-overhead", fullgraph=True)
413
+
414
+ def optimize_training(self):
415
+ # breaks with: Error: accessing tensor output of CUDAGraphs that has been overwritten by a subsequent run.
416
+ # somewhere inside LayerNorm???
417
+ self.encoder = torch.compile(self.encoder, fullgraph=True, mode="reduce-overhead")
418
+ self.decoder = torch.compile(self.decoder, fullgraph=True, mode="reduce-overhead")
419
+
420
+ @property
421
+ def device(self):
422
+ return next(self.parameters()).device
423
+
424
+ def generate_one(self, toks, toks_positions, cps_emb, xenc, xenc_positions, T, top_k):
425
+ probs, _ = self(None, None, None, None, toks, in_stoks_positions=toks_positions, loss=None, xenc=xenc, xenc_positions=xenc_positions, cps_emb=cps_emb)
426
+ probs = probs[:,-1]
427
+ probs[self.embeddings.embedding.codes:] = -torch.inf
428
+ return inference.sample(probs, T, top_k)
429
+
430
+ def generate_next(self, *args, **kwargs):
431
+ return self.generate_one(*args, **kwargs)
432
+
433
+ @torch.no_grad()
434
+ def prep(self, txt, cps=15, lang="en"):
435
+ dev = self.device
436
+ ttoks = torch.tensor(self.tokenizer.encode(txt), device=dev)
437
+ ttoks = F.pad(ttoks, (0, self.ttoks_len - len(ttoks)), value=self.tokenizer.eot).unsqueeze(0)
438
+ cpss = torch.tensor([cps], device=dev)
439
+ langs = torch.tensor([languages.to_id(lang)], device=dev)
440
+ return ttoks, cpss, langs
441
+
442
+ @torch.no_grad()
443
+ def generate(self, txt, cps=15, lang="en", stoks_prompt=None, N=None, bs=1, T=0.7, top_k=None, step=None, show_progress_bar=True):
444
+ self.ensure_tokenizer()
445
+ N = N or self.stoks_len
446
+ dev = self.device
447
+ ttoks = []
448
+ langs = []
449
+ if isinstance(lang, list):
450
+ lang0 = lang[0]
451
+ assert isinstance(txt, list), "lang and txt have to be both lists or strings"
452
+ for txt, lang in zip(txt, lang):
453
+ tt = self.tokenizer.encode(txt)
454
+ ttoks += tt
455
+ langs += [languages.to_id(lang)] * len(tt)
456
+ elif isinstance(lang, torch.Tensor):
457
+ langs = lang
458
+ ttoks = self.tokenizer.encode(txt)
459
+ else:
460
+ lang0 = lang
461
+ ttoks = self.tokenizer.encode(txt)
462
+ langs = torch.tensor([languages.to_id(lang)], device=dev)
463
+ ttoks = torch.tensor(ttoks, device=dev)
464
+ ttoks = F.pad(ttoks, (1, self.ttoks_len - len(ttoks) - 1), value=self.tokenizer.eot)
465
+ cpss = torch.tensor([cps], device=dev)
466
+ T = torch.tensor(T, device=dev)
467
+ if not isinstance(langs, torch.Tensor):
468
+ langs = torch.tensor(langs, device=dev)
469
+ langs = F.pad(langs, (1, self.ttoks_len - len(langs) - 1), value=languages.to_id(lang0))
470
+
471
+ toks = torch.zeros((bs,N), dtype=torch.long, device=dev)
472
+ toks[:,0] = self.stoks_codes + self.tunables.padding_token_offset
473
+ start = 0
474
+ if stoks_prompt is not None:
475
+ toks[:,1:len(stoks_prompt)+1] = stoks_prompt
476
+ start = len(stoks_prompt)
477
+ it = range(start+1,N-1)
478
+ if show_progress_bar: it = progress_bar(it)
479
+
480
+ toks_positions = torch.arange(N, device=dev)
481
+ with record_function("encode"):
482
+ ttoks = ttoks.repeat(bs, 1)
483
+ langs, cpss = [x.repeat(bs) for x in (langs, cpss)]
484
+ xenc, xenc_positions, cps_emb = self.run_encoder(ttoks, langs, cpss)
485
+ toks_positions = torch.arange(N+1, device=dev)
486
+
487
+ with record_function("prefill"):
488
+ toks[:,start+1] = self.generate_one(toks[:,:start+1].contiguous(), toks_positions[:start+1], cps_emb, xenc, xenc_positions, T, top_k)[:,0]
489
+ with inference.inference_context():
490
+ for i in it:
491
+ toks[:,i+1] = self.generate_next(toks[:,i:i+1], toks_positions[i:i+1], cps_emb, xenc, xenc_positions, T, top_k)[:,0]
492
+ if (toks[:,i+1] == self.stoks_codes+self.tunables.padding_token_offset).all(): return toks[:,1:i+1]
493
+
494
+ # for profiling, debugging or early exit
495
+ if step is not None: step()
496
+ return toks[:,1:]
497
+
498
+ @torch.no_grad()
499
+ def generate_batch(self, txts, N=None, T=1.1, top_k=7, show_progress_bar=True):
500
+ self.ensure_tokenizer()
501
+ N = self.stoks_len
502
+ dev = self.device
503
+ ttoks = []
504
+ for txt in txts:
505
+ ttoks_ = torch.tensor(self.tokenizer.encode(txt), device=dev)
506
+ ttoks_ = F.pad(ttoks_, (0, self.ttoks_len - len(ttoks_)), value=self.tokenizer.eot).unsqueeze(0)
507
+ ttoks.append(ttoks_)
508
+ ttoks = torch.cat(ttoks, dim=0)
509
+ toks = torch.zeros((len(ttoks),N), dtype=torch.long, device=dev)
510
+ it = range(N)
511
+ if show_progress_bar: it = progress_bar(it)
512
+ for i in it:
513
+ p, _ = self(ttoks, toks[:,:i], loss=None)
514
+ last_p = p[:,-1]
515
+ if top_k:
516
+ last_p[last_p < torch.topk(last_p, top_k).values[:,-1,None]] = -torch.inf
517
+ tok = torch.multinomial((last_p / float(T)).softmax(-1), 1)
518
+ toks[:,i] = tok[:,0]
519
+ if (toks[:,i] == self.stoks_codes-1).all(): return toks[:,:i]
520
+ return toks
521
+
522
+ # %% ../nbs/5B. Multi-lang text to semantic token modeling.ipynb 16
523
+ def _make_model(size:str, tunables:Tunables=Tunables(), dataset=None, **kwargs):
524
+ kwargs = dict(stoks_len = dataset.stoks_len, ttoks_len = dataset.ttoks_len, tunables=tunables, **kwargs)
525
+ if 'stoks_codes' not in kwargs: kwargs['stoks_codes'] = dataset.stoks_codes
526
+ if size == 'micro':
527
+ return TSARTransformer(depth=2, n_head=3, ffn_mult=1, **kwargs)
528
+ if size == 'tiny':
529
+ return TSARTransformer(depth=4, n_head=6, **kwargs)
530
+ if size == 'base':
531
+ return TSARTransformer(depth=6, n_head=8, **kwargs)
532
+ if size == 'small':
533
+ return TSARTransformer(depth=12, n_head=12, **kwargs)
534
+ if size == 'small+':
535
+ return TSARTransformer(depth=12, n_head=16, **kwargs)
536
+ if size == 'medium':
537
+ return TSARTransformer(depth=24, n_head=16, **kwargs)
538
+
539
+ def make_model(size:str, frozen_embeddings_model:str=None, tunables:Tunables=Tunables(), dataset:torch.utils.data.Dataset=None):
540
+ from whisperspeech import vq_stoks
541
+
542
+ if frozen_embeddings_model:
543
+ vqmodel = vq_stoks.RQBottleneckTransformer.load_model(frozen_embeddings_model)
544
+ model = _make_model(size, tunables, dataset, stoks_codes=vqmodel.vq_codes+1, stoks_width=vqmodel.rq.layers[0]._codebook.embed[0].shape[-1])
545
+ model.load_frozen_semantic_embeddings(vqmodel)
546
+ else:
547
+ model = _make_model(size, tunables, dataset, mode=mode)
548
+ return model
whisperspeech/testing.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/C2. Testing.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['test_model']
5
+
6
+ # %% ../nbs/C2. Testing.ipynb 1
7
+ import webdataset as wds
8
+
9
+ # %% ../nbs/C2. Testing.ipynb 2
10
+ def test_model(model, ds, bs=1):
11
+ dev = next(model.parameters()).device
12
+ logits, loss = model(*[x.to(dev) for x in next(iter(wds.WebLoader(ds, batch_size=None).unbatched().batched(bs)))])
13
+ loss.backward()
14
+ for name, param in model.named_parameters():
15
+ if param.grad is None:
16
+ print('Unused parameter: '+name)
whisperspeech/train.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/B1. Training.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['SimpleVisual', 'validate', 'train']
5
+
6
+ # %% ../nbs/B1. Training.ipynb 2
7
+ import io
8
+ import time
9
+ import random
10
+ from pathlib import Path
11
+
12
+ from fastprogress import progress_bar, master_bar
13
+ import fastprogress
14
+
15
+ import numpy as np
16
+ import pylab as plt
17
+ import math
18
+
19
+ import IPython
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ from torch.utils.data.dataloader import DataLoader
24
+ from torch.profiler import record_function
25
+ from whisperspeech import utils
26
+
27
+ import webdataset as wds
28
+
29
+ torch.backends.cudnn.benchmark = True
30
+ torch.backends.cudnn.enabled = True
31
+ torch.backends.cuda.matmul.allow_tf32 = True
32
+ torch.set_float32_matmul_precision('medium')
33
+
34
+ # %% ../nbs/B1. Training.ipynb 3
35
+ class SimpleVisual:
36
+ def __init__ (self, model, masterbar, total_steps):
37
+ self.model = model
38
+ self.masterbar = masterbar
39
+ self.total_steps = total_steps
40
+ self.epochs = total_steps // masterbar.main_bar.total
41
+
42
+ gs = plt.GridSpec(2, 1, height_ratios=[3,1])
43
+ graph_fig = plt.figure(figsize=(10,6))
44
+ self.graph_fig = graph_fig
45
+ self.loss_p = graph_fig.add_subplot(gs[0])
46
+ self.lr_p = graph_fig.add_subplot(gs[1], sharex=self.loss_p)
47
+ self.lr_p.tick_params('x', labelbottom=False)
48
+ self.graph_out = None
49
+
50
+ self.its = []
51
+ self.train_losses = []
52
+ self.val_losses = []
53
+ self.lr_history = []
54
+
55
+ def show(self):
56
+ self.start_t = time.time()
57
+ self.masterbar.write(["samples", "train", "val", "time"], table=True)
58
+ self.graph_out = display(self.graph_fig, display_id=True, clear=True)
59
+
60
+ def hide(self):
61
+ if self.graph_out is not None:
62
+ self.graph_out.update(IPython.display.HTML(''))
63
+
64
+ def plot(self):
65
+ loss_p, lr_p = self.loss_p, self.lr_p
66
+ loss_p.clear()
67
+ loss_p.plot(self.its, self.train_losses)
68
+ loss_p.plot(self.its, self.val_losses)
69
+ loss_p.set_xlim(0, self.total_steps)
70
+ loss_p.set_yscale('log')
71
+ lr_p.clear()
72
+ lrs = np.array(self.lr_history)
73
+ lr_p.plot(self.its, lrs)
74
+ self.graph_out.update(self.graph_fig)
75
+
76
+ def add_data(self, it, lr, train_loss, val_los):
77
+ self.its.append(it)
78
+ self.train_losses.append(train_loss)
79
+ self.val_losses.append(val_los)
80
+ self.lr_history.append(lr)
81
+ self.plot()
82
+
83
+ def add_table_row(self, it, avg_train_loss, val_loss):
84
+ elapsed_t = time.time() - self.start_t
85
+ self.masterbar.write([it, f"{avg_train_loss:.5f}", f"{val_loss:.5f}", fastprogress.core.format_time(elapsed_t)], table=True)
86
+
87
+ def on_iter(self, bar, it, avg_train_loss, val_loss):
88
+ epoch = math.ceil(it / self.total_steps * self.epochs)
89
+ bar.comment = f"#{epoch}/{self.epochs} loss: {avg_train_loss:.3f} / {val_loss:.3f}"
90
+
91
+ # %% ../nbs/B1. Training.ipynb 4
92
+ # FIXME: we need to keep this synchronised with the validation code below...
93
+ def validate(model, val, half=True, bs=16, drop_last=False, dl_workers=8, device="cuda"):
94
+ if isinstance(val, torch.utils.data.IterableDataset):
95
+ val_loader = wds.WebLoader(val, batch_size=None, num_workers=dl_workers, drop_last=drop_last) \
96
+ .unbatched().shuffle(1024).batched(bs)
97
+ else:
98
+ val_loader = DataLoader(val, batch_size=bs, num_workers=dl_workers, pin_memory=True, drop_last=drop_last)
99
+
100
+ with torch.no_grad():
101
+ val_loss = 0
102
+ val_samples = 0
103
+ for args in val_loader:
104
+ args = [x.to(device, non_blocking=True) for x in args]
105
+ with torch.autocast(device_type=device, dtype=torch.float16 if half else torch.float32, enabled=device!='cpu'):
106
+ ps, loss = model(*args)
107
+ N = args[0].shape[0]
108
+ val_loss += loss.mean().item() * N
109
+ val_samples += N
110
+ val_loss = val_loss / val_samples
111
+
112
+ return val_loss
113
+
114
+ # %% ../nbs/B1. Training.ipynb 5
115
+ def train(checkpoint_path, model, train, val, half=True, bs=16, lr=1e-4, drop_last=False,
116
+ weight_decay=0.1, warmup_steps=10000, epochs=10, clip_gradient_norm=None,
117
+ dl_workers=8, visual_class = SimpleVisual, profiler=None,
118
+ run_valid_every_iters=8000, table_row_every_iters=80000, chkpt_every_iters=None,
119
+ device="cuda", trainable_params=None, callback=None, lr_schedule='wsd'):
120
+ if chkpt_every_iters is None:
121
+ chkpt_every_iters = table_row_every_iters
122
+
123
+ mb = master_bar(range(epochs))
124
+ if isinstance(train, torch.utils.data.IterableDataset):
125
+ total_steps = epochs * (train.total_samples // bs)
126
+ else:
127
+ total_steps = epochs * len(train) / bs
128
+
129
+ visual = visual_class(model, mb, total_steps * bs)
130
+ model.visual = visual
131
+
132
+ Path(checkpoint_path).mkdir(exist_ok=True)
133
+
134
+ if isinstance(train, torch.utils.data.IterableDataset):
135
+ # train_loader = DataLoader(train, batch_size=None, num_workers=dl_workers, pin_memory=True, drop_last=False, shuffle=False)
136
+ # val_loader = DataLoader(val, batch_size=None, num_workers=dl_workers, pin_memory=True, drop_last=False)
137
+ train_loader = wds.WebLoader(utils.join_datasets([train]), batch_size=None, shuffle=False, num_workers=dl_workers, drop_last=drop_last, persistent_workers=True) \
138
+ .unbatched().shuffle(1024).batched(bs, partial=False).with_length(total_steps)
139
+ val_loader = wds.WebLoader(val, batch_size=None, shuffle=False, num_workers=dl_workers, drop_last=drop_last, ) \
140
+ .unbatched().batched(bs).with_length(val.total_samples // bs)
141
+ else:
142
+ train_loader = DataLoader(train, batch_size=bs, num_workers=dl_workers, pin_memory=True, drop_last=drop_last, shuffle=True)
143
+ val_loader = DataLoader(val, batch_size=bs, num_workers=dl_workers, pin_memory=True, drop_last=drop_last)
144
+
145
+ val_loss = torch.nan
146
+ avg_train_loss = torch.nan
147
+
148
+ if hasattr(model, 'setup'):
149
+ model.setup(device)
150
+
151
+ try:
152
+ lr_scheduler = None
153
+
154
+ if trainable_params is None: trainable_params = model.parameters()
155
+ all_params = set(trainable_params)
156
+ customized_params = set()
157
+ groups = []
158
+ group_map = {}
159
+ for name,m in model.named_modules():
160
+ if hasattr(m, 'no_weight_decay') or hasattr(m, 'lr_scale'):
161
+ m_trainable = [x for x in m.parameters() if x in all_params]
162
+ if not m_trainable: continue
163
+ customized_params |= set(m_trainable)
164
+ m_wd = 0 if hasattr(m, 'no_weight_decay') else weight_decay
165
+ m_lr = lr * getattr(m, 'lr_scale', 1)
166
+ group = group_map.get((m_wd, m_lr), None)
167
+ if not group:
168
+ group = {"params": [], "names": [], "weight_decay": m_wd, "lr": m_lr}
169
+ groups.append(group)
170
+ group_map[(m_wd, m_lr)] = group
171
+ group['params'] += m_trainable
172
+ group['names'].append(name)
173
+
174
+ other_params = all_params - customized_params
175
+
176
+ if other_params:
177
+ groups = groups + [
178
+ {"names": ["other"], "params": list(other_params), "weight_decay": weight_decay },
179
+ ]
180
+
181
+ optimizer = torch.optim.AdamW(lr=lr, betas=(0.9, 0.95), fused=device!='cpu', params=groups)
182
+ model._optimizer = optimizer
183
+ scaler = torch.cuda.amp.GradScaler(enabled=half)
184
+
185
+ if lr_schedule == 'cosine':
186
+ lr_scheduler = torch.optim.lr_scheduler.OneCycleLR(
187
+ optimizer,
188
+ pct_start=self.model_hparams['pct_start'],
189
+ max_lr=[pg.get('lr', lr) for pg in param_groups],
190
+ steps_per_epoch=num_steps_per_epoch(),
191
+ epochs=int(self.model_hparams['epochs']),
192
+ final_div_factor=25
193
+ )
194
+ elif lr_schedule == 'linear':
195
+ warmup_scheduler = torch.optim.lr_scheduler.LinearLR(
196
+ optimizer, 1e-3, 1, warmup_steps
197
+ )
198
+ train_scheduler = torch.optim.lr_scheduler.LinearLR(
199
+ optimizer, 1, 1/25, total_steps - warmup_steps
200
+ )
201
+ lr_scheduler = torch.optim.lr_scheduler.SequentialLR(
202
+ optimizer, schedulers=[warmup_scheduler, train_scheduler], milestones=[warmup_steps]
203
+ )
204
+ elif lr_schedule == 'wsd':
205
+ warmup_scheduler = torch.optim.lr_scheduler.LinearLR(
206
+ optimizer, 1e-3, 1, warmup_steps
207
+ )
208
+ train_scheduler = torch.optim.lr_scheduler.MultiStepLR(
209
+ optimizer, [int(total_steps - warmup_steps - 0.1*total_steps)], 1/8,
210
+ )
211
+ lr_scheduler = torch.optim.lr_scheduler.SequentialLR(
212
+ optimizer, schedulers=[warmup_scheduler, train_scheduler], milestones=[warmup_steps]
213
+ )
214
+ else:
215
+ raise Exception("Unknown learning rate schedule")
216
+
217
+ it = 0
218
+ next_val_it = 0
219
+ next_chkpt_it = chkpt_every_iters
220
+ next_table_it = 0
221
+
222
+ visual.show()
223
+
224
+ running_loss = []
225
+
226
+ for epoch in mb:
227
+ bar = progress_bar(train_loader, total=train.total_samples//bs, parent=mb)
228
+ for args in bar:
229
+ with record_function("forward"):
230
+ args = [x.to(device, non_blocking=True) for x in args]
231
+
232
+ # zero the parameter gradients
233
+ optimizer.zero_grad(set_to_none=True)
234
+
235
+ with torch.autocast(device_type=device, dtype=torch.float16 if half else torch.float32, enabled=device!='cpu'):
236
+ ps, loss = model(*args)
237
+ loss = loss.mean()
238
+
239
+ with record_function("backward"):
240
+ scaler.scale(loss).backward()
241
+
242
+ with record_function("running_loss"):
243
+ running_loss.append(loss.item())
244
+ running_loss = running_loss[-5:]
245
+ avg_train_loss = sum(running_loss)/len(running_loss)
246
+
247
+ if it >= next_val_it:
248
+ next_val_it += run_valid_every_iters
249
+ with record_function("validation"):
250
+ with record_function("model.eval"):
251
+ model.eval()
252
+ with torch.no_grad():
253
+ val_loss = 0
254
+ val_samples = 0
255
+ for args in val_loader:
256
+ args = [x.to(device, non_blocking=True) for x in args]
257
+ with torch.autocast(device_type=device, dtype=torch.float16 if half else torch.float32, enabled=device!='cpu'):
258
+ ps, loss = model(*args)
259
+ N = args[0].shape[0]
260
+ val_loss += loss.mean().item() * N
261
+ val_samples += N
262
+ val_loss = val_loss / val_samples
263
+ with record_function("model.train"):
264
+ model.train()
265
+ with record_function("plotting"):
266
+ visual.add_data(it, lr_scheduler.get_last_lr(), avg_train_loss, val_loss)
267
+
268
+ if it >= next_table_it:
269
+ visual.add_table_row(it, avg_train_loss, val_loss)
270
+ next_table_it += table_row_every_iters
271
+
272
+ with record_function("optimizer"):
273
+ if clip_gradient_norm:
274
+ scaler.unscale_(optimizer)
275
+ # Since the gradients of optimizer's assigned params are unscaled, clips as usual:
276
+ torch.nn.utils.clip_grad_norm_(model.parameters(), clip_gradient_norm)
277
+
278
+ scaler.step(optimizer)
279
+ scaler.update()
280
+
281
+ lr_scheduler.step()
282
+
283
+ if profiler is not None: profiler.step()
284
+
285
+ if it >= next_chkpt_it:
286
+ with record_function("checkpoint"):
287
+ next_chkpt_it += chkpt_every_iters
288
+ torch.save(model.state_dict(), f'{checkpoint_path}/{it:08d}.pt')
289
+
290
+ it += bs
291
+ visual.on_iter(bar, it, avg_train_loss, val_loss)
292
+ if callback is not None: callback(it)
293
+ except KeyboardInterrupt:
294
+ mb.write(f"interrupted")
295
+ mb.show()
296
+ pass
297
+ finally:
298
+ visual.add_table_row(it, avg_train_loss, val_loss)
299
+ mb.show()
300
+ visual.hide()
whisperspeech/train_multi.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/B2. Training (Lightning).ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/B2. Training (Lightning).ipynb 2
7
+ import io
8
+ import os
9
+ import time
10
+ import random
11
+ import re
12
+ from pathlib import Path
13
+ import requests
14
+
15
+ from fastprogress import progress_bar, master_bar
16
+ import fastprogress
17
+ import wandb
18
+
19
+ import numpy as np
20
+ import pylab as plt
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ from torch.utils.data.dataloader import DataLoader
25
+ from torch.profiler import record_function
26
+ from whisperspeech import utils, testing
27
+
28
+ # %% ../nbs/B2. Training (Lightning).ipynb 3
29
+ import lightning.pytorch as pl
30
+ import math
31
+
32
+ class TrainingTask(pl.LightningModule):
33
+ def __init__(self, model, model_hparams=None):
34
+ super().__init__()
35
+ self.model = model
36
+ self.model_hparams = model_hparams
37
+
38
+ def on_fit_start(self):
39
+ if getattr(self.model, 'setup'):
40
+ self.model.setup(self.device)
41
+ if self.model_hparams['torch_compile'] and getattr(self.model, 'optimize_training'):
42
+ import torch._dynamo
43
+ torch._dynamo.config.optimize_ddp = False
44
+ # FIXME: define a batch of dummy tensors in the model
45
+ testing.test_model(model, train_dss[0], bs=batch_size)
46
+ model.optimize_training()
47
+
48
+ def configure_optimizers(self):
49
+ """ Initialize AdamW optimizer"""
50
+ lr = self.model_hparams['lr0']
51
+ weight_decay = self.model_hparams['weight_decay']
52
+
53
+ all_params = set(model.parameters())
54
+ customized_params = set()
55
+ groups = []
56
+ group_map = {}
57
+ for name,m in model.named_modules():
58
+ if hasattr(m, 'no_weight_decay') or hasattr(m, 'lr_scale'):
59
+ customized_params |= set(m.parameters())
60
+ m_wd = 0 if hasattr(m, 'no_weight_decay') else weight_decay
61
+ m_lr = lr * getattr(m, 'lr_scale', 1)
62
+ group = group_map.get((m_wd, m_lr), None)
63
+ if not group:
64
+ group = {"params": [], "names": [], "weight_decay": m_wd, "lr": m_lr}
65
+ groups.append(group)
66
+ group_map[(m_wd, m_lr)] = group
67
+ group['params'] += m.parameters()
68
+ group['names'].append(name)
69
+
70
+ other_params = all_params - customized_params
71
+
72
+ param_groups = groups + [
73
+ {"names": ["other"], "params": list(other_params), "weight_decay": weight_decay },
74
+ ]
75
+
76
+ optimizer = torch.optim.AdamW(lr=lr, betas=(0.9, 0.95), params=param_groups)
77
+
78
+ # modified from https://github.com/Lightning-AI/lightning/issues/5449#issuecomment-1501597319
79
+ def num_steps_per_epoch() -> int:
80
+ """Get number of steps"""
81
+ # Accessing _data_source is flaky and might break
82
+ dataset = self.trainer.fit_loop._data_source.dataloader()
83
+ dataset_size = len(dataset)
84
+ # math.ceil so always overestimate (underestimating throws exceptions)
85
+ num_steps = math.ceil(dataset_size / self.trainer.accumulate_grad_batches)
86
+ return num_steps
87
+
88
+ warmup_steps = self.model_hparams['warmup_steps']
89
+ total_steps = self.model_hparams['iterations']
90
+ self.model_hparams['pct_start'] = min(0.3, warmup_steps / total_steps)
91
+
92
+ print(f"{self.model_hparams['iterations']=} steps")
93
+
94
+ if self.model_hparams['lr_schedule'] == 'cosine':
95
+ lr_scheduler = torch.optim.lr_scheduler.OneCycleLR(
96
+ optimizer,
97
+ pct_start=self.model_hparams['pct_start'],
98
+ max_lr=[pg.get('lr', lr) for pg in param_groups],
99
+ steps_per_epoch=num_steps_per_epoch(),
100
+ epochs=1,
101
+ final_div_factor=25
102
+ )
103
+ elif self.model_hparams['lr_schedule'] == 'linear':
104
+ warmup_scheduler = torch.optim.lr_scheduler.LinearLR(
105
+ optimizer, 1e-3, 1, warmup_steps
106
+ )
107
+ train_scheduler = torch.optim.lr_scheduler.LinearLR(
108
+ optimizer, 1, 1/25, total_steps - warmup_steps
109
+ )
110
+ lr_scheduler = torch.optim.lr_scheduler.SequentialLR(
111
+ optimizer, schedulers=[warmup_scheduler, train_scheduler], milestones=[warmup_steps]
112
+ )
113
+ elif self.model_hparams['lr_schedule'] == 'wsd':
114
+ warmup_scheduler = torch.optim.lr_scheduler.LinearLR(
115
+ optimizer, 1e-3, 1, warmup_steps
116
+ )
117
+ train_scheduler = torch.optim.lr_scheduler.MultiStepLR(
118
+ optimizer, [int(total_steps - warmup_steps - 0.1*total_steps)], 1/8,
119
+ )
120
+ lr_scheduler = torch.optim.lr_scheduler.SequentialLR(
121
+ optimizer, schedulers=[warmup_scheduler, train_scheduler], milestones=[warmup_steps]
122
+ )
123
+ else:
124
+ raise Exception("Unknown learning rate schedule")
125
+
126
+ return [optimizer], [{'scheduler': lr_scheduler, 'interval': 'step'}]
127
+
128
+ def training_step(self, train_batch, batch_idx):
129
+ train_out = self.model.forward(*train_batch)
130
+ train_loss = train_out[-1]
131
+
132
+ self.log("train_loss", train_loss, sync_dist=True)
133
+ return train_loss
134
+
135
+ def validation_step(self, val_batch, batch_idx, dataloader_idx=0):
136
+ val_out = self.model.forward(*val_batch)
137
+ val_loss = val_out[-1]
138
+
139
+ name = val_dss_names[dataloader_idx]
140
+ self.log(f"val_loss/{name}", val_loss.detach(), sync_dist=True, add_dataloader_idx=False)
141
+ if hasattr(self.model, 'get_metrics'):
142
+ self.log_dict({f'metrics/{k}_{name}':v for k,v in self.model.get_metrics().items()}, sync_dist=True, add_dataloader_idx=False)
143
+ return val_loss.detach()
144
+
145
+ def on_validation_epoch_end(self):
146
+ for name, weight in zip(train_dss_names, train_weights):
147
+ self.log(f"trainer/{name}-batches", weight.to(self.device) * self.global_step, sync_dist=True)
148
+
149
+ def test_step(self, val_batch, batch_idx):
150
+ test_out = self.model.forward(*val_batch)
151
+ test_loss = test_out[-1]
152
+
153
+ self.log("test_loss", test_loss, sync_dist=True)
154
+ return test_loss
155
+
156
+ # %% ../nbs/B2. Training (Lightning).ipynb 4
157
+ from fastcore.script import anno_parser
158
+ import shlex
159
+
160
+ # watch out: we can only pass Python values as keyword arguments (not positional)
161
+ # everything else has to be a string
162
+ def parse_and_call(name, fun, args, kwargs={}, log_to_wandb=True):
163
+ print(f"Parsing arguments for {name}, {args}")
164
+ p = anno_parser(fun, prog=name)
165
+ args = p.parse_args(args).__dict__
166
+ args.pop('xtra'); args.pop('pdb')
167
+ args.update({k:v for k, v in kwargs.items()})
168
+ if log_to_wandb and type(wandb_logger.experiment.config) == wandb.sdk.wandb_config.Config:
169
+ wandb_logger.experiment.config[name] = {k:v for k,v in args.items() if k not in ['dataset', 'tunables']}
170
+ return fun(**args)
171
+
172
+ # %% ../nbs/B2. Training (Lightning).ipynb 8
173
+ # split only full path components to stabilize the names
174
+ def simplify_folder_names(lst):
175
+ lst = [x.strip('/') for x in lst] # normalize pathnames, removing trailing and leading slashes
176
+ parts = [x.split('/') for x in lst]
177
+ prefix = os.path.commonprefix(parts)
178
+ suffix = os.path.commonprefix([x[::-1] for x in parts])
179
+ print(prefix, suffix)
180
+ return ['_'.join(x[len(prefix):len(x)-len(suffix)]) for x in parts]
181
+
182
+ # %% ../nbs/B2. Training (Lightning).ipynb 11
183
+ import argparse
184
+
185
+ parser = argparse.ArgumentParser()
186
+ parser.add_argument('--task', type=str, help='Task to train')
187
+ parser.add_argument('--seed', type=int, default=0, help='Global training seed')
188
+ parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
189
+ parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
190
+ parser.add_argument('--input-dir', type=str, default='', help='input data path') # fixed in the model for now
191
+ parser.add_argument('--dataset-config', type=str, default='', help='common dataset options')
192
+ parser.add_argument('--training-data', action='append', type=str, default=[], help='training dataset')
193
+ parser.add_argument('--validation-data', action='append', type=str, default=[], help='validation dataset (can be passed multiple times)')
194
+ parser.add_argument('--monitored-metric', type=str, default="val_loss", help='metric to monitor for checkpointing')
195
+ parser.add_argument("--checkpoint-dir", type=str, default="./checkpoints/", help="directory to save the checkpoints")
196
+ parser.add_argument('--iterations', type=int, default=8000, help='total training iterations')
197
+ parser.add_argument('--validate-every-n-steps', type=int, default=500, help='how training steps to run between validations')
198
+ parser.add_argument('--weight-decay', type=float, default=1e-2, help='optimizer weight decay')
199
+ parser.add_argument('--lr0', type=float, default=1e-4, help='optimizer initial learning rate')
200
+ parser.add_argument('--lr-schedule', type=str, default="cosine", help='the learning rate schedule [cosine, linear or wsd]')
201
+ parser.add_argument('--clip-gradient-norm', type=float, default=None, help='enable gradient norm clipping')
202
+ parser.add_argument('--accumulate-grad-batches', type=int, default=1, help='perform the optimizer step only after going through several batches of samples')
203
+ parser.add_argument('--precision', type=str, default="16-mixed", help="floating point precision")
204
+ parser.add_argument('--torch-compile', type=bool, default=False, help='compile (parts of) the model with torch.compile')
205
+ parser.add_argument('--warmup-steps', type=int, default=10000, help='total number steps during which the learning rate rises (defaults to 10k updates)')
206
+ parser.add_argument('--tunables', type=str, default="", help='tunable hyperparameters')
207
+ parser.add_argument('--resume-from', type=Path, default=None, help='resume training from the given checkpoint')
208
+ parser.add_argument('--load-from', type=Path, default=None, help='initialize the weights from the given model')
209
+ parser.add_argument('--strategy', type=str, default='ddp', help='distributed training strategy')
210
+ parser.add_argument('--wandb-suffix', type=str, default=None, help='W&B project name suffix')
211
+ parser.add_argument('--wandb-task-name', type=str, default=None, help='Task name for the W&B project name')
212
+
213
+ args = parser.parse_args().__dict__
214
+
215
+ task_args: list = shlex.split(args.pop("task"))
216
+ task_name, task_args = task_args[0], task_args[1:]
217
+ input_args: list = shlex.split(args.pop("input_dir"))
218
+ dataset_config: list = shlex.split(args.pop("dataset_config"))
219
+ monitored_metric: str = args.pop("monitored_metric")
220
+ checkpoint_dir: str = args.pop("checkpoint_dir")
221
+ num_workers: int = args.pop("workers")
222
+ batch_size: int = args.pop("batch_size")
223
+ iterations: int = args.pop("iterations")
224
+ tunables_args: list = shlex.split(args.pop("tunables"))
225
+
226
+ hyp_params = {}
227
+ hyp_params['batch_size'] = batch_size
228
+ hyp_params['warmup_steps'] = args['warmup_steps']
229
+ hyp_params['weight_decay'] = args['weight_decay']
230
+ hyp_params['clip_gradient_norm'] = args['clip_gradient_norm']
231
+ hyp_params['accumulate_grad_batches'] = args['accumulate_grad_batches']
232
+ hyp_params['validate_every_n_steps'] = args["validate_every_n_steps"]
233
+ hyp_params['precision'] = args['precision']
234
+ hyp_params['torch_compile'] = args['torch_compile']
235
+ hyp_params['lr0'] = args['lr0']
236
+ hyp_params['lr_schedule'] = args['lr_schedule']
237
+ hyp_params['iterations'] = iterations
238
+ hyp_params['strategy'] = args['strategy']
239
+ if 'SLURM_NTASKS' in os.environ:
240
+ hyp_params['world_size'] = os.environ['SLURM_NTASKS']
241
+ else:
242
+ hyp_params['world_size'] = 1
243
+
244
+ # %% ../nbs/B2. Training (Lightning).ipynb 12
245
+ def parse_dataset_string(s):
246
+ cwd = [None]
247
+ def load_file_reference(matchobj):
248
+ fname = matchobj.group(1)
249
+ cwd[0] = Path(fname).parent
250
+ if fname.startswith('http://') or fname.startswith('https://'):
251
+ response = requests.get(target_url)
252
+ return response.text.strip()
253
+ else:
254
+ with open(fname, 'r') as f:
255
+ return f.read().strip()
256
+ s = re.sub('@([^ ]+)', load_file_reference, s)
257
+ arg_list = shlex.split(s)
258
+ if cwd[0]: arg_list += ['--cwd', str(cwd[0])]
259
+ return arg_list
260
+
261
+ # %% ../nbs/B2. Training (Lightning).ipynb 13
262
+ from lightning.pytorch.loggers import WandbLogger
263
+ from lightning.pytorch.callbacks import LearningRateMonitor
264
+ from lightning.fabric.utilities.rank_zero import rank_zero_only
265
+ import datetime
266
+ import webdataset as wds
267
+ import importlib
268
+ import dataclasses
269
+
270
+ torch.set_float32_matmul_precision('medium')
271
+
272
+ project = f"WhisperSpeech-{args['wandb_task_name'] or task_name}"
273
+ if args['wandb_suffix']:
274
+ project += "-"+args['wandb_suffix']
275
+
276
+ from faker import Faker
277
+ fake = Faker()
278
+ name = (fake.name().split()[0] + "_" + fake.color_name()).lower()
279
+
280
+ if rank_zero_only.rank == 0:
281
+ print('Experiment name:', name)
282
+ wandb_logger = WandbLogger(project=project, name=name)
283
+
284
+ ckpt_callback = pl.callbacks.ModelCheckpoint(
285
+ dirpath=f'{task_name}',
286
+ filename=f'{task_name}-{name}'+"-{step}-acc={"+monitored_metric+":.2f}",
287
+ monitor=monitored_metric,
288
+ save_top_k=16,
289
+ train_time_interval=datetime.timedelta(minutes=14),
290
+ auto_insert_metric_name=False
291
+ )
292
+
293
+ lr_monitor_callback = LearningRateMonitor(logging_interval='step')
294
+
295
+ task = importlib.import_module("whisperspeech."+task_name)
296
+
297
+ # load all training sets
298
+ train_dss = [parse_and_call(f'train_ds_{i}', task.load_dataset,
299
+ parse_dataset_string(train_ds_config) + dataset_config)
300
+ for i,train_ds_config in enumerate(args['training_data'])]
301
+ train_dss_names = simplify_folder_names([parse_dataset_string(train_ds_config)[0] for train_ds_config in args['training_data']])
302
+ print('train names:', train_dss_names)
303
+ counts = [x.total_samples for x in train_dss]
304
+ print(counts)
305
+ print(torch.tensor(counts).log2())
306
+ train_weights = torch.tensor(counts).log2() - torch.tensor(counts).log2().min() + 1
307
+ for tds, w in zip(train_dss, train_weights):
308
+ tds.weight = w
309
+
310
+ train_total_batches = hyp_params['iterations']
311
+ if train_total_batches < hyp_params['validate_every_n_steps']:
312
+ # validate once at the end of every epoch for very short experiments
313
+ hyp_params['validate_every_n_steps'] = train_total_batches
314
+
315
+ # persistent_workers=True is critical here so we don't reset the sample shuffling buffers
316
+ # with webdatasets sample shuffling is very bad initially, unless num_workers << num_shards
317
+ train_loader = wds.WebLoader(
318
+ utils.join_datasets(train_dss),
319
+ num_workers=num_workers, drop_last=False, batch_size=None, shuffle=False, persistent_workers=num_workers > 0,
320
+ ).unbatched().shuffle(1024).batched(batch_size).with_length(train_total_batches)
321
+
322
+ # load all validation sets
323
+ val_dss_names = [parse_dataset_string(val_ds_config)[0] for val_ds_config in args['validation_data']]
324
+ val_dss_names = simplify_folder_names(val_dss_names)
325
+ print('validation names:', val_dss_names)
326
+
327
+ val_dss = [parse_and_call(f'val_ds_{i}', task.load_dataset,
328
+ parse_dataset_string(val_ds_config) + dataset_config, {'validation': True})
329
+ for i,val_ds_config in enumerate(args['validation_data'])]
330
+ val_loaders = [wds.WebLoader(
331
+ val_ds, num_workers=num_workers, drop_last=False, batch_size=None, shuffle=False,
332
+ ).unbatched().batched(batch_size).with_length(val_ds.total_samples // batch_size)
333
+ for val_ds in val_dss]
334
+
335
+ tunables = None
336
+ if hasattr(task, "Tunables"):
337
+ tunables = parse_and_call('tunables', task.Tunables, tunables_args, log_to_wandb=False)
338
+ # override command line args from the tunables object
339
+ for k in ["lr0", "clip_gradient_norm", "weight_decay", "warmup_steps"]:
340
+ val = getattr(tunables, k, None)
341
+ if val is not None: hyp_params[k] = val
342
+
343
+ if type(wandb_logger.experiment.config) == wandb.sdk.wandb_config.Config:
344
+ wandb_logger.experiment.config['tunables'] = dataclasses.asdict(tunables)
345
+
346
+ trainer = pl.Trainer(strategy=hyp_params['strategy'],
347
+ max_steps=hyp_params['iterations'],
348
+ accelerator="gpu",
349
+ profiler="simple",
350
+ precision=hyp_params['precision'],
351
+ gradient_clip_val=hyp_params['clip_gradient_norm'],
352
+ accumulate_grad_batches=hyp_params['accumulate_grad_batches'],
353
+ val_check_interval=hyp_params['validate_every_n_steps'],
354
+ check_val_every_n_epoch=None,
355
+ enable_checkpointing=True,
356
+ logger=wandb_logger,
357
+ num_nodes=int(os.environ.get('SLURM_NNODES', 1)),
358
+ devices=int(os.environ.get('SLURM_NTASKS_PER_NODE', 1)),
359
+ callbacks=[ckpt_callback, lr_monitor_callback])
360
+
361
+ # we initialize everything manually anyways
362
+ with trainer.init_module(empty_init=True):
363
+ if args['load_from']:
364
+ model = task.load_model(str(args['load_from']))
365
+ else:
366
+ model_kwargs = dict(dataset=train_dss[0])
367
+ if tunables is not None: model_kwargs['tunables'] = tunables
368
+ model = parse_and_call('model', task.make_model, task_args, model_kwargs)
369
+
370
+ if type(wandb_logger.experiment.config) == wandb.sdk.wandb_config.Config:
371
+ wandb_logger.experiment.config.update(hyp_params)
372
+
373
+ kwargs = {}
374
+ if 'resume_from' in args:
375
+ kwargs['ckpt_path'] = args['resume_from']
376
+ trainer.fit(model=TrainingTask(model, model_hparams=hyp_params),
377
+ train_dataloaders=train_loader,
378
+ val_dataloaders=val_loaders,
379
+ **kwargs)
380
+
381
+ if rank_zero_only.rank == 0:
382
+ Path(task_name).mkdir(exist_ok=True, parents=True)
383
+ fname = f'{task_name}/{name}.model'
384
+ print('Saving:', fname)
385
+ model.save_model(fname)
whisperspeech/utils.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/D. Common dataset utilities.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['shard_glob', 'join_datasets', 'resampler', 'derived_name', 'derived_dataset', 'merge_in', 'AtomicTarWriter',
5
+ 'readlines']
6
+
7
+ # %% ../nbs/D. Common dataset utilities.ipynb 1
8
+ import os
9
+ import torch
10
+ import torchaudio
11
+ import torch.nn.functional as F
12
+ from pathlib import Path
13
+ from contextlib import contextmanager
14
+ import webdataset as wds
15
+
16
+ # %% ../nbs/D. Common dataset utilities.ipynb 2
17
+ import huggingface_hub
18
+
19
+ # adds a environment variable that disables downloads thought the Huggingface Hub API:
20
+
21
+ def wrap_downloader(old):
22
+ def new(*args, **kwargs):
23
+ if os.environ.get('HUGGINGFACE_LOCAL_ONLY', False):
24
+ print(f"Enforcing local_files_only for {old.__qualname__}")
25
+ kwargs['local_files_only'] = True
26
+ return old(*args, **kwargs)
27
+ return new
28
+
29
+ huggingface_hub.snapshot_download = wrap_downloader(huggingface_hub.snapshot_download)
30
+ huggingface_hub.hf_hub_download = wrap_downloader(huggingface_hub.hf_hub_download)
31
+
32
+ # %% ../nbs/D. Common dataset utilities.ipynb 3
33
+ def shard_glob(input):
34
+ if isinstance(input, Path):
35
+ input = str(input)
36
+ if '{' in input:
37
+ return wds.shardlists.expand_urls(input)
38
+ if str:
39
+ path = Path(input)
40
+ if path.is_dir():
41
+ glob = '*.tar.gz'
42
+ else:
43
+ glob = path.name
44
+ path = path.parent
45
+ input = Path(path).glob(glob)
46
+ else:
47
+ raise ArgumentError("input should be either a list or a path with an optional glob specifier")
48
+ return [str(x) for x in input]
49
+
50
+ # %% ../nbs/D. Common dataset utilities.ipynb 7
51
+ class join_datasets(torch.utils.data.IterableDataset):
52
+ def __init__(self, datasets):
53
+ self.datasets = datasets
54
+ self.iters = [iter(ds) for ds in self.datasets]
55
+
56
+ def __iter__(self):
57
+ probs = torch.tensor([getattr(ds, 'weight', 1) for ds in self.datasets], dtype=torch.float)
58
+ while True:
59
+ try:
60
+ yield next(self.iters[torch.multinomial(probs, 1)])
61
+ except StopIteration:
62
+ return
63
+
64
+ def __len__(self):
65
+ return sum([ds.total_samples for ds in self.datasets])
66
+
67
+ # %% ../nbs/D. Common dataset utilities.ipynb 10
68
+ def resampler(newsr = 24000, key = 'samples_24k'):
69
+ _last_sr = None
70
+ tform = None
71
+
72
+ def _resample(samples):
73
+ for s in samples:
74
+ sr = s['sample_rate']
75
+ if sr != newsr:
76
+ if sr != _last_sr: tform = torchaudio.transforms.Resample(sr, newsr)
77
+ s[key] = tform(s['samples'])
78
+ else:
79
+ s[key] = s['samples']
80
+ yield s
81
+
82
+ return _resample
83
+
84
+ # %% ../nbs/D. Common dataset utilities.ipynb 11
85
+ def derived_name(url, kind, suffix=None):
86
+ if suffix is None: suffix = '' if url.endswith('.gz') else ".gz"
87
+ url = Path(url)
88
+ return str(url.parent.parent/kind/url.name) + suffix
89
+
90
+ # %% ../nbs/D. Common dataset utilities.ipynb 12
91
+ def derived_dataset(kind, suffix=None, decoders=[]):
92
+ def deriver(url):
93
+ return wds.WebDataset(
94
+ wds.SimpleShardList([derived_name(url, kind, suffix)])
95
+ ).decode(*decoders)
96
+ return deriver
97
+
98
+ # %% ../nbs/D. Common dataset utilities.ipynb 13
99
+ def merge_in(dataset_fun):
100
+ """Merge a dataset into the current one returning samples with the union of keys. Pass in a function
101
+ that takes a URL of a sample and returns a dataset for it (called everytime the URL changes).
102
+
103
+ It requires (and validates) that both datasets have the same ordering of keys so you have
104
+ to use it before any sample shuffling. Shard shuffling is ok.
105
+ """
106
+ def merge_loop(main_samples):
107
+ #print("new merge loop:", dataset_fun)
108
+ merged_samples = None
109
+ cur_url = None
110
+ i = None
111
+ for s in main_samples:
112
+ url = s['__url__']
113
+ if url != cur_url:
114
+ # this will open a new file when we get the first sample with a new __url__
115
+ merged_samples = iter(dataset_fun(url))
116
+ cur_url = url
117
+ news = {}
118
+ news.update(s)
119
+ if '__skip_merge__' not in s:
120
+ try:
121
+ merge_s = next(merged_samples)
122
+ except StopIteration:
123
+ # if the original shard got repeated we won't observe a __url__ change
124
+ # in this case restart the dataset from the beginning
125
+ merged_samples = iter(dataset_fun(url))
126
+ merge_s = next(merged_samples)
127
+ assert merge_s['__key__'] == s['__key__'], f"sample keys don't match: {merge_s['__key__']}, {s['__key__']} in file {s['__url__']}"
128
+ news.update(merge_s)
129
+ yield news
130
+ return merge_loop
131
+
132
+ # %% ../nbs/D. Common dataset utilities.ipynb 14
133
+ def split_to_chunks(stream, ikey='vad.npy', copy_keys=[], split_keys=[], pad_to_seconds=30, random_shift=False):
134
+ for s in stream:
135
+ audio, sr = s['audio']
136
+ chunks = s[ikey]
137
+ imax = len(chunks) - 1
138
+ for i,(ts,te) in enumerate(chunks):
139
+ if 'mask.npy' in s and not s['mask.npy'][i]:
140
+ # used for fishing out samples in validation sets, see also "3D. Split out validation"
141
+ continue
142
+ samples = audio[0,int(ts*sr):int(te*sr)]
143
+ if pad_to_seconds is not None:
144
+ padding = pad_to_seconds*sr-samples.shape[-1]
145
+ lpad = random.randint(0, padding) if random_shift else 0
146
+ samples = F.pad(samples, (lpad, padding-lpad))
147
+ subs = {"__key__": s['__key__'] + f"_{i:03d}",
148
+ "src_key": s['__key__'],
149
+ "__url__": s['__url__'],
150
+ "i": i, "imax": imax,
151
+ "tstart": ts, "tend": te, "total_seconds": audio.shape[-1]/sr,
152
+ "lpad": lpad, "rpad": padding-lpad,
153
+ "lpad_s": lpad/sr, "rpad_s": (padding-lpad)/sr,
154
+ "samples": samples, "sample_rate": sr,
155
+ "src_sample": s}
156
+ for k in copy_keys:
157
+ subs[k] = s[k]
158
+ for k in split_keys:
159
+ subs[k] = s[k][i]
160
+ yield subs
161
+
162
+ # %% ../nbs/D. Common dataset utilities.ipynb 15
163
+ import re
164
+ import tempfile
165
+
166
+ # %% ../nbs/D. Common dataset utilities.ipynb 16
167
+ # a patch to ignore invalid utf-8 metadata
168
+ import torio.io._streaming_media_decoder
169
+ def new_parse_si(i):
170
+ media_type = i.media_type
171
+ try:
172
+ metadata = i.metadata
173
+ except UnicodeDecodeError:
174
+ metadata = {}
175
+ if media_type == "audio":
176
+ return torio.io._streaming_media_decoder.SourceAudioStream(
177
+ media_type=i.media_type,
178
+ codec=i.codec_name,
179
+ codec_long_name=i.codec_long_name,
180
+ format=i.format,
181
+ bit_rate=i.bit_rate,
182
+ num_frames=i.num_frames,
183
+ bits_per_sample=i.bits_per_sample,
184
+ metadata=metadata,
185
+ sample_rate=i.sample_rate,
186
+ num_channels=i.num_channels,
187
+ )
188
+ if media_type == "video":
189
+ return torio.io._streaming_media_decoder.SourceVideoStream(
190
+ media_type=i.media_type,
191
+ codec=i.codec_name,
192
+ codec_long_name=i.codec_long_name,
193
+ format=i.format,
194
+ bit_rate=i.bit_rate,
195
+ num_frames=i.num_frames,
196
+ bits_per_sample=i.bits_per_sample,
197
+ metadata=metadata,
198
+ width=i.width,
199
+ height=i.height,
200
+ frame_rate=i.frame_rate,
201
+ )
202
+ return torio.io._streaming_media_decoder.SourceStream(
203
+ media_type=i.media_type,
204
+ codec=i.codec_name,
205
+ codec_long_name=i.codec_long_name,
206
+ format=None,
207
+ bit_rate=None,
208
+ num_frames=None,
209
+ bits_per_sample=None,
210
+ metadata=metadata,
211
+ )
212
+ torio.io._streaming_media_decoder._parse_si = new_parse_si
213
+
214
+ def torch_audio_opus(key, data):
215
+ """Decode audio using the torchaudio library.
216
+
217
+ :param key: file name extension
218
+ :param data: data to be decoded
219
+ """
220
+ extension = re.sub(r".*[.]", "", key)
221
+ if extension not in ["flac", "mp3", "sox", "wav", "m4a", "ogg", "wma", "opus"]:
222
+ return None
223
+
224
+ import torchaudio
225
+
226
+ with tempfile.TemporaryDirectory() as dirname:
227
+ fname = os.path.join(dirname, f"file.{extension}")
228
+ with open(fname, "wb") as stream:
229
+ stream.write(data)
230
+ return torchaudio.load(fname, backend='soundfile' if extension == "mp3" else None)
231
+
232
+ # %% ../nbs/D. Common dataset utilities.ipynb 17
233
+ def find_audio(stream, okey='audio', ikeys='flac;mp3;sox;wav;m4a;ogg;wma;opus'):
234
+ ikeys = ikeys.split(';')
235
+ for s in stream:
236
+ for ikey in ikeys:
237
+ if ikey in s:
238
+ s[okey] = s[ikey]
239
+ yield s
240
+ break
241
+ # implicitly skips elements without any audio
242
+
243
+ # %% ../nbs/D. Common dataset utilities.ipynb 18
244
+ def vad_dataset(shards, ikey='vad.npy', kind='vad'):
245
+ return wds.WebDataset(shards).compose(
246
+ wds.decode(torch_audio_opus),
247
+ find_audio,
248
+ merge_in(derived_dataset(kind)),
249
+ lambda x: split_to_chunks(x, ikey=ikey),
250
+ )
251
+
252
+ # %% ../nbs/D. Common dataset utilities.ipynb 19
253
+ @contextmanager
254
+ def AtomicTarWriter(name, throwaway=False):
255
+ Path(name).parent.mkdir(exist_ok=True, parents=True)
256
+ tmp = name+".tmp"
257
+ with wds.TarWriter(tmp, compress=name.endswith('gz')) as sink:
258
+ yield sink
259
+ if not throwaway:
260
+ os.rename(tmp, name)
261
+
262
+ # %% ../nbs/D. Common dataset utilities.ipynb 20
263
+ def readlines(fname):
264
+ with open(fname) as file:
265
+ return [line.rstrip() for line in file]
whisperspeech/vad.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/1B. Voice activity detection.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/1B. Voice activity detection.ipynb 3
7
+ import os
8
+ import random
9
+ import torch
10
+ import torchaudio
11
+
12
+ from pathlib import Path
13
+ from fastprogress import progress_bar
14
+ from fastcore.script import call_parse
15
+
16
+ import numpy as np
17
+ import webdataset as wds
18
+
19
+ import whisperx
20
+
21
+ from whisperspeech.inference import get_compute_device
22
+ from whisperspeech import utils
23
+
24
+ # %% ../nbs/1B. Voice activity detection.ipynb 6
25
+ def extract_segments(vad_result, max_duration):
26
+ binarize = whisperx.vad.Binarize(max_duration=max_duration)
27
+ segments = binarize(vad_result)
28
+ return [(x.start, x.end) for x in segments.get_timeline()]
29
+
30
+ def segment_audio(vad_model, audio, sr=16000):
31
+ vad_result = vad_model({"waveform": audio, "sample_rate": sr})
32
+ return extract_segments(vad_result, 30)
33
+
34
+ # %% ../nbs/1B. Voice activity detection.ipynb 8
35
+ # from https://huggingface.co/spaces/facebook/MusicGen/blob/9cae843238aad3f5c7695a40c9ee77c42dd87aaf/audiocraft/data/audio_utils.py
36
+ def normalize_loudness(wav: torch.Tensor, sample_rate: int, loudness_headroom_db: float = 14,
37
+ loudness_compressor: bool = False, energy_floor: float = 2e-3):
38
+ """Normalize an input signal to a user loudness in dB LKFS.
39
+ Audio loudness is defined according to the ITU-R BS.1770-4 recommendation.
40
+ Args:
41
+ wav (torch.Tensor): Input multichannel audio data.
42
+ sample_rate (int): Sample rate.
43
+ loudness_headroom_db (float): Target loudness of the output in dB LUFS.
44
+ loudness_compressor (bool): Uses tanh for soft clipping.
45
+ energy_floor (float): anything below that RMS level will not be rescaled.
46
+ Returns:
47
+ torch.Tensor: Loudness normalized output data.
48
+ """
49
+ energy = wav.pow(2).mean().sqrt().item()
50
+ if energy < energy_floor:
51
+ return wav, 0
52
+ transform = torchaudio.transforms.Loudness(sample_rate)
53
+ input_loudness_db = transform(wav).item()
54
+ # calculate the gain needed to scale to the desired loudness level
55
+ delta_loudness = -loudness_headroom_db - input_loudness_db
56
+ gain = 10.0 ** (delta_loudness / 20.0)
57
+ output = gain * wav
58
+ if loudness_compressor:
59
+ output = torch.tanh(output)
60
+ assert output.isfinite().all(), (input_loudness_db, wav.pow(2).mean().sqrt())
61
+ return output, gain
62
+
63
+ # %% ../nbs/1B. Voice activity detection.ipynb 9
64
+ @call_parse
65
+ def process_shard(
66
+ input:str, # input shard URL/path
67
+ output:str, # output shard URL/path
68
+ key:str='audio', # string to replace with 'vad' in the shard name
69
+ model:str='whisperx' # VAD model to use (possible values: `whisperx` or `pyannote`)
70
+ ):
71
+ ds = wds.WebDataset(url).compose(
72
+ wds.decode(utils.torch_audio_opus),
73
+ utils.find_audio,
74
+ )
75
+ dl = torch.utils.data.DataLoader(ds, num_workers=1, batch_size=None)
76
+
77
+ if model == 'whisperx':
78
+ vad_model = whisperx.vad.load_vad_model(get_compute_device())
79
+ elif model == 'pyannote':
80
+ from pyannote.audio import Pipeline
81
+ pyannote_vad = Pipeline.from_pretrained("pyannote/voice-activity-detection")
82
+
83
+ def calc_power(audio, sr, ts, te):
84
+ snd = audio[:,int(ts*sr):int(te*sr)]
85
+ return (snd*snd).mean().log()
86
+
87
+ with utils.AtomicTarWriter(output) as sink:
88
+ for s in progress_bar(dl, total='noinfer'):
89
+ audio, sr = s['audio']
90
+ ash = audio.shape
91
+ shift = audio.mean()
92
+ # only normalize the first 2 hours (it fails with OOM for 7.5 hour file)
93
+ audio, gain = normalize_loudness(audio[:,:sr*3600*2] - shift, sr)
94
+ if model == 'whisperx':
95
+ segments = segment_audio(vad_model, audio, sr=sr)
96
+ elif model == 'pyannote':
97
+ segments = [(x.start, x.end)
98
+ for x in pyannote_vad({"waveform":audio,"sample_rate":sr}).get_timeline().support()]
99
+ powers = [calc_power(audio, sr, ts, te) for ts, te in segments]
100
+ sink.write({
101
+ "__key__": s['__key__'],
102
+ "gain_shift.npy": np.array([gain, shift], dtype=np.float32),
103
+ "vad.npy": np.array(segments, dtype=np.float32),
104
+ "powers.npy": np.array(powers, dtype=np.float32),
105
+ })
whisperspeech/vad_merge.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/1C. VAD merging.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/1C. VAD merging.ipynb 2
7
+ import random
8
+
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn.functional as F
12
+
13
+ from fastprogress import progress_bar
14
+ from fastcore.script import *
15
+
16
+ from . import utils
17
+ import webdataset as wds
18
+
19
+ # %% ../nbs/1C. VAD merging.ipynb 7
20
+ # we need to split first to merge in the spk_emb.npy data
21
+ # this is similar to utils.split_to_chunks but works without the audio data
22
+ def split(stream, ikey='vad.npy', copy_keys=[], split_keys=[]):
23
+ for s in stream:
24
+ imax = len(s[ikey]) - 1
25
+ if len(s[ikey]) == 0:
26
+ # Preserve info about audio files without any speech.
27
+ # We need to push this info through a weird side-channel
28
+ # because we want to be able to a merge with naively
29
+ # splitted data.
30
+ new = {"__key__": s['__key__'] + "_none",
31
+ "src_key": s['__key__'],
32
+ "__url__": s['__url__'],
33
+ "__skip_merge__": True}
34
+ for k in copy_keys: new[k] = np.array([])
35
+ for k in split_keys: new[k] = np.array([])
36
+ new[ikey] = s[ikey]
37
+ yield new
38
+ for i,(ts,te) in enumerate(s[ikey]):
39
+ new = {"__key__": s['__key__'] + f"_{i:03d}",
40
+ "src_key": s['__key__'],
41
+ "__url__": s['__url__'],
42
+ "i": i, "imax": imax}
43
+ for k in copy_keys: new[k] = s[k]
44
+ for k in split_keys: new[k] = s[k][i]
45
+ new[ikey] = s[ikey][i]
46
+ yield new
47
+
48
+ def merge_by_src_key(stream, copy_keys=[], merge_keys=['vad.npy']):
49
+ def make_record(src):
50
+ s = {
51
+ "__url__": src['__url__'],
52
+ "__key__": src['src_key'],
53
+ }
54
+ for k in copy_keys: s[k] = src[k]
55
+ for k in merge_keys: s[k] = []
56
+ return s
57
+ def finish_record(s):
58
+ for k in merge_keys: s[k] = np.array(s[k])
59
+ return s
60
+ ms = None
61
+ for s in stream:
62
+ try:
63
+ # push accumulated data
64
+ if ms and s['src_key'] != ms['__key__']:
65
+ yield finish_record(ms)
66
+ ms = None
67
+ # prepare a merged record for the new data
68
+ if ms is None:
69
+ ms = make_record(s)
70
+ for k in merge_keys:
71
+ if k in s: ms[k].append(s[k])
72
+ except:
73
+ print(f"Error processing {s['__key__']}:")
74
+ print(s)
75
+ raise
76
+ yield finish_record(ms)
77
+
78
+ # %% ../nbs/1C. VAD merging.ipynb 11
79
+ def random_cutter(dur):
80
+ if random.random() < 0.5:
81
+ return dur > 30 * (random.random()*0.95+0.05)
82
+ else:
83
+ return dur > 30
84
+
85
+ def random_cutter2(dur):
86
+ if random.random() < 0.25:
87
+ return True
88
+ else:
89
+ return dur > 30 * (random.random()*0.95+0.05)
90
+
91
+ def chunk_merger(prefix, should_cut=lambda x: x > 30):
92
+ def _merger(stream):
93
+ for s in stream:
94
+ segments, speakers = s['vad.npy'], s['spk_emb.npy']
95
+ if segments.size == 0:
96
+ s[prefix+'.vad.npy'], s[prefix+'.spk_emb.npy'] = np.array([]), np.array([])
97
+ s[prefix+'.subvads.pyd'] = []
98
+ yield s
99
+ continue
100
+ curr_start = segments[0][0]
101
+ curr_end = 0
102
+ curr_spk = None
103
+ curr_chunks = []
104
+ spk_acc = torch.tensor(speakers[0])
105
+ spk_acc_N = 1
106
+ merged = []
107
+ merged_chunks = []
108
+ merged_spk = []
109
+
110
+ for (ts,te),new_spk in zip(segments, speakers):
111
+ secs = te - ts
112
+ new_spk = torch.tensor(new_spk)
113
+ spk_change = False
114
+ if curr_spk is not None:
115
+ sim = F.cosine_similarity(curr_spk, new_spk, dim=0)
116
+ spk_change = sim < 0.5 if secs > 2 else sim < 0.1
117
+ if (spk_change or should_cut(te - curr_start)) and curr_end - curr_start > 0:
118
+ merged.append((curr_start, curr_end))
119
+ merged_spk.append(spk_acc / spk_acc_N)
120
+ merged_chunks.append(curr_chunks)
121
+ curr_start = ts
122
+ spk_acc = new_spk
123
+ curr_chunks = []
124
+ curr_spk = new_spk
125
+ if secs > 2:
126
+ spk_acc += new_spk
127
+ spk_acc_N += 1
128
+ curr_end = te
129
+ curr_chunks.append((ts, te))
130
+ merged.append((curr_start, curr_end))
131
+ merged_spk.append(spk_acc / spk_acc_N)
132
+ merged_chunks.append(curr_chunks)
133
+ s[prefix+'.vad.npy'], s[prefix+'.spk_emb.npy'] = np.array(merged), torch.stack(merged_spk).numpy()
134
+ s[prefix+'.subvads.pyd'] = merged_chunks
135
+ yield s
136
+ return _merger
137
+
138
+ # %% ../nbs/1C. VAD merging.ipynb 17
139
+ # we filter before splitting to keep empty merged samples even if we filter out everything
140
+ def filter_bad_samples(stream):
141
+ for s in stream:
142
+ if 'librilight' in s['__url__'] or 'test-shard.tar' in s['__url__']:
143
+ for k in ['vad.npy', 'spk_emb.npy', 'powers.npy']:
144
+ s[k] = s[k][1:-1]
145
+
146
+ if s['vad.npy'].size > 0:
147
+ lengths = s['vad.npy'][:,1] - s['vad.npy'][:,0]
148
+ mask = (lengths < 1) & (s['powers.npy'] < -6)
149
+ for k in ['vad.npy', 'spk_emb.npy', 'powers.npy']:
150
+ s[k] = s[k][~mask]
151
+ yield s
152
+
153
+
154
+ # %% ../nbs/1C. VAD merging.ipynb 19
155
+ @call_parse
156
+ def prepare_mvad(
157
+ input:str, # input VAD shard path
158
+ output:str, # output shard path
159
+ eqvad:bool=False, # make the chunk length distribution more uniform
160
+ ignore_spk_emb:bool=False,
161
+ ):
162
+ if ignore_spk_emb:
163
+ def chg_spk_emb(stream):
164
+ for s in stream:
165
+ for x in s['spk_emb.npy']: x[:] = 1
166
+ yield s
167
+ else:
168
+ def chg_spk_emb(stream):
169
+ for s in stream: yield s
170
+
171
+ ds = wds.WebDataset([input]).compose(
172
+ wds.decode(),
173
+ lambda x: split(x, copy_keys=['gain_shift.npy'], split_keys=['powers.npy']),
174
+ utils.merge_in(utils.derived_dataset('spk_emb')),
175
+ lambda x: merge_by_src_key(x, copy_keys=['gain_shift.npy'], merge_keys=['powers.npy', 'vad.npy', 'spk_emb.npy']),
176
+ filter_bad_samples,
177
+ chg_spk_emb,
178
+ chunk_merger('raw', lambda x: True),
179
+ chunk_merger('eq', random_cutter),
180
+ chunk_merger('max')
181
+ )
182
+
183
+ with utils.AtomicTarWriter(output) as sink:
184
+ for s in progress_bar(ds, total='noinfer'):
185
+ # if len(s['vad.npy']) > 1:
186
+ # print(s)
187
+ del s['vad.npy'], s['spk_emb.npy'], s['powers.npy']
188
+ sink.write(s)
189
+
190
+ # %% ../nbs/1C. VAD merging.ipynb 22
191
+ def find_vad_kind(kind):
192
+ def _finder(stream):
193
+ for s in stream:
194
+ for k in ['vad.npy', 'spk_emb.npy']:
195
+ s[k] = s[f'{kind}.{k}']
196
+ yield s
197
+ return _finder
198
+
199
+ def chunked_audio_dataset(shards, kind='max', copy_keys=['gain_shift.npy'], split_keys=['spk_emb.npy'],
200
+ resampled=False, nodesplitter=wds.shardlists.single_node_only):
201
+ return wds.WebDataset(shards, resampled=resampled, nodesplitter=nodesplitter).compose(
202
+ wds.decode(utils.torch_audio_opus),
203
+ utils.find_audio,
204
+ utils.merge_in(utils.derived_dataset('mvad')),
205
+ find_vad_kind(kind),
206
+ lambda x: utils.split_to_chunks(x, copy_keys=copy_keys, split_keys=split_keys),
207
+ )
whisperspeech/vq_stoks.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/2B. Whisper quantization (semantic token) model.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['RQBottleneckTransformer', 'make_model']
5
+
6
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 2
7
+ import io
8
+ import sys
9
+ import time
10
+ import torch
11
+ import torchaudio
12
+
13
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 3
14
+ from pathlib import Path
15
+ import json
16
+ from fastprogress import progress_bar, master_bar
17
+ import fastprogress
18
+ import numpy as np
19
+ import pylab as plt
20
+ import pandas as pd
21
+ import random
22
+
23
+ import whisper
24
+ import whisper.tokenizer
25
+ from huggingface_hub import hf_hub_download
26
+ from fastcore.basics import store_attr
27
+
28
+ from torch import nn
29
+ import torch.optim as optim
30
+ import torch.nn.functional as F
31
+ from torch.utils.data.dataloader import DataLoader
32
+ import webdataset as wds
33
+ from . import utils, vad_merge
34
+
35
+ from vector_quantize_pytorch import ResidualVQ
36
+
37
+ from fastcore.script import *
38
+
39
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 13
40
+ def add_masks(samples):
41
+ for s in samples:
42
+ seconds = s['tend'] - s['tstart']
43
+ sr = 16000 #s['sample_rate']
44
+ # a mask (downsampled to the Whisper encoder token rate of 50/s) is used
45
+ # to teach the model the concept of padding
46
+ # this let's us decode shorter sequences later
47
+ mask = torch.zeros(30*sr//320, dtype=torch.bool)
48
+ mask[:int(seconds * sr) // 320] = 1
49
+ s['mask'] = mask
50
+ yield s
51
+
52
+ def get_tokenizer(model, language):
53
+ multilingual = not model.endswith(".en")
54
+ return whisper.tokenizer.get_tokenizer(multilingual, language=language, task="transcribe",
55
+ num_languages = 100 if model == 'large-v3' else 99)
56
+
57
+ def tokenize_text(samples, ttoks_size=200, model="base.en", language="en"):
58
+ tokenizer = get_tokenizer(model, language)
59
+ for s in samples:
60
+ ttoks = tokenizer.encode(s['txt'])
61
+ tokens = list(tokenizer.sot_sequence_including_notimestamps) + ttoks
62
+ rpad = ttoks_size - len(tokens)
63
+ s['in_ttoks'] = F.pad(torch.tensor(tokens), (0, rpad), value=tokenizer.eot)
64
+ s['out_ttoks'] = F.pad(torch.tensor(tokens[1:] + [tokenizer.eot]), (0, rpad), value=-100)
65
+ yield s
66
+
67
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 14
68
+ def load_dataset(
69
+ dataset_dir:Path,
70
+ txt_label:str="base.en-txt", # the label of the files containing transcriptions
71
+ model:str="base.en",
72
+ language:str=None,
73
+ weight:float=1,
74
+ validation:bool=False,
75
+ exclude_datasets:str="txt-random-valid", # space separated directory names for validation datasets to exclude
76
+ ):
77
+ dataset_dir = Path(dataset_dir)
78
+ shards = utils.shard_glob(dataset_dir/'audio/*.tar')
79
+ with open(dataset_dir/'txt-samples.list') as f: samples = len(f.readlines())
80
+ language = utils.readlines(dataset_dir/'language')[0]
81
+
82
+ txt_dir = None
83
+ for name in ['small.en-txt', 'medium-txt']:
84
+ if (dataset_dir/name).exists():
85
+ txt_dir = name
86
+ break
87
+ if txt_dir is None: raise ArgumentError(f"No transcripts found in {dataset_dir}")
88
+
89
+ excludes = {x
90
+ for dir in exclude_datasets.split()
91
+ for x in utils.readlines(dataset_dir/Path(dir)/"txt-samples.list")
92
+ } if not validation and exclude_datasets else set()
93
+
94
+ if not language and model.endswith('en'): language = 'en'
95
+ assert language, "please provide the dataset language for multilang models"
96
+
97
+ same_on_all_nodes = lambda urls: urls # will only be used for validation
98
+ ds = vad_merge.chunked_audio_dataset(shards, 'raw',
99
+ resampled=not validation, nodesplitter=same_on_all_nodes).compose(
100
+ utils.merge_in(utils.derived_dataset(txt_dir)),
101
+ wds.select(lambda s: s['__key__'] not in excludes),
102
+ utils.resampler(16000, 'samples_16k'),
103
+ add_masks,
104
+ lambda x: tokenize_text(x, model=model, language=language),
105
+ wds.to_tuple('samples_16k', 'mask', 'in_ttoks', 'out_ttoks'),
106
+ )
107
+ if not validation:
108
+ ds = ds.compose(wds.shuffle(500, initial=500))
109
+ ds = ds.batched(64)
110
+ if validation:
111
+ ds = ds.slice(samples // 64)
112
+ ds.total_samples = samples
113
+ ds.weight = weight
114
+
115
+ return ds
116
+
117
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 22
118
+ from whisperspeech.train import *
119
+ from whisperspeech.modules import *
120
+
121
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 23
122
+ import dataclasses
123
+
124
+ def rand(start, end):
125
+ return random.random() * (end - start) + start
126
+
127
+ def logrand(start, end):
128
+ return 10**rand(math.log10(start), math.log10(end))
129
+
130
+ @dataclasses.dataclass
131
+ class Tunables:
132
+ init_std :float = 1.5
133
+ embeddings_std :float = 4.5e-2
134
+ embeddings_lr_scale: float = 1
135
+ query_mult :float = 2
136
+ rope :bool = True
137
+ mask_embs :bool = True # force embeddings corresponding to the input audio padding to a constant value
138
+ downsample_conv: bool = False
139
+ downsample_mean: bool = True
140
+
141
+ codebook_dim: int = 32 # FIXME: unused
142
+ codebook_decay: float = 0.9
143
+
144
+ lr0 :float = .9e-3
145
+ clip_gradient_norm :float = 2
146
+ weight_decay :float = 1e-3
147
+ warmup_steps :float = 850
148
+
149
+ random :bool = False
150
+
151
+ # unused, for backwards compatibility:
152
+ output_mult :int = None
153
+
154
+ def __post_init__(self):
155
+ # randomize the hyperparams if requested
156
+ if self.random:
157
+ self.init_std = logrand(1, 2)
158
+ self.embeddings_std = logrand(3e-2,6e-2)
159
+ self.embeddings_lr_scale = 2**rand(0,3)
160
+ self.query_mult = logrand(1,8)
161
+ self.codebook_dim = int(logrand(30,50))
162
+ self.codebook_decay = logrand(0.86,0.95)
163
+ self.rope = True
164
+ self.mask_embs = True
165
+ self.downsample_mean = True
166
+
167
+ self.lr0 = logrand(.8e-3,1e-3)
168
+ self.clip_gradient_norm = 10**rand(-1,1)
169
+ self.warmup_steps = logrand(700,1000)
170
+
171
+ @staticmethod
172
+ def upgrade(args):
173
+ args = {k:v for k,v in args.items()}
174
+ def old_default(name, value):
175
+ if name not in args: args[name] = value
176
+ old_default('output_mult', 1)
177
+ old_default('query_mult', 1)
178
+ old_default('rope', False)
179
+ old_default('mask_embs', False)
180
+ old_default('downsample_conv', False)
181
+ old_default('downsample_mean', False)
182
+ if 'encoder_depth_ratio' in args: del args['encoder_depth_ratio']
183
+ if 'vq_codes' in args: del args['vq_codes']
184
+ return args
185
+
186
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 24
187
+ import math
188
+
189
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 25
190
+ class RQBottleneckTransformer(nn.Module):
191
+ def __init__(self, vq_codes=512, q_depth=12, depth=1, n_head=2, head_width=64, ffn_mult=4,
192
+ codebook_dim=2, threshold_ema_dead_code=2, use_cosine_sim = False, kl_loss_mul=1,
193
+ downsample=1, no_quantize=False,
194
+ whisper_model_name='tiny.en', tunables=Tunables()):
195
+ super().__init__()
196
+ width = n_head * head_width
197
+ store_attr("codebook_dim,vq_codes,q_depth,n_head,head_width,ffn_mult,depth,use_cosine_sim,downsample,whisper_model_name")
198
+ self.width = width
199
+ self.base_width = 3 * head_width
200
+ self.vq_codes = vq_codes
201
+ self.tunables = tunables
202
+ self.stoks_len = 1500//downsample
203
+ self.stoks_per_sec = self.stoks_len//30
204
+ self.no_quantize = no_quantize
205
+
206
+ qk_scale = self.tunables.query_mult * 8 / math.sqrt(head_width)
207
+
208
+ self.kl_loss_mul = kl_loss_mul
209
+
210
+ if no_quantize:
211
+ # a mode to get Whisper baselines into W&B easily, skips all training
212
+ self.fake_parameter = nn.Parameter(torch.tensor(0.001))
213
+ else:
214
+ n_mlp = width * ffn_mult
215
+ self.mlp = nn.Sequential(
216
+ nn.Linear(width, n_mlp), nn.GELU(), nn.Linear(n_mlp, width)
217
+ )
218
+ self.mlp_ln = LayerNorm(width)
219
+
220
+ if tunables.downsample_conv:
221
+ self.downsample_conv = nn.Conv1d(width, width, kernel_size=3, stride=downsample, padding=1)
222
+ else:
223
+ self.downsample_conv = None
224
+
225
+ if tunables.mask_embs: vq_codes = vq_codes + 1
226
+ self.rq = ResidualVQ(
227
+ dim = width,
228
+ codebook_size = vq_codes, # codebook size
229
+ decay = tunables.codebook_decay, # the exponential moving average decay, lower means the dictionary will change faster
230
+ commitment_weight = 1., # the weight on the commitment loss
231
+ threshold_ema_dead_code = threshold_ema_dead_code,
232
+ use_cosine_sim = use_cosine_sim,
233
+ codebook_dim = codebook_dim,
234
+ num_quantizers= 1,
235
+ )
236
+
237
+ self.positional_embedding = nn.Embedding(1500, width) # FIXME: should be self.stoks_len
238
+
239
+ self._out_blocks = nn.Sequential(*[
240
+ ResidualAttentionBlock(width, n_head, qk_scale=qk_scale, ffn_mult=ffn_mult, rope=tunables.rope) for _ in range(depth)
241
+ ])
242
+ self.ln_post = LayerNorm(width)
243
+
244
+ self.positions = torch.arange(0, 1500, dtype=torch.long)
245
+
246
+ self.ce_lossf = nn.CrossEntropyLoss(ignore_index=-100)
247
+ self.kl_lossf = nn.KLDivLoss(reduction='batchmean')
248
+
249
+ self.whmodel = None
250
+
251
+ self.apply(self.init_transformer)
252
+ self.register_buffer('val_true', torch.zeros(1))
253
+ self.register_buffer('val_total', torch.zeros(1))
254
+
255
+ def setup(self, device):
256
+ self.ensure_whisper(device)
257
+
258
+ def init_transformer(self, m):
259
+ if isinstance(m, LinearHead):
260
+ m.no_weight_decay = True
261
+ torch.nn.init.constant_(m.weight, 0)
262
+ elif isinstance(m, QueryHead):
263
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
264
+ torch.nn.init.constant_(m.weight, 0)
265
+ elif isinstance(m, nn.Embedding):
266
+ m.no_weight_decay = True
267
+ m.lr_scale = self.tunables.embeddings_lr_scale
268
+ std = self.tunables.embeddings_std
269
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
270
+ elif isinstance(m, nn.Linear):
271
+ m.lr_scale = 1/(m.weight.shape[1] / self.base_width)
272
+ std = self.tunables.init_std / m.weight.shape[1]
273
+ torch.nn.init.trunc_normal_(m.weight, std=std, a=-3*std, b=3*std)
274
+ if m.bias is not None:
275
+ torch.nn.init.trunc_normal_(m.bias, std=std, a=-3*std, b=3*std)
276
+ elif isinstance(m, nn.LayerNorm):
277
+ m.no_weight_decay = True
278
+ torch.nn.init.constant_(m.bias, 0)
279
+ torch.nn.init.constant_(m.weight, 1)
280
+
281
+ @property
282
+ def device(self):
283
+ return next(self.parameters()).device
284
+
285
+ #
286
+ # training
287
+ #
288
+ def log_mel_spectrogram(self, samples):
289
+ return whisper.log_mel_spectrogram(samples, 128 if self.whisper_model_name == 'large-v3' else 80)
290
+
291
+ @torch.no_grad()
292
+ def extract_teacher(self, samples, input_toks, output_toks):
293
+ embs = self.whmodel[0].encoder(self.log_mel_spectrogram(samples))
294
+ teacher_logits = self.whmodel[0].decoder(input_toks, embs)
295
+ # set teacher logits to 0 for padding positions so KLDivLoss ignores them
296
+ teacher_logits[output_toks == -100] = 0
297
+ return embs, teacher_logits
298
+
299
+ def downsample_embeddings(self, x):
300
+ if self.downsample_conv is not None:
301
+ return x[:,::self.downsample] + self.downsample_conv(x.transpose(-1,-2)).transpose(-2,-1)
302
+ elif self.tunables.downsample_mean:
303
+ bs,slen,depth = x.shape
304
+ return x.reshape(bs,slen//self.downsample,self.downsample,depth).mean(-2)
305
+ else:
306
+ return x[:,::self.downsample]
307
+
308
+ def out_blocks(self, x):
309
+ for l in self._out_blocks: x = l(x, self.positions)
310
+ return x
311
+
312
+ def forward(self, samples, mask, input_toks, output_toks):
313
+ embs, teacher_logits = self.extract_teacher(samples, input_toks, output_toks)
314
+
315
+ if not self.no_quantize:
316
+ x = self.downsample_embeddings(embs)
317
+ x = x + self.mlp(self.mlp_ln(x))
318
+ # VQ bottleneck
319
+ quantized, self.indices, self.commit_loss = self.rq(x)
320
+ self.commit_loss = self.commit_loss.mean()
321
+
322
+ x = quantized.repeat_interleave(self.downsample, -2)
323
+ project_out = getattr(self.rq, 'project_out', None) or self.rq.layers[0].project_out
324
+ if self.tunables.mask_embs: x[~mask] = project_out(self.rq.layers[0]._codebook.embed[0,self.vq_codes])
325
+ x = x + self.positional_embedding(self.positions.to(x.device))
326
+ x = self.ln_post(self.out_blocks(x))
327
+
328
+ logits = self.whmodel[0].decoder(input_toks, embs if self.no_quantize else x)
329
+ self.ce_loss = self.ce_lossf(logits.view(-1,logits.shape[-1]), output_toks.view(-1))
330
+ self.kl_loss = self.kl_lossf(F.log_softmax(logits, dim=-1), F.softmax(teacher_logits, dim=-1))
331
+ loss = self.ce_loss + self.kl_loss_mul * self.kl_loss
332
+ if not self.no_quantize: loss += self.commit_loss
333
+ x = None
334
+ if self.no_quantize: loss = loss + self.fake_parameter
335
+
336
+ if not self.training:
337
+ valid_toks = output_toks != -100
338
+ self.val_true += (logits.detach().argmax(-1)[valid_toks] == output_toks[valid_toks]).float().sum()
339
+ self.val_total += valid_toks.float().sum()
340
+
341
+ return x, logits, loss
342
+
343
+ def get_metrics(self):
344
+ metrics = {
345
+ 'acc_0': (self.val_true / self.val_total).item(),
346
+ }
347
+ self.val_true[:] = 0
348
+ self.val_total[:] = 0
349
+ return metrics
350
+
351
+ #
352
+ # inference
353
+ #
354
+ @classmethod
355
+ def load_model(cls, ref="collabora/spear-tts-pytorch:whisper-vq-stoks-medium-en+pl.model",
356
+ repo_id=None, filename=None, local_filename=None):
357
+ if repo_id is None and filename is None and local_filename is None:
358
+ if ":" in ref:
359
+ repo_id, filename = ref.split(":", 1)
360
+ else:
361
+ local_filename = ref
362
+ if not local_filename:
363
+ local_filename = hf_hub_download(repo_id=repo_id, filename=filename)
364
+ spec = torch.load(local_filename, weights_only=False)
365
+ vqmodel = cls(**spec['config'], tunables=Tunables(**Tunables.upgrade(spec.get('tunables', {}))))
366
+ # Older checkpoints store the transformer blocks as `out_blocks.*` while
367
+ # current code names the submodule `_out_blocks`. Remap on load.
368
+ sd = {}
369
+ for k, v in spec['state_dict'].items():
370
+ if k.startswith('out_blocks.'):
371
+ sd['_' + k] = v
372
+ else:
373
+ sd[k] = v
374
+ vqmodel.load_state_dict(sd)
375
+ vqmodel.eval()
376
+ return vqmodel
377
+
378
+ def load_checkpoint(self, local_filename):
379
+ spec = torch.load(local_filename, map_location='cpu')
380
+ assert 'pytorch-lightning_version' in spec, 'not a valid PyTorch Lightning checkpoint'
381
+ state_dict = {k.replace('model.', ''):v
382
+ for k,v in spec['state_dict'].items()}
383
+ self.load_state_dict(state_dict)
384
+ return self
385
+
386
+ def save_model(self, fname, store_parameters=True):
387
+ torch.save(dict(config = self.__stored_args__,
388
+ tunables = dataclasses.asdict(self.tunables),
389
+ state_dict = self.state_dict() if store_parameters else None), fname)
390
+
391
+ def ensure_whisper(self, device=None):
392
+ if self.whmodel is not None: return
393
+ device = device or self.device
394
+ # the list wrapper is a hack to make sure the whole of Whisper is not sucked into self.parameters()
395
+ if self.whmodel is None: self.whmodel = [whisper.load_model(self.whisper_model_name, device=device)]
396
+ self.decoding_options = whisper.DecodingOptions()
397
+ self.tokenizer = get_tokenizer(self.whisper_model_name, None)
398
+
399
+ def quantize(self, embs):
400
+ x = self.downsample_embeddings(embs)
401
+ x = x + self.mlp(self.mlp_ln(x))
402
+ _, stoks, _ = self.rq(x)
403
+ if self.q_depth == 1:
404
+ stoks = stoks.squeeze(-1)
405
+ return stoks
406
+
407
+ def dequantize(self, stoks):
408
+ assert self.q_depth == 1
409
+ assert len(stoks.shape) == 1, "batch processing is not supported"
410
+ if isinstance(stoks, np.ndarray): stoks = torch.tensor(stoks)
411
+ # remove padding
412
+ padding = torch.nonzero(stoks == self.vq_codes)
413
+ if padding.any(): stoks = stoks[:padding[0,0]]
414
+ stoks = F.pad(stoks, (0,self.stoks_len - stoks.shape[-1]), value=self.vq_codes if self.tunables.mask_embs else 0)
415
+ x = self.rq.layers[0]._codebook.embed[0,stoks.to(torch.long).view(-1)]
416
+ x = x.repeat_interleave(self.downsample, -2)
417
+ project_out = getattr(self.rq, 'project_out', None) or self.rq.layers[0].project_out
418
+ x = project_out(x).unsqueeze(0)
419
+ positions = torch.arange(0, x.shape[-2], dtype=torch.long, device=x.device)
420
+ x = x + self.positional_embedding(positions)
421
+ return self.ln_post(self.out_blocks(x))
422
+
423
+ def encode_audio(self, audio):
424
+ if isinstance(audio, str):
425
+ x, sr = torchaudio.load(audio)
426
+ x = torchaudio.transforms.Resample(sr, 16000)(x)[0]
427
+ audio = x.unsqueeze(0)
428
+ return self.encode_mel(self.log_mel_spectrogram(audio).to(self.device))
429
+
430
+ def encode_mel(self, mel):
431
+ assert len(mel.shape) == 3, "invalid mel spectrogram shape, expect (batch,chn,time)"
432
+ self.ensure_whisper()
433
+ n = mel.shape[-1]
434
+ if n > whisper.audio.N_FRAMES:
435
+ padding = 0
436
+ padded = mel[:,:,:whisper.audio.N_FRAMES]
437
+ else:
438
+ padding = -n % whisper.audio.N_FRAMES
439
+ padded = F.pad(mel, (0, padding), value=-1.5)
440
+ embs = self.whmodel[0].encoder(padded)#.to(self.whmodel[0].device))#[:,:n//2]
441
+ stoks = self.quantize(embs)
442
+ if self.tunables.mask_embs:
443
+ return stoks[:,:n//2//self.downsample]
444
+ else:
445
+ return stoks
446
+
447
+ def decode_text(self, stoks, decoding_options=None):
448
+ self.ensure_whisper(self.device)
449
+ if decoding_options is None: decoding_options = self.decoding_options
450
+ embs = self.dequantize(stoks).to(self.whmodel[0].device)
451
+ return self.whmodel[0].decode(embs, decoding_options)
452
+
453
+ # %% ../nbs/2B. Whisper quantization (semantic token) model.ipynb 27
454
+ def make_model(size:str, no_quantize=False, tunables:Tunables=Tunables(), dataset:torch.utils.data.Dataset=None):
455
+ common = dict(
456
+ q_depth=1, depth=1, threshold_ema_dead_code=0, use_cosine_sim=True, tunables=tunables,
457
+ no_quantize = no_quantize,
458
+ )
459
+ if size == 'base.en-2d-4096c':
460
+ model = RQBottleneckTransformer(codebook_dim=32, vq_codes=4096, n_head=8, downsample=2,
461
+ whisper_model_name=size.split("-")[0], **common)
462
+ return model
463
+ if size == 'base.en-2d-512c':
464
+ model = RQBottleneckTransformer(codebook_dim=32, vq_codes=512, n_head=8, downsample=2,
465
+ whisper_model_name=size.split("-")[0], **common)
466
+ return model
467
+ if size == 'base.en-2d-512c-dim64':
468
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=512, n_head=8, downsample=2,
469
+ whisper_model_name=size.split("-")[0], **common)
470
+ return model
471
+ if size == 'base-2d-512c-dim64':
472
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=512, n_head=8, downsample=2,
473
+ whisper_model_name=size.split("-")[0], **common)
474
+ return model
475
+ if size == 'base-2d-1024c-dim64':
476
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=1024, n_head=8, downsample=2,
477
+ whisper_model_name=size.split("-")[0], **common)
478
+ return model
479
+ if size == 'medium-2d-256c-dim64':
480
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=256, n_head=16, downsample=2,
481
+ whisper_model_name=size.split("-")[0], **common)
482
+ return model
483
+ if size == 'medium-2d-256c-dim128':
484
+ model = RQBottleneckTransformer(codebook_dim=128, vq_codes=256, n_head=16, downsample=2,
485
+ whisper_model_name=size.split("-")[0], **common)
486
+ return model
487
+ if size == 'medium-2d-512c-dim64':
488
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=512, n_head=16, downsample=2,
489
+ whisper_model_name=size.split("-")[0], **common)
490
+ return model
491
+ if size == 'medium-2d-512c-dim128':
492
+ model = RQBottleneckTransformer(codebook_dim=128, vq_codes=512, n_head=16, downsample=2,
493
+ whisper_model_name=size.split("-")[0], **common)
494
+ return model
495
+ if size == 'medium-2d-512c-dim256':
496
+ model = RQBottleneckTransformer(codebook_dim=256, vq_codes=512, n_head=16, downsample=2,
497
+ whisper_model_name=size.split("-")[0], **common)
498
+ return model
499
+ if size == 'medium-2d-1024c-dim64':
500
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=1024, n_head=16, downsample=2,
501
+ whisper_model_name=size.split("-")[0], **common)
502
+ return model
503
+ if size == 'medium-2d-2048c-dim64':
504
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=2048, n_head=16, downsample=2,
505
+ whisper_model_name=size.split("-")[0], **common)
506
+ return model
507
+ if size == 'large-v2-2d-512c-dim64':
508
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=512, n_head=20, downsample=2,
509
+ whisper_model_name='large-v2', **common)
510
+ return model
511
+ if size == 'large-v3-2d-512c-dim64':
512
+ model = RQBottleneckTransformer(codebook_dim=64, vq_codes=512, n_head=20, downsample=2,
513
+ whisper_model_name='large-v3', **common)
514
+ return model
515
+ raise ArgumentError(f"invalid model size: {size}")
whisperspeech/wer_metrics.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/C. Word error rate metrics.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['librispeech_data', 'DfBuilder', 'WERStats']
5
+
6
+ # %% ../nbs/C. Word error rate metrics.ipynb 2
7
+ import jiwer
8
+ from whisper_normalizer.english import EnglishTextNormalizer
9
+
10
+ import torchaudio
11
+ from pathlib import Path
12
+ import pandas as pd
13
+
14
+ # %% ../nbs/C. Word error rate metrics.ipynb 3
15
+ engnorm = EnglishTextNormalizer()
16
+ def whisper_normalize(x):
17
+ if type(x) == list:
18
+ return [engnorm(y) for y in x]
19
+ else:
20
+ return engnorm(x)
21
+
22
+ default_transform = jiwer.transforms.Compose([
23
+ jiwer.transforms.ToLowerCase(),
24
+ jiwer.transforms.ExpandCommonEnglishContractions(),
25
+ whisper_normalize,
26
+ jiwer.transforms.RemoveMultipleSpaces(),
27
+ jiwer.transforms.Strip(),
28
+ jiwer.transforms.RemovePunctuation(),
29
+ jiwer.transforms.ReduceToListOfListOfWords(),
30
+ ])
31
+
32
+ # %% ../nbs/C. Word error rate metrics.ipynb 5
33
+ def librispeech_data(datadir, sample_rate=16000):
34
+ for file in Path(datadir).rglob('*.txt'):
35
+ for line in file.read_text().split('\n'):
36
+ if not line: continue
37
+ idx, text = line.split(" ", 1)
38
+ x, sr = torchaudio.load((file.parent/idx).with_suffix('.flac'))
39
+ if sr != sample_rate:
40
+ x = torchaudio.transforms.Resample(sr, self.sample_rate)(x)
41
+ yield x, text
42
+
43
+ # %% ../nbs/C. Word error rate metrics.ipynb 6
44
+ class DfBuilder:
45
+ def __init__(self):
46
+ self.data = {}
47
+
48
+ def push(self, **kwargs):
49
+ for k,v in kwargs.items():
50
+ if k not in self.data:
51
+ self.data[k] = [v]
52
+ else:
53
+ self.data[k].append(v)
54
+
55
+ def df(self):
56
+ return pd.DataFrame(self.data)
57
+
58
+ # %% ../nbs/C. Word error rate metrics.ipynb 7
59
+ class WERStats(DfBuilder):
60
+ def __init__(self, transform=default_transform):
61
+ super().__init__()
62
+ self.reference_transform = transform
63
+ self.hypothesis_transform = transform
64
+
65
+ def push_sample(self, snd, gt_text, text, idx=None):
66
+ if snd is not None: self.push(secs = snd.shape[-1]/16000)
67
+ diff = jiwer.process_words(gt_text, text, reference_transform=self.reference_transform, hypothesis_transform=self.hypothesis_transform)
68
+ self.push(
69
+ idx = idx,
70
+ gt_text = gt_text,
71
+ text = text,
72
+ wer = diff.wer,
73
+ mer = diff.mer,
74
+ wil = diff.wil,
75
+ wip = diff.wip,
76
+ )
77
+ return diff
whisperspeech/wh_transcribe.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/2A. Whisper quantization dataset preparation.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = []
5
+
6
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 3
7
+ import os
8
+ import io
9
+ import time
10
+ import torch
11
+ import torchaudio
12
+
13
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 4
14
+ from pathlib import Path
15
+ import json
16
+ from fastprogress import progress_bar, master_bar
17
+ import numpy as np
18
+ import random
19
+
20
+ import whisper
21
+
22
+ from torch import nn
23
+ import torch.nn.functional as F
24
+ from torch.utils.data.dataloader import DataLoader
25
+
26
+ from fastcore.script import *
27
+
28
+ from . import vad, utils
29
+ import webdataset as wds
30
+
31
+ from .inference import get_compute_device
32
+
33
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 9
34
+ # let's make it a bit more conservative
35
+ # with full 30 second chunks it sometimes misses a small part of the transcript
36
+ def random_cutter(dur):
37
+ if random.random() < 0.5:
38
+ return dur > 28 * (random.random()*0.95+0.05)
39
+ else:
40
+ return dur > 28
41
+
42
+ def chunk_merger(segments, should_cut=lambda x: x > 28):
43
+ if len(segments) == 0: return segments
44
+ curr_start = segments[0][0]
45
+ curr_end = 0
46
+ merged = []
47
+
48
+ for ts,te in segments:
49
+ if should_cut(te - curr_start) and curr_end - curr_start > 0:
50
+ merged.append((curr_start, curr_end))
51
+ curr_start = ts
52
+ curr_end = te
53
+ merged.append((curr_start, curr_end))
54
+ return merged
55
+
56
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 18
57
+ def merge_in(*datasets):
58
+ """Merge multiple datasets into the current one returning samples with the union of keys.
59
+
60
+ It requires (and validates) all datasets to have the same ordering of keys so you have
61
+ to use it before any sample shuffling. Shard shuffling is ok.
62
+ """
63
+ def merge_loop(main_samples):
64
+ for samples in zip(*[main_samples]+[iter(x) for x in datasets]):
65
+ key = samples[0]['__key__']
66
+ news = {}
67
+ for s in samples:
68
+ assert s['__key__'] == key
69
+ news.update(s)
70
+ yield news
71
+ return merge_loop
72
+
73
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 19
74
+ import copy
75
+
76
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 20
77
+ # a workaround for https://github.com/webdataset/webdataset/issues/297
78
+ # should be possible to use ds.compose here
79
+ def wds_compose(ds, *args):
80
+ ds = copy.copy(ds)
81
+ ds.pipeline = copy.copy(ds.pipeline)
82
+ for f in args:
83
+ ds.append(f)
84
+ return ds
85
+
86
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 24
87
+ def split_to_chunks(stream, ikey='vad.npy', pad_to_seconds=30, random_shift=False):
88
+ for s in stream:
89
+ audio, sr = s['audio']
90
+ imax = len(s[ikey]) - 1
91
+ for i,(ts,te) in enumerate(s[ikey]):
92
+ samples = audio[0,int(ts*sr):int(te*sr)]
93
+ if pad_to_seconds is not None:
94
+ padding = pad_to_seconds*sr-samples.shape[-1]
95
+ lpad = random.randint(0, padding) if random_shift else 0
96
+ samples = F.pad(samples, (lpad, padding-lpad))
97
+ yield {"__key__": s['__key__'] + f"_{i:03d}",
98
+ "__url__": s['__url__'],
99
+ "i": i, "imax": imax,
100
+ "tstart": ts, "tend": te, "total_seconds": audio.shape[-1]/sr,
101
+ "lpad": lpad, "rpad": padding-lpad,
102
+ "lpad_s": lpad/sr, "rpad_s": (padding-lpad)/sr,
103
+ "samples": samples, "sample_rate": sr}
104
+
105
+ # %% ../nbs/2A. Whisper quantization dataset preparation.ipynb 39
106
+ def flac_to_txt_name(input, model_size):
107
+ return input.rsplit("/", 1)[1].replace('flac', f'{model_size}-txt') + ".gz"
108
+
109
+ @call_parse
110
+ def process_shard(
111
+ input:str, # input shard URL/path
112
+ output:str=None, # output shard URL/path
113
+ bs:int=None, # batch size (16 uses around 11GB of VRAM)
114
+ n_samples:int=None, # limit the number of samples (useful for quick benchmarking)
115
+ whisper_model:str="base.en", # Whisper model size
116
+ language:str="en", # transcription language
117
+ ):
118
+ device = get_compute_device()
119
+ if output is None: output = flac_to_txt_name(input, whisper_model)
120
+ if bs is None: bs = 16
121
+ if n_samples is None: n_samples = 'noinfer'
122
+ else: n_samples = n_samples // bs
123
+
124
+ ds = wds_compose(vad.load_dataset(input),
125
+ merge_in(wds.WebDataset(vad.flac_to_vad_name(input)).decode()),
126
+ wds.map_dict(**{"vad.npy":chunk_merger}),
127
+ split_to_chunks,
128
+ utils.resampler(16000, 'samples_16k'),
129
+ wds.to_tuple('__key__', 'samples_16k'),
130
+ wds.batched(bs),
131
+ )
132
+ dl = DataLoader(ds, num_workers=2, batch_size=None)
133
+
134
+ whmodel = whisper.load_model(whisper_model).to(device)
135
+ decoding_options = whisper.DecodingOptions(language=language)
136
+
137
+ tmp = output+".tmp"
138
+ with wds.TarWriter(tmp) as sink:
139
+ for keys, samples in progress_bar(dl, total=n_samples):
140
+ with torch.no_grad():
141
+ embs = whmodel.encoder(whisper.log_mel_spectrogram(samples).to(device))
142
+ decs = whmodel.decode(embs, decoding_options)
143
+ for key, dec in zip(keys, decs):
144
+ sink.write({
145
+ "__key__": key,
146
+ "txt": dec.text,
147
+ })
148
+ os.rename(tmp, output)