148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
import base64
|
||||
|
|
import io
|
|||
|
|
import logging
|
|||
|
|
from collections.abc import Mapping
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
import soundfile as sf
|
|||
|
|
import torch
|
|||
|
|
from vllm.benchmarks.datasets import RandomMultiModalDataset, process_image, process_video
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def process_audio(audio: Any) -> Mapping[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Process a single audio input and return a multimedia content dictionary.
|
|||
|
|
|
|||
|
|
Supports the following input types:
|
|||
|
|
|
|||
|
|
1. Dictionary with raw audio bytes: - Expects a dict with a 'bytes' key
|
|||
|
|
containing raw audio data.
|
|||
|
|
|
|||
|
|
2. String input: - Treats the string as a URL or local file path. -
|
|||
|
|
Prepends "file://" if the string doesn't start with "http://" or
|
|||
|
|
"file://". - Returns a dictionary with the audio URL.
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
ValueError: If the input is not a supported type.
|
|||
|
|
"""
|
|||
|
|
if isinstance(audio, dict) and "bytes" in audio:
|
|||
|
|
audio_bytes = audio["bytes"]
|
|||
|
|
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
|||
|
|
return {
|
|||
|
|
"type": "audio_url",
|
|||
|
|
"audio_url": {"url": f"data:audio/mpeg;base64,{audio_base64}"},
|
|||
|
|
}
|
|||
|
|
if isinstance(audio, str):
|
|||
|
|
audio_url = audio if audio.startswith(("http://", "https://", "file://")) else f"file://{audio}"
|
|||
|
|
return {"type": "audio_url", "audio_url": {"url": audio_url}}
|
|||
|
|
|
|||
|
|
raise ValueError(
|
|||
|
|
f"Invalid audio input {audio}. Must be a string of local path/remote url, "
|
|||
|
|
f"or a dictionary with raw audio bytes in the form of `{{'bytes': raw_audio_bytes}}`."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# -----------------------------------------------------------------------------
|
|||
|
|
# MultiModalDataset Implementation
|
|||
|
|
# -----------------------------------------------------------------------------
|
|||
|
|
class OmniRandomMultiModalDataset(RandomMultiModalDataset):
|
|||
|
|
def __init__(self, **kwargs):
|
|||
|
|
super().__init__(**kwargs)
|
|||
|
|
|
|||
|
|
def generate_synthetic_audio(
|
|||
|
|
self,
|
|||
|
|
duration: int, # seconds
|
|||
|
|
num_channels: int, # 1:Mono,2:Stereo 5:5.1 surround sound
|
|||
|
|
) -> dict[str, Any]:
|
|||
|
|
"""Generate synthetic audio with random values.
|
|||
|
|
Default use 48000Hz.
|
|||
|
|
"""
|
|||
|
|
sample_rate = 48000
|
|||
|
|
num_samples = int(sample_rate * duration)
|
|||
|
|
audio_data = self._rng.uniform(-0.5, 0.5, (num_samples, num_channels))
|
|||
|
|
audio_data = np.clip(audio_data, -1.0, 1.0)
|
|||
|
|
audio_tensor = torch.FloatTensor(audio_data.T)
|
|||
|
|
audio_np = audio_tensor.numpy()
|
|||
|
|
|
|||
|
|
buffer = io.BytesIO()
|
|||
|
|
|
|||
|
|
sf.write(buffer, audio_np.T, sample_rate, format="wav")
|
|||
|
|
|
|||
|
|
buffer.seek(0)
|
|||
|
|
audio_bytes = buffer.read()
|
|||
|
|
buffer.close()
|
|||
|
|
return {
|
|||
|
|
"bytes": audio_bytes,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def generate_mm_item(
|
|||
|
|
self,
|
|||
|
|
mm_item_config: tuple[int, int, int],
|
|||
|
|
) -> Mapping[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Create synthetic images and videos and
|
|||
|
|
apply process_image/process_video respectively.
|
|||
|
|
This follows the OpenAI API chat completions
|
|||
|
|
https://github.com/openai/openai-python
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
if self.map_config_to_modality(mm_item_config) == "image":
|
|||
|
|
return process_image(self.generate_synthetic_image(mm_item_config[1], mm_item_config[0]))
|
|||
|
|
elif self.map_config_to_modality(mm_item_config) == "video":
|
|||
|
|
return process_video(self.generate_synthetic_video(mm_item_config[1], mm_item_config[0], mm_item_config[2]))
|
|||
|
|
elif self.map_config_to_modality(mm_item_config) == "audio":
|
|||
|
|
return process_audio(self.generate_synthetic_audio(mm_item_config[1], mm_item_config[2]))
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"Invalid multimodal item configuration: {mm_item_config}")
|
|||
|
|
|
|||
|
|
def generate_synthetic_video(self, width: int, height: int, num_frames: int) -> Any:
|
|||
|
|
"""Generate synthetic video with random values."""
|
|||
|
|
import imageio
|
|||
|
|
|
|||
|
|
video_data = self._rng.integers(
|
|||
|
|
0,
|
|||
|
|
256,
|
|||
|
|
(num_frames, height, width, 3),
|
|||
|
|
dtype=np.uint8,
|
|||
|
|
)
|
|||
|
|
buffer = io.BytesIO()
|
|||
|
|
writer_kwargs = {
|
|||
|
|
"format": "mp4",
|
|||
|
|
"fps": 30,
|
|||
|
|
"codec": "libx264",
|
|||
|
|
"quality": 7,
|
|||
|
|
"pixelformat": "yuv420p",
|
|||
|
|
"macro_block_size": 16,
|
|||
|
|
"ffmpeg_params": [
|
|||
|
|
"-preset",
|
|||
|
|
"medium",
|
|||
|
|
"-crf",
|
|||
|
|
"23",
|
|||
|
|
"-movflags",
|
|||
|
|
"+faststart",
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
with imageio.get_writer(buffer, **writer_kwargs) as writer:
|
|||
|
|
for frame_idx in range(num_frames):
|
|||
|
|
writer.append_data(video_data[frame_idx])
|
|||
|
|
buffer.seek(0)
|
|||
|
|
video_bytes = buffer.read()
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"bytes": video_bytes,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def map_config_to_modality(self, config: tuple[int, int, int]) -> str:
|
|||
|
|
"""Map the configuration to the modality."""
|
|||
|
|
if config[0] == 0:
|
|||
|
|
return "audio"
|
|||
|
|
elif config[-1] == 1:
|
|||
|
|
return "image"
|
|||
|
|
elif config[-1] > 1:
|
|||
|
|
return "video"
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"Invalid multimodal item configuration: {config}")
|