184 lines
6.1 KiB
Python
184 lines
6.1 KiB
Python
import random
|
|
|
|
import librosa
|
|
import torch
|
|
import torchaudio
|
|
from torch import Tensor, no_grad, device
|
|
from torchaudio.transforms import MelSpectrogram, AmplitudeToDB, Resample, TimeStretch
|
|
from pathlib import Path
|
|
import torchaudio.functional as F
|
|
|
|
from tokenizer import ASRTokenizer
|
|
from model import ASRModel
|
|
|
|
CONFIG = {
|
|
# 模型配置
|
|
'input_dim': 640,
|
|
'num_heads': 8,
|
|
'ffn_dim': 2048,
|
|
'num_layers': 8,
|
|
'dropout': 0.1,
|
|
}
|
|
|
|
class ASRInference:
|
|
def __init__(self, model_path: Path, vocab_path: Path, device: device, augment: bool = True, augment_prob: float = 0.5) -> None:
|
|
self.device = device
|
|
self.augment: bool = augment
|
|
self.augment_prob: float = augment_prob
|
|
self.tokenizer = ASRTokenizer(vocab_path=vocab_path)
|
|
self.model = ASRModel(
|
|
vocab_size=self.tokenizer.vocab_size(),
|
|
input_dim=CONFIG['input_dim'],
|
|
num_heads=CONFIG['num_heads'],
|
|
ffn_dim=CONFIG['ffn_dim'],
|
|
num_layers=CONFIG['num_layers'],
|
|
dropout=CONFIG['dropout'],
|
|
).to(device)
|
|
|
|
self.model.load_state_dict(torch.load(model_path, map_location=device)['model_state_dict'])
|
|
self.model.eval()
|
|
|
|
print(f"params params: {self.model.get_num_params():,}",)
|
|
|
|
self.sample_rate = 16000
|
|
self.mel_transform = MelSpectrogram(
|
|
sample_rate=self.sample_rate,
|
|
n_fft=400,
|
|
win_length=400,
|
|
hop_length=160,
|
|
n_mels=80,
|
|
f_min=0,
|
|
f_max=8000,
|
|
power=2.0,
|
|
)
|
|
self.amplitude_to_db = AmplitudeToDB()
|
|
|
|
def _load_audio(self, audio_path: Path) -> Tensor:
|
|
waveform, sample_rate = torchaudio.load_with_torchcodec(audio_path)
|
|
|
|
if sample_rate != self.sample_rate:
|
|
waveform = Resample(sample_rate, self.sample_rate)(waveform)
|
|
|
|
if waveform.shape[0] > 1:
|
|
waveform = waveform.mean(dim=0, keepdim=True)
|
|
|
|
waveform = waveform / (waveform.abs().max() + 1e-8)
|
|
return waveform
|
|
|
|
def _extract_features(self, waveform: Tensor) -> Tensor:
|
|
mel_spec: Tensor = self.mel_transform(waveform)
|
|
log_mel_spec: Tensor = self.amplitude_to_db(mel_spec)
|
|
return log_mel_spec.squeeze(0) # [n_mels, time]
|
|
|
|
def _augment_waveform(self, waveform: Tensor) -> Tensor:
|
|
if not self.augment or random.random() > self.augment_prob:
|
|
return waveform
|
|
|
|
# 1. voice Stretch/Compress
|
|
if random.random() < 0.5:
|
|
waveform = self._voice_stretch_or_compress(waveform=waveform)
|
|
|
|
if random.random() < 0.3:
|
|
waveform = self._drop_frames(waveform)
|
|
|
|
if random.random() < 0.4:
|
|
waveform = self._add_noise(waveform)
|
|
|
|
return waveform
|
|
|
|
def _voice_stretch_or_compress(self, waveform: Tensor) -> Tensor:
|
|
speed_factor = random.uniform(0.6, 1.4) # (Speed Change: 0.6x - 1.4x)
|
|
spec = torch.stft(
|
|
waveform.squeeze(0),
|
|
n_fft=400,
|
|
hop_length=160,
|
|
window=torch.hann_window(400).to(waveform.device),
|
|
return_complex=True
|
|
)
|
|
|
|
# 时间拉伸(不改变音高)
|
|
stretch = TimeStretch(
|
|
hop_length=160,
|
|
n_freq=201,
|
|
fixed_rate=speed_factor
|
|
)
|
|
stretched_spec = stretch(spec)
|
|
|
|
# 转回波形
|
|
waveform_stretched = torch.istft(
|
|
stretched_spec,
|
|
n_fft=400,
|
|
hop_length=160,
|
|
window=torch.hann_window(400).to(waveform.device)
|
|
).unsqueeze(0)
|
|
|
|
return waveform_stretched
|
|
|
|
def _drop_frames(self, waveform: Tensor) -> Tensor:
|
|
audio_len = waveform.shape[1]
|
|
drop_ratio = random.uniform(0.05, 0.15)
|
|
drop_len = int(audio_len * drop_ratio)
|
|
|
|
if audio_len > drop_len:
|
|
start_pos = random.randint(0, audio_len - drop_len)
|
|
# clean
|
|
waveform = torch.cat([waveform[:, :start_pos], waveform[:, start_pos + drop_len:]], dim=1)
|
|
|
|
return waveform
|
|
|
|
def _add_noise(self, waveform: Tensor) -> Tensor:
|
|
snr_db = random.uniform(10, 20)
|
|
signal_power = torch.mean(waveform ** 2)
|
|
|
|
snr_linear = 10 ** (snr_db / 10)
|
|
noise_power = signal_power / snr_linear
|
|
|
|
noise = torch.randn_like(waveform) * torch.sqrt(noise_power)
|
|
return waveform + noise
|
|
|
|
def transcribe(self, audio_path: Path) -> str:
|
|
waveform = self._load_audio(audio_path=audio_path)
|
|
waveform = self._augment_waveform(waveform=waveform)
|
|
mel_spec = self._extract_features(waveform=waveform)
|
|
|
|
mel_spec = mel_spec.unsqueeze(0).to(self.device) # [1, n_mels, time]
|
|
mel_length = torch.tensor([mel_spec.shape[2]], dtype=torch.long, device=self.device)
|
|
|
|
with no_grad():
|
|
log_probs, _ = self.model(mel_specs=mel_spec, mel_lengths=mel_length) # [1, T, vocab]
|
|
|
|
text = self.tokenizer.ctc_greedy_decode(log_probs=log_probs[0])
|
|
return text
|
|
|
|
def transcribe_batch(self, audio_paths: list[Path]) -> list[str]:
|
|
results = []
|
|
|
|
for audio_path in audio_paths:
|
|
text = self.transcribe(audio_path=audio_path)
|
|
results.append(text)
|
|
|
|
return results
|
|
|
|
|
|
def main():
|
|
workspace_dir = Path(__file__).parent.parent
|
|
device = torch.device('cuda:0')
|
|
|
|
# checkpoint = sorted(workspace_dir.glob('.checkpoints/checkpoint_epoch_*.pt'), key=lambda p: int(p.stem.split('_')[1]))[-1]
|
|
checkpoint = workspace_dir / ".checkpoints/checkpoint_step_9500.pt"
|
|
print(f"Load Checkpoint: {checkpoint}")
|
|
|
|
inference = ASRInference(model_path=checkpoint, vocab_path=workspace_dir / 'config/uig_vocab.json' , device=device)
|
|
|
|
audio_path = workspace_dir / 'data/test/F001_001.wav'
|
|
print(f"\n转录音频: {audio_path}")
|
|
text = inference.transcribe(audio_path=audio_path)
|
|
print(f"\n识别结果: {text}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
# workspace_dir = Path(__file__).parent.parent
|
|
# checkpoint = sorted(workspace_dir.glob('.checkpoints/checkpoint_step_*.pt'), key=lambda p: int(p.stem.split('_')[-1]))[-1]
|
|
|
|
# print(checkpoint)
|