Adds 3 new personas (Pixel, Lyra, Reel) and 5 new tool modules: - Git/DevOps tools (GitPython): clone, status, diff, log, blame, branch, add, commit, push, pull, stash — wired to Forge and Helm personas - Image generation (FLUX via diffusers): text-to-image, storyboards, variations — Pixel persona - Music generation (ACE-Step 1.5): full songs with vocals+instrumentals, instrumental tracks, vocal-only tracks — Lyra persona - Video generation (Wan 2.1 via diffusers): text-to-video, image-to-video clips — Reel persona - Creative Director pipeline: multi-step orchestration that chains storyboard → music → video → assembly into 3+ minute final videos - Video assembler (MoviePy + FFmpeg): stitch clips, overlay audio, title cards, subtitles, final export Also includes: - Spark Intelligence tool-level + creative pipeline event capture - Creative Studio dashboard page (/creative/ui) with 4 tabs - Config settings for all new models and output directories - pyproject.toml creative optional extra for GPU dependencies - 107 new tests covering all modules (624 total, all passing) https://claude.ai/code/session_01KJm6jQkNi3aA3yoQJn636c
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
"""Tests for creative.director — Creative Director pipeline.
|
|
|
|
Tests project management, pipeline orchestration, and tool catalogue.
|
|
All AI model calls are mocked.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from creative.director import (
|
|
create_project,
|
|
get_project,
|
|
list_projects,
|
|
run_storyboard,
|
|
run_music,
|
|
run_video_generation,
|
|
run_assembly,
|
|
run_full_pipeline,
|
|
CreativeProject,
|
|
DIRECTOR_TOOL_CATALOG,
|
|
_projects,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_projects():
|
|
"""Clear project store between tests."""
|
|
_projects.clear()
|
|
yield
|
|
_projects.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_project(tmp_path):
|
|
"""Create a sample project with scenes."""
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
result = create_project(
|
|
title="Test Video",
|
|
description="A test creative project",
|
|
scenes=[
|
|
{"description": "A sunrise over mountains"},
|
|
{"description": "A river flowing through a valley"},
|
|
{"description": "A sunset over the ocean"},
|
|
],
|
|
lyrics="La la la, the sun rises high",
|
|
)
|
|
return result["project"]["id"]
|
|
|
|
|
|
class TestCreateProject:
|
|
def test_creates_project(self, tmp_path):
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
result = create_project("My Video", "A cool video")
|
|
assert result["success"]
|
|
assert result["project"]["title"] == "My Video"
|
|
assert result["project"]["status"] == "planning"
|
|
|
|
def test_project_has_id(self, tmp_path):
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
result = create_project("Test", "Test")
|
|
assert len(result["project"]["id"]) == 12
|
|
|
|
def test_project_with_scenes(self, tmp_path):
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
result = create_project(
|
|
"Scenes", "With scenes",
|
|
scenes=[{"description": "Scene 1"}, {"description": "Scene 2"}],
|
|
)
|
|
assert result["project"]["scene_count"] == 2
|
|
|
|
|
|
class TestGetProject:
|
|
def test_get_existing(self, sample_project):
|
|
result = get_project(sample_project)
|
|
assert result is not None
|
|
assert result["title"] == "Test Video"
|
|
|
|
def test_get_nonexistent(self):
|
|
assert get_project("bogus") is None
|
|
|
|
|
|
class TestListProjects:
|
|
def test_empty(self):
|
|
assert list_projects() == []
|
|
|
|
def test_with_projects(self, sample_project, tmp_path):
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
create_project("Second", "desc")
|
|
assert len(list_projects()) == 2
|
|
|
|
|
|
class TestRunStoryboard:
|
|
def test_fails_without_project(self):
|
|
result = run_storyboard("bogus")
|
|
assert not result["success"]
|
|
assert "not found" in result["error"]
|
|
|
|
def test_fails_without_scenes(self, tmp_path):
|
|
with patch("creative.director._project_dir", return_value=tmp_path):
|
|
result = create_project("Empty", "No scenes")
|
|
pid = result["project"]["id"]
|
|
result = run_storyboard(pid)
|
|
assert not result["success"]
|
|
assert "No scenes" in result["error"]
|
|
|
|
def test_generates_frames(self, sample_project, tmp_path):
|
|
mock_result = {
|
|
"success": True,
|
|
"frame_count": 3,
|
|
"frames": [
|
|
{"path": "/fake/1.png", "scene_index": 0, "prompt": "sunrise"},
|
|
{"path": "/fake/2.png", "scene_index": 1, "prompt": "river"},
|
|
{"path": "/fake/3.png", "scene_index": 2, "prompt": "sunset"},
|
|
],
|
|
}
|
|
with patch("tools.image_tools.generate_storyboard", return_value=mock_result):
|
|
with patch("creative.director._save_project"):
|
|
result = run_storyboard(sample_project)
|
|
assert result["success"]
|
|
assert result["frame_count"] == 3
|
|
|
|
|
|
class TestRunMusic:
|
|
def test_fails_without_project(self):
|
|
result = run_music("bogus")
|
|
assert not result["success"]
|
|
|
|
def test_generates_track(self, sample_project):
|
|
mock_result = {
|
|
"success": True, "path": "/fake/song.wav",
|
|
"genre": "pop", "duration": 60,
|
|
}
|
|
with patch("tools.music_tools.generate_song", return_value=mock_result):
|
|
with patch("creative.director._save_project"):
|
|
result = run_music(sample_project, genre="pop")
|
|
assert result["success"]
|
|
assert result["path"] == "/fake/song.wav"
|
|
|
|
|
|
class TestRunVideoGeneration:
|
|
def test_fails_without_project(self):
|
|
result = run_video_generation("bogus")
|
|
assert not result["success"]
|
|
|
|
def test_generates_clips(self, sample_project):
|
|
mock_clip = {
|
|
"success": True, "path": "/fake/clip.mp4",
|
|
"duration": 5,
|
|
}
|
|
with patch("tools.video_tools.generate_video_clip", return_value=mock_clip):
|
|
with patch("tools.video_tools.image_to_video", return_value=mock_clip):
|
|
with patch("creative.director._save_project"):
|
|
result = run_video_generation(sample_project)
|
|
assert result["success"]
|
|
assert result["clip_count"] == 3
|
|
|
|
|
|
class TestRunAssembly:
|
|
def test_fails_without_project(self):
|
|
result = run_assembly("bogus")
|
|
assert not result["success"]
|
|
|
|
def test_fails_without_clips(self, sample_project):
|
|
result = run_assembly(sample_project)
|
|
assert not result["success"]
|
|
assert "No video clips" in result["error"]
|
|
|
|
|
|
class TestCreativeProject:
|
|
def test_to_dict(self):
|
|
p = CreativeProject(title="Test", description="Desc")
|
|
d = p.to_dict()
|
|
assert d["title"] == "Test"
|
|
assert d["status"] == "planning"
|
|
assert d["scene_count"] == 0
|
|
assert d["has_storyboard"] is False
|
|
assert d["has_music"] is False
|
|
|
|
|
|
class TestDirectorToolCatalog:
|
|
def test_catalog_has_all_tools(self):
|
|
expected = {
|
|
"create_project", "run_storyboard", "run_music",
|
|
"run_video_generation", "run_assembly", "run_full_pipeline",
|
|
}
|
|
assert expected == set(DIRECTOR_TOOL_CATALOG.keys())
|
|
|
|
def test_catalog_entries_callable(self):
|
|
for tool_id, info in DIRECTOR_TOOL_CATALOG.items():
|
|
assert callable(info["fn"])
|