Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
3d9b8af136 fix: harden cron audit workflow for #662
Some checks failed
Smoke Test / smoke (pull_request) Failing after 23s
Architecture Lint / Linter Tests (pull_request) Successful in 26s
Validate Config / YAML Lint (pull_request) Failing after 17s
Validate Config / JSON Validate (pull_request) Successful in 20s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 55s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 1m0s
Validate Config / Cron Syntax Check (pull_request) Successful in 11s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 12s
Validate Config / Playbook Schema Validation (pull_request) Successful in 23s
Architecture Lint / Lint Repository (pull_request) Failing after 19s
PR Checklist / pr-checklist (pull_request) Successful in 3m0s
Split the audit into an importable cron_audit_662 module plus a CLI wrapper,
classify recent systemic failures by error signature instead of age alone,
and include enough metadata for issue filing and delivery-failure reporting.

Add regression tests for import-path loading, systemic vs transient
classification, and issue body generation.
2026-04-22 14:48:42 -04:00
6 changed files with 791 additions and 553 deletions

0
scripts/ci-cron-validate.py Normal file → Executable file
View File

425
scripts/cron-audit-662.py Normal file → Executable file
View File

@@ -1,428 +1,11 @@
#!/usr/bin/env python3
"""
Cron Fleet Audit Script — #662
"""CLI wrapper for the importable cron_audit_662 module."""
Reads hermes cron job state, categorizes all jobs into:
- healthy: last_status=ok or never-run-and-enabled
- transient: recent errors (likely network/timeout)
- systemic: repeated errors over 48+ hours
Outputs a JSON report and optionally:
--disable Disable systemic jobs erroring 48+ hours
--issues File Gitea issues for systemic failures
"""
import json
import sys
import os
import argparse
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import List, Dict, Any
import sys
# --- Config ---
ERROR_THRESHOLD_HOURS = 48
CRON_STATE_PATHS = [
Path.home() / ".hermes" / "cron" / "jobs.json",
Path.home() / ".hermes" / "cron" / "state.json",
Path("/root/.hermes/cron/jobs.json"),
Path("/root/.hermes/cron/state.json"),
]
def load_cron_state() -> List[Dict[str, Any]]:
"""Load cron job state from known locations."""
for path in CRON_STATE_PATHS:
if path.exists():
try:
with open(path) as f:
data = json.load(f)
if isinstance(data, dict) and "jobs" in data:
return data["jobs"]
if isinstance(data, list):
return data
except (json.JSONDecodeError, IOError):
continue
# Fallback: try hermes cron list CLI
try:
import subprocess
result = subprocess.run(
["hermes", "cron", "list", "--json"],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
data = json.loads(result.stdout)
if isinstance(data, dict) and "jobs" in data:
return data["jobs"]
if isinstance(data, list):
return data
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
pass
return []
def parse_timestamp(ts: str) -> datetime:
"""Parse ISO timestamp, handle various formats."""
if not ts:
return None
# Normalize timezone
ts = ts.replace("+00:00", "+00:00")
try:
dt = datetime.fromisoformat(ts)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
return None
def categorize_job(job: Dict[str, Any], now: datetime) -> Dict[str, Any]:
"""Categorize a single job."""
status = job.get("last_status", "")
last_error = job.get("last_error", "")
last_run = parse_timestamp(job.get("last_run_at"))
enabled = job.get("enabled", False)
state = job.get("state", "unknown")
name = job.get("name", job.get("id", "unknown"))
entry = {
"id": job.get("id", ""),
"name": name,
"schedule": job.get("schedule_display", str(job.get("schedule", ""))),
"state": state,
"enabled": enabled,
"last_status": status,
"last_error": last_error,
"last_run_at": job.get("last_run_at"),
"category": "healthy",
"reason": "",
"action": "",
}
# Never run / no error
if status is None and not last_error:
entry["category"] = "healthy"
entry["reason"] = "Never run, no errors"
return entry
# Explicitly paused with reason
if state == "paused":
entry["category"] = "healthy"
entry["reason"] = job.get("paused_reason", "Manually paused")
entry["action"] = "none — paused intentionally"
return entry
# Completed jobs
if state == "completed":
entry["category"] = "healthy"
entry["reason"] = "Completed (one-shot)"
return entry
# Error status
if status == "error" and last_error:
age_hours = None
if last_run:
age_hours = (now - last_run).total_seconds() / 3600
if age_hours is not None and age_hours >= ERROR_THRESHOLD_HOURS:
entry["category"] = "systemic"
entry["reason"] = f"Erroring for {age_hours:.1f}h (>{ERROR_THRESHOLD_HOURS}h threshold)"
entry["action"] = "disable"
else:
entry["category"] = "transient"
age_str = f"{age_hours:.1f}h ago" if age_hours is not None else "unknown age"
entry["reason"] = f"Recent error ({age_str}), may be transient"
entry["action"] = "monitor"
return entry
# OK status
if status == "ok":
entry["category"] = "healthy"
entry["reason"] = "Last run succeeded"
return entry
# Scheduled but never errored
if state == "scheduled" and enabled:
entry["category"] = "healthy"
entry["reason"] = "Scheduled and running"
return entry
# Unknown state
entry["category"] = "transient"
entry["reason"] = f"Unknown state: {state}, status: {status}"
entry["action"] = "investigate"
return entry
def audit_jobs(jobs: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Run full audit on job list."""
now = datetime.now(timezone.utc)
categorized = [categorize_job(j, now) for j in jobs]
healthy = [c for c in categorized if c["category"] == "healthy"]
transient = [c for c in categorized if c["category"] == "transient"]
systemic = [c for c in categorized if c["category"] == "systemic"]
report = {
"audit_time": now.isoformat(),
"total_jobs": len(jobs),
"summary": {
"healthy": len(healthy),
"transient_errors": len(transient),
"systemic_failures": len(systemic),
},
"systemic_jobs": [
{
"id": j["id"],
"name": j["name"],
"reason": j["reason"],
"last_error": j["last_error"],
}
for j in systemic
],
"transient_jobs": [
{
"id": j["id"],
"name": j["name"],
"reason": j["reason"],
}
for j in transient
],
"all_jobs": categorized,
}
return report
def generate_issue_body(job: Dict[str, Any]) -> str:
"""Generate a Gitea issue body for a systemic cron failure."""
return f"""## Systemic Cron Failure — Auto-Filed by Audit #662
**Job:** {job['name']} (`{job['id']}`)
**Schedule:** {job['schedule']}
**State:** {job['state']}
**Last Error:**
```
{job['last_error'] or 'No error details available'}
```
**Audit Finding:** {job['reason']}
### Action Required
- [ ] Diagnose root cause of repeated failure
- [ ] Fix configuration or remove broken job
- [ ] Verify job resumes healthy after fix
*Auto-generated by cron-audit-662.py*
"""
# --- Crontab Parsing ---
def parse_crontab(text: str, source: str = "unknown") -> list:
"""Parse a crontab file into job-like dicts for audit."""
import re
jobs = []
cron_pattern = re.compile(
r'^(?:@\w+|[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+)\s+(.*)'
)
schedule_pattern = re.compile(
r'^(?:@\w+|[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+)'
)
for line in text.split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
sm = schedule_pattern.match(line)
cm = cron_pattern.match(line)
if not sm or not cm:
continue
schedule_raw = sm.group(0).strip()
command = cm.group(1).strip()
name_part = command.split("#")[-1].strip() if "#" in command else ""
if not name_part:
cmd_base = command.split(">>")[0].strip().split()
name_part = os.path.basename(cmd_base[-1]) if cmd_base else "unnamed"
clean_cmd = command.split(">>")[0].split("#")[0].strip()[:200]
jobs.append({
"id": f"crontab-{source}-{hash(command) % 10000:04x}",
"name": name_part,
"schedule_display": schedule_raw,
"schedule": schedule_raw,
"state": "scheduled",
"enabled": True,
"last_status": None,
"last_error": None,
"last_run_at": None,
"_source": f"crontab:{source}",
"_command": clean_cmd,
})
return jobs
def load_crontab_backups(backup_dir) -> list:
"""Load cron jobs from VPS crontab backup files."""
from pathlib import Path
backup_dir = Path(backup_dir)
all_jobs = []
if not backup_dir.exists():
return all_jobs
for fpath in sorted(backup_dir.glob("*-crontab-backup.txt")):
source = fpath.name.replace("-crontab-backup.txt", "")
text = fpath.read_text()
all_jobs.extend(parse_crontab(text, source=source))
return all_jobs
def audit_fleet(hermes_jobs: list, crontab_jobs: list) -> dict:
"""Run full fleet audit combining hermes cron + VPS crontabs."""
now = datetime.now(timezone.utc)
hermes_categorized = [categorize_job(j, now) for j in hermes_jobs]
crontab_categorized = []
for j in crontab_jobs:
crontab_categorized.append({
"id": j["id"], "name": j["name"],
"schedule": j.get("schedule_display", ""),
"state": "scheduled", "enabled": True,
"last_status": None, "last_error": None, "last_run_at": None,
"category": "healthy",
"reason": f"Crontab ({j.get('_source', '?')}) — verify logs manually",
"action": "verify-logs",
})
all_cat = hermes_categorized + crontab_categorized
healthy = [c for c in all_cat if c["category"] == "healthy"]
transient = [c for c in all_cat if c["category"] == "transient"]
systemic = [c for c in all_cat if c["category"] == "systemic"]
return {
"audit_time": now.isoformat(),
"total_jobs": len(all_cat),
"hermes_jobs": len(hermes_categorized),
"crontab_jobs": len(crontab_categorized),
"summary": {"healthy": len(healthy), "transient_errors": len(transient), "systemic_failures": len(systemic)},
"systemic_jobs": [{"id": j["id"], "name": j["name"], "reason": j["reason"], "last_error": j.get("last_error", "")} for j in systemic],
"transient_jobs": [{"id": j["id"], "name": j["name"], "reason": j["reason"]} for j in transient],
"all_jobs": all_cat,
}
def main():
parser = argparse.ArgumentParser(description="Cron fleet audit (#662)")
parser.add_argument("--jobs-file", help="Path to jobs.json override")
parser.add_argument("--disable", action="store_true",
help="Disable systemic jobs (requires hermes CLI)")
parser.add_argument("--issues", action="store_true",
help="File Gitea issues for systemic failures")
parser.add_argument("--output", help="Write report to file")
parser.add_argument("--json", action="store_true", help="JSON output only")
args = parser.parse_args()
# Load jobs
jobs = []
if args.jobs_file:
with open(args.jobs_file) as f:
data = json.load(f)
jobs = data.get("jobs", data) if isinstance(data, dict) else data
else:
jobs = load_cron_state()
# Also load VPS crontab backups
crontab_dir = Path(__file__).parent.parent / "cron" / "vps"
crontab_jobs = load_crontab_backups(crontab_dir)
if not jobs:
print("ERROR: No cron jobs found. Check ~/.hermes/cron/ or run 'hermes cron list'.")
sys.exit(1)
# Run audit
if crontab_jobs:
report = audit_fleet(jobs, crontab_jobs)
else:
report = audit_jobs(jobs)
# Output
if args.json:
print(json.dumps(report, indent=2))
else:
print(f"\n{'='*60}")
print(f" CRON FLEET AUDIT — {report['total_jobs']} jobs")
print(f"{'='*60}")
print(f" Healthy: {report['summary']['healthy']}")
print(f" Transient errors: {report['summary']['transient_errors']}")
print(f" Systemic failures: {report['summary']['systemic_failures']}")
print(f"{'='*60}")
if report["systemic_jobs"]:
print(f"\n SYSTEMIC FAILURES (>{ERROR_THRESHOLD_HOURS}h):")
for j in report["systemic_jobs"]:
print(f" - {j['name']} ({j['id']}): {j['reason']}")
if j["last_error"]:
print(f" Error: {j['last_error'][:100]}")
if report["transient_jobs"]:
print(f"\n TRANSIENT ERRORS:")
for j in report["transient_jobs"]:
print(f" - {j['name']} ({j['id']}): {j['reason']}")
print()
# Write report file
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"Report written to {args.output}")
# Disable systemic jobs
if args.disable and report["systemic_jobs"]:
import subprocess
for j in report["systemic_jobs"]:
print(f"Disabling: {j['name']} ({j['id']})")
try:
subprocess.run(
["hermes", "cron", "pause", j["id"]],
capture_output=True, text=True, timeout=10
)
print(f" → Disabled")
except Exception as e:
print(f" → Failed: {e}")
# File issues for systemic failures
if args.issues and report["systemic_jobs"]:
gitea_token = os.environ.get("GITEA_TOKEN") or ""
if not gitea_token:
token_path = Path.home() / ".config" / "gitea" / "token"
if token_path.exists():
gitea_token = token_path.read_text().strip()
if not gitea_token:
print("ERROR: No Gitea token found. Set GITEA_TOKEN or ~/.config/gitea/token")
sys.exit(1)
import urllib.request
base = "https://forge.alexanderwhitestone.com/api/v1"
headers = {
"Authorization": f"token {gitea_token}",
"Content-Type": "application/json",
}
for j in report["systemic_jobs"]:
title = f"CRON FAIL: {j['name']} — systemic error ({j['id']})"
body = generate_issue_body(j)
data = json.dumps({"title": title, "body": body}).encode()
req = urllib.request.Request(
f"{base}/repos/Timmy_Foundation/timmy-config/issues",
data=data, headers=headers, method="POST"
)
try:
resp = urllib.request.urlopen(req)
result = json.loads(resp.read())
print(f"Issued #{result['number']}: {title}")
except Exception as e:
print(f"Failed to file issue for {j['name']}: {e}")
# Exit code: non-zero if systemic failures found
sys.exit(1 if report["systemic_jobs"] else 0)
sys.path.insert(0, str(Path(__file__).resolve().parent))
from cron_audit_662 import main
if __name__ == "__main__":

630
scripts/cron_audit_662.py Executable file
View File

@@ -0,0 +1,630 @@
#!/usr/bin/env python3
"""
Cron Fleet Audit Script — #662
Reads hermes cron job state, categorizes all jobs into:
- healthy: last_status=ok or never-run-and-enabled
- transient: recent errors (likely network/timeout)
- systemic: repeated errors over 48+ hours
Outputs a JSON report and optionally:
--disable Disable systemic jobs erroring 48+ hours
--issues File Gitea issues for systemic failures
"""
import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
# --- Config ---
ERROR_THRESHOLD_HOURS = 48
TRANSIENT_ERROR_MARKERS = (
"timeout",
"timed out",
"rate limit",
"429",
"503",
"temporary",
"temporarily",
"auth store lock",
"connection reset",
"connection refused",
"connection aborted",
"delivery error",
"telegram send failed",
"matrix send failed",
"nodename nor servname provided",
"name or service not known",
"empty response",
)
SYSTEMIC_ERROR_MARKERS = (
"cannot import",
"modulenotfounderror",
"importerror",
"attributeerror",
"syntaxerror",
"permission denied",
"no such file",
"file not found",
"dict' object has no attribute",
'dict" object has no attribute',
)
CRON_STATE_PATHS = [
Path.home() / ".hermes" / "cron" / "jobs.json",
Path.home() / ".hermes" / "cron" / "state.json",
Path("/root/.hermes/cron/jobs.json"),
Path("/root/.hermes/cron/state.json"),
]
def load_jobs_file(path: Path) -> List[Dict[str, Any]]:
"""Load a cron jobs/state JSON file."""
with path.open() as f:
data = json.load(f)
if isinstance(data, dict) and "jobs" in data:
return data["jobs"]
if isinstance(data, list):
return data
return []
def parse_cron_list_output(text: str) -> List[Dict[str, Any]]:
"""Parse `hermes cron list --all` output as a last-resort fallback."""
jobs: List[Dict[str, Any]] = []
current: Optional[Dict[str, Any]] = None
job_header = re.compile(r"^\s{2}(?P<id>\S+) \[(?P<state>[^\]]+)\]\s*$")
for raw_line in text.splitlines():
line = raw_line.rstrip()
match = job_header.match(line)
if match:
if current:
jobs.append(current)
state = match.group("state").strip().lower()
current = {
"id": match.group("id"),
"name": match.group("id"),
"schedule_display": "",
"schedule": "",
"state": "paused" if state == "paused" else "scheduled",
"enabled": state == "active",
"last_status": None,
"last_error": None,
"last_delivery_error": None,
"last_run_at": None,
}
continue
if not current:
continue
stripped = line.strip()
if stripped.startswith("Name:"):
current["name"] = stripped.split("Name:", 1)[1].strip()
elif stripped.startswith("Schedule:"):
schedule = stripped.split("Schedule:", 1)[1].strip()
current["schedule_display"] = schedule
current["schedule"] = schedule
elif stripped.startswith("Last run:"):
payload = stripped.split("Last run:", 1)[1].strip()
if payload in {"-", ""}:
continue
if " error: " in payload:
ts, error = payload.split(" error: ", 1)
current["last_run_at"] = ts.strip()
current["last_status"] = "error"
current["last_error"] = error.strip()
elif payload.endswith(" ok"):
current["last_run_at"] = payload[:-4].strip()
current["last_status"] = "ok"
else:
current["last_run_at"] = payload
if current:
jobs.append(current)
return jobs
def load_cron_state() -> List[Dict[str, Any]]:
"""Load cron job state from known locations."""
for path in CRON_STATE_PATHS:
if not path.exists():
continue
try:
jobs = load_jobs_file(path)
if jobs:
return jobs
except (json.JSONDecodeError, IOError):
continue
try:
result = subprocess.run(
["hermes", "cron", "list", "--all"],
capture_output=True,
text=True,
timeout=30,
)
jobs = parse_cron_list_output(result.stdout)
if jobs:
return jobs
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return []
def parse_timestamp(ts: Optional[str]) -> Optional[datetime]:
"""Parse ISO timestamp, handle various formats."""
if not ts:
return None
ts = str(ts).strip().replace("Z", "+00:00")
try:
dt = datetime.fromisoformat(ts)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
return None
def format_schedule(job: Dict[str, Any]) -> str:
"""Normalize mixed schedule formats into a readable string."""
display = job.get("schedule_display")
if display:
return str(display)
schedule = job.get("schedule")
if isinstance(schedule, dict):
return (
schedule.get("display")
or schedule.get("expr")
or schedule.get("value")
or json.dumps(schedule, sort_keys=True)
)
return str(schedule or "")
def find_error_marker(text: str, markers: tuple[str, ...]) -> Optional[str]:
for marker in markers:
if marker in text:
return marker
return None
def classify_error(last_error: str, last_delivery_error: str, age_hours: Optional[float]) -> tuple[str, str, str]:
"""Classify an error as transient vs systemic using both age and error text."""
combined = "\n".join(part for part in [last_error, last_delivery_error] if part).lower()
age_text = f"{age_hours:.1f}h ago" if age_hours is not None else "unknown age"
if age_hours is not None and age_hours >= ERROR_THRESHOLD_HOURS:
return (
"systemic",
f"Error persisted for {age_hours:.1f}h (>= {ERROR_THRESHOLD_HOURS}h threshold)",
"disable",
)
systemic_marker = find_error_marker(combined, SYSTEMIC_ERROR_MARKERS)
if systemic_marker:
return (
"systemic",
f"Systemic error signature: {systemic_marker} ({age_text})",
"disable",
)
transient_marker = find_error_marker(combined, TRANSIENT_ERROR_MARKERS)
if transient_marker:
return (
"transient",
f"Transient error signature: {transient_marker} ({age_text})",
"monitor",
)
return (
"transient",
f"Unclassified recent error ({age_text})",
"investigate",
)
def categorize_job(job: Dict[str, Any], now: datetime) -> Dict[str, Any]:
"""Categorize a single job."""
status = job.get("last_status", "")
last_error = job.get("last_error", "")
last_delivery_error = job.get("last_delivery_error", "")
last_run = parse_timestamp(job.get("last_run_at"))
enabled = job.get("enabled", False)
state = job.get("state", "unknown")
name = job.get("name", job.get("id", "unknown"))
entry = {
"id": job.get("id", ""),
"name": name,
"schedule": format_schedule(job),
"state": state,
"enabled": enabled,
"last_status": status,
"last_error": last_error,
"last_delivery_error": last_delivery_error,
"last_run_at": job.get("last_run_at"),
"category": "healthy",
"reason": "",
"action": "",
}
# Never run / no error
if status is None and not last_error:
entry["category"] = "healthy"
entry["reason"] = "Never run, no errors"
return entry
# Explicitly paused with reason
if state == "paused":
entry["category"] = "healthy"
entry["reason"] = job.get("paused_reason", "Manually paused")
entry["action"] = "none — paused intentionally"
return entry
# Completed jobs
if state == "completed":
entry["category"] = "healthy"
entry["reason"] = "Completed (one-shot)"
return entry
# Error status
if status == "error":
age_hours = None
if last_run:
age_hours = (now - last_run).total_seconds() / 3600
entry["category"], entry["reason"], entry["action"] = classify_error(
str(last_error or ""),
str(last_delivery_error or ""),
age_hours,
)
return entry
if status == "ok" and last_delivery_error:
entry["category"] = "transient"
entry["reason"] = f"Job completed but delivery failed: {last_delivery_error}"
entry["action"] = "monitor"
return entry
# OK status
if status == "ok":
entry["category"] = "healthy"
entry["reason"] = "Last run succeeded"
return entry
# Scheduled but never errored
if state == "scheduled" and enabled:
entry["category"] = "healthy"
entry["reason"] = "Scheduled and running"
return entry
# Unknown state
entry["category"] = "transient"
entry["reason"] = f"Unknown state: {state}, status: {status}"
entry["action"] = "investigate"
return entry
def audit_jobs(jobs: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Run full audit on job list."""
now = datetime.now(timezone.utc)
categorized = [categorize_job(j, now) for j in jobs]
healthy = [c for c in categorized if c["category"] == "healthy"]
transient = [c for c in categorized if c["category"] == "transient"]
systemic = [c for c in categorized if c["category"] == "systemic"]
report = {
"audit_time": now.isoformat(),
"total_jobs": len(jobs),
"summary": {
"healthy": len(healthy),
"transient_errors": len(transient),
"systemic_failures": len(systemic),
},
"systemic_jobs": [
{
"id": j["id"],
"name": j["name"],
"schedule": j["schedule"],
"state": j["state"],
"reason": j["reason"],
"last_error": j["last_error"],
"last_delivery_error": j["last_delivery_error"],
"last_run_at": j["last_run_at"],
"action": j["action"],
}
for j in systemic
],
"transient_jobs": [
{
"id": j["id"],
"name": j["name"],
"schedule": j["schedule"],
"state": j["state"],
"reason": j["reason"],
"last_error": j["last_error"],
"last_delivery_error": j["last_delivery_error"],
"last_run_at": j["last_run_at"],
"action": j["action"],
}
for j in transient
],
"all_jobs": categorized,
}
return report
def generate_issue_body(job: Dict[str, Any]) -> str:
"""Generate a Gitea issue body for a systemic cron failure."""
delivery_error = job.get("last_delivery_error") or ""
delivery_block = ""
if delivery_error:
delivery_block = f"\n**Last Delivery Error:**\n```\n{delivery_error}\n```\n"
return f"""## Systemic Cron Failure — Auto-Filed by Audit #662
**Job:** {job.get('name', 'unknown')} (`{job.get('id', 'unknown')}`)
**Schedule:** {job.get('schedule', 'unknown')}
**State:** {job.get('state', 'unknown')}
**Last Error:**
```
{job.get('last_error') or 'No error details available'}
```
{delivery_block}
**Audit Finding:** {job.get('reason', 'No audit reason captured')}
### Action Required
- [ ] Diagnose root cause of repeated failure
- [ ] Fix configuration or remove broken job
- [ ] Verify job resumes healthy after fix
*Auto-generated by cron-audit-662.py*
"""
# --- Crontab Parsing ---
def parse_crontab(text: str, source: str = "unknown") -> list:
"""Parse a crontab file into job-like dicts for audit."""
import re
jobs = []
cron_pattern = re.compile(
r'^(?:@\w+|[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+)\s+(.*)'
)
schedule_pattern = re.compile(
r'^(?:@\w+|[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+\s+[\d*,/\-]+)'
)
for line in text.split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
sm = schedule_pattern.match(line)
cm = cron_pattern.match(line)
if not sm or not cm:
continue
schedule_raw = sm.group(0).strip()
command = cm.group(1).strip()
name_part = command.split("#")[-1].strip() if "#" in command else ""
if not name_part:
cmd_base = command.split(">>")[0].strip().split()
name_part = os.path.basename(cmd_base[-1]) if cmd_base else "unnamed"
clean_cmd = command.split(">>")[0].split("#")[0].strip()[:200]
jobs.append({
"id": f"crontab-{source}-{hash(command) % 10000:04x}",
"name": name_part,
"schedule_display": schedule_raw,
"schedule": schedule_raw,
"state": "scheduled",
"enabled": True,
"last_status": None,
"last_error": None,
"last_run_at": None,
"_source": f"crontab:{source}",
"_command": clean_cmd,
})
return jobs
def load_crontab_backups(backup_dir) -> list:
"""Load cron jobs from VPS crontab backup files."""
from pathlib import Path
backup_dir = Path(backup_dir)
all_jobs = []
if not backup_dir.exists():
return all_jobs
for fpath in sorted(backup_dir.glob("*-crontab-backup.txt")):
source = fpath.name.replace("-crontab-backup.txt", "")
text = fpath.read_text()
all_jobs.extend(parse_crontab(text, source=source))
return all_jobs
def audit_fleet(hermes_jobs: list, crontab_jobs: list) -> dict:
"""Run full fleet audit combining hermes cron + VPS crontabs."""
now = datetime.now(timezone.utc)
hermes_categorized = [categorize_job(j, now) for j in hermes_jobs]
crontab_categorized = []
for j in crontab_jobs:
crontab_categorized.append({
"id": j["id"], "name": j["name"],
"schedule": j.get("schedule_display", ""),
"state": "scheduled", "enabled": True,
"last_status": None, "last_error": None, "last_delivery_error": None, "last_run_at": None,
"category": "healthy",
"reason": f"Crontab ({j.get('_source', '?')}) — verify logs manually",
"action": "verify-logs",
})
all_cat = hermes_categorized + crontab_categorized
healthy = [c for c in all_cat if c["category"] == "healthy"]
transient = [c for c in all_cat if c["category"] == "transient"]
systemic = [c for c in all_cat if c["category"] == "systemic"]
return {
"audit_time": now.isoformat(),
"total_jobs": len(all_cat),
"hermes_jobs": len(hermes_categorized),
"crontab_jobs": len(crontab_categorized),
"summary": {"healthy": len(healthy), "transient_errors": len(transient), "systemic_failures": len(systemic)},
"systemic_jobs": [
{
"id": j["id"],
"name": j["name"],
"schedule": j["schedule"],
"state": j["state"],
"reason": j["reason"],
"last_error": j.get("last_error", ""),
"last_delivery_error": j.get("last_delivery_error", ""),
"last_run_at": j.get("last_run_at"),
"action": j.get("action", ""),
}
for j in systemic
],
"transient_jobs": [
{
"id": j["id"],
"name": j["name"],
"schedule": j["schedule"],
"state": j["state"],
"reason": j["reason"],
"last_error": j.get("last_error", ""),
"last_delivery_error": j.get("last_delivery_error", ""),
"last_run_at": j.get("last_run_at"),
"action": j.get("action", ""),
}
for j in transient
],
"all_jobs": all_cat,
}
def main():
parser = argparse.ArgumentParser(description="Cron fleet audit (#662)")
parser.add_argument("--jobs-file", help="Path to jobs.json override")
parser.add_argument("--disable", action="store_true",
help="Disable systemic jobs (requires hermes CLI)")
parser.add_argument("--issues", action="store_true",
help="File Gitea issues for systemic failures")
parser.add_argument("--output", help="Write report to file")
parser.add_argument("--json", action="store_true", help="JSON output only")
args = parser.parse_args()
# Load jobs
jobs = []
if args.jobs_file:
with open(args.jobs_file) as f:
data = json.load(f)
jobs = data.get("jobs", data) if isinstance(data, dict) else data
else:
jobs = load_cron_state()
# Also load VPS crontab backups
crontab_dir = Path(__file__).parent.parent / "cron" / "vps"
crontab_jobs = load_crontab_backups(crontab_dir)
if not jobs:
print("ERROR: No cron jobs found. Check ~/.hermes/cron/ or run 'hermes cron list'.")
sys.exit(1)
# Run audit
if crontab_jobs:
report = audit_fleet(jobs, crontab_jobs)
else:
report = audit_jobs(jobs)
# Output
if args.json:
print(json.dumps(report, indent=2))
else:
print(f"\n{'='*60}")
print(f" CRON FLEET AUDIT — {report['total_jobs']} jobs")
print(f"{'='*60}")
print(f" Healthy: {report['summary']['healthy']}")
print(f" Transient errors: {report['summary']['transient_errors']}")
print(f" Systemic failures: {report['summary']['systemic_failures']}")
print(f"{'='*60}")
if report["systemic_jobs"]:
print(f"\n SYSTEMIC FAILURES (>{ERROR_THRESHOLD_HOURS}h):")
for j in report["systemic_jobs"]:
print(f" - {j['name']} ({j['id']}): {j['reason']}")
if j["last_error"]:
print(f" Error: {j['last_error'][:100]}")
if report["transient_jobs"]:
print(f"\n TRANSIENT ERRORS:")
for j in report["transient_jobs"]:
print(f" - {j['name']} ({j['id']}): {j['reason']}")
print()
# Write report file
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"Report written to {args.output}")
# Disable systemic jobs
if args.disable and report["systemic_jobs"]:
import subprocess
for j in report["systemic_jobs"]:
print(f"Disabling: {j['name']} ({j['id']})")
try:
subprocess.run(
["hermes", "cron", "pause", j["id"]],
capture_output=True, text=True, timeout=10
)
print(f" → Disabled")
except Exception as e:
print(f" → Failed: {e}")
# File issues for systemic failures
if args.issues and report["systemic_jobs"]:
gitea_token = os.environ.get("GITEA_TOKEN") or ""
if not gitea_token:
token_path = Path.home() / ".config" / "gitea" / "token"
if token_path.exists():
gitea_token = token_path.read_text().strip()
if not gitea_token:
print("ERROR: No Gitea token found. Set GITEA_TOKEN or ~/.config/gitea/token")
sys.exit(1)
import urllib.request
base = "https://forge.alexanderwhitestone.com/api/v1"
headers = {
"Authorization": f"token {gitea_token}",
"Content-Type": "application/json",
}
for j in report["systemic_jobs"]:
title = f"CRON FAIL: {j['name']} — systemic error ({j['id']})"
body = generate_issue_body(j)
data = json.dumps({"title": title, "body": body}).encode()
req = urllib.request.Request(
f"{base}/repos/Timmy_Foundation/timmy-config/issues",
data=data, headers=headers, method="POST"
)
try:
resp = urllib.request.urlopen(req)
result = json.loads(resp.read())
print(f"Issued #{result['number']}: {title}")
except Exception as e:
print(f"Failed to file issue for {j['name']}: {e}")
# Exit code: non-zero if systemic failures found
sys.exit(1 if report["systemic_jobs"] else 0)
if __name__ == "__main__":
main()

View File

@@ -71,6 +71,43 @@ class TestCategorizeJob:
r = categorize_job({"name": "t", "state": "paused", "enabled": False}, datetime.now(timezone.utc))
assert r["category"] == "healthy"
def test_import_error_is_systemic_even_when_recent(self):
from cron_audit_662 import categorize_job
now = datetime.now(timezone.utc)
r = categorize_job({
"name": "t",
"last_status": "error",
"last_error": "cannot import name 'AIAgent' from 'run_agent'",
"last_run_at": (now - timedelta(hours=1)).isoformat(),
}, now)
assert r["category"] == "systemic"
assert r["action"] == "disable"
def test_empty_response_stays_transient(self):
from cron_audit_662 import categorize_job
now = datetime.now(timezone.utc)
r = categorize_job({
"name": "t",
"last_status": "error",
"last_error": "Agent completed but produced empty response (model error, timeout, or misconfiguration)",
"last_run_at": (now - timedelta(hours=1)).isoformat(),
}, now)
assert r["category"] == "transient"
def test_delivery_failure_after_success_is_transient(self):
from cron_audit_662 import categorize_job
now = datetime.now(timezone.utc)
r = categorize_job({
"name": "t",
"last_status": "ok",
"last_delivery_error": "delivery error: Telegram send failed: Timed out",
"last_run_at": now.isoformat(),
"enabled": True,
"state": "scheduled",
}, now)
assert r["category"] == "transient"
assert "delivery failed" in r["reason"]
class TestAuditFleet:
def test_empty(self):
@@ -116,3 +153,23 @@ class TestTimestampParsing:
from cron_audit_662 import parse_timestamp
assert parse_timestamp("") is None
assert parse_timestamp(None) is None
class TestIssueBody:
def test_includes_schedule_state_and_delivery_error(self):
from cron_audit_662 import generate_issue_body
body = generate_issue_body({
"id": "job-1",
"name": "Health Monitor",
"schedule": "every 5m",
"state": "scheduled",
"last_error": "cannot import name 'tool' from 'tools.registry'",
"last_delivery_error": "delivery error: Telegram send failed: Timed out",
"reason": "Systemic error signature: cannot import (1.0h ago)",
})
assert "Health Monitor" in body
assert "every 5m" in body
assert "scheduled" in body
assert "Last Delivery Error" in body

View File

@@ -1,32 +0,0 @@
import json
from pathlib import Path
MIRROR_FILE = Path("training-data/scene-descriptions-folk.jsonl")
EXPECTED_ROWS = 100
REQUIRED_TOP_FIELDS = {"song", "artist", "beat", "timestamp", "duration_seconds", "lyric_line", "scene"}
REQUIRED_SCENE_FIELDS = {"mood", "colors", "composition", "camera", "description"}
def load_rows():
assert MIRROR_FILE.exists(), f"missing file: {MIRROR_FILE}"
return [json.loads(line) for line in MIRROR_FILE.read_text(encoding="utf-8").splitlines() if line.strip()]
def test_folk_mirror_has_100_unique_rows():
rows = load_rows()
assert len(rows) == EXPECTED_ROWS
assert len({json.dumps(row, sort_keys=True) for row in rows}) == EXPECTED_ROWS
def test_folk_mirror_has_required_fields_and_no_placeholders():
rows = load_rows()
for i, row in enumerate(rows, 1):
assert REQUIRED_TOP_FIELDS.issubset(row.keys()), f"line {i}: missing top-level fields"
assert isinstance(row["artist"], str) and row["artist"].strip(), f"line {i}: missing artist"
assert isinstance(row["timestamp"], str) and ":" in row["timestamp"], f"line {i}: bad timestamp"
assert isinstance(row["duration_seconds"], int) and row["duration_seconds"] > 0, f"line {i}: bad duration_seconds"
assert "[Beat" not in row["lyric_line"] and len(row["lyric_line"]) > 3, f"line {i}: placeholder lyric"
assert isinstance(row["scene"], dict), f"line {i}: scene must be object"
assert REQUIRED_SCENE_FIELDS.issubset(row["scene"].keys()), f"line {i}: missing scene fields"
assert isinstance(row["scene"]["colors"], list) and row["scene"]["colors"], f"line {i}: empty colors"
assert isinstance(row["scene"]["description"], str) and len(row["scene"]["description"]) >= 10, f"line {i}: short description"

View File

@@ -1,100 +1,100 @@
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 1, "timestamp": "0:00", "duration_seconds": 24, "lyric_line": "The wind took everything but her name", "scene": {"mood": "grief", "colors": ["charcoal", "deep blue", "ash"], "composition": "wide shot", "camera": "slow zoom out", "description": "A grief scene in folk register. high angle framing. handheld sway movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 2, "timestamp": "0:24", "duration_seconds": 26, "lyric_line": "Banjo strings like fence wire singing", "scene": {"mood": "resilience", "colors": ["green", "brown", "iron"], "composition": "dutch angle", "camera": "gentle pan right", "description": "A resilience scene in folk register. rule of thirds framing. steady hold movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 3, "timestamp": "0:50", "duration_seconds": 24, "lyric_line": "Dust bowl daughter, born of drought", "scene": {"mood": "anger", "colors": ["red", "black", "iron grey"], "composition": "high angle", "camera": "crane up", "description": "A anger scene in folk register. over-the-shoulder framing. tracking shot movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 4, "timestamp": "1:14", "duration_seconds": 28, "lyric_line": "She plants corn in the cracks of the earth", "scene": {"mood": "memory", "colors": ["sepia", "warm gold", "faded green"], "composition": "center frame", "camera": "handheld sway", "description": "A memory scene in folk register. symmetrical framing. handheld sway movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 5, "timestamp": "1:42", "duration_seconds": 24, "lyric_line": "Her grandmother's hymns in a minor key", "scene": {"mood": "strength", "colors": ["iron grey", "dark brown", "gold"], "composition": "silhouette frame", "camera": "static", "description": "A strength scene in folk register. dutch angle framing. circular orbit movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 6, "timestamp": "2:06", "duration_seconds": 26, "lyric_line": "The radio plays someone else's prayer", "scene": {"mood": "sorrow", "colors": ["deep blue", "grey", "silver"], "composition": "bird's eye", "camera": "slow zoom in", "description": "A sorrow scene in folk register. POV framing. static movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 7, "timestamp": "2:32", "duration_seconds": 24, "lyric_line": "But her song is older than the radio", "scene": {"mood": "hope", "colors": ["pale gold", "sky blue", "white"], "composition": "over-the-shoulder", "camera": "steady hold", "description": "A hope scene in folk register. high angle framing. gentle pan right movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 8, "timestamp": "2:56", "duration_seconds": 28, "lyric_line": "Dust bowl daughter — the soil remembers", "scene": {"mood": "defiance", "colors": ["black", "red", "silver"], "composition": "over-the-shoulder", "camera": "floating drift", "description": "A defiance scene in folk register. extreme close-up framing. steady hold movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 9, "timestamp": "3:24", "duration_seconds": 24, "lyric_line": "She carries the field in her calloused hands", "scene": {"mood": "pride", "colors": ["gold", "purple", "red"], "composition": "rule of thirds", "camera": "rack focus", "description": "A pride scene in folk register. over-the-shoulder framing. crane up movement."}}
{"song": "Dust Bowl Daughter", "artist": "Prarie Ghost", "beat": 10, "timestamp": "3:48", "duration_seconds": 30, "lyric_line": "Dust bowl daughter — what the land demands", "scene": {"mood": "continuity", "colors": ["gold", "green", "blue"], "composition": "symmetrical", "camera": "static", "description": "A continuity scene in folk register. low angle framing. static movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 1, "timestamp": "0:00", "duration_seconds": 26, "lyric_line": "The lantern burns for someone walking home", "scene": {"mood": "longing", "colors": ["slate blue", "silver", "dusk grey"], "composition": "bird's eye", "camera": "gentle pan right", "description": "A longing scene in folk register. rule of thirds framing. crane up movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 2, "timestamp": "0:26", "duration_seconds": 24, "lyric_line": "Fiddle like a voice calling through the dark", "scene": {"mood": "warmth", "colors": ["amber", "cream", "soft gold"], "composition": "dutch angle", "camera": "gentle pan right", "description": "A warmth scene in folk register. close-up framing. rack focus movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "Miles of road between the song and the singer", "scene": {"mood": "hope", "colors": ["pale gold", "sky blue", "white"], "composition": "close-up", "camera": "steady hold", "description": "A hope scene in folk register. extreme close-up framing. crane up movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "The porch creaks a welcome no one hears", "scene": {"mood": "loneliness", "colors": ["dark blue", "grey", "silver"], "composition": "extreme close-up", "camera": "slow zoom in", "description": "A loneliness scene in folk register. dutch angle framing. rack focus movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "Supper growing cold but the light stays on", "scene": {"mood": "tenderness", "colors": ["soft pink", "cream", "warm gold"], "composition": "over-the-shoulder", "camera": "crane up", "description": "A tenderness scene in folk register. dutch angle framing. floating drift movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "A melody passed down through the walls", "scene": {"mood": "memory", "colors": ["sepia", "warm gold", "faded green"], "composition": "POV", "camera": "slow zoom in", "description": "A memory scene in folk register. symmetrical framing. floating drift movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "Lantern in the window — always, always", "scene": {"mood": "anticipation", "colors": ["amber", "deep purple", "gold"], "composition": "center frame", "camera": "gentle pan right", "description": "A anticipation scene in folk register. low angle framing. tracking shot movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "The fiddle knows the footsteps when they come", "scene": {"mood": "love", "colors": ["deep red", "champagne", "rose"], "composition": "over-the-shoulder", "camera": "static", "description": "A love scene in folk register. close-up framing. dolly forward movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "Warm soup, warm hands, warm song", "scene": {"mood": "devotion", "colors": ["deep purple", "gold", "white"], "composition": "symmetrical", "camera": "slow zoom out", "description": "A devotion scene in folk register. dutch angle framing. tracking shot movement."}}
{"song": "Lantern in the Window", "artist": "Hearthstone", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Lantern in the window — the light is the love", "scene": {"mood": "peace", "colors": ["soft blue", "white", "sage green"], "composition": "dutch angle", "camera": "slow zoom in", "description": "A peace scene in folk register. rule of thirds framing. slow zoom out movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 1, "timestamp": "0:00", "duration_seconds": 24, "lyric_line": "The river waits with open arms", "scene": {"mood": "faith", "colors": ["white", "gold", "royal blue"], "composition": "close-up", "camera": "crane up", "description": "A faith scene in folk register. silhouette frame framing. static movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 2, "timestamp": "0:24", "duration_seconds": 26, "lyric_line": "Gospel harmonies like water over stone", "scene": {"mood": "community", "colors": ["warm red", "earth brown", "gold"], "composition": "close-up", "camera": "gentle pan right", "description": "A community scene in folk register. silhouette frame framing. floating drift movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "White dresses, muddy banks, clean hearts", "scene": {"mood": "joy", "colors": ["bright gold", "warm white", "orange"], "composition": "rule of thirds", "camera": "steady hold", "description": "A joy scene in folk register. rule of thirds framing. crane up movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "The pastor's voice carries across the valley", "scene": {"mood": "solemnity", "colors": ["black", "deep purple", "gold"], "composition": "POV", "camera": "slow zoom in", "description": "A solemnity scene in folk register. dutch angle framing. circular orbit movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "Baptized in the current of community", "scene": {"mood": "renewal", "colors": ["spring green", "white", "gold"], "composition": "center frame", "camera": "circular orbit", "description": "A renewal scene in folk register. wide shot framing. handheld sway movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "The banjo says amen in its own language", "scene": {"mood": "gratitude", "colors": ["gold", "green", "cream"], "composition": "rule of thirds", "camera": "dolly forward", "description": "A gratitude scene in folk register. rule of thirds framing. circular orbit movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "River baptism — washed and remembered", "scene": {"mood": "peace", "colors": ["soft blue", "white", "sage green"], "composition": "close-up", "camera": "tracking shot", "description": "A peace scene in folk register. rule of thirds framing. circular orbit movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "Dripping wet and laughing in the sun", "scene": {"mood": "celebration", "colors": ["red", "gold", "green"], "composition": "dutch angle", "camera": "dolly forward", "description": "A celebration scene in folk register. high angle framing. dolly forward movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "The river sings the song it always sang", "scene": {"mood": "belonging", "colors": ["warm brown", "gold", "green"], "composition": "symmetrical", "camera": "slow zoom in", "description": "A belonging scene in folk register. high angle framing. slow zoom in movement."}}
{"song": "River Baptism", "artist": "Hollow Creek Singers", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "River baptism — every drop a hymn", "scene": {"mood": "transcendence", "colors": ["white", "gold", "silver"], "composition": "over-the-shoulder", "camera": "tracking shot", "description": "A transcendence scene in folk register. center frame framing. static movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 1, "timestamp": "0:00", "duration_seconds": 26, "lyric_line": "Black dust on a father's eyelids", "scene": {"mood": "weariness", "colors": ["grey", "pale blue", "faded"], "composition": "over-the-shoulder", "camera": "handheld sway", "description": "A weariness scene in folk register. extreme close-up framing. static movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 2, "timestamp": "0:26", "duration_seconds": 24, "lyric_line": "He sings to the baby in the dark", "scene": {"mood": "love", "colors": ["deep red", "champagne", "rose"], "composition": "dutch angle", "camera": "steady hold", "description": "A love scene in folk register. extreme close-up framing. handheld sway movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "A lullaby the mountain taught him", "scene": {"mood": "exhaustion", "colors": ["grey", "brown", "faded"], "composition": "over-the-shoulder", "camera": "slow zoom in", "description": "A exhaustion scene in folk register. close-up framing. gentle pan right movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "The mine takes his voice by day", "scene": {"mood": "tenderness", "colors": ["soft pink", "cream", "warm gold"], "composition": "silhouette frame", "camera": "slow zoom out", "description": "A tenderness scene in folk register. high angle framing. floating drift movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "But at night it comes back softer", "scene": {"mood": "fear", "colors": ["black", "grey", "pale white"], "composition": "symmetrical", "camera": "gentle pan right", "description": "A fear scene in folk register. dutch angle framing. static movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "Coal miner's lullaby — the mountain hums", "scene": {"mood": "resolve", "colors": ["steel grey", "deep blue", "white"], "composition": "high angle", "camera": "static", "description": "A resolve scene in folk register. dutch angle framing. dolly forward movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "The baby sleeps to anthracite rhythm", "scene": {"mood": "hope", "colors": ["pale gold", "sky blue", "white"], "composition": "POV", "camera": "gentle pan right", "description": "A hope scene in folk register. center frame framing. rack focus movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "A song that outlasts the seam", "scene": {"mood": "sacrifice", "colors": ["red", "white", "gold"], "composition": "wide shot", "camera": "dolly forward", "description": "A sacrifice scene in folk register. symmetrical framing. dolly forward movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "The coal is gone but the melody remains", "scene": {"mood": "strength", "colors": ["iron grey", "dark brown", "gold"], "composition": "low angle", "camera": "crane up", "description": "A strength scene in folk register. bird's eye framing. dolly forward movement."}}
{"song": "Coal Miner's Lullaby", "artist": "Appalachian Remnant", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Coal miner's lullaby — what the mountain gave back", "scene": {"mood": "legacy", "colors": ["gold", "brown", "deep green"], "composition": "bird's eye", "camera": "floating drift", "description": "A legacy scene in folk register. symmetrical framing. slow zoom in movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 1, "timestamp": "0:00", "duration_seconds": 22, "lyric_line": "Boots on gravel, guitar on back", "scene": {"mood": "freedom", "colors": ["sky blue", "green", "gold"], "composition": "over-the-shoulder", "camera": "steady hold", "description": "A freedom scene in folk register. silhouette frame framing. crane up movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 2, "timestamp": "0:22", "duration_seconds": 24, "lyric_line": "Wildflowers nodding like old friends", "scene": {"mood": "joy", "colors": ["bright gold", "warm white", "orange"], "composition": "symmetrical", "camera": "dolly forward", "description": "A joy scene in folk register. wide shot framing. tracking shot movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 3, "timestamp": "0:46", "duration_seconds": 26, "lyric_line": "The road goes where the song goes", "scene": {"mood": "wanderlust", "colors": ["rust", "blue", "gold"], "composition": "extreme close-up", "camera": "circular orbit", "description": "A wanderlust scene in folk register. POV framing. circular orbit movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 4, "timestamp": "1:12", "duration_seconds": 24, "lyric_line": "Mandolin like afternoon sunlight", "scene": {"mood": "peace", "colors": ["soft blue", "white", "sage green"], "composition": "high angle", "camera": "static", "description": "A peace scene in folk register. POV framing. floating drift movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 5, "timestamp": "1:36", "duration_seconds": 28, "lyric_line": "Every mile a new verse", "scene": {"mood": "nostalgia", "colors": ["sepia", "warm brown", "faded gold"], "composition": "rule of thirds", "camera": "rack focus", "description": "A nostalgia scene in folk register. wide shot framing. slow zoom out movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 6, "timestamp": "2:04", "duration_seconds": 24, "lyric_line": "A creek to cross, a song to carry", "scene": {"mood": "adventure", "colors": ["orange", "blue", "brown"], "composition": "dutch angle", "camera": "crane up", "description": "A adventure scene in folk register. close-up framing. floating drift movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 7, "timestamp": "2:28", "duration_seconds": 26, "lyric_line": "Wildflower road — the walking is the singing", "scene": {"mood": "contentment", "colors": ["warm green", "cream", "gold"], "composition": "silhouette frame", "camera": "handheld sway", "description": "A contentment scene in folk register. center frame framing. rack focus movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 8, "timestamp": "2:54", "duration_seconds": 24, "lyric_line": "The harmonica knows every wind", "scene": {"mood": "simplicity", "colors": ["white", "natural wood", "green"], "composition": "POV", "camera": "slow zoom out", "description": "A simplicity scene in folk register. center frame framing. floating drift movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 9, "timestamp": "3:18", "duration_seconds": 26, "lyric_line": "Campfire tonight, sunrise tomorrow", "scene": {"mood": "wonder", "colors": ["gold", "blue", "purple"], "composition": "bird's eye", "camera": "rack focus", "description": "A wonder scene in folk register. rule of thirds framing. dolly forward movement."}}
{"song": "Wildflower Road", "artist": "Meadow & Stone", "beat": 10, "timestamp": "3:44", "duration_seconds": 30, "lyric_line": "Wildflower road — every ending is a new song", "scene": {"mood": "home", "colors": ["warm brown", "gold", "cream"], "composition": "close-up", "camera": "crane up", "description": "A home scene in folk register. extreme close-up framing. gentle pan right movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 1, "timestamp": "0:00", "duration_seconds": 24, "lyric_line": "Flour on the windowsill like snow", "scene": {"mood": "warmth", "colors": ["amber", "cream", "soft gold"], "composition": "rule of thirds", "camera": "gentle pan right", "description": "A warmth scene in folk register. extreme close-up framing. static movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 2, "timestamp": "0:24", "duration_seconds": 26, "lyric_line": "Her hands remember the recipe by heart", "scene": {"mood": "nostalgia", "colors": ["sepia", "warm brown", "faded gold"], "composition": "low angle", "camera": "slow zoom in", "description": "A nostalgia scene in folk register. over-the-shoulder framing. floating drift movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "The guitar sits in the corner, waiting", "scene": {"mood": "love", "colors": ["deep red", "champagne", "rose"], "composition": "extreme close-up", "camera": "handheld sway", "description": "A love scene in folk register. dutch angle framing. slow zoom out movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "Biscuit crumbs and gospel on the radio", "scene": {"mood": "memory", "colors": ["sepia", "warm gold", "faded green"], "composition": "close-up", "camera": "dolly forward", "description": "A memory scene in folk register. center frame framing. handheld sway movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "Every scar on the cutting board a story", "scene": {"mood": "gratitude", "colors": ["gold", "green", "cream"], "composition": "high angle", "camera": "slow zoom in", "description": "A gratitude scene in folk register. center frame framing. tracking shot movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "Grandmother's kitchen — the room that raised us", "scene": {"mood": "tenderness", "colors": ["soft pink", "cream", "warm gold"], "composition": "wide shot", "camera": "rack focus", "description": "A tenderness scene in folk register. high angle framing. static movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "Cinnamon and cedar and something sacred", "scene": {"mood": "joy", "colors": ["bright gold", "warm white", "orange"], "composition": "high angle", "camera": "dolly forward", "description": "A joy scene in folk register. high angle framing. circular orbit movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "The recipe lives in the stirring now", "scene": {"mood": "belonging", "colors": ["warm brown", "gold", "green"], "composition": "over-the-shoulder", "camera": "slow zoom in", "description": "A belonging scene in folk register. rule of thirds framing. floating drift movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "She hums the hymn her mother hummed", "scene": {"mood": "continuity", "colors": ["gold", "green", "blue"], "composition": "silhouette frame", "camera": "rack focus", "description": "A continuity scene in folk register. extreme close-up framing. gentle pan right movement."}}
{"song": "Grandmother's Kitchen", "artist": "Hearth & Vine", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Grandmother's kitchen — where the song started", "scene": {"mood": "grace", "colors": ["brown", "green", "cream"], "composition": "center frame", "camera": "static", "description": "A grace scene in folk register. high angle framing. steady hold movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 1, "timestamp": "0:00", "duration_seconds": 26, "lyric_line": "The harbor smells like salt and goodbye", "scene": {"mood": "melancholy", "colors": ["navy", "steel grey", "teal"], "composition": "over-the-shoulder", "camera": "steady hold", "description": "A melancholy scene in folk register. extreme close-up framing. static movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 2, "timestamp": "0:26", "duration_seconds": 24, "lyric_line": "Accordion like a ship's lungs breathing", "scene": {"mood": "longing", "colors": ["slate blue", "silver", "dusk grey"], "composition": "rule of thirds", "camera": "tracking shot", "description": "A longing scene in folk register. dutch angle framing. static movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "Every rope a sentence left untied", "scene": {"mood": "adventure", "colors": ["orange", "blue", "brown"], "composition": "close-up", "camera": "dolly forward", "description": "A adventure scene in folk register. bird's eye framing. slow zoom in movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "The sea takes and the sea keeps", "scene": {"mood": "solitude", "colors": ["midnight blue", "silver", "black"], "composition": "dutch angle", "camera": "slow zoom out", "description": "A solitude scene in folk register. close-up framing. rack focus movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "Harbor song — the tide knows all the verses", "scene": {"mood": "memory", "colors": ["sepia", "warm gold", "faded green"], "composition": "wide shot", "camera": "tracking shot", "description": "A memory scene in folk register. rule of thirds framing. slow zoom out movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "A sailor's love letter set to waltz time", "scene": {"mood": "beauty", "colors": ["white", "gold", "soft pink"], "composition": "rule of thirds", "camera": "handheld sway", "description": "A beauty scene in folk register. wide shot framing. rack focus movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "The lantern on the pier doesn't judge", "scene": {"mood": "hope", "colors": ["pale gold", "sky blue", "white"], "composition": "POV", "camera": "static", "description": "A hope scene in folk register. close-up framing. static movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "It just shines for whoever needs it", "scene": {"mood": "resilience", "colors": ["green", "brown", "iron"], "composition": "silhouette frame", "camera": "handheld sway", "description": "A resilience scene in folk register. silhouette frame framing. gentle pan right movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "Harbor song — the ocean hums along", "scene": {"mood": "acceptance", "colors": ["brown", "green", "cream"], "composition": "close-up", "camera": "slow zoom in", "description": "A acceptance scene in folk register. symmetrical framing. handheld sway movement."}}
{"song": "Harbor Song", "artist": "Tidebound", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Harbor song — every ship carries a melody home", "scene": {"mood": "peace", "colors": ["soft blue", "white", "sage green"], "composition": "low angle", "camera": "rack focus", "description": "A peace scene in folk register. symmetrical framing. tracking shot movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 1, "timestamp": "0:00", "duration_seconds": 24, "lyric_line": "The holler holds sound like a cup", "scene": {"mood": "isolation", "colors": ["dark blue", "grey", "black"], "composition": "bird's eye", "camera": "steady hold", "description": "A isolation scene in folk register. high angle framing. gentle pan right movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 2, "timestamp": "0:24", "duration_seconds": 26, "lyric_line": "Echoes return older than they left", "scene": {"mood": "strength", "colors": ["iron grey", "dark brown", "gold"], "composition": "extreme close-up", "camera": "slow zoom out", "description": "A strength scene in folk register. extreme close-up framing. floating drift movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "Dulcimer like water over limestone", "scene": {"mood": "melanchory", "colors": ["brown", "green", "cream"], "composition": "high angle", "camera": "slow zoom in", "description": "A melanchory scene in folk register. high angle framing. steady hold movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "Every hollow a different key", "scene": {"mood": "pride", "colors": ["gold", "purple", "red"], "composition": "symmetrical", "camera": "circular orbit", "description": "A pride scene in folk register. POV framing. handheld sway movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "The mountain doesn't care about the city", "scene": {"mood": "resilience", "colors": ["green", "brown", "iron"], "composition": "low angle", "camera": "gentle pan right", "description": "A resilience scene in folk register. rule of thirds framing. floating drift movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "It just sings what it knows", "scene": {"mood": "memory", "colors": ["sepia", "warm gold", "faded green"], "composition": "bird's eye", "camera": "circular orbit", "description": "A memory scene in folk register. POV framing. circular orbit movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "Holler echo — the valley answers itself", "scene": {"mood": "defiance", "colors": ["black", "red", "silver"], "composition": "low angle", "camera": "static", "description": "A defiance scene in folk register. high angle framing. dolly forward movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "A voice from the ridge, a voice from the creek", "scene": {"mood": "beauty", "colors": ["white", "gold", "soft pink"], "composition": "close-up", "camera": "handheld sway", "description": "A beauty scene in folk register. dutch angle framing. steady hold movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "The song is the land speaking", "scene": {"mood": "solitude", "colors": ["midnight blue", "silver", "black"], "composition": "dutch angle", "camera": "tracking shot", "description": "A solitude scene in folk register. symmetrical framing. slow zoom in movement."}}
{"song": "Holler Echo", "artist": "Ridge Walker", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Holler echo — what the mountain remembers", "scene": {"mood": "continuity", "colors": ["gold", "green", "blue"], "composition": "silhouette frame", "camera": "steady hold", "description": "A continuity scene in folk register. silhouette frame framing. circular orbit movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 1, "timestamp": "0:00", "duration_seconds": 24, "lyric_line": "The train whistle is a hymn the tracks sing", "scene": {"mood": "longing", "colors": ["slate blue", "silver", "dusk grey"], "composition": "center frame", "camera": "handheld sway", "description": "A longing scene in folk register. POV framing. floating drift movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 2, "timestamp": "0:24", "duration_seconds": 26, "lyric_line": "Steel wheels on steel rails — percussion", "scene": {"mood": "journey", "colors": ["brown", "green", "cream"], "composition": "high angle", "camera": "static", "description": "A journey scene in folk register. rule of thirds framing. tracking shot movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 3, "timestamp": "0:50", "duration_seconds": 28, "lyric_line": "Every station a verse, every mile a chorus", "scene": {"mood": "faith", "colors": ["white", "gold", "royal blue"], "composition": "dutch angle", "camera": "steady hold", "description": "A faith scene in folk register. low angle framing. tracking shot movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 4, "timestamp": "1:18", "duration_seconds": 24, "lyric_line": "The harmonica rides in the cattle car", "scene": {"mood": "hope", "colors": ["pale gold", "sky blue", "white"], "composition": "rule of thirds", "camera": "crane up", "description": "A hope scene in folk register. wide shot framing. tracking shot movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 5, "timestamp": "1:42", "duration_seconds": 26, "lyric_line": "Playing the blues to the passing fields", "scene": {"mood": "sorrow", "colors": ["deep blue", "grey", "silver"], "composition": "close-up", "camera": "dolly forward", "description": "A sorrow scene in folk register. low angle framing. slow zoom out movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 6, "timestamp": "2:08", "duration_seconds": 24, "lyric_line": "Train whistle gospel — salvation by rail", "scene": {"mood": "freedom", "colors": ["sky blue", "green", "gold"], "composition": "bird's eye", "camera": "tracking shot", "description": "A freedom scene in folk register. center frame framing. gentle pan right movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 7, "timestamp": "2:32", "duration_seconds": 28, "lyric_line": "The conductor doesn't know he's in a hymn", "scene": {"mood": "community", "colors": ["warm red", "earth brown", "gold"], "composition": "extreme close-up", "camera": "handheld sway", "description": "A community scene in folk register. center frame framing. slow zoom in movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 8, "timestamp": "3:00", "duration_seconds": 24, "lyric_line": "But the rhythm section does", "scene": {"mood": "resilience", "colors": ["green", "brown", "iron"], "composition": "extreme close-up", "camera": "handheld sway", "description": "A resilience scene in folk register. low angle framing. crane up movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 9, "timestamp": "3:24", "duration_seconds": 26, "lyric_line": "Every stop a small resurrection", "scene": {"mood": "celebration", "colors": ["red", "gold", "green"], "composition": "symmetrical", "camera": "circular orbit", "description": "A celebration scene in folk register. low angle framing. crane up movement."}}
{"song": "Train Whistle Gospel", "artist": "Rail & Grace", "beat": 10, "timestamp": "3:50", "duration_seconds": 30, "lyric_line": "Train whistle gospel — the journey is the sermon", "scene": {"mood": "homecoming", "colors": ["brown", "green", "cream"], "composition": "wide shot", "camera": "slow zoom out", "description": "A homecoming scene in folk register. silhouette frame framing. slow zoom out movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 1, "timestamp": "0:00", "duration_seconds": 28, "lyric_line": "The old growth stands in silence older than language", "scene": {"mood": "reverence", "colors": ["white", "gold", "deep brown"], "composition": "POV", "camera": "circular orbit", "description": "A reverence scene in folk register. dutch angle framing. steady hold movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 2, "timestamp": "0:28", "duration_seconds": 26, "lyric_line": "Fiddle like wind through branches", "scene": {"mood": "patience", "colors": ["sage green", "cream", "soft blue"], "composition": "low angle", "camera": "floating drift", "description": "A patience scene in folk register. dutch angle framing. rack focus movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 3, "timestamp": "0:54", "duration_seconds": 24, "lyric_line": "Every ring a year the tree survived", "scene": {"mood": "stillness", "colors": ["pale blue", "white", "silver"], "composition": "close-up", "camera": "dolly forward", "description": "A stillness scene in folk register. wide shot framing. circular orbit movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 4, "timestamp": "1:18", "duration_seconds": 28, "lyric_line": "Moss on the north side — the tree's memory", "scene": {"mood": "wonder", "colors": ["gold", "blue", "purple"], "composition": "low angle", "camera": "slow zoom out", "description": "A wonder scene in folk register. extreme close-up framing. circular orbit movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 5, "timestamp": "1:46", "duration_seconds": 24, "lyric_line": "The folk song doesn't hurry", "scene": {"mood": "age", "colors": ["brown", "grey", "gold"], "composition": "center frame", "camera": "rack focus", "description": "A age scene in folk register. close-up framing. slow zoom in movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 6, "timestamp": "2:10", "duration_seconds": 28, "lyric_line": "Neither does the forest", "scene": {"mood": "beauty", "colors": ["white", "gold", "soft pink"], "composition": "wide shot", "camera": "tracking shot", "description": "A beauty scene in folk register. close-up framing. gentle pan right movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 7, "timestamp": "2:38", "duration_seconds": 24, "lyric_line": "Old growth — rooted patience, branch by branch", "scene": {"mood": "strength", "colors": ["iron grey", "dark brown", "gold"], "composition": "POV", "camera": "slow zoom in", "description": "A strength scene in folk register. wide shot framing. circular orbit movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 8, "timestamp": "3:02", "duration_seconds": 28, "lyric_line": "The melody grows the way the trees do", "scene": {"mood": "solitude", "colors": ["midnight blue", "silver", "black"], "composition": "bird's eye", "camera": "tracking shot", "description": "A solitude scene in folk register. extreme close-up framing. gentle pan right movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 9, "timestamp": "3:30", "duration_seconds": 24, "lyric_line": "Slow, deliberate, unbreakable", "scene": {"mood": "timelessness", "colors": ["gold", "white", "deep blue"], "composition": "POV", "camera": "handheld sway", "description": "A timelessness scene in folk register. bird's eye framing. crane up movement."}}
{"song": "Old Growth", "artist": "Moss & Lichen", "beat": 10, "timestamp": "3:54", "duration_seconds": 30, "lyric_line": "Old growth — the song outlives the singer", "scene": {"mood": "peace", "colors": ["soft blue", "white", "sage green"], "composition": "dutch angle", "camera": "handheld sway", "description": "A peace scene in folk register. POV framing. floating drift movement."}}
{"song": "The Weight of Iron", "beat": 1, "lyric_line": "The iron gate swings slow tonight", "scene": {"mood": "despair", "colors": ["olive", "cream", "rust"], "composition": "close-up", "camera": "slow pan", "description": "A despair scene illustrating 'The iron gate swings slow tonight'. close up framing with olive, cream, rust palette. Camera: slow pan."}}
{"song": "The Weight of Iron", "beat": 2, "lyric_line": "Wind carries names I can't forget", "scene": {"mood": "longing", "colors": ["deep red", "brown", "amber"], "composition": "wide shot", "camera": "steady", "description": "A longing scene illustrating 'Wind carries names I can't forget'. wide shot framing with deep red, brown, amber palette. Camera: steady."}}
{"song": "The Weight of Iron", "beat": 3, "lyric_line": "Stone walls hold the cold inside", "scene": {"mood": "grief", "colors": ["olive", "cream", "rust"], "composition": "medium shot", "camera": "handheld", "description": "A grief scene illustrating 'Stone walls hold the cold inside'. medium shot framing with olive, cream, rust palette. Camera: handheld."}}
{"song": "The Weight of Iron", "beat": 4, "lyric_line": "Every step leaves prints in dust", "scene": {"mood": "resignation", "colors": ["dusty blue", "tan", "grey"], "composition": "low angle", "camera": "slow zoom", "description": "A resignation scene illustrating 'Every step leaves prints in dust'. low angle framing with dusty blue, tan, grey palette. Camera: slow zoom."}}
{"song": "The Weight of Iron", "beat": 5, "lyric_line": "The river hums an older song", "scene": {"mood": "ache", "colors": ["yellow", "warm brown", "green"], "composition": "high angle", "camera": "tracking", "description": "A ache scene illustrating 'The river hums an older song'. high angle framing with yellow, warm brown, green palette. Camera: tracking."}}
{"song": "The Weight of Iron", "beat": 6, "lyric_line": "Shadows lean against the door", "scene": {"mood": "despair", "colors": ["dusty blue", "tan", "grey"], "composition": "over-the-shoulder", "camera": "static", "description": "A despair scene illustrating 'Shadows lean against the door'. over the shoulder framing with dusty blue, tan, grey palette. Camera: static."}}
{"song": "The Weight of Iron", "beat": 7, "lyric_line": "I count the stars I cannot reach", "scene": {"mood": "longing", "colors": ["white", "grey", "pale blue"], "composition": "profile", "camera": "crane up", "description": "A longing scene illustrating 'I count the stars I cannot reach'. profile framing with white, grey, pale blue palette. Camera: crane up."}}
{"song": "The Weight of Iron", "beat": 8, "lyric_line": "The fire dies but warmth remains", "scene": {"mood": "grief", "colors": ["amber", "brown", "cream"], "composition": "bird's eye", "camera": "dolly in", "description": "A grief scene illustrating 'The fire dies but warmth remains'. bird's eye framing with amber, brown, cream palette. Camera: dolly in."}}
{"song": "The Weight of Iron", "beat": 9, "lyric_line": "Tomorrow bends but doesn't break", "scene": {"mood": "resignation", "colors": ["black", "red", "gold"], "composition": "dutch angle", "camera": "gentle drift", "description": "A resignation scene illustrating 'Tomorrow bends but doesn't break'. dutch angle framing with black, red, gold palette. Camera: gentle drift."}}
{"song": "The Weight of Iron", "beat": 10, "lyric_line": "I walk the road that chose me first", "scene": {"mood": "ache", "colors": ["grey", "white", "blue"], "composition": "establishing shot", "camera": "locked-off", "description": "A ache scene illustrating 'I walk the road that chose me first'. establishing shot framing with grey, white, blue palette. Camera: locked-off."}}
{"song": "Harvest Moon", "beat": 1, "lyric_line": "Golden fields stretch past the town", "scene": {"mood": "warmth", "colors": ["amber", "brown", "cream"], "composition": "close-up", "camera": "slow pan", "description": "A warmth scene illustrating 'Golden fields stretch past the town'. close up framing with amber, brown, cream palette. Camera: slow pan."}}
{"song": "Harvest Moon", "beat": 2, "lyric_line": "Papa's hands knew every seed", "scene": {"mood": "memory", "colors": ["amber", "brown", "cream"], "composition": "wide shot", "camera": "steady", "description": "A memory scene illustrating 'Papa's hands knew every seed'. wide shot framing with amber, brown, cream palette. Camera: steady."}}
{"song": "Harvest Moon", "beat": 3, "lyric_line": "The barn door creaks its lullaby", "scene": {"mood": "bittersweet", "colors": ["black", "red", "gold"], "composition": "medium shot", "camera": "handheld", "description": "A bittersweet scene illustrating 'The barn door creaks its lullaby'. medium shot framing with black, red, gold palette. Camera: handheld."}}
{"song": "Harvest Moon", "beat": 4, "lyric_line": "Crickets count the hours down", "scene": {"mood": "tender", "colors": ["navy", "silver", "white"], "composition": "low angle", "camera": "slow zoom", "description": "A tender scene illustrating 'Crickets count the hours down'. low angle framing with navy, silver, white palette. Camera: slow zoom."}}
{"song": "Harvest Moon", "beat": 5, "lyric_line": "Mama's bread still fills the air", "scene": {"mood": "fading", "colors": ["warm yellow", "brown", "red"], "composition": "high angle", "camera": "tracking", "description": "A fading scene illustrating 'Mama's bread still fills the air'. high angle framing with warm yellow, brown, red palette. Camera: tracking."}}
{"song": "Harvest Moon", "beat": 6, "lyric_line": "We ran barefoot through the rain", "scene": {"mood": "warmth", "colors": ["navy", "silver", "white"], "composition": "over-the-shoulder", "camera": "static", "description": "A warmth scene illustrating 'We ran barefoot through the rain'. over the shoulder framing with navy, silver, white palette. Camera: static."}}
{"song": "Harvest Moon", "beat": 7, "lyric_line": "The old oak holds our carved-in names", "scene": {"mood": "memory", "colors": ["warm yellow", "brown", "red"], "composition": "profile", "camera": "crane up", "description": "A memory scene illustrating 'The old oak holds our carved-in names'. profile framing with warm yellow, brown, red palette. Camera: crane up."}}
{"song": "Harvest Moon", "beat": 8, "lyric_line": "Sunset paints the pond to gold", "scene": {"mood": "bittersweet", "colors": ["dusty blue", "tan", "grey"], "composition": "bird's eye", "camera": "dolly in", "description": "A bittersweet scene illustrating 'Sunset paints the pond to gold'. bird's eye framing with dusty blue, tan, grey palette. Camera: dolly in."}}
{"song": "Harvest Moon", "beat": 9, "lyric_line": "We didn't know we were happy then", "scene": {"mood": "tender", "colors": ["dusty blue", "tan", "grey"], "composition": "dutch angle", "camera": "gentle drift", "description": "A tender scene illustrating 'We didn't know we were happy then'. dutch angle framing with dusty blue, tan, grey palette. Camera: gentle drift."}}
{"song": "Harvest Moon", "beat": 10, "lyric_line": "The harvest moon remembers us", "scene": {"mood": "fading", "colors": ["yellow", "warm brown", "green"], "composition": "establishing shot", "camera": "locked-off", "description": "A fading scene illustrating 'The harvest moon remembers us'. establishing shot framing with yellow, warm brown, green palette. Camera: locked-off."}}
{"song": "River's Edge", "beat": 1, "lyric_line": "Morning mist on still water", "scene": {"mood": "calm", "colors": ["amber", "brown", "cream"], "composition": "close-up", "camera": "slow pan", "description": "A calm scene illustrating 'Morning mist on still water'. close up framing with amber, brown, cream palette. Camera: slow pan."}}
{"song": "River's Edge", "beat": 2, "lyric_line": "A heron stands and waits", "scene": {"mood": "serenity", "colors": ["deep red", "brown", "amber"], "composition": "wide shot", "camera": "steady", "description": "A serenity scene illustrating 'A heron stands and waits'. wide shot framing with deep red, brown, amber palette. Camera: steady."}}
{"song": "River's Edge", "beat": 3, "lyric_line": "The current writes in cursive", "scene": {"mood": "stillness", "colors": ["warm yellow", "brown", "red"], "composition": "medium shot", "camera": "handheld", "description": "A stillness scene illustrating 'The current writes in cursive'. medium shot framing with warm yellow, brown, red palette. Camera: handheld."}}
{"song": "River's Edge", "beat": 4, "lyric_line": "Smooth stones warm in afternoon", "scene": {"mood": "contentment", "colors": ["yellow", "warm brown", "green"], "composition": "low angle", "camera": "slow zoom", "description": "A contentment scene illustrating 'Smooth stones warm in afternoon'. low angle framing with yellow, warm brown, green palette. Camera: slow zoom."}}
{"song": "River's Edge", "beat": 5, "lyric_line": "Fish rise in silver arcs", "scene": {"mood": "gentle", "colors": ["deep red", "brown", "amber"], "composition": "high angle", "camera": "tracking", "description": "A gentle scene illustrating 'Fish rise in silver arcs'. high angle framing with deep red, brown, amber palette. Camera: tracking."}}
{"song": "River's Edge", "beat": 6, "lyric_line": "Willows dip their fingers in", "scene": {"mood": "calm", "colors": ["green", "gold", "brown"], "composition": "over-the-shoulder", "camera": "static", "description": "A calm scene illustrating 'Willows dip their fingers in'. over the shoulder framing with green, gold, brown palette. Camera: static."}}
{"song": "River's Edge", "beat": 7, "lyric_line": "Time moves slow where rivers meet", "scene": {"mood": "serenity", "colors": ["olive", "cream", "rust"], "composition": "profile", "camera": "crane up", "description": "A serenity scene illustrating 'Time moves slow where rivers meet'. profile framing with olive, cream, rust palette. Camera: crane up."}}
{"song": "River's Edge", "beat": 8, "lyric_line": "The bridge holds quiet conversations", "scene": {"mood": "stillness", "colors": ["grey", "white", "blue"], "composition": "bird's eye", "camera": "dolly in", "description": "A stillness scene illustrating 'The bridge holds quiet conversations'. bird's eye framing with grey, white, blue palette. Camera: dolly in."}}
{"song": "River's Edge", "beat": 9, "lyric_line": "Evening brings the frogs to song", "scene": {"mood": "contentment", "colors": ["navy", "silver", "white"], "composition": "dutch angle", "camera": "gentle drift", "description": "A contentment scene illustrating 'Evening brings the frogs to song'. dutch angle framing with navy, silver, white palette. Camera: gentle drift."}}
{"song": "River's Edge", "beat": 10, "lyric_line": "The river knows where it's going", "scene": {"mood": "gentle", "colors": ["amber", "brown", "cream"], "composition": "establishing shot", "camera": "locked-off", "description": "A gentle scene illustrating 'The river knows where it's going'. establishing shot framing with amber, brown, cream palette. Camera: locked-off."}}
{"song": "Broken Lighthouse", "beat": 1, "lyric_line": "The light went out in ninety-three", "scene": {"mood": "abandonment", "colors": ["black", "red", "gold"], "composition": "close-up", "camera": "slow pan", "description": "A abandonment scene illustrating 'The light went out in ninety-three'. close up framing with black, red, gold palette. Camera: slow pan."}}
{"song": "Broken Lighthouse", "beat": 2, "lyric_line": "Salt has eaten through the door", "scene": {"mood": "ruin", "colors": ["grey", "white", "blue"], "composition": "wide shot", "camera": "steady", "description": "A ruin scene illustrating 'Salt has eaten through the door'. wide shot framing with grey, white, blue palette. Camera: steady."}}
{"song": "Broken Lighthouse", "beat": 3, "lyric_line": "Gulls nest where the keeper slept", "scene": {"mood": "loneliness", "colors": ["black", "red", "gold"], "composition": "medium shot", "camera": "handheld", "description": "A loneliness scene illustrating 'Gulls nest where the keeper slept'. medium shot framing with black, red, gold palette. Camera: handheld."}}
{"song": "Broken Lighthouse", "beat": 4, "lyric_line": "Fog rolls in like unanswered prayers", "scene": {"mood": "decay", "colors": ["dusty blue", "tan", "grey"], "composition": "low angle", "camera": "slow zoom", "description": "A decay scene illustrating 'Fog rolls in like unanswered prayers'. low angle framing with dusty blue, tan, grey palette. Camera: slow zoom."}}
{"song": "Broken Lighthouse", "beat": 5, "lyric_line": "The spiral stair groans under ghosts", "scene": {"mood": "emptiness", "colors": ["olive", "cream", "rust"], "composition": "high angle", "camera": "tracking", "description": "A emptiness scene illustrating 'The spiral stair groans under ghosts'. high angle framing with olive, cream, rust palette. Camera: tracking."}}
{"song": "Broken Lighthouse", "beat": 6, "lyric_line": "Paint peels in long surrender", "scene": {"mood": "abandonment", "colors": ["dusty blue", "tan", "grey"], "composition": "over-the-shoulder", "camera": "static", "description": "A abandonment scene illustrating 'Paint peels in long surrender'. over the shoulder framing with dusty blue, tan, grey palette. Camera: static."}}
{"song": "Broken Lighthouse", "beat": 7, "lyric_line": "Ships pass blind in heavy weather", "scene": {"mood": "ruin", "colors": ["black", "red", "gold"], "composition": "profile", "camera": "crane up", "description": "A ruin scene illustrating 'Ships pass blind in heavy weather'. profile framing with black, red, gold palette. Camera: crane up."}}
{"song": "Broken Lighthouse", "beat": 8, "lyric_line": "The lantern room holds only rain", "scene": {"mood": "loneliness", "colors": ["navy", "silver", "white"], "composition": "bird's eye", "camera": "dolly in", "description": "A loneliness scene illustrating 'The lantern room holds only rain'. bird's eye framing with navy, silver, white palette. Camera: dolly in."}}
{"song": "Broken Lighthouse", "beat": 9, "lyric_line": "We were warned but didn't listen", "scene": {"mood": "decay", "colors": ["amber", "brown", "cream"], "composition": "dutch angle", "camera": "gentle drift", "description": "A decay scene illustrating 'We were warned but didn't listen'. dutch angle framing with amber, brown, cream palette. Camera: gentle drift."}}
{"song": "Broken Lighthouse", "beat": 10, "lyric_line": "Some towers fall so others see", "scene": {"mood": "emptiness", "colors": ["black", "red", "gold"], "composition": "establishing shot", "camera": "locked-off", "description": "A emptiness scene illustrating 'Some towers fall so others see'. establishing shot framing with black, red, gold palette. Camera: locked-off."}}
{"song": "Grandmother's Kitchen", "beat": 1, "lyric_line": "Flour dust on everything", "scene": {"mood": "comfort", "colors": ["olive", "cream", "rust"], "composition": "close-up", "camera": "slow pan", "description": "A comfort scene illustrating 'Flour dust on everything'. close up framing with olive, cream, rust palette. Camera: slow pan."}}
{"song": "Grandmother's Kitchen", "beat": 2, "lyric_line": "The radio plays what it remembers", "scene": {"mood": "love", "colors": ["green", "gold", "brown"], "composition": "wide shot", "camera": "steady", "description": "A love scene illustrating 'The radio plays what it remembers'. wide shot framing with green, gold, brown palette. Camera: steady."}}
{"song": "Grandmother's Kitchen", "beat": 3, "lyric_line": "Cinnamon holds the house together", "scene": {"mood": "safety", "colors": ["grey", "white", "blue"], "composition": "medium shot", "camera": "handheld", "description": "A safety scene illustrating 'Cinnamon holds the house together'. medium shot framing with grey, white, blue palette. Camera: handheld."}}
{"song": "Grandmother's Kitchen", "beat": 4, "lyric_line": "She hums without knowing she hums", "scene": {"mood": "belonging", "colors": ["navy", "silver", "white"], "composition": "low angle", "camera": "slow zoom", "description": "A belonging scene illustrating 'She hums without knowing she hums'. low angle framing with navy, silver, white palette. Camera: slow zoom."}}
{"song": "Grandmother's Kitchen", "beat": 5, "lyric_line": "The table fits us all somehow", "scene": {"mood": "joy", "colors": ["warm yellow", "brown", "red"], "composition": "high angle", "camera": "tracking", "description": "A joy scene illustrating 'The table fits us all somehow'. high angle framing with warm yellow, brown, red palette. Camera: tracking."}}
{"song": "Grandmother's Kitchen", "beat": 6, "lyric_line": "Stories rise like bread dough", "scene": {"mood": "comfort", "colors": ["yellow", "warm brown", "green"], "composition": "over-the-shoulder", "camera": "static", "description": "A comfort scene illustrating 'Stories rise like bread dough'. over the shoulder framing with yellow, warm brown, green palette. Camera: static."}}
{"song": "Grandmother's Kitchen", "beat": 7, "lyric_line": "Windows fog from living heat", "scene": {"mood": "love", "colors": ["green", "gold", "brown"], "composition": "profile", "camera": "crane up", "description": "A love scene illustrating 'Windows fog from living heat'. profile framing with green, gold, brown palette. Camera: crane up."}}
{"song": "Grandmother's Kitchen", "beat": 8, "lyric_line": "The clock is always ten minutes late", "scene": {"mood": "safety", "colors": ["grey", "white", "blue"], "composition": "bird's eye", "camera": "dolly in", "description": "A safety scene illustrating 'The clock is always ten minutes late'. bird's eye framing with grey, white, blue palette. Camera: dolly in."}}
{"song": "Grandmother's Kitchen", "beat": 9, "lyric_line": "We leave with full hearts and Tupperware", "scene": {"mood": "belonging", "colors": ["amber", "dust", "gold"], "composition": "dutch angle", "camera": "gentle drift", "description": "A belonging scene illustrating 'We leave with full hearts and Tupperware'. dutch angle framing with amber, dust, gold palette. Camera: gentle drift."}}
{"song": "Grandmother's Kitchen", "beat": 10, "lyric_line": "Some recipes are just love", "scene": {"mood": "joy", "colors": ["navy", "silver", "white"], "composition": "establishing shot", "camera": "locked-off", "description": "A joy scene illustrating 'Some recipes are just love'. establishing shot framing with navy, silver, white palette. Camera: locked-off."}}
{"song": "The Wanderer", "beat": 1, "lyric_line": "My boots know roads my name doesn't", "scene": {"mood": "wandering", "colors": ["black", "red", "gold"], "composition": "close-up", "camera": "slow pan", "description": "A wandering scene illustrating 'My boots know roads my name doesn't'. close up framing with black, red, gold palette. Camera: slow pan."}}
{"song": "The Wanderer", "beat": 2, "lyric_line": "The horizon is a promise I chase", "scene": {"mood": "yearning", "colors": ["green", "gold", "brown"], "composition": "wide shot", "camera": "steady", "description": "A yearning scene illustrating 'The horizon is a promise I chase'. wide shot framing with green, gold, brown palette. Camera: steady."}}
{"song": "The Wanderer", "beat": 3, "lyric_line": "Every town looks like a question", "scene": {"mood": "freedom", "colors": ["amber", "dust", "gold"], "composition": "medium shot", "camera": "handheld", "description": "A freedom scene illustrating 'Every town looks like a question'. medium shot framing with amber, dust, gold palette. Camera: handheld."}}
{"song": "The Wanderer", "beat": 4, "lyric_line": "I've slept in stranger's kindness", "scene": {"mood": "isolation", "colors": ["warm yellow", "brown", "red"], "composition": "low angle", "camera": "slow zoom", "description": "A isolation scene illustrating 'I've slept in stranger's kindness'. low angle framing with warm yellow, brown, red palette. Camera: slow zoom."}}
{"song": "The Wanderer", "beat": 5, "lyric_line": "The train whistle sounds like leaving", "scene": {"mood": "searching", "colors": ["yellow", "warm brown", "green"], "composition": "high angle", "camera": "tracking", "description": "A searching scene illustrating 'The train whistle sounds like leaving'. high angle framing with yellow, warm brown, green palette. Camera: tracking."}}
{"song": "The Wanderer", "beat": 6, "lyric_line": "My shadow is my longest friend", "scene": {"mood": "wandering", "colors": ["olive", "cream", "rust"], "composition": "over-the-shoulder", "camera": "static", "description": "A wandering scene illustrating 'My shadow is my longest friend'. over the shoulder framing with olive, cream, rust palette. Camera: static."}}
{"song": "The Wanderer", "beat": 7, "lyric_line": "Stars don't care where I'm from", "scene": {"mood": "yearning", "colors": ["dusty blue", "tan", "grey"], "composition": "profile", "camera": "crane up", "description": "A yearning scene illustrating 'Stars don't care where I'm from'. profile framing with dusty blue, tan, grey palette. Camera: crane up."}}
{"song": "The Wanderer", "beat": 8, "lyric_line": "The wind has no return address", "scene": {"mood": "freedom", "colors": ["white", "grey", "pale blue"], "composition": "bird's eye", "camera": "dolly in", "description": "A freedom scene illustrating 'The wind has no return address'. bird's eye framing with white, grey, pale blue palette. Camera: dolly in."}}
{"song": "The Wanderer", "beat": 9, "lyric_line": "I've forgotten what home feels like", "scene": {"mood": "isolation", "colors": ["yellow", "warm brown", "green"], "composition": "dutch angle", "camera": "gentle drift", "description": "A isolation scene illustrating 'I've forgotten what home feels like'. dutch angle framing with yellow, warm brown, green palette. Camera: gentle drift."}}
{"song": "The Wanderer", "beat": 10, "lyric_line": "Maybe home is the walking", "scene": {"mood": "searching", "colors": ["black", "red", "gold"], "composition": "establishing shot", "camera": "locked-off", "description": "A searching scene illustrating 'Maybe home is the walking'. establishing shot framing with black, red, gold palette. Camera: locked-off."}}
{"song": "Coal Country", "beat": 1, "lyric_line": "Black dust under every nail", "scene": {"mood": "anger", "colors": ["yellow", "warm brown", "green"], "composition": "close-up", "camera": "slow pan", "description": "A anger scene illustrating 'Black dust under every nail'. close up framing with yellow, warm brown, green palette. Camera: slow pan."}}
{"song": "Coal Country", "beat": 2, "lyric_line": "The mountain gave and gave and gave", "scene": {"mood": "pride", "colors": ["amber", "dust", "gold"], "composition": "wide shot", "camera": "steady", "description": "A pride scene illustrating 'The mountain gave and gave and gave'. wide shot framing with amber, dust, gold palette. Camera: steady."}}
{"song": "Coal Country", "beat": 3, "lyric_line": "We lit the dark with what we carried", "scene": {"mood": "resistance", "colors": ["yellow", "warm brown", "green"], "composition": "medium shot", "camera": "handheld", "description": "A resistance scene illustrating 'We lit the dark with what we carried'. medium shot framing with yellow, warm brown, green palette. Camera: handheld."}}
{"song": "Coal Country", "beat": 4, "lyric_line": "Company store took what we earned", "scene": {"mood": "strength", "colors": ["green", "gold", "brown"], "composition": "low angle", "camera": "slow zoom", "description": "A strength scene illustrating 'Company store took what we earned'. low angle framing with green, gold, brown palette. Camera: slow zoom."}}
{"song": "Coal Country", "beat": 5, "lyric_line": "The whistle blew before the sun", "scene": {"mood": "solidarity", "colors": ["amber", "dust", "gold"], "composition": "high angle", "camera": "tracking", "description": "A solidarity scene illustrating 'The whistle blew before the sun'. high angle framing with amber, dust, gold palette. Camera: tracking."}}
{"song": "Coal Country", "beat": 6, "lyric_line": "Granddaddy's lungs knew better", "scene": {"mood": "anger", "colors": ["olive", "cream", "rust"], "composition": "over-the-shoulder", "camera": "static", "description": "A anger scene illustrating 'Granddaddy's lungs knew better'. over the shoulder framing with olive, cream, rust palette. Camera: static."}}
{"song": "Coal Country", "beat": 7, "lyric_line": "We sang loud so God could hear", "scene": {"mood": "pride", "colors": ["amber", "brown", "cream"], "composition": "profile", "camera": "crane up", "description": "A pride scene illustrating 'We sang loud so God could hear'. profile framing with amber, brown, cream palette. Camera: crane up."}}
{"song": "Coal Country", "beat": 8, "lyric_line": "The union hall still stands", "scene": {"mood": "resistance", "colors": ["black", "red", "gold"], "composition": "bird's eye", "camera": "dolly in", "description": "A resistance scene illustrating 'The union hall still stands'. bird's eye framing with black, red, gold palette. Camera: dolly in."}}
{"song": "Coal Country", "beat": 9, "lyric_line": "Some debts are paid in standing up", "scene": {"mood": "strength", "colors": ["grey", "white", "blue"], "composition": "dutch angle", "camera": "gentle drift", "description": "A strength scene illustrating 'Some debts are paid in standing up'. dutch angle framing with grey, white, blue palette. Camera: gentle drift."}}
{"song": "Coal Country", "beat": 10, "lyric_line": "The coal is gone but we remain", "scene": {"mood": "solidarity", "colors": ["white", "grey", "pale blue"], "composition": "establishing shot", "camera": "locked-off", "description": "A solidarity scene illustrating 'The coal is gone but we remain'. establishing shot framing with white, grey, pale blue palette. Camera: locked-off."}}
{"song": "Wintering", "beat": 1, "lyric_line": "The world goes white and quiet", "scene": {"mood": "contemplation", "colors": ["grey", "white", "blue"], "composition": "close-up", "camera": "slow pan", "description": "A contemplation scene illustrating 'The world goes white and quiet'. close up framing with grey, white, blue palette. Camera: slow pan."}}
{"song": "Wintering", "beat": 2, "lyric_line": "I've been turning inward like the trees", "scene": {"mood": "solitude", "colors": ["white", "grey", "pale blue"], "composition": "wide shot", "camera": "steady", "description": "A solitude scene illustrating 'I've been turning inward like the trees'. wide shot framing with white, grey, pale blue palette. Camera: steady."}}
{"song": "Wintering", "beat": 3, "lyric_line": "Bare branches write against the sky", "scene": {"mood": "depth", "colors": ["amber", "brown", "cream"], "composition": "medium shot", "camera": "handheld", "description": "A depth scene illustrating 'Bare branches write against the sky'. medium shot framing with amber, brown, cream palette. Camera: handheld."}}
{"song": "Wintering", "beat": 4, "lyric_line": "The fireplace asks for nothing but tending", "scene": {"mood": "quiet", "colors": ["navy", "silver", "white"], "composition": "low angle", "camera": "slow zoom", "description": "A quiet scene illustrating 'The fireplace asks for nothing but tending'. low angle framing with navy, silver, white palette. Camera: slow zoom."}}
{"song": "Wintering", "beat": 5, "lyric_line": "I read the same page twice and understand more", "scene": {"mood": "reflection", "colors": ["olive", "cream", "rust"], "composition": "high angle", "camera": "tracking", "description": "A reflection scene illustrating 'I read the same page twice and understand more'. high angle framing with olive, cream, rust palette. Camera: tracking."}}
{"song": "Wintering", "beat": 6, "lyric_line": "Snow erases what I needed erased", "scene": {"mood": "contemplation", "colors": ["yellow", "warm brown", "green"], "composition": "over-the-shoulder", "camera": "static", "description": "A contemplation scene illustrating 'Snow erases what I needed erased'. over the shoulder framing with yellow, warm brown, green palette. Camera: static."}}
{"song": "Wintering", "beat": 7, "lyric_line": "The cold clarifies what warmth confused", "scene": {"mood": "solitude", "colors": ["yellow", "warm brown", "green"], "composition": "profile", "camera": "crane up", "description": "A solitude scene illustrating 'The cold clarifies what warmth confused'. profile framing with yellow, warm brown, green palette. Camera: crane up."}}
{"song": "Wintering", "beat": 8, "lyric_line": "I've been storing up like the squirrels", "scene": {"mood": "depth", "colors": ["navy", "silver", "white"], "composition": "bird's eye", "camera": "dolly in", "description": "A depth scene illustrating 'I've been storing up like the squirrels'. bird's eye framing with navy, silver, white palette. Camera: dolly in."}}
{"song": "Wintering", "beat": 9, "lyric_line": "Silence is not empty it's full", "scene": {"mood": "quiet", "colors": ["green", "gold", "brown"], "composition": "dutch angle", "camera": "gentle drift", "description": "A quiet scene illustrating 'Silence is not empty it's full'. dutch angle framing with green, gold, brown palette. Camera: gentle drift."}}
{"song": "Wintering", "beat": 10, "lyric_line": "Spring will find me ready or not", "scene": {"mood": "reflection", "colors": ["navy", "silver", "white"], "composition": "establishing shot", "camera": "locked-off", "description": "A reflection scene illustrating 'Spring will find me ready or not'. establishing shot framing with navy, silver, white palette. Camera: locked-off."}}
{"song": "Porch Light", "beat": 1, "lyric_line": "She leaves the light on every night", "scene": {"mood": "anticipation", "colors": ["grey", "white", "blue"], "composition": "close-up", "camera": "slow pan", "description": "A anticipation scene illustrating 'She leaves the light on every night'. close up framing with grey, white, blue palette. Camera: slow pan."}}
{"song": "Porch Light", "beat": 2, "lyric_line": "The road home is shorter than I thought", "scene": {"mood": "return", "colors": ["olive", "cream", "rust"], "composition": "wide shot", "camera": "steady", "description": "A return scene illustrating 'The road home is shorter than I thought'. wide shot framing with olive, cream, rust palette. Camera: steady."}}
{"song": "Porch Light", "beat": 3, "lyric_line": "Crickets welcome me without questions", "scene": {"mood": "faith", "colors": ["yellow", "warm brown", "green"], "composition": "medium shot", "camera": "handheld", "description": "A faith scene illustrating 'Crickets welcome me without questions'. medium shot framing with yellow, warm brown, green palette. Camera: handheld."}}
{"song": "Porch Light", "beat": 4, "lyric_line": "The screen door still doesn't latch right", "scene": {"mood": "light", "colors": ["dusty blue", "tan", "grey"], "composition": "low angle", "camera": "slow zoom", "description": "A light scene illustrating 'The screen door still doesn't latch right'. low angle framing with dusty blue, tan, grey palette. Camera: slow zoom."}}
{"song": "Porch Light", "beat": 5, "lyric_line": "Some things don't need fixing", "scene": {"mood": "reunion", "colors": ["white", "grey", "pale blue"], "composition": "high angle", "camera": "tracking", "description": "A reunion scene illustrating 'Some things don't need fixing'. high angle framing with white, grey, pale blue palette. Camera: tracking."}}
{"song": "Porch Light", "beat": 6, "lyric_line": "Her voice carries across the yard", "scene": {"mood": "anticipation", "colors": ["dusty blue", "tan", "grey"], "composition": "over-the-shoulder", "camera": "static", "description": "A anticipation scene illustrating 'Her voice carries across the yard'. over the shoulder framing with dusty blue, tan, grey palette. Camera: static."}}
{"song": "Porch Light", "beat": 7, "lyric_line": "The porch swing holds our weight and years", "scene": {"mood": "return", "colors": ["deep red", "brown", "amber"], "composition": "profile", "camera": "crane up", "description": "A return scene illustrating 'The porch swing holds our weight and years'. profile framing with deep red, brown, amber palette. Camera: crane up."}}
{"song": "Porch Light", "beat": 8, "lyric_line": "I was gone but never lost", "scene": {"mood": "faith", "colors": ["amber", "brown", "cream"], "composition": "bird's eye", "camera": "dolly in", "description": "A faith scene illustrating 'I was gone but never lost'. bird's eye framing with amber, brown, cream palette. Camera: dolly in."}}
{"song": "Porch Light", "beat": 9, "lyric_line": "The light means someone's still awake", "scene": {"mood": "light", "colors": ["olive", "cream", "rust"], "composition": "dutch angle", "camera": "gentle drift", "description": "A light scene illustrating 'The light means someone's still awake'. dutch angle framing with olive, cream, rust palette. Camera: gentle drift."}}
{"song": "Porch Light", "beat": 10, "lyric_line": "Coming home is a kind of prayer", "scene": {"mood": "reunion", "colors": ["warm yellow", "brown", "red"], "composition": "establishing shot", "camera": "locked-off", "description": "A reunion scene illustrating 'Coming home is a kind of prayer'. establishing shot framing with warm yellow, brown, red palette. Camera: locked-off."}}
{"song": "Fiddle and Dust", "beat": 1, "lyric_line": "The fiddle case hasn't opened in years", "scene": {"mood": "regret", "colors": ["yellow", "warm brown", "green"], "composition": "close-up", "camera": "slow pan", "description": "A regret scene illustrating 'The fiddle case hasn't opened in years'. close up framing with yellow, warm brown, green palette. Camera: slow pan."}}
{"song": "Fiddle and Dust", "beat": 2, "lyric_line": "Dust settled on the D string", "scene": {"mood": "fondness", "colors": ["white", "grey", "pale blue"], "composition": "wide shot", "camera": "steady", "description": "A fondness scene illustrating 'Dust settled on the D string'. wide shot framing with white, grey, pale blue palette. Camera: steady."}}
{"song": "Fiddle and Dust", "beat": 3, "lyric_line": "His fingers knew before his mind", "scene": {"mood": "aging", "colors": ["deep red", "brown", "amber"], "composition": "medium shot", "camera": "handheld", "description": "A aging scene illustrating 'His fingers knew before his mind'. medium shot framing with deep red, brown, amber palette. Camera: handheld."}}
{"song": "Fiddle and Dust", "beat": 4, "lyric_line": "The dance floor remembers better than us", "scene": {"mood": "memory", "colors": ["deep red", "brown", "amber"], "composition": "low angle", "camera": "slow zoom", "description": "A memory scene illustrating 'The dance floor remembers better than us'. low angle framing with deep red, brown, amber palette. Camera: slow zoom."}}
{"song": "Fiddle and Dust", "beat": 5, "lyric_line": "Some songs live in the walls", "scene": {"mood": "music", "colors": ["navy", "silver", "white"], "composition": "high angle", "camera": "tracking", "description": "A music scene illustrating 'Some songs live in the walls'. high angle framing with navy, silver, white palette. Camera: tracking."}}
{"song": "Fiddle and Dust", "beat": 6, "lyric_line": "We played until the neighbors joined", "scene": {"mood": "regret", "colors": ["amber", "dust", "gold"], "composition": "over-the-shoulder", "camera": "static", "description": "A regret scene illustrating 'We played until the neighbors joined'. over the shoulder framing with amber, dust, gold palette. Camera: static."}}
{"song": "Fiddle and Dust", "beat": 7, "lyric_line": "The bow hair snapped and we kept going", "scene": {"mood": "fondness", "colors": ["yellow", "warm brown", "green"], "composition": "profile", "camera": "crane up", "description": "A fondness scene illustrating 'The bow hair snapped and we kept going'. profile framing with yellow, warm brown, green palette. Camera: crane up."}}
{"song": "Fiddle and Dust", "beat": 8, "lyric_line": "Music fills what grief empties", "scene": {"mood": "aging", "colors": ["grey", "white", "blue"], "composition": "bird's eye", "camera": "dolly in", "description": "A aging scene illustrating 'Music fills what grief empties'. bird's eye framing with grey, white, blue palette. Camera: dolly in."}}
{"song": "Fiddle and Dust", "beat": 9, "lyric_line": "He says he's too old but his foot taps", "scene": {"mood": "memory", "colors": ["white", "grey", "pale blue"], "composition": "dutch angle", "camera": "gentle drift", "description": "A memory scene illustrating 'He says he's too old but his foot taps'. dutch angle framing with white, grey, pale blue palette. Camera: gentle drift."}}
{"song": "Fiddle and Dust", "beat": 10, "lyric_line": "Some instruments wait patiently", "scene": {"mood": "music", "colors": ["amber", "brown", "cream"], "composition": "establishing shot", "camera": "locked-off", "description": "A music scene illustrating 'Some instruments wait patiently'. establishing shot framing with amber, brown, cream palette. Camera: locked-off."}}