Coverage for wrapper/vibevoice/audio_streamer.py: 24%
137 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1# mypy: ignore-errors
2# Copy from: https://github.com/microsoft/VibeVoice/blob/main/vibevoice/modular/streamer.py
4from __future__ import annotations
6import time
7import torch
8import asyncio
10from queue import Queue
12from typing import Optional
13from typing import Any
15from transformers.generation import BaseStreamer
18class AudioStreamer(BaseStreamer):
19 """
20 Audio streamer that stores audio chunks in queues for each sample in the batch.
21 This allows streaming audio generation for multiple samples simultaneously.
23 Parameters:
24 batch_size (`int`):
25 The batch size for generation
26 stop_signal (`any`, *optional*):
27 The signal to put in the queue when generation ends. Defaults to None.
28 timeout (`float`, *optional*):
29 The timeout for the audio queue. If `None`, the queue will block indefinitely.
30 """
32 def __init__(
33 self,
34 batch_size: int,
35 stop_signal: Optional[Any] = None,
36 timeout: Optional[float] = None,
37 ):
38 self.batch_size = batch_size
39 self.stop_signal = stop_signal
40 self.timeout = timeout
42 # Create a queue for each sample in the batch
43 self.audio_queues: list[Queue] = [Queue() for _ in range(batch_size)]
44 self.finished_flags: list[bool] = [False for _ in range(batch_size)]
45 self.sample_indices_map: dict[int, int] = {} # Maps from sample index to queue index
47 def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor) -> None: # type: ignore[override]
48 """
49 Receives audio chunks and puts them in the appropriate queues.
51 Args:
52 audio_chunks: Tensor of shape (num_samples, ...) containing audio chunks
53 sample_indices: Tensor indicating which samples these chunks belong to
54 """
55 for i, sample_idx in enumerate(sample_indices):
56 idx = sample_idx.item()
57 if idx < self.batch_size and not self.finished_flags[idx]:
58 # Convert to numpy or keep as tensor based on preference
59 audio_chunk = audio_chunks[i].detach().cpu()
60 self.audio_queues[idx].put(audio_chunk, timeout=self.timeout)
62 def end(self, sample_indices: Optional[torch.Tensor] = None) -> None:
63 """
64 Signals the end of generation for specified samples or all samples.
66 Args:
67 sample_indices: Optional tensor of sample indices to end. If None, ends all.
68 """
69 if sample_indices is None:
70 # End all samples
71 for idx in range(self.batch_size):
72 if not self.finished_flags[idx]:
73 self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
74 self.finished_flags[idx] = True
75 else:
76 # End specific samples
77 for sample_idx in sample_indices:
78 idx = sample_idx.item() if torch.is_tensor(sample_idx) else sample_idx # type: ignore[assignment]
79 if idx < self.batch_size and not self.finished_flags[idx]:
80 self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
81 self.finished_flags[idx] = True
83 def __iter__(self) -> AudioBatchIterator:
84 """Returns an iterator over the batch of audio streams."""
85 return AudioBatchIterator(self)
87 def get_stream(self, sample_idx: int) -> AudioSampleIterator:
88 """Get the audio stream for a specific sample."""
89 if sample_idx >= self.batch_size:
90 raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
91 return AudioSampleIterator(self, sample_idx)
94class AudioSampleIterator:
95 """Iterator for a single audio stream from the batch."""
97 def __init__(self, streamer: AudioStreamer, sample_idx: int) -> None:
98 self.streamer = streamer
99 self.sample_idx = sample_idx
101 def __iter__(self) -> AudioSampleIterator:
102 return self
104 def __next__(self) -> Any:
105 value = self.streamer.audio_queues[self.sample_idx].get(timeout=self.streamer.timeout)
106 if value == self.streamer.stop_signal:
107 raise StopIteration()
108 return value
111class AudioBatchIterator:
112 """Iterator that yields audio chunks for all samples in the batch."""
114 def __init__(self, streamer: AudioStreamer) -> None:
115 self.streamer = streamer
116 self.active_samples = set(range(streamer.batch_size))
118 def __iter__(self) -> AudioBatchIterator:
119 return self
121 def __next__(self) -> dict[int, Any]:
122 if not self.active_samples:
123 raise StopIteration()
125 batch_chunks = {}
126 samples_to_remove = set()
128 # Try to get chunks from all active samples
129 for idx in self.active_samples:
130 try:
131 value = self.streamer.audio_queues[idx].get(block=False)
132 if value == self.streamer.stop_signal:
133 samples_to_remove.add(idx)
134 else:
135 batch_chunks[idx] = value
136 except Exception:
137 # Queue is empty for this sample, skip it this iteration
138 pass
140 # Remove finished samples
141 self.active_samples -= samples_to_remove
143 if batch_chunks:
144 return batch_chunks
145 elif self.active_samples:
146 # If no chunks were ready but we still have active samples,
147 # wait a bit and try again
148 time.sleep(0.01)
149 return self.__next__()
150 else:
151 raise StopIteration()
154class AsyncAudioStreamer(AudioStreamer):
155 """
156 Async version of AudioStreamer for use in async contexts.
157 """
159 def __init__(
160 self,
161 batch_size: int,
162 stop_signal: Optional[Any] = None,
163 timeout: Optional[float] = None,
164 ) -> None:
165 super().__init__(batch_size, stop_signal, timeout)
166 # Replace regular queues with async queues
167 self.audio_queues: list[asyncio.Queue] = [ # type: ignore[assignment]
168 asyncio.Queue() for _ in range(batch_size)
169 ]
170 self.loop = asyncio.get_running_loop()
172 def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor) -> None: # type: ignore[override]
173 """Put audio chunks in the appropriate async queues."""
174 for i, sample_idx in enumerate(sample_indices):
175 idx = sample_idx.item()
176 if idx < self.batch_size and not self.finished_flags[idx]:
177 audio_chunk = audio_chunks[i].detach().cpu()
178 self.loop.call_soon_threadsafe(
179 self.audio_queues[idx].put_nowait, audio_chunk
180 )
182 def end(self, sample_indices: Optional[torch.Tensor] = None) -> None:
183 """Signal the end of generation for specified samples."""
184 if sample_indices is None:
185 indices_to_end = range(self.batch_size)
186 else:
187 indices_to_end = [s.item() if torch.is_tensor(s) else s for s in sample_indices] # type: ignore[assignment]
189 for idx in indices_to_end:
190 if idx < self.batch_size and not self.finished_flags[idx]:
191 self.loop.call_soon_threadsafe(
192 self.audio_queues[idx].put_nowait, self.stop_signal
193 )
194 self.finished_flags[idx] = True
196 async def get_stream(self, sample_idx: int) -> Any:
197 """Get async iterator for a specific sample's audio stream."""
198 if sample_idx >= self.batch_size:
199 raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
201 while True:
202 value = await self.audio_queues[sample_idx].get()
203 if value == self.stop_signal:
204 break
205 yield value
207 def __aiter__(self) -> AsyncAudioBatchIterator:
208 """Returns an async iterator over all audio streams."""
209 return AsyncAudioBatchIterator(self)
212class AsyncAudioBatchIterator:
213 """Async iterator for batch audio streaming."""
215 def __init__(self, streamer: AsyncAudioStreamer):
216 self.streamer = streamer
217 self.active_samples = set(range(streamer.batch_size))
219 def __aiter__(self) -> AsyncAudioBatchIterator:
220 return self
222 async def __anext__(self) -> dict[int, Any]:
223 if not self.active_samples:
224 raise StopAsyncIteration()
226 batch_chunks = {}
227 samples_to_remove = set()
229 # Create tasks for all active samples
230 tasks = {
231 idx: asyncio.create_task(self._get_chunk(idx))
232 for idx in self.active_samples
233 }
235 # Wait for at least one chunk to be ready
236 done, pending = await asyncio.wait(
237 tasks.values(),
238 return_when=asyncio.FIRST_COMPLETED,
239 timeout=self.streamer.timeout
240 )
242 # Cancel pending tasks
243 for task in pending:
244 task.cancel()
246 # Process completed tasks
247 for idx, task in tasks.items():
248 if task in done:
249 try:
250 value = await task
251 if value == self.streamer.stop_signal:
252 samples_to_remove.add(idx)
253 else:
254 batch_chunks[idx] = value
255 except asyncio.CancelledError:
256 pass
258 self.active_samples -= samples_to_remove
260 if batch_chunks:
261 return batch_chunks
262 elif self.active_samples:
263 # Try again if we still have active samples
264 return await self.__anext__()
265 else:
266 raise StopAsyncIteration()
268 async def _get_chunk(self, idx: int) -> Any:
269 """Helper to get a chunk from a specific queue."""
270 return await self.streamer.audio_queues[idx].get()