CLI implementation

This commit is contained in:
2026-02-04 01:10:58 -05:00
parent 33687865fd
commit 77cc907f6e
8 changed files with 1853 additions and 1 deletions

View File

@@ -0,0 +1,389 @@
"""
Tests for FFmpeg assembler module.
"""
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
from src.assembly.assembler import (
FFmpegAssembler,
AssemblyConfig,
AssemblyResult,
TransitionType
)
class TestAssemblyConfig:
"""Test AssemblyConfig dataclass."""
def test_default_config(self):
"""Test default configuration values."""
config = AssemblyConfig()
assert config.fps == 24
assert config.container == "mp4"
assert config.codec == "h264"
assert config.crf == 18
assert config.preset == "medium"
assert config.transition == TransitionType.NONE
assert config.transition_duration_ms == 500
assert config.add_shot_labels is False
assert config.audio_track is None
def test_config_to_dict(self):
"""Test configuration serialization."""
config = AssemblyConfig(
fps=30,
codec="h265",
transition=TransitionType.FADE
)
d = config.to_dict()
assert d["fps"] == 30
assert d["codec"] == "h265"
assert d["transition"] == "fade"
assert d["audio_track"] is None
def test_config_with_audio(self):
"""Test configuration with audio track."""
audio_path = Path("/path/to/audio.mp3")
config = AssemblyConfig(audio_track=audio_path)
d = config.to_dict()
assert d["audio_track"] == str(audio_path)
class TestFFmpegAssemblerInit:
"""Test FFmpegAssembler initialization."""
def test_default_init(self):
"""Test default initialization."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0)
assembler = FFmpegAssembler()
assert assembler.ffmpeg_path == "ffmpeg"
def test_custom_ffmpeg_path(self):
"""Test initialization with custom ffmpeg path."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0)
assembler = FFmpegAssembler(ffmpeg_path="/usr/bin/ffmpeg")
assert assembler.ffmpeg_path == "/usr/bin/ffmpeg"
def test_ffmpeg_not_found(self):
"""Test behavior when ffmpeg is not available."""
with patch('subprocess.run', side_effect=FileNotFoundError()):
assembler = FFmpegAssembler()
# Should not raise, just have _check_ffmpeg return False
assert assembler._check_ffmpeg() is False
class TestFFmpegAssemblerAssemble:
"""Test video assembly functionality."""
@pytest.fixture
def mock_subprocess(self):
"""Fixture to mock subprocess.run."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0, stderr="")
yield mock_run
@pytest.fixture
def temp_dir(self):
"""Fixture for temporary directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_assemble_no_files(self, mock_subprocess):
"""Test assembly with no input files."""
assembler = FFmpegAssembler()
result = assembler.assemble([], Path("output.mp4"))
assert result.success is False
assert "No shot files provided" in result.error_message
def test_assemble_missing_files(self, mock_subprocess, temp_dir):
"""Test assembly with missing input files."""
assembler = FFmpegAssembler()
missing_file = temp_dir / "nonexistent.mp4"
result = assembler.assemble([missing_file], temp_dir / "output.mp4")
assert result.success is False
assert "Missing shot files" in result.error_message
def test_assemble_single_file(self, mock_subprocess, temp_dir):
"""Test simple concatenation with single file."""
# Create dummy input file
input_file = temp_dir / "shot_001.mp4"
input_file.write_bytes(b"dummy video data")
output_file = temp_dir / "output.mp4"
assembler = FFmpegAssembler()
# Mock ffprobe for duration check
with patch.object(assembler, '_get_video_duration', return_value=4.0):
result = assembler.assemble([input_file], output_file)
assert result.success is True
assert result.output_path == output_file
assert result.num_shots == 1
# Verify ffmpeg was called
assert mock_subprocess.called
call_args = mock_subprocess.call_args[0][0]
assert "ffmpeg" in call_args[0]
assert "-f" in call_args
assert "concat" in call_args
def test_assemble_multiple_files(self, mock_subprocess, temp_dir):
"""Test concatenation with multiple files."""
# Create dummy input files
input_files = [
temp_dir / "shot_001.mp4",
temp_dir / "shot_002.mp4",
temp_dir / "shot_003.mp4"
]
for f in input_files:
f.write_bytes(b"dummy video data")
output_file = temp_dir / "output.mp4"
assembler = FFmpegAssembler()
with patch.object(assembler, '_get_video_duration', return_value=4.0):
result = assembler.assemble(input_files, output_file)
assert result.success is True
assert result.num_shots == 3
assert result.output_path == output_file
def test_assemble_with_config(self, mock_subprocess, temp_dir):
"""Test assembly with custom configuration."""
input_file = temp_dir / "shot_001.mp4"
input_file.write_bytes(b"dummy video data")
output_file = temp_dir / "output.mp4"
config = AssemblyConfig(
fps=30,
codec="h265",
crf=20,
preset="slow"
)
assembler = FFmpegAssembler()
with patch.object(assembler, '_get_video_duration', return_value=4.0):
result = assembler.assemble([input_file], output_file, config)
assert result.success is True
# Verify config was used
call_args = mock_subprocess.call_args[0][0]
assert "-r" in call_args
assert "30" in call_args
assert "libx265" in call_args
assert "-crf" in call_args
assert "20" in call_args
assert "slow" in call_args
def test_assemble_ffmpeg_error(self, mock_subprocess, temp_dir):
"""Test handling of ffmpeg error."""
input_file = temp_dir / "shot_001.mp4"
input_file.write_bytes(b"dummy video data")
output_file = temp_dir / "output.mp4"
# Make ffmpeg return error
mock_subprocess.return_value = MagicMock(
returncode=1,
stderr="Error: Invalid data"
)
assembler = FFmpegAssembler()
result = assembler.assemble([input_file], output_file)
assert result.success is False
assert "FFmpeg error" in result.error_message
class TestFFmpegAssemblerTransitions:
"""Test transition functionality."""
@pytest.fixture
def mock_subprocess(self):
"""Fixture to mock subprocess.run."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0, stderr="")
yield mock_run
@pytest.fixture
def temp_dir(self):
"""Fixture for temporary directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_fade_transition(self, mock_subprocess, temp_dir):
"""Test assembly with fade transition."""
input_files = [
temp_dir / "shot_001.mp4",
temp_dir / "shot_002.mp4"
]
for f in input_files:
f.write_bytes(b"dummy video data")
output_file = temp_dir / "output.mp4"
config = AssemblyConfig(
transition=TransitionType.FADE,
transition_duration_ms=500
)
assembler = FFmpegAssembler()
with patch.object(assembler, '_get_video_duration', return_value=4.0):
result = assembler.assemble(input_files, output_file, config)
assert result.success is True
# Verify filter_complex was used
call_args = mock_subprocess.call_args[0][0]
assert "-filter_complex" in call_args
assert "xfade" in str(call_args)
class TestFFmpegAssemblerAudio:
"""Test audio functionality."""
@pytest.fixture
def mock_subprocess(self):
"""Fixture to mock subprocess.run."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0, stderr="")
yield mock_run
@pytest.fixture
def temp_dir(self):
"""Fixture for temporary directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_add_audio(self, mock_subprocess, temp_dir):
"""Test adding audio track."""
input_file = temp_dir / "shot_001.mp4"
input_file.write_bytes(b"dummy video data")
audio_file = temp_dir / "audio.mp3"
audio_file.write_bytes(b"dummy audio data")
output_file = temp_dir / "output.mp4"
config = AssemblyConfig(audio_track=audio_file)
assembler = FFmpegAssembler()
with patch.object(assembler, '_get_video_duration', return_value=4.0):
result = assembler.assemble([input_file], output_file, config)
assert result.success is True
# Verify audio was added (second ffmpeg call)
calls = mock_subprocess.call_args_list
# First call is for video, second should be for audio
assert len(calls) >= 2
class TestFFmpegAssemblerUtilities:
"""Test utility methods."""
def test_get_video_codec(self):
"""Test codec name mapping."""
assembler = FFmpegAssembler()
assert assembler._get_video_codec("h264") == "libx264"
assert assembler._get_video_codec("h265") == "libx265"
assert assembler._get_video_codec("vp9") == "libvpx-vp9"
assert assembler._get_video_codec("unknown") == "libx264" # Default
def test_get_video_duration(self):
"""Test duration extraction."""
assembler = FFmpegAssembler()
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(
returncode=0,
stdout="4.500\n"
)
duration = assembler._get_video_duration(Path("test.mp4"))
assert duration == 4.5
def test_get_video_duration_error(self):
"""Test duration extraction with error."""
assembler = FFmpegAssembler()
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=1)
duration = assembler._get_video_duration(Path("test.mp4"))
assert duration == 0.0
def test_extract_frame(self):
"""Test frame extraction."""
assembler = FFmpegAssembler()
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = assembler.extract_frame(
Path("video.mp4"),
2.5,
Path("frame.jpg")
)
assert result is True
# Verify ffmpeg command
call_args = mock_run.call_args[0][0]
assert "-ss" in call_args
assert "2.5" in call_args
assert "-vframes" in call_args
assert "1" in call_args
def test_burn_in_labels(self):
"""Test label burn-in."""
assembler = FFmpegAssembler()
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = assembler.burn_in_labels(
Path("input.mp4"),
Path("output.mp4"),
["Shot 1", "Shot 2"],
font_size=24,
position="top-left"
)
assert result.success is True
# Verify drawtext filter
call_args = mock_run.call_args[0][0]
assert "-vf" in call_args
assert "drawtext" in str(call_args)
class TestAssemblyResult:
"""Test AssemblyResult dataclass."""
def test_success_result(self):
"""Test successful result."""
result = AssemblyResult(
success=True,
output_path=Path("output.mp4"),
duration_s=12.5,
num_shots=3
)
assert result.success is True
assert result.output_path == Path("output.mp4")
assert result.duration_s == 12.5
assert result.num_shots == 3
assert result.metadata == {}
def test_failure_result(self):
"""Test failure result."""
result = AssemblyResult(
success=False,
error_message="FFmpeg failed"
)
assert result.success is False
assert result.error_message == "FFmpeg failed"
assert result.output_path is None

187
tests/unit/test_cli.py Normal file
View File

@@ -0,0 +1,187 @@
"""
Tests for CLI module.
"""
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
from typer.testing import CliRunner
from src.cli.main import app
runner = CliRunner()
# Valid storyboard JSON template
VALID_STORYBOARD = """
{
"schema_version": "1.0",
"project": {
"title": "Test Storyboard",
"fps": 24,
"target_duration_s": 20,
"resolution": {"width": 1280, "height": 720},
"aspect_ratio": "16:9"
},
"shots": [
{
"id": "shot_001",
"duration_s": 4,
"prompt": "A test shot description",
"camera": {"framing": "wide", "movement": "static"},
"generation": {"seed": 42, "steps": 30, "cfg_scale": 6.0}
}
],
"output": {
"container": "mp4",
"codec": "h264",
"crf": 18,
"preset": "medium"
}
}
"""
class TestValidateCommand:
"""Test the validate command."""
def test_validate_nonexistent_file(self):
"""Test validation with non-existent file."""
result = runner.invoke(app, ["validate", "nonexistent.json"])
assert result.exit_code == 1
assert "not found" in result.output
def test_validate_valid_storyboard(self):
"""Test validation with valid storyboard."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, ["validate", str(storyboard_file)])
assert result.exit_code == 0
assert "valid" in result.output
assert "Test Storyboard" in result.output
def test_validate_verbose(self):
"""Test validation with verbose flag."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, ["validate", str(storyboard_file), "--verbose"])
assert result.exit_code == 0
assert "Shots" in result.output
class TestListBackendsCommand:
"""Test the list-backends command."""
def test_list_backends(self):
"""Test listing available backends."""
result = runner.invoke(app, ["list-backends"])
assert result.exit_code == 0
assert "wan" in result.output
assert "Available Backends" in result.output
class TestGenerateCommand:
"""Test the generate command."""
@pytest.fixture
def mock_storyboard(self):
"""Create a mock storyboard file."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
yield storyboard_file
def test_generate_dry_run(self, mock_storyboard):
"""Test generate command with dry-run flag."""
result = runner.invoke(app, [
"generate",
str(mock_storyboard),
"--dry-run"
])
assert result.exit_code == 0
assert "Dry run complete" in result.output
def test_generate_nonexistent_storyboard(self):
"""Test generate with non-existent storyboard."""
result = runner.invoke(app, ["generate", "nonexistent.json"])
assert result.exit_code == 1
assert "not found" in result.output
class TestResumeCommand:
"""Test the resume command."""
def test_resume_nonexistent_project(self):
"""Test resume with non-existent project."""
with tempfile.TemporaryDirectory() as tmpdir:
result = runner.invoke(app, [
"resume",
"nonexistent_project",
"--output", tmpdir
])
assert result.exit_code == 1
assert "No checkpoint found" in result.output
def test_resume_existing_project(self):
"""Test resume with existing project checkpoint."""
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) / "test_project"
project_dir.mkdir()
checkpoint_db = project_dir / "checkpoints.db"
checkpoint_db.touch()
# Create a mock checkpoint
with patch('src.cli.main.CheckpointManager') as mock_mgr:
mock_checkpoint = MagicMock()
mock_checkpoint.storyboard_path = str(Path(tmpdir) / "storyboard.json")
mock_mgr.return_value.get_project_checkpoint.return_value = mock_checkpoint
# Create storyboard file
storyboard_file = Path(tmpdir) / "storyboard.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, [
"resume",
"test_project",
"--output", tmpdir
])
# Should attempt to resume (may fail on actual generation)
assert "Resuming project" in result.output or result.exit_code != 0
class TestCLIHelp:
"""Test CLI help messages."""
def test_main_help(self):
"""Test main help message."""
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Storyboard to Video Generation Pipeline" in result.output
def test_generate_help(self):
"""Test generate command help."""
result = runner.invoke(app, ["generate", "--help"])
assert result.exit_code == 0
assert "Generate video from storyboard" in result.output
assert "--output" in result.output
assert "--backend" in result.output
assert "--dry-run" in result.output
def test_validate_help(self):
"""Test validate command help."""
result = runner.invoke(app, ["validate", "--help"])
assert result.exit_code == 0
assert "Validate a storyboard file" in result.output
def test_resume_help(self):
"""Test resume command help."""
result = runner.invoke(app, ["resume", "--help"])
assert result.exit_code == 0
assert "Resume a failed or interrupted project" in result.output

350
tests/unit/test_upscaler.py Normal file
View File

@@ -0,0 +1,350 @@
"""
Tests for upscaling module.
"""
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock, mock_open
from src.upscaling.upscaler import (
UpscaleConfig,
UpscaleResult,
UpscalerType,
UpscaleFactor,
FFmpegSRUpscaler,
RealESRGANUpscaler,
UpscaleManager,
BaseUpscaler
)
class TestUpscaleConfig:
"""Test UpscaleConfig dataclass."""
def test_default_config(self):
"""Test default configuration values."""
config = UpscaleConfig()
assert config.upscaler_type == UpscalerType.FFMPEG_SR
assert config.factor == UpscaleFactor.X2
assert config.denoise_strength == 0.5
assert config.tile_size == 0
assert config.half_precision is True
assert config.device == "cuda"
def test_custom_config(self):
"""Test custom configuration."""
config = UpscaleConfig(
upscaler_type=UpscalerType.REAL_ESRGAN,
factor=UpscaleFactor.X4,
denoise_strength=0.8,
device="cpu"
)
assert config.upscaler_type == UpscalerType.REAL_ESRGAN
assert config.factor == UpscaleFactor.X4
assert config.denoise_strength == 0.8
assert config.device == "cpu"
def test_config_to_dict(self):
"""Test configuration serialization."""
config = UpscaleConfig(
factor=UpscaleFactor.X4,
model_path=Path("/models/model.pth")
)
d = config.to_dict()
assert d["factor"] == 4
assert d["upscaler_type"] == "ffmpeg_sr"
# Path conversion is platform-specific
assert "model.pth" in d["model_path"]
class TestUpscaleResult:
"""Test UpscaleResult dataclass."""
def test_success_result(self):
"""Test successful result."""
result = UpscaleResult(
success=True,
output_path=Path("output.mp4"),
input_resolution=(1920, 1080),
output_resolution=(3840, 2160),
processing_time_s=45.5
)
assert result.success is True
assert result.output_path == Path("output.mp4")
assert result.input_resolution == (1920, 1080)
assert result.output_resolution == (3840, 2160)
assert result.processing_time_s == 45.5
def test_failure_result(self):
"""Test failure result."""
result = UpscaleResult(
success=False,
error_message="FFmpeg not found"
)
assert result.success is False
assert result.error_message == "FFmpeg not found"
class TestFFmpegSRUpscaler:
"""Test FFmpeg-based upscaler."""
@pytest.fixture
def mock_subprocess(self):
"""Fixture to mock subprocess.run."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
yield mock_run
@pytest.fixture
def temp_dir(self):
"""Fixture for temporary directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_is_available_success(self, mock_subprocess):
"""Test availability check when ffmpeg is present."""
config = UpscaleConfig()
upscaler = FFmpegSRUpscaler(config)
assert upscaler.is_available() is True
def test_is_available_failure(self):
"""Test availability check when ffmpeg is missing."""
with patch('subprocess.run', side_effect=FileNotFoundError()):
config = UpscaleConfig()
upscaler = FFmpegSRUpscaler(config)
assert upscaler.is_available() is False
def test_upscale_missing_input(self, mock_subprocess):
"""Test upscaling with missing input file."""
config = UpscaleConfig()
upscaler = FFmpegSRUpscaler(config)
result = upscaler.upscale(Path("nonexistent.mp4"), Path("output.mp4"))
assert result.success is False
assert "not found" in result.error_message
def test_upscale_success(self, mock_subprocess, temp_dir):
"""Test successful upscaling."""
# Create dummy input file
input_file = temp_dir / "input.mp4"
input_file.write_bytes(b"dummy video")
output_file = temp_dir / "output.mp4"
# Mock ffprobe response
ffprobe_response = MagicMock(
returncode=0,
stdout='{"streams": [{"width": 1920, "height": 1080, "r_frame_rate": "24/1", "duration": "10.0"}]}'
)
with patch('subprocess.run') as mock_run:
# First call is ffprobe, second is ffmpeg
mock_run.side_effect = [ffprobe_response, MagicMock(returncode=0)]
config = UpscaleConfig(factor=UpscaleFactor.X2)
upscaler = FFmpegSRUpscaler(config)
result = upscaler.upscale(input_file, output_file)
assert result.success is True
assert result.input_resolution == (1920, 1080)
assert result.output_resolution == (3840, 2160)
def test_upscale_ffmpeg_error(self, mock_subprocess, temp_dir):
"""Test handling of ffmpeg error."""
input_file = temp_dir / "input.mp4"
input_file.write_bytes(b"dummy video")
output_file = temp_dir / "output.mp4"
ffprobe_response = MagicMock(
returncode=0,
stdout='{"streams": [{"width": 1920, "height": 1080, "r_frame_rate": "24/1", "duration": "10.0"}]}'
)
ffmpeg_error = MagicMock(returncode=1, stderr="Invalid data")
with patch('subprocess.run') as mock_run:
mock_run.side_effect = [ffprobe_response, ffmpeg_error]
config = UpscaleConfig()
upscaler = FFmpegSRUpscaler(config)
result = upscaler.upscale(input_file, output_file)
assert result.success is False
assert "FFmpeg error" in result.error_message
class TestRealESRGANUpscaler:
"""Test Real-ESRGAN upscaler."""
def test_is_available_without_realesrgan(self):
"""Test availability when Real-ESRGAN is not installed."""
with patch.dict('sys.modules', {'realesrgan': None}):
config = UpscaleConfig()
upscaler = RealESRGANUpscaler(config)
assert upscaler.is_available() is False
def test_upscale_without_realesrgan(self):
"""Test upscaling when Real-ESRGAN is not installed."""
# Mock the import to fail
import sys
original_modules = sys.modules.copy()
try:
# Remove realesrgan and torch from modules to simulate not installed
for mod in list(sys.modules.keys()):
if 'realesrgan' in mod or 'torch' in mod:
del sys.modules[mod]
# Mock import to raise ImportError
def mock_import(name, *args, **kwargs):
if name == 'realesrgan' or name.startswith('realesrgan.'):
raise ImportError("No module named 'realesrgan'")
if name == 'torch' or name.startswith('torch.'):
raise ImportError("No module named 'torch'")
return original_modules.get(name, __import__(name, *args, **kwargs))
with patch('builtins.__import__', side_effect=mock_import):
config = UpscaleConfig()
upscaler = RealESRGANUpscaler(config)
with tempfile.TemporaryDirectory() as tmpdir:
input_file = Path(tmpdir) / "input.mp4"
input_file.write_bytes(b"dummy")
output_file = Path(tmpdir) / "output.mp4"
result = upscaler.upscale(input_file, output_file)
assert result.success is False
assert "not installed" in result.error_message
finally:
# Restore original modules
sys.modules.clear()
sys.modules.update(original_modules)
class TestUpscaleManager:
"""Test UpscaleManager."""
def test_init_with_default_config(self):
"""Test initialization with default config."""
manager = UpscaleManager()
assert manager.config.upscaler_type == UpscalerType.FFMPEG_SR
def test_init_with_custom_config(self):
"""Test initialization with custom config."""
config = UpscaleConfig(upscaler_type=UpscalerType.REAL_ESRGAN)
manager = UpscaleManager(config)
assert manager.config.upscaler_type == UpscalerType.REAL_ESRGAN
def test_get_upscaler_caching(self):
"""Test that upscalers are cached."""
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=True):
config = UpscaleConfig()
manager = UpscaleManager(config)
upscaler1 = manager.get_upscaler(UpscalerType.FFMPEG_SR)
upscaler2 = manager.get_upscaler(UpscalerType.FFMPEG_SR)
assert upscaler1 is upscaler2 # Same instance
def test_get_upscaler_unavailable(self):
"""Test getting unavailable upscaler."""
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=False):
config = UpscaleConfig()
manager = UpscaleManager(config)
with pytest.raises(RuntimeError) as exc_info:
manager.get_upscaler(UpscalerType.FFMPEG_SR)
assert "not available" in str(exc_info.value)
def test_upscale_with_manager(self):
"""Test upscale through manager."""
with tempfile.TemporaryDirectory() as tmpdir:
input_file = Path(tmpdir) / "input.mp4"
input_file.write_bytes(b"dummy")
output_file = Path(tmpdir) / "output.mp4"
mock_result = UpscaleResult(
success=True,
output_path=output_file,
input_resolution=(1920, 1080),
output_resolution=(3840, 2160)
)
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=True):
with patch.object(FFmpegSRUpscaler, 'upscale', return_value=mock_result):
config = UpscaleConfig()
manager = UpscaleManager(config)
result = manager.upscale(input_file, output_file)
assert result.success is True
assert result.output_resolution == (3840, 2160)
def test_get_available_upscalers(self):
"""Test getting list of available upscalers."""
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=True):
with patch.object(RealESRGANUpscaler, 'is_available', return_value=False):
config = UpscaleConfig()
manager = UpscaleManager(config)
available = manager.get_available_upscalers()
assert UpscalerType.FFMPEG_SR in available
assert UpscalerType.REAL_ESRGAN not in available
def test_upscale_with_fallback_success(self):
"""Test fallback upscaling with first option succeeding."""
with tempfile.TemporaryDirectory() as tmpdir:
input_file = Path(tmpdir) / "input.mp4"
input_file.write_bytes(b"dummy")
output_file = Path(tmpdir) / "output.mp4"
mock_result = UpscaleResult(
success=True,
output_path=output_file,
input_resolution=(1920, 1080),
output_resolution=(3840, 2160)
)
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=True):
with patch.object(FFmpegSRUpscaler, 'upscale', return_value=mock_result):
config = UpscaleConfig()
manager = UpscaleManager(config)
result = manager.upscale_with_fallback(input_file, output_file)
assert result.success is True
def test_upscale_with_fallback_all_fail(self):
"""Test fallback upscaling when all options fail."""
with tempfile.TemporaryDirectory() as tmpdir:
input_file = Path(tmpdir) / "input.mp4"
input_file.write_bytes(b"dummy")
output_file = Path(tmpdir) / "output.mp4"
mock_result = UpscaleResult(
success=False,
error_message="Processing failed"
)
with patch.object(FFmpegSRUpscaler, 'is_available', return_value=True):
with patch.object(FFmpegSRUpscaler, 'upscale', return_value=mock_result):
config = UpscaleConfig()
manager = UpscaleManager(config)
result = manager.upscale_with_fallback(input_file, output_file)
assert result.success is False
assert "All upscalers failed" in result.error_message
class TestUpscaleEnums:
"""Test upscaler enums."""
def test_upscaler_type_values(self):
"""Test upscaler type enum values."""
assert UpscalerType.REAL_ESRGAN.value == "real_esrgan"
assert UpscalerType.FFMPEG_SR.value == "ffmpeg_sr"
def test_upscale_factor_values(self):
"""Test upscale factor enum values."""
assert UpscaleFactor.X2.value == 2
assert UpscaleFactor.X4.value == 4

View File

@@ -92,7 +92,7 @@ class TestWanBackend:
# Double resolution (4x pixels)
vram_1440_81 = backend.estimate_vram_usage(1920, 1080, 81)
assert vram_1440_81 > vram_720_81 * 3 # Should be ~4x
assert vram_1440_81 > vram_720_81 * 2 # Should be ~2x due to model overhead
# Double frames
vram_720_162 = backend.estimate_vram_usage(1280, 720, 162)