multimodalart's picture
multimodalart HF Staff
Examples take audio only (drop empty text instruction column)
878ee9b verified
Raw
History Blame Contribute Delete
13.4 kB
"""Audio Interaction Model β€” Gradio demo.
Takes an audio clip (microphone or file) and an optional text instruction,
streams a text response from the AudioInteraction model.
"""
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # MUST come before torch / any CUDA-touching import
import torch
# Monkey-patch torch.load to allow loading the audio encoder checkpoint
# (which pickles numpy/object globals and fails under torch 2.6+ weights_only=True default)
_orig_torch_load = torch.load
def _patched_torch_load(*args, **kwargs):
return _orig_torch_load(*args, **{**kwargs, "weights_only": kwargs.get("weights_only", False)})
torch.load = _patched_torch_load
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import random
import numpy as np
import gradio as gr
import whisper
import tempfile
import time
import json
from pathlib import Path
from transformers import AutoConfig
from transformers.models.qwen2_5_omni.modeling_qwen2_5_omni import Qwen2_5OmniAudioEncoder
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
from src.audiointeraction.dataset.TOKENS import (
ASSISTANT, AUDIO_BEGIN, ENGLISH, KEEP_SILENCE, ONLINE, PAD,
SYSTEM, TEXT_BEGIN, TEXT_END,
HAPPY, SAD, ANGRY, SURPRISE, NORMAL, URGENT,
)
from src.audiointeraction.generate.base import (
AUDIO_TOKENS_PER_CHUNK, encode_audio_chunks, encode_silence_chunks, sample
)
from src.audiointeraction.model import GPT, Config
from src.audiointeraction.tokenizer import Tokenizer
# ── Constants ──────────────────────────────────────────────
MODEL_ID = "zhifeixie/AudioInteraction"
SYSTEM_PROMPT = (
"You are a helpful assistant. When there is no user text, if the audio contains a question, "
"please answer it. If it is a sound effect, determine based on the sound whether help is needed."
)
EMOTION_EMOJI = {
HAPPY: "😊",
SAD: "😒",
ANGRY: "😠",
SURPRISE: "😲",
NORMAL: "😐",
URGENT: "⚠️",
}
def set_seed(seed: int = 1337) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# ── Model loading (module scope, eager) ────────────────────
def _resolve_checkpoint_paths(checkpoint_dir: str):
ckpt = Path(checkpoint_dir)
return (
str(ckpt),
str(ckpt),
str(ckpt / "qwen25OmniConfig"),
str(ckpt / "audiointeraction_ChunkwisedEncoder.pth"),
)
def _load_audio_encoder(qwen_omni_ckpt, audio_tower_ckpt, device):
cfg = AutoConfig.from_pretrained(qwen_omni_ckpt)
audio_cfg = cfg.thinker_config.audio_config
encoder = Qwen2_5OmniAudioEncoder._from_config(audio_cfg)
state_dict = torch.load(audio_tower_ckpt, map_location=device)
encoder.load_state_dict(state_dict)
encoder.to(device).requires_grad_(False).eval()
return encoder
# Download model weights at module scope
print("Downloading model weights from HuggingFace...")
ckpt_dir = snapshot_download(
repo_id=MODEL_ID,
repo_type="model",
local_dir="./checkpoints",
resume_download=True,
)
print(f"Model downloaded to {ckpt_dir}")
model_config_dir, trained_checkpoint, qwen_omni_ckpt, audio_tower_ckpt = \
_resolve_checkpoint_paths(ckpt_dir)
set_seed(1337)
device = "cuda"
print("Loading language model...")
config = Config.from_file(Path(model_config_dir) / "model_config.yaml")
model = GPT(config)
model.max_seq_length = config.block_size
checkpoint_dir = Path(trained_checkpoint)
index_path = checkpoint_dir / "model.safetensors.index.json"
with open(index_path) as f:
index = json.load(f)
shard_files = sorted(set(index["weight_map"].values()))
state_dict = {}
for shard in shard_files:
state_dict.update(load_file(str(checkpoint_dir / shard), device="cpu"))
missing, unexpected = model.load_state_dict(state_dict, strict=True)
if missing or unexpected:
print(f"[load_model] missing={missing[:3]}… unexpected={unexpected[:3]}…")
del state_dict # free memory
model = model.to(device).to(torch.bfloat16)
model.eval()
print("Loading audio encoder...")
audio_encoder = _load_audio_encoder(qwen_omni_ckpt, audio_tower_ckpt, device)
# Keep audio encoder in float32 β€” the original code does not convert it to bf16
tokenizer = Tokenizer(model_config_dir)
system_ids = tokenizer.encode(SYSTEM_PROMPT).cpu().tolist()
prefix_ids = torch.LongTensor(
[ONLINE, ENGLISH, SYSTEM, TEXT_BEGIN] + system_ids + [TEXT_END]
).to(device)
print("Model loaded successfully!")
# ── Inference ──────────────────────────────────────────────
@spaces.GPU(duration=180)
def interact(audio_file, text_instruction=""):
"""Process an audio clip and optional text instruction, return the model's text response.
Args:
audio_file: Path to an audio file (wav, mp3, m4a, etc.).
text_instruction: Optional text instruction to guide the model's response.
Returns:
A tuple of (full_response_text, status_text) where full_response_text
is the model's text response and status_text provides processing info.
"""
if audio_file is None:
return "Please provide an audio file.", "No audio input"
t0 = time.perf_counter()
# Set up KV cache on the correct device
model.set_kv_cache(batch_size=1, device=device, dtype=torch.bfloat16)
model.max_seq_length = model.config.block_size
# Ensure cos/sin rope buffers are on the right device
model.cos = model.cos.to(device)
model.sin = model.sin.to(device)
# Build prefix
token = prefix_ids.clone()
input_pos = torch.arange(0, prefix_ids.size(0), device=device, dtype=torch.int64)
input_pos_maxp1 = torch.tensor(prefix_ids.size(0), device=device)
# If text instruction provided, append it
if text_instruction.strip():
text_ids = tokenizer.encode(text_instruction).cpu().tolist()
text_tokens = torch.LongTensor(
[TEXT_BEGIN] + text_ids + [TEXT_END]
).to(device)
token = torch.cat([token, text_tokens])
input_pos = torch.cat([input_pos, torch.arange(
prefix_ids.size(0), prefix_ids.size(0) + len(text_tokens),
device=device, dtype=torch.int64
)])
# Encode audio
audio_chunks = encode_audio_chunks(audio_file, audio_encoder, device)
# Follow the original inference pattern: audio then 1s silence
# The model often responds during the silence round after audio
silence_chunks = encode_silence_chunks(1.0, audio_encoder, device)
all_chunks = audio_chunks + silence_chunks
n_chunks = len(all_chunks)
# Run streaming inference
response_parts = []
current_text = []
listening = True
audio_idx = -1
emotion_tag = ""
max_tokens = 4096
try:
with torch.inference_mode():
for _ in range(max_tokens - input_pos.numel()):
if listening:
audio_idx += 1
if audio_idx >= n_chunks:
break
# Append [AUDIO_BEGIN, PAD*N, ASSISTANT]
new_tokens = torch.LongTensor(
[AUDIO_BEGIN] + [PAD] * AUDIO_TOKENS_PER_CHUNK + [ASSISTANT]
).to(device)
new_positions = input_pos[-1] + torch.arange(1, len(new_tokens) + 1, device=device)
token = torch.cat([token, new_tokens])
input_pos = torch.cat([input_pos, new_positions])
input_pos_maxp1 = input_pos_maxp1 + len(new_tokens)
logits = model(
token.view(1, -1), None, 1,
all_chunks[audio_idx].to(device),
input_pos,
input_pos_maxp1=input_pos_maxp1,
audio_tokens_per_chunk=AUDIO_TOKENS_PER_CHUNK,
)
else:
logits = model(
token.view(1, -1), None, 1,
None,
input_pos,
input_pos_maxp1=input_pos_maxp1,
audio_tokens_per_chunk=AUDIO_TOKENS_PER_CHUNK,
)
token = sample(logits, temperature=0.0, top_p=0.0).to(torch.int64)
int_token = token.item()
input_pos = input_pos[-1].unsqueeze(0).add_(1)
input_pos_maxp1 = input_pos_maxp1 + 1
if listening:
if int_token == TEXT_BEGIN:
listening = False
current_text = [int_token]
elif int_token == KEEP_SILENCE:
pass # model stays silent
else:
break
else:
current_text.append(int_token)
if int_token == TEXT_END:
# Decode the turn (skip TEXT_BEGIN and optional emotion)
text_tokens = current_text[1:-1] # strip TEXT_BEGIN and TEXT_END
if text_tokens:
# Check if first token is an emotion tag
if text_tokens[0] in EMOTION_EMOJI:
emotion_tag = EMOTION_EMOJI[text_tokens[0]]
text_tokens = text_tokens[1:]
decoded = tokenizer.decode(torch.tensor(text_tokens))
if decoded:
response_parts.append((emotion_tag, decoded))
current_text = []
listening = True
elif len(current_text) >= 2:
# Could be streaming text, but we collect at TEXT_END
pass
# Handle any incomplete turn
if not listening and len(current_text) > 1:
text_tokens = current_text[1:] # skip TEXT_BEGIN
if text_tokens and text_tokens[0] in EMOTION_EMOJI:
emotion_tag = EMOTION_EMOJI[text_tokens[0]]
text_tokens = text_tokens[1:]
decoded = tokenizer.decode(torch.tensor(text_tokens))
if decoded:
response_parts.append((emotion_tag, decoded))
finally:
model.clear_kv_cache()
elapsed = time.perf_counter() - t0
if not response_parts:
return "The model listened to the audio but chose to stay silent. (This is expected for non-speech sounds when no response is warranted.)", \
f"Processed {n_chunks} audio chunks in {elapsed:.1f}s β€” model stayed silent"
# Format response
formatted = []
for emoji, text in response_parts:
if emoji:
formatted.append(f"{emoji} {text}")
else:
formatted.append(text)
full_response = "\n".join(formatted)
status = f"Processed {n_chunks} audio chunks in {elapsed:.1f}s β€” {len(response_parts)} response(s)"
return full_response, status
# ── Gradio UI ──────────────────────────────────────────────
CSS = """
#col-container { max-width: 900px; margin: 0 auto; }
"""
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
gr.Markdown("# πŸŽ™οΈ Audio Interaction Model")
gr.Markdown(
"Upload or record an audio clip β€” the model listens and responds with text. "
"This is a demo of [AudioInteraction](https://huggingface.co/zhifeixie/AudioInteraction), "
"a streaming audio-language model that perceives audio and decides when to speak."
)
with gr.Column(elem_id="col-container"):
with gr.Row():
audio_input = gr.Audio(
label="Audio Input",
type="filepath",
sources=["microphone", "upload"],
)
text_input = gr.Textbox(
label="Text Instruction (optional)",
placeholder="e.g., 'Translate this to English' or leave blank for auto-response",
lines=2,
)
run_btn = gr.Button("Run", variant="primary")
with gr.Row():
response_output = gr.Textbox(
label="Model Response",
lines=8,
interactive=False,
)
status_output = gr.Textbox(
label="Status",
interactive=False,
)
run_btn.click(
fn=interact,
inputs=[audio_input, text_input],
outputs=[response_output, status_output],
)
gr.Examples(
examples=[
["samples/sample01_count_bark.wav"],
["samples/sample03_cough_music.wav"],
["samples/sample02a_translate.wav"],
["samples/sample02b_translate.wav"],
],
inputs=[audio_input],
outputs=[response_output, status_output],
fn=interact,
cache_examples=True,
cache_mode="lazy",
)
demo.launch(mcp_server=True)