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}")