Step 5 — Effects
Status: ✅ Live (Chapter XV)
Unlocks when: Episode is images_ready
Advances to: effects_applied (on explicit episode approval, after all scenes approved)
Each scene gets one motion effect applied to its background image. Effects are selected per scene (editable inline in Step 5). Studio suggests one during segmentation — Tomas can override per scene.
How It Works
- Scene 1 active — "Apply Effect" button visible, rest locked
- Select effect type from dropdown (saves immediately)
- Click Apply Effect → MoviePy renders
.mp4clip → video player appears - Preview the effect — click Approve → next scene unlocks
- Or click Re-render to try a different effect type
- All scenes approved → "Approve Effects" → episode advances to
effects_applied
Base Effects (Sprint 67)
| Effect | Description |
|---|---|
slow_zoom_in |
Ken Burns — gradual zoom toward centre (1.0 → 1.15) |
slow_zoom_out |
Ken Burns — gradual zoom away from centre (1.15 → 1.0) |
drift_left |
Pan slowly left |
drift_right |
Pan slowly right |
fade_through_black |
Fade to black and back — scene transitions |
static |
No movement |
The 20 Effect Types
Zoom
| Effect | Description |
|---|---|
slow_zoom_in |
Ken Burns — cosine zoom toward centre |
slow_zoom_out |
Ken Burns — cosine zoom away from centre |
slow_zoom_off_center_tl |
Zoom toward Top Left corner |
slow_zoom_off_center_tr |
Zoom toward Top Right corner |
slow_zoom_off_center_bl |
Zoom toward Bottom Left corner |
slow_zoom_off_center_br |
Zoom toward Bottom Right corner |
Pan (viewer perspective)
| Effect | Description |
|---|---|
drift_left |
Move Right |
drift_right |
Move Left |
drift_up |
Move Down |
drift_down |
Move Up |
diagonal_drift_tl |
Move diagonally from Top Left |
diagonal_drift_tr |
Move diagonally from Top Right |
diagonal_drift_bl |
Move diagonally from Bottom Left |
diagonal_drift_br |
Move diagonally from Bottom Right |
Horror
| Effect | Description |
|---|---|
flicker |
Random brightness flicker — dying fluorescent light feel |
heartbeat_zoom |
Rhythmic double-pulse zoom at ~60bpm |
shake |
Random micro-jitter — handheld camera feel |
vignette_pulse |
Cosine-pulsing dark vignette — edges darken rhythmically |
Neutral
| Effect | Description |
|---|---|
fade_through_black |
Fade to black and back |
static |
No movement |
Per-Scene Controls
| Field | Default | Description |
|---|---|---|
effect_type |
static |
Primary effect |
effect_type_2 |
'' |
Second effect — alternates with primary at each cosine reversal. Empty = same throughout. |
effect_speed |
1.0 | Zoom/pan magnitude multiplier (0.5-3.0) |
effect_cycle |
20.0 | Half-cycle duration in seconds (0 = static). Slider goes 0 to narration duration. |
effect_upscale |
2 | In-memory upscale factor (1-4x). Higher = smoother but slower render. |
Motion Model
All motion effects use cosine oscillation: progress = (1 - cos(pi x t / cycle)) / 2
- Perfectly smooth reversals — no discontinuity at turning points
- Source image upscaled Nx in memory before the frame loop for sub-pixel smoothness
- 60fps output
- Cycle=0 auto-switches effect to Static
Alternating Effects
When effect_type_2 is set, apply_effect splits the total duration into half-cycle segments and alternates between the two effect types. Each segment is rendered independently and concatenated into one .mp4. Same effect throughout = seamless cosine; different effects = clean cut at each reversal.
File Naming
Effect clips stored in the scene's own subfolder alongside narration: media/studio/{series_slug}/{book_slug}/chapters/{n}/scenes/scene_NN_effect.mp4
Service
studio/effects.py — pure Python, zero Django imports. apply_effect(image_path, effect_type, duration_seconds, output_path, effect_speed, effect_cycle, effect_upscale, effect_type_2).
studio/service.py: run_scene_effect, approve_scene_effect, regenerate_scene_effect, approve_effects.