1. 新增内置TTS工具,支持OpenAI兼容语音合成服务 2. 新增ASR音频解析和视频解析处理器 3. 扩展文件解析支持范围,添加音视频格式支持 4. 新增ffmpeg音视频处理基础工具库 5. 补充相关单元测试用例
131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
"""ffmpeg 基础设施模块单元测试。"""
|
|
|
|
import json
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.knowledge.parser.ffmpeg import (
|
|
FFMPEG_MAX_AUDIO_DURATION_SECS,
|
|
FFMPEG_MAX_BUFFER_BYTES,
|
|
FFMPEG_MAX_CONCURRENT,
|
|
FFMPEG_TIMEOUT_MS,
|
|
FFPROBE_TIMEOUT_MS,
|
|
FFmpegError,
|
|
MAX_AUDIO_BYTES,
|
|
MAX_VIDEO_BYTES,
|
|
convert_to_wav,
|
|
extract_audio_from_video,
|
|
extract_keyframes,
|
|
probe_audio_duration,
|
|
run_ffmpeg,
|
|
run_ffprobe,
|
|
)
|
|
|
|
|
|
class TestFFmpegError:
|
|
def test_error_attributes(self):
|
|
err = FFmpegError("test error", returncode=1)
|
|
assert str(err) == "test error"
|
|
assert err.returncode == 1
|
|
assert err.message == "test error"
|
|
|
|
def test_default_returncode(self):
|
|
err = FFmpegError("test")
|
|
assert err.returncode == -1
|
|
|
|
|
|
class TestRunFFmpeg:
|
|
@patch("yuxi.knowledge.parser.ffmpeg.subprocess.run")
|
|
def test_success(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=b"output", stderr=b"")
|
|
result = run_ffmpeg(["-version"])
|
|
assert result == "output"
|
|
mock_run.assert_called_once()
|
|
|
|
@patch("yuxi.knowledge.parser.ffmpeg.subprocess.run")
|
|
def test_timeout(self, mock_run):
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="ffmpeg", timeout=45)
|
|
with pytest.raises(FFmpegError, match="超时"):
|
|
run_ffmpeg(["-i", "input.mp4"])
|
|
|
|
@patch("yuxi.knowledge.parser.ffmpeg.subprocess.run")
|
|
def test_nonzero_returncode(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=1, stdout=b"", stderr=b"error msg")
|
|
with pytest.raises(FFmpegError, match="非零退出码"):
|
|
run_ffmpeg(["-i", "bad.mp4"])
|
|
|
|
|
|
class TestRunFFprobe:
|
|
@patch("yuxi.knowledge.parser.ffmpeg.subprocess.run")
|
|
def test_success(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=b'{"format":{}}', stderr=b"")
|
|
result = run_ffprobe(["-show_format", "test.mp3"])
|
|
assert result == '{"format":{}}'
|
|
|
|
|
|
class TestProbeAudioDuration:
|
|
@patch("yuxi.knowledge.parser.ffmpeg.run_ffprobe")
|
|
def test_success(self, mock_ffprobe):
|
|
mock_ffprobe.return_value = json.dumps({"format": {"duration": "120.5"}})
|
|
duration = probe_audio_duration("test.mp3")
|
|
assert duration == 120.5
|
|
|
|
@patch("yuxi.knowledge.parser.ffmpeg.run_ffprobe")
|
|
def test_failure_returns_none(self, mock_ffprobe):
|
|
mock_ffprobe.side_effect = FFmpegError("failed")
|
|
duration = probe_audio_duration("bad.mp3")
|
|
assert duration is None
|
|
|
|
|
|
class TestConvertToWav:
|
|
@patch("yuxi.knowledge.parser.ffmpeg.run_ffmpeg")
|
|
def test_calls_ffmpeg_with_correct_args(self, mock_ffmpeg):
|
|
convert_to_wav("input.mp3", "output.wav")
|
|
mock_ffmpeg.assert_called_once()
|
|
args = mock_ffmpeg.call_args[0][0]
|
|
assert "-i" in args
|
|
assert "input.mp3" in args
|
|
assert "-ar" in args
|
|
assert "16000" in args
|
|
assert "-ac" in args
|
|
assert "1" in args
|
|
|
|
|
|
class TestExtractAudioFromVideo:
|
|
@patch("yuxi.knowledge.parser.ffmpeg.run_ffmpeg")
|
|
def test_calls_ffmpeg_with_correct_args(self, mock_ffmpeg):
|
|
extract_audio_from_video("video.mp4", "audio.wav")
|
|
mock_ffmpeg.assert_called_once()
|
|
args = mock_ffmpeg.call_args[0][0]
|
|
assert "-i" in args
|
|
assert "video.mp4" in args
|
|
assert "-vn" in args
|
|
|
|
|
|
class TestExtractKeyframes:
|
|
@patch("yuxi.knowledge.parser.ffmpeg._detect_scene_change_pts", return_value=[])
|
|
def test_no_frames(self, mock_detect, tmp_path):
|
|
frames = extract_keyframes("video.mp4", str(tmp_path))
|
|
assert frames == []
|
|
|
|
@patch("yuxi.knowledge.parser.ffmpeg.run_ffmpeg")
|
|
@patch("yuxi.knowledge.parser.ffmpeg._detect_scene_change_pts", return_value=[5.0, 12.5])
|
|
def test_with_frames(self, mock_detect, mock_ffmpeg, tmp_path):
|
|
frames = extract_keyframes("video.mp4", str(tmp_path))
|
|
assert len(frames) == 2
|
|
assert frames[0][1] == 5.0
|
|
assert frames[1][1] == 12.5
|
|
|
|
|
|
class TestConstants:
|
|
def test_constants_values(self):
|
|
assert FFMPEG_MAX_BUFFER_BYTES == 10 * 1024 * 1024
|
|
assert FFPROBE_TIMEOUT_MS == 10_000
|
|
assert FFMPEG_TIMEOUT_MS == 45_000
|
|
assert FFMPEG_MAX_AUDIO_DURATION_SECS == 1200
|
|
assert MAX_AUDIO_BYTES == 16 * 1024 * 1024
|
|
assert MAX_VIDEO_BYTES == 16 * 1024 * 1024
|
|
assert FFMPEG_MAX_CONCURRENT == 4
|