From 3b3f992834c8cf90c8cb8c9c1fcc7871d024b22a Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 6 Jul 2026 16:36:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(summary)=20support=20media=20files?= =?UTF-8?q?=20with=20bad=20streams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rarely media files may have one or multiple empty streams when they are badly formatted. The extract metadata code would crash when that happened. We now avoid crashing and create a clean file from the bad one to make sure API calls with that data works properly (observed some failures otherwise in my tests). --- CHANGELOG.md | 1 + src/summary/summary/core/file_service.py | 16 +++++++--- src/summary/tests/unit/test_file_service.py | 35 +++++++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df979fe..78e2c3f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to - 🚀(front) fix frontend build failure - 🐛(makefile) fix args in make test - 🩹(backend) fix case-insensitive email deduplication in merge command +- 🐛(summary) support media files with bad streams #1478 ## [1.22.0] - 2026-07-03 diff --git a/src/summary/summary/core/file_service.py b/src/summary/summary/core/file_service.py index 9088ccc9..1b4437f4 100644 --- a/src/summary/summary/core/file_service.py +++ b/src/summary/summary/core/file_service.py @@ -142,6 +142,7 @@ class MediaInfo: has_video: bool audio_duration_seconds: float | None audio_codec_name: str | None + has_bad_stream: bool = False def get_media_info(local_path: Path) -> MediaInfo: @@ -174,8 +175,9 @@ def get_media_info(local_path: Path) -> MediaInfo: data = json.loads(result.stdout) streams = data.get("streams", []) - has_audio = any(el["codec_type"] == "audio" for el in streams) - has_video = any(el["codec_type"] == "video" for el in streams) + has_audio = any(el.get("codec_type") == "audio" for el in streams) + has_video = any(el.get("codec_type") == "video" for el in streams) + has_bad_stream = any(el.get("codec_type", None) is None for el in streams) audio_codec_name = next( ( stream.get("codec_name") @@ -193,10 +195,11 @@ def get_media_info(local_path: Path) -> MediaInfo: has_video=has_video, audio_duration_seconds=audio_duration_seconds, audio_codec_name=audio_codec_name, + has_bad_stream=has_bad_stream, ) -def extract_audio_from_video(media_info: MediaInfo) -> Path: +def extract_audio_from_media(media_info: MediaInfo) -> Path: """Extracts the audio track from a video file and saves it as a separate audio file. Based on the provided audio codec, @@ -451,7 +454,12 @@ class FileService: if media_info.has_video: logger.info("Video file detected, extracting audio...") - processed_path = extract_audio_from_video(media_info) + processed_path = extract_audio_from_media(media_info) + # Bad streams may cause transcription issues on WhisperX, + # So we extract the audio properly + elif media_info.has_bad_stream: + logger.info("Bad stream detected, extracting audio...") + processed_path = extract_audio_from_media(media_info) else: processed_path = downloaded_path diff --git a/src/summary/tests/unit/test_file_service.py b/src/summary/tests/unit/test_file_service.py index 8f9a92dd..f7e6fc6d 100644 --- a/src/summary/tests/unit/test_file_service.py +++ b/src/summary/tests/unit/test_file_service.py @@ -1,12 +1,15 @@ """Unit tests for the file service.""" +import json from pathlib import Path +from unittest.mock import Mock import pytest +from summary.core import file_service from summary.core.file_service import ( MediaInfo, - extract_audio_from_video, + extract_audio_from_media, get_media_info, ) @@ -110,12 +113,40 @@ def test_media_info_invalid_file() -> None: ) +def test_media_info_ignores_empty_stream_entry(monkeypatch: pytest.MonkeyPatch) -> None: + """Test stream parsing when ffprobe returns an empty stream object.""" + ffprobe_payload = { + "programs": [], + "stream_groups": [], + "streams": [ + {"codec_name": "vorbis", "codec_type": "audio"}, + {}, + ], + } + + run_mock = Mock( + return_value=Mock(stdout=json.dumps(ffprobe_payload), stderr="", returncode=0) + ) + monkeypatch.setattr(file_service.subprocess, "run", run_mock) + monkeypatch.setattr( + file_service, "get_media_duration_seconds", Mock(return_value=2.5) + ) + + media_info = get_media_info(BASE_PATH / "audio-sample-android-firefox.ogg") + + assert media_info.has_audio is True + assert media_info.has_video is False + assert media_info.has_bad_stream is True + assert media_info.audio_codec_name == "vorbis" + assert media_info.audio_duration_seconds == 2.5 + + def test_extract_audio_from_video(): """Test that extract_audio_from_video can extract audio from a video file.""" path = None # A bit of cleanup logic since this is not a generator try: - path = extract_audio_from_video(MEDIA_INFO_SAMPLE_VISIO) + path = extract_audio_from_media(MEDIA_INFO_SAMPLE_VISIO) assert path.name.endswith(".m4a") except Exception as e: pytest.fail(f"Failed to extract audio from video: {e}")