chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:05:25 +08:00
commit b0897668a9
102 changed files with 20314 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
# Initially taken from Github's Python gitignore file
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# tests and logs
tests/fixtures/cached_*_text.txt
logs/
lightning_logs/
lang_code_data/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# vscode
.vs
.vscode
# Pycharm
.idea
# TF code
tensorflow_code
# Models
proc_data
# examples
runs
/runs_old
/wandb
/examples/runs
/examples/**/*.args
/examples/rag/sweep
# data
/data
serialization_dir
# emacs
*.*~
debug.env
# vim
.*.swp
#ctags
tags
# pre-commit
.pre-commit*
# .lock
*.lock
# DS_Store (MacOS)
.DS_Store
# ruff
.ruff_cache
# our proj
/output/
/outputs/
/checkpoint/
/checkpoints/
exp
.gradio/
experimental_voices
+43
View File
@@ -0,0 +1,43 @@
# Contributing to VibeVoice
Thank you for your interest in **VibeVoice**. VibeVoice is an academic-oriented research project.
Our core principles are **code minimalism**, **high readability**, and **functional purity**.
## 1. Preferred Contributions
- **Bug Fixes & New Features**
These are our primary focus.
- **Minimalism First**
Code must be concise, clear, and minimal.
## 2. Rejected Patterns
- **Over-Engineering**
We will reject unnecessary encapsulation, excessive abstraction, or complex architectural refactoring.
Please remember this is research-oriented code, not a commercial enterprise project.
- **Style Tweaks**
PRs solely for formatting, beautification, or non-functional style adjustments will be rejected.
- **Non-English Contributions**
PRs containing non-English code comments, documentation, commit messages, or descriptions will be rejected.
## 3. Review Policy
- **Line-by-Line Review**
Maintainers strictly audit every single line of change manually. Before submitting, ensure you have personally scrutinized and fully mastered all code logic.
- **Caution Regarding AI-Generated Code**
LLM-generated code often contains redundant logic and subtle defects. Large chunks of AI-generated code will be rejected unless they have undergone rigorous human cleaning and verification.
Ensure that every line of code has an absolute necessity to exist.
## 4. Documentation Standards
- **Precise & Concise**
VibeVoice's documentation serves a global research community. We hold extremely high standards: changes must be succinct and accurate.
Please eliminate all verbosity and redundancy to ensure maximum information density.
---
We look forward to receiving **precise, streamlined, and substantively valuable** research contributions.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+207
View File
@@ -0,0 +1,207 @@
<div align="center">
## 🎙️ VibeVoice: Open-Source Frontier Voice AI
[![Project Page](https://img.shields.io/badge/Project-Page-blue?logo=githubpages)](https://microsoft.github.io/VibeVoice)
[![Hugging Face](https://img.shields.io/badge/HuggingFace-Collection-orange?logo=huggingface)](https://huggingface.co/collections/microsoft/vibevoice-68a2ef24a875c44be47b034f)
[![TTS Report](https://img.shields.io/badge/TTS-Report-red?logo=arxiv)](https://openreview.net/pdf?id=FihSkzyxdv)
[![ASR Report](https://img.shields.io/badge/ASR-Report-yellow?logo=arxiv)](https://arxiv.org/pdf/2601.18184)
[![Colab](https://img.shields.io/badge/StreamingTTS-Colab-green?logo=googlecolab)](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/VibeVoice_colab.ipynb)
[![ASR Playground](https://img.shields.io/badge/ASR-Playground-6F42C1?logo=gradio)](https://aka.ms/vibevoice-asr)
[![microsoft%2FVibeVoice | Trendshift](https://trendshift.io/api/badge/repositories/15465)](https://trendshift.io/repositories/15465)
</div>
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="Figures/VibeVoice_logo_white.png">
<img src="Figures/VibeVoice_logo.png" alt="VibeVoice Logo" width="300">
</picture>
</div>
<div align="left">
<h3>📰 News</h3>
<strong>2026-03-06: 🚀 VibeVoice ASR is now part of a <a href="https://huggingface.co/microsoft/VibeVoice-ASR-HF">Transformers release</a>! You can now use our speech recognition model directly through the Hugging Face Transformers library for seamless integration into your projects.</strong>
<strong>2026-01-21:</strong> 📣 We open-sourced <a href="docs/vibevoice-asr.md"><strong>VibeVoice-ASR</strong></a>, a unified speech-to-text model designed to handle 60-minute long-form audio in a single pass, generating structured transcriptions containing Who (Speaker), When (Timestamps), and What (Content), with support for User-Customized Context. Try it in [Playground](https://aka.ms/vibevoice-asr).
- ⭐️ VibeVoice-ASR is natively multilingual, supporting over 50 languages — check the [supported languages](docs/vibevoice-asr.md#language-distribution) for details.
- 🔥 The VibeVoice-ASR [finetuning code](finetuning-asr/README.md) is now available!
- ⚡️ **vLLM inference** is now supported for faster inference; see [vllm-asr](docs/vibevoice-vllm-asr.md) for more details.
- 📑 [VibeVoice-ASR Technique Report](https://arxiv.org/pdf/2601.18184) is available.
2025-12-16: 📣 We added experimental speakers to <a href="docs/vibevoice-realtime-0.5b.md"><strong>VibeVoiceRealtime0.5B</strong></a> for exploration, including multilingual voices in nine languages (DE, FR, IT, JP, KR, NL, PL, PT, ES) and 11 distinct English style voices. [Try it](docs/vibevoice-realtime-0.5b.md#optional-more-experimental-voices). More speaker types will be added over time.
2025-12-03: 📣 We open-sourced <a href="docs/vibevoice-realtime-0.5b.md"><strong>VibeVoiceRealtime0.5B</strong></a>, a realtime texttospeech model that supports streaming text input and robust long-form speech generation. Try it on [Colab](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb).
2025-09-05: VibeVoice is an open-source research framework intended to advance collaboration in the speech synthesis community. After release, we discovered instances where the tool was used in ways inconsistent with the stated intent. Since responsible use of AI is one of Microsofts guiding principles, we have removed the VibeVoice-TTS code from this repository.
2025-08-25: 📣 We open-sourced <a href="docs/vibevoice-tts.md"><strong>VibeVoice-TTS</strong></a>, a long-form multi-speaker text-to-speech model that can synthesize speech up to 90 minutes long with up to 4 distinct speakers. — accepted as an [Oral](https://openreview.net/forum?id=FihSkzyxdv) at ICLR 2026! 🔥
</div>
## Overview
VibeVoice is a **family of open-source frontier voice AI models** that includes both Text-to-Speech (TTS) and Automatic Speech Recognition (ASR) models.
A core innovation of VibeVoice is its use of continuous speech tokenizers (Acoustic and Semantic) operating at an ultra-low frame rate of **7.5 Hz**. These tokenizers efficiently preserve audio fidelity while significantly boosting computational efficiency for processing long sequences. VibeVoice employs a [next-token diffusion](https://arxiv.org/abs/2412.08635) framework, leveraging a Large Language Model (LLM) to understand textual context and dialogue flow, and a diffusion head to generate high-fidelity acoustic details.
For more information, demos, and examples, please visit our [Project Page](https://microsoft.github.io/VibeVoice).
<div align="center">
| Model | Weight | Quick Try |
|-------|--------------|---------|
| VibeVoice-ASR-7B | [HF Link](https://huggingface.co/microsoft/VibeVoice-ASR) | [Playground](https://aka.ms/vibevoice-asr) |
| VibeVoice-TTS-1.5B | [HF Link](https://huggingface.co/microsoft/VibeVoice-1.5B) | Disabled |
| VibeVoice-Realtime-0.5B | [HF Link](https://huggingface.co/microsoft/VibeVoice-Realtime-0.5B) | [Colab](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb) |
</div>
## Models
### 1. 📖 [VibeVoice-ASR](docs/vibevoice-asr.md) - Long-form Speech Recognition
**VibeVoice-ASR** is a unified speech-to-text model designed to handle **60-minute long-form audio** in a single pass, generating structured transcriptions containing **Who (Speaker), When (Timestamps), and What (Content)**, with support for **Customized Hotwords**.
- **🕒 60-minute Single-Pass Processing**:
Unlike conventional ASR models that slice audio into short chunks (often losing global context), VibeVoice ASR accepts up to **60 minutes** of continuous audio input within 64K token length. This ensures consistent speaker tracking and semantic coherence across the entire hour.
- **👤 Customized Hotwords**:
Users can provide customized hotwords (e.g., specific names, technical terms, or background info) to guide the recognition process, significantly improving accuracy on domain-specific content.
- **📝 Rich Transcription (Who, When, What)**:
The model jointly performs ASR, diarization, and timestamping, producing a structured output that indicates *who* said *what* and *when*.
[📖 Documentation](docs/vibevoice-asr.md) | [🤗 Hugging Face](https://huggingface.co/microsoft/VibeVoice-ASR) | [🎮 Playground](https://aka.ms/vibevoice-asr) | [🛠️ Finetuning](finetuning-asr/README.md) | [📊 Paper](docs/VibeVoice-ASR-Report.pdf)
<p align="center">
<img src="Figures/DER.jpg" alt="DER" width="50%"><br>
<img src="Figures/cpWER.jpg" alt="cpWER" width="50%"><br>
<img src="Figures/tcpWER.jpg" alt="tcpWER" width="50%">
</p>
<div align="center" id="vibevoice-asr">
https://github.com/user-attachments/assets/acde5602-dc17-4314-9e3b-c630bc84aefa
</div>
<br>
### 2. 🎙️ [VibeVoice-TTS](docs/vibevoice-tts.md) - Long-form Multi-speaker TTS
**Best for**: Long-form conversational audio, podcasts, multi-speaker dialogues
- **⏱️ 90-minute Long-form Generation**:
Synthesizes conversational/single-speaker speech up to **90 minutes** in a single pass, maintaining speaker consistency and semantic coherence throughout.
- **👥 Multi-speaker Support**:
Supports up to **4 distinct speakers** in a single conversation, with natural turn-taking and speaker consistency across long dialogues.
- **🎭 Expressive Speech**:
Generates expressive, natural-sounding speech that captures conversational dynamics and emotional nuances.
- **🌐 Multi-lingual Support**:
Supports English, Chinese and other languages.
[📖 Documentation](docs/vibevoice-tts.md) | [🤗 Hugging Face](https://huggingface.co/microsoft/VibeVoice-1.5B) | [📊 Paper](https://arxiv.org/pdf/2508.19205)
<div align="center">
<img src="Figures/VibeVoice-TTS-results.jpg" alt="VibeVoice Results" width="80%">
</div>
**English**
<div align="center">
https://github.com/user-attachments/assets/0967027c-141e-4909-bec8-091558b1b784
</div>
**Chinese**
<div align="center">
https://github.com/user-attachments/assets/322280b7-3093-4c67-86e3-10be4746c88f
</div>
**Cross-Lingual**
<div align="center">
https://github.com/user-attachments/assets/838d8ad9-a201-4dde-bb45-8cd3f59ce722
</div>
**Spontaneous Singing**
<div align="center">
https://github.com/user-attachments/assets/6f27a8a5-0c60-4f57-87f3-7dea2e11c730
</div>
**Long Conversation with 4 people**
<div align="center">
https://github.com/user-attachments/assets/a357c4b6-9768-495c-a576-1618f6275727
</div>
<br>
### 3. ⚡ [VibeVoice-Streaming](docs/vibevoice-realtime-0.5b.md) - Real-time Streaming TTS
VibeVoice-Realtime is a **lightweight realtime** text-to-speech model supporting **streaming text input** and **robust long-form speech generation**.
- Parameter size: 0.5B (deployment-friendly)
- Real-time TTS (~300 milliseconds first audible latency)
- Streaming text input
- Robust long-form speech generation (~10 minutes)
[📖 Documentation](docs/vibevoice-realtime-0.5b.md) | [🤗 Hugging Face](https://huggingface.co/microsoft/VibeVoice-Realtime-0.5B) | [🚀 Colab](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb)
<div align="center" id="generated-example-audio-vibevoice-realtime">
https://github.com/user-attachments/assets/0901d274-f6ae-46ef-a0fd-3c4fba4f76dc
</div>
<br>
## Contributing
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
## ⚠️ Risks and Limitations
While efforts have been made to optimize it through various techniques, it may still produce outputs that are unexpected, biased, or inaccurate. VibeVoice inherits any biases, errors, or omissions produced by its base model (specifically, Qwen2.5 1.5b in this release).
Potential for Deepfakes and Disinformation: High-quality synthetic speech can be misused to create convincing fake audio content for impersonation, fraud, or spreading disinformation. Users must ensure transcripts are reliable, check content accuracy, and avoid using generated content in misleading ways. Users are expected to use the generated content and to deploy the models in a lawful manner, in full compliance with all applicable laws and regulations in the relevant jurisdictions. It is best practice to disclose the use of AI when sharing AI-generated content.
We do not recommend using VibeVoice in commercial or real-world applications without further testing and development. This model is intended for research and development purposes only. Please use responsibly.
## Star History
![Star History Chart](https://api.star-history.com/svg?repos=Microsoft/vibevoice&type=date&legend=top-left)
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`microsoft/VibeVoice`
- 原始仓库:https://github.com/microsoft/VibeVoice
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+14
View File
@@ -0,0 +1,14 @@
<!-- BEGIN MICROSOFT SECURITY.MD V1.0.0 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which
includes all source code repositories in our GitHub organizations.
**Please do not report security vulnerabilities through public GitHub issues.**
For security reporting information, locations, contact information, and policies,
please review the latest guidance for Microsoft repositories at
[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
echo "[INFO] Starting download of experimental voices..."
# Absolute path of the current script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Target directory relative to this script location
TARGET_DIR="$SCRIPT_DIR/voices/streaming_model/experimental_voices"
echo "[INFO] Script directory: $SCRIPT_DIR"
echo "[INFO] Target directory: $TARGET_DIR"
# Ensure the target directory exists
echo "[INFO] Creating target directory if needed..."
mkdir -p "$TARGET_DIR"
# List of archives and their URLs
FILES=(
"experimental_voices_de.tar.gz|https://github.com/user-attachments/files/24035887/experimental_voices_de.tar.gz"
"experimental_voices_fr.tar.gz|https://github.com/user-attachments/files/24035880/experimental_voices_fr.tar.gz"
"experimental_voices_jp.tar.gz|https://github.com/user-attachments/files/24035882/experimental_voices_jp.tar.gz"
"experimental_voices_kr.tar.gz|https://github.com/user-attachments/files/24035883/experimental_voices_kr.tar.gz"
"experimental_voices_pl.tar.gz|https://github.com/user-attachments/files/24035885/experimental_voices_pl.tar.gz"
"experimental_voices_pt.tar.gz|https://github.com/user-attachments/files/24035886/experimental_voices_pt.tar.gz"
"experimental_voices_sp.tar.gz|https://github.com/user-attachments/files/24035884/experimental_voices_sp.tar.gz"
"experimental_voices_en1.tar.gz|https://github.com/user-attachments/files/24189272/experimental_voices_en1.tar.gz"
"experimental_voices_en2.tar.gz|https://github.com/user-attachments/files/24189273/experimental_voices_en2.tar.gz"
)
# Download, extract, and clean up each archive
for entry in "${FILES[@]}"; do
IFS="|" read -r FNAME URL <<< "$entry"
echo "[INFO] Downloading $FNAME ..."
wget -O "$FNAME" "$URL"
echo "[INFO] Extracting $FNAME ..."
tar -xzvf "$FNAME" -C "$TARGET_DIR"
echo "[INFO] Cleaning up $FNAME ..."
rm -f "$FNAME"
done
echo "[SUCCESS] All experimental speakers installed successfully!"
echo "[SUCCESS] Speakers are located at:"
echo " $TARGET_DIR"
+311
View File
@@ -0,0 +1,311 @@
import argparse
import os
import re
import traceback
from typing import List, Tuple, Union, Dict, Any
import time
import torch
import copy
import glob
from vibevoice.modular.modeling_vibevoice_streaming_inference import VibeVoiceStreamingForConditionalGenerationInference
from vibevoice.processor.vibevoice_streaming_processor import VibeVoiceStreamingProcessor
from transformers.cache_utils import DynamicCache
from transformers.modeling_outputs import BaseModelOutputWithPast
from transformers.utils import logging
logging.set_verbosity_info()
logger = logging.get_logger(__name__)
class VoiceMapper:
"""Maps speaker names to voice file paths"""
def __init__(self):
self.setup_voice_presets()
# for k, v in self.voice_presets.items():
# print(f"{k}: {v}")
def setup_voice_presets(self):
"""Setup voice presets by scanning the voices directory."""
voices_dir = os.path.join(os.path.dirname(__file__), "voices/streaming_model")
# Check if voices directory exists
if not os.path.exists(voices_dir):
print(f"Warning: Voices directory not found at {voices_dir}")
self.voice_presets = {}
self.available_voices = {}
return
# Scan for all VOICE files in the voices directory
self.voice_presets = {}
# Get all .pt files in the voices directory
pt_files = glob.glob(os.path.join(voices_dir, "**", "*.pt"), recursive=True)
# Create dictionary with filename (without extension) as key
for pt_file in pt_files:
# key: filename without extension
name = os.path.splitext(os.path.basename(pt_file))[0].lower()
full_path = os.path.abspath(pt_file)
self.voice_presets[name] = full_path
# Sort the voice presets alphabetically by name for better UI
self.voice_presets = dict(sorted(self.voice_presets.items()))
# Filter out voices that don't exist (this is now redundant but kept for safety)
self.available_voices = {
name: path for name, path in self.voice_presets.items()
if os.path.exists(path)
}
print(f"Found {len(self.available_voices)} voice files in {voices_dir}")
print(f"Available voices: {', '.join(self.available_voices.keys())}")
def get_voice_path(self, speaker_name: str) -> str:
"""Get voice file path for a given speaker name"""
# First try exact match
speaker_name = speaker_name.lower()
if speaker_name in self.voice_presets:
return self.voice_presets[speaker_name]
# Try partial matching (case insensitive)
matched_path = None
for preset_name, path in self.voice_presets.items():
if preset_name.lower() in speaker_name or speaker_name in preset_name.lower():
if matched_path is not None:
raise ValueError(f"Multiple voice presets match the speaker name '{speaker_name}', please make the speaker_name more specific.")
matched_path = path
if matched_path is not None:
return matched_path
# Default to first voice if no match found
default_voice = list(self.voice_presets.values())[0]
print(f"Warning: No voice preset found for '{speaker_name}', using default voice: {default_voice}")
return default_voice
def parse_args():
parser = argparse.ArgumentParser(description="VibeVoiceStreaming Processor TXT Input Test")
parser.add_argument(
"--model_path",
type=str,
default="microsoft/VibeVoice-Realtime-0.5B",
help="Path to the HuggingFace model directory",
)
parser.add_argument(
"--txt_path",
type=str,
default="demo/text_examples/1p_vibevoice.txt",
help="Path to the txt file containing the script",
)
parser.add_argument(
"--speaker_name",
type=str,
default="Wayne",
help="Single speaker name (e.g., --speaker_name Wayne)",
)
parser.add_argument(
"--output_dir",
type=str,
default="./outputs",
help="Directory to save output audio files",
)
parser.add_argument(
"--device",
type=str,
default=("cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")),
help="Device for inference: cuda | mps | cpu",
)
parser.add_argument(
"--cfg_scale",
type=float,
default=1.5,
help="CFG (Classifier-Free Guidance) scale for generation (default: 1.5)",
)
return parser.parse_args()
def main():
args = parse_args()
# Normalize potential 'mpx' typo to 'mps'
if args.device.lower() == "mpx":
print("Note: device 'mpx' detected, treating it as 'mps'.")
args.device = "mps"
# Validate mps availability if requested
if args.device == "mps" and not torch.backends.mps.is_available():
print("Warning: MPS not available. Falling back to CPU.")
args.device = "cpu"
print(f"Using device: {args.device}")
# Initialize voice mapper
voice_mapper = VoiceMapper()
# Check if txt file exists
if not os.path.exists(args.txt_path):
print(f"Error: txt file not found: {args.txt_path}")
return
# Read and parse txt file
print(f"Reading script from: {args.txt_path}")
with open(args.txt_path, 'r', encoding='utf-8') as f:
scripts = f.read().strip()
if not scripts:
print("Error: No valid scripts found in the txt file")
return
full_script = scripts.replace("", "'").replace('', '"').replace('', '"')
print(f"Loading processor & model from {args.model_path}")
processor = VibeVoiceStreamingProcessor.from_pretrained(args.model_path)
# Decide dtype & attention implementation
if args.device == "mps":
load_dtype = torch.float32 # MPS requires float32
attn_impl_primary = "sdpa" # flash_attention_2 not supported on MPS
elif args.device == "cuda":
load_dtype = torch.bfloat16
attn_impl_primary = "flash_attention_2"
else: # cpu
load_dtype = torch.float32
attn_impl_primary = "sdpa"
print(f"Using device: {args.device}, torch_dtype: {load_dtype}, attn_implementation: {attn_impl_primary}")
# Load model with device-specific logic
try:
if args.device == "mps":
model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
args.model_path,
torch_dtype=load_dtype,
attn_implementation=attn_impl_primary,
device_map=None, # load then move
)
model.to("mps")
elif args.device == "cuda":
model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
args.model_path,
torch_dtype=load_dtype,
device_map="cuda",
attn_implementation=attn_impl_primary,
)
else: # cpu
model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
args.model_path,
torch_dtype=load_dtype,
device_map="cpu",
attn_implementation=attn_impl_primary,
)
except Exception as e:
if attn_impl_primary == 'flash_attention_2':
print(f"[ERROR] : {type(e).__name__}: {e}")
print(traceback.format_exc())
print("Error loading the model. Trying to use SDPA. However, note that only flash_attention_2 has been fully tested, and using SDPA may result in lower audio quality.")
model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
args.model_path,
torch_dtype=load_dtype,
device_map=(args.device if args.device in ("cuda", "cpu") else None),
attn_implementation='sdpa'
)
if args.device == "mps":
model.to("mps")
else:
raise e
model.eval()
model.set_ddpm_inference_steps(num_steps=5)
if hasattr(model.model, 'language_model'):
print(f"Language model attention: {model.model.language_model.config._attn_implementation}")
target_device = args.device if args.device != "cpu" else "cpu"
voice_sample = voice_mapper.get_voice_path(args.speaker_name)
print(f"Using voice preset for {args.speaker_name}: {voice_sample}")
with torch.serialization.safe_globals([BaseModelOutputWithPast, DynamicCache]):
all_prefilled_outputs = torch.load(voice_sample, map_location=target_device, weights_only=True)
# Prepare inputs for the model
inputs = processor.process_input_with_cached_prompt(
text=full_script,
cached_prompt=all_prefilled_outputs,
padding=True,
return_tensors="pt",
return_attention_mask=True,
)
# Move tensors to target device
for k, v in inputs.items():
if torch.is_tensor(v):
inputs[k] = v.to(target_device)
print(f"Starting generation with cfg_scale: {args.cfg_scale}")
# Generate audio
start_time = time.time()
outputs = model.generate(
**inputs,
max_new_tokens=None,
cfg_scale=args.cfg_scale,
tokenizer=processor.tokenizer,
generation_config={'do_sample': False},
verbose=True,
all_prefilled_outputs=copy.deepcopy(all_prefilled_outputs) if all_prefilled_outputs is not None else None,
)
generation_time = time.time() - start_time
print(f"Generation time: {generation_time:.2f} seconds")
# Calculate audio duration and additional metrics
if outputs.speech_outputs and outputs.speech_outputs[0] is not None:
# Assuming 24kHz sample rate (common for speech synthesis)
sample_rate = 24000
audio_samples = outputs.speech_outputs[0].shape[-1] if len(outputs.speech_outputs[0].shape) > 0 else len(outputs.speech_outputs[0])
audio_duration = audio_samples / sample_rate
rtf = generation_time / audio_duration if audio_duration > 0 else float('inf')
print(f"Generated audio duration: {audio_duration:.2f} seconds")
print(f"RTF (Real Time Factor): {rtf:.2f}x")
else:
print("No audio output generated")
return
# Calculate token metrics
input_tokens = inputs['tts_text_ids'].shape[1] # Number of input tokens
output_tokens = outputs.sequences.shape[1] # Total tokens (input + generated)
generated_tokens = output_tokens - input_tokens - all_prefilled_outputs['tts_lm']['last_hidden_state'].size(1)
print(f"Prefilling text tokens: {input_tokens}")
print(f"Generated speech tokens: {generated_tokens}")
print(f"Total tokens: {output_tokens}")
# Save output (processor handles device internally)
txt_filename = os.path.splitext(os.path.basename(args.txt_path))[0]
output_path = os.path.join(args.output_dir, f"{txt_filename}_generated.wav")
os.makedirs(args.output_dir, exist_ok=True)
processor.save_audio(
outputs.speech_outputs[0], # First (and only) batch item
output_path=output_path,
)
print(f"Saved output to {output_path}")
# Print summary
print("\n" + "="*50)
print("GENERATION SUMMARY")
print("="*50)
print(f"Input file: {args.txt_path}")
print(f"Output file: {output_path}")
print(f"Speaker names: {args.speaker_name}")
print(f"Prefilling text tokens: {input_tokens}")
print(f"Generated speech tokens: {generated_tokens}")
print(f"Total tokens: {output_tokens}")
print(f"Generation time: {generation_time:.2f} seconds")
print(f"Audio duration: {audio_duration:.2f} seconds")
print(f"RTF (Real Time Factor): {rtf:.2f}x")
print("="*50)
if __name__ == "__main__":
main()
+2
View File
@@ -0,0 +1,2 @@
Generating long-form, multi-speaker conversational audio like podcasts poses significant challenges for traditional Text-to-Speech (TTS) systems, particularly in scalability, speaker consistency, and natural turn-taking. This report presents VibeVoice, a novel model designed to synthesize long-form speech with multiple speakers by employing the next-token diffusion framework, a unified method for modeling continuous data by autoregressively generating latent vectors via diffusion.
A core component of our approach is the continuous speech tokenizers operating at an ultra-low frame rate of 7.5. This tokenizer effectively preserves audio fidelity while significantly boosting computational efficiency for processing long sequences. This enables VibeVoice to synthesize long-form speech for up to 90 minutes (in a 64K context window length) with up to 4 speakers, capturing the authentic conversational "vibe" and surpassing all known open-source and closed-source dialogue models (for example, Gemini 2.5 Pro Preview TTS). Code and checkpoint are available now.
+1
View File
@@ -0,0 +1 @@
VibeVoice is a novel framework designed for generating expressive, long-form, multi-speaker conversational audio, such as podcasts, from text. It addresses significant challenges in traditional Text-to-Speech (TTS) systems, particularly in scalability, speaker consistency, and natural turn-taking. A core innovation of VibeVoice is its use of continuous speech tokenizers operating at an ultra-low frame rate of 7.5 Hz. These tokenizers efficiently preserve audio fidelity while significantly boosting computational efficiency for processing long sequences. VibeVoice employs a next-token diffusion framework, leveraging a Large Language Model to understand textual context and dialogue flow, and a diffusion head to generate high-fidelity acoustic details. The model can synthesize speech up to 90 minutes long with up to 4 distinct speakers, surpassing the typical 1-2 speaker limits of many prior models.
File diff suppressed because it is too large Load Diff
+581
View File
@@ -0,0 +1,581 @@
#!/usr/bin/env python
"""
VibeVoice ASR Batch Inference Demo Script
This script supports batch inference for ASR model and compares results
between batch processing and single-sample processing.
"""
import os
import sys
import torch
import numpy as np
from pathlib import Path
import argparse
import time
import json
import re
from typing import List, Dict, Any, Optional
from functools import wraps
from vibevoice.modular.modeling_vibevoice_asr import VibeVoiceASRForConditionalGeneration
from vibevoice.processor.vibevoice_asr_processor import VibeVoiceASRProcessor
from vibevoice.processor.audio_utils import COMMON_AUDIO_EXTS
class VibeVoiceASRBatchInference:
"""Batch inference wrapper for VibeVoice ASR model."""
def __init__(
self,
model_path: str,
device: str = "cuda",
dtype: torch.dtype = torch.bfloat16,
attn_implementation: str = "sdpa"
):
"""
Initialize the ASR batch inference pipeline.
Args:
model_path: Path to the pretrained model
device: Device to run inference on (cuda, mps, xpu, cpu, auto)
dtype: Data type for model weights
attn_implementation: Attention implementation to use ('flash_attention_2', 'sdpa', 'eager')
"""
print(f"Loading VibeVoice ASR model from {model_path}")
# Load processor
self.processor = VibeVoiceASRProcessor.from_pretrained(
model_path,
language_model_pretrained_name="Qwen/Qwen2.5-7B"
)
# Load model with specified attention implementation
print(f"Using attention implementation: {attn_implementation}")
self.model = VibeVoiceASRForConditionalGeneration.from_pretrained(
model_path,
dtype=dtype,
device_map=device if device == "auto" else None,
attn_implementation=attn_implementation,
trust_remote_code=True
)
if device != "auto":
self.model = self.model.to(device)
self.device = device if device != "auto" else next(self.model.parameters()).device
self.dtype = dtype
self.model.eval()
print(f"Model loaded successfully on {self.device}")
def _prepare_generation_config(
self,
max_new_tokens: int = 512,
temperature: float = 0.0,
top_p: float = 0.9,
do_sample: bool = True,
num_beams: int = 1,
) -> dict:
"""Prepare generation configuration."""
config = {
"max_new_tokens": max_new_tokens,
"pad_token_id": self.processor.pad_id,
"eos_token_id": self.processor.tokenizer.eos_token_id,
}
# Beam search vs sampling
if num_beams > 1:
config["num_beams"] = num_beams
config["do_sample"] = False # Beam search doesn't use sampling
else:
config["do_sample"] = do_sample
# Only set temperature and top_p when sampling is enabled
if do_sample:
config["temperature"] = temperature
config["top_p"] = top_p
return config
def transcribe_batch(
self,
audio_inputs: List,
max_new_tokens: int = 512,
temperature: float = 0.0,
top_p: float = 1.0,
do_sample: bool = True,
num_beams: int = 1,
) -> List[Dict[str, Any]]:
"""
Transcribe multiple audio files/arrays in a single batch.
Args:
audio_inputs: List of audio file paths or (array, sampling_rate) tuples
max_new_tokens: Maximum tokens to generate
temperature: Temperature for sampling
top_p: Top-p for nucleus sampling
do_sample: Whether to use sampling
Returns:
List of transcription results
"""
if len(audio_inputs) == 0:
return []
batch_size = len(audio_inputs)
print(f"\nProcessing batch of {batch_size} audio(s)...")
# Process all audio together
inputs = self.processor(
audio=audio_inputs,
sampling_rate=None,
return_tensors="pt",
padding=True,
add_generation_prompt=True
)
# Move to device
inputs = {k: v.to(self.device) if isinstance(v, torch.Tensor) else v
for k, v in inputs.items()}
# Print batch info
print(f" Input IDs shape: {inputs['input_ids'].shape}")
print(f" Speech tensors shape: {inputs['speech_tensors'].shape}")
print(f" Attention mask shape: {inputs['attention_mask'].shape}")
# Generate
generation_config = self._prepare_generation_config(
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
do_sample=do_sample,
num_beams=num_beams,
)
start_time = time.time()
with torch.no_grad():
output_ids = self.model.generate(
**inputs,
**generation_config
)
generation_time = time.time() - start_time
# Decode outputs for each sample in the batch
results = []
input_length = inputs['input_ids'].shape[1]
for i, audio_input in enumerate(audio_inputs):
# Get generated tokens for this sample (excluding input tokens)
generated_ids = output_ids[i, input_length:]
# Remove padding tokens from the end
# Find the first eos_token or pad_token
eos_positions = (generated_ids == self.processor.tokenizer.eos_token_id).nonzero(as_tuple=True)[0]
if len(eos_positions) > 0:
generated_ids = generated_ids[:eos_positions[0] + 1]
generated_text = self.processor.decode(generated_ids, skip_special_tokens=True)
# Parse structured output
try:
transcription_segments = self.processor.post_process_transcription(generated_text)
except Exception as e:
print(f"Warning: Failed to parse structured output: {e}")
transcription_segments = []
# Get file name based on input type
if isinstance(audio_input, str):
file_name = audio_input
elif isinstance(audio_input, dict) and 'id' in audio_input:
file_name = audio_input['id']
else:
file_name = f"audio_{i}"
results.append({
"file": file_name,
"raw_text": generated_text,
"segments": transcription_segments,
"generation_time": generation_time / batch_size,
})
print(f" Total generation time: {generation_time:.2f}s")
print(f" Average time per sample: {generation_time/batch_size:.2f}s")
return results
def transcribe_with_batching(
self,
audio_inputs: List,
batch_size: int = 4,
max_new_tokens: int = 512,
temperature: float = 0.0,
top_p: float = 1.0,
do_sample: bool = True,
num_beams: int = 1,
) -> List[Dict[str, Any]]:
"""
Transcribe multiple audio files/arrays with automatic batching.
Args:
audio_inputs: List of audio file paths or (array, sampling_rate) tuples
batch_size: Number of samples per batch
max_new_tokens: Maximum tokens to generate
temperature: Temperature for sampling
top_p: Top-p for nucleus sampling
do_sample: Whether to use sampling
Returns:
List of transcription results
"""
all_results = []
# Process in batches
for i in range(0, len(audio_inputs), batch_size):
batch_inputs = audio_inputs[i:i + batch_size]
print(f"\n{'='*60}")
print(f"Processing batch {i//batch_size + 1}/{(len(audio_inputs) + batch_size - 1)//batch_size}")
batch_results = self.transcribe_batch(
batch_inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
do_sample=do_sample,
num_beams=num_beams,
)
all_results.extend(batch_results)
return all_results
def print_result(result: Dict[str, Any]):
"""Pretty print a single transcription result."""
print(f"\nFile: {result['file']}")
print(f"Generation Time: {result['generation_time']:.2f}s")
print(f"\n--- Raw Output ---")
print(result['raw_text'][:500] + "..." if len(result['raw_text']) > 500 else result['raw_text'])
if result['segments']:
print(f"\n--- Structured Output ({len(result['segments'])} segments) ---")
for seg in result['segments'][:50]: # Show first 50 segments
print(f"[{seg.get('start_time', 'N/A')} - {seg.get('end_time', 'N/A')}] "
f"Speaker {seg.get('speaker_id', 'N/A')}: {seg.get('text', '')}...")
if len(result['segments']) > 50:
print(f" ... and {len(result['segments']) - 50} more segments")
def load_dataset_and_concatenate(
dataset_name: str,
split: str,
max_duration: float,
num_audios: int,
target_sr: int = 24000
) -> Optional[List[np.ndarray]]:
"""
Load a HuggingFace dataset and concatenate audio samples into long audio chunks.
(Note, just for demo purpose, not for benchmark evaluation)
Args:
dataset_name: HuggingFace dataset name (e.g., 'openslr/librispeech_asr')
split: Dataset split to use (e.g., 'test', 'test.other')
max_duration: Maximum duration in seconds for each concatenated audio
num_audios: Number of concatenated audios to create
target_sr: Target sample rate (default: 24000)
Returns:
List of concatenated audio arrays, or None if loading fails
"""
try:
from datasets import load_dataset
import torchcodec # just for decode audio in datasets
except ImportError:
print("Please install it with: pip install datasets torchcodec")
return None
print(f"\nLoading dataset: {dataset_name} (split: {split})")
print(f"Will create {num_audios} concatenated audio(s), each up to {max_duration:.1f}s ({max_duration/3600:.2f} hours)")
try:
# Use streaming to avoid downloading the entire dataset
dataset = load_dataset(dataset_name, split=split, streaming=True)
print(f"Dataset loaded in streaming mode")
concatenated_audios = [] # List of concatenated audio metadata
# Create multiple concatenated audios based on num_audios
current_chunks = []
current_duration = 0.0
current_samples_used = 0
sample_idx = 0
for sample in dataset:
if len(concatenated_audios) >= num_audios:
break
if 'audio' not in sample:
continue
audio_data = sample['audio']
audio_array = audio_data['array']
sr = audio_data['sampling_rate']
# Resample if needed
if sr != target_sr:
duration = len(audio_array) / sr
new_length = int(duration * target_sr)
audio_array = np.interp(
np.linspace(0, len(audio_array) - 1, new_length),
np.arange(len(audio_array)),
audio_array
)
chunk_duration = len(audio_array) / target_sr
# Check if adding this chunk exceeds max_duration
if current_duration + chunk_duration > max_duration:
remaining_duration = max_duration - current_duration
if remaining_duration > 0.5: # Only add if > 0.5s remaining
samples_to_take = int(remaining_duration * target_sr)
current_chunks.append(audio_array[:samples_to_take])
current_duration += remaining_duration
current_samples_used += 1
# Save current concatenated audio and start a new one
if current_chunks:
concatenated_audios.append({
'array': np.concatenate(current_chunks),
'duration': current_duration,
'samples_used': current_samples_used,
})
print(f" Created audio {len(concatenated_audios)}: {current_duration:.1f}s from {current_samples_used} samples")
# Reset for next concatenated audio
current_chunks = []
current_duration = 0.0
current_samples_used = 0
if len(concatenated_audios) >= num_audios:
break
current_chunks.append(audio_array)
current_duration += chunk_duration
current_samples_used += 1
sample_idx += 1
if sample_idx % 100 == 0:
print(f" Processed {sample_idx} samples...")
# Don't forget the last batch if it has content
if current_chunks and len(concatenated_audios) < num_audios:
concatenated_audios.append({
'array': np.concatenate(current_chunks),
'duration': current_duration,
'samples_used': current_samples_used,
})
print(f" Created audio {len(concatenated_audios)}: {current_duration:.1f}s from {current_samples_used} samples")
if not concatenated_audios:
print("Warning: No audio samples found in dataset")
return None
# Extract arrays and print summary
result = [a['array'] for a in concatenated_audios]
total_duration = sum(a['duration'] for a in concatenated_audios)
total_samples = sum(a['samples_used'] for a in concatenated_audios)
print(f"\nCreated {len(result)} concatenated audio(s), total {total_duration:.1f}s ({total_duration/60:.1f} min) from {total_samples} samples")
return result
except Exception as e:
print(f"Error loading dataset: {e}")
import traceback
traceback.print_exc()
return None
def main():
parser = argparse.ArgumentParser(description="VibeVoice ASR Batch Inference Demo")
parser.add_argument(
"--model_path",
type=str,
default="",
help="Path to the model checkpoint"
)
parser.add_argument(
"--audio_files",
type=str,
nargs='+',
required=False,
help="Paths to audio files for transcription"
)
parser.add_argument(
"--audio_dir",
type=str,
required=False,
help="Directory containing audio files for batch transcription"
)
parser.add_argument(
"--dataset",
type=str,
required=False,
help="HuggingFace dataset name (e.g., 'openslr/librispeech_asr')"
)
parser.add_argument(
"--split",
type=str,
default="test",
help="Dataset split to use (e.g., 'test', 'test.other', 'test.clean')"
)
parser.add_argument(
"--max_duration",
type=float,
default=3600.0,
help="Maximum duration in seconds for concatenated dataset audio (default: 3600 = 1 hour)"
)
parser.add_argument(
"--batch_size",
type=int,
default=2,
help="Batch size for processing multiple files"
)
parser.add_argument(
"--device",
type=str,
default="cuda" if torch.cuda.is_available() else ("xpu" if torch.backends.xpu.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu") ),
choices=["cuda", "cpu", "mps","xpu", "auto"],
help="Device to run inference on"
)
parser.add_argument(
"--max_new_tokens",
type=int,
default=32768,
help="Maximum number of tokens to generate"
)
parser.add_argument(
"--temperature",
type=float,
default=0.0,
help="Temperature for sampling (0 = greedy decoding)"
)
parser.add_argument(
"--top_p",
type=float,
default=1.0,
help="Top-p for nucleus sampling"
)
parser.add_argument(
"--num_beams",
type=int,
default=1,
help="Number of beams for beam search. Use 1 for greedy/sampling"
)
parser.add_argument(
"--attn_implementation",
type=str,
default="auto",
choices=["flash_attention_2", "sdpa", "eager", "auto"],
help="Attention implementation to use. 'auto' will select the best available for your device (flash_attention_2 for CUDA, sdpa for MPS/CPU/XPU)"
)
args = parser.parse_args()
# Auto-detect best attention implementation based on device
if args.attn_implementation == "auto":
if args.device == "cuda" and torch.cuda.is_available():
try:
import flash_attn
args.attn_implementation = "flash_attention_2"
except ImportError:
print("flash_attn not installed, falling back to sdpa")
args.attn_implementation = "sdpa"
else:
# MPS/XPU/CPU don't support flash_attention_2
args.attn_implementation = "sdpa"
print(f"Auto-detected attention implementation: {args.attn_implementation}")
# Collect audio files
audio_files = []
concatenated_audio = None # For storing concatenated dataset audio
if args.audio_files:
audio_files.extend(args.audio_files)
if args.audio_dir:
supported = set(e.lower() for e in COMMON_AUDIO_EXTS)
for f in os.listdir(args.audio_dir):
if os.path.splitext(f)[1].lower() in supported:
audio_files.append(os.path.join(args.audio_dir, f))
if args.dataset:
concatenated_audio = load_dataset_and_concatenate(
dataset_name=args.dataset,
split=args.split,
max_duration=args.max_duration,
num_audios=args.batch_size,
)
if concatenated_audio is None:
return
if len(audio_files) == 0 and concatenated_audio is None:
print("No audio files provided. Please specify --audio_files, --audio_dir, or --dataset.")
return
if audio_files:
print(f"\nAudio files to process ({len(audio_files)}):")
for f in audio_files:
print(f" - {f}")
if concatenated_audio:
print(f"\nConcatenated dataset audios: {len(concatenated_audio)} audio(s)")
# Initialize model
# Handle MPS device and dtype
if args.device == "mps":
model_dtype = torch.float32 # MPS works better with float32
elif args.device == "xpu":
model_dtype = torch.float32
elif args.device == "cpu":
model_dtype = torch.float32
else:
model_dtype = torch.bfloat16
asr = VibeVoiceASRBatchInference(
model_path=args.model_path,
device=args.device,
dtype=model_dtype,
attn_implementation=args.attn_implementation
)
# If temperature is 0, use greedy decoding (no sampling)
do_sample = args.temperature > 0
# Combine all audio inputs
all_audio_inputs = audio_files + (concatenated_audio or [])
print("\n" + "="*80)
print(f"Processing {len(all_audio_inputs)} audio(s)")
print("="*80)
all_results = asr.transcribe_with_batching(
all_audio_inputs,
batch_size=args.batch_size,
max_new_tokens=args.max_new_tokens,
temperature=args.temperature,
top_p=args.top_p,
do_sample=do_sample,
num_beams=args.num_beams,
)
# Print results
print("\n" + "="*80)
print("Results")
print("="*80)
for result in all_results:
print("\n" + "-"*60)
print_result(result)
if __name__ == "__main__":
main()
+216
View File
@@ -0,0 +1,216 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d1785adb",
"metadata": {
"colab_type": "text",
"id": "view-in-github"
},
"source": [
"<a href=\"https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"id": "WvIaUJD2y0yU",
"metadata": {
"id": "WvIaUJD2y0yU"
},
"source": [
"# VibeVoice-Realtime Colab — T4 Quickstart\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "e8fTKYGx7DZk",
"metadata": {
"id": "e8fTKYGx7DZk"
},
"source": [
"## Step 1: Setup Environment"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4wxJ6QHM-ZOb",
"metadata": {
"id": "4wxJ6QHM-ZOb"
},
"outputs": [],
"source": [
"# Check for T4 GPU\n",
"import torch\n",
"if torch.cuda.is_available() and \"T4\" in torch.cuda.get_device_name(0):\n",
" print(\"✅ T4 GPU detected\")\n",
"else:\n",
" print(\"\"\"\n",
" ⚠️ WARNING: T4 GPU not detected\n",
"\n",
" The recommended runtime for this Colab notebook is \"T4 GPU\".\n",
"\n",
" To change the runtime type:\n",
"\n",
" 1. Click on \"Runtime\" in the top navigation menu\n",
" 2. Click on \"Change runtime type\"\n",
" 3. Select \"T4 GPU\"\n",
" 4. Click \"OK\" if a \"Disconnect and delete runtime\" window appears\n",
" 5. Click on \"Save\"\n",
"\n",
" \"\"\")\n",
"\n",
"# Clone the VibeVoice repository\n",
"![ -d /content/VibeVoice ] || git clone --quiet --branch main --depth 1 https://github.com/microsoft/VibeVoice.git /content/VibeVoice\n",
"print(\"✅ Cloned VibeVoice repository\")\n",
"\n",
"# Install project dependencies\n",
"!uv pip --quiet install --system -e /content/VibeVoice[streamingtts]\n",
"!wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O cloudflared && chmod +x cloudflared\n",
"print(\"✅ Installed dependencies\")\n",
"\n",
"# Download model\n",
"from huggingface_hub import snapshot_download\n",
"snapshot_download(\"microsoft/VibeVoice-Realtime-0.5B\", local_dir=\"/content/models/VibeVoice-Realtime-0.5B\")\n",
"print(\"✅ Downloaded model: microsoft/VibeVoice-Realtime-0.5B\")\n"
]
},
{
"cell_type": "markdown",
"id": "88c727ab",
"metadata": {},
"source": [
"[Optional] If the download exceeds 1 minute, it is probably stuck. You can: (1) interrupt the execution, (2) log in to Hugging Face, and (3) try download again."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dec6b870",
"metadata": {},
"outputs": [],
"source": [
"from huggingface_hub import login, snapshot_download\n",
"login()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c579654b",
"metadata": {},
"outputs": [],
"source": [
"snapshot_download(\"microsoft/VibeVoice-Realtime-0.5B\", local_dir=\"/content/models/VibeVoice-Realtime-0.5B\")\n",
"print(\"✅ Downloaded model: microsoft/VibeVoice-Realtime-0.5B\")"
]
},
{
"cell_type": "markdown",
"id": "dfe30d6f",
"metadata": {},
"source": [
"[Optional] More experimental voices"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb33c9ce",
"metadata": {},
"outputs": [],
"source": [
"!bash /content/VibeVoice/demo/download_experimental_voices.sh"
]
},
{
"cell_type": "markdown",
"id": "pgKlV7153Ifi",
"metadata": {
"id": "pgKlV7153Ifi"
},
"source": [
"## Step 2: Launch VibeVoice-Realtime Demo"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "Yc1N9EHswFxA",
"metadata": {
"id": "Yc1N9EHswFxA"
},
"outputs": [],
"source": [
"import subprocess, re, time, threading\n",
"\n",
"srv = subprocess.Popen(\n",
" \"python /content/VibeVoice/demo/vibevoice_realtime_demo.py --model_path /content/models/VibeVoice-Realtime-0.5B --port 8000\",\n",
" shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True,\n",
")\n",
"cf = subprocess.Popen(\n",
" \"./cloudflared tunnel --url http://localhost:8000 --no-autoupdate\",\n",
" shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True,\n",
")\n",
"\n",
"public_url = None\n",
"server_ready = False\n",
"url_pattern = re.compile(r\"(https://[a-z0-9-]+\\.trycloudflare\\.com)\")\n",
"\n",
"def read_srv():\n",
" global server_ready\n",
" for ln in srv.stdout:\n",
" print(ln.strip())\n",
" if \"Uvicorn running on\" in ln:\n",
" server_ready = True\n",
"\n",
"def read_cf():\n",
" global public_url\n",
" for ln in cf.stdout:\n",
" m = url_pattern.search(ln)\n",
" if m:\n",
" public_url = m.group(1)\n",
" break\n",
"\n",
"threading.Thread(target=read_srv, daemon=True).start()\n",
"threading.Thread(target=read_cf, daemon=True).start()\n",
"\n",
"\n",
"while True:\n",
" if server_ready and public_url:\n",
" print(f\"✅ Public URL: {public_url}\\n\");\n",
" public_url = None\n",
" time.sleep(0.25)"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "T4",
"include_colab_link": true,
"machine_shape": "hm",
"name": "VibeVoice_Colab.ipynb",
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+17
View File
@@ -0,0 +1,17 @@
import argparse, os, uvicorn
def main():
p = argparse.ArgumentParser()
p.add_argument("--port", type=int, default=3000)
p.add_argument("--model_path", type=str, default="microsoft/VibeVoice-Realtime-0.5B")
p.add_argument("--device", type=str, default="cuda", choices=["cpu", "cuda", "mpx", "mps"])
p.add_argument("--reload", action="store_true", help="Reload the model or not")
args = p.parse_args()
os.environ["MODEL_PATH"] = args.model_path
os.environ["MODEL_DEVICE"] = args.device
uvicorn.run("web.app:app", host="0.0.0.0", port=args.port, reload=args.reload)
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+518
View File
@@ -0,0 +1,518 @@
import datetime
import builtins
import asyncio
import json
import os
import threading
import traceback
from pathlib import Path
from queue import Empty, Queue
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, cast
import numpy as np
import torch
from transformers.cache_utils import DynamicCache
from transformers.modeling_outputs import BaseModelOutputWithPast
from fastapi import FastAPI, WebSocket
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.websockets import WebSocketDisconnect, WebSocketState
from vibevoice.modular.modeling_vibevoice_streaming_inference import (
VibeVoiceStreamingForConditionalGenerationInference,
)
from vibevoice.processor.vibevoice_streaming_processor import (
VibeVoiceStreamingProcessor,
)
from vibevoice.modular.streamer import AudioStreamer
import copy
BASE = Path(__file__).parent
SAMPLE_RATE = 24_000
def get_timestamp():
timestamp = datetime.datetime.utcnow().replace(
tzinfo=datetime.timezone.utc
).astimezone(
datetime.timezone(datetime.timedelta(hours=8))
).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
return timestamp
class StreamingTTSService:
def __init__(
self,
model_path: str,
device: str = "cuda",
inference_steps: int = 5,
) -> None:
# Keep model_path as string for HuggingFace repo IDs (Path() converts / to \ on Windows)
self.model_path = model_path
self.inference_steps = inference_steps
self.sample_rate = SAMPLE_RATE
self.processor: Optional[VibeVoiceStreamingProcessor] = None
self.model: Optional[VibeVoiceStreamingForConditionalGenerationInference] = None
self.voice_presets: Dict[str, Path] = {}
self.default_voice_key: Optional[str] = None
self._voice_cache: Dict[str, Tuple[object, Path, str]] = {}
if device == "mpx":
print("Note: device 'mpx' detected, treating it as 'mps'.")
device = "mps"
if device == "mps" and not torch.backends.mps.is_available():
print("Warning: MPS not available. Falling back to CPU.")
device = "cpu"
self.device = device
self._torch_device = torch.device(device)
def load(self) -> None:
print(f"[startup] Loading processor from {self.model_path}")
self.processor = VibeVoiceStreamingProcessor.from_pretrained(self.model_path)
# Decide dtype & attention
if self.device == "mps":
load_dtype = torch.float32
device_map = None
attn_impl_primary = "sdpa"
elif self.device == "cuda":
load_dtype = torch.bfloat16
device_map = 'cuda'
attn_impl_primary = "flash_attention_2"
else:
load_dtype = torch.float32
device_map = 'cpu'
attn_impl_primary = "sdpa"
print(f"Using device: {device_map}, torch_dtype: {load_dtype}, attn_implementation: {attn_impl_primary}")
# Load model
try:
self.model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
self.model_path,
torch_dtype=load_dtype,
device_map=device_map,
attn_implementation=attn_impl_primary,
)
if self.device == "mps":
self.model.to("mps")
except Exception as e:
if attn_impl_primary == 'flash_attention_2':
print("Error loading the model. Trying to use SDPA. However, note that only flash_attention_2 has been fully tested, and using SDPA may result in lower audio quality.")
self.model = VibeVoiceStreamingForConditionalGenerationInference.from_pretrained(
self.model_path,
torch_dtype=load_dtype,
device_map=self.device,
attn_implementation='sdpa',
)
print("Load model with SDPA successfully ")
else:
raise e
self.model.eval()
self.model.model.noise_scheduler = self.model.model.noise_scheduler.from_config(
self.model.model.noise_scheduler.config,
algorithm_type="sde-dpmsolver++",
beta_schedule="squaredcos_cap_v2",
)
self.model.set_ddpm_inference_steps(num_steps=self.inference_steps)
self.voice_presets = self._load_voice_presets()
preset_name = os.environ.get("VOICE_PRESET")
self.default_voice_key = self._determine_voice_key(preset_name)
self._ensure_voice_cached(self.default_voice_key)
def _load_voice_presets(self) -> Dict[str, Path]:
voices_dir = BASE.parent / "voices" / "streaming_model"
if not voices_dir.exists():
raise RuntimeError(f"Voices directory not found: {voices_dir}")
presets: Dict[str, Path] = {}
for pt_path in voices_dir.rglob("*.pt"):
presets[pt_path.stem] = pt_path
if not presets:
raise RuntimeError(f"No voice preset (.pt) files found in {voices_dir}")
print(f"[startup] Found {len(presets)} voice presets")
return dict(sorted(presets.items()))
def _determine_voice_key(self, name: Optional[str]) -> str:
if name and name in self.voice_presets:
return name
default_key = "en-Carter_man"
if default_key in self.voice_presets:
return default_key
first_key = next(iter(self.voice_presets))
print(f"[startup] Using fallback voice preset: {first_key}")
return first_key
def _ensure_voice_cached(self, key: str) -> Tuple[object, Path, str]:
if key not in self.voice_presets:
raise RuntimeError(f"Voice preset {key!r} not found")
if key not in self._voice_cache:
preset_path = self.voice_presets[key]
print(f"[startup] Loading voice preset {key} from {preset_path}")
print(f"[startup] Loading prefilled prompt from {preset_path}")
with torch.serialization.safe_globals([BaseModelOutputWithPast, DynamicCache]):
prefilled_outputs = torch.load(
preset_path,
map_location=self._torch_device,
weights_only=True,
)
self._voice_cache[key] = prefilled_outputs
return self._voice_cache[key]
def _get_voice_resources(self, requested_key: Optional[str]) -> Tuple[str, object, Path, str]:
key = requested_key if requested_key and requested_key in self.voice_presets else self.default_voice_key
if key is None:
key = next(iter(self.voice_presets))
self.default_voice_key = key
prefilled_outputs = self._ensure_voice_cached(key)
return key, prefilled_outputs
def _prepare_inputs(self, text: str, prefilled_outputs: object):
if not self.processor or not self.model:
raise RuntimeError("StreamingTTSService not initialized")
processor_kwargs = {
"text": text.strip(),
"cached_prompt": prefilled_outputs,
"padding": True,
"return_tensors": "pt",
"return_attention_mask": True,
}
processed = self.processor.process_input_with_cached_prompt(**processor_kwargs)
prepared = {
key: value.to(self._torch_device) if hasattr(value, "to") else value
for key, value in processed.items()
}
return prepared
def _run_generation(
self,
inputs,
audio_streamer: AudioStreamer,
errors,
cfg_scale: float,
do_sample: bool,
temperature: float,
top_p: float,
refresh_negative: bool,
prefilled_outputs,
stop_event: threading.Event,
) -> None:
try:
self.model.generate(
**inputs,
max_new_tokens=None,
cfg_scale=cfg_scale,
tokenizer=self.processor.tokenizer,
generation_config={
"do_sample": do_sample,
"temperature": temperature if do_sample else 1.0,
"top_p": top_p if do_sample else 1.0,
},
audio_streamer=audio_streamer,
stop_check_fn=stop_event.is_set,
verbose=False,
refresh_negative=refresh_negative,
all_prefilled_outputs=copy.deepcopy(prefilled_outputs),
)
except Exception as exc: # pragma: no cover - diagnostic logging
errors.append(exc)
traceback.print_exc()
audio_streamer.end()
def stream(
self,
text: str,
cfg_scale: float = 1.5,
do_sample: bool = False,
temperature: float = 0.9,
top_p: float = 0.9,
refresh_negative: bool = True,
inference_steps: Optional[int] = None,
voice_key: Optional[str] = None,
log_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None,
stop_event: Optional[threading.Event] = None,
) -> Iterator[np.ndarray]:
if not text.strip():
return
text = text.replace("", "'")
selected_voice, prefilled_outputs = self._get_voice_resources(voice_key)
def emit(event: str, **payload: Any) -> None:
if log_callback:
try:
log_callback(event, **payload)
except Exception as exc:
print(f"[log_callback] Error while emitting {event}: {exc}")
steps_to_use = self.inference_steps
if inference_steps is not None:
try:
parsed_steps = int(inference_steps)
if parsed_steps > 0:
steps_to_use = parsed_steps
except (TypeError, ValueError):
pass
if self.model:
self.model.set_ddpm_inference_steps(num_steps=steps_to_use)
self.inference_steps = steps_to_use
inputs = self._prepare_inputs(text, prefilled_outputs)
audio_streamer = AudioStreamer(batch_size=1, stop_signal=None, timeout=None)
errors: list = []
stop_signal = stop_event or threading.Event()
thread = threading.Thread(
target=self._run_generation,
kwargs={
"inputs": inputs,
"audio_streamer": audio_streamer,
"errors": errors,
"cfg_scale": cfg_scale,
"do_sample": do_sample,
"temperature": temperature,
"top_p": top_p,
"refresh_negative": refresh_negative,
"prefilled_outputs": prefilled_outputs,
"stop_event": stop_signal,
},
daemon=True,
)
thread.start()
generated_samples = 0
try:
stream = audio_streamer.get_stream(0)
for audio_chunk in stream:
if torch.is_tensor(audio_chunk):
audio_chunk = audio_chunk.detach().cpu().to(torch.float32).numpy()
else:
audio_chunk = np.asarray(audio_chunk, dtype=np.float32)
if audio_chunk.ndim > 1:
audio_chunk = audio_chunk.reshape(-1)
peak = np.max(np.abs(audio_chunk)) if audio_chunk.size else 0.0
if peak > 1.0:
audio_chunk = audio_chunk / peak
generated_samples += int(audio_chunk.size)
emit(
"model_progress",
generated_sec=generated_samples / self.sample_rate,
chunk_sec=audio_chunk.size / self.sample_rate,
)
chunk_to_yield = audio_chunk.astype(np.float32, copy=False)
yield chunk_to_yield
finally:
stop_signal.set()
audio_streamer.end()
thread.join()
if errors:
emit("generation_error", message=str(errors[0]))
raise errors[0]
def chunk_to_pcm16(self, chunk: np.ndarray) -> bytes:
chunk = np.clip(chunk, -1.0, 1.0)
pcm = (chunk * 32767.0).astype(np.int16)
return pcm.tobytes()
app = FastAPI()
@app.on_event("startup")
async def _startup() -> None:
model_path = os.environ.get("MODEL_PATH")
if not model_path:
raise RuntimeError("MODEL_PATH not set in environment")
device = os.environ.get("MODEL_DEVICE", "cuda")
service = StreamingTTSService(
model_path=model_path,
device=device
)
service.load()
app.state.tts_service = service
app.state.model_path = model_path
app.state.device = device
app.state.websocket_lock = asyncio.Lock()
print("[startup] Model ready.")
def streaming_tts(text: str, **kwargs) -> Iterator[np.ndarray]:
service: StreamingTTSService = app.state.tts_service
yield from service.stream(text, **kwargs)
@app.websocket("/stream")
async def websocket_stream(ws: WebSocket) -> None:
await ws.accept()
text = ws.query_params.get("text", "")
print(f"Client connected, text={text!r}")
cfg_param = ws.query_params.get("cfg")
steps_param = ws.query_params.get("steps")
voice_param = ws.query_params.get("voice")
try:
cfg_scale = float(cfg_param) if cfg_param is not None else 1.5
except ValueError:
cfg_scale = 1.5
if cfg_scale <= 0:
cfg_scale = 1.5
try:
inference_steps = int(steps_param) if steps_param is not None else None
if inference_steps is not None and inference_steps <= 0:
inference_steps = None
except ValueError:
inference_steps = None
service: StreamingTTSService = app.state.tts_service
lock: asyncio.Lock = app.state.websocket_lock
if lock.locked():
busy_message = {
"type": "log",
"event": "backend_busy",
"data": {"message": "Please wait for the other requests to complete."},
"timestamp": get_timestamp(),
}
print("Please wait for the other requests to complete.")
try:
await ws.send_text(json.dumps(busy_message))
except Exception:
pass
await ws.close(code=1013, reason="Service busy")
return
acquired = False
try:
await lock.acquire()
acquired = True
log_queue: "Queue[Dict[str, Any]]" = Queue()
def enqueue_log(event: str, **data: Any) -> None:
log_queue.put({"event": event, "data": data})
async def flush_logs() -> None:
while True:
try:
entry = log_queue.get_nowait()
except Empty:
break
message = {
"type": "log",
"event": entry.get("event"),
"data": entry.get("data", {}),
"timestamp": get_timestamp(),
}
try:
await ws.send_text(json.dumps(message))
except Exception:
break
enqueue_log(
"backend_request_received",
text_length=len(text or ""),
cfg_scale=cfg_scale,
inference_steps=inference_steps,
voice=voice_param,
)
stop_signal = threading.Event()
iterator = streaming_tts(
text,
cfg_scale=cfg_scale,
inference_steps=inference_steps,
voice_key=voice_param,
log_callback=enqueue_log,
stop_event=stop_signal,
)
sentinel = object()
first_ws_send_logged = False
await flush_logs()
try:
while ws.client_state == WebSocketState.CONNECTED:
await flush_logs()
chunk = await asyncio.to_thread(next, iterator, sentinel)
if chunk is sentinel:
break
chunk = cast(np.ndarray, chunk)
payload = service.chunk_to_pcm16(chunk)
await ws.send_bytes(payload)
if not first_ws_send_logged:
first_ws_send_logged = True
enqueue_log("backend_first_chunk_sent")
await flush_logs()
except WebSocketDisconnect:
print("Client disconnected (WebSocketDisconnect)")
enqueue_log("client_disconnected")
stop_signal.set()
except Exception as e:
print(f"Error in websocket stream: {e}")
traceback.print_exc()
enqueue_log("backend_error", message=str(e))
stop_signal.set()
finally:
stop_signal.set()
enqueue_log("backend_stream_complete")
await flush_logs()
try:
iterator_close = getattr(iterator, "close", None)
if callable(iterator_close):
iterator_close()
except Exception:
pass
# clear the log queue
while not log_queue.empty():
try:
log_queue.get_nowait()
except Empty:
break
try:
if ws.client_state == WebSocketState.CONNECTED:
await ws.close()
except Exception as e:
print(f"Error closing websocket: {e}")
print("WS handler exit")
finally:
if acquired:
lock.release()
@app.get("/")
def index():
return FileResponse(BASE / "index.html")
@app.get("/config")
def get_config():
service: StreamingTTSService = app.state.tts_service
voices = sorted(service.voice_presets.keys())
return {
"voices": voices,
"default_voice": service.default_voice_key,
}
+1017
View File
File diff suppressed because it is too large Load Diff
+196
View File
@@ -0,0 +1,196 @@
# VibeVoice Gradio Demo Setup Guide
End-to-end instructions to deploy the VibeVoice ASR server and launch the Gradio web demo.
## Prerequisites
- CUDA-capable GPU(s)
- Docker with GPU support (`nvidia-docker`)
- VibeVoice repository cloned locally
```bash
git clone https://github.com/microsoft/VibeVoice.git
cd VibeVoice
```
---
## Step 1 — Start the ASR Server
Launch a Docker container running the vLLM ASR server. The launcher script handles everything automatically (system deps, pip install, model download, tokenizer generation, server start).
### Single GPU (default)
```bash
docker run -d --gpus '"device=0"' --name vibevoice-asr-demo \
--ipc=host \
-p 6001:6001 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --port 6001"
```
### Multi-GPU with Data Parallel (load balancing)
Run 4 independent replicas, one per GPU. vLLM distributes requests automatically:
```bash
docker run -d --gpus '"device=0,1,2,3"' --name vibevoice-asr-demo \
--ipc=host \
-p 6001:6001 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --port 6001 --dp 4"
```
> **Tip**: Use `--dp N` for N-way data parallel (throughput scaling). Use `--tp N` for tensor parallel (large models). See `docs/vibevoice-vllm-asr.md` for details.
### Check Logs
```bash
docker logs -f vibevoice-asr-demo
```
Wait until you see `Application startup complete.` — this means the server is ready.
---
## Step 2 — Verify the Server
```bash
# Check the model is loaded
curl http://localhost:6001/v1/models
```
Expected output:
```json
{
"data": [{ "id": "vibevoice", ... }]
}
```
### Quick Test with Audio File
```bash
docker exec -it vibevoice-asr-demo \
python3 /app/vllm_plugin/tests/test_api.py /app/en-Alice_woman.wav \
--url http://localhost:6001
```
---
## Step 3 — Launch the Gradio Demo
### Install tmux inside the container (to keep it running)
```bash
docker exec vibevoice-asr-demo apt-get install -y tmux
```
### Start Gradio in tmux
```bash
docker exec vibevoice-asr-demo bash -c \
"PYTHONUNBUFFERED=1 tmux new-session -d -s gradio \
'PYTHONUNBUFFERED=1 python3 /app/vllm_plugin/scripts/gradio_asr_demo_api_video.py \
--api_url http://localhost:6001 --share \
2>&1 | tee /tmp/gradio.log'"
```
### Get the Share Link
Wait ~20 seconds, then:
```bash
docker exec vibevoice-asr-demo cat /tmp/gradio.log
```
You should see:
```
✅ Connected to API: http://localhost:6001 | Model: vibevoice
🚀 Starting VibeVoice ASR Demo
* Running on local URL: http://0.0.0.0:7860
* Running on public URL: https://xxxxxx.gradio.live
```
The `gradio.live` link is publicly accessible (valid for 1 week).
### Gradio Options
| Flag | Description | Default |
|------|-------------|---------|
| `--api_url URL` | vLLM server URL | `http://localhost:8000` |
| `--share` | Create a public Gradio link | off |
| `--port PORT` | Local Gradio port | `7860` |
| `--cloudflared` | Use Cloudflare tunnel instead of Gradio share | off |
| `--max_video_size MB` | Max upload video size | `50` |
---
## Managing the Service
### Stop Gradio (keep ASR server running)
```bash
docker exec vibevoice-asr-demo tmux kill-session -t gradio
```
### Restart Gradio
Re-run the tmux command from Step 3.
### Stop Everything
```bash
docker stop vibevoice-asr-demo
docker rm vibevoice-asr-demo
```
---
## Example: Full Setup on GPU 0 with Port 6001
```bash
# 1. Start server
docker run -d --gpus '"device=0"' --name vibevoice-asr-demo \
--ipc=host -p 6001:6001 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app -w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --port 6001"
# 2. Wait for startup (~2 min), then verify
docker logs -f vibevoice-asr-demo # wait for "Application startup complete."
curl http://localhost:6001/v1/models
# 3. Install tmux and launch Gradio
docker exec vibevoice-asr-demo apt-get install -y tmux
docker exec vibevoice-asr-demo bash -c \
"PYTHONUNBUFFERED=1 tmux new-session -d -s gradio \
'PYTHONUNBUFFERED=1 python3 /app/vllm_plugin/scripts/gradio_asr_demo_api_video.py \
--api_url http://localhost:6001 --share \
2>&1 | tee /tmp/gradio.log'"
# 4. Get the public link
sleep 20 && docker exec vibevoice-asr-demo cat /tmp/gradio.log
```
## Troubleshooting
| Issue | Fix |
|-------|-----|
| `CUDA out of memory` | Use a different GPU (`device=X`) or reduce `--gpu-memory-utilization 0.7` in `start_server.py` |
| Gradio log is empty | Wait longer (~30s); Gradio buffers output. Use `PYTHONUNBUFFERED=1` as shown above |
| `Port already in use` | Pick a different port or stop the existing container: `docker stop <name> && docker rm <name>` |
| Share link shows "No interface" | Gradio is still loading. Wait for `Application startup complete` in the log |
| `tmux: command not found` | Run `docker exec <container> apt-get install -y tmux` first |
+134
View File
@@ -0,0 +1,134 @@
# VibeVoice-ASR
[![Hugging Face](https://img.shields.io/badge/HuggingFace-Collection-orange?logo=huggingface)](https://huggingface.co/microsoft/VibeVoice-ASR)
[![Live Playground](https://img.shields.io/badge/Live-Playground-green?logo=gradio)](https://aka.ms/vibevoice-asr)
**VibeVoice-ASR** is a unified speech-to-text model designed to handle **60-minute long-form audio** in a single pass, generating structured transcriptions containing **Who (Speaker), When (Timestamps), and What (Content)**, with support for **Customized Hotwords** and over **50 languages**.
**Model:** [VibeVoice-ASR-7B](https://huggingface.co/microsoft/VibeVoice-ASR)<br>
**Demo:** [VibeVoice-ASR-Demo](https://aka.ms/vibevoice-asr)<br>
**Report:** [VibeVoice-ASR-Report](https://arxiv.org/pdf/2601.18184)<br>
**Finetuning:** [finetune-guide](../finetuning-asr/README.md)<br>
**vLLM:** [vLLM-asr](./vibevoice-vllm-asr.md)<br>
**Transformers:** [VibeVoice-ASR-HF](https://huggingface.co/microsoft/VibeVoice-ASR-HF)<br>
## 🔥 Key Features
- **🕒 60-minute Single-Pass Processing**:
Unlike conventional ASR models that slice audio into short chunks (often losing global context), VibeVoice ASR accepts up to **60 minutes** of continuous audio input within 64K token length. This ensures consistent speaker tracking and semantic coherence across the entire hour.
- **👤 Customized Hotwords**:
Users can provide customized hotwords (e.g., specific names, technical terms, or background info) to guide the recognition process, significantly improving accuracy on domain-specific content.
- **📝 Rich Transcription (Who, When, What)**:
The model jointly performs ASR, diarization, and timestamping, producing a structured output that indicates *who* said *what* and *when*.
- **🌍 Multilingual & Code-Switching Support**:
It supports over 50 languages, requires no explicit language setting, and natively handles code-switching within and across utterances. See the [Language distribution](#language-distribution).
## 🏗️ Model Architecture
<p align="center">
<img src="../Figures/VibeVoice_ASR_archi.png" alt="VibeVoice ASR Architecture" width="80%">
</p>
# Demo
<div align="center" id="vibevoice-asr">
https://github.com/user-attachments/assets/acde5602-dc17-4314-9e3b-c630bc84aefa
</div>
## Evaluation
<p align="center">
<img src="../Figures/DER.jpg" alt="DER" width="50%"><br>
<img src="../Figures/cpWER.jpg" alt="cpWER" width="50%"><br>
<img src="../Figures/tcpWER.jpg" alt="tcpWER" width="50%">
</p>
## Installation
We recommend using NVIDIA Deep Learning Container to manage the CUDA environment.
1. Launch docker
```bash
# NVIDIA PyTorch Container 24.07 ~ 25.12 verified.
# Previous versions are also compatible.
sudo docker run --privileged --net=host --ipc=host --ulimit memlock=-1:-1 --ulimit stack=-1:-1 --gpus all --rm -it nvcr.io/nvidia/pytorch:25.12-py3
## If flash attention is not included in your docker environment, you need to install it manually
## Refer to https://github.com/Dao-AILab/flash-attention for installation instructions
# pip install flash-attn --no-build-isolation
```
2. Install from github
```bash
git clone https://github.com/microsoft/VibeVoice.git
cd VibeVoice
pip install -e .
```
## Usages
### Usage 1: Launch Gradio demo
```bash
apt update && apt install ffmpeg -y # for demo
python demo/vibevoice_asr_gradio_demo.py --model_path microsoft/VibeVoice-ASR --share
```
### Usage 2: Inference from files directly
```bash
python demo/vibevoice_asr_inference_from_file.py --model_path microsoft/VibeVoice-ASR --audio_files [add an audio path here]
```
## Finetuning
LoRA (Low-Rank Adaptation) fine-tuning is supported. See [Finetuning](../finetuning-asr/README.md) for detailed guide.
## Results
### Multilingual
| Dataset | Language | DER | cpWER | tcpWER | WER |
|----------------|-----------|------|-------|--------|------|
| MLC-Challenge | English | 4.28 | 11.48 | 13.02 | 7.99 |
| MLC-Challenge | French | 3.80 | 18.80 | 19.64 | 15.21 |
| MLC-Challenge | German | 1.04 | 17.10 | 17.26 | 16.30 |
| MLC-Challenge | Italian | 2.08 | 15.76 | 15.91 | 13.91 |
| MLC-Challenge | Japanese | 0.82 | 15.33 | 15.41 | 14.69 |
| MLC-Challenge | Korean | 4.52 | 15.35 | 16.07 | 9.65 |
| MLC-Challenge | Portuguese| 7.98 | 29.91 | 31.65 | 21.54 |
| MLC-Challenge | Russian | 0.90 | 12.94 | 12.98 | 12.40 |
| MLC-Challenge | Spanish | 2.67 | 10.51 | 11.71 | 8.04 |
| MLC-Challenge | Thai | 4.09 | 14.91 | 15.57 | 13.61 |
| MLC-Challenge | Vietnamese| 0.16 | 14.57 | 14.57 | 14.43 |
---
| Dataset | Language | DER | cpWER | tcpWER | WER |
|----------------|-----------|------|-------|--------|------|
| AISHELL-4 | Chinese | 6.77 | 24.99 | 25.35 | 21.40 |
| AMI-IHM | English | 11.92| 20.41 | 20.82 | 18.81 |
| AMI-SDM | English | 13.43| 28.82 | 29.80 | 24.65 |
| AliMeeting | Chinese | 10.92| 29.33 | 29.51 | 27.40 |
| MLC-Challenge | Average | 3.42 | 14.81 | 15.66 | 12.07|
## Language Distribution
<p align="center">
<img src="../Figures/language_distribution_horizontal.png" alt="Language Distribution" width="80%">
</p>
## 📄 License
This project is licensed under the [MIT License](../LICENSE).
+142
View File
@@ -0,0 +1,142 @@
<div align="center">
## 🎙️ VibeVoice-Realtime: Real-time LongForm TexttoSpeech with Streaming Input
[![Hugging Face](https://img.shields.io/badge/HuggingFace-Collection-orange?logo=huggingface)](https://huggingface.co/microsoft/VibeVoice-Realtime-0.5B)
[![Colab](https://img.shields.io/badge/Run-Colab-orange?logo=googlecolab)](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb)
</div>
VibeVoice-Realtime is a **lightweight realtime** text-to-speech model supporting **streaming text input** and **robust long-form speech generation**. It can be used to build real-time TTS services, narrate live data streams, and let different LLMs start speaking from their very first tokens (plug in your preferred model) long before a full answer is generated. It produces initial audible speech in **~200 milliseconds** (hardware dependent).
**Model:** [VibeVoice Realtime 0.5B (on Hugging Face)](https://huggingface.co/microsoft/VibeVoice-Realtime-0.5B)<br>
**Colab:** [VibeVoice Realtime Colab (Jupyter Notebook)](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb)<br>
> Note (multilingual exploration): Although the model is primarily built for English, we found that it still exhibits a certain level of multilingual capability—and even performs reasonably well in some languages. We provide nine additional languages (German, French, Italian, Japanese, Korean, Dutch, Polish, Portuguese, and Spanish) for users to explore. These multilingual behaviors have not been extensively tested; use with caution and share observations.
The model uses an interleaved, windowed design: it incrementally encodes incoming text chunks while, in parallel, continuing diffusion-based acoustic latent generation from prior context. Unlike the full multi-speaker long-form variants, this streaming model removes the semantic tokenizer and relies solely on an efficient acoustic tokenizer operating at an ultra-low frame rate (7.5 Hz).
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../Figures/VibeVoice_logo_white.png">
<img src="../Figures/VibeVoice_Realtime.png" alt="VibeVoice Realtime Overview" width="800" />
</picture>
<br>
<em>Overview of VibeVoice Realtime Model.</em>
</div>
Key features:
- Parameter size: 0.5B (deployment-friendly)
- Real-time TTS (~200 milliseconds first audible latency)
- Streaming text input
- Robust long-form speech generation
- 8k context window( ~10 minutes audio generation)
This real-time variant supports only a single speaker. For multispeaker conversational speech generation, please use other VibeVoice models (longform multispeaker variants). The model is currently intended for English speech only; other languages may produce unpredictable results.
To mitigate deepfake risks and ensure low latency for the first speech chunk, voice prompts are provided in an embedded format. For users requiring voice customization, please reach out to our team. We will also be expanding the range of available speakers.
### 📋 TODO
- [√] Add more voices (expand available speakers/voice timbres)
- [ ] Implement streaming text input function to feed new tokens while audio is still being generated
- [ ] Merge models into official HuggingFace's `transformers` repository
### 🎵 Demo Examples
<div align="center" id="generated-example-audio-vibevoice-realtime">
https://github.com/user-attachments/assets/9aa8ab3c-681d-4a02-b9ea-3f54ffd180b2
</div>
## Results
The model achieves satisfactory performance on short-sentence benchmarks, while the model is more focused on longform speech generation.
### Zero-shot TTS performance on LibriSpeech test-clean set
| Model | WER (%) ↓ | Speaker Similarity ↑ |
|:--------------------|:---------:|:----------------:|
| VALL-E 2 | 2.40 | 0.643 |
| Voicebox | 1.90 | 0.662 |
| MELLE | 2.10 | 0.625 |
| **VibeVoice-Realtime-0.5B** | 2.00 | 0.695 |
### Zero-shot TTS performance on SEED test-en set
| Model | WER (%) ↓ | Speaker Similarity ↑ |
|:--------------------|:---------:|:----------------:|
| MaskGCT | 2.62 | 0.714 |
| Seed-TTS | 2.25 | 0.762 |
| FireRedTTS | 3.82 | 0.460 |
| SparkTTS | 1.98 | 0.584 |
| CosyVoice2 | 2.57 | 0.652 |
| **VibeVoice-Realtime-0.5B** | 2.05 | 0.633 |
## Installation
We recommend using NVIDIA Deep Learning Container to manage the CUDA environment.
1. Launch docker
```bash
# NVIDIA PyTorch Container 24.07 / 24.10 / 24.12 verified.
# Later versions are also compatible.
sudo docker run --privileged --net=host --ipc=host --ulimit memlock=-1:-1 --ulimit stack=-1:-1 --gpus all --rm -it nvcr.io/nvidia/pytorch:24.07-py3
## If flash attention is not included in your docker environment, you need to install it manually
## Refer to https://github.com/Dao-AILab/flash-attention for installation instructions
# pip install flash-attn --no-build-isolation
```
2. Install from github
```bash
git clone https://github.com/microsoft/VibeVoice.git
cd VibeVoice/
pip install -e .[streamingtts]
```
## Usages
### Usage 1: Launch real-time websocket demo
Note: NVIDIA T4 / Mac M4 Pro achieve real-time performance in our tests; other devices with weaker inference capability may require further testing and speed optimizations.
Due to network latency, the time when audio playback is heard may exceed the ~300 ms first speech chunk generation latency.
```bash
python demo/vibevoice_realtime_demo.py --model_path microsoft/VibeVoice-Realtime-0.5B
```
Tip: Just try it on [Colab](https://colab.research.google.com/github/microsoft/VibeVoice/blob/main/demo/vibevoice_realtime_colab.ipynb).
### Usage 2: Inference from files directly
```bash
# We provide some example scripts under demo/text_examples/ for demo
python demo/realtime_model_inference_from_file.py --model_path microsoft/VibeVoice-Realtime-0.5B --txt_path demo/text_examples/1p_vibevoice.txt --speaker_name Carter
```
### [Optional] More experimental voices
Download additional experimental multi-lingual speakers before launching demo or inference from files.
```bash
bash demo/download_experimental_voices.sh
```
## Risks and limitations
While efforts have been made to optimize it through various techniques, it may still produce outputs that are unexpected, biased, or inaccurate. VibeVoice inherits any biases, errors, or omissions produced by its base model (specifically, Qwen2.5 0.5b in this release).
Potential for Deepfakes and Disinformation: High-quality synthetic speech can be misused to create convincing fake audio content for impersonation, fraud, or spreading disinformation. Users must ensure transcripts are reliable, check content accuracy, and avoid using generated content in misleading ways. Users are expected to use the generated content and to deploy the models in a lawful manner, in full compliance with all applicable laws and regulations in the relevant jurisdictions. It is best practice to disclose the use of AI when sharing AI-generated content.
English only: Transcripts in languages other than English may result in unexpected audio outputs.
Non-Speech Audio: The model focuses solely on speech synthesis and does not handle background noise, music, or other sound effects.
Code, formulas, and special symbols: The model does not currently support reading code, mathematical formulas, or uncommon symbols. Please preprocess input text to remove or normalize such content to avoid unpredictable results.
Very short inputs: When the input text is extremely short (three words or fewer), the models stability may degrade.
We do not recommend using VibeVoice in commercial or real-world applications without further testing and development. This model is intended for research and development purposes only. Please use responsibly.
+147
View File
@@ -0,0 +1,147 @@
# VibeVoice-TTS
[![Hugging Face](https://img.shields.io/badge/HuggingFace-Collection-orange?logo=huggingface)](https://huggingface.co/microsoft/VibeVoice-1.5B)
[![Technical Report](https://img.shields.io/badge/Technical-Report-red?logo=arxiv)](https://arxiv.org/pdf/2508.19205)
**VibeVoice-TTS** is a **long-form**, **multi-speaker** text-to-speech model designed to generate **expressive conversational audio** such as podcasts from text. It can synthesize speech up to **90 minutes** long with up to **4 distinct speakers**, surpassing the typical 12 speaker limits of many prior models.
**Model:** [VibeVoice-1.5B](https://huggingface.co/microsoft/VibeVoice-1.5B)<br>
**Report:** [Technical Report](https://arxiv.org/pdf/2508.19205)<br>
<div align="center">
| Model | Context Length | Generation Length | Weight |
|-------|----------------|-------------------|--------|
| VibeVoice-1.5B | 64K | ~90 min | [HF link](https://huggingface.co/microsoft/VibeVoice-1.5B) |
| VibeVoice-Large | 32K | ~45 min | Disabled |
</div>
## 🔥 Key Features
- **⏱️ 90-minute Long-form Generation**:
Synthesizes conversational/single-speaker speech up to **90 minutes** in a single pass, maintaining speaker consistency and semantic coherence throughout.
- **👥 Multi-speaker Support**:
Supports up to **4 distinct speakers** in a single conversation, with natural turn-taking and speaker consistency across long dialogues.
- **🎭 Expressive Speech**:
Generates expressive, natural-sounding speech that captures conversational dynamics and emotional nuances.
- **🌐 Multi-lingual Support**:
Supports English, Chinese and other languages.
## 🏗️ Model Architecture
VibeVoice-TTS employs a [next-token diffusion](https://arxiv.org/pdf/2508.19205) framework that combines:
- **Large Language Model (LLM)**: Based on Qwen2.5, understands textual context and dialogue flow
- **Continuous Speech Tokenizers**: Acoustic and Semantic tokenizers operating at an ultra-low frame rate of **7.5 Hz**, efficiently preserving audio fidelity while boosting computational efficiency
- **Diffusion Head**: Generates high-fidelity acoustic details through diffusion-based generation
<div align="center">
<img src="../Figures/VibeVoice.jpg" alt="VibeVoice Overview" width="80%">
</div>
## 🎵 Demo Examples
**English**
<div align="center">
https://github.com/user-attachments/assets/0967027c-141e-4909-bec8-091558b1b784
</div>
**Chinese**
<div align="center">
https://github.com/user-attachments/assets/322280b7-3093-4c67-86e3-10be4746c88f
</div>
**Cross-Lingual**
<div align="center">
https://github.com/user-attachments/assets/838d8ad9-a201-4dde-bb45-8cd3f59ce722
</div>
**Spontaneous Singing**
<div align="center">
https://github.com/user-attachments/assets/6f27a8a5-0c60-4f57-87f3-7dea2e11c730
</div>
**Long Conversation with 4 people**
<div align="center">
https://github.com/user-attachments/assets/a357c4b6-9768-495c-a576-1618f6275727
</div>
For more examples, see the [Project Page](https://microsoft.github.io/VibeVoice).
## Installation and Usage
Disabled due to widespread misuse.
## Results
The model achieves state-of-the-art performance on long-form multi-speaker speech generation tasks. For detailed evaluation results, please refer to the [paper](https://arxiv.org/pdf/2508.19205).
<div align="center">
<img src="../Figures/VibeVoice-TTS-results.jpg" alt="VibeVoice Results" width="80%">
</div>
## 🚨 Tips
We observed users may encounter occasional instability when synthesizing Chinese speech. We recommend:
- Using English punctuation even for Chinese text, preferably only commas and periods.
- Using the Large model variant, which is considerably more stable.
- If you find that the generated voice speaks too fast, try chunking your text into multiple turns with the same speaker label.
We'd like to thank [PsiPi](https://huggingface.co/PsiPi) for sharing an interesting way for emotion control. Details can be found via [discussion12](https://huggingface.co/microsoft/VibeVoice-1.5B/discussions/12).
## FAQ
#### Q1: Is this a pretrained model?
**A:** Yes, it's a pretrained model without any post-training or benchmark-specific optimizations. In a way, this makes VibeVoice very versatile and fun to use.
#### Q2: Randomly trigger Sounds / Music / BGM.
**A:** As you can see from our demo page, the background music or sounds are spontaneous. This means we can't directly control whether they are generated or not. The model is content-aware, and these sounds are triggered based on the input text and the chosen voice prompt.
Here are a few things we've noticed:
* If the voice prompt you use contains background music, the generated speech is more likely to have it as well. (The Large model is quite stable and effective at this—give it a try on the demo!)
* If the voice prompt is clean (no BGM), but the input text includes introductory words or phrases like "Welcome to," "Hello," or "However," background music might still appear.
* Speaker voice related, using "Alice" results in random BGM than others (fixed).
* In other scenarios, the Large model is more stable and has a lower probability of generating unexpected background music.
In fact, we intentionally decided not to denoise our training data because we think it's an interesting feature for BGM to show up at just the right moment. You can think of it as a little easter egg we left for you.
#### Q3: Text normalization?
**A:** We don't perform any text normalization during training or inference. Our philosophy is that a large language model should be able to handle complex user inputs on its own. However, due to the nature of the training data, you might still run into some corner cases.
#### Q4: Singing Capability.
**A:** Our training data **doesn't contain any music data**. The ability to sing is an emergent capability of the model (which is why it might sound off-key, even on a famous song like 'See You Again'). (The Large model is more likely to exhibit this than the 1.5B).
#### Q5: Some Chinese pronunciation errors.
**A:** The volume of Chinese data in our training set is significantly smaller than the English data. Additionally, certain special characters (e.g., Chinese quotation marks) may occasionally cause pronunciation issues.
#### Q6: Instability of cross-lingual transfer.
**A:** The model does exhibit strong cross-lingual transfer capabilities, including the preservation of accents, but its performance can be unstable. This is an emergent ability of the model that we have not specifically optimized. It's possible that a satisfactory result can be achieved through repeated sampling.
## Risks and Limitations
While efforts have been made to optimize it through various techniques, it may still produce outputs that are unexpected, biased, or inaccurate. VibeVoice inherits any biases, errors, or omissions produced by its base model (specifically, Qwen2.5 1.5b in this release). Potential for Deepfakes and Disinformation: High-quality synthetic speech can be misused to create convincing fake audio content for impersonation, fraud, or spreading disinformation. Users must ensure transcripts are reliable, check content accuracy, and avoid using generated content in misleading ways. Users are expected to use the generated content and to deploy the models in a lawful manner, in full compliance with all applicable laws and regulations in the relevant jurisdictions. It is best practice to disclose the use of AI when sharing AI-generated content.
English and Chinese only: Transcripts in languages other than English or Chinese may result in unexpected audio outputs.
Non-Speech Audio: The model focuses solely on speech synthesis and does not handle background noise, music, or other sound effects.
Overlapping Speech: The current model does not explicitly model or generate overlapping speech segments in conversations.
We do not recommend using VibeVoice in commercial or real-world applications without further testing and development. This model is intended for research and development purposes only. Please use responsibly.
+189
View File
@@ -0,0 +1,189 @@
# VibeVoice vLLM ASR Deployment
<a href="https://huggingface.co/microsoft/VibeVoice-ASR"><img alt="Huggingface" src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-VibeVoice--ASR-blue"></a>
Deploy VibeVoice ASR model as a high-performance API service using [vLLM](https://github.com/vllm-project/vllm). This plugin provides OpenAI-compatible API endpoints for speech-to-text transcription with streaming support.
## 🔥 Key Features
- **🚀 High-Performance Serving**: Optimized for high-throughput ASR inference with vLLM's continuous batching
- **📡 OpenAI-Compatible API**: Standard `/v1/chat/completions` endpoint with streaming support
- **🎵 Long Audio Support**: Process up to 60+ minutes of audio in a single request
- **🔌 Plugin Architecture**: No vLLM source code modification required - just install and run
- **⚡ Data Parallel (DP)**: Run independent model replicas across multiple GPUs with automatic load balancing behind a single port
## 🛠️ Installation
Using Official vLLM Docker Image (Recommended)
1. Clone the repository
```bash
git clone https://github.com/microsoft/VibeVoice.git
cd VibeVoice
```
2. Launch the server (background mode)
```bash
docker run -d --gpus all --name vibevoice-vllm \
--ipc=host \
-p 8000:8000 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py"
```
## ⚡ Multi-GPU Deployment
The launcher supports two types of GPU parallelism via `--tp` and `--dp` flags:
| Flag | Name | What it does |
|------|------|-------------|
| `--tp N` | Tensor Parallel | Splits **one model** across N GPUs (for models too large for a single GPU) |
| `--dp N` | Data Parallel | Runs **N independent replicas**, one per GPU, with automatic load balancing behind a single port |
### Data Parallel (Recommended for scaling throughput)
Run N independent replicas on N GPUs with automatic load balancing behind a single port.
When `--dp N` is specified (N > 1), the launcher automatically starts N independent vLLM
processes behind an nginx reverse proxy (2×N workers) for optimal throughput:
```bash
docker run -d --gpus '"device=0,1,2,3"' --name vibevoice-vllm \
--ipc=host \
-p 8000:8000 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --dp 4"
```
Run on all 8 GPUs:
```bash
docker run -d --gpus all --name vibevoice-vllm \
--ipc=host \
-p 8000:8000 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --dp 8"
```
### Tensor Parallel
Split a single model across 2 GPUs (useful if GPU memory is limited):
```bash
docker run -d --gpus '"device=0,1"' --name vibevoice-vllm \
--ipc=host \
-p 8000:8000 \
-e VIBEVOICE_FFMPEG_MAX_CONCURRENCY=64 \
-e PYTORCH_ALLOC_CONF=expandable_segments:True \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --tp 2"
```
### Hybrid (DP × TP)
Combine both — e.g., 2 replicas, each split across 2 GPUs (4 GPUs total):
```bash
docker run -d --gpus '"device=0,1,2,3"' --name vibevoice-vllm \
--ipc=host \
-p 8000:8000 \
-v $(pwd):/app \
-w /app \
--entrypoint bash \
vllm/vllm-openai:v0.14.1 \
-c "python3 /app/vllm_plugin/scripts/start_server.py --dp 2 --tp 2"
```
> **Note**: Total GPUs required = `dp × tp`. Make sure to expose enough GPU devices in the Docker `--gpus` flag.
3. View logs
```bash
docker logs -f vibevoice-vllm
```
> **Note**:
> - The `-d` flag runs the container in background (detached mode)
> - Use `docker stop vibevoice-vllm` to stop the service
> - The model will be downloaded to HuggingFace cache (`~/.cache/huggingface`) inside the container
## 🚀 Usages
### Test the API
Once the vLLM server is running, test it with the provided script:
```bash
# Basic transcription
docker exec -it vibevoice-vllm python3 vllm_plugin/tests/test_api.py /app/audio.wav
# With hotwords for better recognition of specific terms
docker exec -it vibevoice-vllm python3 vllm_plugin/tests/test_api.py /app/audio.wav --hotwords "Microsoft,VibeVoice"
```
```bash
# With auto-recovery from repetition loops (for long audio)
docker exec -it vibevoice-vllm python3 vllm_plugin/tests/test_api_auto_recover.py /app/audio.wav
# Auto-recover with hotwords
docker exec -it vibevoice-vllm python3 vllm_plugin/tests/test_api_auto_recover.py /app/audio.wav --hotwords "Microsoft,VibeVoice"
```
> **Note**:
> - The audio/video file must be inside the mounted directory (`/app` in the container). Copy your files to the VibeVoice folder before testing.
> - Hotwords help improve recognition of domain-specific terms like proper nouns, technical terms, and speaker names.
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `VIBEVOICE_FFMPEG_MAX_CONCURRENCY` | Maximum FFmpeg processes for audio decoding | `64` |
| `PYTORCH_ALLOC_CONF` | PyTorch memory allocator config | `expandable_segments:True` |
## 📊 Performance Tips
1. **GPU Memory**: Use `--gpu-memory-utilization 0.9` for maximum throughput if you have dedicated GPU
2. **Batch Size**: Increase `--max-num-seqs` for higher concurrency (requires more GPU memory)
3. **FFmpeg Concurrency**: Tune `VIBEVOICE_FFMPEG_MAX_CONCURRENCY` based on CPU cores
## 🚨 Troubleshooting
### Common Issues
1. **"CUDA out of memory"**
- Reduce `--gpu-memory-utilization`
- Reduce `--max-num-seqs`
- Use smaller `--max-model-len`
2. **"Audio decoding failed"**
- Ensure FFmpeg is installed: `ffmpeg -version`
- Check audio file format is supported
3. **"Model not found"**
- Ensure model path contains `config.json` and model weights
- Generate tokenizer files if missing
4. **"Plugin not loaded"**
- Verify installation: `pip show vibevoice`
- Check entry point: `pip show -f vibevoice | grep entry`
+154
View File
@@ -0,0 +1,154 @@
# VibeVoice ASR LoRA Fine-tuning
This directory contains scripts for LoRA (Low-Rank Adaptation) fine-tuning of the VibeVoice ASR model.
## Requirements
```bash
# Install vibevoice first
pip install -e .
pip install peft
```
## Toy Dataset
> **Note**: The `toy_dataset/` included in this directory contains **synthetic audio generated by VibeVoice TTS** for demonstration purposes only. It is NOT a full finetuning dataset.
>
> When using your own data, you should:
> - Prepare real audio recordings with accurate transcriptions
> - Adjust hyperparameters (learning rate, epochs, LoRA rank) based on your dataset size and domain
> - Consider the audio quality and speaker diversity in your data
## Data Format
Training data should be organized as pairs of audio files and JSON labels in the same directory:
```
toy_dataset/
├── 0.mp3
├── 0.json
├── 1.mp3
├── 1.json
└── ...
```
### JSON Label Format
Each JSON file should have the following structure:
```json
{
"audio_duration": 351.73,
"audio_path": "0.mp3",
"segments": [
{
"speaker": 0,
"text": "Hey everyone, welcome back...",
"start": 0.0,
"end": 38.68
},
{
"speaker": 1,
"text": "Thanks for having me...",
"start": 38.75,
"end": 77.88
}
],
"customized_context": ["Tea Brew", "Aiden Host", "The property is near Meter Street."] // optional, domain-specific terms or context sentences
}
```
## Training
### Basic
```bash
# 1 GPU
torchrun --nproc_per_node=1 lora_finetune.py \
--model_path microsoft/VibeVoice-ASR \
--data_dir ./toy_dataset \
--output_dir ./output \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--learning_rate 1e-4 \
--bf16 \
--report_to none
# Specific GPUs (e.g., GPU 0,1,2,3)
CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 lora_finetune.py \
--model_path microsoft/VibeVoice-ASR \
--data_dir ./toy_dataset \
--output_dir ./output \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--learning_rate 1e-4 \
--bf16 \
--report_to none
```
### Full Options
The script uses HuggingFace's `TrainingArguments`, so all standard options are available:
```bash
torchrun --nproc_per_node=4 lora_finetune.py \
--model_path microsoft/VibeVoice-ASR \
--data_dir ./toy_dataset \
--output_dir ./output \
--lora_r 16 \
--lora_alpha 32 \
--lora_dropout 0.05 \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 4 \
--learning_rate 1e-4 \
--warmup_ratio 0.1 \
--weight_decay 0.01 \
--max_grad_norm 1.0 \
--logging_steps 10 \
--save_steps 100 \
--gradient_checkpointing \
--bf16 \
--report_to none
```
### Key Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `--lora_r` | 16 | LoRA rank (lower = fewer params, higher = more expressive) |
| `--lora_alpha` | 32 | LoRA scaling factor (typically 2x rank) |
| `--lora_dropout` | 0.05 | Dropout for LoRA layers |
| `--per_device_train_batch_size` | 8 | Batch size per device |
| `--gradient_accumulation_steps` | 1 | Effective batch size = batch_size × grad_accum |
| `--learning_rate` | 5e-5 | Learning rate (1e-4 to 2e-4 typical for LoRA) |
| `--gradient_checkpointing` | False | Enable to reduce memory usage |
| `--use_customized_context` | True | Include customized_context from JSON as additional context |
| `--max_audio_length` | None | Skip audio longer than this (seconds) |
## Inference with Fine-tuned Model
```bash
python inference_lora.py \
--base_model microsoft/VibeVoice-ASR \
--lora_path ./output \
--audio_file ./toy_dataset/0.mp3 \
--context_info "Tea Brew, Aiden Host"
```
## Merging LoRA Weights (Optional)
To merge LoRA weights into the base model for faster inference:
```python
from peft import PeftModel
# Load base model + LoRA
model = VibeVoiceASRForConditionalGeneration.from_pretrained("microsoft/VibeVoice-ASR", ...)
model = PeftModel.from_pretrained(model, "./output")
# Merge and save
model = model.merge_and_unload()
model.save_pretrained("./merged_model")
```
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python
"""
Inference with LoRA Fine-tuned VibeVoice ASR Model
This script loads a LoRA fine-tuned model and runs inference.
Usage:
python inference_lora.py \
--base_model microsoft/VibeVoice-ASR \
--lora_path ./output \
--audio_file ./toy_dataset/0.mp3
"""
import argparse
import torch
from peft import PeftModel
from vibevoice.modular.modeling_vibevoice_asr import VibeVoiceASRForConditionalGeneration
from vibevoice.processor.vibevoice_asr_processor import VibeVoiceASRProcessor
def load_lora_model(
base_model_path: str,
lora_path: str,
device: str = "cuda",
dtype: torch.dtype = torch.bfloat16,
):
"""
Load base model and merge with LoRA weights.
Args:
base_model_path: Path to base pretrained model
lora_path: Path to LoRA adapter weights
device: Device to load model on
dtype: Data type for model
Returns:
Tuple of (model, processor)
"""
print(f"Loading base model from {base_model_path}")
# Load processor
processor = VibeVoiceASRProcessor.from_pretrained(
base_model_path,
language_model_pretrained_name="Qwen/Qwen2.5-7B"
)
# Load base model
model = VibeVoiceASRForConditionalGeneration.from_pretrained(
base_model_path,
dtype=dtype,
device_map=device if device == "auto" else None,
attn_implementation="flash_attention_2",
trust_remote_code=True,
)
if device != "auto":
model = model.to(device)
# Load LoRA adapter
print(f"Loading LoRA adapter from {lora_path}")
model = PeftModel.from_pretrained(model, lora_path)
# Optionally merge LoRA weights into base model for faster inference
# model = model.merge_and_unload()
model.eval()
print("Model loaded successfully")
return model, processor
def transcribe(
model,
processor,
audio_path: str,
max_new_tokens: int = 4096,
temperature: float = 0.0,
context_info: str = None,
device: str = "cuda",
):
"""
Transcribe an audio file using the LoRA fine-tuned model.
Args:
model: The LoRA fine-tuned model
processor: The processor
audio_path: Path to audio file
max_new_tokens: Maximum tokens to generate
temperature: Sampling temperature (0 = greedy)
context_info: Optional context info (e.g., hotwords)
device: Device
Returns:
Transcription result
"""
print(f"\nTranscribing: {audio_path}")
# Process audio
inputs = processor(
audio=audio_path,
sampling_rate=None,
return_tensors="pt",
padding=True,
add_generation_prompt=True,
context_info=context_info,
)
# Move to device
inputs = {k: v.to(device) if isinstance(v, torch.Tensor) else v
for k, v in inputs.items()}
# Generation config
gen_config = {
"max_new_tokens": max_new_tokens,
"pad_token_id": processor.pad_id,
"eos_token_id": processor.tokenizer.eos_token_id,
"do_sample": temperature > 0,
}
if temperature > 0:
gen_config["temperature"] = temperature
gen_config["top_p"] = 0.9
# Generate
with torch.no_grad():
output_ids = model.generate(**inputs, **gen_config)
# Decode
input_length = inputs['input_ids'].shape[1]
generated_ids = output_ids[0, input_length:]
generated_text = processor.decode(generated_ids, skip_special_tokens=True)
# Parse structured output
try:
segments = processor.post_process_transcription(generated_text)
except Exception as e:
print(f"Warning: Failed to parse structured output: {e}")
segments = []
return {
"raw_text": generated_text,
"segments": segments,
}
def main():
parser = argparse.ArgumentParser(description="Inference with LoRA Fine-tuned VibeVoice ASR")
parser.add_argument(
"--base_model",
type=str,
default="microsoft/VibeVoice-ASR",
help="Path to base pretrained model"
)
parser.add_argument(
"--lora_path",
type=str,
required=True,
help="Path to LoRA adapter weights"
)
parser.add_argument(
"--audio_file",
type=str,
required=True,
help="Path to audio file to transcribe"
)
parser.add_argument(
"--context_info",
type=str,
default=None,
help="Optional context info (e.g., 'Hotwords: Tea Brew, Aiden Host')"
)
parser.add_argument(
"--max_new_tokens",
type=int,
default=4096,
help="Maximum tokens to generate"
)
parser.add_argument(
"--temperature",
type=float,
default=0.0,
help="Sampling temperature (0 = greedy)"
)
parser.add_argument(
"--device",
type=str,
default="cuda" if torch.cuda.is_available() else "cpu",
help="Device to use"
)
args = parser.parse_args()
# Load model
dtype = torch.bfloat16 if args.device != "cpu" else torch.float32
model, processor = load_lora_model(
base_model_path=args.base_model,
lora_path=args.lora_path,
device=args.device,
dtype=dtype,
)
# Transcribe
result = transcribe(
model=model,
processor=processor,
audio_path=args.audio_file,
max_new_tokens=args.max_new_tokens,
temperature=args.temperature,
context_info=args.context_info,
device=args.device,
)
# Print results
print("\n" + "="*60)
print("Transcription Result")
print("="*60)
print("\n--- Raw Output ---")
raw_text = result['raw_text']
print(raw_text[:2000] + "..." if len(raw_text) > 2000 else raw_text)
if result['segments']:
print(f"\n--- Structured Output ({len(result['segments'])} segments) ---")
for seg in result['segments'][:20]:
print(f"[{seg.get('start_time', 'N/A')} - {seg.get('end_time', 'N/A')}] "
f"Speaker {seg.get('speaker_id', 'N/A')}: {seg.get('text', '')[:80]}...")
if len(result['segments']) > 20:
print(f" ... and {len(result['segments']) - 20} more segments")
if __name__ == "__main__":
main()
+556
View File
@@ -0,0 +1,556 @@
#!/usr/bin/env python
"""
VibeVoice ASR LoRA Fine-tuning Script
This script implements LoRA (Low-Rank Adaptation) fine-tuning for the VibeVoice ASR model.
It uses PEFT (Parameter-Efficient Fine-Tuning) library for efficient training.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import numpy as np
from transformers import (
TrainingArguments,
Trainer,
HfArgumentParser,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType,
)
from vibevoice.modular.modeling_vibevoice_asr import VibeVoiceASRForConditionalGeneration
from vibevoice.processor.vibevoice_asr_processor import VibeVoiceASRProcessor
# Setup logging
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
@dataclass
class ModelArguments:
"""Arguments for model configuration."""
model_path: str = field(
default="microsoft/VibeVoice-ASR",
metadata={"help": "Path to pretrained model (HuggingFace model ID or local path)"}
)
@dataclass
class DataArguments:
"""Arguments for data configuration."""
data_dir: str = field(
default="./toy_dataset",
metadata={"help": "Directory containing training data"}
)
max_audio_length: Optional[float] = field(
default=None,
metadata={"help": "Maximum audio length in seconds (default: no limit)"}
)
use_customized_context: bool = field(
default=True,
metadata={"help": "Whether to use customized_context from JSON as additional context"}
)
@dataclass
class LoraArguments:
"""Arguments for LoRA configuration."""
lora_r: int = field(
default=16,
metadata={"help": "LoRA rank"}
)
lora_alpha: int = field(
default=32,
metadata={"help": "LoRA alpha (scaling factor)"}
)
lora_dropout: float = field(
default=0.05,
metadata={"help": "LoRA dropout"}
)
@dataclass
class VibeVoiceASRDataCollator:
"""
Data collator for VibeVoice ASR fine-tuning.
Handles batching of variable-length audio and text sequences.
"""
processor: VibeVoiceASRProcessor
pad_token_id: int
label_pad_token_id: int = -100
def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
"""
Collate a batch of features into model inputs.
"""
# Separate inputs and labels
input_ids_list = [f["input_ids"] for f in features]
labels_list = [f["labels"] for f in features]
acoustic_mask_list = [f["acoustic_input_mask"] for f in features]
speech_list = [f["speech"] for f in features]
vae_tok_lens = [f["vae_tok_len"] for f in features]
# Determine max lengths
max_seq_len = max(len(ids) for ids in input_ids_list)
max_speech_len = max(len(s) for s in speech_list)
max_vae_len = max(vae_tok_lens)
batch_size = len(features)
# Initialize padded tensors
input_ids = torch.full((batch_size, max_seq_len), self.pad_token_id, dtype=torch.long)
attention_mask = torch.zeros((batch_size, max_seq_len), dtype=torch.long)
labels = torch.full((batch_size, max_seq_len), self.label_pad_token_id, dtype=torch.long)
acoustic_input_mask = torch.zeros((batch_size, max_seq_len), dtype=torch.bool)
speech_tensors = torch.zeros((batch_size, max_speech_len), dtype=torch.float32)
speech_masks = torch.zeros((batch_size, max_vae_len), dtype=torch.bool)
# Fill in the tensors (right padding for training)
# Note: processor uses left padding for inference/generation, but training uses right padding
for i, (ids, lbls, amask, speech, vae_len) in enumerate(
zip(input_ids_list, labels_list, acoustic_mask_list, speech_list, vae_tok_lens)
):
seq_len = len(ids)
# Right padding for input_ids and labels
input_ids[i, :seq_len] = torch.tensor(ids, dtype=torch.long)
attention_mask[i, :seq_len] = 1
labels[i, :seq_len] = torch.tensor(lbls, dtype=torch.long)
acoustic_input_mask[i, :seq_len] = torch.tensor(amask, dtype=torch.bool)
# Speech tensors (right padding, zeros work as padding)
speech_len = len(speech)
speech_tensors[i, :speech_len] = torch.tensor(speech, dtype=torch.float32)
speech_masks[i, :vae_len] = True
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels,
"acoustic_input_mask": acoustic_input_mask,
"speech_tensors": speech_tensors,
"speech_masks": speech_masks,
}
class VibeVoiceASRDataset(Dataset):
"""
Dataset for VibeVoice ASR fine-tuning.
Expected data format:
- Audio files: .mp3, .wav, .flac, etc.
- Label files: .json with matching name
JSON format:
{
"audio_path": "0.mp3",
"audio_duration": 351.73,
"segments": [
{
"speaker": 0,
"text": "Hey everyone, welcome back...",
"start": 0.0,
"end": 38.68
},
...
],
"customized_context": ["Tea Brew", "The property is near Meter Street."] # optional
}
"""
def __init__(
self,
data_dir: str,
processor: VibeVoiceASRProcessor,
max_audio_length: Optional[float] = None, # in seconds
use_customized_context: bool = True,
):
"""
Initialize the dataset.
Args:
data_dir: Directory containing audio files and JSON labels
processor: VibeVoice ASR processor
max_audio_length: Maximum audio length in seconds (None = no limit)
use_customized_context: Whether to include customized_context in prompt
"""
self.data_dir = Path(data_dir)
self.processor = processor
self.max_audio_length = max_audio_length
self.use_customized_context = use_customized_context
# Find all JSON files
self.samples = self._load_samples()
logger.info(f"Loaded {len(self.samples)} samples from {data_dir}")
def _load_samples(self) -> List[Dict[str, Any]]:
"""Load and validate all samples from data directory."""
samples = []
for json_path in sorted(self.data_dir.glob("*.json")):
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Get audio path from JSON
audio_filename = data.get("audio_path")
if not audio_filename:
logger.warning(f"No audio_path specified in {json_path}")
continue
audio_path = self.data_dir / audio_filename
if not audio_path.exists():
logger.warning(f"Audio file not found: {audio_path}")
continue
# Optional: filter by duration
if self.max_audio_length is not None:
duration = data.get("audio_duration", float("inf"))
if duration > self.max_audio_length:
logger.info(f"Skipping {json_path.stem}: duration {duration:.1f}s > max {self.max_audio_length}s")
continue
samples.append({
"audio_path": str(audio_path),
"json_path": str(json_path),
"data": data,
})
except Exception as e:
logger.warning(f"Error loading {json_path}: {e}")
continue
return samples
def _format_transcription(self, segments: List[Dict], audio_duration: float) -> str:
"""
Format transcription segments into JSON output format.
This matches the expected model output format used in training.
"""
formatted_segments = []
for seg in segments:
formatted_seg = {}
# Add timestamp
formatted_seg["Start"] = round(seg['start'], 2)
formatted_seg["End"] = round(seg['end'], 2)
# Add speaker if available
if "speaker" in seg:
formatted_seg["Speaker"] = seg["speaker"]
# Add content
formatted_seg["Content"] = seg.get("text", "")
formatted_segments.append(formatted_seg)
# Return as compact JSON string (no spaces after separators)
return json.dumps(formatted_segments, ensure_ascii=False, separators=(',', ':'))
def __len__(self) -> int:
return len(self.samples)
def __getitem__(self, idx: int) -> Dict[str, Any]:
"""
Get a single sample for training.
Returns:
Dict with:
- input_ids: Token IDs for input (system + user + assistant prompt)
- labels: Token IDs for labels (-100 for non-predicted tokens)
- acoustic_input_mask: Mask for speech token positions
- speech: Raw audio array
- vae_tok_len: Number of speech tokens
"""
sample = self.samples[idx]
data = sample["data"]
audio_path = sample["audio_path"]
# Prepare context info (customized_context)
context_info = None
if self.use_customized_context and "customized_context" in data:
customized_context = data["customized_context"]
if customized_context:
context_info = "\n".join(customized_context)
# Process audio using the processor's internal method
encoding = self.processor._process_single_audio(
audio_path,
sampling_rate=None,
add_generation_prompt=True,
use_streaming=True,
context_info=context_info,
)
# Get the input tokens (system + user + generation prompt)
input_ids = encoding["input_ids"]
acoustic_input_mask = encoding["acoustic_input_mask"]
speech = encoding["speech"]
vae_tok_len = encoding["vae_tok_len"]
# Format the target transcription
target_text = self._format_transcription(
data["segments"],
data.get("audio_duration", len(speech) / 24000)
)
# Encode target using apply_chat_template to match training format
# This adds the assistant role tokens (e.g., <|im_start|>assistant\n...<|im_end|>)
target_tokens = self.processor.tokenizer.apply_chat_template(
[{"role": "assistant", "content": target_text}],
tokenize=True,
add_generation_prompt=False,
)
# Combine input and target
full_input_ids = input_ids + target_tokens
full_acoustic_mask = acoustic_input_mask + [0] * len(target_tokens)
# Create labels: -100 for input tokens, actual tokens for target
# We mask the input portion so loss is only computed on the response
labels = [-100] * len(input_ids) + target_tokens
return {
"input_ids": full_input_ids,
"labels": labels,
"acoustic_input_mask": full_acoustic_mask,
"speech": speech,
"vae_tok_len": vae_tok_len,
}
def get_lora_config(
r: int = 16,
lora_alpha: int = 32,
lora_dropout: float = 0.05,
target_modules: Optional[List[str]] = None,
) -> LoraConfig:
"""
Create LoRA configuration for VibeVoice ASR model.
We apply LoRA to the language model's attention layers and MLP,
following common practices for LLM fine-tuning.
Args:
r: LoRA rank
lora_alpha: LoRA scaling factor
lora_dropout: Dropout for LoRA layers
target_modules: List of module names to apply LoRA to
Returns:
LoraConfig object
"""
if target_modules is None:
# Target Qwen2 attention and MLP layers
# These are the common targets for language model fine-tuning
target_modules = [
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
]
return LoraConfig(
r=r,
lora_alpha=lora_alpha,
target_modules=target_modules,
lora_dropout=lora_dropout,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
def setup_model_for_training(
model_path: str,
lora_config: LoraConfig,
device: str = "cuda",
dtype: torch.dtype = torch.bfloat16,
gradient_checkpointing: bool = True,
) -> Tuple[nn.Module, VibeVoiceASRProcessor]:
"""
Load and prepare model for LoRA training.
Args:
model_path: Path to pretrained model
lora_config: LoRA configuration
device: Device to use
dtype: Data type for model
gradient_checkpointing: Whether to use gradient checkpointing
Returns:
Tuple of (model, processor)
"""
logger.info(f"Loading model from {model_path}")
# Load processor
processor = VibeVoiceASRProcessor.from_pretrained(
model_path,
language_model_pretrained_name="Qwen/Qwen2.5-7B"
)
# Load model
model = VibeVoiceASRForConditionalGeneration.from_pretrained(
model_path,
dtype=dtype,
device_map=device if device == "auto" else None,
attn_implementation="flash_attention_2",
trust_remote_code=True,
)
if device != "auto":
model = model.to(device)
# Freeze speech tokenizers (we only want to fine-tune the language model)
for name, param in model.named_parameters():
if "acoustic_tokenizer" in name or "semantic_tokenizer" in name:
param.requires_grad = False
logger.debug(f"Frozen: {name}")
# Apply LoRA
logger.info(f"Applying LoRA with config: r={lora_config.r}, alpha={lora_config.lora_alpha}")
model = get_peft_model(model, lora_config)
# Print trainable parameters
model.print_trainable_parameters()
# Enable gradient checkpointing if requested
if gradient_checkpointing:
model.enable_input_require_grads()
model.gradient_checkpointing_enable()
return model, processor
def train(
model_args: ModelArguments,
data_args: DataArguments,
lora_args: LoraArguments,
training_args: TrainingArguments,
gradient_checkpointing: bool = True,
):
"""
Main training function for LoRA fine-tuning.
Args:
model_args: Model configuration arguments
data_args: Data configuration arguments
lora_args: LoRA configuration arguments
training_args: HuggingFace TrainingArguments
gradient_checkpointing: Whether to use gradient checkpointing
"""
# Set seed
torch.manual_seed(training_args.seed)
np.random.seed(training_args.seed)
# Setup LoRA config
lora_config = get_lora_config(
r=lora_args.lora_r,
lora_alpha=lora_args.lora_alpha,
lora_dropout=lora_args.lora_dropout,
)
# Determine device
device = "cuda" if torch.cuda.is_available() else "cpu"
# Load model and processor
dtype = torch.bfloat16 if device != "cpu" else torch.float32
model, processor = setup_model_for_training(
model_path=model_args.model_path,
lora_config=lora_config,
device=device,
dtype=dtype,
gradient_checkpointing=gradient_checkpointing,
)
# Create dataset
train_dataset = VibeVoiceASRDataset(
data_dir=data_args.data_dir,
processor=processor,
max_audio_length=data_args.max_audio_length,
use_customized_context=data_args.use_customized_context,
)
if len(train_dataset) == 0:
logger.error("No training samples found!")
return
# Create data collator
data_collator = VibeVoiceASRDataCollator(
processor=processor,
pad_token_id=processor.pad_id,
)
# Set some sensible defaults for audio training
training_args.dataloader_num_workers = 0 # Audio loading can be tricky with multiprocessing
training_args.remove_unused_columns = False # Keep all columns
# Create trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
data_collator=data_collator,
)
# Train
logger.info("Starting training...")
logger.info(f" Num samples = {len(train_dataset)}")
logger.info(f" Num epochs = {training_args.num_train_epochs}")
logger.info(f" Batch size = {training_args.per_device_train_batch_size}")
logger.info(f" Gradient accumulation steps = {training_args.gradient_accumulation_steps}")
total_steps = len(train_dataset) * int(training_args.num_train_epochs) // (
training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps
)
logger.info(f" Total optimization steps = {total_steps}")
train_result = trainer.train()
# Save final model
logger.info(f"Saving model to {training_args.output_dir}")
trainer.save_model(training_args.output_dir)
# Save training metrics
metrics = train_result.metrics
trainer.log_metrics("train", metrics)
trainer.save_metrics("train", metrics)
# Save processor config
processor.save_pretrained(training_args.output_dir)
logger.info("Training complete!")
return model, processor
def main():
# Use HfArgumentParser to parse all argument dataclasses
parser = HfArgumentParser((ModelArguments, DataArguments, LoraArguments, TrainingArguments))
model_args, data_args, lora_args, training_args = parser.parse_args_into_dataclasses()
# Run training
train(
model_args=model_args,
data_args=data_args,
lora_args=lora_args,
training_args=training_args,
)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
{
"audio_duration": 351.73333333333335,
"audio_path": "0.mp3",
"segments": [
{
"speaker": 0,
"text": "Hey everyone, welcome back to Tea Brew. Im Aidan Host, and, uh, sorry were a tad late today. Travel was a bit wild on the roads and our guest came in from near Meeter Street, so, you know, life happens. But were here now and buzzing to talk property tech. Weve had loads of messages from landlords about spiraling energy costs in HMOs and, at the same time, a bunch of questions on streamlining tenant recruitment. So today were kinda merging both worlds: how a smart heating setup can cut bills, and how a software assistant like Rent Byte can take the grind out of advertising, onboarding, and maintenance reporting. And sitting with me is Sayid Guest, whos, uh, a landlord turned gadget builder—haha—ready to demystify the thermostatic stuff that tenants can actually live with.",
"start": 0.0,
"end": 38.68
},
{
"speaker": 1,
"text": "Thanks, Aidan Host, and, yeah, hi everyone. Im Sayid Guest, and this topic is personal for me. I started out managing a handful of HMOs, and—um—the classic scene was radiators roaring away with nobody home, windows cracked wide open. You pay for the heat and watch it pour right out. Not fun. Theres also that line we cant cross: you cant lock tenants out of controls. So I began tinkering, looking for a way to respect tenant comfort while, uh, controlling the waste. The device I built evolved from a scrappy prototype to a solid system, and the surprising bit was how immediate the savings were. Like, right from the first month, bills were trending down hard, and I thought, wow, landlords need something like this across the board.",
"start": 38.75,
"end": 77.88
},
{
"speaker": 0,
"text": "Exactly. And, actually, on the software side, weve seen a similar DIY-to-pro evolution. Rent Byte started because landlords were fed up with juggling spreadsheets, emails, and random listings. Its, um, designed by landlords with tenants in mind—trust and transparency baked in. With Rent Byte, you can push your property ads out fast, track leads, run checks, and glide folks through the onboarding without the headache. Then, when people move in, it doesnt stop; youve got in-app maintenance reporting, job tracking, and clear timelines, so tenants dont feel, you know, ignored. Tea Brew listeners keep telling us the pain isnt just finding residents, its keeping the whole machine humming. And if you pair that workflow with smart heating controls, youre hitting cost, comfort, and communication all in one go.",
"start": 77.92,
"end": 115.18
},
{
"speaker": 2,
"text": "Hold on, can I jump in? So, um, I manage two HMOs over by Meeter Street, and the energy bills last winter were brutal. Everyone online was shouting tips—weather compensation, schedule tweaks, the whole shebang—but tenants still cranked it up when they felt chilly. We cant, like, lock the thermostat, right? So how does your approach, Sayid Guest, keep the system fair? Tenants get access, but landlords dont get burned—haha, terrible pun. Does it do occupancy sensing, or is it just a timer? And how do you stop the classic boost button from becoming a permanent on?",
"start": 115.19,
"end": 147.56
},
{
"speaker": 1,
"text": "Great questions. So its a combo. Think of it as a comfort-first schedule with protections. You set reasonable heating windows—morning and evening, say—and tenants can press a boost for extra time. But the boost is capped and resets, so it wont, uh, run the boiler all day. For empty rooms and the, you know, window-wide-open scenario, if you add sensors, the system detects rapid drops or no movement and tapers the heat until conditions make sense. Its not about denying warmth; its about stopping waste that literally no one benefits from. With HMOs you need communal logic—landings and kitchens matter—so multi-zone control helps keep spaces balanced. And landlords, like me, get analytics: you can see where energy is leaking and fine-tune settings. When I first tested it, I saw 3050% reductions. Thats not a promise for every building—each setup is its own puzzle—but the pattern has been, um, consistently strong.",
"start": 147.56,
"end": 193.57
},
{
"speaker": 0,
"text": "Yeah, that resonates. I walked into a shared house once—near Meeter Street—and the thermostat was at 29, like sauna levels, with the window propped open. The tenants werent being malicious, they were just coping: drafty room, quick fix, crank the dial. So, as Tea Brew keeps saying, we need systems that, uh, balance tenant agency with sensible guardrails. And the nice thing with Rent Byte is that it keeps the conversation flowing. Tenants can raise an issue through maintenance reporting, and as soon as the ticket is created, the timeline starts. If its a cold spot or a radiator fault, youve got the history at your fingertips. That way, you dont blame behavior when its actually a hardware problem.",
"start": 193.57,
"end": 225.73
},
{
"speaker": 2,
"text": "Okay, so picture this: I advertise a new room, get flooded with inquiries, then Im buried in emails. The software side—like Rent Byte—would pull those leads into a pipeline, right? And if someone mentions the room feels chilly during a viewing, I could, uh, flag that straight away. Heres the kicker: can your device talk to the platform? Like, if theres a temp anomaly or a sensor alert, could it auto-create a maintenance ticket so I dont miss it? And, sorry, Im thinking out loud here, but could Rent Byte show those energy graphs inside the tenant portal without freaking people out—more like, you know, helpful nudges than lecturing?",
"start": 225.74,
"end": 258.74
},
{
"speaker": 1,
"text": "Haha, I love the way you think. Yes, integration is the future. Weve built an API so platforms like Rent Byte can pull summary data—no one needs to drown in charts, just the, um, useful stuff. For example, you can surface a gentle insight: “Heating is already scheduled; boost is available for 30 minutes.” Tenants see options, not rules. If a sensor flags a stuck valve or a window open for ages, Rent Byte can spin up a maintenance ticket, assign it, and track the fix. And because Tea Brew listeners keep asking about transparency, weve found tenants appreciate seeing that theres a fair schedule in place. By the way, thanks again for the invite, Aidan Host; getting this across without jargon is half the battle.",
"start": 258.74,
"end": 292.52
},
{
"speaker": 0,
"text": "Totally. And, uh, weve noticed another benefit: advertising feels cleaner when you can tell prospects, right up front, that the home uses sensible heating with tenant-controlled boosts. It sounds small, but it sets expectations and, you know, avoids future friction. On the admin side, Rent Byte keeps everything documented—from viewing notes to audit trails—so if someone on Meeter Street says their radiators been weird for weeks, you can point to the timeline and fix history quickly. Um, weve had a bunch of landlords message Tea Brew after implementing this kind of setup, saying the combo of software plus smart heating saved money and, honestly, reduced arguments. Thats the vibe we want.",
"start": 292.53,
"end": 323.63
},
{
"speaker": 2,
"text": "Same here. To wrap, uh, Im thinking: start with clear, humane policies, pair them with tech that respects tenant comfort, and keep the communication channel open. Aidan Host, if folks want to try Rent Byte, whats the first step? And, Sayid Guest, for the device, is there like a starter kit guide so, um, non-tech landlords dont panic? Maybe put links under the episode—sorry—under Tea Brew show notes. Ive got two more rooms to fill near Meeter Street, and itd be great to kick this off before the cold snaps hit again.",
"start": 323.64,
"end": 351.73
}
],
"customized_context": [
"Tea Brew",
"Aiden Host",
"Saeed Guest",
"Rent Byte",
"The property is located near Meter Street."
]
}
BIN
View File
Binary file not shown.
+79
View File
@@ -0,0 +1,79 @@
{
"audio_duration": 328.26666666666665,
"audio_path": "1.mp3",
"segments": [
{
"speaker": 0,
"text": "Welcome back to our Youth Month special. Um, before we dive in, Tandi, you ready? Were honoring young folks who, like, you know, shook the ground. The day itself marks those antilanguage policy protests in the 70s—students across the country, campuses, townships—standing up. And today, were asking you all on the WhatsApp line to share who inspired you: a teacher, a cousin, someone you admire. Also, well talk about campaigns like Crown Wrights, because identitys not cosmetic; its, uh, core.",
"start": 0.0,
"end": 33.15
},
{
"speaker": 1,
"text": "Yeah, yeah, Im totally in, haha. Thanks, Leila. So, um, Ive been thinking about how the current youth still carry heavy stuff—joblessness, violence against women, and, heartbreakingly, queer kids being targeted. Its, like, gutting. Yet they keep going. I mean, this is Tandi speaking from the heart here. The person I want to celebrate is Zahra. She stood up at Coyl High when policies tried to tame her natural hair, and she, uh, didnt flinch. She even wrote a childrens story as part of Crown Wrights, to help little ones see their coils as power instead of problem.",
"start": 33.24,
"end": 71.33
},
{
"speaker": 2,
"text": "Wow, okay, that hits hard. Zahra at Coyl High—I remember, like, seeing clips where she just, you know, stood there calmly while adults were telling her to “fix” herself. It gave me chills. Identity isnt some minor detail; its a major, uh, principle—wait, I always mix that with principal, haha. Anyway, the bravery at Crown Wrights events has ripple effects. And Leila, youve talked about how hair, especially coily textures, can be policed as a way to, um, shrink someones confidence.",
"start": 71.52,
"end": 102.9
},
{
"speaker": 0,
"text": "Exactly! And thanks, Tandi, for, like, naming the hard stuff. The way that activist reframed “acceptable” appearance shows young people dont need permission to be whole. When you own your look, you walk into boardrooms you never imagined. You, uh, sit at tables, you speak up. Its the same energy that wins pageants and policies—like, a confidence that shifts rooms. And for everyone listening, send a voice note about your own champion and share stories about reclaiming a “crown.”",
"start": 102.9,
"end": 132.14
},
{
"speaker": 1,
"text": "Hold on—just to paint the scene. At Coyl High, Zahra didnt wait for a senior to intervene; she, um, chose the moment. No hesitation. She knew the risk: future opportunities, social backlash, being labeled “difficult.” And she still stood up. For me, Tandi, that shows the lesson the old protests tried to teach—use your voice now, because silence, like, steals time. And you always say confidence is contagious; when one girl lifts her chin, dozens follow.",
"start": 132.17,
"end": 161.49
},
{
"speaker": 2,
"text": "Yeah, yes—exactly. And, mm, the thing that gets me is the dignity piece. People act like hair is trivial, but for Black girls, policing curls is a way to control presence. That campaign reframes it: from “problem” to “pride.” When that flip happens, you start applying for roles you thought were off-limits, you choose your course, you dont break—uh, brake—just because someone says you dont fit. And, you mentioned queer youth; the intersections matter. That stand speaks beyond curls, into, like, the right to be fully yourself.",
"start": 161.52,
"end": 196.53
},
{
"speaker": 0,
"text": "Speaking of which, Leila, could you share a listener shout-out? We got a bunch of WhatsApps about teachers, neighbors, and, uh, nurses who kept kids afloat. Also theres a note praising the book in the Crown Wrights series—apparently the illustrations made a little one feel seen at school.",
"start": 196.91,
"end": 212.93
},
{
"speaker": 2,
"text": "This is Leila—oh yeah, totally. So, um, theres a message from a parent who says their child used to hide her coils under a cap, and after reading that Crown Wrights story she walked into class with no fear. And another listeners like, “my teacher changed my life,” which—haha—yes. The past protests werent just about language policy; they set a template: organize, insist, repeat. Honestly, Tandi, I feel like honoring Zahra honors every student who refuses to be edited at Coyl High-type schools, you know?",
"start": 213.59,
"end": 245.25
},
{
"speaker": 1,
"text": "Exactly! And, um, can I just say: elders sometimes tell us to wait for leaders to fix things. But the lesson, from then till now, is dont outsource your voice. The moment you speak, the course of the moment changes—like, the crowd pivots from idle to action. And for anyone feeling alone, tag us and the campaign; youll find community fast.",
"start": 245.25,
"end": 266.64
},
{
"speaker": 0,
"text": "Right, and, wow, weve barely scratched the surface. I want to circle back because someone asked if honoring hair is, um, frivolous compared to, say, jobs or safety. My answer: its linked. Coyl High-style rules are part of a bigger system that decides who is welcome. Undo that, and you expand opportunity. And if youre looking for names, Leila suggested a list: local mentors, teacher heroes, youth organizers. That point about resilience—phew—needed.",
"start": 266.64,
"end": 297.57
},
{
"speaker": 2,
"text": "Mhm, and one more thought, then well wrap. Zahra didnt become a symbol because she wanted fame; she became one because she refused to break. That stubborn joy? Its, like, fuel. Im Leila, and Im grateful we still celebrate that day in June as a reminder to keep going. To every listener—send your piece, um, peace—haha—on the line. And thanks, Tandi, for bringing this story, and thanks to campaigns like Crown Wrights for keeping the flame on.",
"start": 297.78,
"end": 328.27
}
],
"customized_context": [
"Thandie",
"Leila",
"Zara",
"The story takes place at Coyle High school.",
"Crown Rites is a campaign for hair rights."
]
}
BIN
View File
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "vibevoice"
version = "1.0.0"
authors = [
{ name="vibevoice team", email="VibeVoice@microsoft.com" },
]
description = "Open-Source Frontier Voice AI."
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
# "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"torch",
"transformers>=4.51.3,<5.0.0",
"accelerate",
"llvmlite>=0.40.0",
"numba>=0.57.0",
"diffusers",
"tqdm",
"numpy",
"scipy",
"librosa",
"ml-collections",
"absl-py",
"gradio",
"av",
"aiortc",
"uvicorn[standard]",
"fastapi",
"pydub",
"requests",
]
[project.optional-dependencies]
streamingtts = [
"transformers==4.51.3",
]
[project.entry-points."vllm.general_plugins"]
vibevoice = "vllm_plugin:register_vibevoice"
[project.urls]
"Homepage" = "https://github.com/microsoft/VibeVoice"
"Bug Tracker" = "https://github.com/microsoft/VibeVoice/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["vibevoice*", "vllm_plugin*"]
+16
View File
@@ -0,0 +1,16 @@
# vibevoice/__init__.py
from vibevoice.modular import (
VibeVoiceStreamingForConditionalGenerationInference,
VibeVoiceStreamingConfig,
)
from vibevoice.processor import (
VibeVoiceStreamingProcessor,
VibeVoiceTokenizerProcessor,
)
__all__ = [
"VibeVoiceStreamingForConditionalGenerationInference",
"VibeVoiceStreamingConfig",
"VibeVoiceStreamingProcessor",
"VibeVoiceTokenizerProcessor",
]
+112
View File
@@ -0,0 +1,112 @@
{
"_attn_implementation_autoset": true,
"acoustic_vae_dim": 64,
"acoustic_tokenizer_config": {
"causal": true,
"channels": 1,
"conv_bias": true,
"conv_norm": "none",
"corpus_normalize": 0.0,
"decoder_depths": null,
"decoder_n_filters": 32,
"decoder_ratios": [
8,
5,
5,
4,
2,
2
],
"disable_last_norm": true,
"encoder_depths": "3-3-3-3-3-3-8",
"encoder_n_filters": 32,
"encoder_ratios": [
8,
5,
5,
4,
2,
2
],
"fix_std": 0.5,
"layer_scale_init_value": 1e-06,
"layernorm": "RMSNorm",
"layernorm_elementwise_affine": true,
"layernorm_eps": 1e-05,
"mixer_layer": "depthwise_conv",
"model_type": "vibepod_acoustic_tokenizer",
"pad_mode": "constant",
"std_dist_type": "gaussian",
"vae_dim": 64,
"weight_init_value": 0.01
},
"decoder_config": {
"attention_dropout": 0.0,
"hidden_act": "silu",
"hidden_size": 1536,
"initializer_range": 0.02,
"intermediate_size": 8960,
"max_position_embeddings": 65536,
"max_window_layers": 28,
"model_type": "qwen2",
"num_attention_heads": 12,
"num_hidden_layers": 28,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-06,
"rope_scaling": null,
"rope_theta": 1000000.0,
"sliding_window": null,
"tie_word_embeddings": true,
"torch_dtype": "bfloat16",
"use_cache": true,
"use_sliding_window": false,
"vocab_size": 151936
},
"diffusion_head_config": {
"ddpm_batch_mul": 4,
"ddpm_beta_schedule": "cosine",
"ddpm_num_inference_steps": 20,
"ddpm_num_steps": 1000,
"diffusion_type": "ddpm",
"head_ffn_ratio": 3.0,
"head_layers": 4,
"hidden_size": 1536,
"latent_size": 64,
"model_type": "vibepod_diffusion_head",
"prediction_type": "v_prediction",
"rms_norm_eps": 1e-05,
"speech_vae_dim": 64
},
"model_type": "vibepod",
"semantic_tokenizer_config": {
"causal": true,
"channels": 1,
"conv_bias": true,
"conv_norm": "none",
"corpus_normalize": 0.0,
"disable_last_norm": true,
"encoder_depths": "3-3-3-3-3-3-8",
"encoder_n_filters": 32,
"encoder_ratios": [
8,
5,
5,
4,
2,
2
],
"fix_std": 0,
"layer_scale_init_value": 1e-06,
"layernorm": "RMSNorm",
"layernorm_elementwise_affine": true,
"layernorm_eps": 1e-05,
"mixer_layer": "depthwise_conv",
"model_type": "vibepod_semantic_tokenizer",
"pad_mode": "constant",
"std_dist_type": "none",
"vae_dim": 128,
"weight_init_value": 0.01
},
"semantic_vae_dim": 128,
"torch_dtype": "bfloat16"
}
+113
View File
@@ -0,0 +1,113 @@
{
"_attn_implementation_autoset": true,
"acoustic_vae_dim": 64,
"acoustic_tokenizer_config": {
"causal": true,
"channels": 1,
"conv_bias": true,
"conv_norm": "none",
"corpus_normalize": 0.0,
"decoder_depths": null,
"decoder_n_filters": 32,
"decoder_ratios": [
8,
5,
5,
4,
2,
2
],
"disable_last_norm": true,
"encoder_depths": "3-3-3-3-3-3-8",
"encoder_n_filters": 32,
"encoder_ratios": [
8,
5,
5,
4,
2,
2
],
"fix_std": 0.5,
"layer_scale_init_value": 1e-06,
"layernorm": "RMSNorm",
"layernorm_elementwise_affine": true,
"layernorm_eps": 1e-05,
"mixer_layer": "depthwise_conv",
"model_type": "vibepod_acoustic_tokenizer",
"pad_mode": "constant",
"std_dist_type": "gaussian",
"vae_dim": 64,
"weight_init_value": 0.01
},
"decoder_config": {
"attention_dropout": 0.0,
"hidden_act": "silu",
"hidden_size": 3584,
"initializer_range": 0.02,
"intermediate_size": 18944,
"max_position_embeddings": 32768,
"max_window_layers": 28,
"model_type": "qwen2",
"num_attention_heads": 28,
"num_hidden_layers": 28,
"num_key_value_heads": 4,
"rms_norm_eps": 1e-06,
"rope_theta": 1000000.0,
"sliding_window": null,
"tie_word_embeddings": false,
"torch_dtype": "bfloat16",
"transformers_version": "4.40.1",
"use_cache": true,
"use_mrope": false,
"use_sliding_window": false,
"vocab_size": 152064
},
"diffusion_head_config": {
"ddpm_batch_mul": 4,
"ddpm_beta_schedule": "cosine",
"ddpm_num_inference_steps": 20,
"ddpm_num_steps": 1000,
"diffusion_type": "ddpm",
"head_ffn_ratio": 3.0,
"head_layers": 4,
"hidden_size": 3584,
"latent_size": 64,
"model_type": "vibepod_diffusion_head",
"prediction_type": "v_prediction",
"rms_norm_eps": 1e-05,
"speech_vae_dim": 64
},
"model_type": "vibepod",
"semantic_tokenizer_config": {
"causal": true,
"channels": 1,
"conv_bias": true,
"conv_norm": "none",
"corpus_normalize": 0.0,
"disable_last_norm": true,
"encoder_depths": "3-3-3-3-3-3-8",
"encoder_n_filters": 32,
"encoder_ratios": [
8,
5,
5,
4,
2,
2
],
"fix_std": 0,
"layer_scale_init_value": 1e-06,
"layernorm": "RMSNorm",
"layernorm_elementwise_affine": true,
"layernorm_eps": 1e-05,
"mixer_layer": "depthwise_conv",
"model_type": "vibepod_semantic_tokenizer",
"pad_mode": "constant",
"std_dist_type": "none",
"vae_dim": 128,
"weight_init_value": 0.01
},
"semantic_vae_dim": 128,
"torch_dtype": "bfloat16"
}
+14
View File
@@ -0,0 +1,14 @@
# vibevoice/modular/__init__.py
from .modeling_vibevoice_streaming_inference import VibeVoiceStreamingForConditionalGenerationInference
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
from .modeling_vibevoice_streaming import VibeVoiceStreamingModel, VibeVoiceStreamingPreTrainedModel
from .streamer import AudioStreamer, AsyncAudioStreamer
__all__ = [
"VibeVoiceStreamingForConditionalGenerationInference",
"VibeVoiceStreamingConfig",
"VibeVoiceStreamingModel",
"VibeVoiceStreamingPreTrainedModel",
"AudioStreamer",
"AsyncAudioStreamer",
]
@@ -0,0 +1,406 @@
""" VibeVoice_AcousticTokenizer model configuration"""
from typing import Dict, List, Optional, Tuple
import torch
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import logging
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
logger = logging.get_logger(__name__)
def _convert_dtype_to_string(config_dict: dict) -> dict:
"""
Convert torch.dtype objects to their string representation for JSON serialization.
This fixes the "Object of type dtype is not JSON serializable" error that occurs
when transformers tries to log/serialize the config with torch_dtype as a torch.dtype object.
See: https://github.com/microsoft/VibeVoice/issues/199
"""
if "torch_dtype" in config_dict and config_dict["torch_dtype"] is not None:
dtype = config_dict["torch_dtype"]
if isinstance(dtype, torch.dtype):
# Convert torch.dtype to string (e.g., torch.bfloat16 -> "bfloat16")
config_dict["torch_dtype"] = str(dtype).replace("torch.", "")
return config_dict
class VibeVoiceAcousticTokenizerConfig(PretrainedConfig):
model_type = "vibevoice_acoustic_tokenizer"
def __init__(
self,
channels: int = 1,
corpus_normalize: float = 0.0,
causal: bool = True,
vae_dim: int = 64,
fix_std: float = 0.5,
std_dist_type: str = 'gaussian',
# common
mixer_layer: str = 'depthwise_conv',
conv_norm: str = 'none',
pad_mode: str = 'constant',
disable_last_norm: bool = True,
layernorm: str = 'RMSNorm',
layernorm_eps: float = 1e-5,
layernorm_elementwise_affine: bool = True,
conv_bias: bool = True,
layer_scale_init_value: float = 1e-6,
weight_init_value: float = 1e-2,
# encoder specific
encoder_n_filters: int = 32,
encoder_ratios: Optional[List[int]] = [8,5,5,4,2,2],
encoder_depths: str = "3-3-3-3-3-3-8",
# decoder specific
decoder_n_filters: int = 32,
decoder_ratios: Optional[List[int]] = None, # if None, same as encoder
decoder_depths: Optional[str] = None,
**kwargs
):
super().__init__(**kwargs)
self.channels = channels
self.corpus_normalize = corpus_normalize
self.causal = causal
self.vae_dim = vae_dim
self.fix_std = fix_std
self.std_dist_type = std_dist_type
# common parameters
self.conv_norm = conv_norm
self.pad_mode = pad_mode
self.layernorm_eps = layernorm_eps
self.disable_last_norm = disable_last_norm
self.layernorm = layernorm
self.layernorm_elementwise_affine = layernorm_elementwise_affine
self.conv_bias = conv_bias
self.layer_scale_init_value = layer_scale_init_value
self.weight_init_value = weight_init_value
self.mixer_layer = mixer_layer
# encoder specific parameters
self.encoder_n_filters = encoder_n_filters
self.encoder_ratios = encoder_ratios
self.encoder_depths = encoder_depths
# decoder specific parameters
self.decoder_ratios = decoder_ratios if decoder_ratios is not None else encoder_ratios
self.decoder_n_filters = decoder_n_filters
self.decoder_depths = decoder_depths
class VibeVoiceSemanticTokenizerConfig(PretrainedConfig):
model_type = "vibevoice_semantic_tokenizer"
def __init__(
self,
channels: int = 1,
corpus_normalize: float = 0.0,
causal: bool = True,
vae_dim: int = 64,
fix_std: float = 0,
std_dist_type: str = 'none',
# common
mixer_layer: str = 'depthwise_conv',
conv_norm: str = 'none',
pad_mode: str = 'constant',
disable_last_norm: bool = True,
layernorm: str = 'RMSNorm',
layernorm_eps: float = 1e-5,
layernorm_elementwise_affine: bool = True,
conv_bias: bool = True,
layer_scale_init_value: float = 1e-6,
weight_init_value: float = 1e-2,
# encoder specific
encoder_n_filters: int = 32,
encoder_ratios: Optional[List[int]] = [8,5,5,4,2,2],
encoder_depths: str = "3-3-3-3-3-3-8",
**kwargs
):
super().__init__(**kwargs)
self.channels = channels
self.corpus_normalize = corpus_normalize
self.causal = causal
self.vae_dim = vae_dim
self.fix_std = fix_std
self.std_dist_type = std_dist_type
# common parameters
self.conv_norm = conv_norm
self.pad_mode = pad_mode
self.layernorm_eps = layernorm_eps
self.disable_last_norm = disable_last_norm
self.layernorm = layernorm
self.layernorm_elementwise_affine = layernorm_elementwise_affine
self.conv_bias = conv_bias
self.layer_scale_init_value = layer_scale_init_value
self.weight_init_value = weight_init_value
self.mixer_layer = mixer_layer
# encoder specific parameters
self.encoder_n_filters = encoder_n_filters
self.encoder_ratios = encoder_ratios
self.encoder_depths = encoder_depths
class VibeVoiceDiffusionHeadConfig(PretrainedConfig):
model_type = "vibevoice_diffusion_head"
def __init__(
self,
hidden_size=768,
head_layers=4,
head_ffn_ratio=3.0,
rms_norm_eps=1e-5,
latent_size=64,
speech_vae_dim=None,
prediction_type="v_prediction",
diffusion_type="ddpm",
ddpm_num_steps=1000,
ddpm_num_inference_steps=20,
ddpm_beta_schedule="cosine",
ddpm_batch_mul=4,
**kwargs
):
self.hidden_size = hidden_size
self.head_layers = head_layers
self.head_ffn_ratio = head_ffn_ratio
self.rms_norm_eps = rms_norm_eps
self.latent_size = latent_size
self.speech_vae_dim = speech_vae_dim
self.prediction_type = prediction_type
self.diffusion_type = diffusion_type
self.ddpm_num_steps = ddpm_num_steps
self.ddpm_num_inference_steps = ddpm_num_inference_steps
self.ddpm_beta_schedule = ddpm_beta_schedule
self.ddpm_batch_mul = ddpm_batch_mul
super().__init__(**kwargs)
class VibeVoiceConfig(PretrainedConfig):
model_type = "vibevoice"
is_composition = True
sub_configs = {
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
"semantic_tokenizer_config": VibeVoiceSemanticTokenizerConfig,
"decoder_config": Qwen2Config,
"diffusion_head_config": VibeVoiceDiffusionHeadConfig,
}
# keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Qwen2`
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
def __init__(
self,
acoustic_tokenizer_config=None,
semantic_tokenizer_config=None,
decoder_config=None,
diffusion_head_config=None,
**kwargs
):
# kwargs["_attn_implementation"] = "flash_attention_2"
kwargs["_attn_implementation_autoset"] = False
if acoustic_tokenizer_config is None:
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
elif isinstance(acoustic_tokenizer_config, dict):
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
# If an instance of the config class is provided
self.acoustic_tokenizer_config = acoustic_tokenizer_config
if semantic_tokenizer_config is None:
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"]()
elif isinstance(semantic_tokenizer_config, dict):
semantic_tokenizer_config["model_type"] = "vibevoice_semantic_tokenizer"
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"](**semantic_tokenizer_config)
elif isinstance(semantic_tokenizer_config, VibeVoiceSemanticTokenizerConfig):
# If an instance of the config class is provided
self.semantic_tokenizer_config = semantic_tokenizer_config
if decoder_config is None:
self.decoder_config = self.sub_configs["decoder_config"]()
elif isinstance(decoder_config, dict):
# If a dictionary is provided, instantiate the config class with it
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
if decoder_config.get("model_type", '') == "qwen2":
self.decoder_config = Qwen2Config(**decoder_config)
else:
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
elif isinstance(decoder_config, (Qwen2Config,)):
# If an instance of the config class is provided
self.decoder_config = decoder_config
if diffusion_head_config is None:
self.diffusion_head_config = self.sub_configs["diffusion_head_config"]()
elif isinstance(diffusion_head_config, dict):
diffusion_head_config["model_type"] = "vibevoice_diffusion_head"
self.diffusion_head_config = self.sub_configs["diffusion_head_config"](**diffusion_head_config)
elif isinstance(diffusion_head_config, VibeVoiceDiffusionHeadConfig):
# If an instance of the config class is provided
self.diffusion_head_config = diffusion_head_config
# other parameters
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
self.semantic_vae_dim = getattr(self.semantic_tokenizer_config, 'vae_dim', 128)
super().__init__(**kwargs)
def get_text_config(self, decoder=False):
"""
Returns the text config for this model.
vLLM uses this method to get the text configuration from multimodal models.
This allows vLLM to correctly determine hidden_size, num_attention_heads,
and other properties needed for memory profiling and model execution.
For VibeVoice, the "text config" is the decoder_config (Qwen2Config).
Args:
decoder: If True, return the decoder config (for encoder-decoder models).
For VibeVoice, this is always the decoder_config.
Returns:
The decoder configuration (Qwen2Config) which contains hidden_size, etc.
"""
return self.decoder_config
def to_dict(self):
"""
Override to_dict to handle torch.dtype serialization.
Fixes: https://github.com/microsoft/VibeVoice/issues/199
"""
output = super().to_dict()
return _convert_dtype_to_string(output)
class VibeVoiceASRConfig(PretrainedConfig):
model_type = "vibevoice"
is_composition = True
sub_configs = {
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
"semantic_tokenizer_config": VibeVoiceSemanticTokenizerConfig,
"decoder_config": Qwen2Config,
}
# keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Qwen2`
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
def __init__(
self,
acoustic_tokenizer_config=None,
semantic_tokenizer_config=None,
decoder_config=None,
**kwargs
):
# kwargs["_attn_implementation"] = "flash_attention_2"
kwargs["_attn_implementation_autoset"] = False
if acoustic_tokenizer_config is None:
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
elif isinstance(acoustic_tokenizer_config, dict):
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
# If an instance of the config class is provided
self.acoustic_tokenizer_config = acoustic_tokenizer_config
if semantic_tokenizer_config is None:
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"]()
elif isinstance(semantic_tokenizer_config, dict):
semantic_tokenizer_config["model_type"] = "vibevoice_semantic_tokenizer"
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"](**semantic_tokenizer_config)
elif isinstance(semantic_tokenizer_config, VibeVoiceSemanticTokenizerConfig):
# If an instance of the config class is provided
self.semantic_tokenizer_config = semantic_tokenizer_config
if decoder_config is None:
self.decoder_config = self.sub_configs["decoder_config"]()
elif isinstance(decoder_config, dict):
# If a dictionary is provided, instantiate the config class with it
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
if decoder_config.get("model_type", '') == "qwen2":
self.decoder_config = Qwen2Config(**decoder_config)
else:
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
elif isinstance(decoder_config, Qwen2Config):
# If an instance of the config class is provided
self.decoder_config = decoder_config
# other parameters
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
self.semantic_vae_dim = getattr(self.semantic_tokenizer_config, 'vae_dim', 128)
super().__init__(**kwargs)
def to_dict(self):
"""
Override to_dict to handle torch.dtype serialization.
Fixes: https://github.com/microsoft/VibeVoice/issues/199
"""
output = super().to_dict()
return _convert_dtype_to_string(output)
def get_text_config(self, decoder: bool = False):
"""Return the text (decoder) config for generation."""
return self.decoder_config
@property
def vocab_size(self):
"""Return vocab_size from decoder config for generation compatibility."""
return self.decoder_config.vocab_size
@property
def num_attention_heads(self):
"""Return num_attention_heads from decoder config for Ulysses SP compatibility."""
return self.decoder_config.num_attention_heads
@property
def num_key_value_heads(self):
"""Return num_key_value_heads from decoder config for Ulysses SP compatibility."""
return self.decoder_config.num_key_value_heads
@property
def hidden_size(self):
"""Return hidden_size from decoder config for model compatibility."""
return self.decoder_config.hidden_size
@property
def num_hidden_layers(self):
"""Return num_hidden_layers from decoder config for Ulysses SP compatibility."""
return self.decoder_config.num_hidden_layers
@property
def head_dim(self):
"""Return head_dim from decoder config for Ulysses SP compatibility."""
return getattr(self.decoder_config, 'head_dim', self.hidden_size // self.num_attention_heads)
__all__ = [
"VibeVoiceAcousticTokenizerConfig",
"VibeVoiceSemanticTokenizerConfig",
"VibeVoiceDiffusionHeadConfig",
"VibeVoiceConfig",
"VibeVoiceASRConfig"
]
@@ -0,0 +1,104 @@
""" VibeVoice Streaming model configuration"""
import torch
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import logging
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
from .configuration_vibevoice import VibeVoiceAcousticTokenizerConfig, VibeVoiceDiffusionHeadConfig, _convert_dtype_to_string
logger = logging.get_logger(__name__)
class VibeVoiceStreamingConfig(PretrainedConfig):
model_type = "vibevoice_streaming"
is_composition = True
sub_configs = {
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
"decoder_config": Qwen2Config,
"diffusion_head_config": VibeVoiceDiffusionHeadConfig,
}
# keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Qwen2`
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
def __init__(
self,
acoustic_tokenizer_config=None,
decoder_config=None,
diffusion_head_config=None,
tts_backbone_num_hidden_layers=20,
**kwargs
):
# kwargs["_attn_implementation"] = "flash_attention_2"
kwargs["_attn_implementation_autoset"] = False
if acoustic_tokenizer_config is None:
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
elif isinstance(acoustic_tokenizer_config, dict):
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
# If an instance of the config class is provided
self.acoustic_tokenizer_config = acoustic_tokenizer_config
if decoder_config is None:
self.decoder_config = self.sub_configs["decoder_config"]()
elif isinstance(decoder_config, dict):
# If a dictionary is provided, instantiate the config class with it
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
if decoder_config.get("model_type", '') == "qwen2":
self.decoder_config = Qwen2Config(**decoder_config)
else:
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
elif isinstance(decoder_config, (Qwen2Config,)):
# If an instance of the config class is provided
self.decoder_config = decoder_config
if diffusion_head_config is None:
self.diffusion_head_config = self.sub_configs["diffusion_head_config"]()
elif isinstance(diffusion_head_config, dict):
diffusion_head_config["model_type"] = "vibevoice_diffusion_head"
self.diffusion_head_config = self.sub_configs["diffusion_head_config"](**diffusion_head_config)
elif isinstance(diffusion_head_config, VibeVoiceDiffusionHeadConfig):
# If an instance of the config class is provided
self.diffusion_head_config = diffusion_head_config
# other parameters
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
# The decoder of the model is divided into two components. The lower Transformer layers are only used for encoding text, while the upper Transformer layers are used for encoding text and generating speech. `tts_backbone_num_hidden_layers` indicates the number of upper layers used for TTS.
self.tts_backbone_num_hidden_layers = tts_backbone_num_hidden_layers
super().__init__(**kwargs)
def get_text_config(self, decoder=False):
"""Returns the decoder config (required for transformers >= 4.57 cache compatibility)."""
return self.decoder_config
@property
def num_hidden_layers(self):
"""Proxy to decoder_config.num_hidden_layers (required for transformers >= 4.57)."""
return self.decoder_config.num_hidden_layers
def to_dict(self):
"""
Override to_dict to handle torch.dtype serialization.
Fixes: https://github.com/microsoft/VibeVoice/issues/199
"""
output = super().to_dict()
return _convert_dtype_to_string(output)
__all__ = [
"VibeVoiceStreamingConfig"
]
+496
View File
@@ -0,0 +1,496 @@
# copied from https://github.com/vibevoice-community/VibeVoice/blob/main/vibevoice/modular/modeling_vibevoice.py
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union, Callable
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.distributed as dist
from transformers.models.auto import AutoModel, AutoModelForCausalLM
from transformers.activations import ACT2FN
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast, ModelOutput
from transformers.models.llama.modeling_llama import LlamaRMSNorm
from transformers import modeling_utils
from transformers.modeling_utils import PreTrainedModel
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
from transformers.utils import logging
from .modular_vibevoice_tokenizer import VibeVoiceTokenizerStreamingCache, VibeVoiceAcousticTokenizerModel, VibeVoiceSemanticTokenizerModel
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
from .configuration_vibevoice import VibeVoiceConfig
logger = logging.get_logger(__name__)
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
@dataclass
class VibeVoiceCausalLMOutputWithPast(ModelOutput):
loss: Optional[torch.FloatTensor] = None
diffusion_loss: Optional[torch.FloatTensor] = None
speech_token_num: Optional[int] = None
logits: torch.FloatTensor = None
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None
hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
@dataclass
class VibeVoiceGenerationOutput(ModelOutput):
"""
Output type for VibeVoice generation.
Args:
sequences (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
The generated sequences.
speech_outputs (`List[torch.FloatTensor]`, *optional*):
List of generated speech waveforms or latents for each speech segment.
"""
sequences: torch.LongTensor = None
speech_outputs: Optional[List[torch.FloatTensor]] = None
class SpeechConnector(nn.Module):
def __init__(self, input_dim, output_dim):
super().__init__()
self.fc1 = nn.Linear(input_dim, output_dim)
self.norm = LlamaRMSNorm(output_dim, eps=1e-6)
self.fc2 = nn.Linear(output_dim, output_dim)
def forward(self, features, **kwargs):
x = self.fc1(features)
x = self.norm(x)
x = self.fc2(x)
return x
# @auto_docstring
class VibeVoicePreTrainedModel(PreTrainedModel):
config_class = VibeVoiceConfig
base_model_prefix = "model"
supports_gradient_checkpointing = True
_skip_keys_device_placement = "past_key_values"
_supports_cache_class = True
_supports_flash_attn_2 = True
_supports_sdpa = True
_supports_quantized_cache = True
_supports_static_cache = True
_supports_attention_backend = True
def _init_weights(self, module):
if isinstance(module, VibeVoiceDiffusionHead):
module.initialize_weights()
return
# Use the language model's initializer_range if available
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
std = self.config.language_model_config.initializer_range
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
std = self.config.decoder_config.initializer_range
else:
std = 0.02 # Default value
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.LayerNorm):
module.weight.data.fill_(1.0)
module.bias.data.zero_()
# @auto_docstring
class VibeVoiceModel(VibeVoicePreTrainedModel):
def __init__(self, config):
super().__init__(config)
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
if isinstance(config.torch_dtype, str):
dtype = getattr(torch, config.torch_dtype)
else:
dtype = config.torch_dtype
else:
dtype = torch.float32
# Initialize Qwen2 model for language modeling
lm_config = config.decoder_config
self.language_model = AutoModel.from_config(lm_config)
# Initialize speech components if needed
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
self.semantic_tokenizer = AutoModel.from_config(config.semantic_tokenizer_config).to(dtype)
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
self.semantic_connector = SpeechConnector(config.semantic_vae_dim, lm_config.hidden_size).to(dtype)
# Register scaling factors as buffers - use 1D tensors for FSDP compatibility
self.register_buffer('speech_scaling_factor', torch.tensor(float('nan')))
self.register_buffer('speech_bias_factor', torch.tensor(float('nan')))
# Initialize prediction head for speech generation
self.prediction_head = AutoModel.from_config(config.diffusion_head_config).to(dtype)
# Initialize noise scheduler
self.noise_scheduler = DPMSolverMultistepScheduler(
num_train_timesteps=config.diffusion_head_config.ddpm_num_steps,
beta_schedule=config.diffusion_head_config.ddpm_beta_schedule,
prediction_type=config.diffusion_head_config.prediction_type
)
def get_input_embeddings(self):
if hasattr(self.language_model, 'embed_tokens'):
# If the language model has an embed_tokens attribute, return it
return self.language_model.embed_tokens
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
if attr.orig_name == 'embed_tokens.weight':
return getattr(self.language_model, name)
assert False, 'should not arrive here'
def set_input_embeddings(self, value):
self.language_model.embed_tokens = value
def set_speech_tokenizers(self, acoustic_tokenizer=None, semantic_tokenizer=None):
"""Set the speech tokenizers used for encoding and decoding speech."""
self.acoustic_tokenizer = acoustic_tokenizer
self.semantic_tokenizer = semantic_tokenizer
# Reset the encoder to evaluation mode
if self.acoustic_tokenizer is not None:
self.acoustic_tokenizer.eval()
if self.semantic_tokenizer is not None:
self.semantic_tokenizer.eval()
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
**kwargs,
) -> Union[Tuple, BaseModelOutputWithPast]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Forward through language model
outputs = self.language_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
**kwargs,
)
if not return_dict:
return outputs
return BaseModelOutputWithPast(
last_hidden_state=outputs.last_hidden_state,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
class VibeVoiceForConditionalGeneration(VibeVoicePreTrainedModel):
_tied_weights_keys = ["lm_head.weight"]
_tp_plan = {"lm_head": "colwise_rep"}
def __init__(self, config):
super().__init__(config)
self.model = VibeVoiceModel(config)
self.vocab_size = config.decoder_config.vocab_size
self.lm_head = nn.Linear(config.decoder_config.hidden_size, self.vocab_size, bias=False)
self.post_init()
def get_input_embeddings(self):
return self.model.get_input_embeddings()
def set_input_embeddings(self, value):
self.model.set_input_embeddings(value)
def get_output_embeddings(self):
return self.lm_head
def set_decoder(self, decoder):
self.model.language_model = decoder
def get_decoder(self):
return self.model.language_model
def tie_weights(self):
"""
Tie the weights between the input embeddings and the output embeddings.
"""
if getattr(self.config.decoder_config, 'tie_word_embeddings', False):
# The standard PreTrainedModel method will handle the tying.
# It typically does a simple parameter object assignment, which is
# CORRECT to do BEFORE FSDP wraps the model.
output_embeddings = self.get_output_embeddings()
input_embeddings = self.get_input_embeddings()
if hasattr(input_embeddings, 'weight'):
output_embeddings.weight = input_embeddings.weight
else:
# maybe returned input_embeddings a tensor directly
output_embeddings.weight = input_embeddings
if getattr(output_embeddings, "bias", None) is not None:
output_embeddings.bias.data = nn.functional.pad(
output_embeddings.bias.data,
(0, output_embeddings.weight.shape[0] - output_embeddings.bias.shape[0]),
"constant",
0,
)
print("Tied input and output embeddings using standard assignment.")
else:
print("tie_word_embeddings is False, not tying weights.")
# Also, ensure set_output_embeddings is safe, though your implementation looks okay.
# The key is to avoid calling it after accelerator.prepare().
def set_output_embeddings(self, new_embeddings):
# Your current implementation using data.copy_ is good practice,
# but the best way is to not call this after prepare().
self.lm_head = new_embeddings
def forward_speech_features(
self,
speech_tensors=None,
speech_masks=None,
speech_type="audio",
return_unmask=False
):
if speech_tensors is None:
# Use config to get vae_dim instead of non-existent self.args
vae_dim = self.config.acoustic_tokenizer_config.vae_dim
audio_features = torch.zeros(1, 1, vae_dim).to(self.get_input_embeddings().weight)
connect_features = self.model.acoustic_connector(audio_features)
return audio_features, connect_features
else:
with torch.no_grad():
if speech_type == "audio":
with torch.no_grad():
frames = self.model.acoustic_tokenizer.encode(speech_tensors.unsqueeze(1))[0][0]
audio_tokens = frames.sample(self.model.acoustic_tokenizer.std_dist_type)[0]
elif speech_type == "vae":
# Use config to get vae_dim instead of non-existent self.args
vae_dim = self.config.acoustic_tokenizer_config.vae_dim
speech_mode = speech_tensors.reshape(speech_tensors.size(0), -1, vae_dim)
# gaussian sample from the speech_mode
batch_size = speech_mode.size(0)
value = self.model.acoustic_tokenizer.fix_std / 0.8
std = torch.randn(batch_size, dtype=speech_mode.dtype, device=speech_mode.device) * value
std = std.view(-1, *[1] * (speech_mode.dim() - 1))
audio_tokens = speech_mode + std * torch.randn(speech_mode.shape).to(speech_mode)
else:
raise NotImplementedError(f"Speech type {speech_type} not implemented")
if torch.isnan(self.model.speech_scaling_factor) or torch.isnan(self.model.speech_bias_factor):
scaling_factor = 1. / audio_tokens[speech_masks].flatten().std()
bias_factor = -audio_tokens[speech_masks].flatten().mean()
# Only use distributed operations if the process group is initialized
if dist.is_available() and dist.is_initialized():
dist.all_reduce(scaling_factor, op=dist.ReduceOp.SUM)
dist.all_reduce(bias_factor, op=dist.ReduceOp.SUM)
world_size = dist.get_world_size()
self.model.speech_scaling_factor.copy_(scaling_factor / world_size)
self.model.speech_bias_factor.copy_(bias_factor / world_size)
print(f"Speech scaling factor (distributed): {self.model.speech_scaling_factor}, bias factor: {self.model.speech_bias_factor}", flush=True)
else:
# Single process case
self.model.speech_scaling_factor.copy_(scaling_factor)
self.model.speech_bias_factor.copy_(bias_factor)
print(f"Speech scaling factor (single process): {self.model.speech_scaling_factor}, bias factor: {self.model.speech_bias_factor}", flush=True)
audio_features = (audio_tokens + self.model.speech_bias_factor) * self.model.speech_scaling_factor
connect_features = self.model.acoustic_connector(audio_features)
if return_unmask:
return audio_features, connect_features
return audio_features[speech_masks], connect_features[speech_masks]
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[torch.FloatTensor]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = False,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
# New arguments for speech processing and loss calculation
speech_tensors: Optional[torch.FloatTensor] = None,
speech_masks: Optional[torch.BoolTensor] = None,
speeches_loss_input: Optional[torch.FloatTensor] = None,
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
acoustic_input_mask: Optional[torch.BoolTensor] = None,
acoustic_loss_mask: Optional[torch.BoolTensor] = None,
ddpm_batch_mul: int = 1,
**kwargs: Optional[Dict[str, Union[torch.Tensor, str]]],
) -> Union[Tuple, VibeVoiceCausalLMOutputWithPast]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
x = self.get_input_embeddings()(input_ids)
semantic_speech_all_connect_features = self.model.semantic_connector(speech_semantic_tensors)
if speeches_loss_input is not None:
# only part audio need diffuse
speech_all_features, speech_all_connect_features = self.forward_speech_features(
speech_tensors=speech_tensors.type_as(x) if speech_tensors is not None else None,
speech_masks=speech_masks,
speech_type=kwargs.get("speech_type", "audio"),
return_unmask=True
)
if speech_tensors is not None:
if semantic_speech_all_connect_features is not None:
x[acoustic_input_mask] = (
speech_all_connect_features[speech_masks]
+ semantic_speech_all_connect_features[speech_masks]
)
else:
x[acoustic_input_mask] = speech_all_connect_features[speech_masks]
# Select only the target segments' latents for diffusion loss.
# Both masks are [num_segments, max_latent_len]; using 2D mask on [B,T,D] selects [N_true, D].
target_latent_mask = speeches_loss_input & speech_masks
speech_features = speech_all_features[target_latent_mask]
speech_connect_features = speech_all_connect_features[target_latent_mask]
else:
speech_features, speech_connect_features = self.forward_speech_features(
speech_tensors=speech_tensors.type_as(x) if speech_tensors is not None else None,
speech_masks=speech_masks,
speech_type=kwargs.get("speech_type", "audio"),
)
if speech_tensors is not None:
x[acoustic_input_mask] = speech_connect_features
outputs = self.model(
input_ids=None,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=x,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=False,
return_dict=return_dict,
cache_position=cache_position,
)
hidden_states = outputs.last_hidden_state
logits = self.lm_head(hidden_states)
# logits = logits.float()
loss = None
if labels is not None:
# The custom CE loss with masking is calculated in the training script.
# We leave the standard loss calculation here as None.
pass
# --- Diffusion Loss Calculation ---
diffusion_loss = None
# This block is executed only if we are in a context that involves speech.
if speech_tensors is not None and acoustic_loss_mask.sum().item() > 0:
condition_features = hidden_states[acoustic_loss_mask]
speech_len, latent_size = speech_features.shape
noise = torch.randn(
(speech_len * ddpm_batch_mul, latent_size),
device=hidden_states.device,
dtype=hidden_states.dtype
)
timesteps = torch.multinomial(
torch.ones(self.config.diffusion_head_config.ddpm_num_steps),
speech_len * ddpm_batch_mul,
replacement=True,
).to(hidden_states.device)
speech_features_repeated = speech_features.repeat_interleave(ddpm_batch_mul, dim=0)
condition_features_repeated = condition_features.repeat_interleave(ddpm_batch_mul, dim=0)
noisy_speech_features = self.model.noise_scheduler.add_noise(
speech_features_repeated, noise, timesteps
)
model_output = self.model.prediction_head(
noisy_speech_features,
timesteps.type_as(x),
condition_features_repeated
)
prediction_type = self.config.diffusion_head_config.prediction_type
if prediction_type == "epsilon":
target_for_loss = noise
elif prediction_type == "v_prediction":
target_for_loss = self.model.noise_scheduler.get_velocity(
speech_features_repeated, noise, timesteps
)
else:
raise NotImplementedError(f"Prediction type {prediction_type} not implemented")
diffusion_loss = F.mse_loss(model_output.float(), target_for_loss.float(), reduction='sum')
if latent_size > 0 and ddpm_batch_mul > 0:
diffusion_loss = diffusion_loss / latent_size / ddpm_batch_mul
else:
diffusion_loss = torch.tensor(0.0, device=diffusion_loss.device)
else:
# Dummy loss for DDP to work when there are no speech samples in a batch,
# but we are in a speech context.
diffusion_loss = sum(p.sum() for p in self.model.prediction_head.parameters()) * 0.0
diffusion_loss += sum(p.sum() for p in self.model.acoustic_connector.parameters()) * 0.0
diffusion_loss += sum(p.sum() for p in self.model.semantic_connector.parameters()) * 0.0
# --- End Diffusion Loss Calculation ---
if not return_dict:
output = (logits, speech_len) + outputs.to_tuple()[1:]
return (loss, diffusion_loss) + output
return VibeVoiceCausalLMOutputWithPast(
loss=loss,
diffusion_loss=diffusion_loss,
speech_token_num=speech_len if speech_tensors is not None else 0,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
AutoModel.register(VibeVoiceConfig, VibeVoiceModel)
AutoModelForCausalLM.register(VibeVoiceConfig, VibeVoiceForConditionalGeneration)
__all__ = [
"VibeVoiceModel",
"VibeVoicePreTrainedModel",
"VibeVoiceForConditionalGeneration",
"VibeVoiceCausalLMOutputWithPast",
"VibeVoiceGenerationOutput",
]
+522
View File
@@ -0,0 +1,522 @@
from typing import List, Optional, Tuple, Union
import torch
import torch.nn as nn
from transformers.models.auto import AutoModel, AutoModelForCausalLM
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast
from transformers import modeling_utils
from transformers.modeling_utils import PreTrainedModel
from transformers.utils import logging
from transformers.generation import GenerationMixin
from .modular_vibevoice_tokenizer import (
VibeVoiceTokenizerStreamingCache,
VibeVoiceTokenizerEncoderOutput
)
from .configuration_vibevoice import VibeVoiceASRConfig
from .modeling_vibevoice import (
VibeVoiceCausalLMOutputWithPast,
SpeechConnector
)
logger = logging.get_logger(__name__)
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
# @auto_docstring
class VibeVoiceASRPreTrainedModel(PreTrainedModel):
config_class = VibeVoiceASRConfig
base_model_prefix = "model"
supports_gradient_checkpointing = True
_skip_keys_device_placement = "past_key_values"
_supports_cache_class = True
_supports_flash_attn = True
_supports_flash_attn_2 = True
_supports_sdpa = True
_supports_quantized_cache = True
_supports_static_cache = True
_supports_attention_backend = True
def _init_weights(self, module):
# Use the language model's initializer_range if available
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
std = self.config.language_model_config.initializer_range
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
std = self.config.decoder_config.initializer_range
else:
std = 0.02 # Default value
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.LayerNorm):
module.weight.data.fill_(1.0)
module.bias.data.zero_()
# @auto_docstring
class VibeVoiceASRModel(VibeVoiceASRPreTrainedModel):
def __init__(self, config):
super().__init__(config)
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
if isinstance(config.torch_dtype, str):
dtype = getattr(torch, config.torch_dtype)
else:
dtype = config.torch_dtype
else:
dtype = torch.float32
# Initialize Qwen2 model for language modeling
lm_config = config.decoder_config
self.language_model = AutoModel.from_config(lm_config)
# Initialize speech components if needed
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
self.semantic_tokenizer = AutoModel.from_config(config.semantic_tokenizer_config).to(dtype)
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
self.semantic_connector = SpeechConnector(config.semantic_vae_dim, lm_config.hidden_size).to(dtype)
def get_input_embeddings(self):
if hasattr(self.language_model, 'embed_tokens'):
# If the language model has an embed_tokens attribute, return it
return self.language_model.embed_tokens
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
if attr.orig_name == 'embed_tokens.weight':
return getattr(self.language_model, name)
assert False, 'should not arrive here'
def set_input_embeddings(self, value):
self.language_model.embed_tokens = value
def set_speech_tokenizers(self, acoustic_tokenizer=None, semantic_tokenizer=None):
"""Set the speech tokenizers used for encoding and decoding speech."""
self.acoustic_tokenizer = acoustic_tokenizer
self.semantic_tokenizer = semantic_tokenizer
# Reset the encoder to evaluation mode
if self.acoustic_tokenizer is not None:
self.acoustic_tokenizer.eval()
if self.semantic_tokenizer is not None:
self.semantic_tokenizer.eval()
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
**kwargs,
) -> Union[Tuple, BaseModelOutputWithPast]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Forward through language model
outputs = self.language_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
**kwargs,
)
if not return_dict:
return outputs
return BaseModelOutputWithPast(
last_hidden_state=outputs.last_hidden_state,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
class VibeVoiceASRForConditionalGeneration(VibeVoiceASRPreTrainedModel, GenerationMixin):
"""
VibeVoice model for Automatic Speech Recognition (ASR) with language modeling head for conditional generation.
This class is designed for inference and generation tasks.
"""
_tied_weights_keys = ["lm_head.weight"]
_tp_plan = {"lm_head": "colwise_rep"}
def __init__(self, config):
super().__init__(config)
self.model = VibeVoiceASRModel(config)
self.vocab_size = config.decoder_config.vocab_size
# Determine the dtype to use
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
if isinstance(config.torch_dtype, str):
dtype = getattr(torch, config.torch_dtype)
else:
dtype = config.torch_dtype
else:
dtype = torch.float32
# Initialize lm_head with the correct dtype
self.lm_head = nn.Linear(config.decoder_config.hidden_size, self.vocab_size, bias=False).to(dtype)
# Initialize weights and apply final processing
self.post_init()
def get_input_embeddings(self):
return self.model.get_input_embeddings()
def set_input_embeddings(self, value):
self.model.set_input_embeddings(value)
def get_output_embeddings(self):
return self.lm_head
def set_output_embeddings(self, new_embeddings):
self.lm_head = new_embeddings
def set_decoder(self, decoder):
self.model.language_model = decoder
def get_decoder(self):
return self.model.language_model
def tie_weights(self):
"""Tie the weights between the input embeddings and the output embeddings."""
if getattr(self.config.decoder_config, 'tie_word_embeddings', False):
output_embeddings = self.get_output_embeddings()
input_embeddings = self.get_input_embeddings()
if hasattr(input_embeddings, 'weight'):
output_embeddings.weight = input_embeddings.weight
else:
output_embeddings.weight = input_embeddings
def encode_speech(
self,
speech_tensors: torch.FloatTensor,
speech_masks: Optional[torch.BoolTensor] = None,
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
streaming_segment_duration: float = 60.0, # seconds
):
"""
Encode speech input into features that can be used by the language model.
This method is called once before generation to process the speech input.
For long audio (>600s by default), uses streaming processing to avoid conv overflow (>2^32).
Segments are processed independently, then concatenated before final sampling.
Args:
speech_tensors: Input audio tensor [batch_size, samples]
speech_masks: Optional mask for speech features
speech_semantic_tensors: Optional pre-computed semantic tokens
streaming_segment_duration: Segment duration in seconds for streaming processing (default: 60s)
"""
if hasattr(self.config, 'torch_dtype') and self.config.torch_dtype is not None:
if isinstance(self.config.torch_dtype, str):
dtype = getattr(torch, self.config.torch_dtype)
else:
dtype = self.config.torch_dtype
else:
dtype = torch.float32
speech_tensors = speech_tensors.to(dtype)
# Ensure proper shape: (batch, samples)
if speech_tensors.ndim == 1:
speech_tensors = speech_tensors.unsqueeze(0)
batch_size, total_samples = speech_tensors.shape
sample_rate = 24000 # fix 24kHz sample rate
# Calculate segment size in samples
segment_samples = int(streaming_segment_duration * sample_rate)
# Decide whether to use streaming based on audio length
use_streaming = total_samples > segment_samples
with torch.no_grad():
if not use_streaming:
# Short audio: direct processing (original behavior)
encoder_output = self.model.acoustic_tokenizer.encode(speech_tensors.unsqueeze(1))
audio_tokens = encoder_output.sample(dist_type=self.model.acoustic_tokenizer.std_dist_type)[0]
acoustic_features = self.model.acoustic_connector(audio_tokens)
# Encode semantic features
if speech_semantic_tensors is not None:
semantic_features = self.model.semantic_connector(speech_semantic_tensors)
else:
semantic_tokens = self.model.semantic_tokenizer.encode(speech_tensors.unsqueeze(1)).mean
semantic_features = self.model.semantic_connector(semantic_tokens)
else:
# Long audio: streaming processing
# print(f"Using streaming processing for long audio: {total_samples/sample_rate:.1f}s "
# f"(segment size: {streaming_segment_duration}s)")
# Initialize caches for both tokenizers
acoustic_encoder_cache = VibeVoiceTokenizerStreamingCache()
semantic_encoder_cache = VibeVoiceTokenizerStreamingCache()
acoustic_mean_segments = []
semantic_mean_segments = []
sample_indices = torch.arange(batch_size, device=speech_tensors.device)
# Helper function from batch_asr_sft_cache.py
def _iter_segments(total_length: int, segment_length: int):
"""Iterate over audio segments with a given segment length."""
if segment_length <= 0:
raise ValueError("segment_length must be positive")
for start in range(0, total_length, segment_length):
end = min(start + segment_length, total_length)
if end > start:
yield start, end
# Process each segment for both acoustic and semantic tokenizers
segments = list(_iter_segments(total_samples, segment_samples))
num_segments = len(segments)
for seg_idx, (start, end) in enumerate(segments):
chunk = speech_tensors[:, start:end].contiguous()
if chunk.numel() == 0:
continue
# Check if this is the final segment
is_final = (seg_idx == num_segments - 1)
# Encode chunk for acoustic tokenizer (don't sample yet)
acoustic_encoder_output = self.model.acoustic_tokenizer.encode(
chunk.unsqueeze(1),
cache=acoustic_encoder_cache,
sample_indices=sample_indices,
use_cache=True,
is_final_chunk=is_final,
)
acoustic_mean_segments.append(acoustic_encoder_output.mean)
# Encode chunk for semantic tokenizer (take mean directly)
semantic_encoder_output = self.model.semantic_tokenizer.encode(
chunk.unsqueeze(1),
cache=semantic_encoder_cache,
sample_indices=sample_indices,
use_cache=True,
is_final_chunk=is_final,
)
semantic_mean_segments.append(semantic_encoder_output.mean)
# print(f"Processed {len(acoustic_mean_segments)} segments.")
# Concatenate all acoustic means and sample once
acoustic_mean_full = torch.cat(acoustic_mean_segments, dim=1).contiguous()
acoustic_encoder_output = VibeVoiceTokenizerEncoderOutput(
mean=acoustic_mean_full,
std=self.model.acoustic_tokenizer.fix_std
)
audio_tokens = acoustic_encoder_output.sample(
dist_type=self.model.acoustic_tokenizer.std_dist_type
)[0]
acoustic_features = self.model.acoustic_connector(audio_tokens)
# Concatenate all semantic means
semantic_tokens = torch.cat(semantic_mean_segments, dim=1).contiguous()
semantic_features = self.model.semantic_connector(semantic_tokens)
# Combine acoustic and semantic features
if speech_masks is not None:
combined_features = acoustic_features[speech_masks] + semantic_features[speech_masks]
else:
combined_features = acoustic_features + semantic_features
return combined_features
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[torch.FloatTensor]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
# Speech-specific arguments
speech_tensors: Optional[torch.FloatTensor] = None,
speech_masks: Optional[torch.BoolTensor] = None,
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
acoustic_input_mask: Optional[torch.BoolTensor] = None,
**kwargs,
) -> Union[Tuple, CausalLMOutput]:
"""
Forward pass for the model. Handles both training and generation scenarios.
"""
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
use_cache = use_cache if use_cache is not None else getattr(self.config, 'use_cache', False)
# Process inputs
if inputs_embeds is None and input_ids is not None:
inputs_embeds = self.get_input_embeddings()(input_ids)
# If we have speech input and acoustic_input_mask, encode and insert speech features
if speech_tensors is not None and acoustic_input_mask is not None:
speech_features = self.encode_speech(
speech_tensors=speech_tensors,
speech_masks=speech_masks,
speech_semantic_tensors=speech_semantic_tensors,
)
# Clone to avoid in-place operation on leaf variable during training
inputs_embeds = inputs_embeds.clone()
inputs_embeds[acoustic_input_mask] = speech_features
# Forward through the model
outputs = self.model(
input_ids=None,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
)
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
logits = self.lm_head(hidden_states)
loss = None
if labels is not None:
# Shift so that tokens < n predict n
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Flatten the tokens
loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
shift_logits = shift_logits.view(-1, self.vocab_size)
shift_labels = shift_labels.view(-1)
# Enable model parallelism
shift_labels = shift_labels.to(shift_logits.device)
loss = loss_fct(shift_logits, shift_labels)
if not return_dict:
output = (logits,) + outputs[1:]
return (loss,) + output if loss is not None else output
return VibeVoiceCausalLMOutputWithPast(
loss=loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
def prepare_inputs_for_generation(
self,
input_ids,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
cache_position=None,
position_ids=None,
use_cache=True,
speech_tensors=None,
speech_masks=None,
speech_semantic_tensors=None,
acoustic_input_mask=None,
**kwargs,
):
"""
Prepare inputs for generation step. This method is called by generate()
for each token generation step.
Following Qwen2-VL's approach: speech inputs are only forwarded on the first pass
(when cache_position[0] == 0), and are excluded in subsequent generation steps.
"""
# If we have past key values, we only need to process the new tokens
if past_key_values is not None:
if isinstance(past_key_values, tuple):
past_length = past_key_values[0][0].shape[2]
else:
past_length = past_key_values.get_seq_length()
# Keep only the new tokens
if input_ids is not None and input_ids.shape[1] > past_length:
input_ids = input_ids[:, past_length:]
# Prepare position ids
if position_ids is None and attention_mask is not None:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
if past_key_values is not None and input_ids is not None:
position_ids = position_ids[:, -input_ids.shape[1]:]
# Prepare cache position
if cache_position is None:
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
cache_position = torch.arange(
past_seen_tokens,
past_seen_tokens + (input_ids.shape[1] if input_ids is not None else inputs_embeds.shape[1]),
device=input_ids.device if input_ids is not None else inputs_embeds.device
)
# Prepare model inputs
if inputs_embeds is not None and past_key_values is None:
model_inputs = {"inputs_embeds": inputs_embeds}
else:
model_inputs = {"input_ids": input_ids}
model_inputs.update(
{
"position_ids": position_ids,
"cache_position": cache_position,
"past_key_values": past_key_values,
"use_cache": use_cache,
"attention_mask": attention_mask,
}
)
# Following Qwen2-VL pattern: only include speech inputs on the first forward pass
# (when cache_position[0] == 0), exclude them in subsequent generation steps
if cache_position is not None and len(cache_position) > 0 and cache_position[0] == 0:
# First forward pass - include speech inputs if provided
model_inputs.update({
"speech_tensors": speech_tensors,
"speech_masks": speech_masks,
"speech_semantic_tensors": speech_semantic_tensors,
"acoustic_input_mask": acoustic_input_mask,
})
else:
# Subsequent generation steps - exclude speech inputs
model_inputs.update({
"speech_tensors": None,
"speech_masks": None,
"speech_semantic_tensors": None,
"acoustic_input_mask": None,
})
# Include any remaining kwargs that might be needed
model_inputs.update(kwargs)
return model_inputs
AutoModel.register(VibeVoiceASRConfig, VibeVoiceASRModel)
AutoModelForCausalLM.register(VibeVoiceASRConfig, VibeVoiceASRForConditionalGeneration)
__all__ = [
"VibeVoiceASRPreTrainedModel",
"VibeVoiceASRModel",
"VibeVoiceASRForConditionalGeneration",
]
@@ -0,0 +1,190 @@
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union, Callable
from tqdm import tqdm
import copy
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.distributed as dist
from transformers.models.auto import AutoModel, AutoModelForCausalLM
from transformers.activations import ACT2FN
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast, ModelOutput
from transformers.models.llama.modeling_llama import LlamaRMSNorm
from transformers import modeling_utils
from transformers.modeling_utils import PreTrainedModel
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
from transformers.utils import logging
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
logger = logging.get_logger(__name__)
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
class BinaryClassifier(nn.Module):
def __init__(self, hidden_size):
super(BinaryClassifier, self).__init__()
self.fc1 = nn.Linear(hidden_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, 1)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
class SpeechConnector(nn.Module):
def __init__(self, input_dim, output_dim):
super().__init__()
self.fc1 = nn.Linear(input_dim, output_dim)
self.norm = LlamaRMSNorm(output_dim, eps=1e-6)
self.fc2 = nn.Linear(output_dim, output_dim)
def forward(self, features, **kwargs):
x = self.fc1(features)
x = self.norm(x)
x = self.fc2(x)
return x
# @auto_docstring
class VibeVoiceStreamingPreTrainedModel(PreTrainedModel):
config_class = VibeVoiceStreamingConfig
base_model_prefix = "model"
supports_gradient_checkpointing = True
_skip_keys_device_placement = "past_key_values"
_supports_cache_class = True
_supports_flash_attn_2 = True
_supports_sdpa = True
_supports_quantized_cache = True
_supports_static_cache = True
_supports_attention_backend = True
def _init_weights(self, module):
if isinstance(module, VibeVoiceDiffusionHead):
module.initialize_weights()
return
# Use the language model's initializer_range if available
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
std = self.config.language_model_config.initializer_range
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
std = self.config.decoder_config.initializer_range
else:
std = 0.02 # Default value
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.LayerNorm):
module.weight.data.fill_(1.0)
module.bias.data.zero_()
# @auto_docstring
class VibeVoiceStreamingModel(VibeVoiceStreamingPreTrainedModel):
def __init__(self, config):
super().__init__(config)
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
if isinstance(config.torch_dtype, str):
dtype = getattr(torch, config.torch_dtype)
else:
dtype = config.torch_dtype
else:
dtype = torch.float32
# Initialize Qwen2 model for language modeling.
# The lower Transformer layers are only used for encoding text, while the upper Transformer layers are used for encoding text and generating speech.
# To keep the code clean, we constructs two language models.
# The final norm layer of the first language_model is set to identity and will not be used in inference.
lm_config = copy.deepcopy(config.decoder_config)
lm_backbone_num_hidden_layers = getattr(lm_config, 'num_hidden_layers', 24) - config.tts_backbone_num_hidden_layers
lm_config.num_hidden_layers = lm_backbone_num_hidden_layers
self.language_model = AutoModel.from_config(lm_config)
self.language_model.norm = nn.Identity()
# We only need the Transformer layers here. Note that embed_tokens in tts_language_model is unused
tts_lm_config = copy.deepcopy(lm_config)
tts_lm_config.num_hidden_layers = config.tts_backbone_num_hidden_layers
self.tts_language_model = AutoModel.from_config(tts_lm_config)
# Marks the text that needs to be spoken by the TTS model.
self.tts_input_types = nn.Embedding(num_embeddings=2, embedding_dim=config.decoder_config.hidden_size)
# Initialize speech components if needed
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
# Register scaling factors as buffers - use 1D tensors for FSDP compatibility
self.register_buffer('speech_scaling_factor', torch.tensor(float('nan')))
self.register_buffer('speech_bias_factor', torch.tensor(float('nan')))
# Initialize prediction head for speech generation
self.prediction_head = AutoModel.from_config(config.diffusion_head_config).to(dtype)
# Initialize noise scheduler
self.noise_scheduler = DPMSolverMultistepScheduler(
num_train_timesteps=config.diffusion_head_config.ddpm_num_steps,
beta_schedule=config.diffusion_head_config.ddpm_beta_schedule,
prediction_type=config.diffusion_head_config.prediction_type
)
def get_input_embeddings(self):
if hasattr(self.language_model, 'embed_tokens'):
# If the language model has an embed_tokens attribute, return it
return self.language_model.embed_tokens
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
if attr.orig_name == 'embed_tokens.weight':
return getattr(self.language_model, name)
assert False, 'should not arrive here'
def set_input_embeddings(self, value):
self.language_model.embed_tokens = value
def set_speech_tokenizers(self, acoustic_tokenizer=None):
"""Set the speech tokenizers used for encoding and decoding speech."""
self.acoustic_tokenizer = acoustic_tokenizer
# Reset the encoder to evaluation mode
if self.acoustic_tokenizer is not None:
self.acoustic_tokenizer.eval()
def forward(self, *args, **kwargs):
"""
Intentionally not implemented.
This streaming model is split into two explicit submodules:
- `language_model` for plain text processing (lower layers).
- `tts_language_model` for TTS-related upper layers.
We deliberately avoid a unified `forward` to prevent accidental calls
that mix responsibilities.
To use the model:
- Call `self.language_model(...)` for text embeddings / hidden states.
- Call `self.tts_language_model(...)` for the TTS portion.
- Use the dedicated inference class for combined generation logic.
"""
raise RuntimeError(
"VibeVoiceStreamingModel.forward is intentionally disabled. "
"Use `model.language_model(...)` or `model.tts_language_model(...)` instead."
)
AutoModel.register(VibeVoiceStreamingConfig, VibeVoiceStreamingModel)
__all__ = [
"VibeVoiceStreamingPreTrainedModel",
"VibeVoiceStreamingModel",
]
@@ -0,0 +1,906 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
from tqdm import tqdm
import inspect
import torch
import torch.nn as nn
from transformers.models.auto import AutoModel, AutoModelForCausalLM
from transformers.generation import GenerationMixin, GenerationConfig, LogitsProcessor, LogitsProcessorList, StoppingCriteriaList
from transformers.modeling_outputs import BaseModelOutputWithPast, ModelOutput
from transformers import modeling_utils
from transformers.modeling_utils import PreTrainedModel
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
from transformers.utils import logging
from .modular_vibevoice_tokenizer import VibeVoiceTokenizerStreamingCache
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
from .modular_vibevoice_text_tokenizer import VibeVoiceTextTokenizer, VibeVoiceTextTokenizerFast
from .modeling_vibevoice_streaming import VibeVoiceStreamingPreTrainedModel, VibeVoiceStreamingModel, BinaryClassifier
from .streamer import AudioStreamer, AsyncAudioStreamer
logger = logging.get_logger(__name__)
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
TTS_TEXT_WINDOW_SIZE = 5
TTS_SPEECH_WINDOW_SIZE = 6
# ============================================================================
# Transformers >= 4.57 Compatibility Layer
# The cache system was refactored in transformers 4.57, requiring these helpers.
# ============================================================================
class MockCacheLayer:
"""
Mock cache layer for transformers >= 4.57 compatibility.
Provides the `layers` interface expected by DynamicCache in newer versions.
"""
def __init__(self, key_cache, value_cache, parent_cache=None, layer_idx=0):
self.key_cache = key_cache
self.value_cache = value_cache
self._parent_cache = parent_cache
self._layer_idx = layer_idx
def get_mask_sizes(self, cache_position):
"""Return KV length and offset for mask creation."""
seq_length = self.key_cache.shape[2] if self.key_cache is not None else 0
query_length = cache_position.shape[0]
return seq_length + query_length, 0
def update(self, key_states, value_states, cache_kwargs=None):
"""Update the cache with new key/value states."""
if self._parent_cache is None:
return self.key_cache, self.value_cache
parent = self._parent_cache
idx = self._layer_idx
# Extend cache lists if needed
while len(parent.key_cache) <= idx:
parent.key_cache.append(None)
parent.value_cache.append(None)
# Concatenate or initialize cache
if parent.key_cache[idx] is not None:
parent.key_cache[idx] = torch.cat([parent.key_cache[idx], key_states], dim=2)
parent.value_cache[idx] = torch.cat([parent.value_cache[idx], value_states], dim=2)
else:
parent.key_cache[idx] = key_states
parent.value_cache[idx] = value_states
# Update local references
self.key_cache = parent.key_cache[idx]
self.value_cache = parent.value_cache[idx]
return self.key_cache, self.value_cache
def _ensure_cache_has_layers(cache):
"""
Ensure the cache has all required attributes for transformers >= 4.57.
Creates MockCacheLayer wrappers to provide the expected `layers` interface.
"""
if cache is None:
return cache
# Add required attributes (skip if read-only)
for attr, default in [('layer_class_to_replicate', None), ('offloading', False), ('is_compileable', False)]:
if not hasattr(cache, attr):
try:
setattr(cache, attr, default)
except AttributeError:
pass
# Build layers list from key_cache/value_cache
if hasattr(cache, 'key_cache') and hasattr(cache, 'value_cache'):
try:
cache.layers = [
MockCacheLayer(cache.key_cache[i], cache.value_cache[i], parent_cache=cache, layer_idx=i)
for i in range(len(cache.key_cache))
]
except AttributeError:
pass
elif not hasattr(cache, 'layers'):
try:
cache.layers = []
except AttributeError:
pass
return cache
def _update_model_kwargs_for_generation(
outputs: ModelOutput,
model_kwargs: Dict[str, Any],
num_new_tokens: int = 1,
) -> Dict[str, Any]:
"""
Update model_kwargs after adding new tokens (supports multi-token windows).
Updates past_key_values, attention_mask, and cache_position for the next forward pass.
"""
model_kwargs["past_key_values"] = _ensure_cache_has_layers(outputs.past_key_values)
attention_mask = model_kwargs["attention_mask"]
model_kwargs["attention_mask"] = torch.cat(
[attention_mask, attention_mask.new_ones((attention_mask.shape[0], num_new_tokens))], dim=-1
)
cache_pos = model_kwargs["cache_position"]
model_kwargs["cache_position"] = torch.arange(
cache_pos[-1] + 1, cache_pos[-1] + num_new_tokens + 1, device=cache_pos.device
)
return model_kwargs
@dataclass
class VibeVoiceCausalLMOutputWithPast(BaseModelOutputWithPast):
logits: Optional[torch.FloatTensor] = None
@dataclass
class VibeVoiceGenerationOutput(ModelOutput):
"""
Output type for VibeVoice generation.
Args:
sequences (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
The generated sequences.
speech_outputs (`List[torch.FloatTensor]`, *optional*):
List of generated speech waveforms or latents for each speech segment.
"""
sequences: torch.LongTensor = None
speech_outputs: Optional[List[torch.FloatTensor]] = None
reach_max_step_sample: Optional[torch.BoolTensor] = None
class VibeVoiceStreamingForConditionalGenerationInference(VibeVoiceStreamingPreTrainedModel, GenerationMixin):
def __init__(self, config):
super().__init__(config)
# Initialize the base model
self.model = VibeVoiceStreamingModel(config)
# TTS generation EOS classifier
self.tts_eos_classifier = BinaryClassifier(config.decoder_config.hidden_size)
# inference configuration
self.ddpm_inference_steps = config.diffusion_head_config.ddpm_num_inference_steps
# Initialize weights and apply final processing
self.post_init()
@property
def noise_scheduler(self):
return self.model.noise_scheduler
@property
def prediction_head(self):
return self.model.prediction_head
@property
def speech_scaling_factor(self):
return self.model.speech_scaling_factor
@property
def speech_bias_factor(self):
return self.model.speech_bias_factor
@property
def acoustic_tokenizer(self):
return self.model.acoustic_tokenizer
@property
def acoustic_connector(self):
return self.model.acoustic_connector
def tie_weights(self):
"""
Tie the weights between the input embeddings and the output embeddings.
"""
# Tie lm_head.weight to language_model.embed_tokens.weight
if not getattr(self.config, 'tie_word_embeddings', False):
return
if hasattr(self, 'lm_head') and hasattr(self.model.language_model, 'embed_tokens'):
self.lm_head.weight = self.model.language_model.embed_tokens.weight
def get_input_embeddings(self):
return self.model.get_input_embeddings()
def set_input_embeddings(self, value):
self.model.set_input_embeddings(value)
def get_output_embeddings(self):
"""
This model does not define an `lm_head` (vocabulary projection).
"""
return None
def set_output_embeddings(self, new_embeddings):
"""
No-op because there is no `lm_head`. Provided only to satisfy optional API calls.
To enable, first create `self.lm_head` then allow assignment.
"""
raise RuntimeError("Output embeddings (lm_head) are not defined for this model. "
"Create one before calling set_output_embeddings if needed.")
def set_speech_tokenizers(self, acoustic_tokenizer=None):
"""Set the speech tokenizers used for encoding and decoding speech."""
self.model.set_speech_tokenizers(acoustic_tokenizer)
def set_ddpm_inference_steps(self, num_steps=None):
self.ddpm_inference_steps = num_steps or self.config.diffusion_head_config.ddpm_num_inference_steps
def prepare_inputs_for_generation(
self,
input_ids: torch.LongTensor,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
cache_position=None,
**kwargs,
):
"""Prepare model inputs for generation (transformers >= 4.57 compatible)."""
model_inputs = {"cache_position": cache_position}
# Slice inputs when using cache
if past_key_values is not None:
model_inputs["past_key_values"] = past_key_values
if inputs_embeds is not None and input_ids.shape[1] == 0:
inputs_embeds = inputs_embeds[:, -cache_position.shape[0]:]
elif inputs_embeds is not None or (cache_position is not None and cache_position[-1] >= input_ids.shape[1]):
input_ids = input_ids[:, -cache_position.shape[0]:]
elif cache_position is not None and input_ids.shape[1] != cache_position.shape[0]:
input_ids = input_ids[:, cache_position]
# Set input_ids or inputs_embeds
use_embeds = inputs_embeds is not None and (
past_key_values is None or (cache_position is not None and len(cache_position) == inputs_embeds.shape[1])
)
if use_embeds:
model_inputs["input_ids"] = None
model_inputs["inputs_embeds"] = inputs_embeds
else:
model_inputs["input_ids"] = input_ids.clone(memory_format=torch.contiguous_format) if input_ids is not None else None
model_inputs["inputs_embeds"] = None
if attention_mask is not None:
model_inputs["attention_mask"] = attention_mask
# Create position_ids from attention_mask
if attention_mask is not None and kwargs.get("position_ids") is None:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
kwargs["position_ids"] = position_ids
# Slice position_ids when using cache
if kwargs.get("position_ids") is not None:
if past_key_values is not None:
seq_len = model_inputs["inputs_embeds"].shape[1] if model_inputs.get("inputs_embeds") is not None else model_inputs["input_ids"].shape[1]
model_inputs["position_ids"] = kwargs["position_ids"][:, -seq_len:].clone(memory_format=torch.contiguous_format)
else:
model_inputs["position_ids"] = kwargs.pop("position_ids").clone(memory_format=torch.contiguous_format)
# Forward remaining kwargs
for key, value in kwargs.items():
if key not in model_inputs:
model_inputs[key] = value
model_inputs.pop("labels", None)
return model_inputs
def _update_model_kwargs_for_generation(
self,
outputs,
model_kwargs,
is_encoder_decoder=False,
num_new_tokens=1,
):
"""Override to ensure cache compatibility with transformers >= 4.57."""
model_kwargs = super()._update_model_kwargs_for_generation(
outputs, model_kwargs, is_encoder_decoder=is_encoder_decoder, num_new_tokens=num_new_tokens
)
if "past_key_values" in model_kwargs:
model_kwargs["past_key_values"] = _ensure_cache_has_layers(model_kwargs["past_key_values"])
return model_kwargs
def _init_cache_for_generation(self, generation_config, model_kwargs, batch_size, max_cache_length, device):
"""
Initialize cache for generation, handling different transformers versions.
For transformers >= 4.57, returns None to let the model create the cache dynamically.
"""
try:
from transformers.cache_utils import DynamicCache
sig = inspect.signature(DynamicCache.__init__)
if 'config' in sig.parameters:
# transformers >= 4.57: let model handle cache creation
return None
else:
# Older versions: use parent method
prep_sig = inspect.signature(self._prepare_cache_for_generation)
if 'device' in prep_sig.parameters:
self._prepare_cache_for_generation(generation_config, model_kwargs, None, batch_size, max_cache_length, device)
else:
self._prepare_cache_for_generation(generation_config, model_kwargs, None, batch_size, max_cache_length)
return model_kwargs.get("past_key_values")
except Exception:
return None
# @can_return_tuple
def forward_lm(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
**kwargs,
) -> Union[Tuple, BaseModelOutputWithPast]:
"""
Single pass of the base text LM.
- Builds embeddings if `inputs_embeds` not provided.
- Uses (and returns) `past_key_values` when `use_cache=True`.
- No loss / no lm_head / no speech logic.
Args:
input_ids: (B, S) token ids.
attention_mask: (B, S) mask.
past_key_values: cache from previous steps.
cache_position: positions for cached tokens.
labels: unsupported (will raise).
Returns:
BaseModelOutputWithPast with `last_hidden_state` and `past_key_values`.
"""
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Get embeddings
if inputs_embeds is None:
inputs_embeds = self.model.get_input_embeddings()(input_ids)
outputs = self.model.language_model(
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
**kwargs,
)
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
if labels is not None:
raise NotImplementedError("Loss computation is not implemented in this version.")
return BaseModelOutputWithPast(
past_key_values=outputs.past_key_values,
last_hidden_state=hidden_states,
attentions=outputs.attentions,
)
# @can_return_tuple
def forward_tts_lm(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
lm_last_hidden_state: Optional[torch.FloatTensor] = None,
tts_text_masks: Optional[torch.BoolTensor] = None,
**kwargs,
) -> Union[Tuple, VibeVoiceCausalLMOutputWithPast]:
"""
Single pass of the TTS LM.
- Overwrites tail embeddings with `lm_last_hidden_state`.
- Adds type embedding via `tts_text_masks` (1=text, 0=speech).
- Predicts EOS from last hidden state (binary classifier).
- No loss / no full acoustic decoding here.
Args:
input_ids: (B, S) token ids.
attention_mask: (B, S) mask.
lm_last_hidden_state: (B, K, H) hidden states to splice into the tail.
tts_text_masks: (B, 1) mask marking current position as text(1)/speech(0).
past_key_values: cache from previous TTS steps.
cache_position: positions for cached tokens.
labels: unsupported (will raise).
Returns:
VibeVoiceCausalLMOutputWithPast with `logits` (EOS), `last_hidden_state`, `past_key_values`.
"""
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Get embeddings
if inputs_embeds is None:
# Will be replaced with lm_last_hidden_state
inputs_embeds = self.model.get_input_embeddings()(input_ids)
# Replace the last part of inputs_embeds with lm_last_hidden_state
start_idx = inputs_embeds.shape[1] - lm_last_hidden_state.shape[1]
inputs_embeds[:, start_idx:, :] = lm_last_hidden_state
# Adds type embedding via `tts_text_masks`.
inputs_embeds = inputs_embeds + self.model.tts_input_types(tts_text_masks.long())
outputs = self.model.tts_language_model(
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
**kwargs,
)
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
logits = self.tts_eos_classifier(hidden_states[:, -1, :])
if labels is not None:
raise NotImplementedError("Loss computation is not implemented in this version.")
return VibeVoiceCausalLMOutputWithPast(
logits=logits,
past_key_values=outputs.past_key_values,
last_hidden_state=hidden_states,
attentions=outputs.attentions,
)
def forward(self, *args, **kwargs):
"""
Unified forward is intentionally disabled.
Reasons:
1. The inference pipeline is staged: base text LM, then TTS LM, plus streaming & diffusion handled in `generate`.
2. A monolithic call would hide required sequencing (prefill, window stepping, speech diffusion sampling).
Use instead:
- self.forward_lm(...) for a base text LM step (prefill or incremental).
- self.forward_tts_lm(...) for a single TTS LM step (needs LM hidden states).
- self.generate(...) for full streaming (text + speech + diffusion + audio assembly).
Raises:
RuntimeError: Always (by design).
"""
raise RuntimeError(
"Unified forward is disabled. Use `forward_lm`, `forward_tts_lm`, or `generate` instead."
)
def _build_generate_config_model_kwargs(self, generation_config, inputs, tokenizer, return_processors=False, **kwargs):
if generation_config is None:
generation_config = GenerationConfig(
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
pad_token_id = tokenizer.pad_token_id
)
else:
generation_config = GenerationConfig(
**generation_config,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
pad_token_id = tokenizer.pad_token_id
)
generation_config, model_kwargs = self._prepare_generation_config(
generation_config,
True,
speech_start_id=tokenizer.speech_start_id,
speech_end_id=tokenizer.speech_end_id,
speech_diffusion_id=tokenizer.speech_diffusion_id,
**kwargs
)
generation_config.speech_start_id = tokenizer.speech_start_id
generation_config.speech_end_id = tokenizer.speech_end_id
generation_config.speech_diffusion_id = tokenizer.speech_diffusion_id
inputs_tensor, model_input_name, model_kwargs = self._prepare_model_inputs(inputs, generation_config.bos_token_id, model_kwargs)
batch_size = inputs_tensor.shape[0]
device = self.device
self._prepare_special_tokens(generation_config, True, device=device)
generation_config.use_cache = True
model_kwargs["use_cache"] = generation_config.use_cache
input_ids = inputs_tensor.to(self.device)
input_ids_length = input_ids.shape[1]
has_default_max_length = kwargs.get("max_length") is None and generation_config.max_length is not None
has_default_min_length = kwargs.get("min_length") is None and generation_config.min_length is not None
generation_config = self._prepare_generated_length(
generation_config=generation_config,
has_default_max_length=has_default_max_length,
has_default_min_length=has_default_min_length,
model_input_name=model_input_name,
inputs_tensor=inputs_tensor,
input_ids_length=input_ids_length,
)
max_cache_length = generation_config.max_length - 1
# Handle cache initialization for different transformers versions
model_kwargs["past_key_values"] = self._init_cache_for_generation(
generation_config, model_kwargs, batch_size, max_cache_length, device
)
model_kwargs['cache_position'] = torch.arange(input_ids_length, device=device, dtype=torch.long)
for k, v in model_kwargs.items():
if isinstance(v, torch.Tensor):
model_kwargs[k] = v.to(device=device)
if return_processors:
logits_processor = self._get_logits_processor(
generation_config=generation_config,
input_ids_seq_length=input_ids_length,
encoder_input_ids=inputs_tensor,
prefix_allowed_tokens_fn=None,
logits_processor=LogitsProcessorList(),
device=inputs_tensor.device,
model_kwargs=model_kwargs,
)
stopping_criteria = self._get_stopping_criteria(generation_config=generation_config, stopping_criteria=StoppingCriteriaList())
return generation_config, model_kwargs, input_ids, logits_processor, stopping_criteria
else:
return generation_config, model_kwargs, input_ids
@torch.no_grad()
def generate(
self,
inputs: Optional[torch.Tensor] = None,
generation_config: Optional[GenerationConfig] = None,
logits_processor: Optional[LogitsProcessorList] = None,
stopping_criteria: Optional[StoppingCriteriaList] = None,
prefix_allowed_tokens_fn: Optional[Callable[[int, torch.Tensor], List[int]]] = None,
synced_gpus: Optional[bool] = None,
assistant_model: Optional["PreTrainedModel"] = None,
audio_streamer: Optional[Union[AudioStreamer, AsyncAudioStreamer]] = None,
negative_prompt_ids: Optional[torch.Tensor] = None,
negative_prompt_attention_mask: Optional[torch.Tensor] = None,
speech_tensors: Optional[torch.FloatTensor] = None,
speech_masks: Optional[torch.BoolTensor] = None,
speech_input_mask: Optional[torch.BoolTensor] = None,
tts_text_ids: Optional[torch.LongTensor] = None,
return_speech: bool = True,
cfg_scale: float = 1.0,
stop_check_fn: Optional[Callable[[], bool]] = None,
**kwargs,
) -> Union[torch.LongTensor, VibeVoiceGenerationOutput]:
"""
Text is fed in small windows (dynamic slicing of `tts_text_ids`), which enables streaming text input: you dont need the full text upfront. After each text window, a loop samples several speech latents (diffusion). The interleaved text encoding + speech generation enables streaming text input and realtime speech output.
The function only supports batch size = 1 currently.
- Windowed text prefill → incremental LM + TTS LM updates.
- Interleave speech token diffusion sampling (`sample_speech_tokens`).
- Stops on EOS (binary classifier) or max length / external `stop_check_fn`.
- Returns final token `sequences` and (optionally) concatenated speech audio.
Args (selected):
tts_text_ids: Full text tokens to stream in windows.
audio_streamer: If provided, emits audio chunks during generation.
cfg_scale: Classifier-free guidance scale for speech diffusion.
return_speech: If False, skips audio decode concatenation.
stop_check_fn: External early-stop hook (returns True to halt).
Returns:
VibeVoiceGenerationOutput with:
- sequences: final token ids
- speech_outputs: list of concatenated audio tensors (or None)
- reach_max_step_sample: flags for samples stopped by max length
"""
# 1. Handle `generation_config` and kwargs that might update it, and validate the `.generate()` call
tokenizer = kwargs.pop("tokenizer", None)
neg_text_input_id = tokenizer.convert_tokens_to_ids("<|image_pad|>")
tts_lm_input_ids = kwargs.pop("tts_lm_input_ids", None)
tts_lm_attention_mask = kwargs.pop("tts_lm_attention_mask", None)
# all_prefilled_outputs: cached prefilled prompt outputs for lm, tts_lm, neg_lm, neg_tts_lm
all_prefilled_outputs = kwargs.pop("all_prefilled_outputs", None)
tts_text_ids = tts_text_ids.to(self.device)
if kwargs.get('max_new_tokens', None) is None:
kwargs['max_new_tokens'] = self.config.decoder_config.max_position_embeddings - tts_lm_input_ids.shape[-1]
generation_config, model_kwargs, input_ids, logits_processor, stopping_criteria = self._build_generate_config_model_kwargs(
generation_config, inputs, tokenizer, return_processors=True, **kwargs
)
negative_kwargs = {
'input_ids': torch.full((kwargs['input_ids'].shape[0], 1), neg_text_input_id, dtype=torch.long, device=kwargs['input_ids'].device),
'attention_mask': torch.ones((kwargs['input_ids'].shape[0], 1), dtype=torch.long, device=kwargs['input_ids'].device),
'max_new_tokens': kwargs.get('max_new_tokens', 100)
}
negative_generation_config, negative_model_kwargs, negative_input_ids = self._build_generate_config_model_kwargs(
None, None, tokenizer, return_processors=False, **negative_kwargs
)
tts_lm_kwargs = {
'input_ids': tts_lm_input_ids,
'attention_mask': tts_lm_attention_mask,
'max_new_tokens': kwargs.get('max_new_tokens', 100)
}
tts_lm_generation_config, tts_lm_model_kwargs, tts_lm_input_ids = self._build_generate_config_model_kwargs(
None, None, tokenizer, return_processors=False, **tts_lm_kwargs
)
tts_lm_negative_kwargs = {
'input_ids': torch.full((kwargs['input_ids'].shape[0], 1), neg_text_input_id, dtype=torch.long, device=kwargs['input_ids'].device),
'attention_mask': torch.ones((kwargs['input_ids'].shape[0], 1), dtype=torch.long, device=kwargs['input_ids'].device),
'max_new_tokens': kwargs.get('max_new_tokens', 100)
}
tts_lm_negative_generation_config, tts_lm_negative_model_kwargs, tts_lm_negative_input_ids = self._build_generate_config_model_kwargs(
None, None, tokenizer, return_processors=False, **tts_lm_negative_kwargs
)
acoustic_cache = VibeVoiceTokenizerStreamingCache()
batch_size = input_ids.shape[0]
assert batch_size == 1, "Currently only supports batch size == 1"
device = input_ids.device
finished_tags = torch.zeros(batch_size, dtype=torch.bool, device=device)
verbose = kwargs.get("verbose", False)
# Initialize audio chunks storage for each sample
audio_chunks = [[] for _ in range(batch_size)]
tts_text_window_index = 0
reach_max_step_sample = torch.zeros(batch_size, dtype=torch.bool, device=device)
first_text_window_size = TTS_TEXT_WINDOW_SIZE if tts_text_ids.shape[1] >= TTS_TEXT_WINDOW_SIZE else tts_text_ids.shape[1]
outputs = all_prefilled_outputs["lm"]
tts_lm_outputs = all_prefilled_outputs["tts_lm"]
negative_outputs = all_prefilled_outputs["neg_lm"]
tts_lm_negative_outputs = all_prefilled_outputs["neg_tts_lm"]
model_kwargs = _update_model_kwargs_for_generation(
outputs, model_kwargs, num_new_tokens=first_text_window_size,
)
tts_lm_model_kwargs = _update_model_kwargs_for_generation(
tts_lm_outputs, tts_lm_model_kwargs, num_new_tokens=first_text_window_size,
)
negative_model_kwargs = self._update_model_kwargs_for_generation(
negative_outputs, negative_model_kwargs, is_encoder_decoder=False,
)
tts_lm_negative_model_kwargs = self._update_model_kwargs_for_generation(
tts_lm_negative_outputs, tts_lm_negative_model_kwargs, is_encoder_decoder=False,
)
step = tts_lm_input_ids.shape[1]
total_generated_speech_tokens = 0
total_prefilled_text_tokens = 0
if kwargs.get("show_progress_bar", True):
progress_bar = tqdm(
total=tts_lm_generation_config.max_length,
desc=f"Prefilled {step} tokens, current step ({step} / {tts_lm_generation_config.max_length})",
initial=step,
leave=False
)
else:
progress_bar = None
while True:
# Check for external stop signal
if stop_check_fn is not None and stop_check_fn():
if verbose:
print(f"Generation stopped externally at step {step + 1}")
# End the audio streamer if it exists
if audio_streamer is not None:
audio_streamer.end()
break
# # Check if audio_streamer has been ended (stopped externally)
# if audio_streamer is not None and hasattr(audio_streamer, 'finished_flags'):
# if any(audio_streamer.finished_flags):
# if verbose:
# print(f"Audio generation stopped externally at step {step + 1}")
# break
if finished_tags.all():
if hasattr(progress_bar, 'set_description'):
progress_bar.set_description("Generation complete")
break
cur_input_tts_text_ids = tts_text_ids[:, tts_text_window_index*TTS_TEXT_WINDOW_SIZE:(tts_text_window_index+1)*TTS_TEXT_WINDOW_SIZE]
next_text_window_size = tts_text_ids[:, (tts_text_window_index+1)*TTS_TEXT_WINDOW_SIZE:(tts_text_window_index+2)*TTS_TEXT_WINDOW_SIZE].shape[1]
tts_text_window_index += 1
if cur_input_tts_text_ids.shape[1] > 0:
input_ids = torch.cat([input_ids, cur_input_tts_text_ids], dim=-1)
tts_lm_input_ids = torch.cat([tts_lm_input_ids, cur_input_tts_text_ids], dim=-1)
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
if verbose:
print(f"Reached maximum generation length {generation_config.max_length}, stopped it.")
reached_samples = torch.arange(batch_size, device=device)[~finished_tags]
if reached_samples.numel() > 0:
reach_max_step_sample[reached_samples] = True
break
step += cur_input_tts_text_ids.shape[1]
total_prefilled_text_tokens += cur_input_tts_text_ids.shape[1]
if progress_bar is not None:
progress_bar.update(cur_input_tts_text_ids.shape[1])
progress_bar.set_description(f"Prefilled {total_prefilled_text_tokens} text tokens, generated {total_generated_speech_tokens} speech tokens, current step ({step} / {tts_lm_generation_config.max_length})")
model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs)
# Forward pass through the model
outputs = self.forward_lm(
**model_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
)
model_kwargs = _update_model_kwargs_for_generation(
outputs, model_kwargs, num_new_tokens=next_text_window_size,
)
tts_lm_model_inputs = self.prepare_inputs_for_generation(tts_lm_input_ids, **tts_lm_model_kwargs)
tts_lm_additional_inputs = {
"tts_text_masks": torch.ones_like(tts_lm_input_ids[:, -1:]),
"lm_last_hidden_state": outputs.last_hidden_state,
}
# Forward pass through the model
tts_lm_outputs = self.forward_tts_lm(
**tts_lm_model_inputs, **tts_lm_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
)
tts_lm_model_kwargs = self._update_model_kwargs_for_generation(
tts_lm_outputs, tts_lm_model_kwargs, is_encoder_decoder=False,
)
diffusion_indices = torch.LongTensor([0])
for cur_speech_index in range(TTS_SPEECH_WINDOW_SIZE):
positive_condition = tts_lm_outputs.last_hidden_state[diffusion_indices, -1, :]
negative_condition = tts_lm_negative_outputs.last_hidden_state[diffusion_indices, -1, :]
speech_latent = self.sample_speech_tokens(
positive_condition,
negative_condition,
cfg_scale=cfg_scale,
).unsqueeze(1)
# Decode acoustic latent to audio using acoustic streaming cache
scaled_latent = speech_latent / self.model.speech_scaling_factor.to(speech_latent.device) - self.model.speech_bias_factor.to(speech_latent.device)
audio_chunk = self.model.acoustic_tokenizer.decode(
scaled_latent.to(self.model.acoustic_tokenizer.device),
cache=acoustic_cache, # Use acoustic-specific cache
sample_indices=diffusion_indices.to(self.model.acoustic_tokenizer.device),
use_cache=True,
debug=False
)
# Store audio chunks for each sample
for i, sample_idx in enumerate(diffusion_indices):
idx = sample_idx.item()
# Only append audio chunk if the sample is not finished
if not finished_tags[idx]:
audio_chunks[idx].append(audio_chunk[i])
# Add streaming support here
if audio_streamer is not None:
# Stream the audio chunks immediately
audio_streamer.put(audio_chunk, diffusion_indices)
acoustic_embed = self.model.acoustic_connector(speech_latent)
tts_lm_input_ids = torch.cat([tts_lm_input_ids, torch.ones_like(tts_lm_input_ids[:, -1:])], dim=-1)
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
break
step += 1
total_generated_speech_tokens += 1
if progress_bar is not None:
progress_bar.update(1)
progress_bar.set_description(f"Prefilled {total_prefilled_text_tokens} text tokens, generated {total_generated_speech_tokens} speech tokens, current step ({step} / {tts_lm_generation_config.max_length})")
tts_lm_model_inputs = self.prepare_inputs_for_generation(tts_lm_input_ids, **tts_lm_model_kwargs)
tts_lm_additional_inputs = {
"tts_text_masks": torch.zeros_like(tts_lm_input_ids[:, -1:]),
"lm_last_hidden_state": acoustic_embed,
}
# Forward pass through the model
tts_lm_outputs = self.forward_tts_lm(
**tts_lm_model_inputs, **tts_lm_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
)
if cur_speech_index == TTS_SPEECH_WINDOW_SIZE - 1 and next_text_window_size > 0:
tts_lm_model_kwargs = _update_model_kwargs_for_generation(
tts_lm_outputs, tts_lm_model_kwargs, num_new_tokens=next_text_window_size,
)
else:
tts_lm_model_kwargs = self._update_model_kwargs_for_generation(
tts_lm_outputs, tts_lm_model_kwargs, is_encoder_decoder=False,
)
tts_lm_negative_input_ids = torch.cat([tts_lm_negative_input_ids, torch.ones_like(tts_lm_input_ids[:, -1:])], dim=-1)
tts_lm_negative_model_inputs = self.prepare_inputs_for_generation(tts_lm_negative_input_ids, **tts_lm_negative_model_kwargs)
# Forward negative pass through the model
tts_lm_negative_additional_inputs = {
"tts_text_masks": torch.zeros_like(tts_lm_negative_input_ids[:, -1:]),
"lm_last_hidden_state": acoustic_embed,
}
tts_lm_negative_outputs = self.forward_tts_lm(
**tts_lm_negative_model_inputs, **tts_lm_negative_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
)
tts_lm_negative_model_kwargs = self._update_model_kwargs_for_generation(
tts_lm_negative_outputs, tts_lm_negative_model_kwargs, is_encoder_decoder=False,
)
tts_eos_logits = torch.sigmoid(self.tts_eos_classifier(tts_lm_outputs.last_hidden_state[diffusion_indices, -1, :]))
if tts_eos_logits[0].item() > 0.5:
# If EOS token is predicted, we can stop generation for this sample
finished_tags[diffusion_indices] = True
if audio_streamer is not None:
audio_streamer.end(diffusion_indices)
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
if verbose:
print(f"Reached maximum generation length {tts_lm_generation_config.max_length}, stopped it.")
reached_samples = torch.arange(batch_size, device=device)[~finished_tags]
if reached_samples.numel() > 0:
reach_max_step_sample[reached_samples] = True
break
if audio_streamer is not None:
audio_streamer.end()
# Concatenate audio chunks for each sample
final_audio_outputs = []
for sample_chunks in audio_chunks:
if sample_chunks:
# Concatenate all chunks along the time dimension (assumed to be the last dimension)
concatenated_audio = torch.cat(sample_chunks, dim=-1)
final_audio_outputs.append(concatenated_audio)
else:
# If no audio was generated for this sample, append None
final_audio_outputs.append(None)
if reach_max_step_sample is not None and reach_max_step_sample.any():
print(f"Reached maximum generation length {tts_lm_generation_config.max_length}, stopped it.")
return VibeVoiceGenerationOutput(
sequences=tts_lm_input_ids,
speech_outputs=final_audio_outputs if return_speech else None,
reach_max_step_sample=reach_max_step_sample,
)
@torch.no_grad()
def sample_speech_tokens(self, condition, neg_condition, cfg_scale=3.0):
self.model.noise_scheduler.set_timesteps(self.ddpm_inference_steps)
condition = torch.cat([condition, neg_condition], dim=0).to(self.model.prediction_head.device)
speech = torch.randn(condition.shape[0], self.config.acoustic_vae_dim).to(condition)
for t in self.model.noise_scheduler.timesteps:
half = speech[: len(speech) // 2]
combined = torch.cat([half, half], dim=0)
eps = self.model.prediction_head(combined, t.repeat(combined.shape[0]).to(combined), condition=condition)
cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
half_eps = uncond_eps + cfg_scale * (cond_eps - uncond_eps)
eps = torch.cat([half_eps, half_eps], dim=0)
speech = self.model.noise_scheduler.step(eps, t, speech).prev_sample
return speech[: len(speech) // 2]
AutoModelForCausalLM.register(VibeVoiceStreamingConfig, VibeVoiceStreamingForConditionalGenerationInference)
__all__ = [
"VibeVoiceStreamingForConditionalGenerationInference",
]
@@ -0,0 +1,287 @@
import math
from typing import Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers.models.auto import AutoModel
from transformers.modeling_utils import PreTrainedModel
# from transformers.modeling_layers import GradientCheckpointingLayer
from transformers.activations import ACT2FN
from transformers.utils import logging
from .configuration_vibevoice import VibeVoiceDiffusionHeadConfig
logger = logging.get_logger(__name__)
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine=True, memory_efficient=False):
super().__init__()
self.dim = dim
self.eps = eps
self.elementwise_affine = elementwise_affine
if self.elementwise_affine:
self.weight = nn.Parameter(torch.ones(dim))
else:
self.register_parameter('weight', None)
def _norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
output = self._norm(x.float()).type_as(x)
if self.weight is not None:
output = output * self.weight
return output
def extra_repr(self) -> str:
return f'dim={self.dim}, eps={self.eps}, elementwise_affine={self.elementwise_affine}'
def modulate(x, shift, scale):
"""Apply modulation to input tensor."""
return x * (1 + scale) + shift
class TimestepEmbedder(nn.Module):
"""
Embeds scalar timesteps into vector representations.
Args:
hidden_size (`int`): Size of the output embedding
frequency_embedding_size (`int`, optional): Size of the intermediate frequency embedding
"""
def __init__(self, hidden_size, frequency_embedding_size=256):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(frequency_embedding_size, hidden_size, bias=False),
# nn.SiLU(),
ACT2FN['silu'],
nn.Linear(hidden_size, hidden_size, bias=False),
)
self.frequency_embedding_size = frequency_embedding_size
@staticmethod
def timestep_embedding(t, dim, max_period=10000):
"""
Create sinusoidal timestep embeddings.
Args:
t (`torch.Tensor`): A 1-D Tensor of N indices, one per batch element.
These may be fractional.
dim (`int`): The dimension of the output.
max_period (`int`, optional): Controls the minimum frequency of the embeddings.
Returns:
`torch.Tensor`: An [N, D] Tensor of positional embeddings.
"""
half = dim // 2
freqs = torch.exp(
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
).to(t.device)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
return embedding.to(t.dtype)
def forward(self, t):
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
t_emb = self.mlp(t_freq)
return t_emb
class FeedForwardNetwork(nn.Module):
"""
Standard feed-forward network with SwiGLU activation.
Args:
embed_dim (`int`): Input dimension
ffn_dim (`int`): Hidden dimension
"""
def __init__(
self,
embed_dim,
ffn_dim,
):
super().__init__()
self.embed_dim = embed_dim
self.gate_proj = nn.Linear(self.embed_dim, ffn_dim, bias=False)
self.up_proj = nn.Linear(self.embed_dim, ffn_dim, bias=False)
self.down_proj = nn.Linear(ffn_dim, self.embed_dim, bias=False)
self.act_fn = ACT2FN['silu'] # Using SiLU as the activation function
def forward(self, x):
gate = self.gate_proj(x)
up = self.up_proj(x)
# SwiGLU activation
# gate = F.silu(gate)
gate = self.act_fn(gate)
return self.down_proj(gate * up)
class HeadLayer(nn.Module):
"""
A layer in the diffusion head.
Args:
embed_dim (`int`): Input dimension
ffn_dim (`int`): Hidden dimension
cond_dim (`int`): Condition embedding dimension
norm_eps (`float`, optional): Epsilon for normalization
"""
def __init__(
self,
embed_dim,
ffn_dim,
cond_dim,
norm_eps=1e-5,
):
super().__init__()
self.embed_dim = embed_dim
self.cond_dim = cond_dim
self.ffn_dim = ffn_dim
self.ffn = FeedForwardNetwork(
self.embed_dim,
self.ffn_dim,
)
self.norm = RMSNorm(self.embed_dim, eps=norm_eps)
self.adaLN_modulation = nn.Sequential(
# nn.SiLU(),
ACT2FN['silu'],
nn.Linear(cond_dim, 3 * self.embed_dim, bias=False)
)
def forward(self, x, c):
shift_ffn, scale_ffn, gate_ffn = self.adaLN_modulation(c).chunk(3, dim=-1)
x = x + gate_ffn * self.ffn(modulate(self.norm(x), shift_ffn, scale_ffn))
return x
class FinalLayer(nn.Module):
"""
Final layer in the diffusion head.
Args:
hidden_size (`int`): Input dimension
output_size (`int`): Output dimension
cond_size (`int`): Condition embedding dimension
norm_eps (`float`, optional): Epsilon for normalization
"""
def __init__(self, hidden_size, output_size, cond_size, norm_eps=1e-5):
super().__init__()
self.norm_final = RMSNorm(hidden_size, eps=norm_eps, elementwise_affine=False)
self.linear = nn.Linear(hidden_size, output_size, bias=False)
self.adaLN_modulation = nn.Sequential(
# nn.SiLU(),
ACT2FN['silu'],
nn.Linear(cond_size, 2 * hidden_size, bias=False)
)
def forward(self, x, c):
shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
x = modulate(self.norm_final(x), shift, scale)
x = self.linear(x)
return x
class VibeVoiceDiffusionHead(PreTrainedModel):
"""
Diffusion head model for vibevoice.
Args:
config (`VibeVoiceDiffusionHeadConfig`): Model configuration
latent_size (`int`, optional): Size of the latent space. If not provided, uses `config.latent_size`.
"""
config_class = VibeVoiceDiffusionHeadConfig
supports_gradient_checkpointing = True
_supports_flash_attn_2 = True
_supports_sdpa = True
def __init__(
self,
config,
):
super().__init__(config)
self.config = config
self.cond_dim = config.hidden_size
latent_size = config.latent_size
self.noisy_images_proj = nn.Linear(latent_size, config.hidden_size, bias=False)
self.cond_proj = nn.Linear(config.hidden_size, self.cond_dim, bias=False)
self.t_embedder = TimestepEmbedder(self.cond_dim)
ffn_dim = int(config.hidden_size * config.head_ffn_ratio)
# Create the intermediate layers
self.layers = nn.ModuleList([
HeadLayer(
embed_dim=config.hidden_size,
ffn_dim=ffn_dim,
cond_dim=self.cond_dim,
norm_eps=config.rms_norm_eps
)
for _ in range(config.head_layers)
])
# Final layer for output
self.final_layer = FinalLayer(
hidden_size=config.hidden_size,
output_size=latent_size,
cond_size=self.cond_dim,
norm_eps=config.rms_norm_eps
)
self.initialize_weights()
def initialize_weights(self):
"""Initialize the weights of the model."""
# Initialize timestep embedder
nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
# Zero-out adaLN modulation layers
for layer in self.layers:
nn.init.constant_(layer.adaLN_modulation[-1].weight, 0)
# Zero-out output layers
nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
nn.init.constant_(self.final_layer.linear.weight, 0)
def forward(
self,
noisy_images,
timesteps,
condition,
):
"""
Forward pass of the prediction head.
Args:
noisy_images (`torch.Tensor`): Noisy images/latents to denoise
timesteps (`torch.Tensor`): Timesteps for diffusion
condition (`torch.Tensor`): Conditioning information
Returns:
`torch.Tensor`: The predicted noise/velocity
"""
x = self.noisy_images_proj(noisy_images)
t = self.t_embedder(timesteps)
condition = self.cond_proj(condition)
c = condition + t
for layer in self.layers:
x = layer(x, c)
x = self.final_layer(x, c)
return x
AutoModel.register(VibeVoiceDiffusionHeadConfig, VibeVoiceDiffusionHead)
__all__ = [
"VibeVoiceDiffusionHead",
]
@@ -0,0 +1,313 @@
"""Tokenization classes for vibevoice."""
from typing import List, Optional, Union
from transformers.utils import logging
from transformers.models.qwen2.tokenization_qwen2 import Qwen2Tokenizer
from transformers.models.qwen2.tokenization_qwen2_fast import Qwen2TokenizerFast
logger = logging.get_logger(__name__)
class VibeVoiceTextTokenizer(Qwen2Tokenizer):
"""
Construct a VibeVoice tokenizer. Based on the Qwen2 tokenizer with additional special tokens for speech.
Args:
vocab_file (`str`):
Path to the vocabulary file.
merges_file (`str`):
Path to the merges file.
errors (`str`, *optional*, defaults to `"replace"`):
Paradigm to follow when decoding bytes to UTF-8.
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The unknown token.
bos_token (`str`, *optional*):
The beginning of sequence token. Not used for vibevoice.
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The end of sequence token.
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The token used for padding.
add_special_tokens (`bool`, *optional*, defaults to `True`):
Whether or not to add special tokens when encoding.
"""
model_input_names = ["input_ids", "attention_mask"]
def __init__(
self,
vocab_file,
merges_file,
errors="replace",
unk_token="<|endoftext|>",
bos_token=None,
eos_token="<|endoftext|>",
pad_token="<|endoftext|>",
add_prefix_space=False,
add_special_tokens=True,
**kwargs,
):
super().__init__(
vocab_file=vocab_file,
merges_file=merges_file,
errors=errors,
unk_token=unk_token,
bos_token=bos_token,
eos_token=eos_token,
pad_token=pad_token,
add_prefix_space=add_prefix_space,
add_special_tokens=add_special_tokens,
**kwargs,
)
# Add VibeVoice-specific special tokens
self._add_vibevoice_special_tokens()
def _add_vibevoice_special_tokens(self):
"""Add VibeVoice-specific special tokens."""
special_tokens = {
"additional_special_tokens": [
"<|vision_start|>", # Speech start (reusing vision tokens)
"<|vision_end|>", # Speech end
"<|vision_pad|>", # Speech diffusion pad
]
}
num_added = self.add_special_tokens(special_tokens)
# Cache special token IDs
self._speech_start_id = self.convert_tokens_to_ids("<|vision_start|>")
self._speech_end_id = self.convert_tokens_to_ids("<|vision_end|>")
self._speech_diffusion_id = self.convert_tokens_to_ids("<|vision_pad|>")
self._eos_id = self.convert_tokens_to_ids('<|endoftext|>')
return num_added
@property
def eos_id(self) -> int:
"""ID of the end of sequence token."""
return self._eos_id
@property
def speech_start_id(self) -> int:
"""ID of the speech start token."""
return self._speech_start_id
@property
def speech_end_id(self) -> int:
"""ID of the speech end token."""
return self._speech_end_id
@property
def speech_diffusion_id(self) -> int:
"""ID of the speech diffusion token."""
return self._speech_diffusion_id
@property
def pad_id(self) -> int:
"""ID used for padding (returns -100 for loss masking)."""
return -100
class VibeVoiceTextTokenizerFast(Qwen2TokenizerFast):
"""
Construct a "fast" VibeVoice tokenizer (backed by HuggingFace's *tokenizers* library).
Based on the Qwen2 tokenizer with additional special tokens for speech.
Args:
vocab_file (`str`, *optional*):
Path to the vocabulary file.
merges_file (`str`, *optional*):
Path to the merges file.
tokenizer_file (`str`, *optional*):
Path to [tokenizers](https://github.com/huggingface/tokenizers) file.
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The unknown token.
bos_token (`str`, *optional*):
The beginning of sequence token. Not used for vibevoice.
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The end of sequence token.
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The token used for padding.
"""
model_input_names = ["input_ids", "attention_mask"]
def __init__(
self,
vocab_file=None,
merges_file=None,
tokenizer_file=None,
unk_token="<|endoftext|>",
bos_token=None,
eos_token="<|endoftext|>",
pad_token="<|endoftext|>",
add_prefix_space=False,
**kwargs,
):
super().__init__(
vocab_file=vocab_file,
merges_file=merges_file,
tokenizer_file=tokenizer_file,
unk_token=unk_token,
bos_token=bos_token,
eos_token=eos_token,
pad_token=pad_token,
add_prefix_space=add_prefix_space,
**kwargs,
)
# Add VibeVoice-specific special tokens
self._add_vibevoice_special_tokens()
def _add_vibevoice_special_tokens(self):
"""Add VibeVoice-specific special tokens."""
special_tokens = {
"additional_special_tokens": [
"<|vision_start|>", # Speech start (reusing vision tokens)
"<|vision_end|>", # Speech end
"<|vision_pad|>", # Speech diffusion pad
]
}
num_added = self.add_special_tokens(special_tokens)
# Cache special token IDs
self._speech_start_id = self.convert_tokens_to_ids("<|vision_start|>")
self._speech_end_id = self.convert_tokens_to_ids("<|vision_end|>")
self._speech_diffusion_id = self.convert_tokens_to_ids("<|vision_pad|>")
# self._eos_id = self.convert_tokens_to_ids('<|endoftext|>')
self._eos_id = self.eos_token_id # qwen2 / qwen3
self._pad_id = self.convert_tokens_to_ids('<|image_pad|>')
return num_added
@property
def eos_id(self) -> int:
"""ID of the end of sequence token."""
return self._eos_id
@property
def speech_start_id(self) -> int:
"""ID of the speech start token."""
return self._speech_start_id
@property
def speech_end_id(self) -> int:
"""ID of the speech end token."""
return self._speech_end_id
@property
def speech_diffusion_id(self) -> int:
"""ID of the speech diffusion token."""
return self._speech_diffusion_id
@property
def pad_id(self) -> int:
"""ID used for padding (returns -100 for loss masking)."""
return self._pad_id
class VibeVoiceASRTextTokenizerFast(Qwen2TokenizerFast):
"""
Construct a "fast" VibeVoice tokenizer (backed by HuggingFace's *tokenizers* library).
Based on the Qwen2 tokenizer with additional special tokens for speech.
Args:
vocab_file (`str`, *optional*):
Path to the vocabulary file.
merges_file (`str`, *optional*):
Path to the merges file.
tokenizer_file (`str`, *optional*):
Path to [tokenizers](https://github.com/huggingface/tokenizers) file.
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The unknown token.
bos_token (`str`, *optional*):
The beginning of sequence token. Not used for vibevoice.
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The end of sequence token.
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
The token used for padding.
"""
model_input_names = ["input_ids", "attention_mask"]
def __init__(
self,
vocab_file=None,
merges_file=None,
tokenizer_file=None,
unk_token="<|endoftext|>",
bos_token=None,
eos_token="<|endoftext|>",
pad_token="<|endoftext|>",
add_prefix_space=False,
**kwargs,
):
super().__init__(
vocab_file=vocab_file,
merges_file=merges_file,
tokenizer_file=tokenizer_file,
unk_token=unk_token,
bos_token=bos_token,
eos_token=eos_token,
pad_token=pad_token,
add_prefix_space=add_prefix_space,
**kwargs,
)
# Add VibeVoice-specific special tokens
self._add_vibevoice_special_tokens()
# https://github.com/QwenLM/Qwen2.5-VL/blob/d2240f11656bfe404b9ba56db4e51cd09f522ff1/qwen-vl-finetune/qwenvl/data/data_qwen_packed.py#L57C5-L57C222
self.chat_template = "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
def _add_vibevoice_special_tokens(self):
"""Add VibeVoice-specific special tokens."""
special_tokens = {
"additional_special_tokens": [
"<|object_ref_start|>", # Speech start (reusing vision tokens)
"<|object_ref_end|>", # Speech end
"<|box_start|>", # Speech diffusion pad
]
}
num_added = self.add_special_tokens(special_tokens)
# Cache special token IDs
self._speech_start_id = self.convert_tokens_to_ids("<|object_ref_start|>")
self._speech_end_id = self.convert_tokens_to_ids("<|object_ref_end|>")
self._speech_pad_id = self.convert_tokens_to_ids("<|box_start|>")
self._eos_id = self.eos_token_id # qwen2 / qwen3
self._pad_id = self.convert_tokens_to_ids('<|image_pad|>')
return num_added
@property
def eos_id(self) -> int:
"""ID of the end of sequence token."""
return self._eos_id
@property
def speech_start_id(self) -> int:
"""ID of the speech start token."""
return self._speech_start_id
@property
def speech_end_id(self) -> int:
"""ID of the speech end token."""
return self._speech_end_id
@property
def speech_pad_id(self) -> int:
"""ID of the speech diffusion token."""
return self._speech_pad_id
@property
def pad_id(self) -> int:
return self._pad_id
__all__ = [
"VibeVoiceTextTokenizer",
"VibeVoiceTextTokenizerFast",
"VibeVoiceASRTextTokenizerFast",
]
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
from __future__ import annotations
import torch
import asyncio
from queue import Empty, Queue
from typing import TYPE_CHECKING, Optional
from transformers.generation import BaseStreamer
class AudioStreamer(BaseStreamer):
"""
Audio streamer that stores audio chunks in queues for each sample in the batch.
This allows streaming audio generation for multiple samples simultaneously.
Parameters:
batch_size (`int`):
The batch size for generation
stop_signal (`any`, *optional*):
The signal to put in the queue when generation ends. Defaults to None.
timeout (`float`, *optional*):
The timeout for the audio queue. If `None`, the queue will block indefinitely.
"""
def __init__(
self,
batch_size: int,
stop_signal: Optional[any] = None,
timeout: Optional[float] = None,
):
self.batch_size = batch_size
self.stop_signal = stop_signal
self.timeout = timeout
# Create a queue for each sample in the batch
self.audio_queues = [Queue() for _ in range(batch_size)]
self.finished_flags = [False for _ in range(batch_size)]
self.sample_indices_map = {} # Maps from sample index to queue index
def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor):
"""
Receives audio chunks and puts them in the appropriate queues.
Args:
audio_chunks: Tensor of shape (num_samples, ...) containing audio chunks
sample_indices: Tensor indicating which samples these chunks belong to
"""
for i, sample_idx in enumerate(sample_indices):
idx = sample_idx.item()
if idx < self.batch_size and not self.finished_flags[idx]:
# Convert to numpy or keep as tensor based on preference
audio_chunk = audio_chunks[i].detach().cpu()
self.audio_queues[idx].put(audio_chunk, timeout=self.timeout)
def end(self, sample_indices: Optional[torch.Tensor] = None):
"""
Signals the end of generation for specified samples or all samples.
Args:
sample_indices: Optional tensor of sample indices to end. If None, ends all.
"""
if sample_indices is None:
# End all samples
for idx in range(self.batch_size):
if not self.finished_flags[idx]:
self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
self.finished_flags[idx] = True
else:
# End specific samples
for sample_idx in sample_indices:
idx = sample_idx.item() if torch.is_tensor(sample_idx) else sample_idx
if idx < self.batch_size and not self.finished_flags[idx]:
self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
self.finished_flags[idx] = True
def __iter__(self):
"""Returns an iterator over the batch of audio streams."""
return AudioBatchIterator(self)
def get_stream(self, sample_idx: int):
"""Get the audio stream for a specific sample."""
if sample_idx >= self.batch_size:
raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
return AudioSampleIterator(self, sample_idx)
class AudioSampleIterator:
"""Iterator for a single audio stream from the batch."""
def __init__(self, streamer: AudioStreamer, sample_idx: int):
self.streamer = streamer
self.sample_idx = sample_idx
def __iter__(self):
return self
def __next__(self):
value = self.streamer.audio_queues[self.sample_idx].get(timeout=self.streamer.timeout)
if value == self.streamer.stop_signal:
raise StopIteration()
return value
class AudioBatchIterator:
"""Iterator that yields audio chunks for all samples in the batch."""
def __init__(self, streamer: AudioStreamer):
self.streamer = streamer
self.active_samples = set(range(streamer.batch_size))
def __iter__(self):
return self
def __next__(self):
if not self.active_samples:
raise StopIteration()
batch_chunks = {}
samples_to_remove = set()
# Try to get chunks from all active samples
for idx in self.active_samples:
try:
value = self.streamer.audio_queues[idx].get(block=False)
if value == self.streamer.stop_signal:
samples_to_remove.add(idx)
else:
batch_chunks[idx] = value
except Empty:
# Queue is empty for this sample, skip it this iteration
pass
# Remove finished samples
self.active_samples -= samples_to_remove
if batch_chunks:
return batch_chunks
elif self.active_samples:
# If no chunks were ready but we still have active samples,
# wait a bit and try again
import time
time.sleep(0.01)
return self.__next__()
else:
raise StopIteration()
class AsyncAudioStreamer(AudioStreamer):
"""
Async version of AudioStreamer for use in async contexts.
"""
def __init__(
self,
batch_size: int,
stop_signal: Optional[any] = None,
timeout: Optional[float] = None,
):
super().__init__(batch_size, stop_signal, timeout)
# Replace regular queues with async queues
self.audio_queues = [asyncio.Queue() for _ in range(batch_size)]
self.loop = asyncio.get_running_loop()
def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor):
"""Put audio chunks in the appropriate async queues."""
for i, sample_idx in enumerate(sample_indices):
idx = sample_idx.item()
if idx < self.batch_size and not self.finished_flags[idx]:
audio_chunk = audio_chunks[i].detach().cpu()
self.loop.call_soon_threadsafe(
self.audio_queues[idx].put_nowait, audio_chunk
)
def end(self, sample_indices: Optional[torch.Tensor] = None):
"""Signal the end of generation for specified samples."""
if sample_indices is None:
indices_to_end = range(self.batch_size)
else:
indices_to_end = [s.item() if torch.is_tensor(s) else s for s in sample_indices]
for idx in indices_to_end:
if idx < self.batch_size and not self.finished_flags[idx]:
self.loop.call_soon_threadsafe(
self.audio_queues[idx].put_nowait, self.stop_signal
)
self.finished_flags[idx] = True
async def get_stream(self, sample_idx: int):
"""Get async iterator for a specific sample's audio stream."""
if sample_idx >= self.batch_size:
raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
while True:
value = await self.audio_queues[sample_idx].get()
if value == self.stop_signal:
break
yield value
def __aiter__(self):
"""Returns an async iterator over all audio streams."""
return AsyncAudioBatchIterator(self)
class AsyncAudioBatchIterator:
"""Async iterator for batch audio streaming."""
def __init__(self, streamer: AsyncAudioStreamer):
self.streamer = streamer
self.active_samples = set(range(streamer.batch_size))
def __aiter__(self):
return self
async def __anext__(self):
if not self.active_samples:
raise StopAsyncIteration()
batch_chunks = {}
samples_to_remove = set()
# Create tasks for all active samples
tasks = {
idx: asyncio.create_task(self._get_chunk(idx))
for idx in self.active_samples
}
# Wait for at least one chunk to be ready
done, pending = await asyncio.wait(
tasks.values(),
return_when=asyncio.FIRST_COMPLETED,
timeout=self.streamer.timeout
)
# Cancel pending tasks
for task in pending:
task.cancel()
# Process completed tasks
for idx, task in tasks.items():
if task in done:
try:
value = await task
if value == self.streamer.stop_signal:
samples_to_remove.add(idx)
else:
batch_chunks[idx] = value
except asyncio.CancelledError:
pass
self.active_samples -= samples_to_remove
if batch_chunks:
return batch_chunks
elif self.active_samples:
# Try again if we still have active samples
return await self.__anext__()
else:
raise StopAsyncIteration()
async def _get_chunk(self, idx):
"""Helper to get a chunk from a specific queue."""
return await self.streamer.audio_queues[idx].get()
+11
View File
@@ -0,0 +1,11 @@
# vibevoice/processor/__init__.py
from .vibevoice_processor import VibeVoiceProcessor
from .vibevoice_streaming_processor import VibeVoiceStreamingProcessor
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor, AudioNormalizer
__all__ = [
"VibeVoiceProcessor",
"VibeVoiceStreamingProcessor",
"VibeVoiceTokenizerProcessor",
"AudioNormalizer",
]
+217
View File
@@ -0,0 +1,217 @@
import os
import threading
import numpy as np
from subprocess import run
from typing import List, Optional, Union, Dict, Any
COMMON_AUDIO_EXTS = [
'.mp3', '.MP3', '.Mp3', # All case variations of mp3
'.m4a',
'.mp4', '.MP4',
'.wav', '.WAV',
'.m4v',
'.aac',
'.ogg',
'.mov', '.MOV',
'.opus',
'.m4b',
'.flac',
'.wma', '.WMA',
'.rm', '.3gp', '.mpeg', '.flv', '.webm', '.mp2', '.aif', '.aiff', '.oga', '.ogv', '.mpga', '.m3u8', '.amr'
]
def load_audio_use_ffmpeg(file: str, resample: bool = False, target_sr: int = 24000):
"""
Open an audio file and read as mono waveform, optionally resampling.
Returns both the audio data and the original sample rate.
Parameters
----------
file: str
The audio file to open
resample: bool
Whether to resample the audio
target_sr: int
The target sample rate if resampling is requested
Returns
-------
A tuple containing:
- A NumPy array with the audio waveform in float32 dtype
- The original sample rate of the audio file
"""
if not resample:
# First, get the original sample rate
cmd_probe = [
"ffprobe",
"-v", "quiet",
"-show_entries", "stream=sample_rate",
"-of", "default=noprint_wrappers=1:nokey=1",
file
]
original_sr = int(run(cmd_probe, capture_output=True, check=True).stdout.decode().strip())
else:
original_sr = None
# Now load the audio
sr_to_use = target_sr if resample else original_sr
cmd = [
"ffmpeg",
"-loglevel", "error",
"-nostdin",
"-threads", "0",
"-i", file,
"-f", "s16le",
"-ac", "1",
"-acodec", "pcm_s16le",
"-ar", str(sr_to_use),
"-",
]
out = _run_ffmpeg(cmd).stdout
audio_data = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
return audio_data, sr_to_use
def _get_ffmpeg_max_concurrency() -> int:
"""Get the maximum FFmpeg concurrency from environment variable."""
v = os.getenv("VIBEVOICE_FFMPEG_MAX_CONCURRENCY", "")
try:
n = int(v) if v.strip() else 0
except Exception:
n = 0
# 0/negative means no explicit limit.
return n
_FFMPEG_MAX_CONCURRENCY = _get_ffmpeg_max_concurrency()
_FFMPEG_SEM = threading.Semaphore(_FFMPEG_MAX_CONCURRENCY) if _FFMPEG_MAX_CONCURRENCY > 0 else None
def _run_ffmpeg(cmd: list, *, stdin_bytes: bytes = None):
"""Run ffmpeg with optional global concurrency limiting.
This is important for vLLM multi-request concurrency: spawning too many
ffmpeg processes can saturate CPU/IO and cause request failures/timeouts.
"""
if _FFMPEG_SEM is None:
return run(cmd, capture_output=True, check=True, input=stdin_bytes)
with _FFMPEG_SEM:
return run(cmd, capture_output=True, check=True, input=stdin_bytes)
def load_audio_bytes_use_ffmpeg(data: bytes, *, resample: bool = False, target_sr: int = 24000):
"""Decode audio bytes via ffmpeg stdin pipe.
Compared to writing bytes to a temp file, this avoids filesystem IO and
reduces contention under high request concurrency.
Parameters
----------
data: bytes
The audio data bytes
resample: bool
Whether to resample the audio (must be True)
target_sr: int
The target sample rate if resampling is requested
Returns
-------
A tuple containing:
- A NumPy array with the audio waveform in float32 dtype
- The sample rate
"""
if not resample:
# For stdin bytes, we don't have a cheap/robust way to probe original sr.
# Keep behavior explicit.
raise ValueError("load_audio_bytes_use_ffmpeg requires resample=True")
cmd = [
"ffmpeg",
"-loglevel", "error",
"-threads", "0",
"-i", "pipe:0",
"-f", "s16le",
"-ac", "1",
"-acodec", "pcm_s16le",
"-ar", str(target_sr),
"-",
]
out = _run_ffmpeg(cmd, stdin_bytes=data).stdout
audio_data = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
return audio_data, target_sr
class AudioNormalizer:
"""
Audio normalization class for VibeVoice tokenizer.
This class provides audio normalization to ensure consistent input levels
for the VibeVoice tokenizer while maintaining audio quality.
"""
def __init__(self, target_dB_FS: float = -25, eps: float = 1e-6):
"""
Initialize the audio normalizer.
Args:
target_dB_FS (float): Target dB FS level for the audio. Default: -25
eps (float): Small value to avoid division by zero. Default: 1e-6
"""
self.target_dB_FS = target_dB_FS
self.eps = eps
def tailor_dB_FS(self, audio: np.ndarray) -> tuple:
"""
Adjust the audio to the target dB FS level.
Args:
audio (np.ndarray): Input audio signal
Returns:
tuple: (normalized_audio, rms, scalar)
"""
rms = np.sqrt(np.mean(audio**2))
scalar = 10 ** (self.target_dB_FS / 20) / (rms + self.eps)
normalized_audio = audio * scalar
return normalized_audio, rms, scalar
def avoid_clipping(self, audio: np.ndarray, scalar: Optional[float] = None) -> tuple:
"""
Avoid clipping by scaling down if necessary.
Args:
audio (np.ndarray): Input audio signal
scalar (float, optional): Explicit scaling factor
Returns:
tuple: (normalized_audio, scalar)
"""
if scalar is None:
max_val = np.max(np.abs(audio))
if max_val > 1.0:
scalar = max_val + self.eps
else:
scalar = 1.0
return audio / scalar, scalar
def __call__(self, audio: np.ndarray) -> np.ndarray:
"""
Normalize the audio by adjusting to target dB FS and avoiding clipping.
Args:
audio (np.ndarray): Input audio signal
Returns:
np.ndarray: Normalized audio signal
"""
# First adjust to target dB FS
audio, _, _ = self.tailor_dB_FS(audio)
# Then avoid clipping
audio, _ = self.avoid_clipping(audio)
return audio
@@ -0,0 +1,572 @@
"""
Processor class for VibeVoice ASR models.
"""
import os
import json
import math
import warnings
from typing import List, Optional, Union, Dict, Any, Tuple
import numpy as np
import torch
from transformers.tokenization_utils_base import BatchEncoding
from transformers.utils import TensorType, logging
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor, AudioNormalizer
try:
from .audio_utils import load_audio_use_ffmpeg
HAS_FFMPEG_UTILS = True
except ImportError:
HAS_FFMPEG_UTILS = False
warnings.warn("audio_utils not available, will fall back to soundfile for audio loading")
logger = logging.get_logger(__name__)
SYSTEM_PROMPT = "You are a helpful assistant that transcribes audio input into text output in JSON format."
class VibeVoiceASRProcessor:
"""
Processor for VibeVoice ASR (Automatic Speech Recognition) models.
This processor handles audio preprocessing and tokenization for ASR tasks,
following the exact format used in training with proper chat templates.
Args:
tokenizer: The text tokenizer for processing text
audio_processor: The audio processor for processing speech
speech_tok_compress_ratio (int): Compression ratio for speech tokenization. Default: 3200 (product of encoder ratios [8,5,5,4,2,2])
target_sample_rate (int): Target sample rate for audio
normalize_audio (bool): Whether to normalize audio input
"""
def __init__(
self,
tokenizer=None,
audio_processor=None,
speech_tok_compress_ratio=3200,
target_sample_rate=24000,
normalize_audio=True,
**kwargs
):
self.tokenizer = tokenizer
self.audio_processor = audio_processor or VibeVoiceTokenizerProcessor(
sampling_rate=target_sample_rate,
normalize_audio=normalize_audio
)
self.speech_tok_compress_ratio = speech_tok_compress_ratio
self.target_sample_rate = target_sample_rate
self.normalize_audio = normalize_audio
if normalize_audio:
self.audio_normalizer = AudioNormalizer()
else:
self.audio_normalizer = None
# Cache special token IDs
self._cache_special_tokens()
def _cache_special_tokens(self):
"""Cache special token IDs for efficiency."""
# Add safety checks for special tokens
if hasattr(self.tokenizer, 'speech_start_id'):
self.speech_start_id = self.tokenizer.speech_start_id
else:
self.speech_start_id = self.tokenizer.convert_tokens_to_ids("<|speech_start|>")
if hasattr(self.tokenizer, 'speech_end_id'):
self.speech_end_id = self.tokenizer.speech_end_id
else:
self.speech_end_id = self.tokenizer.convert_tokens_to_ids("<|speech_end|>")
if hasattr(self.tokenizer, 'speech_pad_id'):
self.speech_pad_id = self.tokenizer.speech_pad_id
else:
self.speech_pad_id = self.tokenizer.convert_tokens_to_ids("<|speech_pad|>")
if hasattr(self.tokenizer, 'pad_id'):
self.pad_id = self.tokenizer.pad_id
elif hasattr(self.tokenizer, 'pad_token_id'):
self.pad_id = self.tokenizer.pad_token_id
else:
self.pad_id = self.tokenizer.convert_tokens_to_ids("<|endoftext|>")
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
"""
Load processor from a pretrained model path.
Args:
pretrained_model_name_or_path: Path to the pretrained model
**kwargs: Additional keyword arguments
Returns:
VibeVoiceASRProcessor: The loaded processor
"""
import json
from transformers.utils import cached_file
from vibevoice.modular.modular_vibevoice_text_tokenizer import VibeVoiceASRTextTokenizerFast
# Try to load configuration
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
config = {}
if os.path.exists(config_path):
with open(config_path, 'r') as f:
config = json.load(f)
else:
try:
config_file = cached_file(
pretrained_model_name_or_path,
"preprocessor_config.json",
**kwargs
)
with open(config_file, 'r') as f:
config = json.load(f)
except Exception as e:
logger.warning(f"Could not load preprocessor_config.json: {e}")
logger.warning("Using default configuration")
# Extract parameters
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
target_sample_rate = config.get("target_sample_rate", 24000)
normalize_audio = config.get("normalize_audio", True)
# Load tokenizer
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
if 'qwen' in language_model_pretrained_name.lower():
tokenizer = VibeVoiceASRTextTokenizerFast.from_pretrained(
language_model_pretrained_name,
**kwargs
)
else:
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}")
# Load audio processor
audio_processor = VibeVoiceTokenizerProcessor(
sampling_rate=target_sample_rate,
normalize_audio=normalize_audio,
target_dB_FS=config.get("target_dB_FS", -25),
eps=config.get("eps", 1e-6),
)
return cls(
tokenizer=tokenizer,
audio_processor=audio_processor,
speech_tok_compress_ratio=speech_tok_compress_ratio,
target_sample_rate=target_sample_rate,
normalize_audio=normalize_audio,
)
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
"""
Save processor configuration to a directory.
Args:
save_directory: Directory to save the configuration
**kwargs: Additional keyword arguments
"""
import json
os.makedirs(save_directory, exist_ok=True)
# Save processor configuration
processor_config = {
"processor_class": "VibeVoiceASRProcessor",
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
"target_sample_rate": self.target_sample_rate,
"normalize_audio": self.normalize_audio,
"target_dB_FS": -25,
"eps": 1e-6,
}
config_path = os.path.join(save_directory, "preprocessor_config.json")
with open(config_path, 'w') as f:
json.dump(processor_config, f, indent=2)
logger.info(f"Processor configuration saved in {config_path}")
def __call__(
self,
audio: Optional[Union[str, np.ndarray, torch.Tensor, List[Union[str, np.ndarray, torch.Tensor]]]] = None,
sampling_rate: Optional[int] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
padding: bool = True,
max_length: Optional[int] = None,
truncation: bool = False,
add_generation_prompt: bool = True,
use_streaming: bool = True,
context_info: Optional[str] = None,
**kwargs
) -> BatchEncoding:
"""
Process audio input for ASR model.
Args:
audio: Audio input(s). Can be:
- str: Path to audio file
- np.ndarray: Audio array
- torch.Tensor: Audio tensor
- List of the above for batch processing
sampling_rate: Sampling rate of input audio
return_tensors: Output format ('pt' for PyTorch, 'np' for NumPy)
padding: Whether to pad batch inputs
max_length: Maximum sequence length
truncation: Whether to truncate long sequences
add_generation_prompt: Whether to add generation prompt for inference
use_streaming: Whether to use streaming mode (True by default, auto False if <60s)
context_info: Optional context information (e.g., hotwords, metadata) to help transcription
Returns:
BatchEncoding with:
- input_ids: Token IDs for the model
- attention_mask: Attention mask
- acoustic_input_mask: Mask indicating speech token positions
- speech_tensors: Processed speech features
- speech_masks: Valid speech masks
- vae_tok_seqlens: Length of each speech segment in tokens
"""
if audio is None:
raise ValueError("Audio input is required for ASR processing")
# Handle single vs batch input
if isinstance(audio, list):
is_batched = True
audio_list = audio
else:
is_batched = False
audio_list = [audio]
# Process each audio input
all_encodings = []
for audio_input in audio_list:
encoding = self._process_single_audio(
audio_input,
sampling_rate=sampling_rate,
add_generation_prompt=add_generation_prompt,
use_streaming=use_streaming,
context_info=context_info,
)
all_encodings.append(encoding)
# Combine into batch
batch_encoding = self._batch_encode(
all_encodings,
padding=padding,
max_length=max_length,
truncation=truncation,
return_tensors=return_tensors,
)
return batch_encoding
def _process_single_audio(
self,
audio: Union[str, np.ndarray, torch.Tensor],
sampling_rate: Optional[int] = None,
add_generation_prompt: bool = True,
use_streaming: bool = True,
context_info: Optional[str] = None,
) -> Dict[str, Any]:
"""
Process a single audio input.
Args:
audio: Single audio input
sampling_rate: Audio sampling rate
add_generation_prompt: Whether to add generation prompt
context_info: Optional context information (e.g., hotwords, metadata) to help transcription
Returns:
Dictionary with processed tokens and audio features
"""
# Process audio through audio processor
if isinstance(audio, str):
# Load from file using ffmpeg for better format support
if HAS_FFMPEG_UTILS:
try:
audio_array, file_sr = load_audio_use_ffmpeg(audio, resample=False)
except Exception as e:
# Fall back to soundfile if ffmpeg fails
warnings.warn(f"ffmpeg loading failed, falling back to soundfile: {e}")
import soundfile as sf
audio_array, file_sr = sf.read(audio)
if audio_array.ndim > 1:
audio_array = audio_array.mean(axis=1) # Convert to mono
else:
import soundfile as sf
audio_array, file_sr = sf.read(audio)
if audio_array.ndim > 1:
audio_array = audio_array.mean(axis=1) # Convert to mono
# Resample if needed
if file_sr != self.target_sample_rate:
import librosa
audio_array = librosa.resample(
audio_array,
orig_sr=file_sr,
target_sr=self.target_sample_rate
)
elif isinstance(audio, torch.Tensor):
audio_array = audio.cpu().numpy()
if audio_array.ndim > 1:
audio_array = audio_array.squeeze()
else:
audio_array = np.array(audio, dtype=np.float32)
if audio_array.ndim > 1:
audio_array = audio_array.squeeze()
# Ensure float32
audio_array = audio_array.astype(np.float32)
# Normalize if needed
if self.normalize_audio and self.audio_normalizer:
audio_array = self.audio_normalizer(audio_array)
# Calculate audio duration
audio_duration = len(audio_array) / self.target_sample_rate
# Auto-disable streaming for short audio (<60s)
if use_streaming and audio_duration < 60.0:
use_streaming = False
# Calculate token length based on streaming mode
# Non-streaming: uses ceil (encoder adds extra_padding for stride alignment)
# Streaming: uses floor (segments processed independently, no global alignment)
# if use_streaming:
# vae_tok_len = len(audio_array) // self.speech_tok_compress_ratio
# else:
vae_tok_len = math.ceil(len(audio_array) / self.speech_tok_compress_ratio)
# Build token sequence following training format
# 1. System prompt - use apply_chat_template then encode like in training
system_prompt_text = self.tokenizer.apply_chat_template(
[{"role": "system", "content": SYSTEM_PROMPT}],
tokenize=False
)
system_tokens = self.tokenizer.encode(system_prompt_text)
# 2. User input with speech tokens
# Build speech placeholder string
sp_start_token = self.tokenizer.convert_ids_to_tokens(self.speech_start_id)
sp_pad_token = self.tokenizer.convert_ids_to_tokens(self.speech_pad_id)
sp_end_token = self.tokenizer.convert_ids_to_tokens(self.speech_end_id)
# User suffix with audio duration info
show_keys = ['Start time', 'End time', 'Speaker ID', 'Content']
if context_info and context_info.strip():
user_suffix = f"This is a {audio_duration:.2f} seconds audio, with extra info: {context_info.strip()}\n\nPlease transcribe it with these keys: " + ", ".join(show_keys)
else:
user_suffix = f"This is a {audio_duration:.2f} seconds audio, please transcribe it with these keys: " + ", ".join(show_keys)
user_input_string = ''.join(
[sp_start_token] + [sp_pad_token] * vae_tok_len + [sp_end_token]
) + '\n' + user_suffix
user_tokens = self.tokenizer.apply_chat_template(
[{"role": "user", "content": user_input_string}],
tokenize=True
)
# Combine tokens
full_tokens = system_tokens + user_tokens
# Create acoustic input mask
acoustic_input_mask = [1 if token == self.speech_pad_id else 0 for token in full_tokens]
return {
"input_ids": full_tokens,
"acoustic_input_mask": acoustic_input_mask,
"speech": audio_array,
"vae_tok_len": vae_tok_len,
}
def _batch_encode(
self,
encodings: List[Dict[str, Any]],
padding: bool = True,
max_length: Optional[int] = None,
truncation: bool = False,
return_tensors: Optional[str] = None,
) -> BatchEncoding:
"""
Combine multiple encodings into a batch.
Args:
encodings: List of encoded samples
padding: Whether to pad sequences
max_length: Maximum sequence length
truncation: Whether to truncate
return_tensors: Output format
Returns:
BatchEncoding with batched data
"""
# Extract components
input_ids_list = [enc["input_ids"] for enc in encodings]
acoustic_masks_list = [enc["acoustic_input_mask"] for enc in encodings]
speech_list = [enc["speech"] for enc in encodings]
vae_tok_lens = [enc["vae_tok_len"] for enc in encodings]
# Determine max length for padding
if padding:
if max_length is not None:
target_length = max_length
else:
target_length = max(len(ids) for ids in input_ids_list)
# Pad sequences
padded_input_ids = []
padded_acoustic_masks = []
attention_masks = []
for input_ids, acoustic_mask in zip(input_ids_list, acoustic_masks_list):
# Truncate if needed
if truncation and len(input_ids) > target_length:
input_ids = input_ids[:target_length]
acoustic_mask = acoustic_mask[:target_length]
# Pad sequences to left (for autoregressive generation)
padding_length = target_length - len(input_ids)
padded_ids = [self.pad_id] * padding_length + input_ids
padded_acoustic = [0] * padding_length + acoustic_mask
attention_mask = [0] * padding_length + [1] * len(input_ids)
padded_input_ids.append(padded_ids)
padded_acoustic_masks.append(padded_acoustic)
attention_masks.append(attention_mask)
input_ids_list = padded_input_ids
acoustic_masks_list = padded_acoustic_masks
else:
attention_masks = [[1] * len(ids) for ids in input_ids_list]
# Process speech tensors - raw audio is 1D, so we keep it as is
max_speech_length = max(len(s) for s in speech_list)
padded_speeches = np.zeros((len(speech_list), max_speech_length), dtype=np.float32)
speech_masks = np.zeros((len(speech_list), max(vae_tok_lens)), dtype=bool)
for i, (speech, vae_len) in enumerate(zip(speech_list, vae_tok_lens)):
padded_speeches[i, :len(speech)] = speech
speech_masks[i, :vae_len] = True
# Create batch encoding
batch_encoding = BatchEncoding()
if return_tensors == "pt":
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
batch_encoding["acoustic_input_mask"] = torch.tensor(acoustic_masks_list, dtype=torch.bool)
batch_encoding["speech_tensors"] = torch.tensor(padded_speeches, dtype=torch.float32)
batch_encoding["speech_masks"] = torch.tensor(speech_masks, dtype=torch.bool)
# Note: vae_tok_seqlens and speech_type are not included as they are not model inputs
else:
batch_encoding["input_ids"] = input_ids_list if len(input_ids_list) > 1 else input_ids_list[0]
batch_encoding["attention_mask"] = attention_masks if len(attention_masks) > 1 else attention_masks[0]
batch_encoding["acoustic_input_mask"] = acoustic_masks_list if len(acoustic_masks_list) > 1 else acoustic_masks_list[0]
batch_encoding["speech_tensors"] = padded_speeches if len(padded_speeches) > 1 else padded_speeches[0]
batch_encoding["speech_masks"] = speech_masks if len(speech_masks) > 1 else speech_masks[0]
return batch_encoding
def batch_decode(self, *args, **kwargs):
"""
Decode batch of token IDs to text.
Forwards to tokenizer's batch_decode method.
"""
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
"""
Decode token IDs to text.
Forwards to tokenizer's decode method.
"""
return self.tokenizer.decode(*args, **kwargs)
def post_process_transcription(self, text: str) -> List[Dict[str, Any]]:
"""
Post-process the generated transcription text to extract structured data.
Args:
text: Generated text from the model
Returns:
List of dictionaries with transcription segments
"""
try:
# Try to parse as JSON
if "```json" in text:
# Extract JSON from markdown code block
json_start = text.find("```json") + 7
json_end = text.find("```", json_start)
json_str = text[json_start:json_end].strip()
else:
# Try to find JSON array or object
json_start = text.find("[")
if json_start == -1:
json_start = text.find("{")
if json_start != -1:
# Find matching closing bracket
bracket_count = 0
json_end = json_start
for i in range(json_start, len(text)):
if text[i] in "[{":
bracket_count += 1
elif text[i] in "]}":
bracket_count -= 1
if bracket_count == 0:
json_end = i + 1
break
json_str = text[json_start:json_end]
else:
json_str = text
# Parse JSON
result = json.loads(json_str)
# Ensure it's a list
if isinstance(result, dict):
result = [result]
# Validate and clean up the result
cleaned_result = []
for item in result:
if isinstance(item, dict):
cleaned_item = {}
# Map keys to expected format
key_mapping = {
"Start time": "start_time",
"Start": "start_time",
"End time": "end_time",
"End": "end_time",
"Speaker ID": "speaker_id",
"Speaker": "speaker_id",
"Content": "text",
}
for key, mapped_key in key_mapping.items():
if key in item:
cleaned_item[mapped_key] = item[key]
if cleaned_item:
cleaned_result.append(cleaned_item)
return cleaned_result
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse JSON from transcription: {e}")
logger.debug(f"Raw text: {text}")
return []
except Exception as e:
logger.warning(f"Error post-processing transcription: {e}")
return []
@property
def model_input_names(self):
"""Return the list of inputs accepted by the model."""
return ["input_ids", "attention_mask", "acoustic_input_mask", "speech_tensors", "speech_masks"]
__all__ = ["VibeVoiceASRProcessor"]
+692
View File
@@ -0,0 +1,692 @@
import math
import warnings
from typing import List, Optional, Union, Dict, Any, Tuple
import os
import re
import numpy as np
import torch
from transformers.tokenization_utils_base import BatchEncoding, PaddingStrategy, PreTokenizedInput, TextInput, TruncationStrategy
from transformers.utils import TensorType, logging
from .vibevoice_tokenizer_processor import AudioNormalizer
logger = logging.get_logger(__name__)
class VibeVoiceProcessor:
r"""
Constructs a VibeVoice processor which wraps a VibeVoice tokenizer and audio processor into a single processor.
[`VibeVoiceProcessor`] offers all the functionalities of [`VibeVoiceTokenizer`] and [`VibeVoiceTokenizerProcessor`].
See the [`~VibeVoiceProcessor.__call__`] and [`~VibeVoiceProcessor.decode`] for more information.
Args:
tokenizer (`VibeVoiceTextTokenizer` or `VibeVoiceTextTokenizerFast`):
The tokenizer for text processing.
audio_processor (`VibeVoiceTokenizerProcessor`):
The audio processor for speech processing.
speech_tok_compress_ratio (`int`, *optional*, defaults to 3200):
The compression ratio for speech tokenization.
db_normalize (`bool`, *optional*, defaults to True):
Whether to apply decibel normalization to audio inputs.
"""
def __init__(self, tokenizer=None, audio_processor=None, speech_tok_compress_ratio=3200, db_normalize=True, **kwargs):
self.tokenizer = tokenizer
self.audio_processor = audio_processor
self.speech_tok_compress_ratio = speech_tok_compress_ratio
self.db_normalize = db_normalize
self.audio_normalizer = AudioNormalizer() if db_normalize else None
self.system_prompt = " Transform the text provided by various speakers into speech output, utilizing the distinct voice of each respective speaker.\n"
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
"""
Instantiate a VibeVoiceProcessor from a pretrained VibeVoice processor.
Args:
pretrained_model_name_or_path (`str` or `os.PathLike`):
This can be either:
- a string, the *model id* of a pretrained model
- a path to a *directory* containing processor config
Returns:
[`VibeVoiceProcessor`]: The processor object instantiated from pretrained model.
"""
import os
import json
from transformers.utils import cached_file
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor
from vibevoice.modular.modular_vibevoice_text_tokenizer import (
VibeVoiceTextTokenizer,
VibeVoiceTextTokenizerFast
)
# Try to load from local path first, then from HF hub
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
config = None
if os.path.exists(config_path):
# Local path exists
with open(config_path, 'r') as f:
config = json.load(f)
else:
# Try to load from HF hub
try:
config_file = cached_file(
pretrained_model_name_or_path,
"preprocessor_config.json",
**kwargs
)
with open(config_file, 'r') as f:
config = json.load(f)
except Exception as e:
logger.warning(f"Could not load preprocessor_config.json from {pretrained_model_name_or_path}: {e}")
logger.warning("Using default configuration")
config = {
"speech_tok_compress_ratio": 3200,
"db_normalize": True,
}
# Extract main processor parameters
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
db_normalize = config.get("db_normalize", True)
# Load tokenizer - try from model path first, then fall back to Qwen
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
if 'qwen' in language_model_pretrained_name.lower():
tokenizer = VibeVoiceTextTokenizerFast.from_pretrained(
language_model_pretrained_name,
**kwargs
)
else:
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}. Supported types: Qwen, Llama, Gemma.")
# Load audio processor
if "audio_processor" in config:
# Create audio processor from config
audio_config = config["audio_processor"]
audio_processor = VibeVoiceTokenizerProcessor(
sampling_rate=audio_config.get("sampling_rate", 24000),
normalize_audio=audio_config.get("normalize_audio", True),
target_dB_FS=audio_config.get("target_dB_FS", -25),
eps=audio_config.get("eps", 1e-6),
)
else:
# Create default audio processor
audio_processor = VibeVoiceTokenizerProcessor()
# Create and return the processor
return cls(
tokenizer=tokenizer,
audio_processor=audio_processor,
speech_tok_compress_ratio=speech_tok_compress_ratio,
db_normalize=db_normalize,
)
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
"""
Save a processor to a directory, so that it can be re-loaded using the
[`~VibeVoiceProcessor.from_pretrained`] class method.
Args:
save_directory (`str` or `os.PathLike`):
Directory where the processor will be saved.
"""
import os
import json
os.makedirs(save_directory, exist_ok=True)
# Save processor configuration
processor_config = {
"processor_class": "VibeVoiceProcessor",
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
"db_normalize": self.db_normalize,
"audio_processor": {
"feature_extractor_type": "VibeVoiceTokenizerProcessor",
"sampling_rate": getattr(self.audio_processor, 'sampling_rate', 24000),
"normalize_audio": getattr(self.audio_processor, 'normalize_audio', True),
"target_dB_FS": getattr(self.audio_processor, 'target_dB_FS', -25),
"eps": getattr(self.audio_processor, 'eps', 1e-6),
}
}
config_path = os.path.join(save_directory, "preprocessor_config.json")
with open(config_path, 'w') as f:
json.dump(processor_config, f, indent=2)
logger.info(f"Processor configuration saved in {config_path}")
def __call__(
self,
text: Optional[Union[str, List[str], TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]]] = None,
voice_samples: Optional[Union[List[Union[str, np.ndarray]], List[List[Union[str, np.ndarray]]]]] = None,
padding: Union[bool, str, PaddingStrategy] = True,
truncation: Union[bool, str, TruncationStrategy] = False,
max_length: Optional[int] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
return_attention_mask: bool = True,
**kwargs,
) -> BatchEncoding:
"""
Main method to process one or more podcast scripts with optional voice samples.
Args:
text (`str`, `List[str]`):
The input text(s) to process. Can be:
- A single script string
- A list of script strings for batch processing
- A path to a .json or .txt file
- A list of paths
voice_samples (`List[Union[str, np.ndarray]]`, `List[List[Union[str, np.ndarray]]]`, *optional*):
Voice samples for each script. Can be:
- A list of samples for a single script
- A list of lists for batch processing
padding (`bool`, `str` or `PaddingStrategy`, defaults to `True`):
Whether to pad sequences to the same length
truncation (`bool`, `str` or `TruncationStrategy`, defaults to `False`):
Whether to truncate sequences
max_length (`int`, *optional*):
Maximum length of the returned sequences
return_tensors (`str` or `TensorType`, *optional*):
If set, will return tensors of a particular framework
return_attention_mask (`bool`, defaults to `True`):
Whether to return the attention mask
Returns:
`BatchEncoding`: A BatchEncoding with the following fields:
- **input_ids** -- List of token id sequences or tensor
- **attention_mask** -- List of attention masks or tensor
- **speech_tensors** -- Padded speech inputs (if voice_samples provided)
- **speech_masks** -- Speech masks (if voice_samples provided)
- **speech_input_mask** -- Boolean masks indicating speech token positions
"""
# Handle single vs batch input
if isinstance(text, str) or (isinstance(text, list) and len(text) > 0 and not isinstance(text[0], str)):
# Single input
texts = [text]
is_batched = False
else:
# Batch input
texts = text
is_batched = True
# Handle voice samples
if voice_samples is not None:
if not is_batched or (isinstance(voice_samples[0], (str, np.ndarray))):
# Single set of voice samples
voice_samples_list = [voice_samples]
else:
# Batch of voice samples
voice_samples_list = voice_samples
else:
voice_samples_list = [None] * len(texts)
# Process each input
all_encodings = []
for text_input, voice_input in zip(texts, voice_samples_list):
encoding = self._process_single(text_input, voice_input)
all_encodings.append(encoding)
# Combine batch
batch_encoding = self._batch_encode(
all_encodings,
padding=padding,
truncation=truncation,
max_length=max_length,
return_tensors=return_tensors,
return_attention_mask=return_attention_mask,
)
return batch_encoding
def _process_single(
self,
text: Union[str, TextInput],
voice_samples: Optional[List[Union[str, np.ndarray]]] = None,
) -> Dict[str, Any]:
"""Process a single podcast script."""
# Determine if text is a file path or direct script
script = None
if isinstance(text, str):
# Check if it's a file path
if text.endswith('.json') and os.path.exists(text):
script = self._convert_json_to_script(text)
elif text.endswith('.txt') and os.path.exists(text):
script = self._convert_text_to_script(text)
else:
# Assume it's the script content directly
script = text
if script is None:
raise ValueError(f"Could not process input text: {text}")
# Parse the script
parsed_lines = self._parse_script(script)
all_speakers = list(set(speaker_id for speaker_id, _ in parsed_lines))
# Create system prompt
# system_tokens = self.tokenizer.encode(self.system_prompt, add_special_tokens=False)
system_tokens = self.tokenizer.encode(self.system_prompt)
# Process voice samples if provided
if voice_samples:
voice_tokens, voice_speech_inputs, voice_speech_masks = self._create_voice_prompt(voice_samples[:len(all_speakers)])
else:
voice_tokens, voice_speech_inputs, voice_speech_masks = [], [], []
# Build full token sequence
full_tokens = system_tokens + voice_tokens
speech_input_mask = [False] * len(system_tokens) + voice_speech_masks
# Add text input section
full_tokens += self.tokenizer.encode(' Text input:\n', add_special_tokens=False)
speech_input_mask += [False] * len(self.tokenizer.encode(' Text input:\n', add_special_tokens=False))
for speaker_id, speaker_text in parsed_lines:
speaker_text_tokens = self.tokenizer.encode(f" Speaker {speaker_id}:{speaker_text}\n", add_special_tokens=False)
full_tokens += speaker_text_tokens
speech_input_mask += [False] * len(speaker_text_tokens)
# Add speech output section
full_tokens += self.tokenizer.encode(' Speech output:\n', add_special_tokens=False) + [self.tokenizer.speech_start_id]
speech_input_mask += [False] * (len(self.tokenizer.encode(' Speech output:\n', add_special_tokens=False)) + 1)
return {
"input_ids": full_tokens,
"speech_inputs": voice_speech_inputs if voice_speech_inputs else None,
"speech_input_mask": speech_input_mask,
"parsed_script": parsed_lines,
"all_speakers": all_speakers,
}
def _batch_encode(
self,
encodings: List[Dict[str, Any]],
padding: Union[bool, str, PaddingStrategy] = True,
truncation: Union[bool, str, TruncationStrategy] = False,
max_length: Optional[int] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
return_attention_mask: bool = True,
) -> BatchEncoding:
"""Combine multiple encodings into a batch with padding."""
# Extract input_ids and create attention_mask
input_ids_list = [enc["input_ids"] for enc in encodings]
speech_input_masks_list = [enc["speech_input_mask"] for enc in encodings]
# Determine padding strategy
if isinstance(padding, bool):
padding_strategy = PaddingStrategy.LONGEST if padding else PaddingStrategy.DO_NOT_PAD
elif isinstance(padding, str):
padding_strategy = PaddingStrategy(padding)
else:
padding_strategy = padding
# Apply padding to input_ids
if padding_strategy != PaddingStrategy.DO_NOT_PAD:
if padding_strategy == PaddingStrategy.LONGEST:
max_len = max(len(ids) for ids in input_ids_list)
elif padding_strategy == PaddingStrategy.MAX_LENGTH and max_length is not None:
max_len = max_length
else:
max_len = max(len(ids) for ids in input_ids_list)
# Pad sequences
padded_input_ids = []
attention_masks = []
padded_speech_input_masks = []
for input_ids, speech_mask in zip(input_ids_list, speech_input_masks_list):
# Truncate if needed
if truncation and len(input_ids) > max_len:
input_ids = input_ids[:max_len]
speech_mask = speech_mask[:max_len]
# Pad
padding_length = max_len - len(input_ids)
# padded_ids = [self.tokenizer.pad_token_id] * padding_length + input_ids
padded_ids = [self.tokenizer.pad_id] * padding_length + input_ids
attention_mask = [0] * padding_length + [1] * len(input_ids)
padded_speech_mask = [False] * padding_length + speech_mask
padded_input_ids.append(padded_ids)
attention_masks.append(attention_mask)
padded_speech_input_masks.append(padded_speech_mask)
input_ids_list = padded_input_ids
speech_input_masks_list = padded_speech_input_masks
else:
# No padding, just create attention masks
attention_masks = [[1] * len(ids) for ids in input_ids_list] if return_attention_mask else None
# Process speech inputs
all_speech_inputs = []
has_speech = False
for enc in encodings:
if enc["speech_inputs"] is not None:
all_speech_inputs.extend(enc["speech_inputs"])
has_speech = True
# Prepare batch encoding
batch_encoding = BatchEncoding()
# Handle tensor conversion
if return_tensors is not None:
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
if return_attention_mask and attention_masks is not None:
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
batch_encoding["speech_input_mask"] = torch.tensor(speech_input_masks_list, dtype=torch.bool)
else:
batch_encoding["input_ids"] = input_ids_list
if return_attention_mask and attention_masks is not None:
batch_encoding["attention_mask"] = attention_masks
batch_encoding["speech_input_mask"] = speech_input_masks_list
# Process speech tensors if present
if has_speech:
speech_dict = self.prepare_speech_inputs(
all_speech_inputs,
return_tensors=return_tensors,
)
batch_encoding["speech_tensors"] = speech_dict["padded_speeches"]
batch_encoding["speech_masks"] = speech_dict["speech_masks"]
else:
batch_encoding["speech_tensors"] = None
batch_encoding["speech_masks"] = None
# Add metadata
batch_encoding["parsed_scripts"] = [enc["parsed_script"] for enc in encodings]
batch_encoding["all_speakers_list"] = [enc["all_speakers"] for enc in encodings]
return batch_encoding
def _create_voice_prompt(
self,
speaker_samples: List[Union[str, np.ndarray]]
) -> Tuple[List[int], List[np.ndarray], List[bool]]:
"""
Create voice prompt tokens and process audio samples.
Returns:
tuple: (voice_tokens, voice_speech_inputs, voice_speech_masks)
"""
vae_token_id = self.tokenizer.speech_diffusion_id
voice_full_tokens = self.tokenizer.encode(' Voice input:\n', add_special_tokens=False)
voice_speech_inputs = []
voice_speech_masks = [False] * len(voice_full_tokens)
for speaker_id, speaker_audio in enumerate(speaker_samples):
prefix_tokens = self.tokenizer.encode(f" Speaker {speaker_id}:", add_special_tokens=False)
# Process audio
if isinstance(speaker_audio, str):
# Load audio from file
wav = self.audio_processor._load_audio_from_path(speaker_audio)
else:
wav = np.array(speaker_audio, dtype=np.float32)
# Apply normalization if needed
if self.db_normalize and self.audio_normalizer:
wav = self.audio_normalizer(wav)
# Calculate token length based on compression ratio
# if speaker_audio.endswith('.pt') or speaker_audio.endswith('.npy'):
# vae_tok_len = wav.shape[0]
# else:
vae_tok_len = math.ceil(wav.shape[0] / self.speech_tok_compress_ratio)
# Build tokens and masks
speaker_tokens = (prefix_tokens +
[self.tokenizer.speech_start_id] +
[vae_token_id] * vae_tok_len +
[self.tokenizer.speech_end_id] +
self.tokenizer.encode('\n', add_special_tokens=False))
vae_input_mask = ([False] * len(prefix_tokens) +
[False] +
[True] * vae_tok_len +
[False] +
[False])
voice_full_tokens.extend(speaker_tokens)
voice_speech_masks.extend(vae_input_mask)
voice_speech_inputs.append(wav)
return voice_full_tokens, voice_speech_inputs, voice_speech_masks
def prepare_speech_inputs(
self,
speech_inputs: List[np.ndarray],
return_tensors: Optional[Union[str, TensorType]] = None,
device: Optional[Union[str, torch.device]] = None,
dtype: Optional[torch.dtype] = None,
) -> Dict[str, Any]:
"""
Prepare speech inputs for model consumption.
Args:
speech_inputs: List of speech arrays
return_tensors: Output tensor type
device: Device to place tensors on
dtype: Data type for tensors
Returns:
Dictionary with padded_speeches and speech_masks
"""
if not speech_inputs:
return {"padded_speeches": None, "speech_masks": None}
# Calculate sequence lengths
vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) for s in speech_inputs]
# vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) if s.ndim == 1 else s.shape[0] for s in speech_inputs]
max_speech_length = max(s.shape[0] for s in speech_inputs)
# Pad speeches
if speech_inputs[0].ndim == 1:
padded_speeches = np.full((len(speech_inputs), max_speech_length), fill_value=0, dtype=np.float32)
else:
padded_speeches = np.full((len(speech_inputs), max_speech_length, speech_inputs[0].shape[-1]), fill_value=0, dtype=np.float32)
speech_masks = np.zeros((len(speech_inputs), max(vae_tok_seqlens)), dtype=np.bool_)
for i, (speech, vae_tok_length) in enumerate(zip(speech_inputs, vae_tok_seqlens)):
padded_speeches[i, :len(speech)] = speech
speech_masks[i, :vae_tok_length] = True
result = {
"padded_speeches": padded_speeches,
"speech_masks": speech_masks,
}
# Convert to tensors if requested
if return_tensors == "pt":
result["padded_speeches"] = torch.tensor(padded_speeches, device=device, dtype=dtype or torch.float32)
result["speech_masks"] = torch.tensor(speech_masks, device=device, dtype=torch.bool)
return result
def _convert_json_to_script(self, json_file: str) -> str:
"""
Convert JSON format to script format.
Expected JSON format:
[
{"speaker": "1", "text": "Hello everyone..."},
{"speaker": "2", "text": "Great to be here..."}
]
"""
import json
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("JSON file must contain a list of speaker entries")
script_lines = []
for item in data:
if not isinstance(item, dict):
logger.warning(f"Skipping non-dict entry: {item}")
continue
speaker = item.get('speaker')
text = item.get('text')
if speaker is None or text is None:
logger.warning(f"Skipping entry missing speaker or text: {item}")
continue
# Ensure speaker ID is valid
try:
speaker_id = int(speaker)
except (ValueError, TypeError):
logger.warning(f"Invalid speaker ID: {speaker}, skipping entry")
continue
# Clean up text
text = text.strip()
if text:
script_lines.append(f"Speaker {speaker_id}: {text}")
if not script_lines:
raise ValueError("No valid entries found in JSON file")
return "\n".join(script_lines)
def _convert_text_to_script(self, text_file: str) -> str:
"""
Convert text file to script format.
Handles multiple formats:
1. Already formatted as "Speaker X: text"
2. Plain text (assigns to Speaker 1)
Handles edge cases like multiple colons in a line.
"""
with open(text_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
script_lines = []
current_speaker = 1
for line in lines:
line = line.strip()
if not line:
continue
# Try to parse as "Speaker X: text" format
# Use regex to be more robust
speaker_match = re.match(r'^Speaker\s+(\d+)\s*:\s*(.*)$', line, re.IGNORECASE)
if speaker_match:
speaker_id = int(speaker_match.group(1))
text = speaker_match.group(2).strip()
if text:
script_lines.append(f"Speaker {speaker_id}: {text}")
else:
# Treat as plain text - assign to current speaker
script_lines.append(f"Speaker {current_speaker}: {line}")
if not script_lines:
raise ValueError("No valid content found in text file")
return "\n".join(script_lines)
def _parse_script(self, script: str) -> List[Tuple[int, str]]:
"""Parse script into list of (speaker_id, text) tuples."""
lines = script.strip().split("\n")
parsed_lines = []
speaker_ids = []
# First pass: parse all lines and collect speaker IDs
for line in lines:
if not line.strip():
continue
# Use regex to handle edge cases like multiple colons
match = re.match(r'^Speaker\s+(\d+)\s*:\s*(.*)$', line.strip(), re.IGNORECASE)
if match:
speaker_id = int(match.group(1))
text = ' ' + match.group(2).strip()
parsed_lines.append((speaker_id, text))
speaker_ids.append(speaker_id)
else:
logger.warning(f"Could not parse line: '{line}'")
if not parsed_lines:
raise ValueError("No valid speaker lines found in script")
# Check if we need to normalize speaker IDs (only if all are > 0)
min_speaker_id = min(speaker_ids)
if min_speaker_id > 0:
# Normalize to start from 0
normalized_lines = []
for speaker_id, text in parsed_lines:
normalized_lines.append((speaker_id - 1, text))
return normalized_lines
else:
# Keep original IDs
return parsed_lines
def _merge_inputs(self, text_inputs: BatchEncoding, audio_inputs: Dict) -> BatchEncoding:
"""Merge text and audio inputs into a single BatchEncoding."""
# Start with text inputs
merged = BatchEncoding(text_inputs)
# Add audio-specific fields
if "audio" in audio_inputs:
merged["speech_inputs"] = audio_inputs["audio"]
if "streaming" in audio_inputs:
merged["streaming"] = audio_inputs["streaming"]
return merged
def batch_decode(self, *args, **kwargs):
"""
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.batch_decode`].
Please refer to the docstring of this method for more information.
"""
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
"""
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.decode`].
Please refer to the docstring of this method for more information.
"""
return self.tokenizer.decode(*args, **kwargs)
@property
def model_input_names(self):
"""
Return the list of inputs accepted by the model.
"""
tokenizer_input_names = self.tokenizer.model_input_names
audio_processor_input_names = self.audio_processor.model_input_names
return list(dict.fromkeys(tokenizer_input_names + audio_processor_input_names + ["speech_inputs", "speech_input_mask"]))
def save_audio(self,
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
output_path: str = "output.wav",
sampling_rate: Optional[int] = None,
normalize: bool = False,
batch_prefix: str = "audio_",
) -> str:
"""
Save audio data to a file.
Args:
audio (Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]]):
The audio data to save. Can be a single tensor/array or a list of them.
output_path (str, optional): Path to save the audio file. Defaults to "output.wav".
sampling_rate (int, optional): Sampling rate for the audio. If None, uses the processor's default.
normalize (bool, optional): Whether to normalize the audio before saving. Defaults to False.
batch_prefix (str, optional): Prefix for batch audio files. Defaults to "audio_".
Returns:
str: The path to the saved audio file.
"""
return self.audio_processor.save_audio(audio, output_path=output_path, sampling_rate=sampling_rate, normalize=normalize, batch_prefix=batch_prefix)
__all__ = [
"VibeVoiceProcessor",
]
@@ -0,0 +1,409 @@
import math
import warnings
from typing import List, Optional, Union, Dict, Any, Tuple
import os
import re
import numpy as np
import torch
from transformers.tokenization_utils_base import BatchEncoding, PaddingStrategy, PreTokenizedInput, TextInput, TruncationStrategy
from transformers.utils import TensorType, logging
from .vibevoice_tokenizer_processor import AudioNormalizer
logger = logging.get_logger(__name__)
class VibeVoiceStreamingProcessor:
r"""
Constructs a VibeVoice Streaming processor which wraps a VibeVoice tokenizer and audio processor into a single processor.
Args:
tokenizer (`VibeVoiceTextTokenizer` or `VibeVoiceTextTokenizerFast`):
The tokenizer for text processing.
audio_processor (`VibeVoiceTokenizerProcessor`):
The audio processor for speech processing.
speech_tok_compress_ratio (`int`, *optional*, defaults to 3200):
The compression ratio for speech tokenization.
db_normalize (`bool`, *optional*, defaults to True):
Whether to apply decibel normalization to audio inputs.
"""
def __init__(self, tokenizer=None, audio_processor=None, speech_tok_compress_ratio=3200, db_normalize=True, **kwargs):
self.tokenizer = tokenizer
self.audio_processor = audio_processor
self.speech_tok_compress_ratio = speech_tok_compress_ratio
self.db_normalize = db_normalize
self.audio_normalizer = AudioNormalizer() if db_normalize else None
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
"""
Instantiate a VibeVoiceStreamingProcessor from a pretrained VibeVoice Streaming processor.
Args:
pretrained_model_name_or_path (`str` or `os.PathLike`):
This can be either:
- a string, the *model id* of a pretrained model
- a path to a *directory* containing processor config
Returns:
[`VibeVoiceStreamingProcessor`]: The processor object instantiated from pretrained model.
"""
import os
import json
from transformers.utils import cached_file
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor
from vibevoice.modular.modular_vibevoice_text_tokenizer import (
VibeVoiceTextTokenizer,
VibeVoiceTextTokenizerFast
)
# Try to load from local path first, then from HF hub
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
config = None
if os.path.exists(config_path):
# Local path exists
with open(config_path, 'r') as f:
config = json.load(f)
else:
# Try to load from HF hub
try:
config_file = cached_file(
pretrained_model_name_or_path,
"preprocessor_config.json",
**kwargs
)
with open(config_file, 'r') as f:
config = json.load(f)
except Exception as e:
logger.warning(f"Could not load preprocessor_config.json from {pretrained_model_name_or_path}: {e}")
logger.warning("Using default configuration")
config = {
"speech_tok_compress_ratio": 3200,
"db_normalize": True,
}
# Extract main processor parameters
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
db_normalize = config.get("db_normalize", True)
# Load tokenizer - try from model path first, then fall back to Qwen
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
if 'qwen' in language_model_pretrained_name.lower():
tokenizer = VibeVoiceTextTokenizerFast.from_pretrained(
language_model_pretrained_name,
**kwargs
)
else:
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}. Supported types: Qwen, Llama, Gemma.")
# Load audio processor
if "audio_processor" in config:
# Create audio processor from config
audio_config = config["audio_processor"]
audio_processor = VibeVoiceTokenizerProcessor(
sampling_rate=audio_config.get("sampling_rate", 24000),
normalize_audio=audio_config.get("normalize_audio", True),
target_dB_FS=audio_config.get("target_dB_FS", -25),
eps=audio_config.get("eps", 1e-6),
)
else:
# Create default audio processor
audio_processor = VibeVoiceTokenizerProcessor()
# Create and return the processor
return cls(
tokenizer=tokenizer,
audio_processor=audio_processor,
speech_tok_compress_ratio=speech_tok_compress_ratio,
db_normalize=db_normalize,
)
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
"""
Save a processor to a directory, so that it can be re-loaded using the
[`~VibeVoiceStreamingProcessor.from_pretrained`] class method.
Args:
save_directory (`str` or `os.PathLike`):
Directory where the processor will be saved.
"""
import os
import json
os.makedirs(save_directory, exist_ok=True)
# Save processor configuration
processor_config = {
"processor_class": "VibeVoiceStreamingProcessor",
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
"db_normalize": self.db_normalize,
"audio_processor": {
"feature_extractor_type": "VibeVoiceTokenizerProcessor",
"sampling_rate": getattr(self.audio_processor, 'sampling_rate', 24000),
"normalize_audio": getattr(self.audio_processor, 'normalize_audio', True),
"target_dB_FS": getattr(self.audio_processor, 'target_dB_FS', -25),
"eps": getattr(self.audio_processor, 'eps', 1e-6),
}
}
config_path = os.path.join(save_directory, "preprocessor_config.json")
with open(config_path, 'w') as f:
json.dump(processor_config, f, indent=2)
logger.info(f"Processor configuration saved in {config_path}")
def __call__(self) -> BatchEncoding:
"""
Note:
This method is intentionally not implemented in the streaming processor.
Use `process_input_with_cached_prompt` for streaming use cases.
"""
raise NotImplementedError(
"VibeVoiceStreamingProcessor.__call__ is not implemented. "
"Use process_input_with_cached_prompt for streaming inputs."
)
def process_input_with_cached_prompt(
self,
text: Optional[str] = None,
cached_prompt: Optional[Dict[str, Any]] = None,
padding: Union[bool, str, PaddingStrategy] = True,
truncation: Union[bool, str, TruncationStrategy] = False,
max_length: Optional[int] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
return_attention_mask: bool = True,
**kwargs,
) -> BatchEncoding:
"""
Main method to process one text script based on cached prompt. The function currently only supports single examples.
Args:
text (`str`):
The input text to process.
cached_prompt (`Dict[str, Any]`, *optional*):
The cached prompt to use for processing. It contains the kv cache of the voice prompt.
padding (`bool`, `str` or `PaddingStrategy`, defaults to `True`):
Whether to pad sequences to the same length
truncation (`bool`, `str` or `TruncationStrategy`, defaults to `False`):
Whether to truncate sequences
max_length (`int`, *optional*):
Maximum length of the returned sequences
return_tensors (`str` or `TensorType`, *optional*):
If set, will return tensors of a particular framework
return_attention_mask (`bool`, defaults to `True`):
Whether to return the attention mask
Returns:
`BatchEncoding`: A BatchEncoding with the following fields:
- **input_ids** -- List of token id sequences or tensor
- **attention_mask** -- List of attention masks or tensor
- **tts_lm_input_ids** -- List of token id sequences or tensor used for TTS LM
- **tts_lm_attention_mask** -- List of attention masks or tensor used for TTS LM
- **tts_text_ids** -- List of token id sequences or tensor for TTS text input
- **speech_tensors** -- Padded speech inputs (if voice_samples provided)
- **speech_masks** -- Speech masks (if voice_samples provided)
- **speech_input_mask** -- Boolean masks indicating speech token positions
"""
# Only support single example
texts = [text]
cached_prompts = [cached_prompt]
is_batched = False
# Process each input
all_encodings = []
for text_input, cached_prompt_input in zip(texts, cached_prompts):
script_tokens = self.tokenizer.encode(text_input.strip() + "\n", add_special_tokens=False)
input_id_length = cached_prompt_input['lm']['last_hidden_state'].size(1)
tts_lm_input_id_length = cached_prompt_input['tts_lm']['last_hidden_state'].size(1)
# pseudo input ids and masks
input_ids = [self.tokenizer.pad_id] * input_id_length
tts_lm_input_ids = [self.tokenizer.pad_id] * tts_lm_input_id_length
speech_input_mask = [False] * tts_lm_input_id_length
encoding = {
"input_ids": input_ids,
"tts_lm_input_ids": tts_lm_input_ids,
"tts_text_ids": script_tokens,
"speech_inputs": None,
"speech_input_mask": speech_input_mask,
}
all_encodings.append(encoding)
# Combine batch
batch_encoding = self._batch_encode(
all_encodings,
padding=padding,
truncation=truncation,
max_length=max_length,
return_tensors=return_tensors,
return_attention_mask=return_attention_mask,
)
return batch_encoding
def _batch_encode(
self,
encodings: List[Dict[str, Any]],
padding: Union[bool, str, PaddingStrategy] = True,
truncation: Union[bool, str, TruncationStrategy] = False,
max_length: Optional[int] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
return_attention_mask: bool = True,
) -> BatchEncoding:
"""Combine multiple encodings into a batch with padding."""
# Extract input_ids and create attention_mask
input_ids_list = [enc["input_ids"] for enc in encodings]
tts_lm_input_ids_list = [enc["tts_lm_input_ids"] for enc in encodings]
tts_text_ids_list = [enc["tts_text_ids"] for enc in encodings]
speech_input_masks_list = [enc["speech_input_mask"] for enc in encodings]
attention_masks = [[1] * len(ids) for ids in input_ids_list] if return_attention_mask else None
tts_lm_attention_masks = [[1] * len(ids) for ids in tts_lm_input_ids_list] if return_attention_mask else None
# Process speech inputs
all_speech_inputs = []
has_speech = False
for enc in encodings:
if enc["speech_inputs"] is not None:
all_speech_inputs.extend(enc["speech_inputs"])
has_speech = True
# Prepare batch encoding
batch_encoding = BatchEncoding()
# Handle tensor conversion
if return_tensors is not None:
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
batch_encoding["tts_lm_input_ids"] = torch.tensor(tts_lm_input_ids_list, dtype=torch.long)
batch_encoding["tts_text_ids"] = torch.tensor(tts_text_ids_list, dtype=torch.long)
if return_attention_mask and attention_masks is not None:
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
batch_encoding["tts_lm_attention_mask"] = torch.tensor(tts_lm_attention_masks, dtype=torch.long)
batch_encoding["speech_input_mask"] = torch.tensor(speech_input_masks_list, dtype=torch.bool)
else:
batch_encoding["input_ids"] = input_ids_list
batch_encoding["tts_lm_input_ids"] = tts_lm_input_ids_list
batch_encoding["tts_text_ids"] = tts_text_ids_list
if return_attention_mask and attention_masks is not None:
batch_encoding["attention_mask"] = attention_masks
batch_encoding["tts_lm_attention_mask"] = tts_lm_attention_masks
batch_encoding["speech_input_mask"] = speech_input_masks_list
# Process speech tensors if present
if has_speech:
speech_dict = self.prepare_speech_inputs(
all_speech_inputs,
return_tensors=return_tensors,
)
batch_encoding["speech_tensors"] = speech_dict["padded_speeches"]
batch_encoding["speech_masks"] = speech_dict["speech_masks"]
else:
batch_encoding["speech_tensors"] = None
batch_encoding["speech_masks"] = None
return batch_encoding
def prepare_speech_inputs(
self,
speech_inputs: List[np.ndarray],
return_tensors: Optional[Union[str, TensorType]] = None,
device: Optional[Union[str, torch.device]] = None,
dtype: Optional[torch.dtype] = None,
) -> Dict[str, Any]:
"""
Prepare speech inputs for model consumption.
Args:
speech_inputs: List of speech arrays
return_tensors: Output tensor type
device: Device to place tensors on
dtype: Data type for tensors
Returns:
Dictionary with padded_speeches and speech_masks
"""
if not speech_inputs:
return {"padded_speeches": None, "speech_masks": None}
# Calculate sequence lengths
vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) for s in speech_inputs]
# vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) if s.ndim == 1 else s.shape[0] for s in speech_inputs]
max_speech_length = max(s.shape[0] for s in speech_inputs)
# Pad speeches
if speech_inputs[0].ndim == 1:
padded_speeches = np.full((len(speech_inputs), max_speech_length), fill_value=0, dtype=np.float32)
else:
padded_speeches = np.full((len(speech_inputs), max_speech_length, speech_inputs[0].shape[-1]), fill_value=0, dtype=np.float32)
speech_masks = np.zeros((len(speech_inputs), max(vae_tok_seqlens)), dtype=np.bool_)
for i, (speech, vae_tok_length) in enumerate(zip(speech_inputs, vae_tok_seqlens)):
padded_speeches[i, :len(speech)] = speech
speech_masks[i, :vae_tok_length] = True
result = {
"padded_speeches": padded_speeches,
"speech_masks": speech_masks,
}
# Convert to tensors if requested
if return_tensors == "pt":
result["padded_speeches"] = torch.tensor(padded_speeches, device=device, dtype=dtype or torch.float32)
result["speech_masks"] = torch.tensor(speech_masks, device=device, dtype=torch.bool)
return result
def batch_decode(self, *args, **kwargs):
"""
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.batch_decode`].
Please refer to the docstring of this method for more information.
"""
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
"""
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.decode`].
Please refer to the docstring of this method for more information.
"""
return self.tokenizer.decode(*args, **kwargs)
@property
def model_input_names(self):
"""
Return the list of inputs accepted by the model.
"""
tokenizer_input_names = self.tokenizer.model_input_names
audio_processor_input_names = self.audio_processor.model_input_names
return list(dict.fromkeys(tokenizer_input_names + audio_processor_input_names + ["speech_inputs", "speech_input_mask"]))
def save_audio(self,
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
output_path: str = "output.wav",
sampling_rate: Optional[int] = None,
normalize: bool = False,
batch_prefix: str = "audio_",
) -> str:
"""
Save audio data to a file.
Args:
audio (Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]]):
The audio data to save. Can be a single tensor/array or a list of them.
output_path (str, optional): Path to save the audio file. Defaults to "output.wav".
sampling_rate (int, optional): Sampling rate for the audio. If None, uses the processor's default.
normalize (bool, optional): Whether to normalize the audio before saving. Defaults to False.
batch_prefix (str, optional): Prefix for batch audio files. Defaults to "audio_".
Returns:
str: The path to the saved audio file.
"""
return self.audio_processor.save_audio(audio, output_path=output_path, sampling_rate=sampling_rate, normalize=normalize, batch_prefix=batch_prefix)
__all__ = [
"VibeVoiceStreamingProcessor",
]
@@ -0,0 +1,413 @@
"""
Processor class for VibeVoice models.
"""
import os
import json
import warnings
from typing import List, Optional, Union, Dict, Any
import numpy as np
import torch
from transformers.feature_extraction_utils import FeatureExtractionMixin
from transformers.utils import logging
from .audio_utils import AudioNormalizer
logger = logging.get_logger(__name__)
# Change from ProcessorMixin to FeatureExtractionMixin which is designed for single components
class VibeVoiceTokenizerProcessor(FeatureExtractionMixin):
"""
Processor for VibeVoice acoustic tokenizer models.
This processor handles audio preprocessing for VibeVoice models, including:
- Audio format conversion (stereo to mono)
- Optional audio normalization
- Streaming support for infinite-length audio
Args:
sampling_rate (int, optional): Expected sampling rate. Defaults to 24000.
normalize_audio (bool, optional): Whether to normalize audio. Defaults to True.
target_dB_FS (float, optional): Target dB FS for normalization. Defaults to -25.
eps (float, optional): Small value for numerical stability. Defaults to 1e-6.
"""
model_input_names = ["input_features"]
def __init__(
self,
sampling_rate: int = 24000,
normalize_audio: bool = True,
target_dB_FS: float = -25,
eps: float = 1e-6,
**kwargs,
):
super().__init__(**kwargs)
self.sampling_rate = sampling_rate
self.normalize_audio = normalize_audio
# Initialize audio normalizer if needed
if self.normalize_audio:
self.normalizer = AudioNormalizer(target_dB_FS=target_dB_FS, eps=eps)
else:
self.normalizer = None
# Save config
self.feature_extractor_dict = {
"sampling_rate": sampling_rate,
"normalize_audio": normalize_audio,
"target_dB_FS": target_dB_FS,
"eps": eps,
}
def _ensure_mono(self, audio: np.ndarray) -> np.ndarray:
"""
Convert stereo audio to mono if needed.
Args:
audio (np.ndarray): Input audio array
Returns:
np.ndarray: Mono audio array
"""
if len(audio.shape) == 1:
return audio
elif len(audio.shape) == 2:
if audio.shape[0] == 2: # (2, time)
return np.mean(audio, axis=0)
elif audio.shape[1] == 2: # (time, 2)
return np.mean(audio, axis=1)
else:
# If one dimension is 1, squeeze it
if audio.shape[0] == 1:
return audio.squeeze(0)
elif audio.shape[1] == 1:
return audio.squeeze(1)
else:
raise ValueError(f"Unexpected audio shape: {audio.shape}")
else:
raise ValueError(f"Audio should be 1D or 2D, got shape: {audio.shape}")
def _process_single_audio(self, audio: Union[np.ndarray, List[float]]) -> np.ndarray:
"""
Process a single audio array.
Args:
audio: Single audio input
Returns:
np.ndarray: Processed audio
"""
# Convert to numpy array
if not isinstance(audio, np.ndarray):
audio = np.array(audio, dtype=np.float32)
else:
audio = audio.astype(np.float32)
# Ensure mono
audio = self._ensure_mono(audio)
# Normalize if requested
if self.normalize_audio and self.normalizer is not None:
audio = self.normalizer(audio)
return audio
def __call__(
self,
audio: Union[str, np.ndarray, List[float], List[np.ndarray], List[List[float]], List[str]] = None,
sampling_rate: Optional[int] = None,
return_tensors: Optional[str] = None,
**kwargs,
):
"""
Process audio for VibeVoice models.
Args:
audio: Audio input(s) to process. Can be:
- str: Path to audio file
- np.ndarray: Audio array
- List[float]: Audio as list of floats
- List[np.ndarray]: Batch of audio arrays
- List[str]: Batch of audio file paths
sampling_rate (int, optional): Sampling rate of the input audio
return_tensors (str, optional): Return format ('pt' for PyTorch, 'np' for NumPy)
Returns:
dict: Processed audio inputs with keys:
- input_features: Audio tensor(s) ready for the model
"""
if audio is None:
raise ValueError("Audio input is required")
# Validate sampling rate
if sampling_rate is not None and sampling_rate != self.sampling_rate:
logger.warning(
f"Input sampling rate ({sampling_rate}) differs from expected "
f"sampling rate ({self.sampling_rate}). Please resample your audio."
)
# Handle different input types
if isinstance(audio, str):
# Single audio file path
audio = self._load_audio_from_path(audio)
is_batched = False
elif isinstance(audio, list):
if len(audio) == 0:
raise ValueError("Empty audio list provided")
# Check if it's a list of file paths
if all(isinstance(item, str) for item in audio):
# Batch of audio file paths
audio = [self._load_audio_from_path(path) for path in audio]
is_batched = True
else:
# Check if it's batched audio arrays
is_batched = isinstance(audio[0], (np.ndarray, list))
else:
# Single audio array or list
is_batched = False
# Process audio
if is_batched:
processed_audio = [self._process_single_audio(a) for a in audio]
else:
processed_audio = [self._process_single_audio(audio)]
# Convert to tensors if requested
if return_tensors == "pt":
if len(processed_audio) == 1:
# Create a proper batch dimension (B, T)
input_features = torch.from_numpy(processed_audio[0]).unsqueeze(0).unsqueeze(1)
else:
# For batched input with different lengths, create a batch properly
input_features = torch.stack([torch.from_numpy(a) for a in processed_audio]).unsqueeze(1)
elif return_tensors == "np":
if len(processed_audio) == 1:
input_features = processed_audio[0][np.newaxis, np.newaxis, :]
else:
input_features = np.stack(processed_audio)[:, np.newaxis, :]
else:
input_features = processed_audio[0] if len(processed_audio) == 1 else processed_audio
outputs = {
"audio": input_features, # Use "audio" instead of "input_features"
}
return outputs
def _load_audio_from_path(self, audio_path: str) -> np.ndarray:
"""
Load audio from file path.
Args:
audio_path (str): Path to audio file
Returns:
np.ndarray: Loaded audio array
"""
# Get file extension to determine loading method
file_ext = os.path.splitext(audio_path)[1].lower()
if file_ext in ['.wav', '.mp3', '.flac', '.m4a', '.ogg']:
# Audio file - use librosa
import librosa
audio_array, sr = librosa.load(
audio_path,
sr=self.sampling_rate,
mono=True
)
return audio_array
elif file_ext == '.pt':
# PyTorch tensor file
audio_tensor = torch.load(audio_path, map_location='cpu', weights_only=True).squeeze()
if isinstance(audio_tensor, torch.Tensor):
audio_array = audio_tensor.numpy()
else:
audio_array = np.array(audio_tensor)
return audio_array.astype(np.float32)
elif file_ext == '.npy':
# NumPy file
audio_array = np.load(audio_path)
return audio_array.astype(np.float32)
else:
raise ValueError(
f"Unsupported file format: {file_ext}. "
f"Supported formats: .wav, .mp3, .flac, .m4a, .ogg, .pt, .npy, .npz"
)
def preprocess_audio(
self,
audio_path_or_array: Union[str, np.ndarray],
normalize: Optional[bool] = None,
) -> np.ndarray:
"""
Convenience method to preprocess audio from file path or array.
This method is kept for backward compatibility but __call__ is recommended.
Args:
audio_path_or_array: Path to audio file or numpy array
normalize: Whether to normalize (overrides default setting)
Returns:
np.ndarray: Preprocessed audio array
"""
if isinstance(audio_path_or_array, str):
audio_array = self._load_audio_from_path(audio_path_or_array)
else:
audio_array = np.array(audio_path_or_array, dtype=np.float32)
# Override normalization setting if specified
original_normalize = self.normalize_audio
if normalize is not None:
self.normalize_audio = normalize
try:
processed = self._process_single_audio(audio_array)
finally:
# Restore original setting
self.normalize_audio = original_normalize
return processed
# Override to_dict method for configuration saving
def to_dict(self) -> Dict[str, Any]:
"""
Convert the object to a dict containing all attributes needed for serialization.
"""
return self.feature_extractor_dict
def save_audio(
self,
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
output_path: str = "output.wav",
sampling_rate: Optional[int] = None,
normalize: bool = False,
batch_prefix: str = "audio_",
):
"""
Save audio data to WAV file(s).
Args:
audio: Audio data to save. Can be:
- torch.Tensor: PyTorch tensor with shape (B, C, T) or (B, T) or (T)
- np.ndarray: NumPy array with shape (B, C, T) or (B, T) or (T)
- List of tensors or arrays
output_path: Path where to save the audio. If saving multiple files,
this is treated as a directory and individual files will be saved inside.
sampling_rate: Sampling rate for the saved audio. Defaults to the processor's rate.
normalize: Whether to normalize audio before saving.
batch_prefix: Prefix for batch files when saving multiple audios.
Returns:
List[str]: Paths to the saved audio files.
"""
if sampling_rate is None:
sampling_rate = self.sampling_rate
try:
import soundfile as sf
except ImportError:
raise ImportError(
"soundfile is required to save audio files. "
"Install it with: pip install soundfile"
)
# Ensure audio is in the right format
if isinstance(audio, torch.Tensor):
# Convert PyTorch tensor to numpy
audio_np = audio.float().detach().cpu().numpy()
elif isinstance(audio, np.ndarray):
audio_np = audio
elif isinstance(audio, list):
# Handle list of tensors or arrays
if all(isinstance(a, torch.Tensor) for a in audio):
audio_np = [a.float().detach().cpu().numpy() for a in audio]
else:
audio_np = audio
else:
raise ValueError(f"Unsupported audio type: {type(audio)}")
saved_paths = []
# Handle based on shape or type
if isinstance(audio_np, list):
# Multiple separate audios to save
output_dir = output_path
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Save each audio
for i, audio_item in enumerate(audio_np):
audio_item = self._prepare_audio_for_save(audio_item, normalize)
file_path = os.path.join(output_dir, f"{batch_prefix}{i}.wav")
sf.write(file_path, audio_item, sampling_rate)
saved_paths.append(file_path)
else:
# Handle different dimensions
if len(audio_np.shape) >= 3: # (B, C, T) or similar
# Get batch size
batch_size = audio_np.shape[0]
if batch_size > 1:
# Multiple audios in a batch
output_dir = output_path
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Save each audio in the batch
for i in range(batch_size):
# Extract single audio and remove channel dim if present
single_audio = audio_np[i]
if len(single_audio.shape) > 1:
if single_audio.shape[0] == 1: # (1, T)
single_audio = single_audio.squeeze(0)
single_audio = self._prepare_audio_for_save(single_audio, normalize)
file_path = os.path.join(output_dir, f"{batch_prefix}{i}.wav")
sf.write(file_path, single_audio, sampling_rate)
saved_paths.append(file_path)
else:
# Single audio with batch and channel dims
audio_item = audio_np.squeeze() # Remove batch and channel dimensions
audio_item = self._prepare_audio_for_save(audio_item, normalize)
sf.write(output_path, audio_item, sampling_rate)
saved_paths.append(output_path)
else:
# Single audio without batch dimension
audio_item = self._prepare_audio_for_save(audio_np, normalize)
sf.write(output_path, audio_item, sampling_rate)
saved_paths.append(output_path)
return saved_paths
def _prepare_audio_for_save(self, audio: np.ndarray, normalize: bool) -> np.ndarray:
"""
Prepare audio for saving by ensuring it's the right shape and optionally normalizing.
Args:
audio: Audio data as numpy array
normalize: Whether to normalize audio
Returns:
np.ndarray: Processed audio ready for saving
"""
# Ensure right dimensionality
if len(audio.shape) > 1 and audio.shape[0] == 1: # (1, T)
audio = audio.squeeze(0)
# Normalize if requested
if normalize:
max_val = np.abs(audio).max()
if max_val > 0:
audio = audio / max_val
return audio
__all__ = ["VibeVoiceTokenizerProcessor", "AudioNormalizer"]
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
import math
import torch
class UniformSampler:
def __init__(self, timesteps = 1000):
self.timesteps = timesteps
def sample(self, batch_size, device):
return torch.randint(0, self.timesteps, (batch_size,), device=device)
class LogitNormalSampler:
def __init__(self, timesteps = 1000, m = 0, s = 1):
self.timesteps = timesteps
timesteps = torch.linspace(0, 1, timesteps)
logit = torch.log(timesteps / (1 - timesteps))
self.prob = torch.exp(-0.5 * (logit - m) ** 2 / s ** 2) / (s * math.sqrt(2 * math.pi))
def sample(self, batch_size, device):
return torch.multinomial(self.prob, batch_size, replacement=True).to(device)
View File
+62
View File
@@ -0,0 +1,62 @@
"""VibeVoice vLLM Plugin - Registers VibeVoice model for vLLM inference.
This plugin enables VibeVoice ASR models to be loaded and served through vLLM.
It registers the model architecture, configuration, tokenizer, and processor
with their respective registries.
The plugin is automatically loaded by vLLM via the 'vllm.general_plugins'
entry point defined in pyproject.toml.
"""
from vllm.model_executor.models import ModelRegistry
from transformers import AutoConfig, AutoTokenizer, Qwen2Tokenizer, AutoProcessor, Qwen2AudioProcessor
from vibevoice.modular.configuration_vibevoice import VibeVoiceConfig
from vibevoice.modular.modular_vibevoice_text_tokenizer import VibeVoiceASRTextTokenizerFast
from .model import VibeVoiceForCausalLM
def register_vibevoice():
"""Register VibeVoice model with vLLM and transformers.
This function is called automatically by vLLM through the entry point
mechanism. It registers:
- VibeVoiceConfig with AutoConfig
- VibeVoiceASRTextTokenizerFast with AutoTokenizer (for ASR)
- Qwen2AudioProcessor with AutoProcessor
- VibeVoiceForCausalLM with vLLM ModelRegistry
"""
# Register the configuration class with transformers
AutoConfig.register("vibevoice", VibeVoiceConfig)
# Register the tokenizer with transformers.
# IMPORTANT (ASR): Align with the PyTorch ASR path.
# VibeVoiceASRTextTokenizerFast maps:
# speech_start_id -> <|object_ref_start|>
# speech_pad_id -> <|box_start|>
# speech_end_id -> <|object_ref_end|>
# This significantly affects ASR quality even when requests succeed.
try:
AutoTokenizer.register(
VibeVoiceConfig,
slow_tokenizer_class=Qwen2Tokenizer,
fast_tokenizer_class=VibeVoiceASRTextTokenizerFast,
)
except Exception:
pass # May already be registered
# Register the processor with transformers
try:
AutoProcessor.register(VibeVoiceConfig, processor_class=Qwen2AudioProcessor)
except Exception:
pass # May already be registered
# Register the model class with the architecture name "VibeVoice"
# This name must match the "architectures" list in config.json
ModelRegistry.register_model("VibeVoice", VibeVoiceForCausalLM)
ModelRegistry.register_model("VibeVoiceForASRTraining", VibeVoiceForCausalLM)
# Note: This function is called via vllm.general_plugins entry point
# defined in pyproject.toml, ensuring it runs in all vLLM processes
+101
View File
@@ -0,0 +1,101 @@
"""Audio input mapper for vLLM multimodal pipeline.
This module handles audio data loading and preprocessing for VibeVoice ASR inference.
It converts various audio input formats (path, bytes, numpy array) into tensors
that can be processed by the VibeVoice model.
"""
import os
import logging
import torch
import numpy as np
from typing import Union, List
from vllm.multimodal.inputs import MultiModalInputs
from vibevoice.processor.audio_utils import load_audio_use_ffmpeg, load_audio_bytes_use_ffmpeg, AudioNormalizer
logger = logging.getLogger(__name__)
# Maximum audio duration in seconds. Default 3660s (61 minutes) matches the
# model's designed capacity. Override via environment variable to guard against
# OOM on GPUs with less VRAM. See https://github.com/microsoft/VibeVoice/issues/210
_MAX_AUDIO_DURATION = float(os.environ.get("VIBEVOICE_MAX_AUDIO_DURATION", "3660"))
def load_audio(audio_path: str, target_sr: int = 24000) -> np.ndarray:
"""Load and normalize audio from file path.
Args:
audio_path: Path to audio file
target_sr: Target sample rate (default 24kHz for VibeVoice)
Returns:
Normalized audio waveform as numpy array
"""
# Load with FFmpeg (handles various formats)
audio, sr = load_audio_use_ffmpeg(audio_path, resample=True, target_sr=target_sr)
# Normalize audio
normalizer = AudioNormalizer()
audio = normalizer(audio)
return audio
def vibevoice_audio_input_mapper(ctx, data: Union[str, bytes, np.ndarray, List[str]]) -> MultiModalInputs:
"""Map audio input data to vLLM MultiModalInputs format.
This function is registered as the input mapper for VibeVoice audio processing.
It handles multiple input formats and converts them to normalized tensors.
Args:
ctx: vLLM context (unused)
data: Audio data in one of these formats:
- str: Path to audio file
- bytes: Raw audio bytes (any format FFmpeg supports)
- np.ndarray: Pre-loaded audio waveform
- List[str]: List of audio paths (only first is used)
Returns:
MultiModalInputs containing:
- audio: Audio tensor (float32)
- audio_length: Length of audio in samples
"""
# Handle list input (take first item)
if isinstance(data, list):
data = data[0]
audio_waveform = None
if isinstance(data, str):
# Load from file path
audio_waveform = load_audio(data)
elif isinstance(data, bytes):
# Decode bytes directly via ffmpeg stdin pipe to avoid temp-file IO
audio_waveform, _sr = load_audio_bytes_use_ffmpeg(data, resample=True, target_sr=24000)
normalizer = AudioNormalizer()
audio_waveform = normalizer(audio_waveform)
elif isinstance(data, np.ndarray):
# Already loaded numpy array
audio_waveform = data
else:
raise ValueError(f"Unsupported audio data type: {type(data)}")
# Validate audio duration before tensor conversion to catch OOM early
duration_sec = len(audio_waveform) / 24000
if duration_sec > _MAX_AUDIO_DURATION:
raise ValueError(
f"Audio duration ({duration_sec:.1f}s) exceeds the configured "
f"limit ({_MAX_AUDIO_DURATION:.0f}s). Set the "
f"VIBEVOICE_MAX_AUDIO_DURATION environment variable to adjust "
f"this limit, or use shorter audio."
)
# Convert to tensor
audio_tensor = torch.from_numpy(audio_waveform).float()
audio_length = audio_tensor.shape[0]
return MultiModalInputs({
"audio": audio_tensor,
"audio_length": audio_length
})
+1251
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+451
View File
@@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""
VibeVoice vLLM ASR Server Launcher
One-click deployment script that handles:
1. Installing system dependencies (FFmpeg, etc.)
2. Installing VibeVoice Python package
3. Downloading model from HuggingFace
4. Generating tokenizer files
5. Starting vLLM server
For DP > 1, launches N independent vLLM processes behind an nginx
reverse proxy for optimal throughput (avoids single-process HTTP
bottleneck of vLLM's built-in DP coordinator).
Usage:
python3 start_server.py [--model MODEL_ID] [--port PORT]
"""
import argparse
import os
import signal
import subprocess
import sys
import textwrap
import time
def run_command(cmd: list[str], description: str, shell: bool = False) -> None:
"""Run a command with logging."""
print(f"\n{'='*60}")
print(f" {description}")
print(f"{'='*60}\n")
if shell:
subprocess.run(cmd, shell=True, check=True)
else:
subprocess.run(cmd, check=True)
def install_system_deps() -> None:
"""Install system dependencies (FFmpeg, etc.)."""
run_command(["apt-get", "update"], "Updating package list")
run_command(
["apt-get", "install", "-y", "ffmpeg", "libsndfile1"],
"Installing FFmpeg and audio libraries"
)
def install_vibevoice() -> None:
"""Install VibeVoice Python package."""
run_command(
[sys.executable, "-m", "pip", "install", "-e", "/app[vllm]"],
"Installing VibeVoice with vLLM support"
)
def download_model(model_id: str) -> str:
"""Download model from HuggingFace using default cache."""
print(f"\n{'='*60}")
print(f" Downloading model: {model_id}")
print(f"{'='*60}\n")
import warnings
from huggingface_hub import snapshot_download
# Suppress deprecation warnings from huggingface_hub
with warnings.catch_warnings():
warnings.simplefilter("ignore")
model_path = snapshot_download(model_id)
print(f"\n{'='*60}")
print(f" ✅ Model downloaded successfully!")
print(f" 📁 Path: {model_path}")
print(f"{'='*60}\n")
return model_path
def generate_tokenizer(model_path: str) -> None:
"""Generate tokenizer files for the model."""
run_command(
[sys.executable, "-m", "vllm_plugin.tools.generate_tokenizer_files",
"--output", model_path],
"Generating tokenizer files"
)
def _build_vllm_cmd(model_path: str, port: int,
tensor_parallel_size: int = 1,
data_parallel_size: int = 1,
max_num_seqs: int = 64,
max_model_len: int = 65536,
gpu_memory_utilization: float = 0.8) -> list[str]:
"""Build the vllm serve command."""
return [
"vllm", "serve", model_path,
"--served-model-name", "vibevoice",
"--trust-remote-code",
"--dtype", "bfloat16",
"--max-num-seqs", str(max_num_seqs),
"--max-model-len", str(max_model_len),
"--gpu-memory-utilization", str(gpu_memory_utilization),
"--no-enable-prefix-caching",
"--enable-chunked-prefill",
"--chat-template-content-format", "openai",
"--tensor-parallel-size", str(tensor_parallel_size),
"--data-parallel-size", str(data_parallel_size),
"--allowed-local-media-path", "/app",
"--port", str(port),
]
def start_vllm_server(model_path: str, port: int,
tensor_parallel_size: int = 1,
data_parallel_size: int = 1,
max_num_seqs: int = 64,
max_model_len: int = 65536,
gpu_memory_utilization: float = 0.8) -> None:
"""Start a single vLLM server (replaces current process)."""
print(f"\n{'='*60}")
print(f" Starting vLLM server on port {port}")
print(f" Tensor Parallel (TP): {tensor_parallel_size}")
print(f" Data Parallel (DP): {data_parallel_size}")
print(f" Max Num Seqs: {max_num_seqs}")
print(f" Max Model Len: {max_model_len}")
print(f" GPU Mem Utilization: {gpu_memory_utilization}")
print(f"{'='*60}\n")
vllm_cmd = _build_vllm_cmd(
model_path, port,
tensor_parallel_size=tensor_parallel_size,
data_parallel_size=data_parallel_size,
max_num_seqs=max_num_seqs,
max_model_len=max_model_len,
gpu_memory_utilization=gpu_memory_utilization,
)
os.execvp("vllm", vllm_cmd)
def _install_nginx() -> None:
"""Install nginx if not already available."""
if subprocess.run(["which", "nginx"], capture_output=True).returncode != 0:
run_command(["apt-get", "update"], "Updating package list for nginx")
run_command(
["apt-get", "install", "-y", "nginx"],
"Installing nginx for load balancing"
)
def _write_nginx_config(frontend_port: int, backend_ports: list[int],
num_workers: int = 0) -> str:
"""Write nginx config for round-robin load balancing.
Args:
num_workers: Number of nginx worker processes. 0 = auto (2 × num backends).
"""
if num_workers <= 0:
num_workers = len(backend_ports) * 2
backends = "\n".join(f" server 127.0.0.1:{p};" for p in backend_ports)
config = textwrap.dedent(f"""\
worker_processes {num_workers};
worker_rlimit_nofile 65536;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events {{
worker_connections 8192;
}}
http {{
access_log off;
upstream vllm_backends {{
least_conn;
{backends}
}}
server {{
listen {frontend_port};
client_max_body_size 200m;
client_body_buffer_size 10m;
proxy_buffering on;
proxy_buffer_size 64k;
proxy_buffers 16 64k;
location / {{
proxy_pass http://vllm_backends;
proxy_read_timeout 600s;
proxy_connect_timeout 10s;
proxy_send_timeout 600s;
proxy_http_version 1.1;
proxy_set_header Connection "";
}}
}}
}}
""")
config_path = "/tmp/nginx_vllm.conf"
with open(config_path, "w") as f:
f.write(config)
return config_path
def start_dp_server(model_path: str, frontend_port: int,
data_parallel_size: int,
tensor_parallel_size: int = 1,
max_num_seqs: int = 64,
max_model_len: int = 65536,
gpu_memory_utilization: float = 0.8) -> None:
"""Start multiple vLLM workers behind nginx for data parallelism.
Launches N independent vLLM processes (one per GPU group) on internal
ports, with an nginx reverse proxy on the frontend port for load
balancing. This avoids the single-process HTTP bottleneck of vLLM's
built-in DP coordinator when handling large audio payloads.
"""
import torch
num_gpus = torch.cuda.device_count()
gpus_per_replica = tensor_parallel_size
total_gpus_needed = data_parallel_size * gpus_per_replica
assert num_gpus >= total_gpus_needed, (
f"Need {total_gpus_needed} GPUs (dp={data_parallel_size} × tp={tensor_parallel_size}) "
f"but only {num_gpus} available"
)
# Auto-tune per-worker env vars based on dp size
ffmpeg_concurrency = max(
64, int(os.environ.get("VIBEVOICE_FFMPEG_MAX_CONCURRENCY", "64"))
)
media_threads = max(
8, int(os.environ.get("VLLM_MEDIA_LOADING_THREAD_COUNT", "8"))
)
_install_nginx()
# Assign internal ports: frontend_port + 100, +101, ...
backend_ports = [frontend_port + 100 + i for i in range(data_parallel_size)]
print(f"\n{'='*60}")
print(f" Starting DP server with nginx load balancing")
print(f" Frontend port: {frontend_port} (nginx)")
print(f" Backend ports: {backend_ports}")
print(f" Data Parallel: {data_parallel_size}")
print(f" Tensor Parallel: {tensor_parallel_size}")
print(f" GPUs per replica: {gpus_per_replica}")
print(f" Max Num Seqs: {max_num_seqs}")
print(f" Max Model Len: {max_model_len}")
print(f" FFmpeg concurrency (per worker): {ffmpeg_concurrency}")
print(f" Media loading threads (per worker): {media_threads}")
print(f"{'='*60}\n")
# Write nginx config
nginx_conf = _write_nginx_config(frontend_port, backend_ports)
# Launch vLLM workers
workers: list[subprocess.Popen] = []
for rank in range(data_parallel_size):
gpu_start = rank * gpus_per_replica
gpu_ids = ",".join(str(gpu_start + j) for j in range(gpus_per_replica))
port = backend_ports[rank]
env = os.environ.copy()
env["CUDA_VISIBLE_DEVICES"] = gpu_ids
env["VIBEVOICE_FFMPEG_MAX_CONCURRENCY"] = str(ffmpeg_concurrency)
env["VLLM_MEDIA_LOADING_THREAD_COUNT"] = str(media_threads)
vllm_cmd = _build_vllm_cmd(
model_path, port,
tensor_parallel_size=tensor_parallel_size,
data_parallel_size=1, # Each worker is dp=1
max_num_seqs=max_num_seqs,
max_model_len=max_model_len,
gpu_memory_utilization=gpu_memory_utilization,
)
print(f" Launching worker rank={rank} on GPU(s) {gpu_ids}, port {port}")
proc = subprocess.Popen(vllm_cmd, env=env)
workers.append(proc)
# Start nginx
print(f"\n Starting nginx on port {frontend_port} ...")
nginx_proc = subprocess.Popen(
["nginx", "-c", nginx_conf, "-g", "daemon off;"]
)
# Wait for all backends to be ready
print(" Waiting for all backends to be ready ...")
import urllib.request
for port in backend_ports:
url = f"http://127.0.0.1:{port}/v1/models"
for attempt in range(600): # up to 10 minutes
try:
urllib.request.urlopen(url, timeout=2)
print(f" ✅ Backend on port {port} is ready")
break
except Exception:
time.sleep(1)
else:
print(f" ❌ Backend on port {port} failed to start")
print(f"\n{'='*60}")
print(f" ✅ VibeVoice DP server ready on port {frontend_port}")
print(f" {data_parallel_size} replicas behind nginx load balancer")
print(f"{'='*60}\n")
# Handle shutdown: forward signals to all children
def _shutdown(signum, frame):
print("\nShutting down ...")
nginx_proc.terminate()
for w in workers:
w.terminate()
for w in workers:
w.wait(timeout=10)
nginx_proc.wait(timeout=5)
sys.exit(0)
signal.signal(signal.SIGTERM, _shutdown)
signal.signal(signal.SIGINT, _shutdown)
# Wait for any child to exit (indicates a failure)
while True:
for i, w in enumerate(workers):
ret = w.poll()
if ret is not None:
print(f" ❌ Worker {i} exited with code {ret}")
_shutdown(None, None)
if nginx_proc.poll() is not None:
print(f" ❌ nginx exited with code {nginx_proc.returncode}")
_shutdown(None, None)
time.sleep(1)
def main():
parser = argparse.ArgumentParser(
description="VibeVoice vLLM ASR Server - One-Click Deployment",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Start with default settings (single GPU)
python3 start_server.py
# Use custom port
python3 start_server.py --port 8080
# Data parallel: 4 replicas on 4 GPUs (nginx load balancing)
python3 start_server.py --dp 4
# Tensor parallel: split model across 2 GPUs
python3 start_server.py --tp 2
# Skip dependency installation (if already installed)
python3 start_server.py --skip-deps
"""
)
parser.add_argument(
"--model", "-m",
default="microsoft/VibeVoice-ASR",
help="HuggingFace model ID (default: microsoft/VibeVoice-ASR)"
)
parser.add_argument(
"--port", "-p",
type=int,
default=8000,
help="Server port (default: 8000)"
)
parser.add_argument(
"--skip-deps",
action="store_true",
help="Skip installing system dependencies"
)
parser.add_argument(
"--skip-tokenizer",
action="store_true",
help="Skip generating tokenizer files"
)
parser.add_argument(
"--tp", "--tensor-parallel-size",
type=int,
default=1,
dest="tensor_parallel_size",
help="Tensor parallel size: split one model across N GPUs (default: 1)"
)
parser.add_argument(
"--dp", "--data-parallel-size",
type=int,
default=1,
dest="data_parallel_size",
help="Data parallel size: run N independent model replicas for load balancing (default: 1)"
)
parser.add_argument(
"--max-num-seqs",
type=int,
default=64,
dest="max_num_seqs",
help="Maximum number of sequences per batch (default: 64)"
)
parser.add_argument(
"--max-model-len",
type=int,
default=65536,
dest="max_model_len",
help="Maximum model context length (default: 65536)"
)
parser.add_argument(
"--gpu-memory-utilization",
type=float,
default=0.8,
dest="gpu_memory_utilization",
help="GPU memory utilization fraction (default: 0.8)"
)
args = parser.parse_args()
print("\n" + "="*60)
print(" VibeVoice vLLM ASR Server - One-Click Deployment")
print("="*60)
# Step 1: Install system dependencies
if not args.skip_deps:
install_system_deps()
# Step 2: Install VibeVoice
install_vibevoice()
# Step 3: Download model
model_path = download_model(args.model)
# Step 4: Generate tokenizer files
if not args.skip_tokenizer:
generate_tokenizer(model_path)
# Step 5: Start server
if args.data_parallel_size > 1:
start_dp_server(
model_path, args.port,
data_parallel_size=args.data_parallel_size,
tensor_parallel_size=args.tensor_parallel_size,
max_num_seqs=args.max_num_seqs,
max_model_len=args.max_model_len,
gpu_memory_utilization=args.gpu_memory_utilization,
)
else:
start_vllm_server(
model_path, args.port,
tensor_parallel_size=args.tensor_parallel_size,
data_parallel_size=1,
max_num_seqs=args.max_num_seqs,
max_model_len=args.max_model_len,
gpu_memory_utilization=args.gpu_memory_utilization,
)
if __name__ == "__main__":
main()
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
Test VibeVoice vLLM API with Streaming and Optional Hotwords Support.
This script tests ASR transcription via the vLLM OpenAI-compatible API.
By default, it runs standard transcription without hotwords.
Optionally, you can provide hotwords (context_info) to improve recognition
of domain-specific content like proper nouns, technical terms, and speaker names.
Hotwords are embedded in the prompt as "with extra info: {hotwords}".
Usage:
python test_api_with_hotwords.py [audio_path] [--url URL] [--hotwords "word1,word2"]
Examples:
# Standard transcription (no hotwords)
python3 test_api.py audio.wav
# With hotwords for better recognition of specific terms
python3 test_api.py audio.wav --hotwords "Microsoft,Azure,VibeVoice"
"""
import requests
import json
import base64
import time
import sys
import os
import subprocess
import argparse
def _guess_mime_type(path: str) -> str:
"""Guess MIME type from file extension."""
ext = os.path.splitext(path)[1].lower()
mime_map = {
".wav": "audio/wav",
".mp3": "audio/mpeg",
".m4a": "audio/mp4",
".mp4": "video/mp4",
".flac": "audio/flac",
".ogg": "audio/ogg",
".opus": "audio/ogg",
}
return mime_map.get(ext, "application/octet-stream")
def _get_duration_seconds_ffprobe(path: str) -> float:
"""Get audio duration using ffprobe."""
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
path,
]
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8").strip()
return float(out)
def _is_video_file(path: str) -> bool:
"""Check if the file is a video file that needs audio extraction."""
ext = os.path.splitext(path)[1].lower()
return ext in (".mp4", ".m4v", ".mov", ".webm", ".avi", ".mkv")
def _extract_audio_from_video(video_path: str) -> str:
"""
Extract audio from video file (mp4/mov/webm) to a temporary mp3 file.
Returns the path to the extracted audio file.
"""
import tempfile
# Create temp file with .mp3 extension
fd, audio_path = tempfile.mkstemp(suffix=".mp3")
os.close(fd)
cmd = [
"ffmpeg", "-y", "-i", video_path,
"-vn", # No video
"-acodec", "libmp3lame",
"-q:a", "2", # High quality
audio_path
]
subprocess.run(cmd, check=True, capture_output=True)
return audio_path
def test_transcription_with_hotwords(
audio_path: str,
context_info: str = None,
base_url: str = "http://localhost:8000",
):
"""
Test ASR transcription with customized hotwords.
Hotwords are embedded in the prompt text as "with extra info: {hotwords}".
This helps the model recognize domain-specific terms more accurately.
Args:
audio_path: Path to the audio file
context_info: Hotwords string (e.g., "Microsoft,Azure,VibeVoice")
base_url: vLLM server URL
"""
print(f"=" * 70)
print(f"Testing Customized Hotwords Support")
print(f"=" * 70)
print(f"Input file: {audio_path}")
print(f"Hotwords: {context_info or '(none)'}")
print()
# Handle video files: extract audio first
temp_audio_path = None
actual_audio_path = audio_path
if _is_video_file(audio_path):
print(f"🎬 Detected video file, extracting audio...")
temp_audio_path = _extract_audio_from_video(audio_path)
actual_audio_path = temp_audio_path
print(f"✅ Audio extracted to: {temp_audio_path}")
# Load audio
try:
duration = _get_duration_seconds_ffprobe(actual_audio_path)
print(f"Audio duration: {duration:.2f} seconds")
with open(actual_audio_path, "rb") as f:
audio_bytes = f.read()
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
print(f"Audio size: {len(audio_bytes)} bytes")
except Exception as e:
print(f"Error preparing audio: {e}")
# Cleanup temp file if created
if temp_audio_path and os.path.exists(temp_audio_path):
os.remove(temp_audio_path)
return
# Build the request
url = f"{base_url}/v1/chat/completions"
show_keys = ["Start time", "End time", "Speaker ID", "Content"]
# Build prompt with optional hotwords
# Hotwords are embedded as "with extra info: {hotwords}" in the prompt
if context_info and context_info.strip():
prompt_text = (
f"This is a {duration:.2f} seconds audio, with extra info: {context_info.strip()}\n\n"
f"Please transcribe it with these keys: " + ", ".join(show_keys)
)
print(f"\n📝 Hotwords embedded in prompt: '{context_info}'")
else:
prompt_text = (
f"This is a {duration:.2f} seconds audio, please transcribe it with these keys: "
+ ", ".join(show_keys)
)
print(f"\n📝 No hotwords provided")
mime = _guess_mime_type(actual_audio_path)
data_url = f"data:{mime};base64,{audio_b64}"
payload = {
"model": "vibevoice",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant that transcribes audio input into text output in JSON format."
},
{
"role": "user",
"content": [
{"type": "audio_url", "audio_url": {"url": data_url}},
{"type": "text", "text": prompt_text}
]
}
],
"max_tokens": 32768,
"temperature": 0.0,
"stream": True,
"top_p": 1.0,
}
print(f"\n{'=' * 70}")
print(f"Sending request to {url}")
print(f"{'=' * 70}")
t0 = time.time()
try:
response = requests.post(url, json=payload, stream=True, timeout=12000)
if response.status_code == 200:
print("\n✅ Response received. Streaming content:\n")
print("-" * 50)
printed = ""
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith("data: "):
json_str = decoded_line[6:]
if json_str.strip() == "[DONE]":
print("\n" + "-" * 50)
print("✅ [Finished]")
break
try:
data = json.loads(json_str)
delta = data['choices'][0]['delta']
content = delta.get('content', '')
if content:
if content.startswith(printed):
to_print = content[len(printed):]
else:
to_print = content
if to_print:
print(to_print, end='', flush=True)
printed += to_print
except json.JSONDecodeError:
pass
else:
print(f"❌ Error: {response.status_code}")
print(response.text)
except requests.exceptions.Timeout:
print("❌ Request timed out!")
except Exception as e:
print(f"❌ Error: {e}")
elapsed = time.time() - t0
print(f"\n{'=' * 70}")
print(f"⏱️ Total time elapsed: {elapsed:.2f}s")
print(f"📊 RTF (Real-Time Factor): {elapsed / duration:.2f}x")
print(f"{'=' * 70}")
# Cleanup temp audio file if created
if temp_audio_path and os.path.exists(temp_audio_path):
os.remove(temp_audio_path)
print(f"🗑️ Cleaned up temp file: {temp_audio_path}")
def main():
parser = argparse.ArgumentParser(
description="Test VibeVoice vLLM API with Customized Hotwords"
)
parser.add_argument(
"audio_path",
help="Path to audio file (wav, mp3, flac, etc.) or video file"
)
parser.add_argument(
"--url",
default="http://localhost:8000",
help="vLLM server URL (default: http://localhost:8000)"
)
parser.add_argument(
"--hotwords",
type=str,
default=None,
help="Hotwords to improve recognition (e.g., 'Microsoft,Azure,VibeVoice')"
)
args = parser.parse_args()
# Run test
test_transcription_with_hotwords(
audio_path=args.audio_path,
context_info=args.hotwords,
base_url=args.url,
)
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More