Files
Timmy-time-dashboard/tests/fixtures/media.py

183 lines
5.5 KiB
Python

"""Real media file fixtures for integration tests.
Generates actual PNG images, WAV audio files, and MP4 video clips
using numpy, Pillow, and MoviePy — no AI models required.
"""
from __future__ import annotations
import wave
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
# ── Color palettes for visual variety ─────────────────────────────────────────
SCENE_COLORS = [
(30, 60, 120), # dark blue — "night sky"
(200, 100, 30), # warm orange — "sunrise"
(50, 150, 50), # forest green — "mountain forest"
(20, 120, 180), # teal blue — "river"
(180, 60, 60), # crimson — "sunset"
(40, 40, 80), # deep purple — "twilight"
]
def make_storyboard_frame(
path: Path,
label: str,
color: tuple[int, int, int] = (60, 60, 60),
width: int = 320,
height: int = 180,
) -> Path:
"""Create a real PNG image with a colored background and text label.
Returns the path to the written file.
"""
img = Image.new("RGB", (width, height), color=color)
draw = ImageDraw.Draw(img)
# Draw label text in white, centered
bbox = draw.textbbox((0, 0), label)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
x = (width - tw) // 2
y = (height - th) // 2
draw.text((x, y), label, fill=(255, 255, 255))
# Add a border
draw.rectangle([2, 2, width - 3, height - 3], outline=(255, 255, 255), width=2)
path.parent.mkdir(parents=True, exist_ok=True)
img.save(path)
return path
def make_storyboard(
output_dir: Path,
scene_labels: list[str],
width: int = 320,
height: int = 180,
) -> list[Path]:
"""Generate a full storyboard — one PNG per scene."""
frames = []
for i, label in enumerate(scene_labels):
color = SCENE_COLORS[i % len(SCENE_COLORS)]
path = output_dir / f"frame_{i:03d}.png"
make_storyboard_frame(path, label, color=color, width=width, height=height)
frames.append(path)
return frames
def make_audio_track(
path: Path,
duration_seconds: float = 10.0,
sample_rate: int = 44100,
frequency: float = 440.0,
fade_in: float = 0.5,
fade_out: float = 0.5,
) -> Path:
"""Create a real WAV audio file — a sine wave tone with fade in/out.
Good enough to verify audio overlay, mixing, and codec encoding.
"""
n_samples = int(sample_rate * duration_seconds)
t = np.linspace(0, duration_seconds, n_samples, endpoint=False)
# Generate a sine wave with slight frequency variation for realism
signal = np.sin(2 * np.pi * frequency * t)
# Add a second harmonic for richness
signal += 0.3 * np.sin(2 * np.pi * frequency * 2 * t)
# Fade in/out
fade_in_samples = int(sample_rate * fade_in)
fade_out_samples = int(sample_rate * fade_out)
if fade_in_samples > 0:
signal[:fade_in_samples] *= np.linspace(0, 1, fade_in_samples)
if fade_out_samples > 0:
signal[-fade_out_samples:] *= np.linspace(1, 0, fade_out_samples)
# Normalize and convert to 16-bit PCM
signal = (signal / np.max(np.abs(signal)) * 32767 * 0.8).astype(np.int16)
path.parent.mkdir(parents=True, exist_ok=True)
with wave.open(str(path), "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(signal.tobytes())
return path
def make_video_clip(
path: Path,
duration_seconds: float = 3.0,
fps: int = 12,
width: int = 320,
height: int = 180,
color_start: tuple[int, int, int] = (30, 30, 80),
color_end: tuple[int, int, int] = (80, 30, 30),
label: str = "",
) -> Path:
"""Create a real MP4 video clip with a color gradient animation.
Frames transition smoothly from color_start to color_end,
producing a visible animation that's easy to visually verify.
"""
from moviepy import ImageSequenceClip
n_frames = int(duration_seconds * fps)
frames = []
for i in range(n_frames):
t = i / max(1, n_frames - 1)
r = int(color_start[0] + (color_end[0] - color_start[0]) * t)
g = int(color_start[1] + (color_end[1] - color_start[1]) * t)
b = int(color_start[2] + (color_end[2] - color_start[2]) * t)
img = Image.new("RGB", (width, height), color=(r, g, b))
if label:
draw = ImageDraw.Draw(img)
draw.text((10, 10), label, fill=(255, 255, 255))
# Frame counter
draw.text((10, height - 20), f"f{i}/{n_frames}", fill=(200, 200, 200))
frames.append(np.array(img))
path.parent.mkdir(parents=True, exist_ok=True)
clip = ImageSequenceClip(frames, fps=fps)
clip.write_videofile(str(path), codec="libx264", audio=False, logger=None)
return path
def make_scene_clips(
output_dir: Path,
scene_labels: list[str],
duration_per_clip: float = 3.0,
fps: int = 12,
width: int = 320,
height: int = 180,
) -> list[Path]:
"""Generate one video clip per scene, each with a distinct color animation."""
clips = []
for i, label in enumerate(scene_labels):
c1 = SCENE_COLORS[i % len(SCENE_COLORS)]
c2 = SCENE_COLORS[(i + 1) % len(SCENE_COLORS)]
path = output_dir / f"clip_{i:03d}.mp4"
make_video_clip(
path,
duration_seconds=duration_per_clip,
fps=fps,
width=width,
height=height,
color_start=c1,
color_end=c2,
label=label,
)
clips.append(path)
return clips