nanoTTS / stable_codec /model.py
3v324v23's picture
add files
9807699
Raw
History Blame Contribute Delete
6.24 kB
import json
import torch
import torch.nn as nn
import torchaudio
from typing import Optional, List, Tuple, Union
from einops import rearrange
from stable_audio_tools.models import create_model_from_config
from stable_audio_tools.models.fsq import DitheredFSQ
from stable_audio_tools.models.utils import load_ckpt_state_dict
from stable_audio_tools.models.utils import copy_state_dict
from stable_audio_tools.data.utils import VolumeNorm
from .residual_fsq import ResidualFSQBottleneck
from stable_audio_tools import get_pretrained_model
class StableCodec(nn.Module):
def __init__(self,
model_config_path: Optional[str] = None, ckpt_path: Optional[str] = None, pretrained_model: Optional[str] = None, device = torch.device("cpu"),
):
super().__init__()
self.device = device
if pretrained_model is not None:
print(f"Loading pretrained model `{pretrained_model}`.\n")
self.model, model_config = get_pretrained_model(pretrained_model)
else:
if model_config_path is None:
raise ValueError("Either `model_config_path` or `pretrained_model` should be provided.")
print(f"Loading config from `{model_config_path}`.\n")
with open(model_config_path) as f:
model_config = json.load(f)
self.model = create_model_from_config(model_config)
if ckpt_path is not None:
print(f"Loading weights from `{ckpt_path}`.\n")
state = load_ckpt_state_dict(ckpt_path)
copy_state_dict(self.model, state)
self.model = self.model.to(self.device).eval().requires_grad_(False)
self.residual_fsq: Optional[ResidualFSQBottleneck] = None
self.sample_rate = model_config["sample_rate"]
self.volume_norm = VolumeNorm([-20, 0], self.sample_rate)
self.preset_bottleneck_configs = {
"1x46656_400bps": [
([6, 6, 6, 6, 6, 6], 1.0)
],
"2x15625_700bps": [
([5, 5, 5, 5, 5, 5], 1.0),
([5, 5, 5, 5, 5, 5], 0.25),
],
"4x729_1000bps": [
([3, 3, 3, 3, 3, 3], 1.0),
([3, 3, 3, 3, 3, 3], 0.5),
([3, 3, 3, 3, 3, 3], 0.25),
([3, 3, 3, 3, 3, 3], 0.125),
]
}
def set_posthoc_bottleneck(self, stages):
if isinstance(stages,str):
if stages in self.preset_bottleneck_configs:
stages = self.preset_bottleneck_configs[stages]
else:
raise ValueError(f"Unsupported preset bottleneck configuration `{stages}`.")
self.residual_fsq = ResidualFSQBottleneck(stages).to(self.device).eval().requires_grad_(False)
def encode(self, audio: Union[str, torch.Tensor], posthoc_bottleneck: bool = False, normalize: bool = True,**kwargs):
"""
Encode audio into latents and tokens.
Args:
audio : Union[str, torch.Tensor]
Path to an audio file or a `Tensor` of the eaudio itself.
posthoc_bottleneck : bool
Whether to inject a posthoc FSQ instead of the FSQ used during training.
If `True`, its configuration should've been passed in with the `self.set_posthoc_bottleneck` method.
normalize : bool
Whether to normalize the audio to -20 LUFS before encoding (recommended).
Other `kwargs` are the same as in `AudioAutoencoder.encode_audio` method.
Returns:
Tuple of `(continuous_latents, tokens)`.
continuous_latents : torch.Tensor
Pre-bottleneck latents in the `(B, H, S)` shape.
tokens : torch.Tensor
Bottleneck tokens in the `(B, S, 1)` shape.
Where `B` is the batch size, `H` is the hidden dimension and `S` is the sequence length.
"""
if isinstance(audio, str):
audio, sample_rate = torchaudio.load(audio)
audio = self.model.preprocess_audio_for_encoder(audio.to(self.device), sample_rate)
if normalize:
audio = self.volume_norm(audio.squeeze(0)).unsqueeze(0)
latents, info = self.model.encode_audio(audio,
return_info=True, skip_bottleneck=posthoc_bottleneck, **kwargs)
if posthoc_bottleneck:
tokens = self.residual_fsq.encode(latents)
else:
tokens = info["quantizer_indices"]
return info["pre_bottleneck_latents"], tokens
def decode(self, tokens: torch.Tensor, posthoc_bottleneck: bool = False, **kwargs):
"""
Decode audio from tokens.
Args:
tokens : torch.Tensor
Integer tokens produced by `encode` stage in `(B, S, 1)` shape.
posthoc_bottleneck : bool
Whether to inject a posthoc FSQ instead of the FSQ used during training.
If `True`, its configuration should've been passed in with `self.set_posthoc_bottleneck` method.
Returns:
Decoded audio in the `(B, C, L)` shape.
Where `B` is the batch size, `C` is the number of channels and `L` is the number of frames.
"""
if posthoc_bottleneck:
latents = self.residual_fsq.decode(tokens)
else:
latents = self.model.bottleneck.decode_tokens(tokens)
latents = rearrange(latents, "b c n -> b n c")
audio = self.model.decode_audio(latents, **kwargs)
return audio
def main():
sc = StableCodec(
pretrained_model="stabilityai/stable-codec-speech-16k",
device = torch.device("cuda")
)
sc.set_posthoc_bottleneck("2x15625_700bps")
wavfile = "test.wav"
posthoc_bottleneck = False
latents, tokens = sc.encode(wavfile, posthoc_bottleneck=posthoc_bottleneck)
decoded = sc.decode(tokens, posthoc_bottleneck=posthoc_bottleneck)
torchaudio.save("decode.wav", decoded.squeeze(0).cpu(), 16000)
posthoc_bottleneck = True
latents, tokens = sc.encode(wavfile, posthoc_bottleneck=posthoc_bottleneck)
decoded = sc.decode(tokens, posthoc_bottleneck=posthoc_bottleneck)
torchaudio.save("decode-res.wav", decoded.squeeze(0).cpu(), 16000)
if __name__ == "__main__":
main()