ggospodinov commited on
Commit
2f8a571
·
verified ·
1 Parent(s): 14a55fb

GigaAM Multilingual

Browse files
Files changed (4) hide show
  1. README.md +85 -0
  2. config.json +122 -0
  3. modeling_gigaam.py +2149 -0
  4. pytorch_model.bin +3 -0
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - ru
5
+ - en
6
+ - kk
7
+ - ky
8
+ - uz
9
+ pipeline_tag: automatic-speech-recognition
10
+ ---
11
+
12
+ # GigaAM Multilingual
13
+
14
+ GigaAM Multilingual is a family of Conformer-based foundation models (220M / 600M parameters) pre-trained with a HuBERT-style objective on **2M hours** of speech across **70+ languages** and fine-tuned for speech recognition with character-wise CTC decoders on 50K hours.
15
+
16
+ The models provide best-in-class open-source quality on Russian, Kazakh, Kyrgyz, and Uzbek, and moderate quality on English.
17
+
18
+ GigaAM Multilingual includes the following model variants:
19
+ - `ssl` — 220M self-supervised encoder
20
+ - `ctc` — 220M ASR model with a character-wise CTC decoder
21
+ - `large_ssl` — 600M self-supervised encoder
22
+ - `large_ctc` — 600M ASR model with a character-wise CTC decoder
23
+
24
+ ## Model Performance
25
+
26
+ Word Error Rate (%) on [Common Voice](https://commonvoice.mozilla.org) (CV), [FLEURS](https://huggingface.co/datasets/google/fleurs), and internal in-the-wild test sets. Utterances longer than 30 s and references containing digits are excluded; references/hypotheses are normalized (lowercasing, punctuation removal, numerals→words); greedy decoding. Best per row in **bold**.
27
+
28
+ | Language | Dataset | GigaAM Multilingual | GigaAM Multilingual Large | Omnilingual 1B (LLM) | Seamless M4T large v2 | Whisper large v3 |
29
+ |:--------|:--------|------------:|------------:|---------------------:|----------------------:|-----------------:|
30
+ | English | CV | 26.0 | 21.5 | 24.7 | **16.2** | 20.0 |
31
+ | English | FLEURS | 12.2 | 9.4 | 7.1 | 5.8 | **3.9** |
32
+ | Russian | CV | 7.1 | **5.1** | 13.6 | 9.2 | 9.1 |
33
+ | Russian | FLEURS | 4.4 | **3.0** | 6.4 | 4.6 | 3.1 |
34
+ | Russian | Internal | 7.6 | **6.0** | 14.6 | 16.1 | 10.1 |
35
+ | Kazakh | CV | 17.2 | **13.8** | 23.7 | 23.8 | 57.8 |
36
+ | Kazakh | FLEURS | 5.2 | **4.4** | 6.6 | 6.8 | 32.4 |
37
+ | Kazakh | Internal | 18.8 | **15.8** | 32.2 | 62.9 | 65.2 |
38
+ | Kyrgyz | CV | 12.5 | **10.2** | 21.6 | 14.3 | 95.2 |
39
+ | Kyrgyz | FLEURS | 7.0 | **5.5** | 8.1 | 9.5 | 86.3 |
40
+ | Kyrgyz | Internal | 11.1 | **9.8** | 25.0 | 78.3 | 102.2 |
41
+ | Uzbek | CV | 11.3 | **9.2** | 32.8 | 25.1 | 109.9 |
42
+ | Uzbek | FLEURS | 10.0 | **7.3** | 15.4 | 11.9 | 105.4 |
43
+ | Uzbek | Internal | 13.8 | **12.7** | 30.2 | 40.0 | 120.6 |
44
+
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from transformers import AutoModel
50
+
51
+ revision = "ctc" # any variant: ssl, ctc, large_ssl, large_ctc
52
+ model = AutoModel.from_pretrained(
53
+ "ai-sage/GigaAM-Multilingual",
54
+ revision=revision,
55
+ trust_remote_code=True,
56
+ )
57
+
58
+ transcription = model.transcribe("example.wav")
59
+ print(transcription)
60
+ ```
61
+
62
+ Recommended versions:
63
+ - `torch==2.10.*`, `torchaudio==2.10.*`
64
+ - `transformers==5.*`
65
+ - (any) `hydra-core`, `omegaconf`
66
+
67
+ Full usage guide can be found in the [example](https://github.com/salute-developers/GigaAM/blob/main/colab_example.ipynb).
68
+
69
+ ## Fine-tuning to a new language
70
+
71
+ The `ssl` / `large_ssl` backbones can be adapted to a new language — see the [fine-tuning guide](https://github.com/salute-developers/GigaAM/blob/main/train_utils/README.md) and the [example notebook](https://github.com/salute-developers/GigaAM/blob/main/train_utils/example.ipynb).
72
+
73
+ ## Citation
74
+
75
+ ```bibtex
76
+ @misc{gigaam_multilingual,
77
+ title={GigaAM Multilingual: Foundation Model for Underrepresented Languages},
78
+ author={Andrei Kuzmenko and Alexandr Maximenko and Aleksandr Kutsakov and Georgii Gospodinov and Dmitrii Bolotov and Oleg Kutuzov and Pavel Bogomolov and Fyodor Minkin},
79
+ year={2026},
80
+ eprint={2607.10371},
81
+ archivePrefix={arXiv},
82
+ primaryClass={eess.AS},
83
+ url={https://arxiv.org/abs/2607.10371}
84
+ }
85
+ ```
config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "gigaam",
3
+ "auto_map": {
4
+ "AutoConfig": "modeling_gigaam.GigaAMConfig",
5
+ "AutoModel": "modeling_gigaam.GigaAMModel"
6
+ },
7
+ "cfg": {
8
+ "model": {
9
+ "cfg": {
10
+ "model_class": "ctc",
11
+ "sample_rate": 16000,
12
+ "model_name": "multilingual_ctc",
13
+ "preprocessor": {
14
+ "_target_": "modeling_gigaam.FeatureExtractor",
15
+ "sample_rate": 16000,
16
+ "features": 64,
17
+ "win_length": 320,
18
+ "hop_length": 160,
19
+ "n_fft": 320,
20
+ "center": false
21
+ },
22
+ "encoder": {
23
+ "_target_": "modeling_gigaam.ConformerEncoder",
24
+ "feat_in": 64,
25
+ "n_layers": 16,
26
+ "d_model": 768,
27
+ "subsampling": "conv1d",
28
+ "subs_kernel_size": 5,
29
+ "subsampling_factor": 4,
30
+ "ff_expansion_factor": 4,
31
+ "self_attention_model": "rotary",
32
+ "pos_emb_max_len": 5000,
33
+ "n_heads": 16,
34
+ "conv_norm_type": "layer_norm",
35
+ "conv_kernel_size": 5,
36
+ "flash_attn": false
37
+ },
38
+ "head": {
39
+ "_target_": "modeling_gigaam.CTCHead",
40
+ "feat_in": 768,
41
+ "num_classes": 71
42
+ },
43
+ "decoding": {
44
+ "_target_": "modeling_gigaam.CTCGreedyDecoding",
45
+ "vocabulary": [
46
+ " ",
47
+ "'",
48
+ "a",
49
+ "b",
50
+ "c",
51
+ "d",
52
+ "e",
53
+ "f",
54
+ "g",
55
+ "h",
56
+ "i",
57
+ "j",
58
+ "k",
59
+ "l",
60
+ "m",
61
+ "n",
62
+ "o",
63
+ "p",
64
+ "q",
65
+ "r",
66
+ "s",
67
+ "t",
68
+ "u",
69
+ "v",
70
+ "w",
71
+ "x",
72
+ "y",
73
+ "z",
74
+ "а",
75
+ "б",
76
+ "в",
77
+ "г",
78
+ "д",
79
+ "е",
80
+ "ж",
81
+ "з",
82
+ "и",
83
+ "й",
84
+ "к",
85
+ "л",
86
+ "м",
87
+ "н",
88
+ "о",
89
+ "п",
90
+ "р",
91
+ "с",
92
+ "т",
93
+ "у",
94
+ "ф",
95
+ "х",
96
+ "ц",
97
+ "ч",
98
+ "ш",
99
+ "щ",
100
+ "ъ",
101
+ "ы",
102
+ "ь",
103
+ "э",
104
+ "ю",
105
+ "я",
106
+ "ё",
107
+ "і",
108
+ "ғ",
109
+ "қ",
110
+ "ң",
111
+ "ү",
112
+ "ұ",
113
+ "һ",
114
+ "ә",
115
+ "ө"
116
+ ]
117
+ }
118
+ },
119
+ "_target_": "modeling_gigaam.GigaAMASR"
120
+ }
121
+ }
122
+ }
modeling_gigaam.py ADDED
@@ -0,0 +1,2149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained GigaAM modeling file (generated by tools/build_hf_modeling.py)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import math
7
+ import os
8
+ import sys
9
+ import unicodedata
10
+ import warnings
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Iterable
13
+ from contextlib import contextmanager
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from subprocess import CalledProcessError, run
17
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union, cast
18
+
19
+ import hydra
20
+ import numpy as np
21
+ import omegaconf
22
+ import soundfile as sf
23
+ import torch
24
+ import torch.nn.functional as F
25
+ import torchaudio
26
+ from huggingface_hub import snapshot_download
27
+ from huggingface_hub.errors import LocalEntryNotFoundError
28
+ from hydra.utils import instantiate
29
+ from sentencepiece import SentencePieceProcessor
30
+ from torch import Tensor, nn
31
+ from torch.jit import TracerWarning
32
+ from torch.torch_version import TorchVersion
33
+ from torch.utils.checkpoint import checkpoint
34
+ from torch.utils.data import DataLoader
35
+ from transformers import PretrainedConfig, PreTrainedModel
36
+ from transformers.utils import cached_file
37
+
38
+ if TYPE_CHECKING:
39
+ try:
40
+ from pyannote.audio import Model, Pipeline
41
+ except ImportError:
42
+ pass
43
+
44
+ DIR_NAME = os.path.dirname(os.path.abspath(__file__))
45
+ sys.path.append(DIR_NAME) # enable hydra targets like modeling_gigaam.<ClassName>
46
+
47
+ # ==== gigaam/types.py ====
48
+
49
+
50
+ @dataclass
51
+ class AudioDatasetSample:
52
+ item: Union[str, np.ndarray, Tensor]
53
+ duration: float
54
+ text: Optional[str] = None
55
+ tokens: Optional[List[int]] = None
56
+
57
+
58
+ @dataclass
59
+ class Word:
60
+ text: str
61
+ start: float
62
+ end: float
63
+
64
+
65
+ @dataclass
66
+ class TranscriptionResult:
67
+ text: str
68
+ words: Optional[List[Word]] = None
69
+
70
+ def __str__(self) -> str:
71
+ return self.text
72
+
73
+
74
+ @dataclass
75
+ class Segment:
76
+ text: str
77
+ start: float
78
+ end: float
79
+ words: Optional[List[Word]] = None
80
+
81
+
82
+ @dataclass
83
+ class LongformTranscriptionResult:
84
+ segments: List[Segment]
85
+
86
+ @property
87
+ def words(self) -> List[Word]:
88
+ """Flatten all words from all segments."""
89
+ result = []
90
+ for seg in self.segments:
91
+ if seg.words:
92
+ result.extend(seg.words)
93
+ return result
94
+
95
+ @property
96
+ def has_word_timestamps(self) -> bool:
97
+ return bool(self.segments) and self.segments[0].words is not None
98
+
99
+ @property
100
+ def text(self) -> str:
101
+ return " ".join(s.text for s in self.segments)
102
+
103
+ def __str__(self) -> str:
104
+ return self.text
105
+
106
+ def __iter__(self):
107
+ return iter(self.segments)
108
+
109
+ def __len__(self) -> int:
110
+ return len(self.segments)
111
+
112
+
113
+ # ==== gigaam/preprocess.py ====
114
+
115
+ SAMPLE_RATE = 16000
116
+
117
+
118
+ def load_audio(audio_path: str, sample_rate: int = SAMPLE_RATE) -> Tensor:
119
+ """
120
+ Load an audio file and resample it to the specified sample rate.
121
+ """
122
+ cmd = [
123
+ "ffmpeg",
124
+ "-nostdin",
125
+ "-threads",
126
+ "0",
127
+ "-i",
128
+ audio_path,
129
+ "-f",
130
+ "s16le",
131
+ "-ac",
132
+ "1",
133
+ "-acodec",
134
+ "pcm_s16le",
135
+ "-ar",
136
+ str(sample_rate),
137
+ "-",
138
+ ]
139
+ try:
140
+ audio = run(cmd, capture_output=True, check=True).stdout
141
+ except CalledProcessError as exc:
142
+ raise RuntimeError("Failed to load audio") from exc
143
+
144
+ with warnings.catch_warnings():
145
+ warnings.simplefilter("ignore", category=UserWarning)
146
+ return torch.frombuffer(audio, dtype=torch.int16).float() / 32768.0
147
+
148
+
149
+ class SpecScaler(nn.Module):
150
+ """
151
+ Module that applies logarithmic scaling to spectrogram values.
152
+ This module clamps the input values within a certain range and then applies a natural logarithm.
153
+ """
154
+
155
+ def forward(self, x: Tensor) -> Tensor:
156
+ return torch.log(x.clamp_(1e-9, 1e9))
157
+
158
+
159
+ class FeatureExtractor(nn.Module):
160
+ """
161
+ Module for extracting Log-mel spectrogram features from raw audio signals.
162
+ This module uses Torchaudio's MelSpectrogram transform to extract features
163
+ and applies logarithmic scaling.
164
+ """
165
+
166
+ def __init__(self, sample_rate: int, features: int, **kwargs):
167
+ super().__init__()
168
+ self.hop_length = kwargs.get("hop_length", sample_rate // 100)
169
+ self.win_length = kwargs.get("win_length", sample_rate // 40)
170
+ self.n_fft = kwargs.get("n_fft", sample_rate // 40)
171
+ self.center = kwargs.get("center", True)
172
+ self.featurizer = nn.Sequential(
173
+ torchaudio.transforms.MelSpectrogram(
174
+ sample_rate=sample_rate,
175
+ n_mels=features,
176
+ win_length=self.win_length,
177
+ hop_length=self.hop_length,
178
+ n_fft=self.n_fft,
179
+ center=self.center,
180
+ ),
181
+ SpecScaler(),
182
+ )
183
+
184
+ def out_len(self, input_lengths: Tensor) -> Tensor:
185
+ """
186
+ Calculates the output length after the feature extraction process.
187
+ """
188
+ if self.center:
189
+ return (
190
+ input_lengths.div(self.hop_length, rounding_mode="floor").add(1).long()
191
+ )
192
+ else:
193
+ return (
194
+ (input_lengths - self.win_length)
195
+ .div(self.hop_length, rounding_mode="floor")
196
+ .add(1)
197
+ .long()
198
+ )
199
+
200
+ def forward(self, input_signal: Tensor, length: Tensor) -> Tuple[Tensor, Tensor]:
201
+ """
202
+ Extract Log-mel spectrogram features from the input audio signal.
203
+ """
204
+ return self.featurizer(input_signal), self.out_len(length)
205
+
206
+
207
+ # ==== gigaam/utils.py ====
208
+
209
+
210
+ def normalize_raw_text(text: str) -> str:
211
+ """
212
+ Script-agnostic normalization: lowercase, collapse whitespace,
213
+ and keep only alphanumeric characters plus spaces — i.e. drop punctuation/symbols.
214
+
215
+ Word-internal apostrophes are preserved (e.g. ``don't`` stays ``don't``).
216
+ Apostrophe variants are normalized to ASCII ``'``.
217
+ Standalone quotes are dropped (not between two alphanumerics).
218
+ Hyphens/dashes (Unicode category Pd) split words (``word-internal`` -> ``word internal``).
219
+ """
220
+ text = text.replace("ё", "е").replace("Ё", "Е").lower()
221
+ for quote in ("’", "‘", "ʻ", "ʼ"):
222
+ text = text.replace(quote, "'")
223
+ out = []
224
+ for i, c in enumerate(text):
225
+ if c == "'":
226
+ if (
227
+ 0 < i < len(text) - 1
228
+ and text[i - 1].isalnum()
229
+ and text[i + 1].isalnum()
230
+ ):
231
+ out.append(c)
232
+ elif c.isalnum() or c.isspace():
233
+ out.append(c)
234
+ elif unicodedata.category(c) == "Pd":
235
+ out.append(" ")
236
+ return " ".join("".join(out).split())
237
+
238
+
239
+ def onnx_converter(
240
+ model_name: str,
241
+ module: torch.nn.Module,
242
+ out_dir: str,
243
+ inputs: Optional[Tuple[Tensor, ...]] = None,
244
+ input_names: Optional[List[str]] = None,
245
+ output_names: Optional[List[str]] = None,
246
+ dynamic_axes: Optional[
247
+ Union[Dict[str, List[int]], Dict[str, Dict[int, str]]]
248
+ ] = None,
249
+ opset_version: int = 17,
250
+ export_dtype: torch.dtype = torch.float32,
251
+ ):
252
+ """
253
+ Export a submodule to ONNX: casts inputs and ``module`` to ``export_dtype`` for tracing,
254
+ then restores the module to float32 via ``module.float()`` so the model stays usable.
255
+ """
256
+ if inputs is None:
257
+ inputs = module.input_example() # type: ignore[operator]
258
+ if input_names is None:
259
+ input_names = module.input_names() # type: ignore[operator]
260
+ if output_names is None:
261
+ output_names = module.output_names() # type: ignore[operator]
262
+
263
+ inputs = tuple(
264
+ x.to(export_dtype) if x.dtype == torch.float32 else x for x in inputs
265
+ )
266
+
267
+ Path(out_dir).mkdir(exist_ok=True, parents=True)
268
+ out_path = str(Path(out_dir) / f"{model_name}.onnx")
269
+ with warnings.catch_warnings(), torch.no_grad():
270
+ warnings.simplefilter("ignore", category=UserWarning)
271
+ warnings.simplefilter("ignore", category=TracerWarning)
272
+ torch.onnx.export(
273
+ module.to(export_dtype),
274
+ inputs,
275
+ out_path,
276
+ input_names=input_names,
277
+ output_names=output_names,
278
+ dynamic_axes=dynamic_axes,
279
+ opset_version=opset_version,
280
+ dynamo=False,
281
+ )
282
+ print(f"Successfully ported onnx {model_name} to {out_path}.")
283
+ # We force the whole module to float32 to avoid fp16 preprocessing issues
284
+ module.float()
285
+
286
+
287
+ def format_time(seconds: float) -> str:
288
+ """
289
+ Formats time in seconds to HH:MM:SS:mm format.
290
+ """
291
+ hours = int(seconds // 3600)
292
+ minutes = int((seconds % 3600) // 60)
293
+ seconds = seconds % 60
294
+ full_seconds = int(seconds)
295
+ milliseconds = int((seconds - full_seconds) * 100)
296
+
297
+ if hours > 0:
298
+ return f"{hours:02}:{minutes:02}:{full_seconds:02}:{milliseconds:02}"
299
+ return f"{minutes:02}:{full_seconds:02}:{milliseconds:02}"
300
+
301
+
302
+ def rtt_half(x: Tensor) -> Tensor:
303
+ x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :]
304
+ return torch.cat([-x2, x1], dim=x1.ndim - 1)
305
+
306
+
307
+ def apply_rotary_pos_emb(
308
+ q: Tensor, k: Tensor, cos: Tensor, sin: Tensor, offset: int = 0
309
+ ) -> Tuple[Tensor, Tensor]:
310
+ """
311
+ Applies Rotary Position Embeddings to query and key tensors.
312
+ """
313
+ cos, sin = (
314
+ cos[offset : q.shape[0] + offset, ...],
315
+ sin[offset : q.shape[0] + offset, ...],
316
+ )
317
+ cos = cos.to(dtype=q.dtype)
318
+ sin = sin.to(dtype=q.dtype)
319
+ return (q * cos) + (rtt_half(q) * sin), (k * cos) + (rtt_half(k) * sin)
320
+
321
+
322
+ def apply_masked_flash_attn(
323
+ q: Tensor,
324
+ k: Tensor,
325
+ v: Tensor,
326
+ mask: Tensor,
327
+ h: int,
328
+ d_k: int,
329
+ ) -> Tensor:
330
+ """
331
+ Applies Flash Attention with padding masks.
332
+ """
333
+
334
+ try:
335
+ from einops import rearrange
336
+ from flash_attn import flash_attn_varlen_func
337
+ from flash_attn.bert_padding import pad_input, unpad_input
338
+ except ImportError as err:
339
+ raise RuntimeError("flash_attn and einops are required") from err
340
+
341
+ pad_mask = ~mask[:, 0, :]
342
+ b, t = pad_mask.shape
343
+ q = q.view(b, t, h * d_k)
344
+ k = k.view(b, t, h * d_k)
345
+ v = v.view(b, t, h * d_k)
346
+
347
+ q_unpad, indices_q, _, max_seqlen_q = unpad_input(q, pad_mask)[:4]
348
+ q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h)
349
+
350
+ k_unpad = unpad_input(k, pad_mask)[0]
351
+ k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h)
352
+
353
+ v_unpad = unpad_input(v, pad_mask)[0]
354
+ v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h)
355
+
356
+ lengths_q = pad_mask.sum(1).to(torch.int32).to(q.device)
357
+ cu_seqlens_q = F.pad(lengths_q.cumsum(0), (1, 0), value=0).to(torch.int32)
358
+ max_seqlen_q = torch.max(lengths_q)
359
+
360
+ output_unpad = flash_attn_varlen_func(
361
+ q_unpad,
362
+ k_unpad,
363
+ v_unpad,
364
+ cu_seqlens_q,
365
+ cu_seqlens_q,
366
+ max_seqlen_q,
367
+ max_seqlen_q,
368
+ )
369
+
370
+ scores = pad_input(
371
+ rearrange(output_unpad, "nnz h d -> nnz (h d)"),
372
+ indices_q,
373
+ b,
374
+ t,
375
+ )
376
+
377
+ return scores
378
+
379
+
380
+ def download_short_audio() -> str:
381
+ """Download test audio file if not exists"""
382
+ audio_file = "example.wav"
383
+ if not os.path.exists(audio_file):
384
+ os.system(
385
+ 'wget -O example.wav "https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/example.wav"'
386
+ )
387
+ assert os.path.exists(audio_file), "Short audio file not found"
388
+ return audio_file
389
+
390
+
391
+ def download_long_audio() -> str:
392
+ """Download test audio file if not exists"""
393
+ audio_file = "long_example.wav"
394
+ if not os.path.exists(audio_file):
395
+ os.system(
396
+ 'wget -O long_example.wav "https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/long_example.wav"'
397
+ )
398
+ assert os.path.exists(audio_file), "Long audio file not found"
399
+ return audio_file
400
+
401
+
402
+ class AudioDataset(torch.utils.data.Dataset):
403
+ """
404
+ Unified dataset class for training and inference.
405
+ Supports loading from manifest file or an iterable of audio paths / waveforms.
406
+ Provides min / max duration filtering, text normalization, and pre-tokenization.
407
+ """
408
+
409
+ def __init__(
410
+ self,
411
+ data: Union[str, Iterable[Union[str, np.ndarray, torch.Tensor]]],
412
+ tokenizer=None,
413
+ max_duration: Optional[float] = None,
414
+ min_duration: float = 0.0,
415
+ raw_text: bool = False,
416
+ return_tokens: bool = False,
417
+ ):
418
+ self.raw_text = raw_text
419
+ self.return_tokens = return_tokens
420
+ self.tokenizer = tokenizer
421
+ self.samples: List[AudioDatasetSample] = []
422
+
423
+ if return_tokens and tokenizer is None:
424
+ raise ValueError("tokenizer is required when return_tokens=True")
425
+
426
+ self.encode = self._make_encoder(tokenizer)
427
+
428
+ if isinstance(data, str):
429
+ self._load_manifest(data, min_duration, max_duration)
430
+ elif isinstance(data, Iterable) and not isinstance(
431
+ data, (str, bytes, bytearray)
432
+ ):
433
+ self._load_iterable(data, min_duration, max_duration)
434
+ else:
435
+ raise TypeError(f"Unsupported data type: {type(data)}")
436
+
437
+ if not self.samples:
438
+ raise ValueError("No valid samples found after filtering")
439
+
440
+ def _make_encoder(self, tokenizer):
441
+ if tokenizer is None:
442
+ return None
443
+
444
+ if getattr(tokenizer, "charwise", False):
445
+ c2i = {c: i for i, c in enumerate(tokenizer.vocab)}
446
+ return lambda text: [c2i[c] for c in text if c in c2i]
447
+
448
+ return tokenizer.model.encode
449
+
450
+ def normalize_text(self, text: str) -> str:
451
+ if not self.raw_text:
452
+ return text
453
+
454
+ text = normalize_raw_text(text)
455
+
456
+ if self.tokenizer is not None and getattr(self.tokenizer, "charwise", False):
457
+ vocab = set(self.tokenizer.vocab)
458
+ return "".join(c for c in text if c in vocab)
459
+
460
+ return text
461
+
462
+ @staticmethod
463
+ def _get_duration(item: Union[str, np.ndarray, Tensor]) -> float:
464
+ if isinstance(item, str):
465
+ with sf.SoundFile(item) as f:
466
+ return f.frames / f.samplerate
467
+ if isinstance(item, np.ndarray):
468
+ return len(item) / SAMPLE_RATE
469
+ if isinstance(item, torch.Tensor):
470
+ return item.numel() / SAMPLE_RATE
471
+ raise TypeError(f"Unexpected sample type: {type(item)}")
472
+
473
+ def _duration_ok(
474
+ self, duration: float, min_duration: float, max_duration: Optional[float]
475
+ ) -> bool:
476
+ if duration < min_duration:
477
+ return False
478
+ if max_duration is not None and duration > max_duration:
479
+ return False
480
+ return True
481
+
482
+ @staticmethod
483
+ def _print_filtered(
484
+ n_total: int, dur_total: float, n_filt: int, dur_filt: float
485
+ ) -> None:
486
+ if n_total == 0:
487
+ return
488
+ pn = 100.0 * n_filt / n_total
489
+ pd = 100.0 * dur_filt / dur_total if dur_total > 0 else 0.0
490
+ h_filt, h_total = dur_filt / 3600.0, dur_total / 3600.0
491
+ print(
492
+ f"filtered by duration: {n_filt}/{n_total} samples ({pn:.1f}%), "
493
+ f"{h_filt:.2f}/{h_total:.2f} h ({pd:.1f}%)"
494
+ )
495
+
496
+ def _append_sample(
497
+ self,
498
+ item: Union[str, np.ndarray, Tensor],
499
+ duration: float,
500
+ text: Optional[str] = None,
501
+ ) -> None:
502
+ norm_text: Optional[str] = None
503
+ tokens: Optional[List[int]] = None
504
+ if text is not None:
505
+ norm_text = self.normalize_text(text.strip())
506
+ if self.return_tokens:
507
+ assert self.encode is not None
508
+ tokens = self.encode(norm_text)
509
+ self.samples.append(
510
+ AudioDatasetSample(
511
+ item=item, duration=duration, text=norm_text, tokens=tokens
512
+ )
513
+ )
514
+
515
+ def _load_manifest(
516
+ self, manifest_path: str, min_duration: float, max_duration: Optional[float]
517
+ ):
518
+ data_dir = Path(manifest_path).resolve().parent
519
+ n_total = n_filt = 0
520
+ dur_total = dur_filt = 0.0
521
+
522
+ with open(manifest_path) as f:
523
+ for row in csv.DictReader(f, delimiter="\t"):
524
+ duration = float(row["duration"])
525
+ n_total += 1
526
+ dur_total += duration
527
+ if not self._duration_ok(duration, min_duration, max_duration):
528
+ n_filt += 1
529
+ dur_filt += duration
530
+ continue
531
+
532
+ pth = Path(row["path"])
533
+ path = str((pth if pth.is_absolute() else data_dir / pth).resolve())
534
+ text = row["transcription"] if "transcription" in row else None
535
+ self._append_sample(path, duration, text=text)
536
+
537
+ self._print_filtered(n_total, dur_total, n_filt, dur_filt)
538
+
539
+ def _load_iterable(
540
+ self,
541
+ data: Iterable[Union[str, np.ndarray, torch.Tensor]],
542
+ min_duration: float,
543
+ max_duration: Optional[float],
544
+ ):
545
+ n_total = n_filt = 0
546
+ dur_total = dur_filt = 0.0
547
+ for item in data:
548
+ if not isinstance(item, (str, np.ndarray, torch.Tensor)):
549
+ raise TypeError(f"Unexpected dtype: {type(item)}")
550
+
551
+ duration = self._get_duration(item)
552
+ n_total += 1
553
+ dur_total += duration
554
+ if not self._duration_ok(duration, min_duration, max_duration):
555
+ n_filt += 1
556
+ dur_filt += duration
557
+ continue
558
+
559
+ self._append_sample(item, duration)
560
+
561
+ self._print_filtered(n_total, dur_total, n_filt, dur_filt)
562
+
563
+ def __len__(self) -> int:
564
+ return len(self.samples)
565
+
566
+ @staticmethod
567
+ def _load_audio(item: Union[str, np.ndarray, Tensor]) -> Tensor:
568
+ if isinstance(item, str):
569
+ wav, sr = torchaudio.load(item)
570
+ if wav.shape[0] > 1:
571
+ wav = wav.mean(dim=0, keepdim=True)
572
+ wav = wav.squeeze(0)
573
+ if sr != SAMPLE_RATE:
574
+ wav = torchaudio.functional.resample(wav, sr, SAMPLE_RATE)
575
+ return wav
576
+ if isinstance(item, np.ndarray):
577
+ return torch.from_numpy(item)
578
+ if isinstance(item, torch.Tensor):
579
+ return item
580
+ raise TypeError(f"Unexpected sample type: {type(item)}")
581
+
582
+ def __getitem__(self, idx: int) -> Union[Tensor, Tuple[Tensor, Tensor]]:
583
+ sample = self.samples[idx]
584
+ wav = self._load_audio(sample.item)
585
+
586
+ if self.return_tokens:
587
+ assert sample.tokens is not None
588
+ return wav, torch.tensor(sample.tokens, dtype=torch.long)
589
+
590
+ return wav
591
+
592
+ @staticmethod
593
+ def collate(wavs: List[Tensor]) -> Tuple[Tensor, Tensor]:
594
+ lengths = torch.tensor([len(w) for w in wavs], dtype=torch.long)
595
+ max_len = int(lengths.max().item())
596
+
597
+ batch = torch.zeros(len(wavs), max_len, dtype=wavs[0].dtype)
598
+ for i, wav in enumerate(wavs):
599
+ batch[i, : wav.shape[-1]] = wav.squeeze()
600
+
601
+ return batch, lengths
602
+
603
+ def collate_fn(
604
+ self, batch: List[Union[Tensor, Tuple[Tensor, Tensor]]]
605
+ ) -> Union[Tuple[Tensor, Tensor], Tuple[Tensor, Tensor, Tensor, Tensor]]:
606
+ if not self.return_tokens:
607
+ return self.collate(cast(List[Tensor], batch))
608
+
609
+ wavs, tokens = zip(*cast(List[Tuple[Tensor, Tensor]], batch))
610
+ wav_pad, wav_lens = self.collate(list(wavs))
611
+ tok_pad, tok_lens = self.collate(list(tokens))
612
+
613
+ return wav_pad, wav_lens, tok_pad, tok_lens
614
+
615
+
616
+ # ==== gigaam/timestamps_utils.py ====
617
+
618
+
619
+ def compute_frame_shift(audio_length_samples: int, seq_len: int) -> float:
620
+ """Compute frame shift (seconds per encoder frame)."""
621
+ return audio_length_samples / SAMPLE_RATE / seq_len
622
+
623
+
624
+ def frames_to_words(
625
+ tokenizer: Tokenizer,
626
+ token_ids: List[int],
627
+ token_frames: List[int],
628
+ frame_shift: float,
629
+ ) -> List[Word]:
630
+ """
631
+ Convert token-level frame indices to word-level timestamps.
632
+ Groups tokens into words at word boundaries (space or sentencepiece '▁' prefix).
633
+ """
634
+ words: List[Word] = []
635
+ current_chars: List[str] = []
636
+ current_frames: List[int] = []
637
+
638
+ def commit():
639
+ if not current_chars:
640
+ return
641
+ text = "".join(current_chars).strip()
642
+ if not text:
643
+ current_chars.clear()
644
+ current_frames.clear()
645
+ return
646
+ start = current_frames[0] * frame_shift
647
+ end = (current_frames[-1] + 1) * frame_shift
648
+ words.append(Word(text=text, start=start, end=end))
649
+ current_chars.clear()
650
+ current_frames.clear()
651
+
652
+ for token_id, frame in zip(token_ids, token_frames):
653
+ char = tokenizer.id_to_str(token_id)
654
+ if char.startswith("▁"):
655
+ commit()
656
+ char = char[1:]
657
+ elif char == " ":
658
+ commit()
659
+ continue
660
+ current_chars.append(char)
661
+ current_frames.append(frame)
662
+
663
+ commit()
664
+ return words
665
+
666
+
667
+ # ==== gigaam/vad_utils.py ====
668
+
669
+ _PIPELINE = None
670
+
671
+
672
+ def resolve_local_segmentation_path(model_id: str) -> str:
673
+ """
674
+ Finds the local path to the segmentation model.
675
+ """
676
+ try:
677
+ return snapshot_download(
678
+ repo_id=model_id,
679
+ local_files_only=True,
680
+ )
681
+ except LocalEntryNotFoundError:
682
+ pass
683
+
684
+ hf_token = os.getenv("HF_TOKEN")
685
+ if not hf_token:
686
+ raise RuntimeError(
687
+ f"Model {model_id} was not found locally, "
688
+ f"and no HF_TOKEN was provided to download it."
689
+ )
690
+
691
+ return snapshot_download(
692
+ repo_id=model_id,
693
+ token=hf_token,
694
+ )
695
+
696
+
697
+ def load_segmentation_model(model_id: str) -> Model:
698
+ """
699
+ Loads the segmentation model from a local snapshot.
700
+ If it doesn’t exist, it first creates (downloads) the snapshot.
701
+ """
702
+ from pyannote.audio import Model
703
+ from pyannote.audio.core.task import Problem, Resolution, Specifications
704
+
705
+ local_path = resolve_local_segmentation_path(model_id=model_id)
706
+
707
+ with torch.serialization.safe_globals(
708
+ [
709
+ TorchVersion,
710
+ Problem,
711
+ Specifications,
712
+ Resolution,
713
+ ]
714
+ ):
715
+ return Model.from_pretrained(local_path)
716
+
717
+
718
+ def get_pipeline(
719
+ device: torch.device, model_id: str = "pyannote/segmentation-3.0"
720
+ ) -> Pipeline:
721
+ """
722
+ Retrieves a PyAnnote voice activity detection pipeline and moves it to the specified device.
723
+ The pipeline is loaded only once and reused across subsequent calls.
724
+ It requires the Hugging Face API token to be set in the HF_TOKEN environment variable.
725
+ """
726
+ from pyannote.audio.pipelines import VoiceActivityDetection
727
+
728
+ global _PIPELINE
729
+ if _PIPELINE is not None:
730
+ return _PIPELINE.to(device)
731
+
732
+ model = load_segmentation_model(model_id=model_id)
733
+
734
+ _PIPELINE = VoiceActivityDetection(segmentation=model)
735
+ _PIPELINE.instantiate({"min_duration_on": 0.0, "min_duration_off": 0.0})
736
+
737
+ return _PIPELINE.to(device)
738
+
739
+
740
+ def segment_audio_file(
741
+ wav_file: str,
742
+ sr: int,
743
+ max_duration: float = 22.0,
744
+ min_duration: float = 15.0,
745
+ strict_limit_duration: float = 30.0,
746
+ new_chunk_threshold: float = 0.2,
747
+ device: torch.device = torch.device("cpu"),
748
+ ) -> Tuple[List[torch.Tensor], List[Tuple[float, float]]]:
749
+ """
750
+ Segments an audio waveform into smaller chunks based on speech activity.
751
+ The segmentation is performed using a PyAnnote voice activity detection pipeline.
752
+ """
753
+ from pyannote.core import Annotation
754
+
755
+ audio = load_audio(wav_file)
756
+ pipeline = get_pipeline(device)
757
+ sad_segments = cast(Annotation, pipeline(wav_file))
758
+
759
+ segments: List[torch.Tensor] = []
760
+ curr_duration = 0.0
761
+ curr_start = 0.0
762
+ curr_end = 0.0
763
+ boundaries: List[Tuple[float, float]] = []
764
+
765
+ def _update_segments(curr_start: float, curr_end: float, curr_duration: float):
766
+ if curr_duration > strict_limit_duration:
767
+ max_segments = int(curr_duration / strict_limit_duration) + 1
768
+ segment_duration = curr_duration / max_segments
769
+ curr_end = curr_start + segment_duration
770
+ for _ in range(max_segments - 1):
771
+ segments.append(audio[int(curr_start * sr) : int(curr_end * sr)])
772
+ boundaries.append((curr_start, curr_end))
773
+ curr_start = curr_end
774
+ curr_end += segment_duration
775
+ segments.append(audio[int(curr_start * sr) : int(curr_end * sr)])
776
+ boundaries.append((curr_start, curr_end))
777
+
778
+ # Concat segments from pipeline into chunks for asr according to max/min duration
779
+ # Segments longer than strict_limit_duration are split manually
780
+ for segment in sad_segments.get_timeline().support():
781
+ start = max(0, segment.start)
782
+ end = min(audio.shape[0] / sr, segment.end)
783
+ if curr_duration == 0.0:
784
+ curr_start = start
785
+ elif curr_duration > new_chunk_threshold and (
786
+ curr_duration + (end - curr_end) > max_duration
787
+ or curr_duration > min_duration
788
+ ):
789
+ _update_segments(curr_start, curr_end, curr_duration)
790
+ curr_start = start
791
+ curr_end = end
792
+ curr_duration = curr_end - curr_start
793
+
794
+ if curr_duration > new_chunk_threshold:
795
+ _update_segments(curr_start, curr_end, curr_duration)
796
+
797
+ return segments, boundaries
798
+
799
+
800
+ # ==== gigaam/decoder.py ====
801
+
802
+
803
+ class CTCHead(nn.Module):
804
+ """
805
+ CTC Head module for Connectionist Temporal Classification.
806
+ """
807
+
808
+ def __init__(self, feat_in: int, num_classes: int):
809
+ super().__init__()
810
+ self.decoder_layers = torch.nn.Sequential(
811
+ torch.nn.Conv1d(feat_in, num_classes, kernel_size=1)
812
+ )
813
+
814
+ def forward(self, encoder_output: Tensor) -> Tensor:
815
+ return torch.nn.functional.log_softmax(
816
+ self.decoder_layers(encoder_output).transpose(1, 2), dim=-1
817
+ )
818
+
819
+
820
+ class RNNTJoint(nn.Module):
821
+ """
822
+ RNN-Transducer Joint Network Module.
823
+ This module combines the outputs of the encoder and the prediction network using
824
+ a linear transformation followed by ReLU activation and another linear projection.
825
+ """
826
+
827
+ def __init__(
828
+ self, enc_hidden: int, pred_hidden: int, joint_hidden: int, num_classes: int
829
+ ):
830
+ super().__init__()
831
+ self.enc_hidden = enc_hidden
832
+ self.pred_hidden = pred_hidden
833
+ self.pred = nn.Linear(pred_hidden, joint_hidden)
834
+ self.enc = nn.Linear(enc_hidden, joint_hidden)
835
+ self.joint_net = nn.Sequential(nn.ReLU(), nn.Linear(joint_hidden, num_classes))
836
+
837
+ def joint(self, encoder_out: Tensor, decoder_out: Tensor) -> Tensor:
838
+ """
839
+ Combine the encoder and prediction network outputs into a joint representation.
840
+ """
841
+ enc = self.enc(encoder_out).unsqueeze(2)
842
+ pred = self.pred(decoder_out).unsqueeze(1)
843
+ return self.joint_net(enc + pred).log_softmax(-1)
844
+
845
+ def input_example(self, batch_size: int = 8) -> Tuple[Tensor, Tensor]:
846
+ device = next(self.parameters()).device
847
+ enc = torch.zeros(batch_size, self.enc_hidden, 1)
848
+ dec = torch.zeros(batch_size, self.pred_hidden, 1)
849
+ return enc.float().to(device), dec.float().to(device)
850
+
851
+ def input_names(self) -> List[str]:
852
+ return ["enc", "dec"]
853
+
854
+ def output_names(self) -> List[str]:
855
+ return ["joint"]
856
+
857
+ def dynamic_axes(self) -> Dict[str, Dict[int, str]]:
858
+ return {
859
+ "enc": {0: "batch_size"},
860
+ "dec": {0: "batch_size"},
861
+ "joint": {0: "batch_size"},
862
+ }
863
+
864
+ def forward(self, enc: Tensor, dec: Tensor) -> Tensor:
865
+ return self.joint(enc.transpose(1, 2), dec.transpose(1, 2))
866
+
867
+
868
+ class RNNTDecoder(nn.Module):
869
+ """
870
+ RNN-Transducer Decoder Module.
871
+ This module handles the prediction network part of the RNN-Transducer architecture.
872
+ """
873
+
874
+ def __init__(self, pred_hidden: int, pred_rnn_layers: int, num_classes: int):
875
+ super().__init__()
876
+ self.blank_id = num_classes - 1
877
+ self.pred_hidden = pred_hidden
878
+ self.embed = nn.Embedding(num_classes, pred_hidden, padding_idx=self.blank_id)
879
+ self.lstm = nn.LSTM(pred_hidden, pred_hidden, pred_rnn_layers)
880
+
881
+ def predict(
882
+ self,
883
+ x: Optional[Tensor],
884
+ state: Optional[Tensor],
885
+ batch_size: int = 1,
886
+ ) -> Tuple[Tensor, Tensor]:
887
+ """
888
+ Make predictions based on the current input and previous states.
889
+ If no input is provided, use zeros as the initial input.
890
+ """
891
+ if x is not None:
892
+ emb: Tensor = self.embed(x)
893
+ else:
894
+ emb = torch.zeros(
895
+ (batch_size, 1, self.pred_hidden), device=next(self.parameters()).device
896
+ )
897
+ g, hid = self.lstm(emb.transpose(0, 1), state)
898
+ return g.transpose(0, 1), hid
899
+
900
+ def input_example(self, batch_size: int = 8) -> Tuple[Tensor, Tensor, Tensor]:
901
+ device = next(self.parameters()).device
902
+ label = torch.zeros(batch_size, 1, dtype=torch.long).to(device)
903
+ hidden_h = torch.zeros(self.lstm.num_layers, batch_size, self.pred_hidden).to(
904
+ device
905
+ )
906
+ hidden_c = torch.zeros(self.lstm.num_layers, batch_size, self.pred_hidden).to(
907
+ device
908
+ )
909
+ return label, hidden_h, hidden_c
910
+
911
+ def input_names(self) -> List[str]:
912
+ return ["x", "hi", "ci"]
913
+
914
+ def output_names(self) -> List[str]:
915
+ return ["dec", "ho", "co"]
916
+
917
+ def dynamic_axes(self) -> Dict[str, Dict[int, str]]:
918
+ return {
919
+ "x": {0: "batch_size"},
920
+ "hi": {1: "batch_size"},
921
+ "ci": {1: "batch_size"},
922
+ "dec": {0: "batch_size"},
923
+ "ho": {1: "batch_size"},
924
+ "co": {1: "batch_size"},
925
+ }
926
+
927
+ def forward(self, x: Tensor, h: Tensor, c: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
928
+ """
929
+ ONNX-specific forward with x, state = (h, c) -> x, h, c.
930
+ """
931
+ emb = self.embed(x)
932
+ g, (h, c) = self.lstm(emb.transpose(0, 1), (h, c))
933
+ return g.transpose(0, 1), h, c
934
+
935
+
936
+ class RNNTHead(nn.Module):
937
+ """
938
+ RNN-Transducer Head Module.
939
+ This module combines the decoder and joint network components of the RNN-Transducer architecture.
940
+ """
941
+
942
+ def __init__(self, decoder: Dict[str, int], joint: Dict[str, int]):
943
+ super().__init__()
944
+ self.decoder = RNNTDecoder(**decoder)
945
+ self.joint = RNNTJoint(**joint)
946
+
947
+
948
+ # ==== gigaam/decoding.py ====
949
+
950
+
951
+ class Tokenizer:
952
+ """
953
+ Tokenizer for converting between text and token IDs.
954
+ The tokenizer can operate either character-wise or using a pre-trained SentencePiece model.
955
+ """
956
+
957
+ def __init__(self, vocab: List[str], model_path: Optional[str] = None):
958
+ self.charwise = model_path is None
959
+ if self.charwise:
960
+ self.vocab = vocab
961
+ else:
962
+ self.model = SentencePieceProcessor()
963
+ self.model.load(model_path)
964
+
965
+ def decode(self, tokens: List[int]) -> str:
966
+ """
967
+ Convert a list of token IDs back to a string.
968
+ """
969
+ if self.charwise:
970
+ return "".join(self.vocab[tok] for tok in tokens)
971
+ return self.model.decode(tokens)
972
+
973
+ def __len__(self):
974
+ """
975
+ Get the total number of tokens in the vocabulary.
976
+ """
977
+ return len(self.vocab) if self.charwise else len(self.model)
978
+
979
+ def id_to_str(self, token_id: int) -> str:
980
+ """
981
+ Convert a single token ID to its string representation.
982
+ """
983
+ if self.charwise:
984
+ return self.vocab[token_id]
985
+ return self.model.IdToPiece(token_id)
986
+
987
+
988
+ class CTCGreedyDecoding:
989
+ """
990
+ Class for performing greedy decoding of CTC outputs.
991
+ """
992
+
993
+ def __init__(self, vocabulary: List[str], model_path: Optional[str] = None):
994
+ self.tokenizer = Tokenizer(vocabulary, model_path)
995
+ self.blank_id = len(self.tokenizer)
996
+
997
+ @torch.inference_mode()
998
+ def decode(
999
+ self,
1000
+ head: "CTCHead",
1001
+ encoded: Tensor,
1002
+ lengths: Tensor,
1003
+ ) -> List[Tuple[str, List[int], List[int]]]:
1004
+ """
1005
+ CTC greedy decode: returns (text, token_ids, token_frames) per sample.
1006
+ Token frames are time indices (0..T-1) where a token is emitted.
1007
+ """
1008
+ log_probs = head(encoder_output=encoded)
1009
+ C = log_probs.shape[-1]
1010
+ assert (
1011
+ C == len(self.tokenizer) + 1
1012
+ ), f"Num classes {C} != len(vocab)+1 {len(self.tokenizer) + 1}"
1013
+ labels = log_probs.argmax(dim=-1)
1014
+
1015
+ B, T = labels.shape
1016
+ device = labels.device
1017
+ lengths = lengths.to(device=device).clamp(min=0, max=T)
1018
+
1019
+ skip_mask = labels != self.blank_id
1020
+ skip_mask[:, 1:] &= labels[:, 1:] != labels[:, :-1]
1021
+
1022
+ time = torch.arange(T, device=device)[None, :]
1023
+ skip_mask &= time < lengths[:, None]
1024
+
1025
+ idx = skip_mask.nonzero(as_tuple=False)
1026
+ batch_idx = idx[:, 0]
1027
+ token_frames_flat = idx[:, 1]
1028
+ token_ids_flat = labels[skip_mask]
1029
+
1030
+ counts = torch.bincount(batch_idx, minlength=B).cpu().tolist()
1031
+ ids_splits = token_ids_flat.cpu().split(counts)
1032
+ fr_splits = token_frames_flat.cpu().split(counts)
1033
+
1034
+ return [
1035
+ (self.tokenizer.decode(ids.tolist()), ids.tolist(), fr.tolist())
1036
+ for ids, fr in zip(ids_splits, fr_splits)
1037
+ ]
1038
+
1039
+
1040
+ class RNNTGreedyDecoding:
1041
+ """
1042
+ Class for performing greedy decoding of RNN-T outputs.
1043
+ """
1044
+
1045
+ def __init__(
1046
+ self,
1047
+ vocabulary: List[str],
1048
+ model_path: Optional[str] = None,
1049
+ max_symbols_per_step: int = 10,
1050
+ ):
1051
+ self.tokenizer = Tokenizer(vocabulary, model_path)
1052
+ self.blank_id = len(self.tokenizer)
1053
+ self.max_symbols = max_symbols_per_step
1054
+
1055
+ @staticmethod
1056
+ def _cat_states(states):
1057
+ """Pack per-sample LSTM states into batched (h, c)."""
1058
+ hs = [s[0] for s in states]
1059
+ cs = [s[1] for s in states]
1060
+ return torch.cat(hs, dim=1), torch.cat(cs, dim=1)
1061
+
1062
+ @staticmethod
1063
+ def _split_state(state):
1064
+ """Unpack batched (h, c) into per-sample states."""
1065
+ h, c = state
1066
+ b = h.shape[1]
1067
+ return [(h[:, i : i + 1], c[:, i : i + 1]) for i in range(b)]
1068
+
1069
+ @torch.inference_mode()
1070
+ def decode(
1071
+ self,
1072
+ head: "RNNTHead",
1073
+ encoded: Tensor,
1074
+ enc_len: Tensor,
1075
+ ) -> List[Tuple[str, List[int], List[int]]]:
1076
+ """
1077
+ RNN-T greedy decode: returns (text, token_ids, token_frames) per sample.
1078
+ Token frames are encoder time indices where tokens are emitted.
1079
+ """
1080
+ x = encoded.transpose(1, 2) # [B, T, D]
1081
+ B, T, _ = x.shape
1082
+ device = x.device
1083
+
1084
+ hyps: List[List[int]] = [[] for _ in range(B)]
1085
+ token_frames: List[List[int]] = [[] for _ in range(B)]
1086
+ last_label: List[Optional[Tensor]] = [None] * B
1087
+ dec_state: List[Optional[Tuple[Tensor, Tensor]]] = [None] * B
1088
+
1089
+ def emit_batch(batch_idx: List[int], t: int, fresh: bool) -> List[int]:
1090
+ """One batched predictor+joint step; returns samples that emitted non-blank."""
1091
+ idx = torch.tensor(batch_idx, device=device, dtype=torch.long)
1092
+ f = x[idx, t : t + 1, :] # [b, 1, D]
1093
+
1094
+ if fresh:
1095
+ g, hidden = head.decoder.predict(None, None, batch_size=len(batch_idx))
1096
+ else:
1097
+ labels = torch.cat([last_label[i] for i in batch_idx], dim=0) # [b, 1]
1098
+ state = self._cat_states([dec_state[i] for i in batch_idx])
1099
+ g, hidden = head.decoder.predict(
1100
+ labels, state, batch_size=len(batch_idx)
1101
+ )
1102
+
1103
+ k = head.joint.joint(f, g)[:, 0, 0, :].argmax(dim=-1) # [b]
1104
+ emit = k.ne(self.blank_id)
1105
+
1106
+ if not emit.any():
1107
+ return []
1108
+
1109
+ hidden_parts = self._split_state(hidden)
1110
+ out = []
1111
+
1112
+ for p in emit.nonzero(as_tuple=False).squeeze(1).tolist():
1113
+ bi = batch_idx[p]
1114
+ tok = int(k[p])
1115
+
1116
+ hyps[bi].append(tok)
1117
+ token_frames[bi].append(t)
1118
+ last_label[bi] = k[p : p + 1].view(1, 1)
1119
+ dec_state[bi] = hidden_parts[p]
1120
+ out.append(bi)
1121
+
1122
+ return out
1123
+
1124
+ enc_len = enc_len.cpu()
1125
+ for t in range(T):
1126
+ active = (t < enc_len).nonzero(as_tuple=False).squeeze(1).tolist()
1127
+ if not active:
1128
+ break
1129
+
1130
+ for _ in range(self.max_symbols):
1131
+ if not active:
1132
+ break
1133
+
1134
+ fresh = [i for i in active if dec_state[i] is None]
1135
+ stateful = [i for i in active if dec_state[i] is not None]
1136
+
1137
+ next_active = []
1138
+ if fresh:
1139
+ next_active.extend(emit_batch(fresh, t, fresh=True))
1140
+ if stateful:
1141
+ next_active.extend(emit_batch(stateful, t, fresh=False))
1142
+
1143
+ if not next_active:
1144
+ break
1145
+
1146
+ active = next_active
1147
+
1148
+ return [(self.tokenizer.decode(h), h, tf) for h, tf in zip(hyps, token_frames)]
1149
+
1150
+
1151
+ # ==== gigaam/encoder.py ====
1152
+
1153
+ try:
1154
+ from flash_attn import flash_attn_func
1155
+
1156
+ IMPORT_FLASH = True
1157
+ except Exception as err:
1158
+ IMPORT_FLASH = False
1159
+ IMPORT_FLASH_ERR = err
1160
+
1161
+
1162
+ def _conformer_layer_fwd(
1163
+ layer: nn.Module,
1164
+ x: Tensor,
1165
+ pos_emb: Union[Tensor, List[Tensor]],
1166
+ att_mask: Optional[Tensor],
1167
+ pad_mask: Optional[Tensor],
1168
+ ) -> Tensor:
1169
+ return layer(x=x, pos_emb=pos_emb, att_mask=att_mask, pad_mask=pad_mask)
1170
+
1171
+
1172
+ class StridingSubsampling(nn.Module):
1173
+ """
1174
+ Strided Subsampling layer used to reduce the sequence length.
1175
+ """
1176
+
1177
+ def __init__(
1178
+ self,
1179
+ subsampling: str,
1180
+ kernel_size: int,
1181
+ subsampling_factor: int,
1182
+ feat_in: int,
1183
+ feat_out: int,
1184
+ conv_channels: int,
1185
+ ):
1186
+ super().__init__()
1187
+ self.subsampling_type = subsampling
1188
+ assert self.subsampling_type in ["conv1d", "conv2d"]
1189
+ self._sampling_num = int(math.log(subsampling_factor, 2))
1190
+ self._stride = 2
1191
+ self._kernel_size = kernel_size
1192
+ self._padding = (self._kernel_size - 1) // 2
1193
+
1194
+ layers: List[nn.Module] = []
1195
+ in_channels = 1 if self.subsampling_type == "conv2d" else feat_in
1196
+ subs_conv_class = (
1197
+ torch.nn.Conv2d if self.subsampling_type == "conv2d" else torch.nn.Conv1d
1198
+ )
1199
+ for _ in range(self._sampling_num):
1200
+ layers.append(
1201
+ subs_conv_class(
1202
+ in_channels=in_channels,
1203
+ out_channels=conv_channels,
1204
+ kernel_size=self._kernel_size,
1205
+ stride=self._stride,
1206
+ padding=self._padding,
1207
+ )
1208
+ )
1209
+ layers.append(nn.ReLU())
1210
+ in_channels = conv_channels
1211
+
1212
+ out_length = self.calc_output_length(torch.tensor(feat_in))
1213
+ if self.subsampling_type == "conv2d":
1214
+ self.out = torch.nn.Linear(conv_channels * int(out_length), feat_out)
1215
+ self.conv = torch.nn.Sequential(*layers)
1216
+
1217
+ def calc_output_length(
1218
+ self, lengths: Tensor, num_stages: Optional[int] = None
1219
+ ) -> Tensor:
1220
+ """
1221
+ Valid length after applying ``num_stages`` strided subsampling conv
1222
+ stages (defaults to all of them, i.e. the full subsampling output).
1223
+ """
1224
+ if num_stages is None:
1225
+ num_stages = self._sampling_num
1226
+ add_pad = 2 * self._padding - self._kernel_size
1227
+ lengths = lengths.to(torch.float)
1228
+ for _ in range(num_stages):
1229
+ lengths = torch.floor((lengths + add_pad) / self._stride + 1.0)
1230
+ return lengths.to(dtype=torch.int)
1231
+
1232
+ def _mask_time(self, x: Tensor, lengths: Tensor) -> Tensor:
1233
+ """
1234
+ Zero out the padded tail along the time axis (dim 2). The subsampling
1235
+ convolutions are strided and have a receptive field wider than the
1236
+ stride, so the padded frames of shorter samples leak into the last
1237
+ valid frames. Left unmasked, the padding is the log-mel floor
1238
+ (``log(1e-9) ~= -20.7``) of zero-padded audio, not zero, so a batched
1239
+ short sample sees a different boundary than the same sample run alone
1240
+ (where conv zero-padding applies instead). Re-zeroing after every conv
1241
+ stage keeps the valid frames of batched inference aligned with the
1242
+ batch-size-1 result.
1243
+ """
1244
+ time = torch.arange(x.size(2), device=x.device)
1245
+ pad = time[None, :] >= lengths[:, None] # [b, t]
1246
+ pad = pad[:, None] # add channel dim -> [b, 1, t]
1247
+ if x.dim() == 4:
1248
+ pad = pad[..., None] # add feature dim for conv2d -> [b, 1, t, 1]
1249
+ return x.masked_fill(pad, 0.0)
1250
+
1251
+ def forward(self, x: Tensor, lengths: Tensor) -> Tuple[Tensor, Tensor]:
1252
+ if self.subsampling_type == "conv2d":
1253
+ x = x.unsqueeze(1)
1254
+ else:
1255
+ x = x.transpose(1, 2)
1256
+
1257
+ cur_len = lengths
1258
+ x = self._mask_time(x, cur_len)
1259
+ for module in self.conv:
1260
+ x = module(x)
1261
+ if isinstance(module, (torch.nn.Conv1d, torch.nn.Conv2d)):
1262
+ cur_len = self.calc_output_length(cur_len, 1)
1263
+ x = self._mask_time(x, cur_len)
1264
+
1265
+ if self.subsampling_type == "conv2d":
1266
+ b, _, t, _ = x.size()
1267
+ x = self.out(x.transpose(1, 2).reshape(b, t, -1))
1268
+ else:
1269
+ x = x.transpose(1, 2)
1270
+ return x, self.calc_output_length(lengths)
1271
+
1272
+
1273
+ class MultiHeadAttention(nn.Module, ABC):
1274
+ """
1275
+ Base class of Multi-Head Attention Mechanisms.
1276
+ """
1277
+
1278
+ def __init__(
1279
+ self, n_head: int, n_feat: int, flash_attn=False, torch_sdpa_attn=False
1280
+ ):
1281
+ super().__init__()
1282
+ assert n_feat % n_head == 0
1283
+ self.d_k = n_feat // n_head
1284
+ self.h = n_head
1285
+ self.linear_q = nn.Linear(n_feat, n_feat)
1286
+ self.linear_k = nn.Linear(n_feat, n_feat)
1287
+ self.linear_v = nn.Linear(n_feat, n_feat)
1288
+ self.linear_out = nn.Linear(n_feat, n_feat)
1289
+ self.flash_attn = flash_attn
1290
+ self.torch_sdpa_attn = torch_sdpa_attn
1291
+ if self.flash_attn and not IMPORT_FLASH:
1292
+ raise RuntimeError(
1293
+ f"flash_attn_func was imported with err {IMPORT_FLASH_ERR}. "
1294
+ "Please install flash_attn or use --no_flash flag. "
1295
+ "If you have already done this, "
1296
+ "--force-reinstall flag might be useful"
1297
+ )
1298
+
1299
+ def forward_qkv(
1300
+ self, query: Tensor, key: Tensor, value: Tensor
1301
+ ) -> Tuple[Tensor, Tensor, Tensor]:
1302
+ """
1303
+ Projects the inputs into queries, keys, and values for multi-head attention.
1304
+ """
1305
+ b = query.size(0)
1306
+ q = self.linear_q(query).view(b, -1, self.h, self.d_k)
1307
+ k = self.linear_k(key).view(b, -1, self.h, self.d_k)
1308
+ v = self.linear_v(value).view(b, -1, self.h, self.d_k)
1309
+ if self.flash_attn:
1310
+ return q, k, v
1311
+ return q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
1312
+
1313
+ def forward_attention(
1314
+ self, value: Tensor, scores: Tensor, mask: Optional[Tensor]
1315
+ ) -> Tensor:
1316
+ """
1317
+ Computes the scaled dot-product attention given the projected values and scores.
1318
+ """
1319
+ b = value.size(0)
1320
+ if mask is not None:
1321
+ mask = mask.unsqueeze(1)
1322
+ scores = scores.masked_fill(mask, -10000.0)
1323
+ attn = torch.softmax(scores, dim=-1).masked_fill(mask, 0.0)
1324
+ else:
1325
+ attn = torch.softmax(scores, dim=-1)
1326
+ x = torch.matmul(attn, value)
1327
+ x = x.transpose(1, 2).reshape(b, -1, self.h * self.d_k)
1328
+ return self.linear_out(x)
1329
+
1330
+
1331
+ class RelPositionMultiHeadAttention(MultiHeadAttention):
1332
+ """
1333
+ Relative Position Multi-Head Attention module.
1334
+ """
1335
+
1336
+ def __init__(self, n_head: int, n_feat: int):
1337
+ super().__init__(n_head, n_feat)
1338
+ self.linear_pos = nn.Linear(n_feat, n_feat, bias=False)
1339
+ self.pos_bias_u = nn.Parameter(torch.FloatTensor(self.h, self.d_k))
1340
+ self.pos_bias_v = nn.Parameter(torch.FloatTensor(self.h, self.d_k))
1341
+
1342
+ def rel_shift(self, x: Tensor) -> Tensor:
1343
+ b, h, qlen, pos_len = x.size()
1344
+ x = torch.nn.functional.pad(x, pad=(1, 0))
1345
+ x = x.view(b, h, -1, qlen)
1346
+ return x[:, :, 1:].view(b, h, qlen, pos_len)
1347
+
1348
+ def forward(
1349
+ self,
1350
+ query: Tensor,
1351
+ key: Tensor,
1352
+ value: Tensor,
1353
+ pos_emb: Tensor,
1354
+ mask: Optional[Tensor] = None,
1355
+ ) -> Tensor:
1356
+ q, k, v = self.forward_qkv(query, key, value)
1357
+ q = q.transpose(1, 2)
1358
+ pos_emb = pos_emb.to(dtype=self.linear_pos.weight.dtype)
1359
+ p = self.linear_pos(pos_emb)
1360
+ p = p.view(pos_emb.shape[0], -1, self.h, self.d_k).transpose(1, 2)
1361
+ q_with_bias_u = (q + self.pos_bias_u).transpose(1, 2)
1362
+ q_with_bias_v = (q + self.pos_bias_v).transpose(1, 2)
1363
+ matrix_bd = torch.matmul(q_with_bias_v, p.transpose(-2, -1))
1364
+ matrix_bd = self.rel_shift(matrix_bd)
1365
+ matrix_ac = torch.matmul(q_with_bias_u, k.transpose(-2, -1))
1366
+ matrix_bd = matrix_bd[:, :, :, : matrix_ac.size(-1)]
1367
+ scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k)
1368
+ return self.forward_attention(v, scores, mask)
1369
+
1370
+
1371
+ class RotaryPositionMultiHeadAttention(MultiHeadAttention):
1372
+ """
1373
+ Rotary Position Multi-Head Attention module.
1374
+ """
1375
+
1376
+ def forward(
1377
+ self,
1378
+ query: Tensor,
1379
+ key: Tensor,
1380
+ value: Tensor,
1381
+ pos_emb: List[Tensor],
1382
+ mask: Optional[Tensor] = None,
1383
+ ) -> Tensor:
1384
+ b, t, _ = value.size()
1385
+ query = query.transpose(0, 1).view(t, b, self.h, self.d_k)
1386
+ key = key.transpose(0, 1).view(t, b, self.h, self.d_k)
1387
+ value = value.transpose(0, 1).view(t, b, self.h, self.d_k)
1388
+
1389
+ cos, sin = pos_emb
1390
+ query, key = apply_rotary_pos_emb(query, key, cos, sin, offset=0)
1391
+
1392
+ q, k, v = self.forward_qkv(
1393
+ query.view(t, b, self.h * self.d_k).transpose(0, 1),
1394
+ key.view(t, b, self.h * self.d_k).transpose(0, 1),
1395
+ value.view(t, b, self.h * self.d_k).transpose(0, 1),
1396
+ )
1397
+
1398
+ if self.flash_attn:
1399
+ if mask is None:
1400
+ scores = flash_attn_func(q, k, v)
1401
+ else:
1402
+ scores = apply_masked_flash_attn(q, k, v, mask, self.h, self.d_k)
1403
+ scores = scores.view(b, -1, self.h * self.d_k)
1404
+ return self.linear_out(scores)
1405
+ elif self.torch_sdpa_attn:
1406
+ attn_mask = None
1407
+ if mask is not None:
1408
+ attn_mask = ~mask.unsqueeze(1)
1409
+ # SDPA masks padding queries with true -inf; softmax over such a row is NaN in forward and backward.
1410
+ # Unmask such rows entirely: their output is finite garbage that nothing reads.
1411
+ attn_mask = attn_mask | (~attn_mask.any(dim=-1, keepdim=True))
1412
+ attn_output = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
1413
+ attn_output = attn_output.transpose(1, 2).reshape(b, t, self.h * self.d_k)
1414
+ return self.linear_out(attn_output)
1415
+ else:
1416
+ scores = torch.matmul(q, k.transpose(-2, -1) / math.sqrt(self.d_k))
1417
+ return self.forward_attention(v, scores, mask)
1418
+
1419
+
1420
+ class PositionalEncoding(nn.Module, ABC):
1421
+ """
1422
+ Base class of Positional Encodings.
1423
+ """
1424
+
1425
+ def __init__(self, dim: int, base: int):
1426
+ super().__init__()
1427
+ self.dim = dim
1428
+ self.base = base
1429
+
1430
+ @abstractmethod
1431
+ def create_pe(self, length: int, device: torch.device) -> Optional[Tensor]:
1432
+ pass
1433
+
1434
+ def extend_pe(self, length: int, device: torch.device):
1435
+ """
1436
+ Extends the positional encoding buffer to process longer sequences.
1437
+ """
1438
+ pe = self.create_pe(length, device)
1439
+ if pe is None:
1440
+ return
1441
+ if hasattr(self, "pe"):
1442
+ self.pe = pe
1443
+ else:
1444
+ self.register_buffer("pe", pe, persistent=False)
1445
+
1446
+
1447
+ class RelPositionalEmbedding(PositionalEncoding):
1448
+ """
1449
+ Relative Positional Embedding module.
1450
+ """
1451
+
1452
+ def create_pe(self, length: int, device: torch.device) -> Optional[Tensor]:
1453
+ """
1454
+ Creates the relative positional encoding matrix.
1455
+ """
1456
+ if hasattr(self, "pe") and self.pe.shape[1] >= 2 * length - 1:
1457
+ return None
1458
+ positions = torch.arange(length - 1, -length, -1, device=device).unsqueeze(1)
1459
+ pos_length = positions.size(0)
1460
+ pe = torch.zeros(pos_length, self.dim, device=positions.device)
1461
+ div_term = torch.exp(
1462
+ torch.arange(0, self.dim, 2, device=pe.device)
1463
+ * -(math.log(10000.0) / self.dim)
1464
+ )
1465
+ pe[:, 0::2] = torch.sin(positions * div_term)
1466
+ pe[:, 1::2] = torch.cos(positions * div_term)
1467
+ return pe.unsqueeze(0)
1468
+
1469
+ def forward(self, x: torch.Tensor) -> Tuple[Tensor, Tensor]:
1470
+ input_len = x.size(1)
1471
+ center_pos = self.pe.size(1) // 2 + 1
1472
+ start_pos = center_pos - input_len
1473
+ end_pos = center_pos + input_len - 1
1474
+ return x, self.pe[:, start_pos:end_pos]
1475
+
1476
+
1477
+ class RotaryPositionalEmbedding(PositionalEncoding):
1478
+ """
1479
+ Rotary Positional Embedding module.
1480
+ """
1481
+
1482
+ def create_pe(self, length: int, device: torch.device) -> Optional[Tensor]:
1483
+ """
1484
+ Creates or extends the rotary positional encoding matrix.
1485
+ """
1486
+ if hasattr(self, "pe") and self.pe.size(0) >= 2 * length:
1487
+ return None
1488
+ positions = torch.arange(0, length, dtype=torch.float32, device=device)
1489
+ inv_freq = 1.0 / (
1490
+ self.base ** (torch.arange(0, self.dim, 2).float() / self.dim)
1491
+ )
1492
+ t = torch.arange(length, device=positions.device).type_as(inv_freq)
1493
+ freqs = torch.einsum("i,j->ij", t, inv_freq)
1494
+ emb = torch.cat((freqs, freqs), dim=-1).to(positions.device)
1495
+ return torch.cat([emb.cos()[:, None, None, :], emb.sin()[:, None, None, :]])
1496
+
1497
+ def forward(self, x: torch.Tensor) -> Tuple[Tensor, List[Tensor]]:
1498
+ cos_emb = self.pe[0 : x.shape[1]]
1499
+ half_pe = self.pe.shape[0] // 2
1500
+ sin_emb = self.pe[half_pe : half_pe + x.shape[1]]
1501
+ return x, [cos_emb, sin_emb]
1502
+
1503
+
1504
+ class ConformerConvolution(nn.Module):
1505
+ """
1506
+ Conformer Convolution module.
1507
+ """
1508
+
1509
+ def __init__(
1510
+ self,
1511
+ d_model: int,
1512
+ kernel_size: int,
1513
+ norm_type: str,
1514
+ ):
1515
+ super().__init__()
1516
+ assert (kernel_size - 1) % 2 == 0
1517
+ assert norm_type in ["batch_norm", "layer_norm"]
1518
+ self.norm_type = norm_type
1519
+ self.pointwise_conv1 = nn.Conv1d(d_model, d_model * 2, kernel_size=1)
1520
+ self.depthwise_conv = nn.Conv1d(
1521
+ in_channels=d_model,
1522
+ out_channels=d_model,
1523
+ kernel_size=kernel_size,
1524
+ padding=(kernel_size - 1) // 2,
1525
+ groups=d_model,
1526
+ bias=True,
1527
+ )
1528
+ self.batch_norm = (
1529
+ nn.BatchNorm1d(d_model)
1530
+ if norm_type == "batch_norm"
1531
+ else nn.LayerNorm(d_model)
1532
+ )
1533
+ self.activation = nn.SiLU()
1534
+ self.pointwise_conv2 = nn.Conv1d(d_model, d_model, kernel_size=1)
1535
+
1536
+ def forward(self, x: Tensor, pad_mask: Optional[Tensor] = None) -> Tensor:
1537
+ x = x.transpose(1, 2)
1538
+ x = self.pointwise_conv1(x)
1539
+ x = nn.functional.glu(x, dim=1)
1540
+ if pad_mask is not None:
1541
+ x = x.masked_fill(pad_mask.unsqueeze(1), 0.0)
1542
+ x = self.depthwise_conv(x)
1543
+ if self.norm_type == "batch_norm":
1544
+ x = self.batch_norm(x)
1545
+ else:
1546
+ x = self.batch_norm(x.transpose(1, 2)).transpose(1, 2)
1547
+ x = self.activation(x)
1548
+ x = self.pointwise_conv2(x)
1549
+ return x.transpose(1, 2)
1550
+
1551
+
1552
+ class ConformerFeedForward(nn.Module):
1553
+ """
1554
+ Conformer Feed Forward module.
1555
+ """
1556
+
1557
+ def __init__(self, d_model: int, d_ff: int, use_bias=True):
1558
+ super().__init__()
1559
+ self.linear1 = nn.Linear(d_model, d_ff, bias=use_bias)
1560
+ self.activation = nn.SiLU()
1561
+ self.linear2 = nn.Linear(d_ff, d_model, bias=use_bias)
1562
+
1563
+ def forward(self, x: Tensor) -> Tensor:
1564
+ return self.linear2(self.activation(self.linear1(x)))
1565
+
1566
+
1567
+ class ConformerLayer(nn.Module):
1568
+ """
1569
+ Conformer Layer module.
1570
+ This module combines several submodules including feed forward networks,
1571
+ depthwise separable convolution, and multi-head self-attention
1572
+ to form a single Conformer block.
1573
+ """
1574
+
1575
+ def __init__(
1576
+ self,
1577
+ d_model: int,
1578
+ d_ff: int,
1579
+ self_attention_model: str,
1580
+ n_heads: int = 16,
1581
+ conv_norm_type: str = "batch_norm",
1582
+ conv_kernel_size: int = 31,
1583
+ flash_attn: bool = False,
1584
+ ):
1585
+ super().__init__()
1586
+ self.fc_factor = 0.5
1587
+ self.norm_feed_forward1 = nn.LayerNorm(d_model)
1588
+ self.feed_forward1 = ConformerFeedForward(d_model=d_model, d_ff=d_ff)
1589
+ self.norm_conv = nn.LayerNorm(d_model)
1590
+ self.conv = ConformerConvolution(
1591
+ d_model=d_model,
1592
+ kernel_size=conv_kernel_size,
1593
+ norm_type=conv_norm_type,
1594
+ )
1595
+ self.norm_self_att = nn.LayerNorm(d_model)
1596
+ if self_attention_model == "rotary":
1597
+ self.self_attn: nn.Module = RotaryPositionMultiHeadAttention(
1598
+ n_head=n_heads,
1599
+ n_feat=d_model,
1600
+ flash_attn=flash_attn,
1601
+ torch_sdpa_attn=not flash_attn,
1602
+ )
1603
+ else:
1604
+ assert not flash_attn, "Not supported flash_attn for rel_pos"
1605
+ self.self_attn = RelPositionMultiHeadAttention(
1606
+ n_head=n_heads,
1607
+ n_feat=d_model,
1608
+ )
1609
+ self.norm_feed_forward2 = nn.LayerNorm(d_model)
1610
+ self.feed_forward2 = ConformerFeedForward(d_model=d_model, d_ff=d_ff)
1611
+ self.norm_out = nn.LayerNorm(d_model)
1612
+
1613
+ def forward(
1614
+ self,
1615
+ x: Tensor,
1616
+ pos_emb: Union[Tensor, List[Tensor]],
1617
+ att_mask: Optional[Tensor] = None,
1618
+ pad_mask: Optional[Tensor] = None,
1619
+ ) -> Tensor:
1620
+ residual = x
1621
+ x = self.norm_feed_forward1(x)
1622
+ x = self.feed_forward1(x)
1623
+ residual = residual + x * self.fc_factor
1624
+
1625
+ x = self.norm_self_att(residual)
1626
+ x = self.self_attn(x, x, x, pos_emb, mask=att_mask)
1627
+ residual = residual + x
1628
+
1629
+ x = self.norm_conv(residual)
1630
+ x = self.conv(x, pad_mask=pad_mask)
1631
+ residual = residual + x
1632
+
1633
+ x = self.norm_feed_forward2(residual)
1634
+ x = self.feed_forward2(x)
1635
+ residual = residual + x * self.fc_factor
1636
+
1637
+ x = self.norm_out(residual)
1638
+ return x
1639
+
1640
+
1641
+ class ConformerEncoder(nn.Module):
1642
+ """
1643
+ Conformer Encoder module.
1644
+ This module encapsulates the entire Conformer encoder architecture,
1645
+ consisting of a StridingSubsampling layer, positional embeddings, and
1646
+ a stack of Conformer Layers.
1647
+ It serves as the main component responsible for processing speech features.
1648
+ """
1649
+
1650
+ def __init__(
1651
+ self,
1652
+ feat_in: int = 64,
1653
+ n_layers: int = 16,
1654
+ d_model: int = 768,
1655
+ subsampling: str = "conv2d",
1656
+ subs_kernel_size: int = 3,
1657
+ subsampling_factor: int = 4,
1658
+ ff_expansion_factor: int = 4,
1659
+ self_attention_model: str = "rotary",
1660
+ n_heads: int = 16,
1661
+ pos_emb_max_len: int = 5000,
1662
+ conv_norm_type: str = "batch_norm",
1663
+ conv_kernel_size: int = 31,
1664
+ flash_attn: bool = False,
1665
+ activation_checkpointing: bool = False,
1666
+ ):
1667
+ super().__init__()
1668
+ self.feat_in = feat_in
1669
+ self.activation_checkpointing = activation_checkpointing
1670
+ assert self_attention_model in [
1671
+ "rotary",
1672
+ "rel_pos",
1673
+ ], f"Not supported attn = {self_attention_model}"
1674
+
1675
+ self.pre_encode = StridingSubsampling(
1676
+ subsampling=subsampling,
1677
+ kernel_size=subs_kernel_size,
1678
+ subsampling_factor=subsampling_factor,
1679
+ feat_in=feat_in,
1680
+ feat_out=d_model,
1681
+ conv_channels=d_model,
1682
+ )
1683
+
1684
+ self.pos_emb_max_len = pos_emb_max_len
1685
+ if self_attention_model == "rotary":
1686
+ self.pos_enc: PositionalEncoding = RotaryPositionalEmbedding(
1687
+ d_model // n_heads, pos_emb_max_len
1688
+ )
1689
+ else:
1690
+ self.pos_enc = RelPositionalEmbedding(d_model, pos_emb_max_len)
1691
+
1692
+ self.layers = nn.ModuleList()
1693
+ for _ in range(n_layers):
1694
+ layer = ConformerLayer(
1695
+ d_model=d_model,
1696
+ d_ff=d_model * ff_expansion_factor,
1697
+ self_attention_model=self_attention_model,
1698
+ n_heads=n_heads,
1699
+ conv_norm_type=conv_norm_type,
1700
+ conv_kernel_size=conv_kernel_size,
1701
+ flash_attn=flash_attn,
1702
+ )
1703
+ self.layers.append(layer)
1704
+
1705
+ def input_example(
1706
+ self,
1707
+ batch_size: int = 8,
1708
+ seqlen: int = 200,
1709
+ ) -> Tuple[Tensor, Tensor]:
1710
+ device = next(self.parameters()).device
1711
+ features = torch.randn(batch_size, self.feat_in, seqlen)
1712
+ feature_lengths = torch.randint(1, seqlen + 1, (batch_size,))
1713
+ feature_lengths[0] = seqlen
1714
+ return features.float().to(device), feature_lengths.to(device)
1715
+
1716
+ def input_names(self) -> List[str]:
1717
+ return ["audio_signal", "length"]
1718
+
1719
+ def output_names(self) -> List[str]:
1720
+ return ["encoded", "encoded_len"]
1721
+
1722
+ @contextmanager
1723
+ def onnx_export_mode(self):
1724
+ saved = []
1725
+ for layer in self.layers:
1726
+ attn = layer.self_attn
1727
+ saved.append((attn.flash_attn, attn.torch_sdpa_attn))
1728
+ attn.flash_attn = False
1729
+ attn.torch_sdpa_attn = False
1730
+ try:
1731
+ yield
1732
+ finally:
1733
+ for layer, (fa, sdpa) in zip(self.layers, saved):
1734
+ layer.self_attn.flash_attn = fa
1735
+ layer.self_attn.torch_sdpa_attn = sdpa
1736
+
1737
+ def dynamic_axes(self) -> Dict[str, Dict[int, str]]:
1738
+ return {
1739
+ "audio_signal": {0: "batch_size", 2: "seq_len"},
1740
+ "length": {0: "batch_size"},
1741
+ "encoded": {0: "batch_size", 1: "seq_len"},
1742
+ "encoded_len": {0: "batch_size"},
1743
+ }
1744
+
1745
+ def forward(self, audio_signal: Tensor, length: Tensor) -> Tuple[Tensor, Tensor]:
1746
+ if not hasattr(self.pos_enc, "pe"):
1747
+ self.pos_enc.extend_pe(self.pos_emb_max_len, audio_signal.device)
1748
+
1749
+ audio_signal, length = self.pre_encode(
1750
+ x=audio_signal.transpose(1, 2), lengths=length
1751
+ )
1752
+
1753
+ max_len = audio_signal.size(1)
1754
+ audio_signal, pos_emb = self.pos_enc(x=audio_signal)
1755
+
1756
+ pad_mask = torch.arange(0, max_len, device=audio_signal.device).expand(
1757
+ length.size(0), -1
1758
+ ) < length.unsqueeze(-1)
1759
+
1760
+ att_mask = None
1761
+ if audio_signal.shape[0] > 1:
1762
+ att_mask = pad_mask.unsqueeze(1).repeat([1, max_len, 1])
1763
+ att_mask = torch.logical_and(att_mask, att_mask.transpose(1, 2))
1764
+ att_mask = ~att_mask
1765
+
1766
+ pad_mask = ~pad_mask
1767
+
1768
+ for layer in self.layers:
1769
+ if self.activation_checkpointing and self.training:
1770
+ audio_signal = checkpoint(
1771
+ _conformer_layer_fwd,
1772
+ layer,
1773
+ audio_signal,
1774
+ pos_emb,
1775
+ att_mask,
1776
+ pad_mask,
1777
+ use_reentrant=False,
1778
+ )
1779
+ else:
1780
+ audio_signal = layer(
1781
+ x=audio_signal,
1782
+ pos_emb=pos_emb,
1783
+ att_mask=att_mask,
1784
+ pad_mask=pad_mask,
1785
+ )
1786
+
1787
+ return audio_signal.transpose(1, 2), length
1788
+
1789
+
1790
+ # ==== gigaam/model.py ====
1791
+
1792
+ LONGFORM_THRESHOLD = 25 * SAMPLE_RATE
1793
+
1794
+
1795
+ class GigaAM(nn.Module):
1796
+ """
1797
+ Giga Acoustic Model: Self-Supervised Model for Speech Tasks
1798
+ """
1799
+
1800
+ def __init__(self, cfg: omegaconf.DictConfig):
1801
+ super().__init__()
1802
+ self.cfg = cfg
1803
+ self.preprocessor = hydra.utils.instantiate(self.cfg.preprocessor)
1804
+ self.encoder = hydra.utils.instantiate(self.cfg.encoder)
1805
+
1806
+ def forward(
1807
+ self, features: Tensor, feature_lengths: Tensor
1808
+ ) -> Tuple[Tensor, Tensor]:
1809
+ """
1810
+ Perform forward pass through the preprocessor and encoder.
1811
+ """
1812
+ features, feature_lengths = self.preprocessor(features, feature_lengths)
1813
+ if self._device.type == "cpu":
1814
+ return self.encoder(features, feature_lengths)
1815
+ with torch.autocast(device_type=self._device.type, dtype=torch.float16):
1816
+ return self.encoder(features, feature_lengths)
1817
+
1818
+ @property
1819
+ def _device(self) -> torch.device:
1820
+ return next(self.parameters()).device
1821
+
1822
+ @property
1823
+ def _dtype(self) -> torch.dtype:
1824
+ return next(self.parameters()).dtype
1825
+
1826
+ def prepare_wav(self, wav_file: str) -> Tuple[Tensor, Tensor]:
1827
+ """
1828
+ Prepare an audio file for processing by loading it onto
1829
+ the correct device and converting its format.
1830
+ """
1831
+ wav = load_audio(wav_file)
1832
+ wav = wav.to(self._device).to(self._dtype).unsqueeze(0)
1833
+ length = torch.full([1], wav.shape[-1], device=self._device)
1834
+ return wav, length
1835
+
1836
+ def embed_audio(self, wav_file: str) -> Tuple[Tensor, Tensor]:
1837
+ """
1838
+ Extract audio representations using the GigaAM model.
1839
+ """
1840
+ wav, length = self.prepare_wav(wav_file)
1841
+ encoded, encoded_len = self.forward(wav, length)
1842
+ return encoded, encoded_len
1843
+
1844
+ def to_onnx(self, dir_path: str = ".", dtype: torch.dtype = torch.float32) -> None:
1845
+ """
1846
+ Export onnx model encoder to the specified dir.
1847
+ """
1848
+ with self.encoder.onnx_export_mode():
1849
+ self._to_onnx(dir_path, dtype=dtype)
1850
+ omegaconf.OmegaConf.save(self.cfg, f"{dir_path}/{self.cfg.model_name}.yaml")
1851
+
1852
+ def _to_onnx(self, dir_path: str = ".", dtype: torch.dtype = torch.float32) -> None:
1853
+ """
1854
+ Export onnx model encoder to the specified dir.
1855
+ """
1856
+ onnx_converter(
1857
+ model_name=f"{self.cfg.model_name}_encoder",
1858
+ out_dir=dir_path,
1859
+ module=self.encoder,
1860
+ dynamic_axes=self.encoder.dynamic_axes(),
1861
+ export_dtype=dtype,
1862
+ )
1863
+
1864
+
1865
+ class GigaAMASR(GigaAM):
1866
+ """
1867
+ Giga Acoustic Model for Speech Recognition
1868
+ """
1869
+
1870
+ def __init__(self, cfg: omegaconf.DictConfig):
1871
+ super().__init__(cfg)
1872
+ self.head = hydra.utils.instantiate(self.cfg.head)
1873
+ self.decoding = hydra.utils.instantiate(self.cfg.decoding)
1874
+
1875
+ def _decode(
1876
+ self,
1877
+ encoded: Tensor,
1878
+ encoded_len: Tensor,
1879
+ wav_lens: Tensor,
1880
+ word_timestamps: bool = False,
1881
+ ) -> List[Tuple[str, Optional[List[Word]]]]:
1882
+ decoded = self.decoding.decode(self.head, encoded, encoded_len)
1883
+ if not word_timestamps:
1884
+ return [(t, None) for t, _, _ in decoded]
1885
+
1886
+ out: List[Tuple[str, Optional[List[Word]]]] = []
1887
+ for i, (text, token_ids, token_frames) in enumerate(decoded):
1888
+ frame_shift = compute_frame_shift(
1889
+ int(wav_lens[i].item()), int(encoded_len[i].item())
1890
+ )
1891
+ out.append(
1892
+ (
1893
+ text,
1894
+ frames_to_words(
1895
+ self.decoding.tokenizer,
1896
+ token_ids,
1897
+ token_frames,
1898
+ frame_shift,
1899
+ ),
1900
+ )
1901
+ )
1902
+ return out
1903
+
1904
+ @torch.inference_mode()
1905
+ def transcribe(
1906
+ self, wav_file: str, word_timestamps: bool = False
1907
+ ) -> TranscriptionResult:
1908
+ """
1909
+ Transcribes a short audio file into text.
1910
+ Returns TranscriptionResult with optional word-level timestamps.
1911
+ """
1912
+ wav, length = self.prepare_wav(wav_file)
1913
+ if length.item() > LONGFORM_THRESHOLD:
1914
+ raise ValueError("Too long wav file, use 'transcribe_longform' method.")
1915
+
1916
+ encoded, encoded_len = self.forward(wav, length)
1917
+ text, words = self._decode(encoded, encoded_len, length, word_timestamps)[0]
1918
+ return TranscriptionResult(text=text, words=words)
1919
+
1920
+ def forward_for_export(
1921
+ self, features: Tensor, feature_lengths: Tensor
1922
+ ) -> Tuple[Tensor, Tensor]:
1923
+ """
1924
+ Encoder-decoder forward to save model entirely in onnx format.
1925
+ """
1926
+ encoded, encoded_len = self.encoder(features, feature_lengths)
1927
+ return self.head(encoded), encoded_len
1928
+
1929
+ def _to_onnx(self, dir_path: str = ".", dtype: torch.dtype = torch.float32) -> None:
1930
+ """
1931
+ Export onnx ASR model.
1932
+ `ctc`: exported entirely in encoder-decoder format.
1933
+ `rnnt`: exported in encoder/decoder/joint parts separately.
1934
+ """
1935
+ if "ctc" in self.cfg.model_name:
1936
+ saved_forward = self.forward
1937
+ self.forward = self.forward_for_export # type: ignore[assignment, method-assign]
1938
+ try:
1939
+ onnx_converter(
1940
+ model_name=self.cfg.model_name,
1941
+ out_dir=dir_path,
1942
+ module=self,
1943
+ inputs=self.encoder.input_example(),
1944
+ input_names=["features", "feature_lengths"],
1945
+ output_names=["log_probs", "encoded_lengths"],
1946
+ dynamic_axes={
1947
+ "features": {0: "batch_size", 2: "seq_len"},
1948
+ "feature_lengths": {0: "batch_size"},
1949
+ "log_probs": {0: "batch_size", 1: "seq_len"},
1950
+ "encoded_lengths": {0: "batch_size"},
1951
+ },
1952
+ export_dtype=dtype,
1953
+ )
1954
+ finally:
1955
+ self.forward = saved_forward # type: ignore[assignment, method-assign]
1956
+ else:
1957
+ super()._to_onnx(dir_path, dtype=dtype)
1958
+ onnx_converter(
1959
+ model_name=f"{self.cfg.model_name}_decoder",
1960
+ out_dir=dir_path,
1961
+ module=self.head.decoder,
1962
+ dynamic_axes=self.head.decoder.dynamic_axes(),
1963
+ export_dtype=dtype,
1964
+ )
1965
+ onnx_converter(
1966
+ model_name=f"{self.cfg.model_name}_joint",
1967
+ out_dir=dir_path,
1968
+ module=self.head.joint,
1969
+ dynamic_axes=self.head.joint.dynamic_axes(),
1970
+ export_dtype=dtype,
1971
+ )
1972
+
1973
+ @torch.inference_mode()
1974
+ def transcribe_longform(
1975
+ self,
1976
+ wav_file: str,
1977
+ word_timestamps: bool = False,
1978
+ fr_batch_size: int = 16,
1979
+ fr_num_workers: int = 0,
1980
+ **kwargs,
1981
+ ) -> LongformTranscriptionResult:
1982
+ """
1983
+ Transcribes a long audio file by splitting it into segments and
1984
+ then transcribing each segment (batched inference via AudioDataset).
1985
+ Use fr_batch_size and fr_num_workers to control the batched inference.
1986
+ Returns LongformTranscriptionResult with segments containing optional word-level timestamps.
1987
+ """
1988
+
1989
+ segments, boundaries = segment_audio_file(
1990
+ wav_file, SAMPLE_RATE, device=self._device, **kwargs
1991
+ )
1992
+
1993
+ if not segments:
1994
+ return LongformTranscriptionResult(segments=[])
1995
+
1996
+ ds = AudioDataset(segments, tokenizer=None)
1997
+ dl = DataLoader(
1998
+ ds,
1999
+ batch_size=fr_batch_size,
2000
+ shuffle=False,
2001
+ collate_fn=AudioDataset.collate,
2002
+ num_workers=fr_num_workers,
2003
+ )
2004
+
2005
+ result_segments: List[Segment] = []
2006
+ idx = 0
2007
+ for wav_pad, wav_lens in dl:
2008
+ wav_pad = wav_pad.to(self._device).to(self._dtype)
2009
+ wav_lens = wav_lens.to(self._device)
2010
+ encoded, encoded_len = self.forward(wav_pad, wav_lens)
2011
+ for text, words in self._decode(
2012
+ encoded, encoded_len, wav_lens, word_timestamps
2013
+ ):
2014
+ seg_start, seg_end = boundaries[idx]
2015
+ idx += 1
2016
+ if word_timestamps:
2017
+ result_segments.append(
2018
+ Segment(
2019
+ text=text,
2020
+ start=seg_start,
2021
+ end=seg_end,
2022
+ words=[
2023
+ Word(
2024
+ text=w.text,
2025
+ start=round(w.start + seg_start, 3),
2026
+ end=round(w.end + seg_start, 3),
2027
+ )
2028
+ for w in words or []
2029
+ ],
2030
+ )
2031
+ )
2032
+ else:
2033
+ result_segments.append(
2034
+ Segment(text=text, start=seg_start, end=seg_end)
2035
+ )
2036
+ return LongformTranscriptionResult(segments=result_segments)
2037
+
2038
+
2039
+ class GigaAMEmo(GigaAM):
2040
+ """
2041
+ Giga Acoustic Model for Emotion Recognition
2042
+ """
2043
+
2044
+ def __init__(self, cfg: omegaconf.DictConfig):
2045
+ super().__init__(cfg)
2046
+ self.head = hydra.utils.instantiate(self.cfg.head)
2047
+ self.id2name = cfg.id2name
2048
+
2049
+ def get_probs(self, wav_file: str) -> Dict[str, float]:
2050
+ """
2051
+ Calculate probabilities for each emotion class based on the provided audio file.
2052
+ """
2053
+ wav, length = self.prepare_wav(wav_file)
2054
+ encoded, _ = self.forward(wav, length)
2055
+ encoded_pooled = nn.functional.avg_pool1d(
2056
+ encoded, kernel_size=encoded.shape[-1]
2057
+ ).squeeze(-1)
2058
+
2059
+ logits = self.head(encoded_pooled)[0]
2060
+ probs = nn.functional.softmax(logits, dim=-1).detach().tolist()
2061
+
2062
+ return {self.id2name[i]: probs[i] for i in range(len(self.id2name))}
2063
+
2064
+ def forward_for_export(self, features: Tensor, feature_lengths: Tensor) -> Tensor:
2065
+ """
2066
+ Encoder-decoder forward to save model entirely in onnx format.
2067
+ """
2068
+ encoded, _ = self.encoder(features, feature_lengths)
2069
+ enc_pooled = encoded.mean(dim=-1)
2070
+ return nn.functional.softmax(self.head(enc_pooled), dim=-1)
2071
+
2072
+ def _to_onnx(self, dir_path: str = ".", dtype: torch.dtype = torch.float32) -> None:
2073
+ """
2074
+ Export onnx Emo model.
2075
+ """
2076
+ saved_forward = self.forward
2077
+ self.forward = self.forward_for_export # type: ignore[assignment, method-assign]
2078
+ try:
2079
+ onnx_converter(
2080
+ model_name=self.cfg.model_name,
2081
+ out_dir=dir_path,
2082
+ module=self,
2083
+ inputs=self.encoder.input_example(),
2084
+ input_names=["features", "feature_lengths"],
2085
+ output_names=["probs"],
2086
+ dynamic_axes={
2087
+ "features": {0: "batch_size", 2: "seq_len"},
2088
+ "feature_lengths": {0: "batch_size"},
2089
+ "probs": {0: "batch_size", 1: "seq_len"},
2090
+ },
2091
+ export_dtype=dtype,
2092
+ )
2093
+ finally:
2094
+ self.forward = saved_forward # type: ignore[assignment, method-assign]
2095
+
2096
+
2097
+ # ==== HF glue ====
2098
+
2099
+
2100
+ class GigaAMConfig(PretrainedConfig):
2101
+ model_type = "gigaam"
2102
+
2103
+ def __init__(self, cfg: omegaconf.DictConfig = None, **kwargs):
2104
+ super().__init__(**kwargs)
2105
+ self.cfg = cfg
2106
+
2107
+
2108
+ class GigaAMModel(PreTrainedModel):
2109
+ config_class = GigaAMConfig
2110
+ base_model_prefix = "gigaam"
2111
+
2112
+ def __init__(self, config: GigaAMConfig):
2113
+ super().__init__(config)
2114
+ self.config = config
2115
+ inner = self.config.cfg["model"]["cfg"]
2116
+ if "decoding" in inner and "model_path" in inner["decoding"]:
2117
+ inner["decoding"]["model_path"] = cached_file(
2118
+ config.name_or_path,
2119
+ "tokenizer.model",
2120
+ revision=getattr(config, "_commit_hash", None),
2121
+ cache_dir=getattr(config, "cache_dir", None),
2122
+ token=getattr(config, "token", None),
2123
+ )
2124
+ with torch.device("cpu"): # transformers>=5 inits under meta device
2125
+ self.model = instantiate(config.cfg["model"], _recursive_=False)
2126
+ self.post_init()
2127
+
2128
+ def forward(self, features: torch.Tensor, feature_lengths: torch.Tensor):
2129
+ return self.model(features, feature_lengths)
2130
+
2131
+ def embed_audio(self, wav_file: str) -> torch.Tensor:
2132
+ return self.model.embed_audio(wav_file)
2133
+
2134
+ def transcribe(
2135
+ self, wav_file: str, word_timestamps: bool = False
2136
+ ) -> TranscriptionResult:
2137
+ return self.model.transcribe(wav_file, word_timestamps=word_timestamps)
2138
+
2139
+ def transcribe_longform(
2140
+ self, wav_file: str, **kwargs
2141
+ ) -> LongformTranscriptionResult:
2142
+ return self.model.transcribe_longform(wav_file, **kwargs)
2143
+
2144
+ def get_probs(self, wav_file: str) -> Dict[str, float]:
2145
+ return self.model.get_probs(wav_file)
2146
+
2147
+ @torch.no_grad()
2148
+ def to_onnx(self, dir_path: str = ".") -> None:
2149
+ self.model.to_onnx(dir_path)
pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e1db43873ec5e296f229572e06e2470fc157ac9f8d4aacabda295630b9b91728
3
+ size 883170115