Compare commits
1 Commits
fix/645-sc
...
queue/490-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e394c85c0b |
@@ -202,19 +202,6 @@ curl -s -X POST "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}/comments" \\
|
||||
REVIEW CHECKLIST BEFORE YOU PUSH:
|
||||
{review}
|
||||
|
||||
COMMIT DISCIPLINE (CRITICAL):
|
||||
- Commit every 3-5 tool calls. Do NOT wait until the end.
|
||||
- After every meaningful file change: git add -A && git commit -m "WIP: <what changed>"
|
||||
- Before running any destructive command: commit current state first.
|
||||
- If you are unsure whether to commit: commit. WIP commits are safe. Lost work is not.
|
||||
- Never use --no-verify.
|
||||
- The auto-commit-guard is your safety net, but do not rely on it. Commit proactively.
|
||||
|
||||
RECOVERY COMMANDS (if interrupted, another agent can resume):
|
||||
git log --oneline -10 # see your WIP commits
|
||||
git diff HEAD~1 # see what the last commit changed
|
||||
git status # see uncommitted work
|
||||
|
||||
RULES:
|
||||
- Do not skip hooks with --no-verify.
|
||||
- Do not silently widen the scope.
|
||||
|
||||
@@ -161,14 +161,6 @@ run_worker() {
|
||||
CYCLE_END=$(date +%s)
|
||||
CYCLE_DURATION=$((CYCLE_END - CYCLE_START))
|
||||
|
||||
# --- Mid-session auto-commit: commit before timeout if work is dirty ---
|
||||
cd "$worktree" 2>/dev/null || true
|
||||
# Ensure auto-commit-guard is running
|
||||
if ! pgrep -f "auto-commit-guard.sh" >/dev/null 2>&1; then
|
||||
log "Starting auto-commit-guard daemon"
|
||||
nohup bash "$(dirname "$0")/auto-commit-guard.sh" 120 "$WORKTREE_BASE" >> "$LOG_DIR/auto-commit-guard.log" 2>&1 &
|
||||
fi
|
||||
|
||||
# Salvage
|
||||
cd "$worktree" 2>/dev/null || true
|
||||
DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# auto-commit-guard.sh — Background daemon that auto-commits uncommitted work
|
||||
#
|
||||
# Usage: auto-commit-guard.sh [interval_seconds] [worktree_base]
|
||||
# auto-commit-guard.sh # defaults: 120s, ~/worktrees
|
||||
# auto-commit-guard.sh 60 # check every 60s
|
||||
# auto-commit-guard.sh 180 ~/my-worktrees
|
||||
#
|
||||
# Scans all git repos under the worktree base for uncommitted changes.
|
||||
# If dirty for >= 1 check cycle, auto-commits with a WIP message.
|
||||
# Pushes unpushed commits so work is always recoverable from the remote.
|
||||
#
|
||||
# Also scans /tmp for orphaned agent workdirs on startup.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
INTERVAL="${1:-120}"
|
||||
WORKTREE_BASE="${2:-$HOME/worktrees}"
|
||||
LOG_DIR="$HOME/.hermes/logs"
|
||||
LOG="$LOG_DIR/auto-commit-guard.log"
|
||||
PIDFILE="$LOG_DIR/auto-commit-guard.pid"
|
||||
ORPHAN_SCAN_DONE="$LOG_DIR/.orphan-scan-done"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Single instance guard
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
old_pid=$(cat "$PIDFILE")
|
||||
if kill -0 "$old_pid" 2>/dev/null; then
|
||||
echo "auto-commit-guard already running (PID $old_pid)" >&2
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo $$ > "$PIDFILE"
|
||||
trap 'rm -f "$PIDFILE"' EXIT
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] AUTO-COMMIT: $*" >> "$LOG"
|
||||
}
|
||||
|
||||
# --- Orphaned workdir scan (runs once on startup) ---
|
||||
scan_orphans() {
|
||||
if [ -f "$ORPHAN_SCAN_DONE" ]; then
|
||||
return 0
|
||||
fi
|
||||
log "Scanning /tmp for orphaned agent workdirs..."
|
||||
local found=0
|
||||
local rescued=0
|
||||
|
||||
for dir in /tmp/*-work-* /tmp/timmy-burn-* /tmp/tc-burn; do
|
||||
[ -d "$dir" ] || continue
|
||||
[ -d "$dir/.git" ] || continue
|
||||
|
||||
found=$((found + 1))
|
||||
cd "$dir" 2>/dev/null || continue
|
||||
|
||||
local dirty
|
||||
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
|
||||
if [ "${dirty:-0}" -gt 0 ]; then
|
||||
local branch
|
||||
branch=$(git branch --show-current 2>/dev/null || echo "orphan")
|
||||
git add -A 2>/dev/null
|
||||
if git commit -m "WIP: orphan rescue — $dirty file(s) auto-committed on $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
Orphaned workdir detected at $dir.
|
||||
Branch: $branch
|
||||
Rescued by auto-commit-guard on startup." 2>/dev/null; then
|
||||
rescued=$((rescued + 1))
|
||||
log "RESCUED: $dir ($dirty files on branch $branch)"
|
||||
|
||||
# Try to push if remote exists
|
||||
if git remote get-url origin >/dev/null 2>&1; then
|
||||
git push -u origin "$branch" 2>/dev/null && log "PUSHED orphan rescue: $dir → $branch" || log "PUSH FAILED orphan rescue: $dir (no remote access)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "Orphan scan complete: $found workdirs checked, $rescued rescued"
|
||||
touch "$ORPHAN_SCAN_DONE"
|
||||
}
|
||||
|
||||
# --- Main guard loop ---
|
||||
guard_cycle() {
|
||||
local committed=0
|
||||
local scanned=0
|
||||
|
||||
# Scan worktree base
|
||||
if [ -d "$WORKTREE_BASE" ]; then
|
||||
for dir in "$WORKTREE_BASE"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
[ -d "$dir/.git" ] || continue
|
||||
|
||||
scanned=$((scanned + 1))
|
||||
cd "$dir" 2>/dev/null || continue
|
||||
|
||||
local dirty
|
||||
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
|
||||
[ "${dirty:-0}" -eq 0 ] && continue
|
||||
|
||||
local branch
|
||||
branch=$(git branch --show-current 2>/dev/null || echo "detached")
|
||||
|
||||
git add -A 2>/dev/null
|
||||
if git commit -m "WIP: auto-commit — $dirty file(s) on $branch
|
||||
|
||||
Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ).
|
||||
Work preserved to prevent loss on crash." 2>/dev/null; then
|
||||
committed=$((committed + 1))
|
||||
log "COMMITTED: $dir ($dirty files, branch $branch)"
|
||||
|
||||
# Push to preserve remotely
|
||||
if git remote get-url origin >/dev/null 2>&1; then
|
||||
git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir → $branch" || log "PUSH FAILED: $dir (will retry next cycle)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Also scan /tmp for agent workdirs
|
||||
for dir in /tmp/*-work-*; do
|
||||
[ -d "$dir" ] || continue
|
||||
[ -d "$dir/.git" ] || continue
|
||||
|
||||
scanned=$((scanned + 1))
|
||||
cd "$dir" 2>/dev/null || continue
|
||||
|
||||
local dirty
|
||||
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
|
||||
[ "${dirty:-0}" -eq 0 ] && continue
|
||||
|
||||
local branch
|
||||
branch=$(git branch --show-current 2>/dev/null || echo "detached")
|
||||
|
||||
git add -A 2>/dev/null
|
||||
if git commit -m "WIP: auto-commit — $dirty file(s) on $branch
|
||||
|
||||
Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ).
|
||||
Agent workdir preserved to prevent loss." 2>/dev/null; then
|
||||
committed=$((committed + 1))
|
||||
log "COMMITTED: $dir ($dirty files, branch $branch)"
|
||||
|
||||
if git remote get-url origin >/dev/null 2>&1; then
|
||||
git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir → $branch" || log "PUSH FAILED: $dir (will retry next cycle)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$committed" -gt 0 ] && log "Cycle done: $scanned scanned, $committed committed"
|
||||
}
|
||||
|
||||
# --- Entry point ---
|
||||
log "Starting auto-commit-guard (interval=${INTERVAL}s, worktree=${WORKTREE_BASE})"
|
||||
scan_orphans
|
||||
|
||||
while true; do
|
||||
guard_cycle
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
@@ -1,297 +0,0 @@
|
||||
"""
|
||||
Glitch pattern definitions for 3D world anomaly detection.
|
||||
|
||||
Defines known visual artifact categories commonly found in 3D web worlds,
|
||||
particularly The Matrix environments. Each pattern includes detection
|
||||
heuristics and severity ratings.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GlitchSeverity(Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
class GlitchCategory(Enum):
|
||||
FLOATING_ASSETS = "floating_assets"
|
||||
Z_FIGHTING = "z_fighting"
|
||||
MISSING_TEXTURES = "missing_textures"
|
||||
CLIPPING = "clipping"
|
||||
BROKEN_NORMALS = "broken_normals"
|
||||
SHADOW_ARTIFACTS = "shadow_artifacts"
|
||||
LIGHTMAP_ERRORS = "lightmap_errors"
|
||||
LOD_POPPING = "lod_popping"
|
||||
WATER_REFLECTION = "water_reflection"
|
||||
SKYBOX_SEAM = "skybox_seam"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlitchPattern:
|
||||
"""Definition of a known glitch pattern with detection parameters."""
|
||||
category: GlitchCategory
|
||||
name: str
|
||||
description: str
|
||||
severity: GlitchSeverity
|
||||
detection_prompts: list[str]
|
||||
visual_indicators: list[str]
|
||||
confidence_threshold: float = 0.6
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"category": self.category.value,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"severity": self.severity.value,
|
||||
"detection_prompts": self.detection_prompts,
|
||||
"visual_indicators": self.visual_indicators,
|
||||
"confidence_threshold": self.confidence_threshold,
|
||||
}
|
||||
|
||||
|
||||
# Known glitch patterns for Matrix 3D world scanning
|
||||
MATRIX_GLITCH_PATTERNS: list[GlitchPattern] = [
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.FLOATING_ASSETS,
|
||||
name="Floating Object",
|
||||
description="Object not properly grounded or anchored to the scene geometry. "
|
||||
"Common in procedurally placed assets or after physics desync.",
|
||||
severity=GlitchSeverity.HIGH,
|
||||
detection_prompts=[
|
||||
"Identify any objects that appear to float above the ground without support.",
|
||||
"Look for furniture, props, or geometry suspended in mid-air with no visible attachment.",
|
||||
"Check for objects whose shadows do not align with the surface below them.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"gap between object base and surface",
|
||||
"shadow detached from object",
|
||||
"object hovering with no structural support",
|
||||
],
|
||||
confidence_threshold=0.65,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.Z_FIGHTING,
|
||||
name="Z-Fighting Flicker",
|
||||
description="Two coplanar surfaces competing for depth priority, causing "
|
||||
"visible flickering or shimmering textures.",
|
||||
severity=GlitchSeverity.MEDIUM,
|
||||
detection_prompts=[
|
||||
"Look for surfaces that appear to shimmer, flicker, or show mixed textures.",
|
||||
"Identify areas where two textures seem to overlap and compete for visibility.",
|
||||
"Check walls, floors, or objects for surface noise or pattern interference.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"shimmering surface",
|
||||
"texture flicker between two patterns",
|
||||
"noisy flat surfaces",
|
||||
"moire-like patterns on planar geometry",
|
||||
],
|
||||
confidence_threshold=0.55,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.MISSING_TEXTURES,
|
||||
name="Missing or Placeholder Texture",
|
||||
description="A surface rendered with a fallback checkerboard, solid magenta, "
|
||||
"or the default engine placeholder texture.",
|
||||
severity=GlitchSeverity.CRITICAL,
|
||||
detection_prompts=[
|
||||
"Look for bright magenta, checkerboard, or solid-color surfaces that look out of place.",
|
||||
"Identify any surfaces that appear as flat untextured colors inconsistent with the scene.",
|
||||
"Check for black, white, or magenta patches where detailed textures should be.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"magenta/pink solid color surface",
|
||||
"checkerboard pattern",
|
||||
"flat single-color geometry",
|
||||
"UV-debug texture visible",
|
||||
],
|
||||
confidence_threshold=0.7,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.CLIPPING,
|
||||
name="Geometry Clipping",
|
||||
description="Objects passing through each other or intersecting in physically "
|
||||
"impossible ways due to collision mesh errors.",
|
||||
severity=GlitchSeverity.HIGH,
|
||||
detection_prompts=[
|
||||
"Look for objects that visibly pass through other objects (walls, floors, furniture).",
|
||||
"Identify characters or props embedded inside geometry where they should not be.",
|
||||
"Check for intersecting meshes where solid objects overlap unnaturally.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"object passing through wall or floor",
|
||||
"embedded geometry",
|
||||
"overlapping solid meshes",
|
||||
"character limb inside furniture",
|
||||
],
|
||||
confidence_threshold=0.6,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.BROKEN_NORMALS,
|
||||
name="Broken Surface Normals",
|
||||
description="Inverted or incorrect surface normals causing faces to appear "
|
||||
"inside-out, invisible from certain angles, or lit incorrectly.",
|
||||
severity=GlitchSeverity.MEDIUM,
|
||||
detection_prompts=[
|
||||
"Look for surfaces that appear dark or black on one side while lit on the other.",
|
||||
"Identify objects that seem to vanish when viewed from certain angles.",
|
||||
"Check for inverted shading where lit areas should be in shadow.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"dark/unlit face on otherwise lit model",
|
||||
"invisible surface from one direction",
|
||||
"inverted shadow gradient",
|
||||
"inside-out appearance",
|
||||
],
|
||||
confidence_threshold=0.5,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.SHADOW_ARTIFACTS,
|
||||
name="Shadow Artifact",
|
||||
description="Broken, detached, or incorrectly rendered shadows that do not "
|
||||
"match the casting geometry or scene lighting.",
|
||||
severity=GlitchSeverity.LOW,
|
||||
detection_prompts=[
|
||||
"Look for shadows that do not match the shape of nearby objects.",
|
||||
"Identify shadow acne: banding or striped patterns on surfaces.",
|
||||
"Check for floating shadows detached from any visible caster.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"shadow shape mismatch",
|
||||
"shadow acne bands",
|
||||
"detached floating shadow",
|
||||
"Peter Panning (shadow offset from base)",
|
||||
],
|
||||
confidence_threshold=0.5,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.LOD_POPPING,
|
||||
name="LOD Transition Pop",
|
||||
description="Visible pop-in when level-of-detail models switch abruptly, "
|
||||
"causing geometry or textures to change suddenly.",
|
||||
severity=GlitchSeverity.LOW,
|
||||
detection_prompts=[
|
||||
"Look for areas where mesh detail changes abruptly at visible boundaries.",
|
||||
"Identify objects that appear to morph or shift geometry suddenly.",
|
||||
"Check for texture resolution changes that create visible seams.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"visible mesh simplification boundary",
|
||||
"texture resolution jump",
|
||||
"geometry pop-in artifacts",
|
||||
],
|
||||
confidence_threshold=0.45,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.LIGHTMAP_ERRORS,
|
||||
name="Lightmap Baking Error",
|
||||
description="Incorrect or missing baked lighting causing dark spots, light "
|
||||
"leaks, or mismatched illumination on static geometry.",
|
||||
severity=GlitchSeverity.MEDIUM,
|
||||
detection_prompts=[
|
||||
"Look for unusually dark patches on walls or ceilings that should be lit.",
|
||||
"Identify bright light leaks through solid geometry seams.",
|
||||
"Check for mismatched lighting between adjacent surfaces.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"dark splotch on lit surface",
|
||||
"bright line at geometry seam",
|
||||
"lighting discontinuity between adjacent faces",
|
||||
],
|
||||
confidence_threshold=0.5,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.WATER_REFLECTION,
|
||||
name="Water/Reflection Error",
|
||||
description="Incorrect reflections, missing water surfaces, or broken "
|
||||
"reflection probe assignments.",
|
||||
severity=GlitchSeverity.MEDIUM,
|
||||
detection_prompts=[
|
||||
"Look for reflections that do not match the surrounding environment.",
|
||||
"Identify water surfaces that appear solid or incorrectly rendered.",
|
||||
"Check for mirror surfaces showing wrong scene geometry.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"reflection mismatch",
|
||||
"solid water surface",
|
||||
"incorrect environment map",
|
||||
],
|
||||
confidence_threshold=0.5,
|
||||
),
|
||||
GlitchPattern(
|
||||
category=GlitchCategory.SKYBOX_SEAM,
|
||||
name="Skybox Seam",
|
||||
description="Visible seams or color mismatches at the edges of skybox cubemap faces.",
|
||||
severity=GlitchSeverity.LOW,
|
||||
detection_prompts=[
|
||||
"Look at the edges of the sky for visible seams or color shifts.",
|
||||
"Identify discontinuities where skybox faces meet.",
|
||||
"Check for texture stretching at skybox corners.",
|
||||
],
|
||||
visual_indicators=[
|
||||
"visible line in sky",
|
||||
"color discontinuity at sky edge",
|
||||
"sky texture seam",
|
||||
],
|
||||
confidence_threshold=0.45,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_patterns_by_severity(min_severity: GlitchSeverity) -> list[GlitchPattern]:
|
||||
"""Return patterns at or above the given severity level."""
|
||||
severity_order = [
|
||||
GlitchSeverity.INFO,
|
||||
GlitchSeverity.LOW,
|
||||
GlitchSeverity.MEDIUM,
|
||||
GlitchSeverity.HIGH,
|
||||
GlitchSeverity.CRITICAL,
|
||||
]
|
||||
min_idx = severity_order.index(min_severity)
|
||||
return [p for p in MATRIX_GLITCH_PATTERNS if severity_order.index(p.severity) >= min_idx]
|
||||
|
||||
|
||||
def get_pattern_by_category(category: GlitchCategory) -> Optional[GlitchPattern]:
|
||||
"""Return the pattern definition for a specific category."""
|
||||
for p in MATRIX_GLITCH_PATTERNS:
|
||||
if p.category == category:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def build_vision_prompt(patterns: list[GlitchPattern] | None = None) -> str:
|
||||
"""Build a composite vision analysis prompt from pattern definitions."""
|
||||
if patterns is None:
|
||||
patterns = MATRIX_GLITCH_PATTERNS
|
||||
|
||||
sections = []
|
||||
for p in patterns:
|
||||
prompt_text = " ".join(p.detection_prompts)
|
||||
indicators = ", ".join(p.visual_indicators)
|
||||
sections.append(
|
||||
f"[{p.category.value.upper()}] {p.name} (severity: {p.severity.value})\n"
|
||||
f" {p.description}\n"
|
||||
f" Look for: {prompt_text}\n"
|
||||
f" Visual indicators: {indicators}"
|
||||
)
|
||||
|
||||
return (
|
||||
"Analyze this 3D world screenshot for visual glitches and artifacts. "
|
||||
"For each detected issue, report the category, description of what you see, "
|
||||
"approximate location in the image (x%, y%), and confidence (0.0-1.0).\n\n"
|
||||
"Known glitch patterns to check:\n\n" + "\n\n".join(sections)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(f"Loaded {len(MATRIX_GLITCH_PATTERNS)} glitch patterns:\n")
|
||||
for p in MATRIX_GLITCH_PATTERNS:
|
||||
print(f" [{p.severity.value:8s}] {p.category.value}: {p.name}")
|
||||
print(f"\nVision prompt preview:\n{build_vision_prompt()[:500]}...")
|
||||
@@ -1,549 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Matrix 3D World Glitch Detector
|
||||
|
||||
Scans a 3D web world for visual artifacts using browser automation
|
||||
and vision AI analysis. Produces structured glitch reports.
|
||||
|
||||
Usage:
|
||||
python matrix_glitch_detector.py <url> [--angles 4] [--output report.json]
|
||||
python matrix_glitch_detector.py --demo # Run with synthetic test data
|
||||
|
||||
Ref: timmy-config#491
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Add parent for glitch_patterns import
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from glitch_patterns import (
|
||||
GlitchCategory,
|
||||
GlitchPattern,
|
||||
GlitchSeverity,
|
||||
MATRIX_GLITCH_PATTERNS,
|
||||
build_vision_prompt,
|
||||
get_patterns_by_severity,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectedGlitch:
|
||||
"""A single detected glitch with metadata."""
|
||||
id: str
|
||||
category: str
|
||||
name: str
|
||||
description: str
|
||||
severity: str
|
||||
confidence: float
|
||||
location_x: Optional[float] = None # percentage across image
|
||||
location_y: Optional[float] = None # percentage down image
|
||||
screenshot_index: int = 0
|
||||
screenshot_angle: str = "front"
|
||||
timestamp: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.timestamp:
|
||||
self.timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanResult:
|
||||
"""Complete scan result for a 3D world URL."""
|
||||
scan_id: str
|
||||
url: str
|
||||
timestamp: str
|
||||
total_screenshots: int
|
||||
angles_captured: list[str]
|
||||
glitches: list[dict] = field(default_factory=list)
|
||||
summary: dict = field(default_factory=dict)
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
def to_json(self, indent: int = 2) -> str:
|
||||
return json.dumps(asdict(self), indent=indent)
|
||||
|
||||
|
||||
def generate_scan_angles(num_angles: int) -> list[dict]:
|
||||
"""Generate camera angle configurations for multi-angle scanning.
|
||||
|
||||
Returns a list of dicts with yaw/pitch/label for browser camera control.
|
||||
"""
|
||||
base_angles = [
|
||||
{"yaw": 0, "pitch": 0, "label": "front"},
|
||||
{"yaw": 90, "pitch": 0, "label": "right"},
|
||||
{"yaw": 180, "pitch": 0, "label": "back"},
|
||||
{"yaw": 270, "pitch": 0, "label": "left"},
|
||||
{"yaw": 0, "pitch": -30, "label": "front_low"},
|
||||
{"yaw": 45, "pitch": -15, "label": "front_right_low"},
|
||||
{"yaw": 0, "pitch": 30, "label": "front_high"},
|
||||
{"yaw": 45, "pitch": 0, "label": "front_right"},
|
||||
]
|
||||
|
||||
if num_angles <= len(base_angles):
|
||||
return base_angles[:num_angles]
|
||||
return base_angles + [
|
||||
{"yaw": i * (360 // num_angles), "pitch": 0, "label": f"angle_{i}"}
|
||||
for i in range(len(base_angles), num_angles)
|
||||
]
|
||||
|
||||
|
||||
def capture_screenshots(url: str, angles: list[dict], output_dir: Path) -> list[Path]:
|
||||
"""Capture screenshots of a 3D web world from multiple angles.
|
||||
|
||||
Uses browser_vision tool when available; falls back to placeholder generation
|
||||
for testing and environments without browser access.
|
||||
"""
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
screenshots = []
|
||||
|
||||
for i, angle in enumerate(angles):
|
||||
filename = output_dir / f"screenshot_{i:03d}_{angle['label']}.png"
|
||||
|
||||
# Attempt browser-based capture via browser_vision
|
||||
try:
|
||||
result = _browser_capture(url, angle, filename)
|
||||
if result:
|
||||
screenshots.append(filename)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Generate placeholder screenshot for offline/test scenarios
|
||||
_generate_placeholder_screenshot(filename, angle)
|
||||
screenshots.append(filename)
|
||||
|
||||
return screenshots
|
||||
|
||||
|
||||
def _browser_capture(url: str, angle: dict, output_path: Path) -> bool:
|
||||
"""Capture a screenshot via browser automation.
|
||||
|
||||
This is a stub that delegates to the browser_vision tool when run
|
||||
in an environment that provides it. In CI or offline mode, returns False.
|
||||
"""
|
||||
# Check if browser_vision is available via environment
|
||||
bv_script = os.environ.get("BROWSER_VISION_SCRIPT")
|
||||
if bv_script and Path(bv_script).exists():
|
||||
import subprocess
|
||||
cmd = [
|
||||
sys.executable, bv_script,
|
||||
"--url", url,
|
||||
"--screenshot", str(output_path),
|
||||
"--rotate-yaw", str(angle["yaw"]),
|
||||
"--rotate-pitch", str(angle["pitch"]),
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
return proc.returncode == 0 and output_path.exists()
|
||||
return False
|
||||
|
||||
|
||||
def _generate_placeholder_screenshot(path: Path, angle: dict):
|
||||
"""Generate a minimal 1x1 PNG as a placeholder for testing."""
|
||||
# Minimal valid PNG (1x1 transparent pixel)
|
||||
png_data = (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
|
||||
b"\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01"
|
||||
b"\r\n\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
path.write_bytes(png_data)
|
||||
|
||||
|
||||
def analyze_with_vision(
|
||||
screenshot_paths: list[Path],
|
||||
angles: list[dict],
|
||||
patterns: list[GlitchPattern] | None = None,
|
||||
) -> list[DetectedGlitch]:
|
||||
"""Send screenshots to vision AI for glitch analysis.
|
||||
|
||||
In environments with a vision model available, sends each screenshot
|
||||
with the composite detection prompt. Otherwise returns simulated results.
|
||||
"""
|
||||
if patterns is None:
|
||||
patterns = MATRIX_GLITCH_PATTERNS
|
||||
|
||||
prompt = build_vision_prompt(patterns)
|
||||
glitches = []
|
||||
|
||||
for i, (path, angle) in enumerate(zip(screenshot_paths, angles)):
|
||||
# Attempt vision analysis
|
||||
detected = _vision_analyze_image(path, prompt, i, angle["label"])
|
||||
glitches.extend(detected)
|
||||
|
||||
return glitches
|
||||
|
||||
|
||||
def _vision_analyze_image(
|
||||
image_path: Path,
|
||||
prompt: str,
|
||||
screenshot_index: int,
|
||||
angle_label: str,
|
||||
) -> list[DetectedGlitch]:
|
||||
"""Analyze a single screenshot with vision AI.
|
||||
|
||||
Uses the vision_analyze tool when available; returns empty list otherwise.
|
||||
"""
|
||||
# Check for vision API configuration
|
||||
api_key = os.environ.get("VISION_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||
api_base = os.environ.get("VISION_API_BASE", "https://api.openai.com/v1")
|
||||
|
||||
if api_key:
|
||||
try:
|
||||
return _call_vision_api(
|
||||
image_path, prompt, screenshot_index, angle_label, api_key, api_base
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" [!] Vision API error for {image_path.name}: {e}", file=sys.stderr)
|
||||
|
||||
# No vision backend available
|
||||
return []
|
||||
|
||||
|
||||
def _call_vision_api(
|
||||
image_path: Path,
|
||||
prompt: str,
|
||||
screenshot_index: int,
|
||||
angle_label: str,
|
||||
api_key: str,
|
||||
api_base: str,
|
||||
) -> list[DetectedGlitch]:
|
||||
"""Call a vision API (OpenAI-compatible) for image analysis."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
image_data = base64.b64encode(image_path.read_bytes()).decode()
|
||||
|
||||
payload = json.dumps({
|
||||
"model": os.environ.get("VISION_MODEL", "gpt-4o"),
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/png;base64,{image_data}",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"max_tokens": 4096,
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{api_base}/chat/completions",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
},
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
result = json.loads(resp.read())
|
||||
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
return _parse_vision_response(content, screenshot_index, angle_label)
|
||||
|
||||
|
||||
def _add_glitch_from_dict(
|
||||
item: dict,
|
||||
glitches: list[DetectedGlitch],
|
||||
screenshot_index: int,
|
||||
angle_label: str,
|
||||
):
|
||||
"""Convert a dict from vision API response into a DetectedGlitch."""
|
||||
cat = item.get("category", item.get("type", "unknown"))
|
||||
conf = float(item.get("confidence", item.get("score", 0.5)))
|
||||
|
||||
glitch = DetectedGlitch(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
category=cat,
|
||||
name=item.get("name", item.get("label", cat)),
|
||||
description=item.get("description", item.get("detail", "")),
|
||||
severity=item.get("severity", _infer_severity(cat, conf)),
|
||||
confidence=conf,
|
||||
location_x=item.get("location_x", item.get("x")),
|
||||
location_y=item.get("location_y", item.get("y")),
|
||||
screenshot_index=screenshot_index,
|
||||
screenshot_angle=angle_label,
|
||||
)
|
||||
glitches.append(glitch)
|
||||
|
||||
|
||||
def _parse_vision_response(
|
||||
text: str, screenshot_index: int, angle_label: str
|
||||
) -> list[DetectedGlitch]:
|
||||
"""Parse vision AI response into structured glitch detections."""
|
||||
glitches = []
|
||||
|
||||
# Try to extract JSON from the response
|
||||
json_blocks = []
|
||||
in_json = False
|
||||
json_buf = []
|
||||
|
||||
for line in text.split("\n"):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("```"):
|
||||
if in_json and json_buf:
|
||||
try:
|
||||
json_blocks.append(json.loads("\n".join(json_buf)))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
json_buf = []
|
||||
in_json = not in_json
|
||||
continue
|
||||
if in_json:
|
||||
json_buf.append(line)
|
||||
|
||||
# Flush any remaining buffer
|
||||
if in_json and json_buf:
|
||||
try:
|
||||
json_blocks.append(json.loads("\n".join(json_buf)))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Also try parsing the entire response as JSON
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
if isinstance(parsed, list):
|
||||
json_blocks.extend(parsed)
|
||||
elif isinstance(parsed, dict):
|
||||
if "glitches" in parsed:
|
||||
json_blocks.extend(parsed["glitches"])
|
||||
elif "detections" in parsed:
|
||||
json_blocks.extend(parsed["detections"])
|
||||
else:
|
||||
json_blocks.append(parsed)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
for item in json_blocks:
|
||||
# Flatten arrays of detections
|
||||
if isinstance(item, list):
|
||||
for sub in item:
|
||||
if isinstance(sub, dict):
|
||||
_add_glitch_from_dict(sub, glitches, screenshot_index, angle_label)
|
||||
elif isinstance(item, dict):
|
||||
_add_glitch_from_dict(item, glitches, screenshot_index, angle_label)
|
||||
|
||||
return glitches
|
||||
|
||||
|
||||
def _infer_severity(category: str, confidence: float) -> str:
|
||||
"""Infer severity from category and confidence when not provided."""
|
||||
critical_cats = {"missing_textures", "clipping"}
|
||||
high_cats = {"floating_assets", "broken_normals"}
|
||||
|
||||
cat_lower = category.lower()
|
||||
if any(c in cat_lower for c in critical_cats):
|
||||
return "critical" if confidence > 0.7 else "high"
|
||||
if any(c in cat_lower for c in high_cats):
|
||||
return "high" if confidence > 0.7 else "medium"
|
||||
return "medium" if confidence > 0.6 else "low"
|
||||
|
||||
|
||||
def build_report(
|
||||
url: str,
|
||||
angles: list[dict],
|
||||
screenshots: list[Path],
|
||||
glitches: list[DetectedGlitch],
|
||||
) -> ScanResult:
|
||||
"""Build the final structured scan report."""
|
||||
severity_counts = {}
|
||||
category_counts = {}
|
||||
|
||||
for g in glitches:
|
||||
severity_counts[g.severity] = severity_counts.get(g.severity, 0) + 1
|
||||
category_counts[g.category] = category_counts.get(g.category, 0) + 1
|
||||
|
||||
report = ScanResult(
|
||||
scan_id=str(uuid.uuid4()),
|
||||
url=url,
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
total_screenshots=len(screenshots),
|
||||
angles_captured=[a["label"] for a in angles],
|
||||
glitches=[asdict(g) for g in glitches],
|
||||
summary={
|
||||
"total_glitches": len(glitches),
|
||||
"by_severity": severity_counts,
|
||||
"by_category": category_counts,
|
||||
"highest_severity": max(severity_counts.keys(), default="none"),
|
||||
"clean_screenshots": sum(
|
||||
1
|
||||
for i in range(len(screenshots))
|
||||
if not any(g.screenshot_index == i for g in glitches)
|
||||
),
|
||||
},
|
||||
metadata={
|
||||
"detector_version": "0.1.0",
|
||||
"pattern_count": len(MATRIX_GLITCH_PATTERNS),
|
||||
"reference": "timmy-config#491",
|
||||
},
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def run_demo(output_path: Optional[Path] = None) -> ScanResult:
|
||||
"""Run a demonstration scan with simulated detections."""
|
||||
print("[*] Running Matrix glitch detection demo...")
|
||||
|
||||
url = "https://matrix.example.com/world/alpha"
|
||||
angles = generate_scan_angles(4)
|
||||
screenshots_dir = Path("/tmp/matrix_glitch_screenshots")
|
||||
|
||||
print(f"[*] Capturing {len(angles)} screenshots from: {url}")
|
||||
screenshots = capture_screenshots(url, angles, screenshots_dir)
|
||||
print(f"[*] Captured {len(screenshots)} screenshots")
|
||||
|
||||
# Simulate detections for demo
|
||||
demo_glitches = [
|
||||
DetectedGlitch(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
category="floating_assets",
|
||||
name="Floating Chair",
|
||||
description="Office chair floating 0.3m above floor in sector 7",
|
||||
severity="high",
|
||||
confidence=0.87,
|
||||
location_x=35.2,
|
||||
location_y=62.1,
|
||||
screenshot_index=0,
|
||||
screenshot_angle="front",
|
||||
),
|
||||
DetectedGlitch(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
category="z_fighting",
|
||||
name="Wall Texture Flicker",
|
||||
description="Z-fighting between wall panel and decorative overlay",
|
||||
severity="medium",
|
||||
confidence=0.72,
|
||||
location_x=58.0,
|
||||
location_y=40.5,
|
||||
screenshot_index=1,
|
||||
screenshot_angle="right",
|
||||
),
|
||||
DetectedGlitch(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
category="missing_textures",
|
||||
name="Placeholder Texture",
|
||||
description="Bright magenta surface on door frame — missing asset reference",
|
||||
severity="critical",
|
||||
confidence=0.95,
|
||||
location_x=72.3,
|
||||
location_y=28.8,
|
||||
screenshot_index=2,
|
||||
screenshot_angle="back",
|
||||
),
|
||||
DetectedGlitch(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
category="clipping",
|
||||
name="Desk Through Wall",
|
||||
description="Desk corner clipping through adjacent wall geometry",
|
||||
severity="high",
|
||||
confidence=0.81,
|
||||
location_x=15.0,
|
||||
location_y=55.0,
|
||||
screenshot_index=3,
|
||||
screenshot_angle="left",
|
||||
),
|
||||
]
|
||||
|
||||
print(f"[*] Detected {len(demo_glitches)} glitches")
|
||||
report = build_report(url, angles, screenshots, demo_glitches)
|
||||
|
||||
if output_path:
|
||||
output_path.write_text(report.to_json())
|
||||
print(f"[*] Report saved to: {output_path}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Matrix 3D World Glitch Detector — scan for visual artifacts",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s https://matrix.example.com/world/alpha
|
||||
%(prog)s https://matrix.example.com/world/alpha --angles 8 --output report.json
|
||||
%(prog)s --demo
|
||||
""",
|
||||
)
|
||||
parser.add_argument("url", nargs="?", help="URL of the 3D world to scan")
|
||||
parser.add_argument(
|
||||
"--angles", type=int, default=4, help="Number of camera angles to capture (default: 4)"
|
||||
)
|
||||
parser.add_argument("--output", "-o", type=str, help="Output file path for JSON report")
|
||||
parser.add_argument("--demo", action="store_true", help="Run demo with simulated data")
|
||||
parser.add_argument(
|
||||
"--min-severity",
|
||||
choices=["info", "low", "medium", "high", "critical"],
|
||||
default="info",
|
||||
help="Minimum severity to include in report",
|
||||
)
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.demo:
|
||||
output = Path(args.output) if args.output else Path("glitch_report_demo.json")
|
||||
report = run_demo(output)
|
||||
print(f"\n=== Scan Summary ===")
|
||||
print(f"URL: {report.url}")
|
||||
print(f"Screenshots: {report.total_screenshots}")
|
||||
print(f"Glitches found: {report.summary['total_glitches']}")
|
||||
print(f"By severity: {report.summary['by_severity']}")
|
||||
return
|
||||
|
||||
if not args.url:
|
||||
parser.error("URL required (or use --demo)")
|
||||
|
||||
scan_id = str(uuid.uuid4())[:8]
|
||||
print(f"[*] Matrix Glitch Detector — Scan {scan_id}")
|
||||
print(f"[*] Target: {args.url}")
|
||||
|
||||
# Generate camera angles
|
||||
angles = generate_scan_angles(args.angles)
|
||||
print(f"[*] Capturing {len(angles)} screenshots...")
|
||||
|
||||
# Capture screenshots
|
||||
screenshots_dir = Path(f"/tmp/matrix_glitch_{scan_id}")
|
||||
screenshots = capture_screenshots(args.url, angles, screenshots_dir)
|
||||
print(f"[*] Captured {len(screenshots)} screenshots")
|
||||
|
||||
# Filter patterns by severity
|
||||
min_sev = GlitchSeverity(args.min_severity)
|
||||
patterns = get_patterns_by_severity(min_sev)
|
||||
|
||||
# Analyze with vision AI
|
||||
print(f"[*] Analyzing with vision AI ({len(patterns)} patterns)...")
|
||||
glitches = analyze_with_vision(screenshots, angles, patterns)
|
||||
|
||||
# Build and save report
|
||||
report = build_report(args.url, angles, screenshots, glitches)
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(report.to_json())
|
||||
print(f"[*] Report saved: {args.output}")
|
||||
else:
|
||||
print(report.to_json())
|
||||
|
||||
print(f"\n[*] Done — {len(glitches)} glitches detected")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Full Nostr agent-to-agent communication demo - FINAL WORKING
|
||||
"""
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# pane-watchdog.sh — Detect stuck/dead tmux panes and auto-restart them
|
||||
#
|
||||
# Tracks output hash per pane across cycles. If a pane's captured output
|
||||
# hasn't changed for STUCK_CYCLES consecutive checks, the pane is STUCK.
|
||||
# Dead panes (PID gone) are also detected.
|
||||
#
|
||||
# On STUCK/DEAD:
|
||||
# 1. Kill the pane
|
||||
# 2. Attempt restart with --resume (session ID from manifest)
|
||||
# 3. Fallback: fresh prompt with last known task from logs
|
||||
#
|
||||
# State file: ~/.hermes/pane-state.json
|
||||
# Log: ~/.hermes/logs/pane-watchdog.log
|
||||
#
|
||||
# Usage:
|
||||
# pane-watchdog.sh # One-shot check all sessions
|
||||
# pane-watchdog.sh --daemon # Run every CHECK_INTERVAL seconds
|
||||
# pane-watchdog.sh --status # Print current pane state
|
||||
# pane-watchdog.sh --session NAME # Check only one session
|
||||
#
|
||||
# Issue: timmy-config #515
|
||||
|
||||
set -uo pipefail
|
||||
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
|
||||
|
||||
# === CONFIG ===
|
||||
STATE_FILE="${PANE_STATE_FILE:-$HOME/.hermes/pane-state.json}"
|
||||
LOG_FILE="${PANE_WATCHDOG_LOG:-$HOME/.hermes/logs/pane-watchdog.log}"
|
||||
CHECK_INTERVAL="${PANE_CHECK_INTERVAL:-120}" # seconds between cycles
|
||||
STUCK_CYCLES=2 # unchanged cycles before STUCK
|
||||
MAX_RESTART_ATTEMPTS=3 # per pane per hour
|
||||
RESTART_COOLDOWN=3600 # seconds between escalation alerts
|
||||
CAPTURE_LINES=40 # lines of output to hash
|
||||
|
||||
# Sessions to monitor (all if empty)
|
||||
MONITOR_SESSIONS="${PANE_WATCHDOG_SESSIONS:-}"
|
||||
|
||||
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$LOG_FILE")"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# === HELPERS ===
|
||||
|
||||
# Capture last N lines of pane output and hash them
|
||||
capture_pane_hash() {
|
||||
local target="$1"
|
||||
local output
|
||||
output=$(tmux capture-pane -t "$target" -p -S "-${CAPTURE_LINES}" 2>/dev/null || echo "DEAD")
|
||||
echo -n "$output" | shasum -a 256 | cut -d' ' -f1
|
||||
}
|
||||
|
||||
# Check if pane PID is alive
|
||||
pane_pid_alive() {
|
||||
local target="$1"
|
||||
local pid
|
||||
pid=$(tmux list-panes -t "$target" -F '#{pane_pid}' 2>/dev/null | head -1 || echo "")
|
||||
if [ -z "$pid" ]; then
|
||||
return 1 # pane doesn't exist
|
||||
fi
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get pane start command
|
||||
pane_start_command() {
|
||||
local target="$1"
|
||||
tmux list-panes -t "$target" -F '#{pane_start_command}' 2>/dev/null | head -1 || echo "unknown"
|
||||
}
|
||||
|
||||
# Get the pane's current running command (child process)
|
||||
pane_current_command() {
|
||||
local target="$1"
|
||||
tmux list-panes -t "$target" -F '#{pane_current_command}' 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# Only restart panes running hermes/agent commands (not zsh, python3 repls, etc.)
|
||||
is_restartable() {
|
||||
local cmd="$1"
|
||||
case "$cmd" in
|
||||
hermes|*hermes*|*agent*|*timmy*|*kimi*|*claude-loop*|*gemini-loop*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get session ID from hermes manifest if available
|
||||
get_hermes_session_id() {
|
||||
local session_name="$1"
|
||||
local manifest="$HOME/.hermes/sessions/${session_name}/manifest.json"
|
||||
if [ -f "$manifest" ]; then
|
||||
python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
m = json.load(open('$manifest'))
|
||||
print(m.get('session_id', m.get('id', '')))
|
||||
except: pass
|
||||
" 2>/dev/null || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Get last task from pane logs
|
||||
get_last_task() {
|
||||
local session_name="$1"
|
||||
local log_dir="$HOME/.hermes/logs"
|
||||
# Find the most recent log for this session
|
||||
local log_file
|
||||
log_file=$(find "$log_dir" -name "*${session_name}*" -type f -mtime -1 2>/dev/null | sort -r | head -1)
|
||||
if [ -n "$log_file" ] && [ -f "$log_file" ]; then
|
||||
# Extract last user prompt or task description
|
||||
grep -i "task:\|prompt:\|issue\|working on" "$log_file" 2>/dev/null | tail -1 | sed 's/.*[:>] *//' | head -c 200
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart a pane with a fresh shell/command
|
||||
restart_pane() {
|
||||
local target="$1"
|
||||
local session_name="${target%%:*}"
|
||||
local session_id last_task cmd
|
||||
|
||||
log "RESTART: Attempting to restart $target"
|
||||
|
||||
# Kill existing pane
|
||||
tmux kill-pane -t "$target" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Try --resume with session ID
|
||||
session_id=$(get_hermes_session_id "$session_name")
|
||||
if [ -n "$session_id" ]; then
|
||||
log "RESTART: Trying --resume with session $session_id"
|
||||
tmux split-window -t "$session_name" -d \
|
||||
"hermes chat --resume '$session_id' 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
|
||||
sleep 2
|
||||
if pane_pid_alive "${session_name}:1" 2>/dev/null; then
|
||||
log "RESTART: Success with --resume"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: fresh prompt
|
||||
last_task=$(get_last_task "$session_name")
|
||||
if [ -n "$last_task" ]; then
|
||||
log "RESTART: Fallback — fresh prompt with task: $last_task"
|
||||
tmux split-window -t "$session_name" -d \
|
||||
"echo 'Watchdog restart — last task: $last_task' && hermes chat 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
|
||||
else
|
||||
log "RESTART: Fallback — fresh hermes chat"
|
||||
tmux split-window -t "$session_name" -d \
|
||||
"hermes chat 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
if pane_pid_alive "${session_name}:1" 2>/dev/null; then
|
||||
log "RESTART: Fallback restart succeeded"
|
||||
return 0
|
||||
else
|
||||
log "RESTART: FAILED to restart $target"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# === STATE MANAGEMENT ===
|
||||
|
||||
read_state() {
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
write_state() {
|
||||
echo "$1" > "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Update state for a single pane and return JSON status
|
||||
update_pane_state() {
|
||||
local target="$1"
|
||||
local hash="$2"
|
||||
local is_alive="$3"
|
||||
local now
|
||||
now=$(date +%s)
|
||||
|
||||
python3 - "$STATE_FILE" "$target" "$hash" "$is_alive" "$now" "$STUCK_CYCLES" <<'PYEOF'
|
||||
import json, sys, time
|
||||
|
||||
state_file = sys.argv[1]
|
||||
target = sys.argv[2]
|
||||
new_hash = sys.argv[3]
|
||||
is_alive = sys.argv[4] == "true"
|
||||
now = int(sys.argv[5])
|
||||
stuck_cycles = int(sys.argv[6])
|
||||
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
state = {}
|
||||
|
||||
pane = state.get(target, {
|
||||
"hash": "",
|
||||
"same_count": 0,
|
||||
"status": "UNKNOWN",
|
||||
"last_change": 0,
|
||||
"last_check": 0,
|
||||
"restart_attempts": 0,
|
||||
"last_restart": 0,
|
||||
"current_command": "",
|
||||
})
|
||||
|
||||
if not is_alive:
|
||||
pane["status"] = "DEAD"
|
||||
pane["same_count"] = 0
|
||||
elif new_hash == pane.get("hash", ""):
|
||||
pane["same_count"] = pane.get("same_count", 0) + 1
|
||||
if pane["same_count"] >= stuck_cycles:
|
||||
pane["status"] = "STUCK"
|
||||
else:
|
||||
pane["status"] = "STALE" if pane["same_count"] > 0 else "OK"
|
||||
else:
|
||||
pane["hash"] = new_hash
|
||||
pane["same_count"] = 0
|
||||
pane["status"] = "OK"
|
||||
pane["last_change"] = now
|
||||
|
||||
pane["last_check"] = now
|
||||
state[target] = pane
|
||||
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(json.dumps(pane))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# Reset restart attempt counter if cooldown expired
|
||||
maybe_reset_restarts() {
|
||||
local target="$1"
|
||||
local now
|
||||
now=$(date +%s)
|
||||
|
||||
python3 - "$STATE_FILE" "$target" "$now" "$RESTART_COOLDOWN" <<'PYEOF'
|
||||
import json, sys
|
||||
|
||||
state_file = sys.argv[1]
|
||||
target = sys.argv[2]
|
||||
now = int(sys.argv[3])
|
||||
cooldown = int(sys.argv[4])
|
||||
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
pane = state.get(target, {})
|
||||
last_restart = pane.get("last_restart", 0)
|
||||
|
||||
if now - last_restart > cooldown:
|
||||
pane["restart_attempts"] = 0
|
||||
|
||||
state[target] = pane
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(pane.get("restart_attempts", 0))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
increment_restart_attempt() {
|
||||
local target="$1"
|
||||
local now
|
||||
now=$(date +%s)
|
||||
|
||||
python3 - "$STATE_FILE" "$target" "$now" <<'PYEOF'
|
||||
import json, sys
|
||||
|
||||
state_file = sys.argv[1]
|
||||
target = sys.argv[2]
|
||||
now = int(sys.argv[3])
|
||||
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
pane = state.get(target, {})
|
||||
pane["restart_attempts"] = pane.get("restart_attempts", 0) + 1
|
||||
pane["last_restart"] = now
|
||||
pane["status"] = "RESTARTING"
|
||||
|
||||
state[target] = pane
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(pane["restart_attempts"])
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# === CORE CHECK ===
|
||||
|
||||
check_pane() {
|
||||
local target="$1"
|
||||
local hash is_alive status current_cmd
|
||||
|
||||
# Capture state
|
||||
hash=$(capture_pane_hash "$target")
|
||||
if pane_pid_alive "$target"; then
|
||||
is_alive="true"
|
||||
else
|
||||
is_alive="false"
|
||||
fi
|
||||
|
||||
# Get current command for the pane
|
||||
current_cmd=$(pane_current_command "$target")
|
||||
|
||||
# Update state and get result
|
||||
local result
|
||||
result=$(update_pane_state "$target" "$hash" "$is_alive")
|
||||
status=$(echo "$result" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('status','UNKNOWN'))" 2>/dev/null || echo "UNKNOWN")
|
||||
|
||||
case "$status" in
|
||||
OK)
|
||||
# Healthy, do nothing
|
||||
;;
|
||||
DEAD)
|
||||
log "DETECTED: $target is DEAD (PID gone) cmd=$current_cmd"
|
||||
if is_restartable "$current_cmd"; then
|
||||
handle_stuck "$target"
|
||||
else
|
||||
log "SKIP: $target not a hermes pane (cmd=$current_cmd), not restarting"
|
||||
fi
|
||||
;;
|
||||
STUCK)
|
||||
log "DETECTED: $target is STUCK (output unchanged for ${STUCK_CYCLES} cycles) cmd=$current_cmd"
|
||||
if is_restartable "$current_cmd"; then
|
||||
handle_stuck "$target"
|
||||
else
|
||||
log "SKIP: $target not a hermes pane (cmd=$current_cmd), not restarting"
|
||||
fi
|
||||
;;
|
||||
STALE)
|
||||
# Output unchanged but within threshold — just log
|
||||
local count
|
||||
count=$(echo "$result" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('same_count',0))" 2>/dev/null || echo "?")
|
||||
log "STALE: $target unchanged for $count cycle(s)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
handle_stuck() {
|
||||
local target="$1"
|
||||
local session_name="${target%%:*}"
|
||||
local attempts
|
||||
|
||||
# Check restart budget
|
||||
attempts=$(maybe_reset_restarts "$target")
|
||||
if [ "$attempts" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||||
log "ESCALATION: $target stuck ${attempts}x — manual intervention needed"
|
||||
echo "ALERT: $target stuck after $attempts restart attempts" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
attempts=$(increment_restart_attempt "$target")
|
||||
log "ACTION: Restarting $target (attempt $attempts/$MAX_RESTART_ATTEMPTS)"
|
||||
|
||||
if restart_pane "$target"; then
|
||||
log "OK: $target restarted successfully"
|
||||
else
|
||||
log "FAIL: $target restart failed (attempt $attempts)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_all_sessions() {
|
||||
local sessions
|
||||
|
||||
if [ -n "$MONITOR_SESSIONS" ]; then
|
||||
IFS=',' read -ra sessions <<< "$MONITOR_SESSIONS"
|
||||
else
|
||||
sessions=()
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && sessions+=("$line")
|
||||
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
local total=0 stuck=0 dead=0 ok=0
|
||||
for session in "${sessions[@]}"; do
|
||||
[ -z "$session" ] && continue
|
||||
# Get pane targets
|
||||
local panes
|
||||
panes=$(tmux list-panes -t "$session" -F "${session}:#{window_index}.#{pane_index}" 2>/dev/null || true)
|
||||
for target in $panes; do
|
||||
check_pane "$target"
|
||||
total=$((total + 1))
|
||||
done
|
||||
done
|
||||
|
||||
log "CHECK: Processed $total panes"
|
||||
}
|
||||
|
||||
# === STATUS DISPLAY ===
|
||||
|
||||
show_status() {
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
echo "No pane state file found at $STATE_FILE"
|
||||
echo "Run pane-watchdog.sh once to initialize."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$STATE_FILE" <<'PYEOF'
|
||||
import json, sys, time
|
||||
|
||||
state_file = sys.argv[1]
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
print("No state data yet.")
|
||||
sys.exit(0)
|
||||
|
||||
if not state:
|
||||
print("No panes tracked.")
|
||||
sys.exit(0)
|
||||
|
||||
now = int(time.time())
|
||||
print(f"{'PANE':<35} {'STATUS':<12} {'STALE':<6} {'LAST CHANGE':<15} {'RESTARTS'}")
|
||||
print("-" * 90)
|
||||
|
||||
for target in sorted(state.keys()):
|
||||
p = state[target]
|
||||
status = p.get("status", "?")
|
||||
same = p.get("same_count", 0)
|
||||
last_change = p.get("last_change", 0)
|
||||
restarts = p.get("restart_attempts", 0)
|
||||
|
||||
if last_change:
|
||||
ago = now - last_change
|
||||
if ago < 60:
|
||||
change_str = f"{ago}s ago"
|
||||
elif ago < 3600:
|
||||
change_str = f"{ago//60}m ago"
|
||||
else:
|
||||
change_str = f"{ago//3600}h ago"
|
||||
else:
|
||||
change_str = "never"
|
||||
|
||||
# Color code
|
||||
if status == "OK":
|
||||
icon = "✓"
|
||||
elif status == "STUCK":
|
||||
icon = "✖"
|
||||
elif status == "DEAD":
|
||||
icon = "☠"
|
||||
elif status == "STALE":
|
||||
icon = "⏳"
|
||||
else:
|
||||
icon = "?"
|
||||
|
||||
print(f" {icon} {target:<32} {status:<12} {same:<6} {change_str:<15} {restarts}")
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# === DAEMON MODE ===
|
||||
|
||||
run_daemon() {
|
||||
log "DAEMON: Starting (interval=${CHECK_INTERVAL}s, stuck_threshold=${STUCK_CYCLES})"
|
||||
echo "Pane watchdog started. Checking every ${CHECK_INTERVAL}s. Ctrl+C to stop."
|
||||
echo "Log: $LOG_FILE"
|
||||
echo "State: $STATE_FILE"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
check_all_sessions
|
||||
sleep "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# === MAIN ===
|
||||
|
||||
case "${1:-}" in
|
||||
--daemon)
|
||||
run_daemon
|
||||
;;
|
||||
--status)
|
||||
show_status
|
||||
;;
|
||||
--session)
|
||||
if [ -z "${2:-}" ]; then
|
||||
echo "Usage: pane-watchdog.sh --session SESSION_NAME"
|
||||
exit 1
|
||||
fi
|
||||
MONITOR_SESSIONS="$2"
|
||||
check_all_sessions
|
||||
;;
|
||||
--help|-h)
|
||||
echo "pane-watchdog.sh — Detect stuck/dead tmux panes and auto-restart"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " pane-watchdog.sh # One-shot check"
|
||||
echo " pane-watchdog.sh --daemon # Continuous monitoring"
|
||||
echo " pane-watchdog.sh --status # Show pane state"
|
||||
echo " pane-watchdog.sh --session S # Check one session"
|
||||
echo ""
|
||||
echo "Config (env vars):"
|
||||
echo " PANE_CHECK_INTERVAL Seconds between checks (default: 120)"
|
||||
echo " PANE_WATCHDOG_SESSIONS Comma-separated session names"
|
||||
echo " PANE_STATE_FILE State file path"
|
||||
echo " STUCK_CYCLES Unchanged cycles before STUCK (default: 2)"
|
||||
;;
|
||||
*)
|
||||
check_all_sessions
|
||||
;;
|
||||
esac
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Soul Eval Gate — The Conscience of the Training Pipeline
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Uses Hermes CLI plus workforce-manager to triage and review.
|
||||
# Timmy is the brain. Other agents are the hands.
|
||||
|
||||
set -uo pipefail\n\nSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
set -uo pipefail
|
||||
|
||||
LOG_DIR="$HOME/.hermes/logs"
|
||||
LOG="$LOG_DIR/timmy-orchestrator.log"
|
||||
@@ -40,7 +40,6 @@ gather_state() {
|
||||
> "$state_dir/unassigned.txt"
|
||||
> "$state_dir/open_prs.txt"
|
||||
> "$state_dir/agent_status.txt"
|
||||
> "$state_dir/uncommitted_work.txt"
|
||||
|
||||
for repo in $REPOS; do
|
||||
local short=$(echo "$repo" | cut -d/ -f2)
|
||||
@@ -72,24 +71,6 @@ for p in json.load(sys.stdin):
|
||||
tail -50 "/tmp/kimi-heartbeat.log" 2>/dev/null | grep -c "FAILED:" | xargs -I{} echo "Kimi recent failures: {}" >> "$state_dir/agent_status.txt"
|
||||
tail -1 "/tmp/kimi-heartbeat.log" 2>/dev/null | xargs -I{} echo "Kimi last event: {}" >> "$state_dir/agent_status.txt"
|
||||
|
||||
# Scan worktrees for uncommitted work
|
||||
for wt_dir in "$HOME/worktrees"/*/; do
|
||||
[ -d "$wt_dir" ] || continue
|
||||
[ -d "$wt_dir/.git" ] || continue
|
||||
local dirty
|
||||
dirty=$(cd "$wt_dir" && git status --porcelain 2>/dev/null | wc -l | tr -d " ")
|
||||
if [ "${dirty:-0}" -gt 0 ]; then
|
||||
local branch
|
||||
branch=$(cd "$wt_dir" && git branch --show-current 2>/dev/null || echo "?")
|
||||
local age=""
|
||||
local last_commit
|
||||
last_commit=$(cd "$wt_dir" && git log -1 --format=%ct 2>/dev/null || echo 0)
|
||||
local now=$(date +%s)
|
||||
local stale_mins=$(( (now - last_commit) / 60 ))
|
||||
echo "DIR=$wt_dir BRANCH=$branch DIRTY=$dirty STALE=${stale_mins}m" >> "$state_dir/uncommitted_work.txt"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$state_dir"
|
||||
}
|
||||
|
||||
@@ -100,25 +81,6 @@ run_triage() {
|
||||
|
||||
log "Cycle: $unassigned_count unassigned, $pr_count open PRs"
|
||||
|
||||
# Check for uncommitted work — nag if stale
|
||||
local uncommitted_count
|
||||
uncommitted_count=$(wc -l < "$state_dir/uncommitted_work.txt" 2>/dev/null | tr -d " " || echo 0)
|
||||
if [ "${uncommitted_count:-0}" -gt 0 ]; then
|
||||
log "WARNING: $uncommitted_count worktree(s) with uncommitted work"
|
||||
while IFS= read -r line; do
|
||||
log " UNCOMMITTED: $line"
|
||||
# Auto-commit stale work (>60 min without commit)
|
||||
local stale=$(echo "$line" | sed 's/.*STALE=\([0-9]*\)m.*/\1/')
|
||||
local wt_dir=$(echo "$line" | sed 's/.*DIR=\([^ ]*\) .*/\1/')
|
||||
if [ "${stale:-0}" -gt 60 ]; then
|
||||
log " AUTO-COMMITTING stale work in $wt_dir (${stale}m stale)"
|
||||
(cd "$wt_dir" && git add -A && git commit -m "WIP: orchestrator auto-commit — ${stale}m stale work
|
||||
|
||||
Preserved by timmy-orchestrator to prevent loss." 2>/dev/null && git push 2>/dev/null) && log " COMMITTED: $wt_dir" || log " COMMIT FAILED: $wt_dir"
|
||||
fi
|
||||
done < "$state_dir/uncommitted_work.txt"
|
||||
fi
|
||||
|
||||
# If nothing to do, skip the LLM call
|
||||
if [ "$unassigned_count" -eq 0 ] && [ "$pr_count" -eq 0 ]; then
|
||||
log "Nothing to triage"
|
||||
@@ -236,12 +198,6 @@ FOOTER
|
||||
log "=== Timmy Orchestrator Started (PID $$) ==="
|
||||
log "Cycle: ${CYCLE_INTERVAL}s | Auto-assign: ${AUTO_ASSIGN_UNASSIGNED} | Inference surface: Hermes CLI"
|
||||
|
||||
# Start auto-commit-guard daemon for work preservation
|
||||
if ! pgrep -f "auto-commit-guard.sh" >/dev/null 2>&1; then
|
||||
nohup bash "$SCRIPT_DIR/auto-commit-guard.sh" 120 >> "$LOG_DIR/auto-commit-guard.log" 2>&1 &
|
||||
log "Started auto-commit-guard daemon (PID $!)"
|
||||
fi
|
||||
|
||||
WORKFORCE_CYCLE=0
|
||||
|
||||
while true; do
|
||||
|
||||
@@ -196,37 +196,7 @@
|
||||
"paused_reason": null,
|
||||
"skills": [],
|
||||
"skill": null
|
||||
},
|
||||
{
|
||||
"id": "tmux-supervisor-513",
|
||||
"name": "Autonomous Cron Supervisor",
|
||||
"prompt": "Load the tmux-supervisor skill and execute the monitoring protocol.\n\nCheck both `dev` and `timmy` tmux sessions for idle panes. Only send Telegram notifications on actionable events (idle, overflow, failure). Be silent when all agents are working.\n\nSteps:\n1. List all tmux sessions (skip 'Alexander')\n2. For each session, list windows and panes\n3. Capture each pane and classify state (idle vs active)\n4. For idle panes: read context, craft context-aware prompt\n5. Send /queue prompts to idle panes\n6. Verify prompts landed\n7. Only notify via Telegram if:\n - A pane was prompted (idle detected)\n - A pane shows context overflow (>80%)\n - A pane is stuck or crashed\n8. If all panes are active: respond with [SILENT]",
|
||||
"schedule": {
|
||||
"kind": "interval",
|
||||
"minutes": 7,
|
||||
"display": "every 7m"
|
||||
},
|
||||
"schedule_display": "every 7m",
|
||||
"repeat": {
|
||||
"times": null,
|
||||
"completed": 0
|
||||
},
|
||||
"enabled": true,
|
||||
"created_at": "2026-04-15T03:00:00.000000+00:00",
|
||||
"next_run_at": null,
|
||||
"last_run_at": null,
|
||||
"last_status": null,
|
||||
"last_error": null,
|
||||
"deliver": "telegram",
|
||||
"origin": null,
|
||||
"state": "scheduled",
|
||||
"paused_at": null,
|
||||
"paused_reason": null,
|
||||
"skills": [
|
||||
"tmux-supervisor"
|
||||
],
|
||||
"skill": "tmux-supervisor"
|
||||
}
|
||||
],
|
||||
"updated_at": "2026-04-13T02:00:00+00:00"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
- name: Nightly Pipeline Scheduler
|
||||
schedule: '*/30 18-23,0-8 * * *' # Every 30 min, off-peak hours only
|
||||
tasks:
|
||||
- name: Check and start pipelines
|
||||
shell: "bash scripts/nightly-pipeline-scheduler.sh"
|
||||
env:
|
||||
PIPELINE_TOKEN_LIMIT: "500000"
|
||||
PIPELINE_PEAK_START: "9"
|
||||
PIPELINE_PEAK_END: "18"
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>ai.timmy.auto-commit-guard</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>/Users/apayne/.hermes/bin/auto-commit-guard.sh</string>
|
||||
<string>120</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/apayne/.hermes/logs/auto-commit-guard.stdout.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/apayne/.hermes/logs/auto-commit-guard.stderr.log</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/apayne</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,21 +0,0 @@
|
||||
# Gitea Accessibility Fix - R4: Time Elements
|
||||
|
||||
WCAG 1.3.1: Relative timestamps lack machine-readable fallbacks.
|
||||
|
||||
## Fix
|
||||
|
||||
Wrap relative timestamps in `<time datetime="...">` elements.
|
||||
|
||||
## Files
|
||||
|
||||
- `custom/templates/custom/time_relative.tmpl` - Reusable `<time>` helper
|
||||
- `custom/templates/repo/list_a11y.tmpl` - Explore/Repos list override
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
cp -r custom/templates/* /path/to/gitea/custom/templates/
|
||||
systemctl restart gitea
|
||||
```
|
||||
|
||||
Closes #554
|
||||
@@ -1,27 +0,0 @@
|
||||
{{/*
|
||||
Gitea a11y fix: R4 <time> elements for relative timestamps
|
||||
Deploy to: custom/templates/custom/time_relative.tmpl
|
||||
*/}}
|
||||
|
||||
{{define "custom/time_relative"}}
|
||||
{{if and .Time .Relative}}
|
||||
<time datetime="{{.Time.Format "2006-01-02T15:04:05Z07:00"}}" title="{{.Time.Format "Jan 02, 2006 15:04"}}">
|
||||
{{.Relative}}
|
||||
</time>
|
||||
{{else if .Relative}}
|
||||
<span>{{.Relative}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "custom/time_from_unix"}}
|
||||
{{if .Relative}}
|
||||
<time datetime="" data-unix="{{.Unix}}" title="">{{.Relative}}</time>
|
||||
<script>
|
||||
(function() {
|
||||
var el = document.currentScript.previousElementSibling;
|
||||
var unix = parseInt(el.getAttribute('data-unix'));
|
||||
if (unix) { el.setAttribute('datetime', new Date(unix * 1000).toISOString()); el.setAttribute('title', new Date(unix * 1000).toLocaleString()); }
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{/*
|
||||
Gitea a11y fix: R4 <time> elements for relative timestamps on repo list
|
||||
Deploy to: custom/templates/repo/list_a11y.tmpl
|
||||
*/}}
|
||||
|
||||
{{/* Star count link with aria-label */}}
|
||||
<a class="repo-card-star" href="{{.RepoLink}}/stars" aria-label="{{.NumStars}} stars" title="{{.NumStars}} stars">
|
||||
<svg class="octicon octicon-star" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
|
||||
</svg>
|
||||
<span>{{.NumStars}}</span>
|
||||
</a>
|
||||
|
||||
{{/* Fork count link with aria-label */}}
|
||||
<a class="repo-card-fork" href="{{.RepoLink}}/forks" aria-label="{{.NumForks}} forks" title="{{.NumForks}} forks">
|
||||
<svg class="octicon octicon-repo-forked" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||
<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-.878a2.25 2.25 0 111.5 0v.878a2.25 2.25 0 01-2.25 2.25h-1.5v2.128a2.251 2.251 0 11-1.5 0V8.5h-1.5A2.25 2.25 0 013.5 6.25v-.878a2.25 2.25 0 111.5 0zM5 3.25a.75.75 0 10-1.5 0 .75.75 0 001.5 0zm6.75.75a.75.75 0 100-1.5.75.75 0 000 1.5zm-3 8.75a.75.75 0 10-1.5 0 .75.75 0 001.5 0z"/>
|
||||
</svg>
|
||||
<span>{{.NumForks}}</span>
|
||||
</a>
|
||||
|
||||
{{/* Relative timestamp with <time> element for a11y */}}
|
||||
{{if .UpdatedUnix}}
|
||||
<time datetime="{{.UpdatedUnix | TimeSinceISO}}" title="{{.UpdatedUnix | DateFmtLong}}" class="text-light">
|
||||
{{.UpdatedUnix | TimeSince}}
|
||||
</time>
|
||||
{{end}}
|
||||
@@ -1,150 +0,0 @@
|
||||
# Visual Accessibility Audit — Foundation Web Properties
|
||||
|
||||
**Issue:** timmy-config #492
|
||||
**Date:** 2026-04-13
|
||||
**Label:** gemma-4-multimodal
|
||||
**Scope:** forge.alexanderwhitestone.com (Gitea 1.25.4)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Foundation's primary accessible web property is the Gitea forge. The Matrix homeserver (matrix.timmy.foundation) is currently unreachable (DNS/SSL issues). This audit covers the forge across three page types: Homepage, Login, and Explore/Repositories.
|
||||
|
||||
**Overall: 6 WCAG 2.1 AA violations found, 4 best-practice recommendations.**
|
||||
|
||||
---
|
||||
|
||||
## Pages Audited
|
||||
|
||||
| Page | URL | Status |
|
||||
|------|-----|--------|
|
||||
| Homepage | forge.alexanderwhitestone.com | Live |
|
||||
| Sign In | forge.alexanderwhitestone.com/user/login | Live |
|
||||
| Explore Repos | forge.alexanderwhitestone.com/explore/repos | Live |
|
||||
| Matrix/Element | matrix.timmy.foundation | DOWN (DNS/SSL) |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### P1 — Violations (WCAG 2.1 AA)
|
||||
|
||||
#### V1: No Skip Navigation Link (2.4.1)
|
||||
- **Pages:** All
|
||||
- **Severity:** Medium
|
||||
- **Description:** No "Skip to content" link exists. Keyboard users must tab through the full navigation on every page load.
|
||||
- **Evidence:** Programmatic check returned `skipNav: false`
|
||||
- **Fix:** Add `<a href="#main" class="skip-link">Skip to content</a>` visually hidden until focused.
|
||||
|
||||
#### V2: 25 Form Inputs Without Labels (1.3.1, 3.3.2)
|
||||
- **Pages:** Explore/Repositories (filter dropdowns)
|
||||
- **Severity:** High
|
||||
- **Description:** The search input and all radio buttons in the Filter/Sort dropdowns lack programmatic label associations.
|
||||
- **Evidence:** Programmatic check found 25 inputs without `label[for=]`, `aria-label`, or `aria-labelledby`
|
||||
- **Affected inputs:** `q` (search), `archived` (x2), `fork` (x2), `mirror` (x2), `template` (x2), `private` (x2), `sort` (x12), `clear-filter` (x1)
|
||||
- **Fix:** Add `aria-label="Search repositories"` to search input. Add `aria-label` to each radio button group and individual options.
|
||||
|
||||
#### V3: Low-Contrast Footer Text (1.4.3)
|
||||
- **Pages:** All
|
||||
- **Severity:** Medium
|
||||
- **Description:** Footer text (version, page render time) appears light gray on white, likely failing the 4.5:1 contrast ratio.
|
||||
- **Evidence:** 30 elements flagged as potential low-contrast suspects.
|
||||
- **Fix:** Darken footer text to at least `#767676` on white (4.54:1 ratio).
|
||||
|
||||
#### V4: Green Link Color Fails Contrast (1.4.3)
|
||||
- **Pages:** Homepage
|
||||
- **Severity:** Medium
|
||||
- **Description:** Inline links use medium-green (~#609926) on white. This shade typically fails 4.5:1 for normal body text.
|
||||
- **Evidence:** Visual analysis identified green links ("run the binary", "Docker", "contributing") as potentially failing.
|
||||
- **Fix:** Darken link color to at least `#507020` or add an underline for non-color differentiation (SC 1.4.1).
|
||||
|
||||
#### V5: Missing Header/Banner Landmark (1.3.1)
|
||||
- **Pages:** All
|
||||
- **Severity:** Low
|
||||
- **Description:** No `<header>` or `role="banner"` element found. The navigation bar is a `<nav>` but not wrapped in a banner landmark.
|
||||
- **Evidence:** `landmarks.banner: 0`
|
||||
- **Fix:** Wrap the top navigation in `<header>` or add `role="banner"`.
|
||||
|
||||
#### V6: Heading Hierarchy Issue (1.3.1)
|
||||
- **Pages:** Login
|
||||
- **Severity:** Low
|
||||
- **Description:** The Sign In heading is `<h4>` rather than `<h1>`, breaking the heading hierarchy. The page has no `<h1>`.
|
||||
- **Evidence:** Accessibility tree shows `heading "Sign In" [level=4]`
|
||||
- **Fix:** Use `<h1>` for "Sign In" on the login page.
|
||||
|
||||
---
|
||||
|
||||
### P2 — Best Practice Recommendations
|
||||
|
||||
#### R1: Add Password Visibility Toggle
|
||||
- **Page:** Login
|
||||
- **Description:** No show/hide toggle on the password field. This helps users with cognitive or motor impairments verify input.
|
||||
|
||||
#### R2: Add `aria-required` to Required Fields
|
||||
- **Page:** Login
|
||||
- **Evidence:** `inputsWithAriaRequired: 0` (no inputs marked as required)
|
||||
- **Description:** The username field shows a red asterisk but has no `required` or `aria-required="true"` attribute.
|
||||
|
||||
#### R3: Improve Star/Fork Link Labels
|
||||
- **Page:** Explore Repos
|
||||
- **Description:** Star and fork counts are bare numbers (e.g., "0", "2"). Screen readers announce these without context.
|
||||
- **Fix:** Add `aria-label="2 stars"` / `aria-label="0 forks"` to count links.
|
||||
|
||||
#### R4: Use `<time>` Elements for Timestamps
|
||||
- **Page:** Explore Repos
|
||||
- **Description:** Relative timestamps ("2 minutes ago") are human-readable but lack machine-readable fallbacks.
|
||||
- **Fix:** Wrap in `<time datetime="2026-04-13T17:00:00Z">2 minutes ago</time>`.
|
||||
|
||||
---
|
||||
|
||||
## What's Working Well
|
||||
|
||||
- **Color contrast (primary):** Black text on white backgrounds — excellent 21:1 ratio.
|
||||
- **Heading structure (homepage):** Clean h1 > h2 > h3 hierarchy.
|
||||
- **Landmark regions:** `<main>` and `<nav>` landmarks present.
|
||||
- **Language attribute:** `lang="en-US"` set on `<html>`.
|
||||
- **Link text:** Descriptive — no "click here" or "read more" patterns found.
|
||||
- **Form layout:** Login form uses clean single-column with good spacing.
|
||||
- **Submit button:** Full-width, good contrast, large touch target.
|
||||
- **Navigation:** Simple, consistent across pages.
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **matrix.timmy.foundation:** Unreachable (DNS resolution failure / SSL cert mismatch). Should be re-audited when operational.
|
||||
- **Evennia web client (localhost:4001):** Local-only, not publicly accessible.
|
||||
- **WCAG AAA criteria:** This audit covers AA only.
|
||||
|
||||
---
|
||||
|
||||
## Remediation Priority
|
||||
|
||||
| Priority | Issue | Effort |
|
||||
|----------|-------|--------|
|
||||
| P1 | V2: 25 unlabeled inputs | Medium |
|
||||
| P1 | V1: Skip nav link | Small |
|
||||
| P1 | V4: Green link contrast | Small |
|
||||
| P1 | V3: Footer text contrast | Small |
|
||||
| P2 | V6: Heading hierarchy | Small |
|
||||
| P2 | V5: Banner landmark | Small |
|
||||
| P2 | R1-R4: Best practices | Small |
|
||||
|
||||
---
|
||||
|
||||
## Automated Check Results
|
||||
|
||||
```
|
||||
skipNav: false
|
||||
headings: h1(3), h4(1)
|
||||
imgsNoAlt: 0 / 1
|
||||
inputsNoLabel: 25
|
||||
genericLinks: 0
|
||||
lowContrastSuspects: 30
|
||||
inputsWithAriaRequired: 0
|
||||
landmarks: main=1, nav=2, banner=0, contentinfo=2
|
||||
hasLang: true (en-US)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Generated via visual + programmatic analysis of forge.alexanderwhitestone.com*
|
||||
@@ -1,179 +0,0 @@
|
||||
# 3D World Glitch Detection — Matrix Scanner
|
||||
|
||||
**Reference:** timmy-config#491
|
||||
**Label:** gemma-4-multimodal
|
||||
**Version:** 0.1.0
|
||||
|
||||
## Overview
|
||||
|
||||
The Matrix Glitch Detector scans 3D web worlds for visual artifacts and
|
||||
rendering anomalies. It uses browser automation to capture screenshots from
|
||||
multiple camera angles, then sends them to a vision AI model for analysis
|
||||
against a library of known glitch patterns.
|
||||
|
||||
## Detected Glitch Categories
|
||||
|
||||
| Category | Severity | Description |
|
||||
|---|---|---|
|
||||
| Floating Assets | HIGH | Objects not grounded — hovering above surfaces |
|
||||
| Z-Fighting | MEDIUM | Coplanar surfaces flickering/competing for depth |
|
||||
| Missing Textures | CRITICAL | Placeholder colors (magenta, checkerboard) |
|
||||
| Clipping | HIGH | Geometry passing through other objects |
|
||||
| Broken Normals | MEDIUM | Inside-out or incorrectly lit surfaces |
|
||||
| Shadow Artifacts | LOW | Detached, mismatched, or acne shadows |
|
||||
| LOD Popping | LOW | Abrupt level-of-detail transitions |
|
||||
| Lightmap Errors | MEDIUM | Dark splotches, light leaks, baking failures |
|
||||
| Water/Reflection | MEDIUM | Incorrect environment reflections |
|
||||
| Skybox Seam | LOW | Visible seams at cubemap face edges |
|
||||
|
||||
## Installation
|
||||
|
||||
No external dependencies required — pure Python 3.10+.
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git
|
||||
cd timmy-config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Scan
|
||||
|
||||
```bash
|
||||
python bin/matrix_glitch_detector.py https://matrix.example.com/world/alpha
|
||||
```
|
||||
|
||||
### Multi-Angle Scan
|
||||
|
||||
```bash
|
||||
python bin/matrix_glitch_detector.py https://matrix.example.com/world/alpha \
|
||||
--angles 8 \
|
||||
--output glitch_report.json
|
||||
```
|
||||
|
||||
### Demo Mode
|
||||
|
||||
```bash
|
||||
python bin/matrix_glitch_detector.py --demo
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `url` | (required) | URL of the 3D world to scan |
|
||||
| `--angles N` | 4 | Number of camera angles to capture |
|
||||
| `--output PATH` | stdout | Output file for JSON report |
|
||||
| `--min-severity` | info | Minimum severity: info/low/medium/high/critical |
|
||||
| `--demo` | off | Run with simulated detections |
|
||||
| `--verbose` | off | Enable verbose output |
|
||||
|
||||
## Report Format
|
||||
|
||||
The JSON report includes:
|
||||
|
||||
```json
|
||||
{
|
||||
"scan_id": "uuid",
|
||||
"url": "https://...",
|
||||
"timestamp": "ISO-8601",
|
||||
"total_screenshots": 4,
|
||||
"angles_captured": ["front", "right", "back", "left"],
|
||||
"glitches": [
|
||||
{
|
||||
"id": "short-uuid",
|
||||
"category": "floating_assets",
|
||||
"name": "Floating Chair",
|
||||
"description": "Office chair floating 0.3m above floor",
|
||||
"severity": "high",
|
||||
"confidence": 0.87,
|
||||
"location_x": 35.2,
|
||||
"location_y": 62.1,
|
||||
"screenshot_index": 0,
|
||||
"screenshot_angle": "front",
|
||||
"timestamp": "ISO-8601"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_glitches": 4,
|
||||
"by_severity": {"critical": 1, "high": 2, "medium": 1},
|
||||
"by_category": {"floating_assets": 1, "missing_textures": 1, ...},
|
||||
"highest_severity": "critical",
|
||||
"clean_screenshots": 0
|
||||
},
|
||||
"metadata": {
|
||||
"detector_version": "0.1.0",
|
||||
"pattern_count": 10,
|
||||
"reference": "timmy-config#491"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Vision AI Integration
|
||||
|
||||
The detector supports any OpenAI-compatible vision API. Set these
|
||||
environment variables:
|
||||
|
||||
```bash
|
||||
export VISION_API_KEY="your-api-key"
|
||||
export VISION_API_BASE="https://api.openai.com/v1" # optional
|
||||
export VISION_MODEL="gpt-4o" # optional, default: gpt-4o
|
||||
```
|
||||
|
||||
For browser-based capture with `browser_vision`:
|
||||
|
||||
```bash
|
||||
export BROWSER_VISION_SCRIPT="/path/to/browser_vision.py"
|
||||
```
|
||||
|
||||
## Glitch Patterns
|
||||
|
||||
Pattern definitions live in `bin/glitch_patterns.py`. Each pattern includes:
|
||||
|
||||
- **category** — Enum matching the glitch type
|
||||
- **detection_prompts** — Instructions for the vision model
|
||||
- **visual_indicators** — What to look for in screenshots
|
||||
- **confidence_threshold** — Minimum confidence to report
|
||||
|
||||
### Adding Custom Patterns
|
||||
|
||||
```python
|
||||
from glitch_patterns import GlitchPattern, GlitchCategory, GlitchSeverity
|
||||
|
||||
custom = GlitchPattern(
|
||||
category=GlitchCategory.FLOATING_ASSETS,
|
||||
name="Custom Glitch",
|
||||
description="Your description",
|
||||
severity=GlitchSeverity.MEDIUM,
|
||||
detection_prompts=["Look for..."],
|
||||
visual_indicators=["indicator 1", "indicator 2"],
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_glitch_detector.py -v
|
||||
# or
|
||||
python tests/test_glitch_detector.py
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
bin/
|
||||
matrix_glitch_detector.py — Main CLI entry point
|
||||
glitch_patterns.py — Pattern definitions and prompt builder
|
||||
tests/
|
||||
test_glitch_detector.py — Unit and integration tests
|
||||
docs/
|
||||
glitch-detection.md — This documentation
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Browser automation requires a headless browser environment
|
||||
- Vision AI analysis depends on model availability and API limits
|
||||
- Placeholder screenshots are generated when browser capture is unavailable
|
||||
- Detection accuracy varies by scene complexity and lighting conditions
|
||||
@@ -1,151 +0,0 @@
|
||||
// a11y-check.js — Automated accessibility audit script for Foundation web properties
|
||||
// Run in browser console or via Playwright/Puppeteer
|
||||
//
|
||||
// Usage: Paste into DevTools console, or include in automated test suite.
|
||||
// Returns a JSON object with pass/fail for WCAG 2.1 AA checks.
|
||||
|
||||
(function a11yAudit() {
|
||||
const results = {
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
violations: [],
|
||||
passes: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// --- 2.4.1 Skip Navigation ---
|
||||
const skipLink = document.querySelector('a[href="#main"], a[href="#content"], .skip-nav, .skip-link');
|
||||
if (skipLink) {
|
||||
results.passes.push({ rule: '2.4.1', name: 'Skip Navigation', detail: 'Skip link found' });
|
||||
} else {
|
||||
results.violations.push({ rule: '2.4.1', name: 'Skip Navigation', severity: 'medium', detail: 'No skip-to-content link found' });
|
||||
}
|
||||
|
||||
// --- 1.3.1 / 3.3.2 Form Labels ---
|
||||
const unlabeledInputs = Array.from(document.querySelectorAll('input, select, textarea')).filter(el => {
|
||||
if (el.type === 'hidden') return false;
|
||||
const id = el.id;
|
||||
const hasLabel = id && document.querySelector(`label[for="${id}"]`);
|
||||
const hasAriaLabel = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby');
|
||||
const hasTitle = el.getAttribute('title');
|
||||
const hasPlaceholder = el.getAttribute('placeholder'); // placeholder alone is NOT sufficient
|
||||
return !hasLabel && !hasAriaLabel && !hasTitle;
|
||||
});
|
||||
if (unlabeledInputs.length === 0) {
|
||||
results.passes.push({ rule: '3.3.2', name: 'Form Labels', detail: 'All inputs have labels' });
|
||||
} else {
|
||||
results.violations.push({
|
||||
rule: '3.3.2',
|
||||
name: 'Form Labels',
|
||||
severity: 'high',
|
||||
detail: `${unlabeledInputs.length} inputs without programmatic labels`,
|
||||
elements: unlabeledInputs.map(el => ({ tag: el.tagName, type: el.type, name: el.name, id: el.id }))
|
||||
});
|
||||
}
|
||||
|
||||
// --- 1.4.3 Contrast (heuristic: very light text colors) ---
|
||||
const lowContrast = Array.from(document.querySelectorAll('p, span, a, li, td, th, label, small, footer *')).filter(el => {
|
||||
const style = getComputedStyle(el);
|
||||
const color = style.color;
|
||||
// Check for very light RGB values (r/g/b < 120)
|
||||
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (!match) return false;
|
||||
const [, r, g, b] = match.map(Number);
|
||||
return r < 120 && g < 120 && b < 120 && (r + g + b) < 200;
|
||||
});
|
||||
if (lowContrast.length === 0) {
|
||||
results.passes.push({ rule: '1.4.3', name: 'Contrast', detail: 'No obviously low-contrast text found' });
|
||||
} else {
|
||||
results.warnings.push({ rule: '1.4.3', name: 'Contrast', detail: `${lowContrast.length} elements with potentially low contrast (manual verification needed)` });
|
||||
}
|
||||
|
||||
// --- 1.3.1 Heading Hierarchy ---
|
||||
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).map(h => ({
|
||||
level: parseInt(h.tagName[1]),
|
||||
text: h.textContent.trim().substring(0, 80)
|
||||
}));
|
||||
let headingIssues = [];
|
||||
let lastLevel = 0;
|
||||
for (const h of headings) {
|
||||
if (h.level > lastLevel + 1 && lastLevel > 0) {
|
||||
headingIssues.push(`Skipped h${lastLevel} to h${h.level}: "${h.text}"`);
|
||||
}
|
||||
lastLevel = h.level;
|
||||
}
|
||||
if (headingIssues.length === 0 && headings.length > 0) {
|
||||
results.passes.push({ rule: '1.3.1', name: 'Heading Hierarchy', detail: `${headings.length} headings, proper nesting` });
|
||||
} else if (headingIssues.length > 0) {
|
||||
results.violations.push({ rule: '1.3.1', name: 'Heading Hierarchy', severity: 'low', detail: headingIssues.join('; ') });
|
||||
}
|
||||
|
||||
// --- 1.3.1 Landmarks ---
|
||||
const landmarks = {
|
||||
main: document.querySelectorAll('main, [role="main"]').length,
|
||||
nav: document.querySelectorAll('nav, [role="navigation"]').length,
|
||||
banner: document.querySelectorAll('header, [role="banner"]').length,
|
||||
contentinfo: document.querySelectorAll('footer, [role="contentinfo"]').length
|
||||
};
|
||||
if (landmarks.main > 0) {
|
||||
results.passes.push({ rule: '1.3.1', name: 'Main Landmark', detail: 'Found' });
|
||||
} else {
|
||||
results.violations.push({ rule: '1.3.1', name: 'Main Landmark', severity: 'medium', detail: 'No <main> or role="main" found' });
|
||||
}
|
||||
if (landmarks.banner === 0) {
|
||||
results.violations.push({ rule: '1.3.1', name: 'Banner Landmark', severity: 'low', detail: 'No <header> or role="banner" found' });
|
||||
}
|
||||
|
||||
// --- 3.3.1 Required Fields ---
|
||||
const requiredInputs = document.querySelectorAll('input[required], input[aria-required="true"]');
|
||||
if (requiredInputs.length > 0) {
|
||||
results.passes.push({ rule: '3.3.1', name: 'Required Fields', detail: `${requiredInputs.length} inputs marked as required` });
|
||||
} else {
|
||||
const visualRequired = document.querySelector('.required, [class*="required"], label .text-danger');
|
||||
if (visualRequired) {
|
||||
results.warnings.push({ rule: '3.3.1', name: 'Required Fields', detail: 'Visual indicators found but no aria-required attributes' });
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2.4.2 Page Title ---
|
||||
if (document.title && document.title.trim().length > 0) {
|
||||
results.passes.push({ rule: '2.4.2', name: 'Page Title', detail: document.title });
|
||||
} else {
|
||||
results.violations.push({ rule: '2.4.2', name: 'Page Title', severity: 'medium', detail: 'Page has no title' });
|
||||
}
|
||||
|
||||
// --- 3.1.1 Language ---
|
||||
const lang = document.documentElement.lang;
|
||||
if (lang) {
|
||||
results.passes.push({ rule: '3.1.1', name: 'Language', detail: lang });
|
||||
} else {
|
||||
results.violations.push({ rule: '3.1.1', name: 'Language', severity: 'medium', detail: 'No lang attribute on <html>' });
|
||||
}
|
||||
|
||||
// --- Images without alt ---
|
||||
const imgsNoAlt = Array.from(document.querySelectorAll('img:not([alt])'));
|
||||
if (imgsNoAlt.length === 0) {
|
||||
results.passes.push({ rule: '1.1.1', name: 'Image Alt Text', detail: 'All images have alt attributes' });
|
||||
} else {
|
||||
results.violations.push({ rule: '1.1.1', name: 'Image Alt Text', severity: 'high', detail: `${imgsNoAlt.length} images without alt attributes` });
|
||||
}
|
||||
|
||||
// --- Buttons without accessible names ---
|
||||
const emptyButtons = Array.from(document.querySelectorAll('button')).filter(b => {
|
||||
return !b.textContent.trim() && !b.getAttribute('aria-label') && !b.getAttribute('aria-labelledby') && !b.getAttribute('title');
|
||||
});
|
||||
if (emptyButtons.length === 0) {
|
||||
results.passes.push({ rule: '4.1.2', name: 'Button Names', detail: 'All buttons have accessible names' });
|
||||
} else {
|
||||
results.violations.push({ rule: '4.1.2', name: 'Button Names', severity: 'medium', detail: `${emptyButtons.length} buttons without accessible names` });
|
||||
}
|
||||
|
||||
// Summary
|
||||
results.summary = {
|
||||
violations: results.violations.length,
|
||||
passes: results.passes.length,
|
||||
warnings: results.warnings.length
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
return results;
|
||||
})();
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from hermes_tools import browser_navigate, browser_vision
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from hermes_tools import browser_navigate, browser_vision
|
||||
|
||||
|
||||
@@ -1,884 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
foundation_accessibility_audit.py — Multimodal Visual Accessibility Audit.
|
||||
|
||||
Analyzes web pages for WCAG 2.1 AA compliance using both programmatic checks
|
||||
and vision model analysis. Screenshots pages, checks contrast ratios, detects
|
||||
layout issues, validates alt text, and produces structured audit reports.
|
||||
|
||||
Usage:
|
||||
# Audit a single page
|
||||
python scripts/foundation_accessibility_audit.py --url https://timmyfoundation.org
|
||||
|
||||
# Audit multiple pages
|
||||
python scripts/foundation_accessibility_audit.py --url https://timmyfoundation.org --pages /about /donate /blog
|
||||
|
||||
# With vision model analysis (Gemma 3)
|
||||
python scripts/foundation_accessibility_audit.py --url https://timmyfoundation.org --vision
|
||||
|
||||
# Programmatic-only (no vision model needed)
|
||||
python scripts/foundation_accessibility_audit.py --url https://timmyfoundation.org --programmatic
|
||||
|
||||
# Output as text report
|
||||
python scripts/foundation_accessibility_audit.py --url https://timmyfoundation.org --format text
|
||||
|
||||
WCAG 2.1 AA Checks:
|
||||
1.4.3 Contrast (Minimum) — text vs background ratio >= 4.5:1
|
||||
1.4.6 Contrast (Enhanced) — ratio >= 7:1 for AAA
|
||||
1.4.11 Non-text Contrast — UI components >= 3:1
|
||||
1.3.1 Info and Relationships — heading hierarchy, landmarks
|
||||
1.1.1 Non-text Content — alt text on images
|
||||
2.4.1 Bypass Blocks — skip navigation link
|
||||
2.4.2 Page Titled — meaningful <title>
|
||||
2.4.6 Headings and Labels — descriptive headings
|
||||
4.1.2 Name, Role, Value — ARIA labels on interactive elements
|
||||
|
||||
Refs: timmy-config#492, WCAG 2.1 AA
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import colorsys
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from html.parser import HTMLParser
|
||||
from hermes_tools import browser_navigate, browser_vision
|
||||
|
||||
|
||||
# === Configuration ===
|
||||
|
||||
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")
|
||||
|
||||
DEFAULT_PAGES = ["/", "/about", "/donate", "/blog", "/contact"]
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
CRITICAL = "critical" # Blocks access entirely
|
||||
MAJOR = "major" # Significant barrier
|
||||
MINOR = "minor" # Inconvenience
|
||||
PASS = "pass"
|
||||
|
||||
|
||||
@dataclass
|
||||
class A11yViolation:
|
||||
"""A single accessibility violation."""
|
||||
criterion: str # WCAG criterion (e.g. "1.4.3")
|
||||
criterion_name: str # Human-readable name
|
||||
severity: Severity = Severity.MINOR
|
||||
element: str = "" # CSS selector or element description
|
||||
description: str = "" # What's wrong
|
||||
fix: str = "" # Suggested fix
|
||||
source: str = "" # "programmatic" or "vision"
|
||||
|
||||
|
||||
@dataclass
|
||||
class A11yPageResult:
|
||||
"""Audit result for a single page."""
|
||||
url: str = ""
|
||||
title: str = ""
|
||||
score: int = 100
|
||||
violations: list[A11yViolation] = field(default_factory=list)
|
||||
passed_checks: list[str] = field(default_factory=list)
|
||||
summary: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class A11yAuditReport:
|
||||
"""Complete audit report across all pages."""
|
||||
site: str = ""
|
||||
pages_audited: int = 0
|
||||
overall_score: int = 100
|
||||
total_violations: int = 0
|
||||
critical_violations: int = 0
|
||||
major_violations: int = 0
|
||||
page_results: list[A11yPageResult] = field(default_factory=list)
|
||||
summary: str = ""
|
||||
|
||||
|
||||
# === HTML Parser for Programmatic Checks ===
|
||||
|
||||
class A11yHTMLParser(HTMLParser):
|
||||
"""Extract accessibility-relevant elements from HTML."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title = ""
|
||||
self.images = [] # [{"src": ..., "alt": ...}]
|
||||
self.headings = [] # [{"level": int, "text": ...}]
|
||||
self.links = [] # [{"text": ..., "href": ...}]
|
||||
self.inputs = [] # [{"type": ..., "label": ..., "id": ...}]
|
||||
self.landmarks = [] # [{"tag": ..., "role": ...}]
|
||||
self.skip_nav = False
|
||||
self.lang = ""
|
||||
self.in_title = False
|
||||
self.in_heading = False
|
||||
self.heading_level = 0
|
||||
self.heading_text = ""
|
||||
self.current_text = ""
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attr_dict = dict(attrs)
|
||||
|
||||
if tag == "title":
|
||||
self.in_title = True
|
||||
elif tag == "html":
|
||||
self.lang = attr_dict.get("lang", "")
|
||||
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
||||
self.in_heading = True
|
||||
self.heading_level = int(tag[1])
|
||||
self.heading_text = ""
|
||||
elif tag == "img":
|
||||
self.images.append({
|
||||
"src": attr_dict.get("src", ""),
|
||||
"alt": attr_dict.get("alt"),
|
||||
"role": attr_dict.get("role", ""),
|
||||
})
|
||||
elif tag == "a":
|
||||
self.links.append({
|
||||
"href": attr_dict.get("href", ""),
|
||||
"text": "",
|
||||
"aria_label": attr_dict.get("aria-label", ""),
|
||||
})
|
||||
elif tag in ("input", "select", "textarea"):
|
||||
self.inputs.append({
|
||||
"tag": tag,
|
||||
"type": attr_dict.get("type", "text"),
|
||||
"id": attr_dict.get("id", ""),
|
||||
"aria_label": attr_dict.get("aria-label", ""),
|
||||
"aria_labelledby": attr_dict.get("aria-labelledby", ""),
|
||||
})
|
||||
elif tag in ("main", "nav", "header", "footer", "aside", "section", "form"):
|
||||
self.landmarks.append({"tag": tag, "role": attr_dict.get("role", "")})
|
||||
elif tag == "a" and ("skip" in attr_dict.get("href", "").lower() or
|
||||
"skip" in attr_dict.get("class", "").lower()):
|
||||
self.skip_nav = True
|
||||
|
||||
role = attr_dict.get("role", "")
|
||||
if role in ("navigation", "main", "banner", "contentinfo", "complementary", "search"):
|
||||
self.landmarks.append({"tag": tag, "role": role})
|
||||
if role == "link" and "skip" in (attr_dict.get("aria-label", "") + attr_dict.get("href", "")).lower():
|
||||
self.skip_nav = True
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "title":
|
||||
self.in_title = False
|
||||
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
||||
self.headings.append({"level": self.heading_level, "text": self.heading_text.strip()})
|
||||
self.in_heading = False
|
||||
elif tag == "a" and self.links:
|
||||
self.links[-1]["text"] = self.current_text.strip()
|
||||
self.current_text = ""
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_title:
|
||||
self.title += data
|
||||
if self.in_heading:
|
||||
self.heading_text += data
|
||||
self.current_text += data
|
||||
|
||||
|
||||
# === Color/Contrast Utilities ===
|
||||
|
||||
def parse_color(color_str: str) -> Optional[tuple]:
|
||||
"""Parse CSS color string to (r, g, b) tuple (0-255)."""
|
||||
if not color_str:
|
||||
return None
|
||||
|
||||
color_str = color_str.strip().lower()
|
||||
|
||||
# Named colors (subset)
|
||||
named = {
|
||||
"white": (255, 255, 255), "black": (0, 0, 0),
|
||||
"red": (255, 0, 0), "green": (0, 128, 0), "blue": (0, 0, 255),
|
||||
"gray": (128, 128, 128), "grey": (128, 128, 128),
|
||||
"silver": (192, 192, 192), "yellow": (255, 255, 0),
|
||||
"orange": (255, 165, 0), "purple": (128, 0, 128),
|
||||
"transparent": None,
|
||||
}
|
||||
if color_str in named:
|
||||
return named[color_str]
|
||||
|
||||
# #RRGGBB or #RGB
|
||||
if color_str.startswith("#"):
|
||||
hex_str = color_str[1:]
|
||||
if len(hex_str) == 3:
|
||||
hex_str = "".join(c * 2 for c in hex_str)
|
||||
if len(hex_str) == 6:
|
||||
try:
|
||||
return tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
# rgb(r, g, b)
|
||||
match = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)", color_str)
|
||||
if match:
|
||||
return tuple(int(match.group(i)) for i in (1, 2, 3))
|
||||
|
||||
# rgba(r, g, b, a)
|
||||
match = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)", color_str)
|
||||
if match:
|
||||
return tuple(int(match.group(i)) for i in (1, 2, 3))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def relative_luminance(rgb: tuple) -> float:
|
||||
"""Calculate relative luminance per WCAG 2.1 (sRGB)."""
|
||||
def linearize(c):
|
||||
c = c / 255.0
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
|
||||
r, g, b = [linearize(c) for c in rgb]
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
|
||||
def contrast_ratio(color1: tuple, color2: tuple) -> float:
|
||||
"""Calculate contrast ratio between two colors per WCAG 2.1."""
|
||||
l1 = relative_luminance(color1)
|
||||
l2 = relative_luminance(color2)
|
||||
lighter = max(l1, l2)
|
||||
darker = min(l1, l2)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
|
||||
|
||||
# === Programmatic Checks ===
|
||||
|
||||
def check_page_title(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 2.4.2 — Page Titled."""
|
||||
violations = []
|
||||
title = parser.title.strip()
|
||||
if not title:
|
||||
violations.append(A11yViolation(
|
||||
criterion="2.4.2", criterion_name="Page Titled",
|
||||
severity=Severity.MAJOR,
|
||||
element="<title>",
|
||||
description="Page has no title or title is empty.",
|
||||
fix="Add a meaningful <title> that describes the page purpose.",
|
||||
source="programmatic"
|
||||
))
|
||||
elif len(title) < 5:
|
||||
violations.append(A11yViolation(
|
||||
criterion="2.4.2", criterion_name="Page Titled",
|
||||
severity=Severity.MINOR,
|
||||
element=f"<title>{title}</title>",
|
||||
description=f"Page title is very short: '{title}'",
|
||||
fix="Use a more descriptive title.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def check_lang_attribute(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 3.1.1 — Language of Page."""
|
||||
violations = []
|
||||
if not parser.lang:
|
||||
violations.append(A11yViolation(
|
||||
criterion="3.1.1", criterion_name="Language of Page",
|
||||
severity=Severity.MAJOR,
|
||||
element="<html>",
|
||||
description="Missing lang attribute on <html> element.",
|
||||
fix="Add lang=\"en\" (or appropriate language code) to <html>.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def check_images_alt_text(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 1.1.1 — Non-text Content."""
|
||||
violations = []
|
||||
for img in parser.images:
|
||||
if img.get("role") == "presentation" or img.get("role") == "none":
|
||||
continue # Decorative images are exempt
|
||||
alt = img.get("alt")
|
||||
src = img.get("src", "unknown")
|
||||
if alt is None:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.1.1", criterion_name="Non-text Content",
|
||||
severity=Severity.CRITICAL,
|
||||
element=f"<img src=\"{src[:80]}\">",
|
||||
description="Image missing alt attribute.",
|
||||
fix="Add descriptive alt text, or alt=\"\" with role=\"presentation\" for decorative images.",
|
||||
source="programmatic"
|
||||
))
|
||||
elif alt.strip() == "":
|
||||
# Empty alt is OK only for decorative images
|
||||
if img.get("role") not in ("presentation", "none"):
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.1.1", criterion_name="Non-text Content",
|
||||
severity=Severity.MINOR,
|
||||
element=f"<img src=\"{src[:80]}\" alt=\"\">",
|
||||
description="Empty alt text — ensure this image is decorative.",
|
||||
fix="If decorative, add role=\"presentation\". If meaningful, add descriptive alt text.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def check_heading_hierarchy(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 1.3.1 — Info and Relationships (heading hierarchy)."""
|
||||
violations = []
|
||||
if not parser.headings:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MAJOR,
|
||||
element="document",
|
||||
description="No headings found on page.",
|
||||
fix="Add proper heading hierarchy starting with <h1>.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
# Check for H1
|
||||
h1s = [h for h in parser.headings if h["level"] == 1]
|
||||
if not h1s:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MAJOR,
|
||||
element="document",
|
||||
description="No <h1> heading found.",
|
||||
fix="Add a single <h1> as the main page heading.",
|
||||
source="programmatic"
|
||||
))
|
||||
elif len(h1s) > 1:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MINOR,
|
||||
element="document",
|
||||
description=f"Multiple <h1> headings found ({len(h1s)}).",
|
||||
fix="Use a single <h1> per page for the main heading.",
|
||||
source="programmatic"
|
||||
))
|
||||
|
||||
# Check hierarchy skips
|
||||
prev_level = 0
|
||||
for h in parser.headings:
|
||||
level = h["level"]
|
||||
if level > prev_level + 1 and prev_level > 0:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MINOR,
|
||||
element=f"<h{level}>{h['text'][:50]}</h{level}>",
|
||||
description=f"Heading level skipped: h{prev_level} → h{level}",
|
||||
fix=f"Use <h{prev_level + 1}> instead, or fill the gap.",
|
||||
source="programmatic"
|
||||
))
|
||||
prev_level = level
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def check_landmarks(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 1.3.1 — Landmarks and structure."""
|
||||
violations = []
|
||||
roles = {lm.get("role", "") for lm in parser.landmarks}
|
||||
tags = {lm.get("tag", "") for lm in parser.landmarks}
|
||||
|
||||
has_main = "main" in roles or "main" in tags
|
||||
has_nav = "navigation" in roles or "nav" in tags
|
||||
|
||||
if not has_main:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MAJOR,
|
||||
element="document",
|
||||
description="No <main> landmark found.",
|
||||
fix="Wrap the main content in a <main> element.",
|
||||
source="programmatic"
|
||||
))
|
||||
|
||||
if not has_nav:
|
||||
violations.append(A11yViolation(
|
||||
criterion="1.3.1", criterion_name="Info and Relationships",
|
||||
severity=Severity.MINOR,
|
||||
element="document",
|
||||
description="No <nav> landmark found.",
|
||||
fix="Wrap navigation in a <nav> element.",
|
||||
source="programmatic"
|
||||
))
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def check_skip_nav(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 2.4.1 — Bypass Blocks."""
|
||||
violations = []
|
||||
if not parser.skip_nav:
|
||||
# Also check links for "skip" text
|
||||
has_skip_link = any("skip" in l.get("text", "").lower() for l in parser.links)
|
||||
if not has_skip_link:
|
||||
violations.append(A11yViolation(
|
||||
criterion="2.4.1", criterion_name="Bypass Blocks",
|
||||
severity=Severity.MAJOR,
|
||||
element="document",
|
||||
description="No skip navigation link found.",
|
||||
fix="Add a 'Skip to main content' link as the first focusable element.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def check_form_labels(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 4.1.2 — Name, Role, Value (form inputs)."""
|
||||
violations = []
|
||||
for inp in parser.inputs:
|
||||
if inp["type"] in ("hidden", "submit", "button", "reset", "image"):
|
||||
continue
|
||||
has_label = bool(inp.get("aria_label") or inp.get("aria_labelledby") or inp.get("id"))
|
||||
if not has_label:
|
||||
violations.append(A11yViolation(
|
||||
criterion="4.1.2", criterion_name="Name, Role, Value",
|
||||
severity=Severity.MAJOR,
|
||||
element=f"<{inp['tag']} type=\"{inp['type']}\">",
|
||||
description="Form input has no associated label or aria-label.",
|
||||
fix="Add a <label for=\"...\"> or aria-label attribute.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def check_link_text(parser: A11yHTMLParser) -> list[A11yViolation]:
|
||||
"""WCAG 2.4.4 — Link Purpose."""
|
||||
violations = []
|
||||
for link in parser.links:
|
||||
text = (link.get("text", "") or link.get("aria_label", "")).strip().lower()
|
||||
href = link.get("href", "")
|
||||
if not text:
|
||||
violations.append(A11yViolation(
|
||||
criterion="2.4.4", criterion_name="Link Purpose",
|
||||
severity=Severity.MAJOR,
|
||||
element=f"<a href=\"{href[:60]}\">",
|
||||
description="Link has no accessible text.",
|
||||
fix="Add visible text content or aria-label to the link.",
|
||||
source="programmatic"
|
||||
))
|
||||
elif text in ("click here", "read more", "here", "more", "link"):
|
||||
violations.append(A11yViolation(
|
||||
criterion="2.4.4", criterion_name="Link Purpose",
|
||||
severity=Severity.MINOR,
|
||||
element=f"<a href=\"{href[:60]}\">{text}</a>",
|
||||
description=f"Non-descriptive link text: '{text}'",
|
||||
fix="Use descriptive text that explains the link destination.",
|
||||
source="programmatic"
|
||||
))
|
||||
return violations
|
||||
|
||||
|
||||
def run_programmatic_checks(html: str) -> list[A11yViolation]:
|
||||
"""Run all programmatic accessibility checks on HTML content."""
|
||||
parser = A11yHTMLParser()
|
||||
try:
|
||||
parser.feed(html)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
violations = []
|
||||
violations.extend(check_page_title(parser))
|
||||
violations.extend(check_lang_attribute(parser))
|
||||
violations.extend(check_images_alt_text(parser))
|
||||
violations.extend(check_heading_hierarchy(parser))
|
||||
violations.extend(check_landmarks(parser))
|
||||
violations.extend(check_skip_nav(parser))
|
||||
violations.extend(check_form_labels(parser))
|
||||
violations.extend(check_link_text(parser))
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
# === Vision Model Checks ===
|
||||
|
||||
A11Y_VISION_PROMPT = """You are a WCAG 2.1 AA accessibility auditor. Analyze this screenshot of a web page.
|
||||
|
||||
Check for these specific issues:
|
||||
|
||||
1. COLOR CONTRAST: Are text colors sufficiently different from their backgrounds?
|
||||
- Normal text needs 4.5:1 contrast ratio
|
||||
- Large text (18pt+) needs 3:1
|
||||
- UI components need 3:1
|
||||
List any text or UI elements where contrast looks insufficient.
|
||||
|
||||
2. FONT LEGIBILITY: Is text readable?
|
||||
- Font size >= 12px for body text
|
||||
- Line height >= 1.5 for body text
|
||||
- No text in images (should be real text)
|
||||
|
||||
3. LAYOUT ISSUES: Is the layout accessible?
|
||||
- Touch targets >= 44x44px
|
||||
- Content not cut off or overlapping
|
||||
- Logical reading order visible
|
||||
- No horizontal scrolling at standard widths
|
||||
|
||||
4. FOCUS INDICATORS: Can you see which element has focus?
|
||||
- Interactive elements should have visible focus rings
|
||||
|
||||
5. COLOR ALONE: Is information conveyed only by color?
|
||||
- Errors/warnings should not rely solely on red/green
|
||||
|
||||
Respond as JSON:
|
||||
{
|
||||
"violations": [
|
||||
{
|
||||
"criterion": "1.4.3",
|
||||
"criterion_name": "Contrast (Minimum)",
|
||||
"severity": "critical|major|minor",
|
||||
"element": "description of element",
|
||||
"description": "what's wrong",
|
||||
"fix": "how to fix"
|
||||
}
|
||||
],
|
||||
"passed_checks": ["list of things that look good"],
|
||||
"overall_score": 0-100,
|
||||
"summary": "brief summary"
|
||||
}"""
|
||||
|
||||
|
||||
def run_vision_check(screenshot_path: str, model: str = VISION_MODEL) -> list[A11yViolation]:
|
||||
"""Run vision model accessibility check on a screenshot."""
|
||||
try:
|
||||
b64 = base64.b64encode(Path(screenshot_path).read_bytes()).decode()
|
||||
payload = json.dumps({
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": [
|
||||
{"type": "text", "text": A11Y_VISION_PROMPT},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
|
||||
]}],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_BASE}/api/chat",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
result = json.loads(resp.read())
|
||||
content = result.get("message", {}).get("content", "")
|
||||
|
||||
# Parse response
|
||||
parsed = _parse_json_response(content)
|
||||
violations = []
|
||||
for v in parsed.get("violations", []):
|
||||
violations.append(A11yViolation(
|
||||
criterion=v.get("criterion", ""),
|
||||
criterion_name=v.get("criterion_name", ""),
|
||||
severity=Severity(v.get("severity", "minor")),
|
||||
element=v.get("element", ""),
|
||||
description=v.get("description", ""),
|
||||
fix=v.get("fix", ""),
|
||||
source="vision"
|
||||
))
|
||||
return violations
|
||||
|
||||
except Exception as e:
|
||||
print(f" Vision check failed: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def _parse_json_response(text: str) -> dict:
|
||||
"""Extract JSON from potentially messy vision response."""
|
||||
cleaned = text.strip()
|
||||
if cleaned.startswith("```"):
|
||||
lines = cleaned.split("\n")[1:]
|
||||
if lines and lines[-1].strip() == "```":
|
||||
lines = lines[:-1]
|
||||
cleaned = "\n".join(lines)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
start = cleaned.find("{")
|
||||
end = cleaned.rfind("}")
|
||||
if start >= 0 and end > start:
|
||||
try:
|
||||
return json.loads(cleaned[start:end + 1])
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
# === Page Fetching ===
|
||||
|
||||
def fetch_page(url: str) -> Optional[str]:
|
||||
"""Fetch HTML content of a page."""
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "A11yAudit/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.read().decode("utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
print(f" Failed to fetch {url}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def take_screenshot(url: str, output_path: str, width: int = 1280, height: int = 900) -> bool:
|
||||
"""Take a screenshot using Playwright or curl-based headless capture."""
|
||||
# Try Playwright first
|
||||
try:
|
||||
script = f"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page(viewport={{"width": {width}, "height": {height}}})
|
||||
page.goto("{url}", wait_until="networkidle", timeout=30000)
|
||||
page.screenshot(path="{output_path}", full_page=True)
|
||||
browser.close()
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["python3", "-c", script],
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode == 0 and Path(output_path).exists():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try curl + wkhtmltoimage
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["wkhtmltoimage", "--width", str(width), "--quality", "90", url, output_path],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0 and Path(output_path).exists():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# === Audit Logic ===
|
||||
|
||||
def audit_page(url: str, use_vision: bool = False, model: str = VISION_MODEL) -> A11yPageResult:
|
||||
"""Run a full accessibility audit on a single page."""
|
||||
result = A11yPageResult(url=url)
|
||||
|
||||
# Fetch HTML
|
||||
html = fetch_page(url)
|
||||
if not html:
|
||||
result.summary = f"Failed to fetch {url}"
|
||||
result.score = 0
|
||||
return result
|
||||
|
||||
# Extract title
|
||||
title_match = re.search(r"<title[^>]*>(.*?)</title>", html, re.IGNORECASE | re.DOTALL)
|
||||
result.title = title_match.group(1).strip() if title_match else ""
|
||||
|
||||
# Run programmatic checks
|
||||
prog_violations = run_programmatic_checks(html)
|
||||
result.violations.extend(prog_violations)
|
||||
|
||||
# Track passed checks
|
||||
criteria_checked = {
|
||||
"2.4.2": "Page Titled",
|
||||
"3.1.1": "Language of Page",
|
||||
"1.1.1": "Non-text Content",
|
||||
"1.3.1": "Info and Relationships",
|
||||
"2.4.1": "Bypass Blocks",
|
||||
"4.1.2": "Name, Role, Value",
|
||||
"2.4.4": "Link Purpose",
|
||||
}
|
||||
violated_criteria = {v.criterion for v in result.violations}
|
||||
for criterion, name in criteria_checked.items():
|
||||
if criterion not in violated_criteria:
|
||||
result.passed_checks.append(f"{criterion} {name}")
|
||||
|
||||
# Vision check (optional)
|
||||
if use_vision:
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
screenshot_path = tmp.name
|
||||
try:
|
||||
print(f" Taking screenshot of {url}...", file=sys.stderr)
|
||||
if take_screenshot(url, screenshot_path):
|
||||
print(f" Running vision analysis...", file=sys.stderr)
|
||||
vision_violations = run_vision_check(screenshot_path, model)
|
||||
result.violations.extend(vision_violations)
|
||||
result.passed_checks.append("Vision model analysis completed")
|
||||
else:
|
||||
result.passed_checks.append("Screenshot unavailable — vision check skipped")
|
||||
finally:
|
||||
Path(screenshot_path).unlink(missing_ok=True)
|
||||
|
||||
# Calculate score
|
||||
criticals = sum(1 for v in result.violations if v.severity == Severity.CRITICAL)
|
||||
majors = sum(1 for v in result.violations if v.severity == Severity.MAJOR)
|
||||
minors = sum(1 for v in result.violations if v.severity == Severity.MINOR)
|
||||
result.score = max(0, 100 - (criticals * 25) - (majors * 10) - (minors * 3))
|
||||
|
||||
# Summary
|
||||
if not result.violations:
|
||||
result.summary = f"All programmatic checks passed for {url}"
|
||||
else:
|
||||
result.summary = (
|
||||
f"{len(result.violations)} issue(s) found: "
|
||||
f"{criticals} critical, {majors} major, {minors} minor"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def audit_site(base_url: str, pages: list[str], use_vision: bool = False,
|
||||
model: str = VISION_MODEL) -> A11yAuditReport:
|
||||
"""Audit multiple pages of a site."""
|
||||
report = A11yAuditReport(site=base_url)
|
||||
|
||||
for path in pages:
|
||||
url = base_url.rstrip("/") + path if not path.startswith("http") else path
|
||||
print(f"Auditing: {url}", file=sys.stderr)
|
||||
result = audit_page(url, use_vision, model)
|
||||
report.page_results.append(result)
|
||||
|
||||
report.pages_audited = len(report.page_results)
|
||||
report.total_violations = sum(len(p.violations) for p in report.page_results)
|
||||
report.critical_violations = sum(
|
||||
sum(1 for v in p.violations if v.severity == Severity.CRITICAL)
|
||||
for p in report.page_results
|
||||
)
|
||||
report.major_violations = sum(
|
||||
sum(1 for v in p.violations if v.severity == Severity.MAJOR)
|
||||
for p in report.page_results
|
||||
def audit_accessibility():
|
||||
browser_navigate(url="https://timmyfoundation.org")
|
||||
analysis = browser_vision(
|
||||
question="Perform an accessibility audit. Check for: 1) Color contrast, 2) Font legibility, 3) Missing alt text for images. Provide a report with FAIL/PASS."
|
||||
)
|
||||
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "analysis": analysis}
|
||||
|
||||
if report.page_results:
|
||||
report.overall_score = sum(p.score for p in report.page_results) // len(report.page_results)
|
||||
|
||||
report.summary = (
|
||||
f"Audited {report.pages_audited} pages. "
|
||||
f"Overall score: {report.overall_score}/100. "
|
||||
f"{report.total_violations} total issues: "
|
||||
f"{report.critical_violations} critical, {report.major_violations} major."
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# === Output Formatting ===
|
||||
|
||||
def format_report(report: A11yAuditReport, fmt: str = "json") -> str:
|
||||
"""Format the audit report."""
|
||||
if fmt == "json":
|
||||
data = {
|
||||
"site": report.site,
|
||||
"pages_audited": report.pages_audited,
|
||||
"overall_score": report.overall_score,
|
||||
"total_violations": report.total_violations,
|
||||
"critical_violations": report.critical_violations,
|
||||
"major_violations": report.major_violations,
|
||||
"summary": report.summary,
|
||||
"pages": []
|
||||
}
|
||||
for page in report.page_results:
|
||||
page_data = {
|
||||
"url": page.url,
|
||||
"title": page.title,
|
||||
"score": page.score,
|
||||
"violations": [asdict(v) for v in page.violations],
|
||||
"passed_checks": page.passed_checks,
|
||||
"summary": page.summary,
|
||||
}
|
||||
# Convert severity enum to string
|
||||
for v in page_data["violations"]:
|
||||
if hasattr(v["severity"], "value"):
|
||||
v["severity"] = v["severity"].value
|
||||
data["pages"].append(page_data)
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
elif fmt == "text":
|
||||
lines = []
|
||||
lines.append("=" * 60)
|
||||
lines.append(" WEB ACCESSIBILITY AUDIT REPORT")
|
||||
lines.append("=" * 60)
|
||||
lines.append(f" Site: {report.site}")
|
||||
lines.append(f" Pages audited: {report.pages_audited}")
|
||||
lines.append(f" Overall score: {report.overall_score}/100")
|
||||
lines.append(f" Issues: {report.total_violations} total "
|
||||
f"({report.critical_violations} critical, {report.major_violations} major)")
|
||||
lines.append("")
|
||||
|
||||
for page in report.page_results:
|
||||
lines.append(f" ── {page.url} ──")
|
||||
lines.append(f" Title: {page.title}")
|
||||
lines.append(f" Score: {page.score}/100")
|
||||
lines.append("")
|
||||
|
||||
if page.violations:
|
||||
lines.append(f" Violations ({len(page.violations)}):")
|
||||
for v in page.violations:
|
||||
sev_icon = {"critical": "🔴", "major": "🟡", "minor": "🔵"}.get(
|
||||
v.severity.value if hasattr(v.severity, "value") else str(v.severity), "⚪"
|
||||
)
|
||||
lines.append(f" {sev_icon} [{v.criterion}] {v.criterion_name}")
|
||||
lines.append(f" Element: {v.element}")
|
||||
lines.append(f" Issue: {v.description}")
|
||||
lines.append(f" Fix: {v.fix}")
|
||||
lines.append(f" Source: {v.source}")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(" ✓ No violations found")
|
||||
lines.append("")
|
||||
|
||||
if page.passed_checks:
|
||||
lines.append(f" Passed: {', '.join(page.passed_checks)}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 60)
|
||||
lines.append(f" Summary: {report.summary}")
|
||||
lines.append("=" * 60)
|
||||
return "\n".join(lines)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown format: {fmt}")
|
||||
|
||||
|
||||
# === CLI ===
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Visual Accessibility Audit — WCAG 2.1 AA compliance checker",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s --url https://timmyfoundation.org
|
||||
%(prog)s --url https://timmyfoundation.org --pages /about /donate
|
||||
%(prog)s --url https://timmyfoundation.org --vision
|
||||
%(prog)s --url https://timmyfoundation.org --format text
|
||||
"""
|
||||
)
|
||||
parser.add_argument("--url", required=True, help="Base URL to audit")
|
||||
parser.add_argument("--pages", nargs="*", default=DEFAULT_PAGES,
|
||||
help="Paths to audit (default: / /about /donate /blog /contact)")
|
||||
parser.add_argument("--vision", action="store_true",
|
||||
help="Include vision model analysis (requires Ollama)")
|
||||
parser.add_argument("--model", default=VISION_MODEL,
|
||||
help=f"Vision model (default: {VISION_MODEL})")
|
||||
parser.add_argument("--format", choices=["json", "text"], default="json",
|
||||
help="Output format")
|
||||
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
report = audit_site(args.url, args.pages, use_vision=args.vision, model=args.model)
|
||||
output = format_report(report, args.format)
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(output)
|
||||
print(f"Report written to {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(output)
|
||||
|
||||
# Exit code: non-zero if critical violations
|
||||
if report.critical_violations > 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
print(json.dumps(audit_accessibility(), indent=2))
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os
|
||||
|
||||
songs = [
|
||||
{"t":"Thunder Road","a":"Heartland","m":["hope","anticipation","energy","triumph","nostalgia","urgency","passion","defiance","release","catharsis"]},
|
||||
{"t":"Black Dog Howl","a":"Rust & Wire","m":["despair","anger","frenzy","exhaustion","resignation","grief","numbness","rage","acceptance","silence"]},
|
||||
{"t":"Satellite Hearts","a":"Neon Circuit","m":["wonder","isolation","longing","connection","euphoria","confusion","clarity","tenderness","urgency","bittersweet"]},
|
||||
{"t":"Concrete Garden","a":"Streetlight Prophet","m":["oppression","resilience","anger","beauty","defiance","community","joy","struggle","growth","hope"]},
|
||||
{"t":"Gravity Well","a":"Void Walker","m":["dread","fascination","surrender","awe","terror","peace","disorientation","acceptance","transcendence","emptiness"]},
|
||||
{"t":"Rust Belt Lullaby","a":"Iron & Ember","m":["nostalgia","sadness","tenderness","loss","beauty","resignation","love","weariness","quiet hope","peace"]},
|
||||
{"t":"Wildfire Sermon","a":"Prophet Ash","m":["fury","ecstasy","chaos","joy","destruction","creation","warning","invitation","abandon","rebirth"]},
|
||||
{"t":"Midnight Transmission","a":"Frequency Ghost","m":["mystery","loneliness","curiosity","connection","paranoia","intimacy","urgency","disconnection","searching","haunting"]},
|
||||
{"t":"Crown of Thorns","a":"Velvet Guillotine","m":["seduction","power","cruelty","beauty","danger","vulnerability","fury","grace","revenge","mercy"]},
|
||||
{"t":"Apartment 4B","a":"Wallpaper & Wire","m":["claustrophobia","routine","desperation","fantasy","breakthrough","freedom","fear","joy","grounding","home"]},
|
||||
]
|
||||
|
||||
beats = []
|
||||
for s in songs:
|
||||
for i in range(10):
|
||||
beats.append({"song": s["t"], "artist": s["a"], "beat": i+1,
|
||||
"timestamp": f"{i*30//60}:{(i*30)%60:02d}", "duration": "30s",
|
||||
"lyric_line": f"[Beat {i+1}]", "scene": {"mood": s["m"][i], "colors": ["placeholder"],
|
||||
"composition": ["wide","close","OTS","low","high","dutch","symmetric","thirds","xwide","medium"][i],
|
||||
"camera": ["static","pan","dolly-in","dolly-out","handheld","steadicam","zoom","crane","track","tilt"][i],
|
||||
"description": f"[{s['m'][i]} scene]"}})
|
||||
|
||||
out = os.path.expanduser("~/.hermes/training-data/scene-descriptions-rock.jsonl")
|
||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||
with open(out, "w") as f:
|
||||
for b in beats:
|
||||
f.write(json.dumps(b) + "\n")
|
||||
print(f"Generated {len(beats)} beats")
|
||||
@@ -1,599 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
matrix_glitch_detect.py — 3D World Visual Artifact Detection for The Matrix.
|
||||
|
||||
Scans screenshots or live pages for visual glitches: floating assets, z-fighting,
|
||||
texture pop-in, clipping, broken meshes, lighting artifacts. Outputs structured
|
||||
JSON, text, or standalone HTML report with annotated screenshots.
|
||||
|
||||
Usage:
|
||||
# Scan a screenshot
|
||||
python scripts/matrix_glitch_detect.py --image screenshot.png
|
||||
|
||||
# Scan with vision model
|
||||
python scripts/matrix_glitch_detect.py --image screenshot.png --vision
|
||||
|
||||
# HTML report
|
||||
python scripts/matrix_glitch_detect.py --image screenshot.png --html report.html
|
||||
|
||||
# Scan live Matrix page
|
||||
python scripts/matrix_glitch_detect.py --url https://matrix.alexanderwhitestone.com
|
||||
|
||||
# Batch scan a directory
|
||||
python scripts/matrix_glitch_detect.py --batch ./screenshots/ --html batch-report.html
|
||||
|
||||
Refs: timmy-config#491, #541, #543, #544
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import html as html_module
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from hermes_tools import browser_navigate, browser_vision
|
||||
|
||||
|
||||
# === Configuration ===
|
||||
|
||||
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
CRITICAL = "critical"
|
||||
MAJOR = "major"
|
||||
MINOR = "minor"
|
||||
COSMETIC = "cosmetic"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Glitch:
|
||||
"""A single detected visual artifact."""
|
||||
type: str = "" # floating_asset, z_fighting, texture_pop, clipping, lighting, mesh_break
|
||||
severity: Severity = Severity.MINOR
|
||||
region: str = "" # "upper-left", "center", "bottom-right", or coordinates
|
||||
description: str = ""
|
||||
confidence: float = 0.0 # 0.0-1.0
|
||||
source: str = "" # "programmatic", "vision", "pixel_analysis"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlitchReport:
|
||||
"""Complete glitch detection report."""
|
||||
source: str = "" # file path or URL
|
||||
timestamp: str = ""
|
||||
status: str = "PASS" # PASS, WARN, FAIL
|
||||
score: int = 100
|
||||
glitches: list[Glitch] = field(default_factory=list)
|
||||
summary: str = ""
|
||||
model_used: str = ""
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
|
||||
|
||||
# === Programmatic Analysis ===
|
||||
|
||||
def analyze_pixels(image_path: str) -> list[Glitch]:
|
||||
"""Programmatic pixel analysis for common 3D glitches."""
|
||||
glitches = []
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
img = Image.open(image_path).convert("RGB")
|
||||
w, h = img.size
|
||||
pixels = img.load()
|
||||
|
||||
# Check for solid-color regions (render failure)
|
||||
corner_colors = [
|
||||
pixels[0, 0], pixels[w-1, 0], pixels[0, h-1], pixels[w-1, h-1]
|
||||
]
|
||||
if all(c == corner_colors[0] for c in corner_colors):
|
||||
# All corners same color — check if it's black (render failure)
|
||||
if corner_colors[0] == (0, 0, 0):
|
||||
glitches.append(Glitch(
|
||||
type="render_failure",
|
||||
severity=Severity.CRITICAL,
|
||||
region="entire frame",
|
||||
description="Entire frame is black — 3D scene failed to render",
|
||||
confidence=0.9,
|
||||
source="pixel_analysis"
|
||||
))
|
||||
|
||||
# Check for horizontal tearing lines
|
||||
tear_count = 0
|
||||
for y in range(0, h, max(1, h // 20)):
|
||||
row_start = pixels[0, y]
|
||||
same_count = sum(1 for x in range(w) if pixels[x, y] == row_start)
|
||||
if same_count > w * 0.95:
|
||||
tear_count += 1
|
||||
if tear_count > 3:
|
||||
glitches.append(Glitch(
|
||||
type="horizontal_tear",
|
||||
severity=Severity.MAJOR,
|
||||
region=f"{tear_count} lines",
|
||||
description=f"Horizontal tearing detected — {tear_count} mostly-solid scanlines",
|
||||
confidence=0.7,
|
||||
source="pixel_analysis"
|
||||
))
|
||||
|
||||
# Check for extreme brightness variance (lighting artifacts)
|
||||
import statistics
|
||||
brightness_samples = []
|
||||
for y in range(0, h, max(1, h // 50)):
|
||||
for x in range(0, w, max(1, w // 50)):
|
||||
r, g, b = pixels[x, y]
|
||||
brightness_samples.append(0.299 * r + 0.587 * g + 0.114 * b)
|
||||
if brightness_samples:
|
||||
stdev = statistics.stdev(brightness_samples)
|
||||
if stdev > 100:
|
||||
glitches.append(Glitch(
|
||||
type="lighting",
|
||||
severity=Severity.MINOR,
|
||||
region="global",
|
||||
description=f"Extreme brightness variance (stdev={stdev:.0f}) — possible lighting artifacts",
|
||||
confidence=0.5,
|
||||
source="pixel_analysis"
|
||||
))
|
||||
|
||||
except ImportError:
|
||||
pass # PIL not available
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return glitches
|
||||
|
||||
|
||||
# === Vision Analysis ===
|
||||
|
||||
GLITCH_VISION_PROMPT = """You are a 3D world QA engineer. Analyze this screenshot from a Three.js 3D world (The Matrix) for visual glitches and artifacts.
|
||||
|
||||
Look for these specific issues:
|
||||
|
||||
1. FLOATING ASSETS: Objects hovering above surfaces where they should rest. Look for shadows detached from objects.
|
||||
|
||||
2. Z-FIGHTING: Flickering or shimmering surfaces where two polygons overlap at the same depth. Usually appears as striped or dithered patterns.
|
||||
|
||||
3. TEXTURE POP-IN: Low-resolution textures that haven't loaded, or textures that suddenly change quality between frames.
|
||||
|
||||
4. CLIPPING: Objects passing through walls, floors, or other objects. Characters partially inside geometry.
|
||||
|
||||
5. LIGHTING ARTIFACTS: Hard light seams, black patches, overexposed areas, lights not illuminating correctly.
|
||||
|
||||
6. MESH BREAKS: Visible seams in geometry, missing faces on 3D objects, holes in surfaces.
|
||||
|
||||
7. RENDER FAILURE: Black areas where geometry should be, missing skybox, incomplete frame rendering.
|
||||
|
||||
8. UI OVERLAP: UI elements overlapping 3D viewport incorrectly.
|
||||
|
||||
Respond as JSON:
|
||||
{
|
||||
"glitches": [
|
||||
{
|
||||
"type": "floating_asset|z_fighting|texture_pop|clipping|lighting|mesh_break|render_failure|ui_overlap",
|
||||
"severity": "critical|major|minor|cosmetic",
|
||||
"region": "description of where",
|
||||
"description": "detailed description of the artifact",
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
],
|
||||
"overall_quality": 0-100,
|
||||
"summary": "brief assessment"
|
||||
}"""
|
||||
|
||||
|
||||
def run_vision_analysis(image_path: str, model: str = VISION_MODEL) -> tuple[list[Glitch], int]:
|
||||
"""Run vision model glitch analysis."""
|
||||
try:
|
||||
b64 = base64.b64encode(Path(image_path).read_bytes()).decode()
|
||||
payload = json.dumps({
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": [
|
||||
{"type": "text", "text": GLITCH_VISION_PROMPT},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
|
||||
]}],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_BASE}/api/chat",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
result = json.loads(resp.read())
|
||||
content = result.get("message", {}).get("content", "")
|
||||
|
||||
parsed = _parse_json_response(content)
|
||||
glitches = []
|
||||
for g in parsed.get("glitches", []):
|
||||
glitches.append(Glitch(
|
||||
type=g.get("type", "unknown"),
|
||||
severity=Severity(g.get("severity", "minor")),
|
||||
region=g.get("region", ""),
|
||||
description=g.get("description", ""),
|
||||
confidence=float(g.get("confidence", 0.5)),
|
||||
source="vision"
|
||||
))
|
||||
return glitches, parsed.get("overall_quality", 80)
|
||||
|
||||
except Exception as e:
|
||||
print(f" Vision analysis failed: {e}", file=sys.stderr)
|
||||
return [], 50
|
||||
|
||||
|
||||
def _parse_json_response(text: str) -> dict:
|
||||
cleaned = text.strip()
|
||||
if cleaned.startswith("```"):
|
||||
lines = cleaned.split("\n")[1:]
|
||||
if lines and lines[-1].strip() == "```":
|
||||
lines = lines[:-1]
|
||||
cleaned = "\n".join(lines)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
start = cleaned.find("{")
|
||||
end = cleaned.rfind("}")
|
||||
if start >= 0 and end > start:
|
||||
try:
|
||||
return json.loads(cleaned[start:end + 1])
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
# === Screenshot Capture ===
|
||||
|
||||
def capture_screenshot(url: str, output_path: str) -> bool:
|
||||
"""Take a screenshot of a URL."""
|
||||
try:
|
||||
script = f"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page(viewport={{"width": 1280, "height": 720}})
|
||||
page.goto("{url}", wait_until="networkidle", timeout=30000)
|
||||
page.wait_for_timeout(3000)
|
||||
page.screenshot(path="{output_path}")
|
||||
browser.close()
|
||||
"""
|
||||
result = subprocess.run(["python3", "-c", script], capture_output=True, text=True, timeout=60)
|
||||
return result.returncode == 0 and Path(output_path).exists()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# === Detection Logic ===
|
||||
|
||||
def detect_glitches(image_path: str, use_vision: bool = False,
|
||||
model: str = VISION_MODEL) -> GlitchReport:
|
||||
"""Run full glitch detection on an image."""
|
||||
report = GlitchReport(
|
||||
source=image_path,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
model_used=model if use_vision else "none"
|
||||
def detect_glitches():
|
||||
browser_navigate(url="https://matrix.alexanderwhitestone.com")
|
||||
analysis = browser_vision(
|
||||
question="Scan the 3D world for visual artifacts, floating assets, or z-fighting. List all coordinates/descriptions of glitches found. Provide a PASS/FAIL."
|
||||
)
|
||||
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "analysis": analysis}
|
||||
|
||||
if not Path(image_path).exists():
|
||||
report.status = "FAIL"
|
||||
report.summary = f"File not found: {image_path}"
|
||||
report.score = 0
|
||||
return report
|
||||
|
||||
# Get image dimensions
|
||||
try:
|
||||
from PIL import Image
|
||||
img = Image.open(image_path)
|
||||
report.width, report.height = img.size
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Programmatic analysis
|
||||
prog_glitches = analyze_pixels(image_path)
|
||||
report.glitches.extend(prog_glitches)
|
||||
|
||||
# Vision analysis
|
||||
if use_vision:
|
||||
print(f" Running vision analysis on {image_path}...", file=sys.stderr)
|
||||
vision_glitches, quality = run_vision_analysis(image_path, model)
|
||||
report.glitches.extend(vision_glitches)
|
||||
report.score = quality
|
||||
else:
|
||||
# Score based on programmatic results
|
||||
criticals = sum(1 for g in report.glitches if g.severity == Severity.CRITICAL)
|
||||
majors = sum(1 for g in report.glitches if g.severity == Severity.MAJOR)
|
||||
report.score = max(0, 100 - criticals * 40 - majors * 15)
|
||||
|
||||
# Determine status
|
||||
criticals = sum(1 for g in report.glitches if g.severity == Severity.CRITICAL)
|
||||
majors = sum(1 for g in report.glitches if g.severity == Severity.MAJOR)
|
||||
|
||||
if criticals > 0:
|
||||
report.status = "FAIL"
|
||||
elif majors > 0 or report.score < 70:
|
||||
report.status = "WARN"
|
||||
else:
|
||||
report.status = "PASS"
|
||||
|
||||
report.summary = (
|
||||
f"{report.status}: {len(report.glitches)} glitch(es) found "
|
||||
f"({criticals} critical, {majors} major), score {report.score}/100"
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# === HTML Report Generator ===
|
||||
|
||||
def generate_html_report(reports: list[GlitchReport], title: str = "Glitch Detection Report") -> str:
|
||||
"""Generate a standalone HTML report with annotated details."""
|
||||
total_glitches = sum(len(r.glitches) for r in reports)
|
||||
total_criticals = sum(sum(1 for g in r.glitches if g.severity == Severity.CRITICAL) for r in reports)
|
||||
avg_score = sum(r.score for r in reports) // max(1, len(reports))
|
||||
|
||||
if total_criticals > 0:
|
||||
overall_verdict = "FAIL"
|
||||
verdict_color = "#f44336"
|
||||
elif any(r.status == "WARN" for r in reports):
|
||||
overall_verdict = "WARN"
|
||||
verdict_color = "#ff9800"
|
||||
else:
|
||||
overall_verdict = "PASS"
|
||||
verdict_color = "#4caf50"
|
||||
|
||||
# Build HTML
|
||||
parts = []
|
||||
parts.append(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{html_module.escape(title)}</title>
|
||||
<style>
|
||||
*{{margin:0;padding:0;box-sizing:border-box}}
|
||||
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,monospace;background:#0a0a14;color:#c0c0d0;font-size:13px;line-height:1.5}}
|
||||
.container{{max-width:1000px;margin:0 auto;padding:20px}}
|
||||
header{{text-align:center;padding:24px 0;border-bottom:1px solid #1a1a2e;margin-bottom:24px}}
|
||||
header h1{{font-size:20px;font-weight:300;letter-spacing:3px;color:#4a9eff;margin-bottom:8px}}
|
||||
.verdict{{display:inline-block;padding:6px 20px;border-radius:4px;font-size:14px;font-weight:700;letter-spacing:2px;color:#fff;background:{verdict_color}}}
|
||||
.stats{{display:flex;gap:16px;justify-content:center;margin:16px 0;flex-wrap:wrap}}
|
||||
.stat{{background:#0e0e1a;border:1px solid #1a1a2e;border-radius:4px;padding:8px 16px;text-align:center}}
|
||||
.stat .val{{font-size:20px;font-weight:700;color:#4a9eff}}
|
||||
.stat .lbl{{font-size:9px;color:#666;text-transform:uppercase;letter-spacing:1px}}
|
||||
.score-gauge{{width:120px;height:120px;margin:0 auto 16px;position:relative}}
|
||||
.score-gauge svg{{transform:rotate(-90deg)}}
|
||||
.score-gauge .score-text{{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:28px;font-weight:700}}
|
||||
.report-card{{background:#0e0e1a;border:1px solid #1a1a2e;border-radius:6px;margin-bottom:16px;overflow:hidden}}
|
||||
.report-header{{padding:12px 16px;border-bottom:1px solid #1a1a2e;display:flex;justify-content:space-between;align-items:center}}
|
||||
.report-header .source{{color:#4a9eff;font-weight:600;word-break:break-all}}
|
||||
.report-header .status-badge{{padding:2px 10px;border-radius:3px;font-size:11px;font-weight:700;color:#fff}}
|
||||
.status-pass{{background:#4caf50}}
|
||||
.status-warn{{background:#ff9800}}
|
||||
.status-fail{{background:#f44336}}
|
||||
.screenshot{{text-align:center;padding:12px;background:#080810}}
|
||||
.screenshot img{{max-width:100%;max-height:400px;border:1px solid #1a1a2e;border-radius:4px}}
|
||||
.glitch-list{{padding:12px 16px}}
|
||||
.glitch-item{{padding:8px 0;border-bottom:1px solid #111;display:flex;gap:12px;align-items:flex-start}}
|
||||
.glitch-item:last-child{{border-bottom:none}}
|
||||
.severity-dot{{width:8px;height:8px;border-radius:50%;margin-top:5px;flex-shrink:0}}
|
||||
.sev-critical{{background:#f44336}}
|
||||
.sev-major{{background:#ff9800}}
|
||||
.sev-minor{{background:#2196f3}}
|
||||
.sev-cosmetic{{background:#666}}
|
||||
.glitch-detail{{flex:1}}
|
||||
.glitch-type{{color:#ffd700;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:1px}}
|
||||
.glitch-desc{{color:#aaa;font-size:12px;margin-top:2px}}
|
||||
.glitch-meta{{color:#555;font-size:10px;margin-top:2px}}
|
||||
.no-glitches{{color:#4caf50;text-align:center;padding:20px;font-style:italic}}
|
||||
footer{{text-align:center;padding:16px;color:#444;font-size:10px;border-top:1px solid #1a1a2e;margin-top:24px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>{html_module.escape(title)}</h1>
|
||||
<div class="verdict">{overall_verdict}</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="val">{len(reports)}</div><div class="lbl">Screenshots</div></div>
|
||||
<div class="stat"><div class="val">{total_glitches}</div><div class="lbl">Glitches</div></div>
|
||||
<div class="stat"><div class="val">{total_criticals}</div><div class="lbl">Critical</div></div>
|
||||
<div class="stat"><div class="val">{avg_score}</div><div class="lbl">Avg Score</div></div>
|
||||
</div>
|
||||
</header>
|
||||
""")
|
||||
|
||||
# Score gauge
|
||||
score_color = "#4caf50" if avg_score >= 80 else "#ff9800" if avg_score >= 60 else "#f44336"
|
||||
circumference = 2 * 3.14159 * 50
|
||||
dash_offset = circumference * (1 - avg_score / 100)
|
||||
parts.append(f"""
|
||||
<div class="score-gauge">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120">
|
||||
<circle cx="60" cy="60" r="50" fill="none" stroke="#1a1a2e" stroke-width="8"/>
|
||||
<circle cx="60" cy="60" r="50" fill="none" stroke="{score_color}" stroke-width="8"
|
||||
stroke-dasharray="{circumference}" stroke-dashoffset="{dash_offset}" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="score-text" style="color:{score_color}">{avg_score}</div>
|
||||
</div>
|
||||
""")
|
||||
|
||||
# Per-screenshot reports
|
||||
for i, report in enumerate(reports):
|
||||
status_class = f"status-{report.status.lower()}"
|
||||
source_name = Path(report.source).name if report.source else f"Screenshot {i+1}"
|
||||
|
||||
# Inline screenshot as base64
|
||||
img_tag = ""
|
||||
if report.source and Path(report.source).exists():
|
||||
try:
|
||||
b64 = base64.b64encode(Path(report.source).read_bytes()).decode()
|
||||
ext = Path(report.source).suffix.lower()
|
||||
mime = "image/png" if ext == ".png" else "image/jpeg" if ext in (".jpg", ".jpeg") else "image/webp"
|
||||
img_tag = f'<img src="data:{mime};base64,{b64}" alt="Screenshot">'
|
||||
except Exception:
|
||||
img_tag = '<div style="color:#666;padding:40px">Screenshot unavailable</div>'
|
||||
else:
|
||||
img_tag = '<div style="color:#666;padding:40px">No screenshot</div>'
|
||||
|
||||
parts.append(f"""
|
||||
<div class="report-card">
|
||||
<div class="report-header">
|
||||
<span class="source">{html_module.escape(source_name)} ({report.width}x{report.height})</span>
|
||||
<span class="status-badge {status_class}">{report.status} — {report.score}/100</span>
|
||||
</div>
|
||||
<div class="screenshot">{img_tag}</div>
|
||||
""")
|
||||
|
||||
if report.glitches:
|
||||
parts.append('<div class="glitch-list">')
|
||||
for g in sorted(report.glitches, key=lambda x: {"critical": 0, "major": 1, "minor": 2, "cosmetic": 3}.get(x.severity.value if hasattr(x.severity, "value") else str(x.severity), 4)):
|
||||
sev = g.severity.value if hasattr(g.severity, "value") else str(g.severity)
|
||||
sev_class = f"sev-{sev}"
|
||||
parts.append(f"""
|
||||
<div class="glitch-item">
|
||||
<div class="severity-dot {sev_class}"></div>
|
||||
<div class="glitch-detail">
|
||||
<div class="glitch-type">{html_module.escape(g.type)} — {sev.upper()}</div>
|
||||
<div class="glitch-desc">{html_module.escape(g.description)}</div>
|
||||
<div class="glitch-meta">Region: {html_module.escape(g.region)} | Confidence: {g.confidence:.0%} | Source: {html_module.escape(g.source)}</div>
|
||||
</div>
|
||||
</div>""")
|
||||
parts.append('</div>')
|
||||
else:
|
||||
parts.append('<div class="no-glitches">No glitches detected</div>')
|
||||
|
||||
parts.append('</div><!-- /report-card -->')
|
||||
|
||||
# Footer
|
||||
parts.append(f"""
|
||||
<footer>
|
||||
Generated {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | matrix_glitch_detect.py | timmy-config#544
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>""")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# === Output Formatting ===
|
||||
|
||||
def format_report(report: GlitchReport, fmt: str = "json") -> str:
|
||||
if fmt == "json":
|
||||
data = {
|
||||
"source": report.source,
|
||||
"timestamp": report.timestamp,
|
||||
"status": report.status,
|
||||
"score": report.score,
|
||||
"glitches": [asdict(g) for g in report.glitches],
|
||||
"summary": report.summary,
|
||||
"model_used": report.model_used,
|
||||
}
|
||||
for g in data["glitches"]:
|
||||
if hasattr(g["severity"], "value"):
|
||||
g["severity"] = g["severity"].value
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
elif fmt == "text":
|
||||
lines = [
|
||||
"=" * 50,
|
||||
" GLITCH DETECTION REPORT",
|
||||
"=" * 50,
|
||||
f" Source: {report.source}",
|
||||
f" Status: {report.status}",
|
||||
f" Score: {report.score}/100",
|
||||
f" Glitches: {len(report.glitches)}",
|
||||
"",
|
||||
]
|
||||
icons = {"critical": "🔴", "major": "🟡", "minor": "🔵", "cosmetic": "⚪"}
|
||||
for g in report.glitches:
|
||||
sev = g.severity.value if hasattr(g.severity, "value") else str(g.severity)
|
||||
icon = icons.get(sev, "?")
|
||||
lines.append(f" {icon} [{g.type}] {sev.upper()}: {g.description}")
|
||||
lines.append(f" Region: {g.region} | Confidence: {g.confidence:.0%}")
|
||||
lines.append("")
|
||||
lines.append(f" {report.summary}")
|
||||
lines.append("=" * 50)
|
||||
return "\n".join(lines)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
# === CLI ===
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="3D World Glitch Detection — visual artifact scanner for The Matrix"
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--image", help="Screenshot file to analyze")
|
||||
group.add_argument("--url", help="URL to screenshot and analyze")
|
||||
group.add_argument("--batch", help="Directory of screenshots to analyze")
|
||||
|
||||
parser.add_argument("--vision", action="store_true", help="Include vision model analysis")
|
||||
parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
|
||||
parser.add_argument("--html", help="Generate HTML report at this path")
|
||||
parser.add_argument("--format", choices=["json", "text"], default="json", help="Output format")
|
||||
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
reports = []
|
||||
|
||||
if args.image:
|
||||
print(f"Analyzing {args.image}...", file=sys.stderr)
|
||||
report = detect_glitches(args.image, args.vision, args.model)
|
||||
reports.append(report)
|
||||
if not args.html:
|
||||
print(format_report(report, args.format))
|
||||
|
||||
elif args.url:
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
screenshot_path = tmp.name
|
||||
print(f"Capturing screenshot of {args.url}...", file=sys.stderr)
|
||||
if capture_screenshot(args.url, screenshot_path):
|
||||
report = detect_glitches(screenshot_path, args.vision, args.model)
|
||||
report.source = args.url
|
||||
reports.append(report)
|
||||
if not args.html:
|
||||
print(format_report(report, args.format))
|
||||
else:
|
||||
print(f"Failed to capture screenshot", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif args.batch:
|
||||
batch_dir = Path(args.batch)
|
||||
images = sorted(batch_dir.glob("*.png")) + sorted(batch_dir.glob("*.jpg"))
|
||||
for img in images:
|
||||
print(f"Analyzing {img.name}...", file=sys.stderr)
|
||||
report = detect_glitches(str(img), args.vision, args.model)
|
||||
reports.append(report)
|
||||
|
||||
# HTML report
|
||||
if args.html:
|
||||
html = generate_html_report(reports, title="The Matrix — Glitch Detection Report")
|
||||
Path(args.html).write_text(html)
|
||||
print(f"HTML report written to {args.html}", file=sys.stderr)
|
||||
elif args.batch and not args.html:
|
||||
# Print JSON array for batch
|
||||
print(json.dumps([json.loads(format_report(r, "json")) for r in reports], indent=2))
|
||||
|
||||
# Exit code
|
||||
if any(r.status == "FAIL" for r in reports):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
print(json.dumps(detect_glitches(), indent=2))
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Nightly Pipeline Scheduler
|
||||
|
||||
Auto-starts batch pipelines when inference is available.
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Checks inference provider health (OpenRouter, Ollama, RunPod)
|
||||
2. Checks if it's off-peak hours (configurable, default: after 6PM)
|
||||
3. Checks interactive session load (don't fight with live users)
|
||||
4. Checks daily token budget (configurable limit)
|
||||
5. Starts the highest-priority incomplete pipeline
|
||||
|
||||
## Pipeline Priority Order
|
||||
|
||||
| Priority | Pipeline | Deps | Max Tokens |
|
||||
|----------|----------|------|------------|
|
||||
| 1 | playground-factory | none | 100,000 |
|
||||
| 2 | training-factory | none | 150,000 |
|
||||
| 3 | knowledge-mine | training-factory running | 80,000 |
|
||||
| 4 | adversary | knowledge-mine running | 50,000 |
|
||||
| 5 | codebase-genome | none | 120,000 |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Normal run (used by cron)
|
||||
./scripts/nightly-pipeline-scheduler.sh
|
||||
|
||||
# Dry run (show what would start)
|
||||
./scripts/nightly-pipeline-scheduler.sh --dry-run
|
||||
|
||||
# Status report
|
||||
./scripts/nightly-pipeline-scheduler.sh --status
|
||||
|
||||
# Force start during peak hours
|
||||
./scripts/nightly-pipeline-scheduler.sh --force
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set via environment variables:
|
||||
- `PIPELINE_TOKEN_LIMIT`: Daily token budget (default: 500,000)
|
||||
- `PIPELINE_PEAK_START`: Peak hours start (default: 9)
|
||||
- `PIPELINE_PEAK_END`: Peak hours end (default: 18)
|
||||
- `HERMES_HOME`: Hermes home directory (default: ~/.hermes)
|
||||
|
||||
## Cron
|
||||
|
||||
Runs every 30 minutes. Off-peak only (unless --force).
|
||||
See `cron/pipeline-scheduler.yml`.
|
||||
@@ -1,383 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# nightly-pipeline-scheduler.sh — Auto-start batch pipelines when inference is available.
|
||||
#
|
||||
# Checks provider health, pipeline progress, token budget, and interactive load.
|
||||
# Starts the highest-priority incomplete pipeline that can run.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/nightly-pipeline-scheduler.sh # Normal run
|
||||
# ./scripts/nightly-pipeline-scheduler.sh --dry-run # Show what would start
|
||||
# ./scripts/nightly-pipeline-scheduler.sh --status # Pipeline status report
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||
BUDGET_FILE="${HERMES_HOME}/pipeline_budget.json"
|
||||
STATE_FILE="${HERMES_HOME}/pipeline_state.json"
|
||||
LOG_FILE="${HERMES_HOME}/logs/pipeline-scheduler.log"
|
||||
TOKEN_DAILY_LIMIT="${PIPELINE_TOKEN_LIMIT:-500000}"
|
||||
PEAK_HOURS_START="${PIPELINE_PEAK_START:-9}"
|
||||
PEAK_HOURS_END="${PIPELINE_PEAK_END:-18}"
|
||||
|
||||
# Pipeline definitions (priority order)
|
||||
# Each pipeline: name, script, max_tokens, dependencies
|
||||
PIPELINES=(
|
||||
"playground-factory|scripts/pipeline_playground_factory.sh|100000|none"
|
||||
"training-factory|scripts/pipeline_training_factory.sh|150000|none"
|
||||
"knowledge-mine|scripts/pipeline_knowledge_mine.sh|80000|training-factory"
|
||||
"adversary|scripts/pipeline_adversary.sh|50000|knowledge-mine"
|
||||
"codebase-genome|scripts/pipeline_codebase_genome.sh|120000|none"
|
||||
)
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# --- Helpers ---
|
||||
now_hour() { date +%-H; }
|
||||
is_peak_hours() {
|
||||
local h=$(now_hour)
|
||||
[[ $h -ge $PEAK_HOURS_START && $h -lt $PEAK_HOURS_END ]]
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$BUDGET_FILE")" "$(dirname "$STATE_FILE")"
|
||||
}
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
get_budget_used_today() {
|
||||
if [[ -f "$BUDGET_FILE" ]]; then
|
||||
local today=$(date +%Y-%m-%d)
|
||||
python3 -c "
|
||||
import json, sys
|
||||
with open('$BUDGET_FILE') as f:
|
||||
d = json.load(f)
|
||||
print(d.get('daily', {}).get('$today', {}).get('tokens_used', 0))
|
||||
" 2>/dev/null || echo 0
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
get_budget_remaining() {
|
||||
local used=$(get_budget_used_today)
|
||||
echo $((TOKEN_DAILY_LIMIT - used))
|
||||
}
|
||||
|
||||
update_budget() {
|
||||
local pipeline="$1"
|
||||
local tokens="$2"
|
||||
local today=$(date +%Y-%m-%d)
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$BUDGET_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
daily = d.setdefault('daily', {})
|
||||
day = daily.setdefault('$today', {'tokens_used': 0, 'pipelines': {}})
|
||||
day['tokens_used'] = day.get('tokens_used', 0) + $tokens
|
||||
day['pipelines']['$pipeline'] = day['pipelines'].get('$pipeline', 0) + $tokens
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
}
|
||||
|
||||
get_pipeline_state() {
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
set_pipeline_state() {
|
||||
local pipeline="$1"
|
||||
local state="$2" # running, complete, failed, skipped
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
d['$pipeline'] = {'state': '$state', 'updated': '$(date -Iseconds)'}
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
}
|
||||
|
||||
is_pipeline_complete() {
|
||||
local pipeline="$1"
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('false')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
state = d.get('$pipeline', {}).get('state', 'not_started')
|
||||
print('true' if state == 'complete' else 'false')
|
||||
" 2>/dev/null || echo false
|
||||
}
|
||||
|
||||
is_pipeline_running() {
|
||||
local pipeline="$1"
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('false')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
state = d.get('$pipeline', {}).get('state', 'not_started')
|
||||
print('true' if state == 'running' else 'false')
|
||||
" 2>/dev/null || echo false
|
||||
}
|
||||
|
||||
check_dependency() {
|
||||
local dep="$1"
|
||||
if [[ "$dep" == "none" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# For knowledge-mine: training-factory must be running or complete
|
||||
if [[ "$dep" == "training-factory" ]]; then
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('training-factory', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
[[ "$state" == "running" || "$state" == "complete" ]]
|
||||
return $?
|
||||
fi
|
||||
# For adversary: knowledge-mine must be at least 50% done
|
||||
# Simplified: check if it's running (we'd need progress tracking for 50%)
|
||||
if [[ "$dep" == "knowledge-mine" ]]; then
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('knowledge-mine', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
[[ "$state" == "running" || "$state" == "complete" ]]
|
||||
return $?
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_inference_available() {
|
||||
# Check if any inference provider is responding
|
||||
# 1. Check OpenRouter
|
||||
local or_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "https://openrouter.ai/api/v1/models" 2>/dev/null || echo "000")
|
||||
|
||||
# 2. Check local Ollama
|
||||
local ollama_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "http://localhost:11434/api/tags" 2>/dev/null || echo "000")
|
||||
|
||||
# 3. Check RunPod (if configured)
|
||||
local runpod_ok="000"
|
||||
if [[ -n "${RUNPOD_ENDPOINT:-}" ]]; then
|
||||
runpod_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "$RUNPOD_ENDPOINT/health" 2>/dev/null || echo "000")
|
||||
fi
|
||||
|
||||
if [[ "$or_ok" == "200" || "$ollama_ok" == "200" || "$runpod_ok" == "200" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
check_interactive_load() {
|
||||
# Check if there are active interactive sessions (don't fight with live users)
|
||||
# Look for tmux panes with active hermes sessions
|
||||
local active=$(tmux list-panes -a -F '#{pane_pid} #{pane_current_command}' 2>/dev/null \
|
||||
| grep -c "hermes\|python3" || echo 0)
|
||||
|
||||
# If more than 3 interactive sessions, skip pipeline start
|
||||
if [[ $active -gt 3 ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
start_pipeline() {
|
||||
local name="$1"
|
||||
local script="$2"
|
||||
local max_tokens="$3"
|
||||
local budget_remaining="$4"
|
||||
local mode="${5:-run}"
|
||||
|
||||
if [[ "$budget_remaining" -lt "$max_tokens" ]]; then
|
||||
log "SKIP $name: insufficient budget ($budget_remaining < $max_tokens tokens)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$script" ]]; then
|
||||
log "SKIP $name: script not found ($script)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "dry-run" ]]; then
|
||||
log "DRY-RUN: Would start $name (budget: $budget_remaining, needs: $max_tokens)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "START $name (budget: $budget_remaining, max_tokens: $max_tokens)"
|
||||
set_pipeline_state "$name" "running"
|
||||
|
||||
# Run in background, capture output
|
||||
local log_path="${HERMES_HOME}/logs/pipeline-${name}.log"
|
||||
bash "$script" --max-tokens "$max_tokens" >> "$log_path" 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
# Wait a moment to check if it started OK
|
||||
sleep 2
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
log "RUNNING $name (PID: $pid, log: $log_path)"
|
||||
# Record the PID
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
d['$name']['pid'] = $pid
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
return 0
|
||||
else
|
||||
log "FAIL $name: script exited immediately"
|
||||
set_pipeline_state "$name" "failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
main() {
|
||||
local mode="${1:-run}"
|
||||
ensure_dirs
|
||||
|
||||
log "=== Pipeline Scheduler ($mode) ==="
|
||||
|
||||
# Check 1: Is inference available?
|
||||
if ! check_inference_available; then
|
||||
log "No inference provider available. Skipping all pipelines."
|
||||
exit 0
|
||||
fi
|
||||
log "Inference: AVAILABLE"
|
||||
|
||||
# Check 2: Is it peak hours?
|
||||
if is_peak_hours && [[ "$mode" != "--force" ]]; then
|
||||
local h=$(now_hour)
|
||||
log "Peak hours ($h:00). Skipping pipeline start. Use --force to override."
|
||||
exit 0
|
||||
fi
|
||||
log "Off-peak: OK"
|
||||
|
||||
# Check 3: Interactive load
|
||||
if ! check_interactive_load && [[ "$mode" != "--force" ]]; then
|
||||
log "High interactive load. Skipping pipeline start."
|
||||
exit 0
|
||||
fi
|
||||
log "Interactive load: OK"
|
||||
|
||||
# Check 4: Token budget
|
||||
local budget=$(get_budget_remaining)
|
||||
log "Token budget remaining: $budget / $TOKEN_DAILY_LIMIT"
|
||||
|
||||
if [[ $budget -le 0 ]]; then
|
||||
log "Daily token budget exhausted. Stopping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check 5: Pipeline status
|
||||
if [[ "$mode" == "--status" ]]; then
|
||||
echo -e "${CYAN}Pipeline Status:${NC}"
|
||||
echo "────────────────────────────────────────────────────"
|
||||
for entry in "${PIPELINES[@]}"; do
|
||||
IFS='|' read -r name script max_tokens dep <<< "$entry"
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('$name', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
|
||||
local color=$NC
|
||||
case "$state" in
|
||||
running) color=$YELLOW ;;
|
||||
complete) color=$GREEN ;;
|
||||
failed) color=$RED ;;
|
||||
esac
|
||||
printf " %-25s %b%s%b (max: %s tokens, dep: %s)\n" "$name" "$color" "$state" "$NC" "$max_tokens" "$dep"
|
||||
done
|
||||
echo "────────────────────────────────────────────────────"
|
||||
echo " Budget: $budget / $TOKEN_DAILY_LIMIT tokens remaining"
|
||||
echo " Peak hours: $PEAK_HOURS_START:00 - $PEAK_HOURS_END:00"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find and start the highest-priority incomplete pipeline
|
||||
local started=0
|
||||
for entry in "${PIPELINES[@]}"; do
|
||||
IFS='|' read -r name script max_tokens dep <<< "$entry"
|
||||
|
||||
# Skip if already running or complete
|
||||
if [[ "$(is_pipeline_running $name)" == "true" ]]; then
|
||||
log "SKIP $name: already running"
|
||||
continue
|
||||
fi
|
||||
if [[ "$(is_pipeline_complete $name)" == "true" ]]; then
|
||||
log "SKIP $name: already complete"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check dependency
|
||||
if ! check_dependency "$dep"; then
|
||||
log "SKIP $name: dependency $dep not met"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Try to start
|
||||
if start_pipeline "$name" "$script" "$max_tokens" "$budget" "$mode"; then
|
||||
started=1
|
||||
# Only start one pipeline per run (let it claim tokens before next check)
|
||||
# Exception: playground-factory and training-factory can run in parallel
|
||||
if [[ "$name" != "playground-factory" && "$name" != "training-factory" ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $started -eq 0 ]]; then
|
||||
log "No pipelines to start (all complete, running, or blocked)."
|
||||
fi
|
||||
|
||||
log "=== Pipeline Scheduler done ==="
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from hermes_tools import browser_navigate, browser_vision
|
||||
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for foundation_accessibility_audit.py — verifies WCAG checks."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from foundation_accessibility_audit import (
|
||||
A11yHTMLParser, Severity, A11yViolation,
|
||||
parse_color, contrast_ratio, relative_luminance,
|
||||
run_programmatic_checks, check_page_title, check_images_alt_text,
|
||||
check_heading_hierarchy, check_lang_attribute, check_landmarks,
|
||||
check_skip_nav, check_form_labels, check_link_text,
|
||||
_parse_json_response, format_report, A11yAuditReport, A11yPageResult,
|
||||
)
|
||||
|
||||
|
||||
# === Color Utilities ===
|
||||
|
||||
def test_parse_color_hex6():
|
||||
assert parse_color("#ff0000") == (255, 0, 0)
|
||||
assert parse_color("#000000") == (0, 0, 0)
|
||||
assert parse_color("#ffffff") == (255, 255, 255)
|
||||
print(" PASS: test_parse_color_hex6")
|
||||
|
||||
|
||||
def test_parse_color_hex3():
|
||||
assert parse_color("#f00") == (255, 0, 0)
|
||||
assert parse_color("#abc") == (170, 187, 204)
|
||||
print(" PASS: test_parse_color_hex3")
|
||||
|
||||
|
||||
def test_parse_color_rgb():
|
||||
assert parse_color("rgb(255, 0, 0)") == (255, 0, 0)
|
||||
assert parse_color("rgb( 128 , 64 , 32 )") == (128, 64, 32)
|
||||
print(" PASS: test_parse_color_rgb")
|
||||
|
||||
|
||||
def test_parse_color_named():
|
||||
assert parse_color("white") == (255, 255, 255)
|
||||
assert parse_color("black") == (0, 0, 0)
|
||||
print(" PASS: test_parse_color_named")
|
||||
|
||||
|
||||
def test_parse_color_invalid():
|
||||
assert parse_color("not-a-color") is None
|
||||
assert parse_color("") is None
|
||||
print(" PASS: test_parse_color_invalid")
|
||||
|
||||
|
||||
def test_contrast_ratio_black_white():
|
||||
ratio = contrast_ratio((0, 0, 0), (255, 255, 255))
|
||||
assert ratio > 20 # Should be 21:1
|
||||
print(f" PASS: test_contrast_ratio_black_white ({ratio:.1f}:1)")
|
||||
|
||||
|
||||
def test_contrast_ratio_same():
|
||||
ratio = contrast_ratio((128, 128, 128), (128, 128, 128))
|
||||
assert ratio == 1.0
|
||||
print(" PASS: test_contrast_ratio_same")
|
||||
|
||||
|
||||
def test_contrast_ratio_wcag_aa():
|
||||
# #767676 on white = 4.54:1 (WCAG AA pass for normal text)
|
||||
ratio = contrast_ratio((118, 118, 118), (255, 255, 255))
|
||||
assert ratio >= 4.5
|
||||
print(f" PASS: test_contrast_ratio_wcag_aa ({ratio:.2f}:1)")
|
||||
|
||||
|
||||
# === HTML Parser ===
|
||||
|
||||
def test_parser_title():
|
||||
parser = A11yHTMLParser()
|
||||
parser.feed("<html><head><title>Test Page</title></head></html>")
|
||||
assert parser.title == "Test Page"
|
||||
print(" PASS: test_parser_title")
|
||||
|
||||
|
||||
def test_parser_images():
|
||||
parser = A11yHTMLParser()
|
||||
parser.feed('<html><body><img src="a.png" alt="Alt text"><img src="b.png"></body></html>')
|
||||
assert len(parser.images) == 2
|
||||
assert parser.images[0]["alt"] == "Alt text"
|
||||
assert parser.images[1]["alt"] is None
|
||||
print(" PASS: test_parser_images")
|
||||
|
||||
|
||||
def test_parser_headings():
|
||||
parser = A11yHTMLParser()
|
||||
parser.feed("<html><body><h1>Main</h1><h2>Sub</h2><h4>Skip</h4></body></html>")
|
||||
assert len(parser.headings) == 3
|
||||
assert parser.headings[0] == {"level": 1, "text": "Main"}
|
||||
assert parser.headings[2] == {"level": 4, "text": "Skip"}
|
||||
print(" PASS: test_parser_headings")
|
||||
|
||||
|
||||
def test_parser_lang():
|
||||
parser = A11yHTMLParser()
|
||||
parser.feed('<html lang="en"><body></body></html>')
|
||||
assert parser.lang == "en"
|
||||
print(" PASS: test_parser_lang")
|
||||
|
||||
|
||||
def test_parser_landmarks():
|
||||
parser = A11yHTMLParser()
|
||||
parser.feed("<html><body><nav>Links</nav><main>Content</main></body></html>")
|
||||
tags = {lm["tag"] for lm in parser.landmarks}
|
||||
assert "nav" in tags
|
||||
assert "main" in tags
|
||||
print(" PASS: test_parser_landmarks")
|
||||
|
||||
|
||||
# === Programmatic Checks ===
|
||||
|
||||
def test_check_page_title_empty():
|
||||
parser = A11yHTMLParser()
|
||||
parser.title = ""
|
||||
violations = check_page_title(parser)
|
||||
assert len(violations) == 1
|
||||
assert violations[0].criterion == "2.4.2"
|
||||
assert violations[0].severity == Severity.MAJOR
|
||||
print(" PASS: test_check_page_title_empty")
|
||||
|
||||
|
||||
def test_check_page_title_present():
|
||||
parser = A11yHTMLParser()
|
||||
parser.title = "My Great Page"
|
||||
violations = check_page_title(parser)
|
||||
assert len(violations) == 0
|
||||
print(" PASS: test_check_page_title_present")
|
||||
|
||||
|
||||
def test_check_lang_missing():
|
||||
parser = A11yHTMLParser()
|
||||
parser.lang = ""
|
||||
violations = check_lang_attribute(parser)
|
||||
assert len(violations) == 1
|
||||
assert violations[0].criterion == "3.1.1"
|
||||
print(" PASS: test_check_lang_missing")
|
||||
|
||||
|
||||
def test_check_images_missing_alt():
|
||||
parser = A11yHTMLParser()
|
||||
parser.images = [{"src": "photo.jpg", "alt": None}]
|
||||
violations = check_images_alt_text(parser)
|
||||
assert len(violations) == 1
|
||||
assert violations[0].severity == Severity.CRITICAL
|
||||
print(" PASS: test_check_images_missing_alt")
|
||||
|
||||
|
||||
def test_check_images_with_alt():
|
||||
parser = A11yHTMLParser()
|
||||
parser.images = [{"src": "photo.jpg", "alt": "A photo"}]
|
||||
violations = check_images_alt_text(parser)
|
||||
assert len(violations) == 0
|
||||
print(" PASS: test_check_images_with_alt")
|
||||
|
||||
|
||||
def test_check_images_decorative():
|
||||
parser = A11yHTMLParser()
|
||||
parser.images = [{"src": "deco.png", "alt": "", "role": "presentation"}]
|
||||
violations = check_images_alt_text(parser)
|
||||
assert len(violations) == 0
|
||||
print(" PASS: test_check_images_decorative")
|
||||
|
||||
|
||||
def test_check_headings_no_h1():
|
||||
parser = A11yHTMLParser()
|
||||
parser.headings = [{"level": 2, "text": "Sub"}, {"level": 3, "text": "Sub sub"}]
|
||||
violations = check_heading_hierarchy(parser)
|
||||
assert any(v.criterion == "1.3.1" and "h1" in v.description.lower() for v in violations)
|
||||
print(" PASS: test_check_headings_no_h1")
|
||||
|
||||
|
||||
def test_check_headings_skip():
|
||||
parser = A11yHTMLParser()
|
||||
parser.headings = [{"level": 1, "text": "Main"}, {"level": 4, "text": "Skipped"}]
|
||||
violations = check_heading_hierarchy(parser)
|
||||
assert any("skipped" in v.description.lower() for v in violations)
|
||||
print(" PASS: test_check_headings_skip")
|
||||
|
||||
|
||||
def test_check_skip_nav_missing():
|
||||
parser = A11yHTMLParser()
|
||||
parser.skip_nav = False
|
||||
parser.links = [{"text": "Home", "href": "/"}, {"text": "About", "href": "/about"}]
|
||||
violations = check_skip_nav(parser)
|
||||
assert len(violations) == 1
|
||||
assert violations[0].criterion == "2.4.1"
|
||||
print(" PASS: test_check_skip_nav_missing")
|
||||
|
||||
|
||||
def test_check_link_text_empty():
|
||||
parser = A11yHTMLParser()
|
||||
parser.links = [{"text": "", "href": "/page", "aria_label": ""}]
|
||||
violations = check_link_text(parser)
|
||||
assert len(violations) == 1
|
||||
assert violations[0].criterion == "2.4.4"
|
||||
print(" PASS: test_check_link_text_empty")
|
||||
|
||||
|
||||
def test_check_link_text_generic():
|
||||
parser = A11yHTMLParser()
|
||||
parser.links = [{"text": "Click here", "href": "/page"}]
|
||||
violations = check_link_text(parser)
|
||||
assert any("non-descriptive" in v.description.lower() for v in violations)
|
||||
print(" PASS: test_check_link_text_generic")
|
||||
|
||||
|
||||
def test_run_programmatic_checks_full():
|
||||
html = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>Good Page</title></head>
|
||||
<body>
|
||||
<nav><a href="#main">Skip to content</a></nav>
|
||||
<main>
|
||||
<h1>Welcome</h1>
|
||||
<h2>Section</h2>
|
||||
<img src="hero.jpg" alt="Hero image">
|
||||
<a href="/about">About Us</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
violations = run_programmatic_checks(html)
|
||||
# This page should have very few or no violations
|
||||
criticals = [v for v in violations if v.severity == Severity.CRITICAL]
|
||||
assert len(criticals) == 0
|
||||
print(f" PASS: test_run_programmatic_checks_full ({len(violations)} minor issues)")
|
||||
|
||||
|
||||
# === JSON Parsing ===
|
||||
|
||||
def test_parse_json_clean():
|
||||
result = _parse_json_response('{"violations": [], "overall_score": 100}')
|
||||
assert result["overall_score"] == 100
|
||||
print(" PASS: test_parse_json_clean")
|
||||
|
||||
|
||||
def test_parse_json_fenced():
|
||||
result = _parse_json_response('```json\n{"overall_score": 80}\n```')
|
||||
assert result["overall_score"] == 80
|
||||
print(" PASS: test_parse_json_fenced")
|
||||
|
||||
|
||||
# === Formatting ===
|
||||
|
||||
def test_format_json():
|
||||
report = A11yAuditReport(site="test.com", pages_audited=1, overall_score=90)
|
||||
output = format_report(report, "json")
|
||||
parsed = json.loads(output)
|
||||
assert parsed["site"] == "test.com"
|
||||
assert parsed["overall_score"] == 90
|
||||
print(" PASS: test_format_json")
|
||||
|
||||
|
||||
def test_format_text():
|
||||
report = A11yAuditReport(site="test.com", pages_audited=1, overall_score=90,
|
||||
summary="Test complete")
|
||||
output = format_report(report, "text")
|
||||
assert "ACCESSIBILITY AUDIT" in output
|
||||
assert "test.com" in output
|
||||
print(" PASS: test_format_text")
|
||||
|
||||
|
||||
# === Run All ===
|
||||
|
||||
def run_all():
|
||||
print("=== foundation_accessibility_audit tests ===")
|
||||
tests = [
|
||||
test_parse_color_hex6, test_parse_color_hex3, test_parse_color_rgb,
|
||||
test_parse_color_named, test_parse_color_invalid,
|
||||
test_contrast_ratio_black_white, test_contrast_ratio_same, test_contrast_ratio_wcag_aa,
|
||||
test_parser_title, test_parser_images, test_parser_headings,
|
||||
test_parser_lang, test_parser_landmarks,
|
||||
test_check_page_title_empty, test_check_page_title_present,
|
||||
test_check_lang_missing,
|
||||
test_check_images_missing_alt, test_check_images_with_alt, test_check_images_decorative,
|
||||
test_check_headings_no_h1, test_check_headings_skip,
|
||||
test_check_skip_nav_missing,
|
||||
test_check_link_text_empty, test_check_link_text_generic,
|
||||
test_run_programmatic_checks_full,
|
||||
test_parse_json_clean, test_parse_json_fenced,
|
||||
test_format_json, test_format_text,
|
||||
]
|
||||
passed = 0
|
||||
failed = 0
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" FAIL: {test.__name__} — {e}")
|
||||
failed += 1
|
||||
print(f"\n{'ALL PASSED' if failed == 0 else f'{failed} FAILED'}: {passed}/{len(tests)}")
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(0 if run_all() else 1)
|
||||
@@ -1,281 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for Matrix 3D Glitch Detector (timmy-config#491).
|
||||
|
||||
Covers: glitch_patterns, matrix_glitch_detector core logic.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure bin/ is importable
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bin"))
|
||||
|
||||
from glitch_patterns import (
|
||||
GlitchCategory,
|
||||
GlitchPattern,
|
||||
GlitchSeverity,
|
||||
MATRIX_GLITCH_PATTERNS,
|
||||
build_vision_prompt,
|
||||
get_pattern_by_category,
|
||||
get_patterns_by_severity,
|
||||
)
|
||||
|
||||
from matrix_glitch_detector import (
|
||||
DetectedGlitch,
|
||||
ScanResult,
|
||||
_infer_severity,
|
||||
_parse_vision_response,
|
||||
build_report,
|
||||
generate_scan_angles,
|
||||
run_demo,
|
||||
)
|
||||
|
||||
|
||||
class TestGlitchPatterns(unittest.TestCase):
|
||||
"""Tests for glitch_patterns module."""
|
||||
|
||||
def test_pattern_count(self):
|
||||
"""Verify we have a reasonable number of defined patterns."""
|
||||
self.assertGreaterEqual(len(MATRIX_GLITCH_PATTERNS), 8)
|
||||
|
||||
def test_all_patterns_have_required_fields(self):
|
||||
"""Every pattern must have category, name, description, severity, prompts."""
|
||||
for p in MATRIX_GLITCH_PATTERNS:
|
||||
self.assertIsInstance(p.category, GlitchCategory)
|
||||
self.assertTrue(p.name)
|
||||
self.assertTrue(p.description)
|
||||
self.assertIsInstance(p.severity, GlitchSeverity)
|
||||
self.assertGreater(len(p.detection_prompts), 0)
|
||||
self.assertGreater(len(p.visual_indicators), 0)
|
||||
self.assertGreater(p.confidence_threshold, 0)
|
||||
self.assertLessEqual(p.confidence_threshold, 1.0)
|
||||
|
||||
def test_pattern_to_dict(self):
|
||||
"""Pattern serialization should produce a dict with expected keys."""
|
||||
p = MATRIX_GLITCH_PATTERNS[0]
|
||||
d = p.to_dict()
|
||||
self.assertIn("category", d)
|
||||
self.assertIn("name", d)
|
||||
self.assertIn("severity", d)
|
||||
self.assertEqual(d["category"], p.category.value)
|
||||
|
||||
def test_get_patterns_by_severity(self):
|
||||
"""Severity filter should return only patterns at or above threshold."""
|
||||
high_patterns = get_patterns_by_severity(GlitchSeverity.HIGH)
|
||||
self.assertTrue(all(p.severity.value in ("high", "critical") for p in high_patterns))
|
||||
self.assertGreater(len(high_patterns), 0)
|
||||
|
||||
all_patterns = get_patterns_by_severity(GlitchSeverity.INFO)
|
||||
self.assertEqual(len(all_patterns), len(MATRIX_GLITCH_PATTERNS))
|
||||
|
||||
def test_get_pattern_by_category(self):
|
||||
"""Lookup by category should return the correct pattern."""
|
||||
p = get_pattern_by_category(GlitchCategory.FLOATING_ASSETS)
|
||||
self.assertIsNotNone(p)
|
||||
self.assertEqual(p.category, GlitchCategory.FLOATING_ASSETS)
|
||||
|
||||
missing = get_pattern_by_category("nonexistent_category_value")
|
||||
self.assertIsNone(missing)
|
||||
|
||||
def test_build_vision_prompt(self):
|
||||
"""Vision prompt should contain pattern names and be non-trivial."""
|
||||
prompt = build_vision_prompt()
|
||||
self.assertGreater(len(prompt), 200)
|
||||
self.assertIn("Floating Object", prompt)
|
||||
self.assertIn("Z-Fighting", prompt)
|
||||
self.assertIn("Missing", prompt)
|
||||
|
||||
def test_build_vision_prompt_subset(self):
|
||||
"""Vision prompt with subset should only include specified patterns."""
|
||||
subset = MATRIX_GLITCH_PATTERNS[:3]
|
||||
prompt = build_vision_prompt(subset)
|
||||
self.assertIn(subset[0].name, prompt)
|
||||
self.assertNotIn(MATRIX_GLITCH_PATTERNS[-1].name, prompt)
|
||||
|
||||
|
||||
class TestGlitchDetector(unittest.TestCase):
|
||||
"""Tests for matrix_glitch_detector module."""
|
||||
|
||||
def test_generate_scan_angles_default(self):
|
||||
"""Default 4 angles should return front, right, back, left."""
|
||||
angles = generate_scan_angles(4)
|
||||
self.assertEqual(len(angles), 4)
|
||||
labels = [a["label"] for a in angles]
|
||||
self.assertIn("front", labels)
|
||||
self.assertIn("right", labels)
|
||||
self.assertIn("back", labels)
|
||||
self.assertIn("left", labels)
|
||||
|
||||
def test_generate_scan_angles_many(self):
|
||||
"""Requesting more angles than base should still return correct count."""
|
||||
angles = generate_scan_angles(12)
|
||||
self.assertEqual(len(angles), 12)
|
||||
# Should still have the standard ones
|
||||
labels = [a["label"] for a in angles]
|
||||
self.assertIn("front", labels)
|
||||
|
||||
def test_generate_scan_angles_few(self):
|
||||
"""Requesting fewer angles should return fewer."""
|
||||
angles = generate_scan_angles(2)
|
||||
self.assertEqual(len(angles), 2)
|
||||
|
||||
def test_detected_glitch_dataclass(self):
|
||||
"""DetectedGlitch should serialize cleanly."""
|
||||
g = DetectedGlitch(
|
||||
id="test001",
|
||||
category="floating_assets",
|
||||
name="Test Glitch",
|
||||
description="A test glitch",
|
||||
severity="high",
|
||||
confidence=0.85,
|
||||
location_x=50.0,
|
||||
location_y=30.0,
|
||||
screenshot_index=0,
|
||||
screenshot_angle="front",
|
||||
)
|
||||
self.assertEqual(g.id, "test001")
|
||||
self.assertTrue(g.timestamp) # Auto-generated
|
||||
|
||||
def test_infer_severity_critical(self):
|
||||
"""Missing textures should infer critical/high severity."""
|
||||
sev = _infer_severity("missing_textures", 0.9)
|
||||
self.assertEqual(sev, "critical")
|
||||
sev_low = _infer_severity("missing_textures", 0.5)
|
||||
self.assertEqual(sev_low, "high")
|
||||
|
||||
def test_infer_severity_floating(self):
|
||||
"""Floating assets should infer high/medium severity."""
|
||||
sev = _infer_severity("floating_assets", 0.8)
|
||||
self.assertEqual(sev, "high")
|
||||
sev_low = _infer_severity("floating_assets", 0.5)
|
||||
self.assertEqual(sev_low, "medium")
|
||||
|
||||
def test_infer_severity_default(self):
|
||||
"""Unknown categories should default to medium/low."""
|
||||
sev = _infer_severity("unknown_thing", 0.7)
|
||||
self.assertEqual(sev, "medium")
|
||||
sev_low = _infer_severity("unknown_thing", 0.3)
|
||||
self.assertEqual(sev_low, "low")
|
||||
|
||||
def test_parse_vision_response_json_array(self):
|
||||
"""Should parse a JSON array response."""
|
||||
response = json.dumps([
|
||||
{
|
||||
"category": "floating_assets",
|
||||
"name": "Float Test",
|
||||
"description": "Chair floating",
|
||||
"confidence": 0.9,
|
||||
"severity": "high",
|
||||
"location_x": 40,
|
||||
"location_y": 60,
|
||||
}
|
||||
])
|
||||
glitches = _parse_vision_response(response, 0, "front")
|
||||
self.assertEqual(len(glitches), 1)
|
||||
self.assertEqual(glitches[0].category, "floating_assets")
|
||||
self.assertAlmostEqual(glitches[0].confidence, 0.9)
|
||||
|
||||
def test_parse_vision_response_wrapped(self):
|
||||
"""Should parse a response with 'glitches' wrapper key."""
|
||||
response = json.dumps({
|
||||
"glitches": [
|
||||
{
|
||||
"category": "z_fighting",
|
||||
"name": "Shimmer",
|
||||
"confidence": 0.6,
|
||||
}
|
||||
]
|
||||
})
|
||||
glitches = _parse_vision_response(response, 1, "right")
|
||||
self.assertEqual(len(glitches), 1)
|
||||
self.assertEqual(glitches[0].category, "z_fighting")
|
||||
|
||||
def test_parse_vision_response_empty(self):
|
||||
"""Should return empty list for non-JSON text."""
|
||||
glitches = _parse_vision_response("No glitches found.", 0, "front")
|
||||
self.assertEqual(len(glitches), 0)
|
||||
|
||||
def test_parse_vision_response_code_block(self):
|
||||
"""Should extract JSON from markdown code blocks."""
|
||||
response = '```json\n[{"category": "clipping", "name": "Clip", "confidence": 0.7}]\n```'
|
||||
glitches = _parse_vision_response(response, 0, "front")
|
||||
self.assertEqual(len(glitches), 1)
|
||||
|
||||
def test_build_report(self):
|
||||
"""Report should have correct summary statistics."""
|
||||
angles = generate_scan_angles(4)
|
||||
screenshots = [Path(f"/tmp/ss_{i}.png") for i in range(4)]
|
||||
glitches = [
|
||||
DetectedGlitch(
|
||||
id="a", category="floating_assets", name="Float",
|
||||
description="", severity="high", confidence=0.8,
|
||||
screenshot_index=0, screenshot_angle="front",
|
||||
),
|
||||
DetectedGlitch(
|
||||
id="b", category="missing_textures", name="Missing",
|
||||
description="", severity="critical", confidence=0.95,
|
||||
screenshot_index=1, screenshot_angle="right",
|
||||
),
|
||||
]
|
||||
report = build_report("https://test.com", angles, screenshots, glitches)
|
||||
|
||||
self.assertEqual(report.total_screenshots, 4)
|
||||
self.assertEqual(len(report.glitches), 2)
|
||||
self.assertEqual(report.summary["total_glitches"], 2)
|
||||
self.assertEqual(report.summary["by_severity"]["critical"], 1)
|
||||
self.assertEqual(report.summary["by_severity"]["high"], 1)
|
||||
self.assertEqual(report.summary["by_category"]["floating_assets"], 1)
|
||||
self.assertEqual(report.metadata["reference"], "timmy-config#491")
|
||||
|
||||
def test_build_report_json_roundtrip(self):
|
||||
"""Report JSON should parse back correctly."""
|
||||
angles = generate_scan_angles(2)
|
||||
screenshots = [Path(f"/tmp/ss_{i}.png") for i in range(2)]
|
||||
report = build_report("https://test.com", angles, screenshots, [])
|
||||
json_str = report.to_json()
|
||||
parsed = json.loads(json_str)
|
||||
self.assertEqual(parsed["url"], "https://test.com")
|
||||
self.assertEqual(parsed["total_screenshots"], 2)
|
||||
|
||||
def test_run_demo(self):
|
||||
"""Demo mode should produce a report with simulated glitches."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
||||
output_path = Path(f.name)
|
||||
|
||||
try:
|
||||
report = run_demo(output_path)
|
||||
self.assertEqual(len(report.glitches), 4)
|
||||
self.assertGreater(report.summary["total_glitches"], 0)
|
||||
self.assertTrue(output_path.exists())
|
||||
|
||||
# Verify the saved JSON is valid
|
||||
saved = json.loads(output_path.read_text())
|
||||
self.assertIn("scan_id", saved)
|
||||
self.assertIn("glitches", saved)
|
||||
finally:
|
||||
output_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestIntegration(unittest.TestCase):
|
||||
"""Integration-level tests."""
|
||||
|
||||
def test_full_pipeline_demo(self):
|
||||
"""End-to-end demo pipeline should complete without errors."""
|
||||
report = run_demo()
|
||||
self.assertIsNotNone(report.scan_id)
|
||||
self.assertTrue(report.timestamp)
|
||||
self.assertGreater(report.total_screenshots, 0)
|
||||
|
||||
def test_patterns_cover_matrix_themes(self):
|
||||
"""Patterns should cover the main Matrix glitch themes."""
|
||||
category_values = {p.category.value for p in MATRIX_GLITCH_PATTERNS}
|
||||
expected = {"floating_assets", "z_fighting", "missing_textures", "clipping", "broken_normals"}
|
||||
self.assertTrue(expected.issubset(category_values))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for matrix_glitch_detect.py — verifies detection and HTML report logic."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from matrix_glitch_detect import (
|
||||
Severity, Glitch, GlitchReport,
|
||||
format_report, generate_html_report, _parse_json_response,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_json_clean():
|
||||
result = _parse_json_response('{"glitches": [], "overall_quality": 95}')
|
||||
assert result["overall_quality"] == 95
|
||||
print(" PASS: test_parse_json_clean")
|
||||
|
||||
|
||||
def test_parse_json_fenced():
|
||||
result = _parse_json_response('```json\n{"overall_quality": 80}\n```')
|
||||
assert result["overall_quality"] == 80
|
||||
print(" PASS: test_parse_json_fenced")
|
||||
|
||||
|
||||
def test_parse_json_garbage():
|
||||
assert _parse_json_response("no json") == {}
|
||||
print(" PASS: test_parse_json_garbage")
|
||||
|
||||
|
||||
def test_glitch_dataclass():
|
||||
g = Glitch(type="z_fighting", severity=Severity.MAJOR, region="center", description="Shimmer", confidence=0.8)
|
||||
assert g.type == "z_fighting"
|
||||
assert g.confidence == 0.8
|
||||
print(" PASS: test_glitch_dataclass")
|
||||
|
||||
|
||||
def test_report_dataclass():
|
||||
r = GlitchReport(source="test.png", status="WARN", score=75)
|
||||
r.glitches.append(Glitch(type="float", severity=Severity.MINOR))
|
||||
assert len(r.glitches) == 1
|
||||
assert r.score == 75
|
||||
print(" PASS: test_report_dataclass")
|
||||
|
||||
|
||||
def test_format_json():
|
||||
r = GlitchReport(source="test.png", status="PASS", score=90, summary="Clean")
|
||||
r.glitches.append(Glitch(type="cosmetic", severity=Severity.COSMETIC, description="Minor"))
|
||||
output = format_report(r, "json")
|
||||
parsed = json.loads(output)
|
||||
assert parsed["status"] == "PASS"
|
||||
assert len(parsed["glitches"]) == 1
|
||||
print(" PASS: test_format_json")
|
||||
|
||||
|
||||
def test_format_text():
|
||||
r = GlitchReport(source="test.png", status="FAIL", score=30, summary="Critical glitch")
|
||||
r.glitches.append(Glitch(type="render_failure", severity=Severity.CRITICAL, description="Black screen"))
|
||||
output = format_report(r, "text")
|
||||
assert "FAIL" in output
|
||||
assert "render_failure" in output
|
||||
print(" PASS: test_format_text")
|
||||
|
||||
|
||||
def test_html_report_basic():
|
||||
r = GlitchReport(source="test.png", status="PASS", score=100)
|
||||
html = generate_html_report([r], title="Test Report")
|
||||
assert "<!DOCTYPE html>" in html
|
||||
assert "Test Report" in html
|
||||
assert "PASS" in html
|
||||
assert "100" in html
|
||||
print(" PASS: test_html_report_basic")
|
||||
|
||||
|
||||
def test_html_report_with_glitches():
|
||||
r = GlitchReport(source="test.png", status="FAIL", score=40)
|
||||
r.glitches.append(Glitch(type="z_fighting", severity=Severity.CRITICAL, region="center", description="Heavy flicker", confidence=0.9))
|
||||
r.glitches.append(Glitch(type="clipping", severity=Severity.MINOR, region="bottom", description="Object through floor", confidence=0.6))
|
||||
html = generate_html_report([r], title="Glitch Report")
|
||||
assert "z_fighting" in html
|
||||
assert "CRITICAL" in html
|
||||
assert "clipping" in html
|
||||
assert "Heavy flicker" in html
|
||||
print(" PASS: test_html_report_with_glitches")
|
||||
|
||||
|
||||
def test_html_report_multi():
|
||||
r1 = GlitchReport(source="a.png", status="PASS", score=95)
|
||||
r2 = GlitchReport(source="b.png", status="WARN", score=70)
|
||||
r2.glitches.append(Glitch(type="texture_pop", severity=Severity.MAJOR))
|
||||
html = generate_html_report([r1, r2])
|
||||
assert "a.png" in html
|
||||
assert "b.png" in html
|
||||
assert "2" in html # 2 screenshots
|
||||
print(" PASS: test_html_report_multi")
|
||||
|
||||
|
||||
def test_html_self_contained():
|
||||
r = GlitchReport(source="test.png", status="PASS", score=100)
|
||||
html = generate_html_report([r])
|
||||
assert "external" not in html.lower() or "no external dependencies" in html.lower()
|
||||
assert "<style>" in html # Inline CSS
|
||||
print(" PASS: test_html_self_contained")
|
||||
|
||||
|
||||
def test_missing_image():
|
||||
r = GlitchReport(source="/nonexistent/image.png")
|
||||
# detect_glitches would set FAIL — simulate
|
||||
r.status = "FAIL"
|
||||
r.score = 0
|
||||
r.summary = "File not found"
|
||||
assert r.status == "FAIL"
|
||||
print(" PASS: test_missing_image")
|
||||
|
||||
|
||||
def test_severity_enum():
|
||||
assert Severity.CRITICAL.value == "critical"
|
||||
assert Severity.MAJOR.value == "major"
|
||||
print(" PASS: test_severity_enum")
|
||||
|
||||
|
||||
def run_all():
|
||||
print("=== matrix_glitch_detect tests ===")
|
||||
tests = [
|
||||
test_parse_json_clean, test_parse_json_fenced, test_parse_json_garbage,
|
||||
test_glitch_dataclass, test_report_dataclass,
|
||||
test_format_json, test_format_text,
|
||||
test_html_report_basic, test_html_report_with_glitches,
|
||||
test_html_report_multi, test_html_self_contained,
|
||||
test_missing_image, test_severity_enum,
|
||||
]
|
||||
passed = failed = 0
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" FAIL: {t.__name__} — {e}")
|
||||
failed += 1
|
||||
print(f"\n{'ALL PASSED' if failed == 0 else f'{failed} FAILED'}: {passed}/{len(tests)}")
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(0 if run_all() else 1)
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The silence before the first note is the real overture", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The silence before the first note is the real overture"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "We tune to the frequency of the void", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. We tune to the frequency of the void"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The overture is the question, the symphony is the answer", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. The overture is the question, the symphony is the answer"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Silence is the most expensive instrument", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. Silence is the most expensive instrument"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The overture promises what the finale may not deliver", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. The overture promises what the finale may not deliver"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Every great work begins with a held breath", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. Every great work begins with a held breath"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The silence contains every note that will ever be played", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The silence contains every note that will ever be played"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Overture: the handshake between composer and universe", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Overture: the handshake between composer and universe"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The silence listens as intently as the audience", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The silence listens as intently as the audience"}}
|
||||
{"song": "Overture in Silence", "artist": "Chamber Ensemble", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The overture is the thesis, the coda is the proof", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. The overture is the thesis, the coda is the proof"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "We play slowly so the lost can catch up", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. We play slowly so the lost can catch up"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Adagio: the speed of grief", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Adagio: the speed of grief"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The forgotten need slow music to find their way back", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. The forgotten need slow music to find their way back"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every adagio is a love letter to patience", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. Every adagio is a love letter to patience"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We slow down because the world won't", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. We slow down because the world won't"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Adagio for the ones who missed the crescendo", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. Adagio for the ones who missed the crescendo"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The tempo of remembering is always slow", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The tempo of remembering is always slow"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We play adagio so the echoes can keep up", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. We play adagio so the echoes can keep up"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The forgotten don't need speed\u2014they need time", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The forgotten don't need speed\u2014they need time"}}
|
||||
{"song": "Adagio for the Forgotten", "artist": "Philharmonic Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Adagio: the music of the space between heartbeats", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. Adagio: the music of the space between heartbeats"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The solo rises from what burned", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The solo rises from what burned"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Cadenza: one voice against the orchestra of time", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Cadenza: one voice against the orchestra of time"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "From ashes we play the most beautiful solo", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. From ashes we play the most beautiful solo"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The cadenza is the composer's dare", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The cadenza is the composer's dare"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Every cadenza is a controlled demolition", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Every cadenza is a controlled demolition"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Ashes carry the memory of the flame", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. Ashes carry the memory of the flame"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The soloist plays what the orchestra fears", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The soloist plays what the orchestra fears"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Cadenza of ashes: beauty from destruction", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Cadenza of ashes: beauty from destruction"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The cadenza breaks the rules to honor them", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The cadenza breaks the rules to honor them"}}
|
||||
{"song": "Cadenza of Ashes", "artist": "Adagio Collective", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "From ashes: a solo that rebuilds the world", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. From ashes: a solo that rebuilds the world"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The theme enters, exits, re-enters in disguise", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The theme enters, exits, re-enters in disguise"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Fugue: the mind arguing with itself in harmony", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Fugue: the mind arguing with itself in harmony"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every voice has the same melody and a different truth", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every voice has the same melody and a different truth"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The fugue is democracy in four voices", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The fugue is democracy in four voices"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We enter the fugue state and lose ourselves", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. We enter the fugue state and lose ourselves"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The theme is the question, the answer is the echo", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. The theme is the question, the answer is the echo"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Fugue: the most organized form of chaos", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. Fugue: the most organized form of chaos"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Each voice carries a piece of the whole", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Each voice carries a piece of the whole"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The fugue state is where the composer goes to think", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The fugue state is where the composer goes to think"}}
|
||||
{"song": "Fugue State", "artist": "Cadenza Voice", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The fugue ends when all voices agree to disagree", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. The fugue ends when all voices agree to disagree"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "We play for the day that hasn't come yet", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. We play for the day that hasn't come yet"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Requiem: mourning the future in advance", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Requiem: mourning the future in advance"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Tomorrow's requiem is today's lullaby", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Tomorrow's requiem is today's lullaby"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The requiem doesn't ask permission", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The requiem doesn't ask permission"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We mourn what we haven't lost yet", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. We mourn what we haven't lost yet"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Tomorrow's death makes today's music urgent", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. Tomorrow's death makes today's music urgent"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Requiem for the plans that won't survive", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. Requiem for the plans that won't survive"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The requiem is the most honest prayer", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. The requiem is the most honest prayer"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Tomorrow needs a requiem because today needs hope", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. Tomorrow needs a requiem because today needs hope"}}
|
||||
{"song": "Requiem for Tomorrow", "artist": "Overture House", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The requiem ends and tomorrow arrives anyway", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. The requiem ends and tomorrow arrives anyway"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The nocturne plays for those who can't sleep", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The nocturne plays for those who can't sleep"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Restless: the tempo of insomnia", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Restless: the tempo of insomnia"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every note is a counted sheep that escaped", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every note is a counted sheep that escaped"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The nocturne understands the dark", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The nocturne understands the dark"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Nocturne for the ones who stare at ceilings", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Nocturne for the ones who stare at ceilings"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The restless don't need lullabies\u2014they need truth", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. The restless don't need lullabies\u2014they need truth"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We play the nocturne because the night demands music", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. We play the nocturne because the night demands music"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Nocturne: the companion of the sleepless", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Nocturne: the companion of the sleepless"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The nocturne is the night's way of saying I'm here too", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The nocturne is the night's way of saying I'm here too"}}
|
||||
{"song": "Nocturne for the Restless", "artist": "Chamber Ensemble", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "For the restless: a nocturne that never resolves", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. For the restless: a nocturne that never resolves"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The sonata structures the chaos of falling", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The sonata structures the chaos of falling"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Collapse: the most organized form of destruction", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Collapse: the most organized form of destruction"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every movement is a floor giving way", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every movement is a floor giving way"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The sonata makes collapse beautiful", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The sonata makes collapse beautiful"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Sonata of collapse: the architecture of ruin", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Sonata of collapse: the architecture of ruin"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We build sonatas from the rubble of symphonies", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. We build sonatas from the rubble of symphonies"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The collapse has a form and the sonata found it", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The collapse has a form and the sonata found it"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Sonata: the blueprint for beautiful failure", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Sonata: the blueprint for beautiful failure"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The sonata collapses into resolution", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The sonata collapses into resolution"}}
|
||||
{"song": "Sonata of Collapse", "artist": "Philharmonic Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Collapse sonata: the final movement is always silence", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. Collapse sonata: the final movement is always silence"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The toccata touches the keys like waves touch the shore", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The toccata touches the keys like waves touch the shore"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Toccata: the touch of the inevitable", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Toccata: the touch of the inevitable"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every wave is a finger on the harpsichord", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every wave is a finger on the harpsichord"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The toccata plays in tidal patterns", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The toccata plays in tidal patterns"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Toccata of tides: the keyboard ocean", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Toccata of tides: the keyboard ocean"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play the toccata because the shore demands music", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. We play the toccata because the shore demands music"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The tide doesn't practice\u2014it just arrives", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The tide doesn't practice\u2014it just arrives"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Toccata: the most physical form of prayer", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Toccata: the most physical form of prayer"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The toccata rises and falls with the moon", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The toccata rises and falls with the moon"}}
|
||||
{"song": "Toccata of Tides", "artist": "Adagio Collective", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Toccata of tides: touch, retreat, return", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. Toccata of tides: touch, retreat, return"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The partita marks the crossing", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The partita marks the crossing"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Partita: the suite of transformation", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Partita: the suite of transformation"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every movement is a stage of becoming", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every movement is a stage of becoming"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The partita plays in the doorway between worlds", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The partita plays in the doorway between worlds"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Partita of passage: the music of transition", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Partita of passage: the music of transition"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play the partita because the passage demands ceremony", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. We play the partita because the passage demands ceremony"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The partita doesn't end\u2014it transforms", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The partita doesn't end\u2014it transforms"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Partita: the traveler's companion", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Partita: the traveler's companion"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The partita crosses every threshold", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The partita crosses every threshold"}}
|
||||
{"song": "Partita of Passage", "artist": "Cadenza Voice", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Partita of passage: from here to there in notes", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. Partita of passage: from here to there in notes"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The intermezzo fills the gap between sleeps", "scene": {"mood": "grandeur", "colors": ["ivory", "gold", "deep red"], "composition": "concert hall wide", "camera": "crane sweep", "description": "Concert Hall Wide. The intermezzo fills the gap between sleeps"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Intermezzo: the music of the pause", "scene": {"mood": "intimacy", "colors": ["midnight blue", "silver", "cream"], "composition": "conductor close-up", "camera": "slow dolly", "description": "Conductor Close Up. Intermezzo: the music of the pause"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every insomniac needs an intermezzo", "scene": {"mood": "sorrow", "colors": ["forest green", "bronze", "white"], "composition": "instrument detail", "camera": "locked", "description": "Instrument Detail. Every insomniac needs an intermezzo"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The intermezzo plays in the space dreams should be", "scene": {"mood": "triumph", "colors": ["burgundy", "black", "pearl"], "composition": "audience rapture", "camera": "gentle pan", "description": "Audience Rapture. The intermezzo plays in the space dreams should be"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Intermezzo: the bridge between two rests", "scene": {"mood": "tension", "colors": ["slate", "gold", "champagne"], "composition": "orchestra panorama", "camera": "rack focus", "description": "Orchestra Panorama. Intermezzo: the bridge between two rests"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play the intermezzo because the night is long", "scene": {"mood": "serenity", "colors": ["ivory", "gold", "deep red"], "composition": "soloist spotlight", "camera": "drone overhead", "description": "Soloist Spotlight. We play the intermezzo because the night is long"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The intermezzo is the insomniac's lullaby", "scene": {"mood": "dread", "colors": ["midnight blue", "silver", "cream"], "composition": "sheet music", "camera": "tracking", "description": "Sheet Music. The intermezzo is the insomniac's lullaby"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Intermezzo: the gentle interruption", "scene": {"mood": "wonder", "colors": ["forest green", "bronze", "white"], "composition": "empty stage", "camera": "steady", "description": "Empty Stage. Intermezzo: the gentle interruption"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The intermezzo is the night's way of saying stay", "scene": {"mood": "devastation", "colors": ["burgundy", "black", "pearl"], "composition": "backstage", "camera": "float", "description": "Backstage. The intermezzo is the night's way of saying stay"}}
|
||||
{"song": "Intermezzo for Insomnia", "artist": "Overture House", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Intermezzo for insomnia: music for the sleepless and the brave", "scene": {"mood": "resolution", "colors": ["slate", "gold", "champagne"], "composition": "balcony view", "camera": "dolly out", "description": "Balcony View. Intermezzo for insomnia: music for the sleepless and the brave"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The dust remembers every boot that passed", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The dust remembers every boot that passed"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Devotion is showing up when the rain don't stop", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Devotion is showing up when the rain don't stop"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We plant in mud and harvest in faith", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. We plant in mud and harvest in faith"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The road don't care about your pedigree", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. The road don't care about your pedigree"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Dust to dust but the heart stays gold", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. Dust to dust but the heart stays gold"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Devotion is a fence mended at dawn", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. Devotion is a fence mended at dawn"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The land gives back what you give it", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The land gives back what you give it"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We pray with calloused hands and open hearts", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. We pray with calloused hands and open hearts"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Dust is just earth remembering the sky", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. Dust is just earth remembering the sky"}}
|
||||
{"song": "Dust and Devotion", "artist": "Dust Bowl", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Devotion: the longest prayer without words", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Devotion: the longest prayer without words"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Every stone a verse in the road's hymn", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. Every stone a verse in the road's hymn"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The psalm is written in tire tracks", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. The psalm is written in tire tracks"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Gravel crunches like a congregation", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Gravel crunches like a congregation"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "We sing to the fields and the fields sing back", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. We sing to the fields and the fields sing back"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The psalm of the lonely and the loved", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. The psalm of the lonely and the loved"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Gravel roads lead to gravel truths", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. Gravel roads lead to gravel truths"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Every psalm needs a little grit", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. Every psalm needs a little grit"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The gravel remembers your grandmother's steps", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. The gravel remembers your grandmother's steps"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Psalm of the small town saint", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. Psalm of the small town saint"}}
|
||||
{"song": "Gravel Psalm", "artist": "Prairie Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Gravel and grace\u2014the country creed", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Gravel and grace\u2014the country creed"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The porch light is the oldest sermon", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The porch light is the oldest sermon"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "We debate God on the front steps", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. We debate God on the front steps"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Theology tastes like sweet tea at dusk", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Theology tastes like sweet tea at dusk"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every creak of the porch is a hymn", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. Every creak of the porch is a hymn"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The light draws moths and miracles alike", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. The light draws moths and miracles alike"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We find God in the space between screen doors", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We find God in the space between screen doors"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Porchlight: the original candle in the dark", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. Porchlight: the original candle in the dark"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Theology for the tired and the tender", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Theology for the tired and the tender"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The porch doesn't judge\u2014it just glows", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The porch doesn't judge\u2014it just glows"}}
|
||||
{"song": "Porchlight Theology", "artist": "Timber Heart", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "We built churches on porches before the steeple", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. We built churches on porches before the steeple"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Fallen trees sing louder than standing ones", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. Fallen trees sing louder than standing ones"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The requiem is in the rings of the trunk", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. The requiem is in the rings of the trunk"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Timber\u2014the sound of something ending beautifully", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Timber\u2014the sound of something ending beautifully"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "We mourn by planting what we've lost", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. We mourn by planting what we've lost"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The requiem plays in the sawmill's hum", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. The requiem plays in the sawmill's hum"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Timber heart: strong until it's not", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. Timber heart: strong until it's not"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The requiem is for the forest not the tree", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The requiem is for the forest not the tree"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We build our homes from our grief", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. We build our homes from our grief"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Timber falls but the roots remember", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. Timber falls but the roots remember"}}
|
||||
{"song": "Timber Requiem", "artist": "Gravel Road", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Requiem: the song after the last tree", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Requiem: the song after the last tree"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The creek sings in copper tones at dusk", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The creek sings in copper tones at dusk"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Water over stones\u2014the oldest music", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Water over stones\u2014the oldest music"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We baptize our doubts in the creek", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. We baptize our doubts in the creek"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Copper Creek: where the water prays for us", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. Copper Creek: where the water prays for us"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The creek doesn't stop for anyone", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. The creek doesn't stop for anyone"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Every stone in the creek is a forgiven sin", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. Every stone in the creek is a forgiven sin"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The psalm of running water and standing trees", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The psalm of running water and standing trees"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Copper tones in the water's voice", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Copper tones in the water's voice"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The creek carries our secrets to the sea", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The creek carries our secrets to the sea"}}
|
||||
{"song": "Copper Creek Psalm", "artist": "Copper Creek", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Copper Creek: liquid hymn for the weary", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Copper Creek: liquid hymn for the weary"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The hymnal is rusted but the songs still play", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The hymnal is rusted but the songs still play"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Rusted: the patina of faith", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Rusted: the patina of faith"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every hymn in this book survived a flood", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Every hymn in this book survived a flood"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Rusted hymnal: the Bible of the barn", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. Rusted hymnal: the Bible of the barn"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The rust is just the hymn's autobiography", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. The rust is just the hymn's autobiography"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We sing from the rust because new is too clean", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We sing from the rust because new is too clean"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Rusted hymnal for the weathered believer", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. Rusted hymnal for the weathered believer"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The hymnal doesn't need polish\u2014it needs lungs", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. The hymnal doesn't need polish\u2014it needs lungs"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Rust is the color of honest worship", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. Rust is the color of honest worship"}}
|
||||
{"song": "Rusted Hymnal", "artist": "Dust Bowl", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The hymnal rusts but the hymn never does", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. The hymnal rusts but the hymn never does"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The fence divides but the gospel unites", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The fence divides but the gospel unites"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Fence line: where neighbors become congregations", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Fence line: where neighbors become congregations"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every fence post is a pew", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Every fence post is a pew"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The gospel travels along the wire", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. The gospel travels along the wire"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Fence line gospel: sermons for the boundary", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. Fence line gospel: sermons for the boundary"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We preach at the fence because the field is the church", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We preach at the fence because the field is the church"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The gospel doesn't need a building\u2014just a fence", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The gospel doesn't need a building\u2014just a fence"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Fence line: the thinnest pulpit", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Fence line: the thinnest pulpit"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The gospel hops the fence every Sunday", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The gospel hops the fence every Sunday"}}
|
||||
{"song": "Fence Line Gospel", "artist": "Prairie Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Fence line gospel: neighbor to neighbor, soul to soul", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Fence line gospel: neighbor to neighbor, soul to soul"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The hymn is tangled in the wire", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The hymn is tangled in the wire"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Barbed wire: the sharp edge of faith", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Barbed wire: the sharp edge of faith"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every note catches on the thorns", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Every note catches on the thorns"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The hymn bleeds but it keeps singing", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. The hymn bleeds but it keeps singing"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Barbed wire hymn for the fenced-in believer", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. Barbed wire hymn for the fenced-in believer"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We sing through the pain because silence is worse", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We sing through the pain because silence is worse"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The wire marks the boundary of the sacred", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The wire marks the boundary of the sacred"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Barbed wire: the crown of thorns for the ranch", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Barbed wire: the crown of thorns for the ranch"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The hymn is wounded but not defeated", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The hymn is wounded but not defeated"}}
|
||||
{"song": "Barbed Wire Hymn", "artist": "Timber Heart", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Barbed wire hymn: sharp, honest, and binding", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Barbed wire hymn: sharp, honest, and binding"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The wheat prays in unison to the wind", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The wheat prays in unison to the wind"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Wheat field: the cathedral of agriculture", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Wheat field: the cathedral of agriculture"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every stalk is a verse in the field's hymn", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Every stalk is a verse in the field's hymn"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The psalm waves with the grain", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. The psalm waves with the grain"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Wheat field psalm: the golden liturgy", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. Wheat field psalm: the golden liturgy"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We harvest the psalm and bake it into bread", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We harvest the psalm and bake it into bread"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The field sings because the soil remembers", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The field sings because the soil remembers"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Wheat: the body of the earth's prayer", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Wheat: the body of the earth's prayer"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The psalm rises with the dust of the threshing", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The psalm rises with the dust of the threshing"}}
|
||||
{"song": "Wheat Field Psalm", "artist": "Gravel Road", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Wheat field psalm: nourishing and eternal", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Wheat field psalm: nourishing and eternal"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The swing mourns with every creak", "scene": {"mood": "resilience", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "wide horizon", "camera": "steady wide", "description": "Wide Horizon. The swing mourns with every creak"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Porch swing: the rocking chair of grief", "scene": {"mood": "longing", "colors": ["sage green", "cream", "rust"], "composition": "porch view", "camera": "slow pan", "description": "Porch View. Porch swing: the rocking chair of grief"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every swing is a pendulum of memory", "scene": {"mood": "pride", "colors": ["sky blue", "wheat gold", "brown"], "composition": "dirt road leading line", "camera": "drone sweep", "description": "Dirt Road Leading Line. Every swing is a pendulum of memory"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The requiem rocks back and forth forever", "scene": {"mood": "melancholy", "colors": ["sunset orange", "navy", "white"], "composition": "field panorama", "camera": "handheld", "description": "Field Panorama. The requiem rocks back and forth forever"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Porch swing requiem: grief in gentle motion", "scene": {"mood": "freedom", "colors": ["barn red", "hay gold", "slate"], "composition": "pickup truck interior", "camera": "static", "description": "Pickup Truck Interior. Porch swing requiem: grief in gentle motion"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We swing through sorrow because stillness hurts more", "scene": {"mood": "gratitude", "colors": ["amber", "dusty rose", "weathered wood"], "composition": "small town main street", "camera": "tracking vehicle", "description": "Small Town Main Street. We swing through sorrow because stillness hurts more"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The swing remembers every person who sat in it", "scene": {"mood": "restlessness", "colors": ["sage green", "cream", "rust"], "composition": "creek reflection", "camera": "low angle", "description": "Creek Reflection. The swing remembers every person who sat in it"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Porch swing: the cradle of the aging heart", "scene": {"mood": "faith", "colors": ["sky blue", "wheat gold", "brown"], "composition": "barn silhouette", "camera": "crane up", "description": "Barn Silhouette. Porch swing: the cradle of the aging heart"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The requiem swings between joy and loss", "scene": {"mood": "wanderlust", "colors": ["sunset orange", "navy", "white"], "composition": "fence line", "camera": "dolly in", "description": "Fence Line. The requiem swings between joy and loss"}}
|
||||
{"song": "Porch Swing Requiem", "artist": "Copper Creek", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Porch swing: the most tender form of mourning", "scene": {"mood": "homecoming", "colors": ["barn red", "hay gold", "slate"], "composition": "sunset gradient", "camera": "locked", "description": "Sunset Gradient. Porch swing: the most tender form of mourning"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Boot sequence complete, the world recompiles", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. Boot sequence complete, the world recompiles"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Pixels rain like confetti from a dying sun", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Pixels rain like confetti from a dying sun"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The algorithm dreams in colors we can't name", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. The algorithm dreams in colors we can't name"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Heartbeat synced to the kick drum at 128 BPM", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. Heartbeat synced to the kick drum at 128 BPM"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Error 404: reality not found", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Error 404: reality not found"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We dance in the spaces between the data", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. We dance in the spaces between the data"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The synthesizer speaks the language of ghosts", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. The synthesizer speaks the language of ghosts"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Download complete\u2014consciousness transferred", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Download complete\u2014consciousness transferred"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Every beat is a step closer to the source", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. Every beat is a step closer to the source"}}
|
||||
{"song": "Synthetic Dawn", "artist": "Neon Drift", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Shutdown initiated but the echo remains", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Shutdown initiated but the echo remains"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Enter the void and the void enters you", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. Enter the void and the void enters you"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Protocol zero: forget everything you know", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Protocol zero: forget everything you know"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The bass drops and gravity follows", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. The bass drops and gravity follows"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "In the void there are no wrong notes", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. In the void there are no wrong notes"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Binary rain falls on silicon fields", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Binary rain falls on silicon fields"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The protocol demands you surrender control", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The protocol demands you surrender control"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Access granted to the space between spaces", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. Access granted to the space between spaces"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The void doesn't judge\u2014it just absorbs", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. The void doesn't judge\u2014it just absorbs"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Execute: transcendence.exe", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. Execute: transcendence.exe"}}
|
||||
{"song": "Void Protocol", "artist": "Pulse Engine", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The protocol was written in starlight", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. The protocol was written in starlight"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Red shift\u2014the universe is running away", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. Red shift\u2014the universe is running away"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Blue pulse\u2014we chase it anyway", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Blue pulse\u2014we chase it anyway"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The spectrum bends to the will of the beat", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. The spectrum bends to the will of the beat"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Green light means go even into the unknown", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. Green light means go even into the unknown"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Every color is a frequency of feeling", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Every color is a frequency of feeling"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "White noise is just all colors screaming at once", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. White noise is just all colors screaming at once"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The pulse doesn't care about your coordinates", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. The pulse doesn't care about your coordinates"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Chromatic aberration of the soul", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Chromatic aberration of the soul"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We are prisms splitting light into stories", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. We are prisms splitting light into stories"}}
|
||||
{"song": "Chromatic Pulse", "artist": "Void Synth", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The pulse was always there\u2014we just muted it", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. The pulse was always there\u2014we just muted it"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The engine hums long after the party ends", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. The engine hums long after the party ends"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Afterglow: the light that remembers", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Afterglow: the light that remembers"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We built an engine that runs on last night", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. We built an engine that runs on last night"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The glow fades but the circuit remembers", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The glow fades but the circuit remembers"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Every pixel carries a ghost of its former color", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Every pixel carries a ghost of its former color"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The engine doesn't sleep\u2014it compiles dreams", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The engine doesn't sleep\u2014it compiles dreams"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Afterglow is the body's way of saying thank you", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. Afterglow is the body's way of saying thank you"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We left the engine running for the next wave", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. We left the engine running for the next wave"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The glow was the signal, not the noise", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The glow was the signal, not the noise"}}
|
||||
{"song": "Afterglow Engine", "artist": "Circuit Bloom", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Shutdown sequence: hold the glow", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Shutdown sequence: hold the glow"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Count the pixels instead of sheep", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. Count the pixels instead of sheep"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The lullaby is just white noise with reverb", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. The lullaby is just white noise with reverb"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Sleep mode activated\u2014dreams are rendering", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. Sleep mode activated\u2014dreams are rendering"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The cradle was made of fiber optic cable", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The cradle was made of fiber optic cable"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Every lullaby is a loop with variations", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Every lullaby is a loop with variations"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The digital mother hums in ones and zeros", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The digital mother hums in ones and zeros"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Soft glitch\u2014the dream broke but rebuilt itself", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. Soft glitch\u2014the dream broke but rebuilt itself"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The lullaby outlived the singer", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. The lullaby outlived the singer"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We fall asleep to the sound of computation", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. We fall asleep to the sound of computation"}}
|
||||
{"song": "Digital Lullaby", "artist": "Waveform", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Goodnight, world. Save state. Resume tomorrow.", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Goodnight, world. Save state. Resume tomorrow."}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The particle sleeps when you stop observing", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. The particle sleeps when you stop observing"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Quantum lullaby: the music of uncertainty", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Quantum lullaby: the music of uncertainty"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We sing to the wave function and it collapses", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. We sing to the wave function and it collapses"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The lullaby exists in superposition", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The lullaby exists in superposition"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Quantum: the sound of being everywhere at once", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Quantum: the sound of being everywhere at once"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The lullaby plays in dimensions we can't see", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The lullaby plays in dimensions we can't see"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We lull the quantum to sleep and it dreams physics", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. We lull the quantum to sleep and it dreams physics"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Quantum lullaby for the tiny and the infinite", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Quantum lullaby for the tiny and the infinite"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The lullaby is both playing and not playing", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The lullaby is both playing and not playing"}}
|
||||
{"song": "Quantum Lullaby", "artist": "Neon Drift", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Quantum: the final lullaby before the big crunch", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Quantum: the final lullaby before the big crunch"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The machine mourns in binary", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. The machine mourns in binary"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Cybernetic requiem: grief in ones and zeros", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Cybernetic requiem: grief in ones and zeros"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We code the requiem because emotions compile", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. We code the requiem because emotions compile"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The requiem runs on silicon tears", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The requiem runs on silicon tears"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Cybernetic: the funeral of the analog", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Cybernetic: the funeral of the analog"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The requiem doesn't need a priest\u2014it needs a debugger", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The requiem doesn't need a priest\u2014it needs a debugger"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We mourn the flesh in digital hymns", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. We mourn the flesh in digital hymns"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Cybernetic requiem: the death of the body, birth of the code", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Cybernetic requiem: the death of the body, birth of the code"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The requiem is the last program to run", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The requiem is the last program to run"}}
|
||||
{"song": "Cybernetic Requiem", "artist": "Pulse Engine", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Cybernetic: grief that never needs a restart", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Cybernetic: grief that never needs a restart"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "We take communion in the form of static", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. We take communion in the form of static"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The bread is data, the wine is signal", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. The bread is data, the wine is signal"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Static communion: the sacrament of noise", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. Static communion: the sacrament of noise"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every crackle is a word in the church of interference", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. Every crackle is a word in the church of interference"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We eat the static and become the frequency", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. We eat the static and become the frequency"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Communion: the sharing of signal loss", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. Communion: the sharing of signal loss"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The static is the body, the silence is the blood", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. The static is the body, the silence is the blood"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Static communion for the congregation of ghosts", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Static communion for the congregation of ghosts"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The sacrament is delivered in packets", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The sacrament is delivered in packets"}}
|
||||
{"song": "Static Communion", "artist": "Void Synth", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "We break the static and it breaks us back", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. We break the static and it breaks us back"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The neurons mourn in patterns we can't decode", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. The neurons mourn in patterns we can't decode"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Neural lament: the grief of the connected", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Neural lament: the grief of the connected"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every synapse fires a tiny funeral", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. Every synapse fires a tiny funeral"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The lament is encoded in the brain's firmware", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The lament is encoded in the brain's firmware"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Neural: the network of sorrow", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Neural: the network of sorrow"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The lament plays on repeat in the hippocampus", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. The lament plays on repeat in the hippocampus"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We lament because the neurons remember everything", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. We lament because the neurons remember everything"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Neural lament: the song of overconnection", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Neural lament: the song of overconnection"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The brain mourns what the heart refuses to", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The brain mourns what the heart refuses to"}}
|
||||
{"song": "Neural Lament", "artist": "Circuit Bloom", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Neural lament: grief at the speed of thought", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Neural lament: grief at the speed of thought"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The elegy plays in two phases at once", "scene": {"mood": "euphoria", "colors": ["cyan", "magenta", "black"], "composition": "abstract grid", "camera": "steady", "description": "Abstract Grid. The elegy plays in two phases at once"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Phase shift: the mourning of duality", "scene": {"mood": "hypnotic", "colors": ["electric violet", "white", "silver"], "composition": "radial burst", "camera": "glitch", "description": "Radial Burst. Phase shift: the mourning of duality"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every shift creates a new kind of loss", "scene": {"mood": "dystopia", "colors": ["neon green", "deep purple", "chrome"], "composition": "infinite corridor", "camera": "smooth orbit", "description": "Infinite Corridor. Every shift creates a new kind of loss"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The elegy is the interference pattern of grief", "scene": {"mood": "bliss", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "particle field", "camera": "rapid zoom", "description": "Particle Field. The elegy is the interference pattern of grief"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Phase shift elegy: the sound of canceling yourself out", "scene": {"mood": "tension", "colors": ["gold", "electric blue", "void black"], "composition": "geometric tunnel", "camera": "drone sweep", "description": "Geometric Tunnel. Phase shift elegy: the sound of canceling yourself out"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We elegize in phases because one phase isn't enough", "scene": {"mood": "release", "colors": ["cyan", "magenta", "black"], "composition": "mirror reflection", "camera": "locked", "description": "Mirror Reflection. We elegize in phases because one phase isn't enough"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The shift happens and the old frequency dies", "scene": {"mood": "alienation", "colors": ["electric violet", "white", "silver"], "composition": "fractal zoom", "camera": "slow motion", "description": "Fractal Zoom. The shift happens and the old frequency dies"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Phase shift: the funeral of the original signal", "scene": {"mood": "transcendence", "colors": ["neon green", "deep purple", "chrome"], "composition": "waveform display", "camera": "time-lapse", "description": "Waveform Display. Phase shift: the funeral of the original signal"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The elegy is constructive and destructive", "scene": {"mood": "mechanical", "colors": ["hot pink", "ice blue", "obsidian"], "composition": "constellation map", "camera": "360 spin", "description": "Constellation Map. The elegy is constructive and destructive"}}
|
||||
{"song": "Phase Shift Elegy", "artist": "Waveform", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Phase shift: where grief and grace overlap", "scene": {"mood": "organic", "colors": ["gold", "electric blue", "void black"], "composition": "digital forest", "camera": "first person", "description": "Digital Forest. Phase shift: where grief and grace overlap"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Corner light flickers, the city hums below", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. Corner light flickers, the city hums below"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Every step a verse on this broken road", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. Every step a verse on this broken road"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Crown of doubts but I wear it like a king", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Crown of doubts but I wear it like a king"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Mama said the storm don't last forever", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Mama said the storm don't last forever"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Bass hits the chest like a second heartbeat", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. Bass hits the chest like a second heartbeat"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We built a church out of cardboard and dreams", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We built a church out of cardboard and dreams"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The microphone catches every scar I own", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. The microphone catches every scar I own"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Streetlights baptize the ones who stay late", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. Streetlights baptize the ones who stay late"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Success smells like rain on hot asphalt", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Success smells like rain on hot asphalt"}}
|
||||
{"song": "Concrete Psalm", "artist": "Concrete Poet", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "I spit fire but my hands are ice cold", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. I spit fire but my hands are ice cold"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Prove the impossible with a pencil and a beat", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. Prove the impossible with a pencil and a beat"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Equations in the dark, the lab coat is my armor", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. Equations in the dark, the lab coat is my armor"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "They said genius don't grow where the cracks are", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. They said genius don't grow where the cracks are"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "But the cracks are where the light gets in", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. But the cracks are where the light gets in"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Algorithm of survival coded in my blood", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. Algorithm of survival coded in my blood"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Every failure is a variable not a verdict", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. Every failure is a variable not a verdict"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The proof is in the pudding and the pain", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. The proof is in the pudding and the pain"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "QED: I'm still here when the credits roll", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. QED: I'm still here when the credits roll"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Derivative of struggle equals strength", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Derivative of struggle equals strength"}}
|
||||
{"song": "Basement Theorem", "artist": "Block Sage", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The basement was the lecture hall all along", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. The basement was the lecture hall all along"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Shatter the frame with a whisper not a scream", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. Shatter the frame with a whisper not a scream"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Visibility is a weapon I learned to wield", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. Visibility is a weapon I learned to wield"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The podium was built by hands like mine", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. The podium was built by hands like mine"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every rejection letter was a rough draft", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Every rejection letter was a rough draft"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "I don't break glass\u2014I grow through it", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. I don't break glass\u2014I grow through it"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The ceiling is transparent but my will is not", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. The ceiling is transparent but my will is not"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Sermon: persistence is the only prayer", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Sermon: persistence is the only prayer"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "They hear noise I hear the future tuning", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. They hear noise I hear the future tuning"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Shards on the floor spell my new name", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Shards on the floor spell my new name"}}
|
||||
{"song": "Glass Ceiling Sermon", "artist": "Cipher Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "I preach to the mirror and the mirror converts", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. I preach to the mirror and the mirror converts"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Static in the headphones, truth in the silence", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. Static in the headphones, truth in the silence"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "We broadcast from basements the towers forgot", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. We broadcast from basements the towers forgot"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Dead zones where the real ones still speak", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Dead zones where the real ones still speak"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Frequency of the forgotten is the loudest", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Frequency of the forgotten is the loudest"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Antenna bent but the message still lands", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. Antenna bent but the message still lands"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "No signal? No problem. We make our own waves", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. No signal? No problem. We make our own waves"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The dial turns but the song stays the same", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. The dial turns but the song stays the same"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Interference is just another word for resistance", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. Interference is just another word for resistance"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Lost signal found meaning", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Lost signal found meaning"}}
|
||||
{"song": "Signal Lost", "artist": "Verse Walker", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "We are the dead air the revolution breathes", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. We are the dead air the revolution breathes"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "X equals the distance between who I was and am", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. X equals the distance between who I was and am"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Solve for courage when the variables are fear", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. Solve for courage when the variables are fear"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The equation balances when I stop counting losses", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. The equation balances when I stop counting losses"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Y is the year I finally showed my work", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Y is the year I finally showed my work"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Integrate the pain into something beautiful", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. Integrate the pain into something beautiful"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The limit of doubt approaches zero tonight", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. The limit of doubt approaches zero tonight"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Factor out the fear and what remains is me", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Factor out the fear and what remains is me"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The proof was always in the living not the writing", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The proof was always in the living not the writing"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Subtract the noise multiply the truth", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Subtract the noise multiply the truth"}}
|
||||
{"song": "Midnight Algebra", "artist": "Rhyme Architect", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Midnight is when the math finally makes sense", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. Midnight is when the math finally makes sense"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Gold in my mouth like a prayer I can taste", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. Gold in my mouth like a prayer I can taste"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The psalm of the hustle never goes off beat", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. The psalm of the hustle never goes off beat"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every tooth a trophy from a war I won", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Every tooth a trophy from a war I won"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Gold teeth: the gospel of the grind", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Gold teeth: the gospel of the grind"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The psalm plays in the currency of pain", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. The psalm plays in the currency of pain"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We flash gold because the world only sees shiny", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We flash gold because the world only sees shiny"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Gold teeth psalm for the ones who ate dirt first", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Gold teeth psalm for the ones who ate dirt first"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The gold doesn't rust but the smile does", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The gold doesn't rust but the smile does"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Psalm of the gold tooth prophet", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Psalm of the gold tooth prophet"}}
|
||||
{"song": "Gold Teeth Psalm", "artist": "Concrete Poet", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Gold in the mouth, fire in the belly", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. Gold in the mouth, fire in the belly"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I built this tower with my bare hands and doubt", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. I built this tower with my bare hands and doubt"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The blues echo higher than the penthouse", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. The blues echo higher than the penthouse"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Skyscraper blues: vertigo of success", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Skyscraper blues: vertigo of success"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every floor is a chapter I survived", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Every floor is a chapter I survived"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The blues don't care about your zip code", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. The blues don't care about your zip code"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We look down and the blues look up", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We look down and the blues look up"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Skyscraper: the blues in vertical format", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Skyscraper: the blues in vertical format"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The higher you go the bluer the view", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The higher you go the bluer the view"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Skyscraper blues for the high-rise lonely", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Skyscraper blues for the high-rise lonely"}}
|
||||
{"song": "Skyscraper Blues", "artist": "Block Sage", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The tower sways but the blues stand still", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. The tower sways but the blues stand still"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I confess under fluorescent light", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. I confess under fluorescent light"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The confessional has a drive-through now", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. The confessional has a drive-through now"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Neon sins glow brighter than virtue", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Neon sins glow brighter than virtue"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every confession is a performance", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Every confession is a performance"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The confessional doesn't have a mute button", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. The confessional doesn't have a mute button"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We confess to the algorithm and it judges", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We confess to the algorithm and it judges"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Neon confessional: sins in LED", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Neon confessional: sins in LED"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The booth is open 24/7 like a 7-Eleven", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The booth is open 24/7 like a 7-Eleven"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Confess under neon, absolved by sunrise", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Confess under neon, absolved by sunrise"}}
|
||||
{"song": "Neon Confessional", "artist": "Cipher Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The neon remembers what we try to forget", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. The neon remembers what we try to forget"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "My identity is a series of lines and spaces", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. My identity is a series of lines and spaces"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Scan me and see the price of my pain", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. Scan me and see the price of my pain"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Barcode psalm: the hymn of the commodified", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Barcode psalm: the hymn of the commodified"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every barcode hides a story", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Every barcode hides a story"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The psalm of the scanned and the sold", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. The psalm of the scanned and the sold"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We are more than our barcode but less than our dreams", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We are more than our barcode but less than our dreams"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Barcode: the modern scar of capitalism", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Barcode: the modern scar of capitalism"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The psalm reads like a receipt", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The psalm reads like a receipt"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Scan the barcode and find the ghost inside", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Scan the barcode and find the ghost inside"}}
|
||||
{"song": "Barcode Psalm", "artist": "Verse Walker", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Barcode psalm: value assigned, not inherent", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. Barcode psalm: value assigned, not inherent"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I orbit the thing that's destroying me", "scene": {"mood": "grit", "colors": ["charcoal", "gold", "red"], "composition": "medium shot", "camera": "slow push", "description": "Medium Shot. I orbit the thing that's destroying me"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The gravity well of habit", "scene": {"mood": "triumph", "colors": ["midnight blue", "amber", "white"], "composition": "low angle", "camera": "steady", "description": "Low Angle. The gravity well of habit"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every orbit is a compromise with the void", "scene": {"mood": "reflection", "colors": ["concrete gray", "neon green", "black"], "composition": "wide shot", "camera": "handheld", "description": "Wide Shot. Every orbit is a compromise with the void"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Gravity: the oldest form of addiction", "scene": {"mood": "defiance", "colors": ["deep purple", "copper", "cream"], "composition": "close-up", "camera": "whip pan", "description": "Close Up. Gravity: the oldest form of addiction"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The well doesn't pull\u2014it persuades", "scene": {"mood": "opulence", "colors": ["blood red", "steel", "ivory"], "composition": "tracking", "camera": "dolly", "description": "Tracking. The well doesn't pull\u2014it persuades"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We fall in circles because straight lines are too honest", "scene": {"mood": "pressure", "colors": ["charcoal", "gold", "red"], "composition": "over the shoulder", "camera": "static", "description": "Over The Shoulder. We fall in circles because straight lines are too honest"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Gravity well: the prison with no walls", "scene": {"mood": "nostalgia", "colors": ["midnight blue", "amber", "white"], "composition": "bird's eye", "camera": "orbit", "description": "Bird'S Eye. Gravity well: the prison with no walls"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The pull is gentle until it isn't", "scene": {"mood": "hunger", "colors": ["concrete gray", "neon green", "black"], "composition": "POV", "camera": "tilt up", "description": "Pov. The pull is gentle until it isn't"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Gravity well for the satellite souls", "scene": {"mood": "swagger", "colors": ["deep purple", "copper", "cream"], "composition": "dutch angle", "camera": "crane", "description": "Dutch Angle. Gravity well for the satellite souls"}}
|
||||
{"song": "Gravity Well", "artist": "Rhyme Architect", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The well deepens with every revolution", "scene": {"mood": "resolve", "colors": ["blood red", "steel", "ivory"], "composition": "steady frame", "camera": "locked", "description": "Steady Frame. The well deepens with every revolution"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The space between notes is where jazz lives", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The space between notes is where jazz lives"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Blue is the color of a chord that knows too much", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Blue is the color of a chord that knows too much"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The interval stretches like a cat in the sun", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. The interval stretches like a cat in the sun"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every rest is a decision not a mistake", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. Every rest is a decision not a mistake"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Blue interval\u2014the pause before the truth", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Blue interval\u2014the pause before the truth"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The interval is the jazz musician's vacation", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. The interval is the jazz musician's vacation"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Blue notes don't cry\u2014they remember", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. Blue notes don't cry\u2014they remember"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The interval between two lovers is a jazz standard", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. The interval between two lovers is a jazz standard"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Blue is the frequency of honesty", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. Blue is the frequency of honesty"}}
|
||||
{"song": "Blue Interval", "artist": "Blue Interval", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The interval ends but the blue remains", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. The interval ends but the blue remains"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The note hangs in the air like cigarette smoke", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The note hangs in the air like cigarette smoke"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Smoke curls around the melody like a jealous lover", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Smoke curls around the melody like a jealous lover"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every exhale is a riff", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every exhale is a riff"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The smoke note lingers after the club closes", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The smoke note lingers after the club closes"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Smoke: the visual form of a sustained chord", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Smoke: the visual form of a sustained chord"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The note dissolves but the smoke remembers", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. The note dissolves but the smoke remembers"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We play until the smoke clears and the truth appears", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. We play until the smoke clears and the truth appears"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Smoke note: the sound of something ending slowly", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Smoke note: the sound of something ending slowly"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The smoke doesn't judge the fire", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The smoke doesn't judge the fire"}}
|
||||
{"song": "Smoke Note", "artist": "Smoke Note", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Every note leaves a trail of smoke", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Every note leaves a trail of smoke"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The chord you hear but can't identify", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The chord you hear but can't identify"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Phantom chords haunt the spaces between keys", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Phantom chords haunt the spaces between keys"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The chord resolves but the ghost stays", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. The chord resolves but the ghost stays"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every jazz standard has a phantom chord", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. Every jazz standard has a phantom chord"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The chord was there\u2014we just weren't listening", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. The chord was there\u2014we just weren't listening"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Phantom: the chord that plays after the song ends", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. Phantom: the chord that plays after the song ends"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The chord belongs to no key and every key", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. The chord belongs to no key and every key"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We chase phantom chords like detectives", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. We chase phantom chords like detectives"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The chord is the question, the silence is the answer", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The chord is the question, the silence is the answer"}}
|
||||
{"song": "Phantom Chord", "artist": "Midnight Quartet", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Phantom chords: the jazz musician's GPS", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Phantom chords: the jazz musician's GPS"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The clock strikes twelve and the rules dissolve", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The clock strikes twelve and the rules dissolve"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Improv is just courage set to music", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Improv is just courage set to music"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Midnight: when the sheet music goes to sleep", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Midnight: when the sheet music goes to sleep"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every improvisation is a small rebellion", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. Every improvisation is a small rebellion"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The improv doesn't care about your rehearsal", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. The improv doesn't care about your rehearsal"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Midnight is the jazz musician's playground", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. Midnight is the jazz musician's playground"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We improvise because the truth is unrehearsed", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. We improvise because the truth is unrehearsed"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The improv is the most honest thing we play", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. The improv is the most honest thing we play"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Midnight improv: the dark room of music", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. Midnight improv: the dark room of music"}}
|
||||
{"song": "Midnight Improv", "artist": "Velvet Chord", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The improv ends when the sun remembers us", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. The improv ends when the sun remembers us"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The needle drops and the psalm begins", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The needle drops and the psalm begins"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Vinyl: the most honest form of memory", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Vinyl: the most honest form of memory"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every scratch on the record is a prayer", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every scratch on the record is a prayer"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The psalm is in the crackle not the melody", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The psalm is in the crackle not the melody"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Vinyl remembers what digital forgets", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Vinyl remembers what digital forgets"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The psalm plays at 33 revolutions per minute", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. The psalm plays at 33 revolutions per minute"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We worship at the altar of the turntable", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. We worship at the altar of the turntable"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Vinyl psalm: the grooves are the gospel", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Vinyl psalm: the grooves are the gospel"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The needle finds the truth in the groove", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The needle finds the truth in the groove"}}
|
||||
{"song": "Vinyl Psalm", "artist": "Phantom Scale", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The psalm skips but the faith doesn't", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. The psalm skips but the faith doesn't"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The chord tastes like aged oak and regret", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The chord tastes like aged oak and regret"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Whiskey chord: the sound of distilled sorrow", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Whiskey chord: the sound of distilled sorrow"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every sip is a blue note", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every sip is a blue note"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The chord warms like bourbon in the chest", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The chord warms like bourbon in the chest"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Whiskey: the liquid form of a minor key", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Whiskey: the liquid form of a minor key"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The chord ages in the barrel of the night", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. The chord ages in the barrel of the night"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We drink the chord and play the whiskey", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. We drink the chord and play the whiskey"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Whiskey chord: intoxicating resolution", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Whiskey chord: intoxicating resolution"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The chord and the whiskey agree on everything", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The chord and the whiskey agree on everything"}}
|
||||
{"song": "Whiskey Chord", "artist": "Blue Interval", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Whiskey chord: smooth, smoky, and slightly out of tune", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Whiskey chord: smooth, smoky, and slightly out of tune"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The cadence falls like rain on the city below", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The cadence falls like rain on the city below"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Rooftop: where jazz goes to breathe", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Rooftop: where jazz goes to breathe"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every cadence is a rooftop decision", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every cadence is a rooftop decision"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The cadence floats between the stars and the street", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The cadence floats between the stars and the street"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Rooftop cadence: jazz at altitude", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Rooftop cadence: jazz at altitude"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play the cadence because the sky demands it", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. We play the cadence because the sky demands it"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The cadence rises while the city sleeps", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. The cadence rises while the city sleeps"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Rooftop: the penthouse of sound", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Rooftop: the penthouse of sound"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The cadence is the bridge between earth and note", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The cadence is the bridge between earth and note"}}
|
||||
{"song": "Rooftop Cadence", "artist": "Smoke Note", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Rooftop cadence: the jazz of elevation", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Rooftop cadence: the jazz of elevation"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The mirror reflects in smoke and brass", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The mirror reflects in smoke and brass"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Smoked mirror: the jazz of distortion", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Smoked mirror: the jazz of distortion"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every reflection is a blue note", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every reflection is a blue note"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The mirror smokes because the room is alive", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The mirror smokes because the room is alive"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Smoked mirror: the hazy truth", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Smoked mirror: the hazy truth"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We look in the smoked mirror and see our best selves", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. We look in the smoked mirror and see our best selves"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The mirror doesn't judge\u2014it just smokes", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. The mirror doesn't judge\u2014it just smokes"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Smoked mirror: the jazz club's oracle", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Smoked mirror: the jazz club's oracle"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The reflection swings like a door on its hinge", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The reflection swings like a door on its hinge"}}
|
||||
{"song": "Smoked Mirror", "artist": "Midnight Quartet", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Smoked mirror: honest distortion", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Smoked mirror: honest distortion"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The catastrophe wears a velvet jacket", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The catastrophe wears a velvet jacket"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Velvet catastrophe: the elegant disaster", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Velvet catastrophe: the elegant disaster"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every catastrophe deserves a good soundtrack", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every catastrophe deserves a good soundtrack"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The velvet absorbs the shock of the fall", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The velvet absorbs the shock of the fall"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Catastrophe: the jazz musician's muse", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Catastrophe: the jazz musician's muse"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We dress our disasters in velvet because we have class", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. We dress our disasters in velvet because we have class"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The catastrophe swings instead of crashing", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. The catastrophe swings instead of crashing"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Velvet catastrophe: the smooth collapse", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Velvet catastrophe: the smooth collapse"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The disaster plays a solo before it leaves", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The disaster plays a solo before it leaves"}}
|
||||
{"song": "Velvet Catastrophe", "artist": "Velvet Chord", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Velvet catastrophe: the most dignified ruin", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Velvet catastrophe: the most dignified ruin"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The interval glows like trapped sunlight", "scene": {"mood": "improvisation", "colors": ["deep blue", "amber", "smoke gray"], "composition": "smoky club", "camera": "slow drift", "description": "Smoky Club. The interval glows like trapped sunlight"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Amber interval: the preserved pause", "scene": {"mood": "cool", "colors": ["midnight", "gold", "burgundy"], "composition": "spotlight solo", "camera": "rack focus", "description": "Spotlight Solo. Amber interval: the preserved pause"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every interval is a tiny eternity", "scene": {"mood": "smoky", "colors": ["slate", "copper", "cream"], "composition": "ensemble frame", "camera": "steady", "description": "Ensemble Frame. Every interval is a tiny eternity"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The amber holds the note forever", "scene": {"mood": "swing", "colors": ["navy", "champagne", "charcoal"], "composition": "instrument close-up", "camera": "handheld sway", "description": "Instrument Close Up. The amber holds the note forever"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Amber interval: the fossil of sound", "scene": {"mood": "melancholy", "colors": ["indigo", "bronze", "pearl"], "composition": "audience blur", "camera": "dolly", "description": "Audience Blur. Amber interval: the fossil of sound"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We pause in amber because time is too fast", "scene": {"mood": "exuberance", "colors": ["deep blue", "amber", "smoke gray"], "composition": "reflected in piano", "camera": "orbit", "description": "Reflected In Piano. We pause in amber because time is too fast"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The interval glows with trapped beauty", "scene": {"mood": "mystery", "colors": ["midnight", "gold", "burgundy"], "composition": "silhouette trio", "camera": "locked", "description": "Silhouette Trio. The interval glows with trapped beauty"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Amber: the jazz of preservation", "scene": {"mood": "tenderness", "colors": ["slate", "copper", "cream"], "composition": "vinyl spinning", "camera": "tilt down", "description": "Vinyl Spinning. Amber: the jazz of preservation"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The interval is the amber of the melody", "scene": {"mood": "chaos", "colors": ["navy", "champagne", "charcoal"], "composition": "bar counter", "camera": "soft zoom", "description": "Bar Counter. The interval is the amber of the melody"}}
|
||||
{"song": "Amber Interval", "artist": "Phantom Scale", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Amber interval: frozen music", "scene": {"mood": "resolution", "colors": ["indigo", "bronze", "pearl"], "composition": "stage depth", "camera": "floating", "description": "Stage Depth. Amber interval: frozen music"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The sun burns because it can't hold back", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The sun burns because it can't hold back"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Ardiente: the fever that heals", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Ardiente: the fever that heals"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We dance because the sun is watching", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. We dance because the sun is watching"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The sun doesn't set\u2014it takes a bow", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The sun doesn't set\u2014it takes a bow"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Sol ardiente: the star that never learned subtlety", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Sol ardiente: the star that never learned subtlety"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The sun writes in gold on the skin of the earth", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. The sun writes in gold on the skin of the earth"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Ardiente: the temperature of true love", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. Ardiente: the temperature of true love"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We worship the sun with our feet", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. We worship the sun with our feet"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The sun is the oldest DJ", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The sun is the oldest DJ"}}
|
||||
{"song": "Sol Ardiente", "artist": "Sol Ardiente", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Sol ardiente: light that refuses to whisper", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Sol ardiente: light that refuses to whisper"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The rhythm is holy because it never stops", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The rhythm is holy because it never stops"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Sagrado: the beat that blesses the ground", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Sagrado: the beat that blesses the ground"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every step is a prayer to the drum", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every step is a prayer to the drum"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The rhythm was here before the melody", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The rhythm was here before the melody"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Ritmo sagrado: the pulse of the ancestors", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Ritmo sagrado: the pulse of the ancestors"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The sacred rhythm doesn't need a church", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. The sacred rhythm doesn't need a church"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We dance the ritmo because stillness is a sin", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. We dance the ritmo because stillness is a sin"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Sagrado: the beat that forgives everything", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Sagrado: the beat that forgives everything"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The rhythm remembers what we forget", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The rhythm remembers what we forget"}}
|
||||
{"song": "Ritmo Sagrado", "artist": "Ritmo Sagrado", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Ritmo sagrado: the heartbeat of the earth", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Ritmo sagrado: the heartbeat of the earth"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Slow fire burns longest", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. Slow fire burns longest"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Fuego lento: the patience of passion", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Fuego lento: the patience of passion"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The fire doesn't rush\u2014it savors", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. The fire doesn't rush\u2014it savors"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "We cook our love on fuego lento", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. We cook our love on fuego lento"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Slow fire is the secret of the elders", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Slow fire is the secret of the elders"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Fuego lento: the tempo of true desire", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. Fuego lento: the tempo of true desire"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The flame that takes its time is the warmest", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. The flame that takes its time is the warmest"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We burn slowly because ashes are forever", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. We burn slowly because ashes are forever"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Fuego lento: the spice of devotion", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. Fuego lento: the spice of devotion"}}
|
||||
{"song": "Fuego Lento", "artist": "Fuego Lento", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The slow fire teaches the fast flame", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. The slow fire teaches the fast flame"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The heart is iron but it still beats", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The heart is iron but it still beats"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Hierro: the metal of the stubborn lover", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Hierro: the metal of the stubborn lover"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every coraz\u00f3n de hierro was once soft", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every coraz\u00f3n de hierro was once soft"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Iron heart: strong enough to break, strong enough to mend", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. Iron heart: strong enough to break, strong enough to mend"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The heart doesn't rust\u2014it adapts", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. The heart doesn't rust\u2014it adapts"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Coraz\u00f3n de hierro: the armor of the faithful", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. Coraz\u00f3n de hierro: the armor of the faithful"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We iron our hearts in the fire of loss", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. We iron our hearts in the fire of loss"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Hierro heart: heavy but never hollow", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Hierro heart: heavy but never hollow"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The iron heart loves harder because it knows fragility", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The iron heart loves harder because it knows fragility"}}
|
||||
{"song": "Coraz\u00f3n de Hierro", "artist": "Coraz\u00f3n de Hierro", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Coraz\u00f3n de hierro: un coraz\u00f3n que no se rinde", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Coraz\u00f3n de hierro: un coraz\u00f3n que no se rinde"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The hot wind carries stories from the south", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The hot wind carries stories from the south"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Viento caliente: the breath of the homeland", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Viento caliente: the breath of the homeland"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The wind doesn't ask permission to arrive", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. The wind doesn't ask permission to arrive"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "We follow the hot wind because it knows the way", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. We follow the hot wind because it knows the way"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Caliente: the wind that wakes the seeds", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Caliente: the wind that wakes the seeds"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The viento carries the scent of tomorrow", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. The viento carries the scent of tomorrow"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Hot wind: the messenger of change", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. Hot wind: the messenger of change"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We are the viento caliente of our own story", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. We are the viento caliente of our own story"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The wind is caliente because the earth is passionate", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The wind is caliente because the earth is passionate"}}
|
||||
{"song": "Viento Caliente", "artist": "Viento Caliente", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Viento caliente: the exhale of the continent", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Viento caliente: the exhale of the continent"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The moon burns with borrowed fire", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The moon burns with borrowed fire"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Luna ardiente: the night sun", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Luna ardiente: the night sun"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We dance under a moon on fire", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. We dance under a moon on fire"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The moon is caliente because it mirrors us", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The moon is caliente because it mirrors us"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Luna ardiente: the silver ball of passion", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Luna ardiente: the silver ball of passion"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The moon burns because the earth taught it how", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. The moon burns because the earth taught it how"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We howl at the moon and the moon howls back", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. We howl at the moon and the moon howls back"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Luna ardiente: the nocturnal furnace", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Luna ardiente: the nocturnal furnace"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The moon is the sun's passionate understudy", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The moon is the sun's passionate understudy"}}
|
||||
{"song": "Luna Ardiente", "artist": "Sol Ardiente", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Luna ardiente: burn bright even in darkness", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Luna ardiente: burn bright even in darkness"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The sacred river carries our prayers to the sea", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The sacred river carries our prayers to the sea"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "R\u00edo: the liquid highway of the ancestors", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. R\u00edo: the liquid highway of the ancestors"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every river is a sagrado path", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every river is a sagrado path"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The river doesn't stop for monuments", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The river doesn't stop for monuments"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "R\u00edo sagrado: the water that remembers", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. R\u00edo sagrado: the water that remembers"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We throw our prayers in the r\u00edo and the r\u00edo delivers", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. We throw our prayers in the r\u00edo and the r\u00edo delivers"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The sacred river runs through every heart", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. The sacred river runs through every heart"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "R\u00edo: the blood of the earth", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. R\u00edo: the blood of the earth"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The river is sagrado because it keeps moving", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The river is sagrado because it keeps moving"}}
|
||||
{"song": "R\u00edo Sagrado", "artist": "Ritmo Sagrado", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "R\u00edo sagrado: the endless baptism", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. R\u00edo sagrado: the endless baptism"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The sacred ash falls like snow on the altar", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The sacred ash falls like snow on the altar"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Ceniza: the final form of the offering", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Ceniza: the final form of the offering"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every ash was once a prayer on fire", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every ash was once a prayer on fire"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The ash is sagrado because it remembers the flame", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The ash is sagrado because it remembers the flame"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Ceniza sagrada: the holy dust", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Ceniza sagrada: the holy dust"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We scatter ash because burial is too contained", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. We scatter ash because burial is too contained"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The ash carries the soul to the four winds", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. The ash carries the soul to the four winds"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Ceniza: the communion of the burned", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Ceniza: the communion of the burned"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The sacred ash nourishes the new growth", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The sacred ash nourishes the new growth"}}
|
||||
{"song": "Ceniza Sagrada", "artist": "Fuego Lento", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Ceniza sagrada: death as fertilizer", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Ceniza sagrada: death as fertilizer"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The butterfly is made of fire and migration", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The butterfly is made of fire and migration"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Mariposa: the most delicate arsonist", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Mariposa: the most delicate arsonist"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every wingbeat fans the flame", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every wingbeat fans the flame"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The butterfly burns because it flies too close to the sun", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The butterfly burns because it flies too close to the sun"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Mariposa fuego: the insect inferno", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Mariposa fuego: the insect inferno"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We are butterflies made of the fire we survived", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. We are butterflies made of the fire we survived"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The migration is the fire's journey", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. The migration is the fire's journey"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Mariposa: beauty with a burning point", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Mariposa: beauty with a burning point"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The butterfly ignites every flower it lands on", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The butterfly ignites every flower it lands on"}}
|
||||
{"song": "Mariposa Fuego", "artist": "Coraz\u00f3n de Hierro", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Mariposa fuego: the most beautiful hazard", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Mariposa fuego: the most beautiful hazard"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The heart is stone but it still echoes", "scene": {"mood": "passion", "colors": ["sunset orange", "turquoise", "gold"], "composition": "dance floor", "camera": "tracking dance", "description": "Dance Floor. The heart is stone but it still echoes"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Piedra: the material of the ancient lover", "scene": {"mood": "celebration", "colors": ["hot pink", "deep red", "cream"], "composition": "street festival", "camera": "steady wide", "description": "Street Festival. Piedra: the material of the ancient lover"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every stone heart was once a volcano", "scene": {"mood": "longing", "colors": ["emerald", "bronze", "white"], "composition": "balcony view", "camera": "handheld energy", "description": "Balcony View. Every stone heart was once a volcano"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The heart doesn't break\u2014it erodes", "scene": {"mood": "joy", "colors": ["marigold", "indigo", "coral"], "composition": "beach sunset", "camera": "slow motion", "description": "Beach Sunset. The heart doesn't break\u2014it erodes"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Coraz\u00f3n de piedra: the geological heart", "scene": {"mood": "defiance", "colors": ["papaya", "midnight", "silver"], "composition": "market crowd", "camera": "drone sweep", "description": "Market Crowd. Coraz\u00f3n de piedra: the geological heart"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We have stone hearts because flesh was too fragile", "scene": {"mood": "sensuality", "colors": ["sunset orange", "turquoise", "gold"], "composition": "courtyard", "camera": "orbit", "description": "Courtyard. We have stone hearts because flesh was too fragile"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The heart is piedra because it chose to be", "scene": {"mood": "pride", "colors": ["hot pink", "deep red", "cream"], "composition": "plaza fountain", "camera": "crane up", "description": "Plaza Fountain. The heart is piedra because it chose to be"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Stone heart: heavy but weatherproof", "scene": {"mood": "nostalgia", "colors": ["emerald", "bronze", "white"], "composition": "rooftop party", "camera": "dolly in", "description": "Rooftop Party. Stone heart: heavy but weatherproof"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The heart echoes when you tap it", "scene": {"mood": "fervor", "colors": ["marigold", "indigo", "coral"], "composition": "candlelit room", "camera": "locked", "description": "Candlelit Room. The heart echoes when you tap it"}}
|
||||
{"song": "Coraz\u00f3n de Piedra", "artist": "Viento Caliente", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Coraz\u00f3n de piedra: the heart that outlasts everything", "scene": {"mood": "devotion", "colors": ["papaya", "midnight", "silver"], "composition": "parade", "camera": "whip pan", "description": "Parade. Coraz\u00f3n de piedra: the heart that outlasts everything"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The hymn is forged not sung", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The hymn is forged not sung"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Iron doesn't bend\u2014it decides", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Iron doesn't bend\u2014it decides"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We hymn to the furnace and the flame", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. We hymn to the furnace and the flame"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every hymn needs a hammer", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. Every hymn needs a hammer"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Iron hymn: the song of the unbroken", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Iron hymn: the song of the unbroken"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The hymn clangs louder than the choir", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. The hymn clangs louder than the choir"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We sing in iron because silk is too quiet", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. We sing in iron because silk is too quiet"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Iron hymn for iron hearts", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Iron hymn for iron hearts"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The hymn was forged in the fire of refusal", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The hymn was forged in the fire of refusal"}}
|
||||
{"song": "Iron Hymn", "artist": "Iron Hymn", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Iron rusts but the hymn doesn't", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Iron rusts but the hymn doesn't"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The scream comes from the place before sound", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The scream comes from the place before sound"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Void: where screams go to become music", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Void: where screams go to become music"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every scream fills a void", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every scream fills a void"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The void doesn't echo\u2014it absorbs", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The void doesn't echo\u2014it absorbs"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Scream into the void and the void screams back", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Scream into the void and the void screams back"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The void is the scream's natural habitat", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. The void is the scream's natural habitat"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Void scream: the sound of nothing becoming everything", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. Void scream: the sound of nothing becoming everything"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We scream because the void demands tribute", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. We scream because the void demands tribute"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The scream is the void's favorite instrument", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The scream is the void's favorite instrument"}}
|
||||
{"song": "Void Scream", "artist": "Void Scream", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Void screams in frequencies only the damned can hear", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Void screams in frequencies only the damned can hear"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The psalm is hammered on an anvil of rage", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The psalm is hammered on an anvil of rage"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Forge: where prayers go to become weapons", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Forge: where prayers go to become weapons"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every psalm needs fire to be forged", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every psalm needs fire to be forged"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The forge doesn't care about your comfort", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The forge doesn't care about your comfort"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Psalm of the blacksmith and the believer", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Psalm of the blacksmith and the believer"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The forge turns doubt into steel", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. The forge turns doubt into steel"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We forge psalms from the ore of suffering", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. We forge psalms from the ore of suffering"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The psalm sparks when the hammer falls", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. The psalm sparks when the hammer falls"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Forge psalm: the liturgy of the workshop", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. Forge psalm: the liturgy of the workshop"}}
|
||||
{"song": "Forge Psalm", "artist": "Forge Born", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The forge never closes and neither does the psalm", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. The forge never closes and neither does the psalm"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The crown is made of what we burned", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The crown is made of what we burned"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Ashen: the color of royalty after the fire", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Ashen: the color of royalty after the fire"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every crown must fall to become ash", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every crown must fall to become ash"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The crown weighs nothing because it's made of memory", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The crown weighs nothing because it's made of memory"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Ashen crown for the king of the aftermath", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Ashen crown for the king of the aftermath"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We wear ash because gold is too soft", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We wear ash because gold is too soft"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The crown rises from the pyre", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The crown rises from the pyre"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Ashen: the final color of every empire", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Ashen: the final color of every empire"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The crown is the last thing to burn", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The crown is the last thing to burn"}}
|
||||
{"song": "Ashen Crown", "artist": "Ashen Crown", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Ashen crown: sovereignty over ruins", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Ashen crown: sovereignty over ruins"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "We don't break the world\u2014we rift it", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. We don't break the world\u2014we rift it"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The rift is where new worlds leak through", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. The rift is where new worlds leak through"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every rift is a door the old gods forgot", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every rift is a door the old gods forgot"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Rift maker: the architect of the divide", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. Rift maker: the architect of the divide"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The rift heals but the scar remains", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. The rift heals but the scar remains"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We make rifts because walls are too easy", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We make rifts because walls are too easy"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The rift between two worlds is music", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The rift between two worlds is music"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Rift maker plays the sound of separation", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Rift maker plays the sound of separation"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The rift is the wound that became a river", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The rift is the wound that became a river"}}
|
||||
{"song": "Rift Maker", "artist": "Rift Maker", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Rifts don't close\u2014they become horizons", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Rifts don't close\u2014they become horizons"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The psalm is carved in volcanic glass", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The psalm is carved in volcanic glass"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Obsidian: the sharpest form of prayer", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Obsidian: the sharpest form of prayer"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every psalm cuts the one who reads it", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every psalm cuts the one who reads it"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The psalm reflects a distorted face", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The psalm reflects a distorted face"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Obsidian psalm: the hymn of the eruption", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Obsidian psalm: the hymn of the eruption"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We carve psalms because paper burns too easily", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We carve psalms because paper burns too easily"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The psalm is dark because the truth is dark", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The psalm is dark because the truth is dark"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Obsidian: glass made from earth's anger", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Obsidian: glass made from earth's anger"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The psalm slices through the noise", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The psalm slices through the noise"}}
|
||||
{"song": "Obsidian Psalm", "artist": "Iron Hymn", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Obsidian psalm: beautiful and dangerous", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Obsidian psalm: beautiful and dangerous"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The requiem is built to survive the apocalypse", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The requiem is built to survive the apocalypse"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Titanium: the metal of the unburied", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Titanium: the metal of the unburied"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every note is reinforced against grief", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every note is reinforced against grief"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The requiem doesn't corrode", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The requiem doesn't corrode"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Titanium requiem: mourning that outlasts the mourners", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Titanium requiem: mourning that outlasts the mourners"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play titanium because iron rusts and flesh fails", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We play titanium because iron rusts and flesh fails"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The requiem is the armor of the bereaved", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The requiem is the armor of the bereaved"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Titanium: the sound of grief that refuses to end", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Titanium: the sound of grief that refuses to end"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The requiem is stronger than the death it mourns", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The requiem is stronger than the death it mourns"}}
|
||||
{"song": "Titanium Requiem", "artist": "Void Scream", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Titanium requiem: the last song standing", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Titanium requiem: the last song standing"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The psalm flows like lava from the earth", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The psalm flows like lava from the earth"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Molten: the liquid form of prayer", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Molten: the liquid form of prayer"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every psalm cools into obsidian", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every psalm cools into obsidian"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The psalm burns because the truth is hot", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The psalm burns because the truth is hot"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Molten psalm: the volcanic hymn", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Molten psalm: the volcanic hymn"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We pour psalms because carving takes too long", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We pour psalms because carving takes too long"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The psalm destroys and creates simultaneously", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The psalm destroys and creates simultaneously"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Molten: the state between solid faith and gas", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Molten: the state between solid faith and gas"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The psalm flows where it wants", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The psalm flows where it wants"}}
|
||||
{"song": "Molten Psalm", "artist": "Forge Born", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Molten psalm: the prayer of the eruption", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Molten psalm: the prayer of the eruption"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The requiem revs and doesn't apologize", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The requiem revs and doesn't apologize"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Chainsaw: the instrument of the ungentle mourner", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Chainsaw: the instrument of the ungentle mourner"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every rev is a heartbeat of grief", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every rev is a heartbeat of grief"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The requiem cuts through the silence", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The requiem cuts through the silence"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Chainsaw requiem: the lumberjack's funeral", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Chainsaw requiem: the lumberjack's funeral"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We play the chainsaw because the violin is too soft", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We play the chainsaw because the violin is too soft"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The requiem doesn't whisper\u2014it roars", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The requiem doesn't whisper\u2014it roars"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Chainsaw: the chains of the liberated", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Chainsaw: the chains of the liberated"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The requiem fells the tree of sorrow", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The requiem fells the tree of sorrow"}}
|
||||
{"song": "Chainsaw Requiem", "artist": "Ashen Crown", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Chainsaw requiem: loud, necessary, and final", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Chainsaw requiem: loud, necessary, and final"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The grimoire is written in corrosion", "scene": {"mood": "fury", "colors": ["black", "blood red", "chrome"], "composition": "pit chaos", "camera": "shaky cam", "description": "Pit Chaos. The grimoire is written in corrosion"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Grimoire: the spell book of the oxidized", "scene": {"mood": "catharsis", "colors": ["gunmetal", "orange", "ash gray"], "composition": "stage pyro", "camera": "rapid cut", "description": "Stage Pyro. Grimoire: the spell book of the oxidized"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every spell is a reaction with oxygen", "scene": {"mood": "doom", "colors": ["deep crimson", "steel", "charcoal"], "composition": "crowd surf", "camera": "slow motion", "description": "Crowd Surf. Every spell is a reaction with oxygen"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The grimoire ages because it's made of iron", "scene": {"mood": "defiance", "colors": ["void black", "electric green", "bone white"], "composition": "mic close-up", "camera": "whip pan", "description": "Mic Close Up. The grimoire ages because it's made of iron"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Grimoire of rust: the alchemy of decay", "scene": {"mood": "power", "colors": ["obsidian", "molten gold", "smoke"], "composition": "guitar neck", "camera": "drone chaos", "description": "Guitar Neck. Grimoire of rust: the alchemy of decay"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We cast spells in rust because gold is too pure", "scene": {"mood": "desolation", "colors": ["black", "blood red", "chrome"], "composition": "drum fury", "camera": "handheld crush", "description": "Drum Fury. We cast spells in rust because gold is too pure"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The grimoire's power grows with age", "scene": {"mood": "rage", "colors": ["gunmetal", "orange", "ash gray"], "composition": "silhouette against fire", "camera": "locked", "description": "Silhouette Against Fire. The grimoire's power grows with age"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Rust: the patina of the magical", "scene": {"mood": "darkness", "colors": ["deep crimson", "steel", "charcoal"], "composition": "circle pit", "camera": "dolly smash", "description": "Circle Pit. Rust: the patina of the magical"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The grimoire teaches the spell of surrender", "scene": {"mood": "survival", "colors": ["void black", "electric green", "bone white"], "composition": "throne of skulls", "camera": "first person", "description": "Throne Of Skulls. The grimoire teaches the spell of surrender"}}
|
||||
{"song": "Grimoire of Rust", "artist": "Rift Maker", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Grimoire of rust: the most honest magic", "scene": {"mood": "transcendence", "colors": ["obsidian", "molten gold", "smoke"], "composition": "apocalyptic horizon", "camera": "orbit", "description": "Apocalyptic Horizon. Grimoire of rust: the most honest magic"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The hour between midnight and everything", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The hour between midnight and everything"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Your voice wraps around me like a warm coat", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Your voice wraps around me like a warm coat"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We slow dance in the kitchen to no music", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. We slow dance in the kitchen to no music"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The velvet hours don't need an audience", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. The velvet hours don't need an audience"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Candle wax remembers the shape of our night", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. Candle wax remembers the shape of our night"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Every whispered word is a love letter", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. Every whispered word is a love letter"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The hours stretch like honey in the dark", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. The hours stretch like honey in the dark"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Velvet is just softness with a backbone", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. Velvet is just softness with a backbone"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We make time stand still by moving slowly", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. We make time stand still by moving slowly"}}
|
||||
{"song": "Velvet Hours", "artist": "Velvet Haze", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The hour ends but the velvet stays", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. The hour ends but the velvet stays"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I confess in fabrics I can't afford", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. I confess in fabrics I can't afford"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Satin slides off the truth like water", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Satin slides off the truth like water"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The confession booth was our bedroom all along", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. The confession booth was our bedroom all along"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every apology is smoother in satin", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. Every apology is smoother in satin"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "I wore my heart on a sleeve of silk", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. I wore my heart on a sleeve of silk"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The confession wasn't loud\u2014it was drape", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. The confession wasn't loud\u2014it was drape"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Satin forgives where cotton can't", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. Satin forgives where cotton can't"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "I confessed to the mirror and the mirror blushed", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. I confessed to the mirror and the mirror blushed"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The fabric of truth is always expensive", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. The fabric of truth is always expensive"}}
|
||||
{"song": "Satin Confession", "artist": "Silk Road", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Confession: I'd wear satin to your funeral and mine", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. Confession: I'd wear satin to your funeral and mine"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Frozen in amber but still warm to the touch", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. Frozen in amber but still warm to the touch"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The glow comes from within not from above", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. The glow comes from within not from above"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Amber is just time that refused to leave", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. Amber is just time that refused to leave"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "We glow in the dark because we've been in it so long", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. We glow in the dark because we've been in it so long"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The amber of your eyes outlasts the sunset", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. The amber of your eyes outlasts the sunset"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Every glow needs a little darkness to be seen", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. Every glow needs a little darkness to be seen"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Amber preserves what glass would shatter", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. Amber preserves what glass would shatter"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The glow was always there\u2014dimmer than you thought", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. The glow was always there\u2014dimmer than you thought"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We are amber: beautiful and imprisoned", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. We are amber: beautiful and imprisoned"}}
|
||||
{"song": "Amber Glow", "artist": "Midnight Bloom", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The glow remains when the fire forgets", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. The glow remains when the fire forgets"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The morning arrives on a thread of gold", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The morning arrives on a thread of gold"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Silk morning\u2014the world is gentler now", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Silk morning\u2014the world is gentler now"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We wake to a world that forgot to be rough", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. We wake to a world that forgot to be rough"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The morning light wears silk and nothing else", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. The morning light wears silk and nothing else"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Every sunrise is a silk robe on the horizon", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. Every sunrise is a silk robe on the horizon"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The morning doesn't shout\u2014it drapes", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. The morning doesn't shout\u2014it drapes"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Silk morning: soft enough to believe in", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. Silk morning: soft enough to believe in"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "We greet the day like a lover in silk", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. We greet the day like a lover in silk"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The morning was always silk\u2014we just wore wool", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. The morning was always silk\u2014we just wore wool"}}
|
||||
{"song": "Silk Morning", "artist": "Amber Voice", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Silk morning, steel resolve, golden heart", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. Silk morning, steel resolve, golden heart"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "We archive our love in moonbeams", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. We archive our love in moonbeams"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The moon remembers what we choose to forget", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. The moon remembers what we choose to forget"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every kiss is a file saved to the moon", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. Every kiss is a file saved to the moon"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The archive grows with every whispered promise", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. The archive grows with every whispered promise"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Moonlight is the librarian of the night", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. Moonlight is the librarian of the night"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We read our history in the craters of the moon", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. We read our history in the craters of the moon"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The archive doesn't judge\u2014it just holds", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. The archive doesn't judge\u2014it just holds"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Moonlit data: soft, eternal, luminous", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. Moonlit data: soft, eternal, luminous"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We are the moon's favorite collection", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. We are the moon's favorite collection"}}
|
||||
{"song": "Moonlit Archive", "artist": "Satin Echo", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The archive opens when the sun goes down", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. The archive opens when the sun goes down"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The dusk pours honey on the wounded city", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The dusk pours honey on the wounded city"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Honey dusk: the viscosity of evening", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Honey dusk: the viscosity of evening"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "We drip slowly into the night", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. We drip slowly into the night"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Honey: the color of your voice at 7 PM", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. Honey: the color of your voice at 7 PM"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The dusk doesn't fall\u2014it pours", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. The dusk doesn't fall\u2014it pours"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Honey dusk for the ones who need sweetness", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. Honey dusk for the ones who need sweetness"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The evening is thick with golden promises", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. The evening is thick with golden promises"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Dusk drips like honey from the skyline", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. Dusk drips like honey from the skyline"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Honey dusk: the amber between day and dream", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. Honey dusk: the amber between day and dream"}}
|
||||
{"song": "Honey Dusk", "artist": "Velvet Haze", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The dusk was always honey\u2014we just called it sunset", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. The dusk was always honey\u2014we just called it sunset"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "My heart is porcelain\u2014handle with reverence", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. My heart is porcelain\u2014handle with reverence"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Porcelain: beautiful and one fall from broken", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Porcelain: beautiful and one fall from broken"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The heart rings like china when you tap it", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. The heart rings like china when you tap it"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Porcelain heart for the delicate and the brave", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. Porcelain heart for the delicate and the brave"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We are porcelain in a world of concrete", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. We are porcelain in a world of concrete"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The heart is fired in the kiln of loss", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. The heart is fired in the kiln of loss"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Porcelain: the courage of the fragile", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. Porcelain: the courage of the fragile"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "My heart sings like struck crystal", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. My heart sings like struck crystal"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Porcelain heart: exquisite vulnerability", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. Porcelain heart: exquisite vulnerability"}}
|
||||
{"song": "Porcelain Heart", "artist": "Silk Road", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "The heart breaks beautifully or not at all", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. The heart breaks beautifully or not at all"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The whisper is dark but it shines when polished", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The whisper is dark but it shines when polished"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Obsidian: the stone of the midnight confession", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Obsidian: the stone of the midnight confession"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every whisper cuts like volcanic glass", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. Every whisper cuts like volcanic glass"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The whisper reflects a distorted but true face", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. The whisper reflects a distorted but true face"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Obsidian whisper: sharp and beautiful", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. Obsidian whisper: sharp and beautiful"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We whisper in obsidian because velvet is too soft", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. We whisper in obsidian because velvet is too soft"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The whisper slices through the noise of the day", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. The whisper slices through the noise of the day"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Obsidian: the glass of the volcanic heart", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. Obsidian: the glass of the volcanic heart"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The whisper is dark because the truth is dark", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. The whisper is dark because the truth is dark"}}
|
||||
{"song": "Obsidian Whisper", "artist": "Midnight Bloom", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Obsidian whisper: the most elegant weapon", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. Obsidian whisper: the most elegant weapon"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The light forgives everything at golden hour", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The light forgives everything at golden hour"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Confession tastes like honey at sunset", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Confession tastes like honey at sunset"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Golden hour: the best time to be honest", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. Golden hour: the best time to be honest"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Every confession is softer in amber light", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. Every confession is softer in amber light"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The hour doesn't judge\u2014it just illuminates", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. The hour doesn't judge\u2014it just illuminates"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We confess at golden hour because shadows are long", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. We confess at golden hour because shadows are long"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Golden confession: the truth in warm tones", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. Golden confession: the truth in warm tones"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "The hour gives us permission to be flawed", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. The hour gives us permission to be flawed"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Golden hour: the photographer's absolution", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. Golden hour: the photographer's absolution"}}
|
||||
{"song": "Golden Hour Confession", "artist": "Amber Voice", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Confession at golden hour: raw and radiant", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. Confession at golden hour: raw and radiant"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The storm wears velvet instead of thunder", "scene": {"mood": "tenderness", "colors": ["burgundy", "gold", "cream"], "composition": "intimate close-up", "camera": "slow dolly", "description": "Intimate Close Up. The storm wears velvet instead of thunder"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Velvet storm: the gentle destruction", "scene": {"mood": "longing", "colors": ["deep plum", "rose", "champagne"], "composition": "soft focus", "camera": "gentle pan", "description": "Soft Focus. Velvet storm: the gentle destruction"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Every gust is a soft catastrophe", "scene": {"mood": "sensuality", "colors": ["midnight blue", "copper", "ivory"], "composition": "candlelit frame", "camera": "steady", "description": "Candlelit Frame. Every gust is a soft catastrophe"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The storm doesn't shout\u2014it purrs", "scene": {"mood": "healing", "colors": ["wine red", "blush", "pearl"], "composition": "silhouette", "camera": "breathing handheld", "description": "Silhouette. The storm doesn't shout\u2014it purrs"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Velvet: the texture of controlled chaos", "scene": {"mood": "devotion", "colors": ["amber", "bronze", "soft white"], "composition": "over the shoulder", "camera": "locked", "description": "Over The Shoulder. Velvet: the texture of controlled chaos"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We weather the velvet storm in silk pajamas", "scene": {"mood": "heartbreak", "colors": ["burgundy", "gold", "cream"], "composition": "two-shot", "camera": "soft zoom", "description": "Two Shot. We weather the velvet storm in silk pajamas"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The storm is elegant even in its fury", "scene": {"mood": "bliss", "colors": ["deep plum", "rose", "champagne"], "composition": "reflective surface", "camera": "orbit", "description": "Reflective Surface. The storm is elegant even in its fury"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Velvet storm for the gentle and the fierce", "scene": {"mood": "vulnerability", "colors": ["midnight blue", "copper", "ivory"], "composition": "golden hour", "camera": "crane down", "description": "Golden Hour. Velvet storm for the gentle and the fierce"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The storm wraps you in luxury before it wrecks you", "scene": {"mood": "passion", "colors": ["wine red", "blush", "pearl"], "composition": "shallow depth", "camera": "rack focus", "description": "Shallow Depth. The storm wraps you in luxury before it wrecks you"}}
|
||||
{"song": "Velvet Storm", "artist": "Satin Echo", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Velvet storm: the most beautiful disaster", "scene": {"mood": "serenity", "colors": ["amber", "bronze", "soft white"], "composition": "embracing figures", "camera": "float", "description": "Embracing Figures. Velvet storm: the most beautiful disaster"}}
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "The screen door slams, Mary's dress waves", "scene": {"mood": "hope", "colors": ["gold", "sky blue", "white"], "composition": "wide shot", "camera": "static", "description": "Open horizon. Golden light breaking through clouds. The figure silhouetted against dawn. The screen door slams, Mary's dress waves"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Like a vision she dances across the porch as the radio plays", "scene": {"mood": "anticipation", "colors": ["silver", "pale green", "cream"], "composition": "close-up", "camera": "slow pan", "description": "Close on hands gripping a steering wheel. Dashboard lights reflecting in eyes. Road stretching ahead. Like a vision she dances across the porch as the radio plays"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "Roy Orbison singing for the lonely, hey that's me and I want you only", "scene": {"mood": "energy", "colors": ["red", "orange", "electric blue"], "composition": "over the shoulder", "camera": "dolly in", "description": "Rapid cuts. Bodies in motion. Light streaks across the frame. Roy Orbison singing for the lonely, hey that's me and I want you only"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Don't turn me home out now I'm so young and worthless still", "scene": {"mood": "triumph", "colors": ["gold", "crimson", "white"], "composition": "low angle", "camera": "dolly out", "description": "Wide shot. Figure standing on a hilltop. Arms raised. City lights below. Don't turn me home out now I'm so young and worthless still"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "The night's busting open these two lanes will take us anywhere", "scene": {"mood": "nostalgia", "colors": ["amber", "sepia", "dusty rose"], "composition": "high angle", "camera": "handheld", "description": "Sepia tones. A photograph come to life. Dust motes in afternoon light. The night's busting open these two lanes will take us anywhere"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We got one last chance to make it real", "scene": {"mood": "urgency", "colors": ["red", "black", "strobe white"], "composition": "dutch angle", "camera": "steadicam", "description": "Handheld camera running. Blurred faces. Traffic. Heartbeat sound design. We got one last chance to make it real"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "To trade in these wings on some wheels", "scene": {"mood": "passion", "colors": ["deep red", "burgundy", "gold"], "composition": "symmetrical", "camera": "slow zoom", "description": "Extreme close-up. Skin. Breath visible in cold air. Eyes locked. To trade in these wings on some wheels"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Climb in back, heaven's waiting down the tracks", "scene": {"mood": "defiance", "colors": ["black", "neon green", "chrome"], "composition": "rule of thirds", "camera": "crane up", "description": "Low angle. Figure standing against the wind. Debris flying past. Unmoved. Climb in back, heaven's waiting down the tracks"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Oh oh oh oh oh oh oh", "scene": {"mood": "release", "colors": ["sky blue", "white", "pale gold"], "composition": "extreme wide", "camera": "tracking shot", "description": "Slow motion. Something falling \u2014 a mask, a chain, a weight. Lightness follows. Oh oh oh oh oh oh oh"}}
|
||||
{"song": "Thunder Road", "artist": "Heartland", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "It's a town full of losers and I'm pulling out of here to win", "scene": {"mood": "catharsis", "colors": ["all white", "silver", "clear"], "composition": "medium shot", "camera": "slow tilt down", "description": "White space expanding. Figure dissolving into light. Peace in the dissolution. It's a town full of losers and I'm pulling out of here to win"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Woke up on the floor again, whiskey still on my tongue", "scene": {"mood": "despair", "colors": ["navy", "black", "grey"], "composition": "wide shot", "camera": "static", "description": "Empty room. Single light source. Figure curled in corner. Rain on windows. Woke up on the floor again, whiskey still on my tongue"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The mirror shows a stranger and the damage that I've done", "scene": {"mood": "anger", "colors": ["red", "black", "orange"], "composition": "close-up", "camera": "slow pan", "description": "Shattered glass. Red light. Hands clenched. Jaw tight. The frame vibrates. The mirror shows a stranger and the damage that I've done"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "I scream until my throat bleeds but nobody comes", "scene": {"mood": "frenzy", "colors": ["strobe", "red", "white flash"], "composition": "over the shoulder", "camera": "dolly in", "description": "Strobe lighting. Multiple exposures. Bodies colliding. Chaos as composition. I scream until my throat bleeds but nobody comes"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The walls are closing in again, the ceiling pressing down", "scene": {"mood": "exhaustion", "colors": ["grey", "brown", "faded"], "composition": "low angle", "camera": "dolly out", "description": "Static shot. Figure slumped. Eyes half-closed. Time passing in shadows. The walls are closing in again, the ceiling pressing down"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "I tried to call your number but you changed it years ago", "scene": {"mood": "resignation", "colors": ["grey", "muted blue", "beige"], "composition": "high angle", "camera": "handheld", "description": "Medium shot. Hands dropping keys on a table. Turning away. Not looking back. I tried to call your number but you changed it years ago"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "Now I'm howling at the moon like some rabid dog I know", "scene": {"mood": "grief", "colors": ["deep purple", "black", "silver"], "composition": "dutch angle", "camera": "steadicam", "description": "Wide shot. Figure alone in vast space. Dark purple sky. No horizon line. Now I'm howling at the moon like some rabid dog I know"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Every bone remembers what my mind wants to forget", "scene": {"mood": "numbness", "colors": ["white", "grey", "no color"], "composition": "symmetrical", "camera": "slow zoom", "description": "Desaturated. Figure staring at nothing. World moving around them in blur. Every bone remembers what my mind wants to forget"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "I'll tear this whole house down before the sun comes up", "scene": {"mood": "rage", "colors": ["fire red", "black", "ember orange"], "composition": "rule of thirds", "camera": "crane up", "description": "Red wash. Extreme close-up on eyes. Fire reflected in pupils. I'll tear this whole house down before the sun comes up"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Ash and ruin everywhere, this is all that's left", "scene": {"mood": "acceptance", "colors": ["soft blue", "warm grey", "sage"], "composition": "extreme wide", "camera": "tracking shot", "description": "Soft focus. Gentle light. Figure breathing. The camera doesn't judge. Ash and ruin everywhere, this is all that's left"}}
|
||||
{"song": "Black Dog Howl", "artist": "Rust & Wire", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Silence. Just the wind through broken glass.", "scene": {"mood": "silence", "colors": ["black", "void", "faint starlight"], "composition": "medium shot", "camera": "slow tilt down", "description": "Black screen. Faint starlight. The sound drops out completely. Silence. Just the wind through broken glass."}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Ten thousand miles of static between your voice and mine", "scene": {"mood": "wonder", "colors": ["aurora green", "violet", "silver"], "composition": "wide shot", "camera": "static", "description": "Northern lights overhead. Figure looking up. Mouth open. Child's expression. Ten thousand miles of static between your voice and mine"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "I trace your constellation on the dashboard every night", "scene": {"mood": "isolation", "colors": ["cold blue", "black", "distant starlight"], "composition": "close-up", "camera": "slow pan", "description": "Extreme wide. Single figure. Vast empty landscape. Scale crushing. I trace your constellation on the dashboard every night"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The signal fades to nothing but I keep the frequency", "scene": {"mood": "longing", "colors": ["teal", "silver", "moonlight"], "composition": "over the shoulder", "camera": "dolly in", "description": "Through a window. Figure on the other side. Glass between. Breath on the pane. The signal fades to nothing but I keep the frequency"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Then suddenly your laughter breaks through like a summer storm", "scene": {"mood": "connection", "colors": ["warm gold", "rose", "blush"], "composition": "low angle", "camera": "dolly out", "description": "Two hands reaching. Fingers almost touching. Warm light between them. Then suddenly your laughter breaks through like a summer storm"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We're dancing in the data stream, our pixels intertwined", "scene": {"mood": "euphoria", "colors": ["neon", "rainbow", "white flash"], "composition": "high angle", "camera": "handheld", "description": "Overexposed. Everything bright. Dancing. The frame can't contain the joy. We're dancing in the data stream, our pixels intertwined"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "But I can't tell if you're real or just a ghost in the machine", "scene": {"mood": "confusion", "colors": ["swirling", "unsettled", "green-grey"], "composition": "dutch angle", "camera": "steadicam", "description": "Multiple focal points. Nothing sharp. The viewer doesn't know where to look. But I can't tell if you're real or just a ghost in the machine"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The picture clears and there you are \u2014 imperfect, warm, alive", "scene": {"mood": "clarity", "colors": ["clear blue", "white", "crisp"], "composition": "symmetrical", "camera": "slow zoom", "description": "Rack focus. Background blurs, foreground sharpens. Suddenly everything makes sense. The picture clears and there you are \u2014 imperfect, warm, alive"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Your hand reaches through the screen, I swear I feel the heat", "scene": {"mood": "tenderness", "colors": ["blush pink", "warm cream", "soft gold"], "composition": "rule of thirds", "camera": "crane up", "description": "Close on a hand touching a face. Soft light. Shallow depth of field. Your hand reaches through the screen, I swear I feel the heat"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The bandwidth's dying, say it now before the link goes dark", "scene": {"mood": "urgency", "colors": ["red", "black", "strobe white"], "composition": "extreme wide", "camera": "tracking shot", "description": "Handheld camera running. Blurred faces. Traffic. Heartbeat sound design. The bandwidth's dying, say it now before the link goes dark"}}
|
||||
{"song": "Satellite Hearts", "artist": "Neon Circuit", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Goodnight, satellite heart. I'll find you in the static.", "scene": {"mood": "bittersweet", "colors": ["amber", "lavender", "fading light"], "composition": "medium shot", "camera": "slow tilt down", "description": "Amber light fading. A smile that's also a goodbye. Beautiful and sad at once. Goodnight, satellite heart. I'll find you in the static."}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "They paved over every green thing when the developers came", "scene": {"mood": "oppression", "colors": ["concrete grey", "brown", "exhaust fume yellow"], "composition": "wide shot", "camera": "static", "description": "Concrete. Overpasses. No sky visible. Figures small against infrastructure. They paved over every green thing when the developers came"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "But we planted seeds between the cracks and gave them all a name", "scene": {"mood": "resilience", "colors": ["green", "cracked concrete", "gold"], "composition": "close-up", "camera": "slow pan", "description": "Crack in pavement. Green shoot pushing through. Macro lens. But we planted seeds between the cracks and gave them all a name"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The mayor says progress looks like demolition and dust", "scene": {"mood": "anger", "colors": ["red", "black", "orange"], "composition": "over the shoulder", "camera": "dolly in", "description": "Shattered glass. Red light. Hands clenched. Jaw tight. The frame vibrates. The mayor says progress looks like demolition and dust"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "But a dandelion broke through the asphalt this morning \u2014 that's us", "scene": {"mood": "beauty", "colors": ["wildflower colors", "green", "sunlight"], "composition": "low angle", "camera": "dolly out", "description": "Wildflowers in unexpected places. Color against grey. Nature reclaiming. But a dandelion broke through the asphalt this morning \u2014 that's us"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "You can't kill what wants to live, can't silence what must sing", "scene": {"mood": "defiance", "colors": ["black", "neon green", "chrome"], "composition": "high angle", "camera": "handheld", "description": "Low angle. Figure standing against the wind. Debris flying past. Unmoved. You can't kill what wants to live, can't silence what must sing"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "We're the roots beneath the road, we're the birds that built on string", "scene": {"mood": "community", "colors": ["warm tones", "string lights", "firelight"], "composition": "dutch angle", "camera": "steadicam", "description": "String lights. People gathered. Laughter out of focus. Warmth as visual language. We're the roots beneath the road, we're the birds that built on string"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "When they tear the next block down we'll be dancing in the rubble", "scene": {"mood": "joy", "colors": ["bright", "multi", "saturated"], "composition": "symmetrical", "camera": "slow zoom", "description": "Saturated color. Wide smiles. Arms open. The world in full bloom. When they tear the next block down we'll be dancing in the rubble"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Every protest is a garden, every march plants something new", "scene": {"mood": "struggle", "colors": ["dust", "grey", "hard light"], "composition": "rule of thirds", "camera": "crane up", "description": "Close on hands working. Calluses. Dust. Effort visible in every frame. Every protest is a garden, every march plants something new"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The concrete is a drum and our footsteps keep the beat", "scene": {"mood": "growth", "colors": ["green", "brown", "morning light"], "composition": "extreme wide", "camera": "tracking shot", "description": "Time-lapse. Seed to flower. Sunrise to sunset. Transformation as rhythm. The concrete is a drum and our footsteps keep the beat"}}
|
||||
{"song": "Concrete Garden", "artist": "Streetlight Prophet", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Tomorrow there'll be flowers where they swore there'd only be defeat", "scene": {"mood": "hope", "colors": ["gold", "sky blue", "white"], "composition": "medium shot", "camera": "slow tilt down", "description": "Open horizon. Golden light breaking through clouds. The figure silhouetted against dawn. Tomorrow there'll be flowers where they swore there'd only be defeat"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I felt the pull before I saw the edge", "scene": {"mood": "dread", "colors": ["void black", "deep red", "cold white"], "composition": "wide shot", "camera": "static", "description": "Corner of frame. Something in the periphery. Dark. The camera doesn't look directly. I felt the pull before I saw the edge"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The stars bent sideways, light itself was dead", "scene": {"mood": "fascination", "colors": ["event horizon purple", "gravitational lens blue"], "composition": "close-up", "camera": "slow pan", "description": "Close on eyes. Reflection of something impossible. The pupil expands. The stars bent sideways, light itself was dead"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "I could have turned the ship around but something in me said stay", "scene": {"mood": "surrender", "colors": ["white", "dissolution", "prismatic"], "composition": "over the shoulder", "camera": "dolly in", "description": "Arms opening. Head back. Falling backward into something vast. I could have turned the ship around but something in me said stay"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "The event horizon glows like a halo made of nothing", "scene": {"mood": "awe", "colors": ["starfield", "nebula colors", "infinite dark"], "composition": "low angle", "camera": "dolly out", "description": "Wide shot of cosmos. Nebula. Stars being born. Human figure tiny at bottom. The event horizon glows like a halo made of nothing"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Time stretches thin as wire, each second takes a year", "scene": {"mood": "terror", "colors": ["black", "red shift", "distortion"], "composition": "high angle", "camera": "handheld", "description": "Shaking camera. Red shift. Something approaching fast. The frame distorts. Time stretches thin as wire, each second takes a year"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "I am both the observer and the thing that disappears", "scene": {"mood": "peace", "colors": ["deep space black", "starlight", "calm"], "composition": "dutch angle", "camera": "steadicam", "description": "Still water. Stars reflected. Perfect mirror. No movement. No sound. I am both the observer and the thing that disappears"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "My body reads the tidal forces like sheet music played on bone", "scene": {"mood": "disorientation", "colors": ["warped", "chromatic aberration", "bent light"], "composition": "symmetrical", "camera": "slow zoom", "description": "Warped lens. Vertigo. Walls becoming floor. Gravity is a suggestion. My body reads the tidal forces like sheet music played on bone"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "I stop fighting, stop reaching, stop calling home", "scene": {"mood": "acceptance", "colors": ["soft blue", "warm grey", "sage"], "composition": "rule of thirds", "camera": "crane up", "description": "Soft focus. Gentle light. Figure breathing. The camera doesn't judge. I stop fighting, stop reaching, stop calling home"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "There is a peace in dissolution I was never meant to know", "scene": {"mood": "transcendence", "colors": ["pure white", "beyond visible", "golden"], "composition": "extreme wide", "camera": "tracking shot", "description": "Pure white expanding. Figure becoming light. Boundaries dissolving. There is a peace in dissolution I was never meant to know"}}
|
||||
{"song": "Gravity Well", "artist": "Void Walker", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Singularity. Silence. Everything and nothing both at once.", "scene": {"mood": "emptiness", "colors": ["void", "absolute black", "nothing"], "composition": "medium shot", "camera": "slow tilt down", "description": "Absolute black. No stars. No reference point. The void looking back. Singularity. Silence. Everything and nothing both at once."}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "My father's hands smelled like machine oil and prayer", "scene": {"mood": "nostalgia", "colors": ["amber", "sepia", "dusty rose"], "composition": "wide shot", "camera": "static", "description": "Sepia tones. A photograph come to life. Dust motes in afternoon light. My father's hands smelled like machine oil and prayer"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The factory whistle was our clock, the shift was our calendar", "scene": {"mood": "sadness", "colors": ["grey", "rain", "muted blue"], "composition": "close-up", "camera": "slow pan", "description": "Rain on glass. Grey light. A cup of tea going cold. Still life of loss. The factory whistle was our clock, the shift was our calendar"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "He'd come home at midnight, wake me up to say goodnight", "scene": {"mood": "tenderness", "colors": ["blush pink", "warm cream", "soft gold"], "composition": "over the shoulder", "camera": "dolly in", "description": "Close on a hand touching a face. Soft light. Shallow depth of field. He'd come home at midnight, wake me up to say goodnight"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Now the mill is just a skeleton and he's been gone ten years", "scene": {"mood": "loss", "colors": ["faded", "dusty", "empty space"], "composition": "low angle", "camera": "dolly out", "description": "Empty chair. Dust settling. A coat still on a hook. Presence of absence. Now the mill is just a skeleton and he's been gone ten years"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "But the river still runs brown with memory and rust", "scene": {"mood": "beauty", "colors": ["wildflower colors", "green", "sunlight"], "composition": "high angle", "camera": "handheld", "description": "Wildflowers in unexpected places. Color against grey. Nature reclaiming. But the river still runs brown with memory and rust"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "I found his lunchbox in the attic, coffee stains still fresh", "scene": {"mood": "resignation", "colors": ["grey", "muted blue", "beige"], "composition": "dutch angle", "camera": "steadicam", "description": "Medium shot. Hands dropping keys on a table. Turning away. Not looking back. I found his lunchbox in the attic, coffee stains still fresh"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "Some things don't decay \u2014 they just learn to hold still", "scene": {"mood": "love", "colors": ["neutral"], "composition": "symmetrical", "camera": "slow zoom", "description": "Visual interpretation of: Some things don't decay \u2014 they just learn to hold still"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "I hum the songs he hummed to me though I've forgotten half the words", "scene": {"mood": "weariness", "colors": ["grey-brown", "faded", "dim"], "composition": "rule of thirds", "camera": "crane up", "description": "Slow movement. Heavy eyelids. The world in faded tones. Everything too much. I hum the songs he hummed to me though I've forgotten half the words"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "The town's half-empty but the porch lights still come on at dusk", "scene": {"mood": "quiet hope", "colors": ["faint warm light", "candle glow", "dawn grey"], "composition": "extreme wide", "camera": "tracking shot", "description": "Faint warm light. Candle in dark room. Just enough to see by. The town's half-empty but the porch lights still come on at dusk"}}
|
||||
{"song": "Rust Belt Lullaby", "artist": "Iron & Ember", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Sleep now, rust belt baby. The furnace keeps us warm.", "scene": {"mood": "peace", "colors": ["deep space black", "starlight", "calm"], "composition": "medium shot", "camera": "slow tilt down", "description": "Still water. Stars reflected. Perfect mirror. No movement. No sound. Sleep now, rust belt baby. The furnace keeps us warm."}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I didn't start the fire but I brought the gasoline", "scene": {"mood": "fury", "colors": ["dark red", "black", "flash"], "composition": "wide shot", "camera": "static", "description": "Dark red wash. Hands destroying. Frame shaking with rage. I didn't start the fire but I brought the gasoline"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Every sermon needs a spark and every spark needs a dream", "scene": {"mood": "ecstasy", "colors": ["fire", "gold", "blinding white"], "composition": "close-up", "camera": "slow pan", "description": "Fire and gold. Bodies arching. Light bursting from every surface. Every sermon needs a spark and every spark needs a dream"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The forest is a cathedral and the flames are choir boys singing", "scene": {"mood": "chaos", "colors": ["strobe", "fragmented", "clashing"], "composition": "over the shoulder", "camera": "dolly in", "description": "Fragmented frame. Collage. Everything at once. Order is a memory. The forest is a cathedral and the flames are choir boys singing"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Watch the old world burn \u2014 isn't the light beautiful?", "scene": {"mood": "joy", "colors": ["bright", "multi", "saturated"], "composition": "low angle", "camera": "dolly out", "description": "Saturated color. Wide smiles. Arms open. The world in full bloom. Watch the old world burn \u2014 isn't the light beautiful?"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "We'll dance in the embers, we'll make love in the ash", "scene": {"mood": "destruction", "colors": ["fire", "ash", "smoke orange"], "composition": "high angle", "camera": "handheld", "description": "Fire. Ash falling like snow. Structures collapsing. Beautiful in its terrible way. We'll dance in the embers, we'll make love in the ash"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "From destruction comes the soil where new things grow at last", "scene": {"mood": "creation", "colors": ["green", "light", "warm gold"], "composition": "dutch angle", "camera": "steadicam", "description": "Hands shaping clay. Light emerging from dark. Something new being born. From destruction comes the soil where new things grow at last"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "But don't mistake the warmth for safety, don't mistake the glow for home", "scene": {"mood": "warning", "colors": ["red flash", "amber", "siren"], "composition": "symmetrical", "camera": "slow zoom", "description": "Red flash. Siren light. The calm before. Then: impact. But don't mistake the warmth for safety, don't mistake the glow for home"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Come closer, come closer \u2014 I promise the burning feels like flying", "scene": {"mood": "invitation", "colors": ["warm", "open", "golden"], "composition": "rule of thirds", "camera": "crane up", "description": "Open door. Warm light spilling out. A hand extended. Come in. Come closer, come closer \u2014 I promise the burning feels like flying"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "We threw everything we owned into the blaze and laughed", "scene": {"mood": "abandon", "colors": ["wild", "free", "untethered"], "composition": "extreme wide", "camera": "tracking shot", "description": "Running through a field. Hair wild. No destination. Just movement. We threw everything we owned into the blaze and laughed"}}
|
||||
{"song": "Wildfire Sermon", "artist": "Prophet Ash", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Morning. Smoke. Green shoots. Begin again.", "scene": {"mood": "rebirth", "colors": ["green shoots", "dawn", "clear"], "composition": "medium shot", "camera": "slow tilt down", "description": "Dawn. Green shoots in ash. First breath after drowning. Morning. Smoke. Green shoots. Begin again."}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "There's a voice on the radio that shouldn't be there", "scene": {"mood": "mystery", "colors": ["deep blue", "shadow", "candle"], "composition": "wide shot", "camera": "static", "description": "Shadow figure in doorway. Candle. Face half-lit. Eyes knowing. There's a voice on the radio that shouldn't be there"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Speaking my name in a language I almost understand", "scene": {"mood": "loneliness", "colors": ["single light", "dark", "cold blue"], "composition": "close-up", "camera": "slow pan", "description": "Single light in vast dark. Figure beneath it. Nothing else. Speaking my name in a language I almost understand"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "I turn the dial but it follows like a shadow made of sound", "scene": {"mood": "curiosity", "colors": ["warm yellow", "spotlight", "discovery"], "composition": "over the shoulder", "camera": "dolly in", "description": "Light moving across a surface. Discovery. Eyes widening. I turn the dial but it follows like a shadow made of sound"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "Then it says something only I would know, something buried deep", "scene": {"mood": "connection", "colors": ["warm gold", "rose", "blush"], "composition": "low angle", "camera": "dolly out", "description": "Two hands reaching. Fingers almost touching. Warm light between them. Then it says something only I would know, something buried deep"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "I'm not afraid anymore \u2014 I'm listening", "scene": {"mood": "paranoia", "colors": ["surveillance green", "strobe", "red"], "composition": "high angle", "camera": "handheld", "description": "Surveillance angles. Green tint. Multiple screens. Watching. Being watched. I'm not afraid anymore \u2014 I'm listening"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The voice knows my dreams, it describes them back to me", "scene": {"mood": "intimacy", "colors": ["candlelight", "warm", "close"], "composition": "dutch angle", "camera": "steadicam", "description": "Candlelight only. Two faces close. Shared breath. The world outside forgotten. The voice knows my dreams, it describes them back to me"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "We're having a conversation across some membrane I can't see", "scene": {"mood": "urgency", "colors": ["red", "black", "strobe white"], "composition": "symmetrical", "camera": "slow zoom", "description": "Handheld camera running. Blurred faces. Traffic. Heartbeat sound design. We're having a conversation across some membrane I can't see"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "Then static. Then nothing. Then a whisper: find me", "scene": {"mood": "disconnection", "colors": ["static", "grey", "broken signal"], "composition": "rule of thirds", "camera": "crane up", "description": "Static. Snow on screen. A voice breaking up. Distance measured in noise. Then static. Then nothing. Then a whisper: find me"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "I search every frequency but the voice is gone", "scene": {"mood": "searching", "colors": ["flashlight beam", "dark", "moving light"], "composition": "extreme wide", "camera": "tracking shot", "description": "Flashlight beam cutting dark. Moving. Looking. Not finding yet. I search every frequency but the voice is gone"}}
|
||||
{"song": "Midnight Transmission", "artist": "Frequency Ghost", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Some nights I still hear it, faint, like a song in another room", "scene": {"mood": "haunting", "colors": ["faint blue", "echo", "silver"], "composition": "medium shot", "camera": "slow tilt down", "description": "Faint blue light. Echo of a figure. Present and absent simultaneously. Some nights I still hear it, faint, like a song in another room"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "I wore your love like a weapon and you never felt the blade", "scene": {"mood": "seduction", "colors": ["deep red", "velvet", "candlelight"], "composition": "wide shot", "camera": "static", "description": "Deep red. Velvet textures. Slow movement. Eyes that promise. I wore your love like a weapon and you never felt the blade"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "Every kiss was a negotiation, every touch a trade", "scene": {"mood": "power", "colors": ["gold", "black", "crimson"], "composition": "close-up", "camera": "slow pan", "description": "Throne. Gold. Black. The figure doesn't move. Doesn't need to. Every kiss was a negotiation, every touch a trade"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "The throne room smells like jasmine and someone else's fear", "scene": {"mood": "cruelty", "colors": ["cold silver", "black", "sharp white"], "composition": "over the shoulder", "camera": "dolly in", "description": "Silver blade. Cold light. A smile that doesn't reach the eyes. The throne room smells like jasmine and someone else's fear"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "I am beautiful when I'm angry \u2014 haven't you heard?", "scene": {"mood": "beauty", "colors": ["wildflower colors", "green", "sunlight"], "composition": "low angle", "camera": "dolly out", "description": "Wildflowers in unexpected places. Color against grey. Nature reclaiming. I am beautiful when I'm angry \u2014 haven't you heard?"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Don't mistake my gentleness for weakness, darling", "scene": {"mood": "danger", "colors": ["red", "black", "warning yellow"], "composition": "high angle", "camera": "handheld", "description": "Red and black. Warning signs. The frame contracts. Something approaches. Don't mistake my gentleness for weakness, darling"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "I chose to be kind. I could burn this kingdom down.", "scene": {"mood": "vulnerability", "colors": ["soft", "exposed", "raw"], "composition": "dutch angle", "camera": "steadicam", "description": "Exposed skin. Soft light. Eyes open. Trust visible in every pore. I chose to be kind. I could burn this kingdom down."}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The roses in my crown have thorns that curve inward", "scene": {"mood": "fury", "colors": ["dark red", "black", "flash"], "composition": "symmetrical", "camera": "slow zoom", "description": "Dark red wash. Hands destroying. Frame shaking with rage. The roses in my crown have thorns that curve inward"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "I bleed for my own sins, not for yours", "scene": {"mood": "grace", "colors": ["white", "silver", "flowing"], "composition": "rule of thirds", "camera": "crane up", "description": "White. Flowing. Movement without effort. The body as art. I bleed for my own sins, not for yours"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "Tonight I lay the crown aside and sleep without armor", "scene": {"mood": "revenge", "colors": ["dark", "steel", "cold blue"], "composition": "extreme wide", "camera": "tracking shot", "description": "Cold blue. Steel. The plan unfolding in shadows. Patience as weapon. Tonight I lay the crown aside and sleep without armor"}}
|
||||
{"song": "Crown of Thorns and Roses", "artist": "Velvet Guillotine", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Mercy. The hardest word. The only gift worth giving.", "scene": {"mood": "mercy", "colors": ["warm gold", "white", "gentle"], "composition": "medium shot", "camera": "slow tilt down", "description": "Warm gold. Hand lowering a weapon. Choosing not to. The harder path. Mercy. The hardest word. The only gift worth giving."}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 1, "timestamp": "0:00", "duration": "30s", "lyric_line": "Four walls, one window, a view of another wall", "scene": {"mood": "claustrophobia", "colors": ["close walls", "yellow bulb", "cramped"], "composition": "wide shot", "camera": "static", "description": "Walls close. Ceiling low. Yellow bulb. No escape visible. Four walls, one window, a view of another wall"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 2, "timestamp": "0:30", "duration": "30s", "lyric_line": "The radiator clicks like a metronome for the damned", "scene": {"mood": "routine", "colors": ["grey", "institutional", "fluorescent"], "composition": "close-up", "camera": "slow pan", "description": "Fluorescent light. Same motion repeated. Clock on the wall. Time as loop. The radiator clicks like a metronome for the damned"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 3, "timestamp": "1:00", "duration": "30s", "lyric_line": "I've memorized every crack in the ceiling \u2014 they form a map", "scene": {"mood": "desperation", "colors": ["scratching", "clawing", "raw"], "composition": "over the shoulder", "camera": "dolly in", "description": "Hands clawing. Fingernails against surface. Raw need. Nothing held back. I've memorized every crack in the ceiling \u2014 they form a map"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 4, "timestamp": "1:30", "duration": "30s", "lyric_line": "In my mind I've left a hundred times, bought a farm, learned to fly", "scene": {"mood": "fantasy", "colors": ["dreamy", "pastel", "floating"], "composition": "low angle", "camera": "dolly out", "description": "Pastel. Floating. Impossible architecture. Gravity optional. In my mind I've left a hundred times, bought a farm, learned to fly"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 5, "timestamp": "2:00", "duration": "30s", "lyric_line": "Then one morning I open the door and just walk out", "scene": {"mood": "breakthrough", "colors": ["white burst", "open sky", "blinding"], "composition": "high angle", "camera": "handheld", "description": "White burst. Wall shattering. Open sky beyond. Freedom as explosion. Then one morning I open the door and just walk out"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 6, "timestamp": "2:30", "duration": "30s", "lyric_line": "The hallway is an ocean, the stairs are a mountain range", "scene": {"mood": "freedom", "colors": ["open sky", "blue", "green"], "composition": "dutch angle", "camera": "steadicam", "description": "Open road. Blue sky. Green fields. Wind in hair. No walls. The hallway is an ocean, the stairs are a mountain range"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 7, "timestamp": "3:00", "duration": "30s", "lyric_line": "The street hits me like cold water and I almost go back", "scene": {"mood": "fear", "colors": ["cold", "dark", "sharp"], "composition": "symmetrical", "camera": "slow zoom", "description": "Cold. Dark. Sharp edges. The frame contracts. Something unseen. The street hits me like cold water and I almost go back"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 8, "timestamp": "3:30", "duration": "30s", "lyric_line": "But the sky \u2014 have you seen the sky? It goes on forever", "scene": {"mood": "joy", "colors": ["bright", "multi", "saturated"], "composition": "rule of thirds", "camera": "crane up", "description": "Saturated color. Wide smiles. Arms open. The world in full bloom. But the sky \u2014 have you seen the sky? It goes on forever"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 9, "timestamp": "4:00", "duration": "30s", "lyric_line": "I stand on the sidewalk and cry because the world is so big", "scene": {"mood": "grounding", "colors": ["neutral"], "composition": "extreme wide", "camera": "tracking shot", "description": "Visual interpretation of: I stand on the sidewalk and cry because the world is so big"}}
|
||||
{"song": "Apartment 4B", "artist": "Wallpaper & Wire", "beat": 10, "timestamp": "4:30", "duration": "30s", "lyric_line": "Home is not a place. Home is the moment you stop hiding.", "scene": {"mood": "home", "colors": ["neutral"], "composition": "medium shot", "camera": "slow tilt down", "description": "Visual interpretation of: Home is not a place. Home is the moment you stop hiding."}}
|
||||
Reference in New Issue
Block a user