59
AudioItem,
60
MultiModalDataDict,
61
MultiModalFieldConfig,
62
+ MultiModalInputs,
63
MultiModalKwargsItems,
64
VideoItem,
65
)
66
+from vllm.multimodal.media.audio import extract_audio_from_video_bytes
67
from vllm.multimodal.parse import (
68
AudioProcessorItems,
69
ImageEmbeddingItems,
71
ImageSize,
72
MultiModalDataItems,
73
MultiModalDataParser,
74
+ VideoProcessorItems,
75
+)
76
+from vllm.multimodal.processing import (
77
+ BaseDummyInputsBuilder,
78
+ ProcessorInputs,
79
+ TimingContext,
80
)
73
-from vllm.multimodal.processing import BaseDummyInputsBuilder
81
from vllm.multimodal.processing.processor import (
82
BaseMultiModalProcessor,
83
BaseProcessingInfo,
1388
):
1389
"""MultiModalProcessor extended for video support"""
1390
1391
+ def _extract_audio_from_videos(
1392
+ self,
1393
+ mm_items: MultiModalDataItems,
1394
+ ) -> tuple[MultiModalDataItems, list[AudioItem]]:
1395
+ """Extract audio tracks from video bytes in *mm_items*.
1396
+
1397
+ Returns:
1398
+ The augmented *mm_items* (with audio added) and the list of
1399
+ extracted audio items.
1400
+ """
1401
+ videos = mm_items.get_items("video", VideoProcessorItems)
1402
+ assert isinstance(videos.metadata, list)
1403
+ metadata_list = videos.metadata
1404
+
1405
+ audio_items: list[AudioItem] = []
1406
+ for metadata in metadata_list:
1407
+ video_bytes = metadata.get("original_video_bytes")
1408
+ if video_bytes is None or len(video_bytes) == 0:
1409
+ raise ValueError(
1410
+ "Cannot extract audio from video: original_video_bytes is "
1411
+ "missing or empty. When using use_audio_in_video=True, "
1412
+ "video must be loaded with keep_video_bytes=True (e.g. via "
1413
+ "the chat API with a model that sets use_audio_in_video)."
1414
+ )
1415
+ audio_items.append(extract_audio_from_video_bytes(video_bytes))
1416
+
1417
+ # Create a new VideoProcessorItems with metadata that does not contain
1418
+ # the large video bytes, to avoid modifying the input `mm_items`.
1419
+ new_metadata_list = [
1420
+ {k: v for k, v in meta.items() if k != "original_video_bytes"}
1421
+ for meta in metadata_list
1422
+ ]
1423
+ new_videos = VideoProcessorItems(data=videos.data, metadata=new_metadata_list)
1424
+
1425
+ audio_parsed = self.data_parser.parse_mm_data({"audio": audio_items})
1426
+
1427
+ # Create a new MultiModalDataItems with the new video and audio items.
1428
+ new_mm_items_dict = {**mm_items, **audio_parsed, "video": new_videos}
1429
+ mm_items = MultiModalDataItems(new_mm_items_dict)
1430
+
1431
+ return mm_items, audio_items
1432
+
1433
+ def apply(
1434
+ self,
1435
+ processor_inputs: ProcessorInputs,
1436
+ timing_ctx: TimingContext | None = None,
1437
+ ) -> MultiModalInputs:
1438
+ if (hf_processor_mm_kwargs := processor_inputs.hf_processor_mm_kwargs) is None:
1439
+ hf_processor_mm_kwargs = {}
1440
+
1441
+ use_audio_in_video = bool(
1442
+ hf_processor_mm_kwargs.get("use_audio_in_video", False)
1443
+ )
1444
+
1445
+ hf_processor_mm_kwargs = {
1446
+ k: v for k, v in hf_processor_mm_kwargs.items() if k != "use_audio_in_video"
1447
+ }
1448
+
1449
+ processor_inputs.hf_processor_mm_kwargs = hf_processor_mm_kwargs
1450
+
1451
+ if not (
1452
+ use_audio_in_video
1453
+ and "video" in processor_inputs.mm_data_items
1454
+ and "audio" not in processor_inputs.mm_data_items
1455
+ ):
1456
+ return super().apply(
1457
+ processor_inputs,
1458
+ timing_ctx,
1459
+ )
1460
+
1461
+ mm_items, audio_items = self._extract_audio_from_videos(
1462
+ processor_inputs.mm_data_items
1463
+ )
1464
+ processor_inputs.mm_data_items = mm_items
1465
+
1466
+ prompt = processor_inputs.prompt
1467
+ tokenizer = self.info.get_tokenizer()
1468
+ if not isinstance(prompt, str):
1469
+ prompt = tokenizer.decode(prompt, skip_special_tokens=False)
1470
+
1471
+ for _ in audio_items:
1472
+ prompt = prompt.replace("<video>", "<video>" + AUDIO_CONTEXT, 1)
1473
+
1474
+ processor_inputs.prompt = tokenizer.encode(prompt, add_special_tokens=False)
1475
+
1476
+ if processor_inputs.tokenization_kwargs is None:
1477
+ processor_inputs.tokenization_kwargs = {}
1478
+
1479
+ # Bypass the cached path: the HF processor must receive the
1480
+ # prompt (with injected <so_embedding>) and the audio data
1481
+ # together so it can perform audio-token replacement natively.
1482
+ (
1483
+ prompt_ids,
1484
+ mm_info,
1485
+ is_update_applied,
1486
+ ) = self._apply_hf_processor(
1487
+ processor_inputs,
1488
+ timing_ctx=timing_ctx,
1489
+ )
1490
+
1491
+ prompt_ids, mm_placeholders = self._maybe_apply_prompt_updates(
1492
+ mm_items=mm_items,
1493
+ prompt_ids=prompt_ids,
1494
+ mm_kwargs=mm_info.kwargs,
1495
+ mm_prompt_updates=mm_info.prompt_updates,
1496
+ is_update_applied=is_update_applied,
1497
+ )
1498
+
1499
+ mm_placeholder_ranges = {
1500
+ modality: [item.to_range() for item in placeholders]
1501
+ for modality, placeholders in mm_placeholders.items()
1502
+ }
1503
+
1504
+ return MultiModalInputs(
1505
+ type="multimodal",
1506
+ prompt_token_ids=prompt_ids,
1507
+ mm_kwargs=mm_info.kwargs,
1508
+ mm_hashes=mm_info.hashes,
1509
+ mm_placeholders=mm_placeholder_ranges,
1510
+ )
1511
+
1512
def _get_mm_fields_config(
1513
self,
1514
hf_inputs: BatchFeature,