omini-model / passo4_inference.py
marcos
feat: Refactor training with SOLID principles and add optimizations
e20f447
Raw
History Blame Contribute Delete
13.2 kB
#!/usr/bin/env python3
"""
Passo 4: Inference - Speech-to-Speech
Load trained model and generate audio responses from audio input.
"""
import os
import sys
import argparse
import torch
import torchaudio
import numpy as np
from pathlib import Path
# SNAC token configuration
SNAC_BASE = 128266
SNAC_MAX = SNAC_BASE + 7 * 4096 # 7 positions per frame, 4096 tokens each
EOS_TOKEN = 128009
def load_models(checkpoint_path: str, device: str = "cuda"):
"""Load all models for inference."""
from transformers import WhisperModel, WhisperFeatureExtractor, AutoTokenizer
import snac
print("Loading models...")
# Load Whisper encoder
print(" Whisper encoder...")
whisper_model = WhisperModel.from_pretrained("openai/whisper-large-v3").to(device)
whisper_model.eval()
feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-large-v3")
# Load checkpoint
print(f" Loading checkpoint: {checkpoint_path}")
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
# Debug: show checkpoint keys
print(f" Checkpoint keys: {list(checkpoint.keys())}")
# Load LLM with LoRA
print(" Loading LLM...")
from transformers import AutoModelForCausalLM
llm = AutoModelForCausalLM.from_pretrained(
"canopylabs/3b-es_it-ft-research_release",
torch_dtype=torch.bfloat16,
device_map=device
)
# Load LoRA weights if present
lora_loaded = False
for lora_key in ['lora', 'lora_state_dict']:
if lora_key in checkpoint:
print(f" Found LoRA weights with key '{lora_key}'")
from peft import LoraConfig, get_peft_model
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, # No dropout for inference
bias="none",
task_type="CAUSAL_LM"
)
llm = get_peft_model(llm, lora_config)
# Load the LoRA state dict
lora_state = checkpoint[lora_key]
print(f" LoRA state dict has {len(lora_state)} keys")
if len(lora_state) > 0:
print(f" Sample keys: {list(lora_state.keys())[:3]}")
# Try to load with strict=False to see what matches
result = llm.load_state_dict(lora_state, strict=False)
print(f" LoRA load - Missing: {len(result.missing_keys)}, Unexpected: {len(result.unexpected_keys)}")
if result.missing_keys:
print(f" Missing keys (first 3): {result.missing_keys[:3]}")
if result.unexpected_keys:
print(f" Unexpected keys (first 3): {result.unexpected_keys[:3]}")
lora_loaded = True
break
if not lora_loaded:
print(" WARNING: No LoRA weights found in checkpoint! Model will use base weights only.")
print(" This might result in no SNAC token generation.")
llm.eval()
# Load Speech Adapter
print(" Loading Speech Adapter...")
from passo2_finetune_stage1 import SpeechAdapter
adapter = SpeechAdapter(
whisper_dim=1280,
llm_dim=3072,
downsample=5
).to(device)
adapter_loaded = False
for adapter_key in ['adapter', 'adapter_state_dict']:
if adapter_key in checkpoint:
print(f" Found adapter weights with key '{adapter_key}'")
result = adapter.load_state_dict(checkpoint[adapter_key], strict=False)
print(f" Adapter load - Missing: {len(result.missing_keys)}, Unexpected: {len(result.unexpected_keys)}")
adapter_loaded = True
break
if not adapter_loaded:
print(" WARNING: No adapter weights found in checkpoint!")
adapter.eval()
# Load SNAC decoder
print(" SNAC decoder...")
snac_model = snac.SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device)
snac_model.eval()
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained("canopylabs/3b-es_it-ft-research_release")
# Debug tokenizer info
print(f" Tokenizer vocab size: {tokenizer.vocab_size}")
print(f" BOS token: {tokenizer.bos_token} (id={tokenizer.bos_token_id})")
print(f" EOS token: {tokenizer.eos_token} (id={tokenizer.eos_token_id})")
print(f" SNAC token range: {SNAC_BASE} to {SNAC_BASE + 3*4096 - 1}")
print("Models loaded!")
return whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer
def encode_audio(audio_path: str, whisper_model, feature_extractor, adapter, device: str):
"""Encode input audio to embeddings."""
# Load audio
waveform, sr = torchaudio.load(audio_path)
if sr != 16000:
waveform = torchaudio.functional.resample(waveform, sr, 16000)
# Extract Whisper features
with torch.no_grad():
inputs = feature_extractor(waveform.squeeze().numpy(), sampling_rate=16000, return_tensors="pt")
whisper_features = whisper_model.encoder(
inputs.input_features.to(device)
).last_hidden_state
# Adapt to LLM space
adapted = adapter(whisper_features)
return adapted.to(torch.bfloat16)
def decode_snac_tokens(tokens: list, snac_model, device: str):
"""Decode SNAC tokens to audio waveform.
SNAC uses 3 hierarchical layers with 1:2:4 ratio.
Each "frame" has 7 tokens in order:
- 1 token from layer 0 (position 0)
- 2 tokens from layer 1 (positions 1, 2)
- 4 tokens from layer 2 (positions 3, 4, 5, 6)
Tokens are offset by: SNAC_BASE + (position % 7) * 4096
"""
if len(tokens) == 0:
print(" Warning: No SNAC tokens to decode")
return np.zeros(24000, dtype=np.float32) # 1 second of silence
layer0_tokens = []
layer1_tokens = []
layer2_tokens = []
# Parse tokens by removing position-based offsets
for i, tok in enumerate(tokens):
pos = i % 7
# Remove the offset to get the original code
original = tok - SNAC_BASE - (pos * 4096)
# Ensure codes are valid (0-4095)
if original < 0 or original >= 4096:
print(f" Warning: Invalid code {original} at position {i} (token={tok}, pos={pos})")
original = max(0, min(4095, original))
if pos == 0:
layer0_tokens.append(original)
elif pos in [1, 2]:
layer1_tokens.append(original)
else: # pos in [3, 4, 5, 6]
layer2_tokens.append(original)
# Calculate how many complete frames we have
n_frames = len(tokens) // 7
if n_frames == 0:
print(f" Warning: Not enough tokens for a complete frame ({len(tokens)} tokens)")
return np.zeros(24000, dtype=np.float32)
print(f" Decoding {n_frames} frames ({len(layer0_tokens)} L0, {len(layer1_tokens)} L1, {len(layer2_tokens)} L2)")
# Truncate to complete frames
layer0_tokens = layer0_tokens[:n_frames]
layer1_tokens = layer1_tokens[:n_frames * 2]
layer2_tokens = layer2_tokens[:n_frames * 4]
codes = [
torch.tensor([layer0_tokens], dtype=torch.long, device=device),
torch.tensor([layer1_tokens], dtype=torch.long, device=device),
torch.tensor([layer2_tokens], dtype=torch.long, device=device)
]
# Decode
with torch.no_grad():
audio = snac_model.decode(codes)
return audio.squeeze().cpu().numpy()
def generate_response(
audio_input: str,
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
device: str,
max_new_tokens: int = 500,
temperature: float = 0.7,
debug: bool = True,
use_prompt: bool = False
):
"""Generate speech response from audio input."""
# Encode input audio
print("Encoding input audio...")
audio_embeddings = encode_audio(audio_input, whisper_model, feature_extractor, adapter, device)
print(f" Audio embeddings shape: {audio_embeddings.shape}")
# Generate with LLM
print("Generating response...")
with torch.no_grad():
# Get embeddings layer
embed_layer = llm.get_input_embeddings()
if use_prompt:
# Option A: Add BOS token after audio
bos_id = tokenizer.bos_token_id or 128000
prompt_tokens = torch.tensor([[bos_id]], dtype=torch.long, device=device)
prompt_embeds = embed_layer(prompt_tokens)
input_embeds = torch.cat([audio_embeddings.to(torch.bfloat16), prompt_embeds.to(torch.bfloat16)], dim=1)
else:
# Option B: Just audio embeddings (as trained)
# During training, target directly follows audio embeddings
input_embeds = audio_embeddings.to(torch.bfloat16)
print(f" Input embeds shape: {input_embeds.shape}")
# Create attention mask
attention_mask = torch.ones(input_embeds.shape[:2], dtype=torch.long, device=device)
# Different generation strategies
# Strategy 1: Pure greedy (most stable)
if temperature <= 0.01:
outputs = llm.generate(
inputs_embeds=input_embeds,
attention_mask=attention_mask,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=EOS_TOKEN,
use_cache=True
)
# Strategy 2: Sampling with nucleus
else:
outputs = llm.generate(
inputs_embeds=input_embeds,
attention_mask=attention_mask,
max_new_tokens=max_new_tokens,
temperature=temperature,
do_sample=True,
top_p=0.9,
top_k=50,
repetition_penalty=1.1,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=EOS_TOKEN,
use_cache=True
)
# Extract generated tokens
generated_tokens = outputs[0].tolist()
if debug:
print(f"\n=== DEBUG: Generated {len(generated_tokens)} total tokens ===")
# Show first 50 tokens
print(f"First 50 tokens: {generated_tokens[:50]}")
# Show last 20 tokens
print(f"Last 20 tokens: {generated_tokens[-20:]}")
# Show token ranges with correct SNAC range
text_tokens = [t for t in generated_tokens if t < SNAC_BASE]
snac_range_tokens = [t for t in generated_tokens if SNAC_BASE <= t < SNAC_MAX]
other_high = [t for t in generated_tokens if t >= SNAC_MAX]
print(f"\nToken distribution:")
print(f" Text tokens (<{SNAC_BASE}): {len(text_tokens)}")
print(f" SNAC tokens ({SNAC_BASE}-{SNAC_MAX}): {len(snac_range_tokens)}")
print(f" Other high tokens (>={SNAC_MAX}): {len(other_high)}")
# Try to decode text tokens
if text_tokens:
try:
decoded_text = tokenizer.decode(text_tokens, skip_special_tokens=False)
print(f"\nDecoded text: {decoded_text[:500]}")
except Exception as e:
print(f"Could not decode text: {e}")
# Show some SNAC tokens if any
if snac_range_tokens:
print(f"\nFirst 20 SNAC tokens: {snac_range_tokens[:20]}")
# Extract SNAC tokens from output (correct range)
snac_tokens = [t for t in generated_tokens if SNAC_BASE <= t < SNAC_MAX]
print(f"\nGenerated {len(snac_tokens)} SNAC tokens")
# Decode to audio
print("Decoding to audio...")
audio_output = decode_snac_tokens(snac_tokens, snac_model, device)
return audio_output
def main():
parser = argparse.ArgumentParser(description="Speech-to-Speech Inference")
parser.add_argument("--checkpoint", type=str, required=True, help="Path to model checkpoint")
parser.add_argument("--input", type=str, required=True, help="Input audio file")
parser.add_argument("--output", type=str, default="output.wav", help="Output audio file")
parser.add_argument("--max_tokens", type=int, default=1000, help="Max tokens to generate")
parser.add_argument("--temperature", type=float, default=0.7, help="Sampling temperature (0 for greedy)")
parser.add_argument("--use_prompt", action="store_true", help="Add BOS token after audio embeddings")
parser.add_argument("--no_debug", action="store_true", help="Disable debug output")
args = parser.parse_args()
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Device: {device}")
# Load models
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer = load_models(
args.checkpoint, device
)
# Generate
audio_output = generate_response(
args.input,
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
device,
max_new_tokens=args.max_tokens,
temperature=args.temperature,
debug=not args.no_debug,
use_prompt=args.use_prompt
)
# Save output
import soundfile as sf
sf.write(args.output, audio_output, 24000)
print(f"Saved output to: {args.output}")
if __name__ == "__main__":
main()