omini-model / inference.py
marcos
feat: Replace Matcha with Soprano TTS and add inference pipeline
32fdbcd
Raw
History Blame Contribute Delete
10.7 kB
#!/usr/bin/env python3
"""
Inference script for Speech-to-Speech model.
Usage:
python inference.py --checkpoint ./checkpoints/stage2_best.pt --input audio.wav --output response.wav
python inference.py --checkpoint ./checkpoints/stage2_best.pt --text "Hello, how are you?"
"""
import os
import sys
import argparse
import torch
import torch.nn as nn
import numpy as np
# SNAC token offsets for Orpheus
SNAC_BASE_OFFSET = 128266
EOS_TOKEN = 128009
class SpeechAdapter(nn.Module):
"""Same architecture as training - must match exactly."""
def __init__(self, whisper_dim=1280, llm_dim=3072, downsample=5, intermediate_dim=2048):
super().__init__()
self.downsample = downsample
concat_dim = whisper_dim * downsample
self.ffn = nn.Sequential(
nn.Linear(concat_dim, intermediate_dim),
nn.GELU(),
nn.Linear(intermediate_dim, llm_dim),
nn.LayerNorm(llm_dim)
)
def forward(self, x):
B, T, D = x.shape
T_new = (T // self.downsample) * self.downsample
x = x[:, :T_new]
x = x.reshape(B, T_new // self.downsample, D * self.downsample)
return self.ffn(x)
def decode_snac_tokens(snac_tokens, device="cuda"):
"""Decode SNAC tokens to audio waveform."""
try:
from snac import SNAC
# Load SNAC model
snac = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device)
snac.eval()
# Remove offsets from tokens
raw_tokens = []
for i, tok in enumerate(snac_tokens):
pos = i % 7
offset = SNAC_BASE_OFFSET + pos * 4096
raw_tok = tok - offset
if 0 <= raw_tok < 4096:
raw_tokens.append(raw_tok)
if len(raw_tokens) == 0:
return None, 24000
# Reshape to SNAC format: 7 tokens per frame
num_frames = len(raw_tokens) // 7
if num_frames == 0:
return None, 24000
raw_tokens = raw_tokens[:num_frames * 7]
# SNAC expects [batch, layers, time] - 3 layers with different rates
# Layer 0: 1 token/frame, Layer 1: 2 tokens/frame, Layer 2: 4 tokens/frame
codes = []
for frame_idx in range(num_frames):
base = frame_idx * 7
codes.append(raw_tokens[base:base+7])
codes = torch.tensor(codes, device=device)
# Reorganize into SNAC layer format
layer0 = codes[:, 0:1].T # [1, num_frames]
layer1 = codes[:, 1:3].T.reshape(1, -1) # [1, num_frames*2]
layer2 = codes[:, 3:7].T.reshape(1, -1) # [1, num_frames*4]
with torch.no_grad():
audio = snac.decode([layer0, layer1, layer2])
return audio.cpu().numpy().squeeze(), 24000
except Exception as e:
print(f"SNAC decode error: {e}")
return None, 24000
def extract_whisper_features(audio_path, device="cuda"):
"""Extract Whisper encoder features from audio file."""
try:
from transformers import WhisperProcessor, WhisperModel
import librosa
# Load audio
audio, sr = librosa.load(audio_path, sr=16000)
# Load Whisper
processor = WhisperProcessor.from_pretrained("openai/whisper-large-v3")
model = WhisperModel.from_pretrained("openai/whisper-large-v3").to(device)
model.eval()
# Process
inputs = processor(audio, sampling_rate=16000, return_tensors="pt")
input_features = inputs.input_features.to(device)
with torch.no_grad():
encoder_outputs = model.encoder(input_features)
features = encoder_outputs.last_hidden_state
return features
except Exception as e:
print(f"Whisper feature extraction error: {e}")
return None
def generate_response(model, adapter, tokenizer, audio_embeds, device, max_new_tokens=500):
"""Generate interleaved text+audio response."""
# Get the base model for generation
if hasattr(model, 'get_base_model'):
base_model = model.get_base_model()
else:
base_model = model
# Start generation from audio embeddings
generated_tokens = []
# Create initial input from audio embeddings
current_embeds = audio_embeds
with torch.no_grad():
for step in range(max_new_tokens):
# Forward pass
outputs = model(inputs_embeds=current_embeds, use_cache=False)
logits = outputs.logits
# Get next token (greedy)
next_token_logits = logits[:, -1, :]
next_token = torch.argmax(next_token_logits, dim=-1)
token_id = next_token.item()
generated_tokens.append(token_id)
# Check for EOS
if token_id == EOS_TOKEN:
break
# Get embedding for next token
if hasattr(base_model, 'model'):
next_embed = base_model.model.embed_tokens(next_token.unsqueeze(0))
else:
next_embed = base_model.embed_tokens(next_token.unsqueeze(0))
# Append to current embeddings
current_embeds = torch.cat([current_embeds, next_embed], dim=1)
# Truncate if too long (keep last 2048 tokens)
if current_embeds.shape[1] > 2048:
current_embeds = current_embeds[:, -2048:]
return generated_tokens
def separate_tokens(generated_tokens):
"""Separate text and audio tokens from interleaved output."""
text_tokens = []
audio_tokens = []
for tok in generated_tokens:
if tok >= SNAC_BASE_OFFSET:
audio_tokens.append(tok)
elif tok != EOS_TOKEN:
text_tokens.append(tok)
return text_tokens, audio_tokens
def main():
parser = argparse.ArgumentParser(description="Speech-to-Speech Inference")
parser.add_argument("--checkpoint", type=str, required=True, help="Path to checkpoint (stage1 or stage2)")
parser.add_argument("--input", type=str, default=None, help="Input audio file")
parser.add_argument("--text", type=str, default=None, help="Input text (for testing without audio)")
parser.add_argument("--output", type=str, default="./output.wav", help="Output audio file")
parser.add_argument("--model_path", type=str, default="canopylabs/3b-es_it-ft-research_release")
parser.add_argument("--max_tokens", type=int, default=500)
parser.add_argument("--device", type=str, default=None)
args = parser.parse_args()
# Determine device
if args.device:
device = torch.device(args.device)
elif torch.cuda.is_available():
device = torch.device("cuda")
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
print(f"Device: {device}")
# Determine dtype
torch_dtype = torch.bfloat16 if device.type == 'cuda' else torch.float32
# Load tokenizer
print(f"Loading tokenizer: {args.model_path}")
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained(args.model_path)
# Load checkpoint
print(f"Loading checkpoint: {args.checkpoint}")
ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
has_lora = "lora" in ckpt
print(f"Checkpoint type: {'Stage 2 (Adapter + LoRA)' if has_lora else 'Stage 1 (Adapter only)'}")
# Load LLM
print(f"Loading LLM: {args.model_path}")
llm = AutoModelForCausalLM.from_pretrained(
args.model_path,
torch_dtype=torch_dtype,
attn_implementation="sdpa",
).to(device)
# Apply LoRA if Stage 2
if has_lora:
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
lora_dropout=0.0,
bias="none",
task_type=TaskType.CAUSAL_LM
)
llm = get_peft_model(llm, lora_config)
llm.load_state_dict(ckpt["lora"], strict=False)
print("LoRA weights loaded")
llm.eval()
# Load adapter
print("Loading adapter...")
adapter = SpeechAdapter(
whisper_dim=1280,
llm_dim=3072,
downsample=5,
intermediate_dim=2048
).to(device, dtype=torch_dtype)
adapter.load_state_dict(ckpt["adapter"])
adapter.eval()
print("Adapter loaded")
# Get input embeddings
if args.input:
print(f"Processing audio: {args.input}")
whisper_features = extract_whisper_features(args.input, device)
if whisper_features is None:
print("Failed to extract Whisper features")
return
audio_embeds = adapter(whisper_features.to(torch_dtype))
elif args.text:
print(f"Processing text: {args.text}")
# For text input, create dummy audio embeddings (zeros)
# This is just for testing the generation pipeline
dummy_features = torch.randn(1, 100, 1280, device=device, dtype=torch_dtype)
audio_embeds = adapter(dummy_features)
# Optionally prepend text tokens
text_tokens = tokenizer.encode(args.text, add_special_tokens=False)
print(f"Text tokens: {text_tokens[:10]}...")
else:
print("ERROR: Provide --input (audio file) or --text")
return
print(f"Audio embeddings shape: {audio_embeds.shape}")
# Generate response
print(f"Generating response (max {args.max_tokens} tokens)...")
generated_tokens = generate_response(
llm, adapter, tokenizer, audio_embeds, device,
max_new_tokens=args.max_tokens
)
print(f"Generated {len(generated_tokens)} tokens")
# Separate text and audio
text_tokens, audio_tokens = separate_tokens(generated_tokens)
print(f"Text tokens: {len(text_tokens)}, Audio tokens: {len(audio_tokens)}")
# Decode text
if text_tokens:
decoded_text = tokenizer.decode(text_tokens, skip_special_tokens=True)
print(f"\nGenerated text: {decoded_text}")
# Decode audio
if audio_tokens:
print(f"\nDecoding {len(audio_tokens)} audio tokens...")
audio, sr = decode_snac_tokens(audio_tokens, device)
if audio is not None:
import soundfile as sf
sf.write(args.output, audio, sr)
print(f"Audio saved: {args.output}")
else:
print("Failed to decode audio")
else:
print("No audio tokens generated")
# Show raw tokens for debugging
print(f"\nFirst 20 generated tokens: {generated_tokens[:20]}")
if __name__ == "__main__":
main()