PitchIn / inference.py
Hayeonbang's picture
Add PitchIn-Q free ZeroGPU demo with verified adapter
a50a0b9 verified
Raw History Blame Contribute Delete
3.9 kB
"""Text-only PitchIn-Q inference using the paper's merged checkpoint."""
import os
import time
from pathlib import Path
import spaces # Must precede torch for ZeroGPU's CUDA initialization.
import torch
from peft import PeftModel
from transformers import AutoConfig, AutoProcessor, Qwen2_5OmniForConditionalGeneration
GENERATION = dict(
do_sample=True, temperature=0.6, top_p=0.85, top_k=30,
repetition_penalty=1.08, max_new_tokens=384,
eos_token_id=151645, pad_token_id=151643,
)
_ENGINE = None
@spaces.GPU(duration=120)
def generate_on_gpu(wave, question):
# Keep the startup model in the forked worker's globals. Passing a bound
# method's `self` through ZeroGPU's argument queue reserializes it on CPU.
if _ENGINE is None:
raise RuntimeError("PitchIn is not initialized.")
return _ENGINE._generate(wave, question)
class PitchIn:
def __init__(self):
global _ENGINE
root = Path(__file__).parent
merged_path = os.environ.get("PITCHIN_MODEL_PATH")
model_path = merged_path or os.environ.get("PITCHIN_BASE_MODEL", "Qwen/Qwen2.5-Omni-7B")
model_args = {"local_files_only": True} if Path(model_path).is_dir() else {
"revision": "ae9e1690543ffd5c0221dc27f79834d0294cba00",
}
processor_path = merged_path or os.environ.get("PITCHIN_PROCESSOR_PATH", str(root / "processor"))
config = AutoConfig.from_pretrained(model_path, **model_args)
config.enable_audio_output = False
self.processor = AutoProcessor.from_pretrained(processor_path, local_files_only=True)
outer = Qwen2_5OmniForConditionalGeneration.from_pretrained(
model_path, config=config, dtype=torch.bfloat16,
attn_implementation="sdpa", **model_args,
)
if not merged_path:
adapter_path = os.environ.get("PITCHIN_ADAPTER_PATH", str(root / "adapter"))
outer.thinker = PeftModel.from_pretrained(
outer.thinker, adapter_path, is_trainable=False, torch_device="cpu",
).merge_and_unload()
self.model = outer.thinker.to("cuda").eval()
if self.processor.feature_extractor.sampling_rate != 16000:
raise RuntimeError("Unexpected checkpoint audio sample rate.")
_ENGINE = self
def generate(self, wave, question):
return generate_on_gpu(wave, question)
def _generate(self, wave, question):
started = time.monotonic()
messages = [{"role": "user", "content": [
{"type": "audio", "audio": "uploaded_audio.wav"},
{"type": "text", "text": question},
]}]
text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = self.processor(
text=[text], audio=[wave], sampling_rate=16000, return_tensors="pt",
audio_kwargs={"truncation": False},
text_kwargs={"padding": True, "truncation": False},
)
inputs = {k: v.to("cuda", dtype=torch.bfloat16 if v.is_floating_point() else v.dtype)
if isinstance(v, torch.Tensor) else v for k, v in inputs.items()}
input_length = inputs["input_ids"].shape[1]
with torch.inference_mode():
tokens = self.model.generate(**inputs, **GENERATION)[0, input_length:]
response = self.processor.tokenizer.decode(tokens, skip_special_tokens=True).strip()
if not response:
raise RuntimeError("The model returned an empty answer. Please try again.")
count = len(tokens)
stopped_at_limit = count == GENERATION["max_new_tokens"] and tokens[-1].item() != GENERATION["eos_token_id"]
status = f"{len(wave) / 16000:.1f}s audio · {time.monotonic() - started:.1f}s generation"
if stopped_at_limit:
status += " · Response reached the demo length limit."
return response, status