Compare commits
4 Commits
data/scene
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52e3f6a253 | ||
| ad751a6de6 | |||
| 130fa40f0c | |||
| 82f9810081 |
9
cron/pipeline-scheduler.yml
Normal file
9
cron/pipeline-scheduler.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
- name: Nightly Pipeline Scheduler
|
||||
schedule: '*/30 18-23,0-8 * * *' # Every 30 min, off-peak hours only
|
||||
tasks:
|
||||
- name: Check and start pipelines
|
||||
shell: "bash scripts/nightly-pipeline-scheduler.sh"
|
||||
env:
|
||||
PIPELINE_TOKEN_LIMIT: "500000"
|
||||
PIPELINE_PEAK_START: "9"
|
||||
PIPELINE_PEAK_END: "18"
|
||||
50
scripts/nightly-pipeline-scheduler.md
Normal file
50
scripts/nightly-pipeline-scheduler.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Nightly Pipeline Scheduler
|
||||
|
||||
Auto-starts batch pipelines when inference is available.
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Checks inference provider health (OpenRouter, Ollama, RunPod)
|
||||
2. Checks if it's off-peak hours (configurable, default: after 6PM)
|
||||
3. Checks interactive session load (don't fight with live users)
|
||||
4. Checks daily token budget (configurable limit)
|
||||
5. Starts the highest-priority incomplete pipeline
|
||||
|
||||
## Pipeline Priority Order
|
||||
|
||||
| Priority | Pipeline | Deps | Max Tokens |
|
||||
|----------|----------|------|------------|
|
||||
| 1 | playground-factory | none | 100,000 |
|
||||
| 2 | training-factory | none | 150,000 |
|
||||
| 3 | knowledge-mine | training-factory running | 80,000 |
|
||||
| 4 | adversary | knowledge-mine running | 50,000 |
|
||||
| 5 | codebase-genome | none | 120,000 |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Normal run (used by cron)
|
||||
./scripts/nightly-pipeline-scheduler.sh
|
||||
|
||||
# Dry run (show what would start)
|
||||
./scripts/nightly-pipeline-scheduler.sh --dry-run
|
||||
|
||||
# Status report
|
||||
./scripts/nightly-pipeline-scheduler.sh --status
|
||||
|
||||
# Force start during peak hours
|
||||
./scripts/nightly-pipeline-scheduler.sh --force
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set via environment variables:
|
||||
- `PIPELINE_TOKEN_LIMIT`: Daily token budget (default: 500,000)
|
||||
- `PIPELINE_PEAK_START`: Peak hours start (default: 9)
|
||||
- `PIPELINE_PEAK_END`: Peak hours end (default: 18)
|
||||
- `HERMES_HOME`: Hermes home directory (default: ~/.hermes)
|
||||
|
||||
## Cron
|
||||
|
||||
Runs every 30 minutes. Off-peak only (unless --force).
|
||||
See `cron/pipeline-scheduler.yml`.
|
||||
383
scripts/nightly-pipeline-scheduler.sh
Normal file
383
scripts/nightly-pipeline-scheduler.sh
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env bash
|
||||
# nightly-pipeline-scheduler.sh — Auto-start batch pipelines when inference is available.
|
||||
#
|
||||
# Checks provider health, pipeline progress, token budget, and interactive load.
|
||||
# Starts the highest-priority incomplete pipeline that can run.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/nightly-pipeline-scheduler.sh # Normal run
|
||||
# ./scripts/nightly-pipeline-scheduler.sh --dry-run # Show what would start
|
||||
# ./scripts/nightly-pipeline-scheduler.sh --status # Pipeline status report
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||
BUDGET_FILE="${HERMES_HOME}/pipeline_budget.json"
|
||||
STATE_FILE="${HERMES_HOME}/pipeline_state.json"
|
||||
LOG_FILE="${HERMES_HOME}/logs/pipeline-scheduler.log"
|
||||
TOKEN_DAILY_LIMIT="${PIPELINE_TOKEN_LIMIT:-500000}"
|
||||
PEAK_HOURS_START="${PIPELINE_PEAK_START:-9}"
|
||||
PEAK_HOURS_END="${PIPELINE_PEAK_END:-18}"
|
||||
|
||||
# Pipeline definitions (priority order)
|
||||
# Each pipeline: name, script, max_tokens, dependencies
|
||||
PIPELINES=(
|
||||
"playground-factory|scripts/pipeline_playground_factory.sh|100000|none"
|
||||
"training-factory|scripts/pipeline_training_factory.sh|150000|none"
|
||||
"knowledge-mine|scripts/pipeline_knowledge_mine.sh|80000|training-factory"
|
||||
"adversary|scripts/pipeline_adversary.sh|50000|knowledge-mine"
|
||||
"codebase-genome|scripts/pipeline_codebase_genome.sh|120000|none"
|
||||
)
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# --- Helpers ---
|
||||
now_hour() { date +%-H; }
|
||||
is_peak_hours() {
|
||||
local h=$(now_hour)
|
||||
[[ $h -ge $PEAK_HOURS_START && $h -lt $PEAK_HOURS_END ]]
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$BUDGET_FILE")" "$(dirname "$STATE_FILE")"
|
||||
}
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
get_budget_used_today() {
|
||||
if [[ -f "$BUDGET_FILE" ]]; then
|
||||
local today=$(date +%Y-%m-%d)
|
||||
python3 -c "
|
||||
import json, sys
|
||||
with open('$BUDGET_FILE') as f:
|
||||
d = json.load(f)
|
||||
print(d.get('daily', {}).get('$today', {}).get('tokens_used', 0))
|
||||
" 2>/dev/null || echo 0
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
get_budget_remaining() {
|
||||
local used=$(get_budget_used_today)
|
||||
echo $((TOKEN_DAILY_LIMIT - used))
|
||||
}
|
||||
|
||||
update_budget() {
|
||||
local pipeline="$1"
|
||||
local tokens="$2"
|
||||
local today=$(date +%Y-%m-%d)
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$BUDGET_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
daily = d.setdefault('daily', {})
|
||||
day = daily.setdefault('$today', {'tokens_used': 0, 'pipelines': {}})
|
||||
day['tokens_used'] = day.get('tokens_used', 0) + $tokens
|
||||
day['pipelines']['$pipeline'] = day['pipelines'].get('$pipeline', 0) + $tokens
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
}
|
||||
|
||||
get_pipeline_state() {
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
set_pipeline_state() {
|
||||
local pipeline="$1"
|
||||
local state="$2" # running, complete, failed, skipped
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
d['$pipeline'] = {'state': '$state', 'updated': '$(date -Iseconds)'}
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
}
|
||||
|
||||
is_pipeline_complete() {
|
||||
local pipeline="$1"
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('false')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
state = d.get('$pipeline', {}).get('state', 'not_started')
|
||||
print('true' if state == 'complete' else 'false')
|
||||
" 2>/dev/null || echo false
|
||||
}
|
||||
|
||||
is_pipeline_running() {
|
||||
local pipeline="$1"
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('false')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
state = d.get('$pipeline', {}).get('state', 'not_started')
|
||||
print('true' if state == 'running' else 'false')
|
||||
" 2>/dev/null || echo false
|
||||
}
|
||||
|
||||
check_dependency() {
|
||||
local dep="$1"
|
||||
if [[ "$dep" == "none" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# For knowledge-mine: training-factory must be running or complete
|
||||
if [[ "$dep" == "training-factory" ]]; then
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('training-factory', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
[[ "$state" == "running" || "$state" == "complete" ]]
|
||||
return $?
|
||||
fi
|
||||
# For adversary: knowledge-mine must be at least 50% done
|
||||
# Simplified: check if it's running (we'd need progress tracking for 50%)
|
||||
if [[ "$dep" == "knowledge-mine" ]]; then
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('knowledge-mine', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
[[ "$state" == "running" || "$state" == "complete" ]]
|
||||
return $?
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_inference_available() {
|
||||
# Check if any inference provider is responding
|
||||
# 1. Check OpenRouter
|
||||
local or_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "https://openrouter.ai/api/v1/models" 2>/dev/null || echo "000")
|
||||
|
||||
# 2. Check local Ollama
|
||||
local ollama_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "http://localhost:11434/api/tags" 2>/dev/null || echo "000")
|
||||
|
||||
# 3. Check RunPod (if configured)
|
||||
local runpod_ok="000"
|
||||
if [[ -n "${RUNPOD_ENDPOINT:-}" ]]; then
|
||||
runpod_ok=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 "$RUNPOD_ENDPOINT/health" 2>/dev/null || echo "000")
|
||||
fi
|
||||
|
||||
if [[ "$or_ok" == "200" || "$ollama_ok" == "200" || "$runpod_ok" == "200" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
check_interactive_load() {
|
||||
# Check if there are active interactive sessions (don't fight with live users)
|
||||
# Look for tmux panes with active hermes sessions
|
||||
local active=$(tmux list-panes -a -F '#{pane_pid} #{pane_current_command}' 2>/dev/null \
|
||||
| grep -c "hermes\|python3" || echo 0)
|
||||
|
||||
# If more than 3 interactive sessions, skip pipeline start
|
||||
if [[ $active -gt 3 ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
start_pipeline() {
|
||||
local name="$1"
|
||||
local script="$2"
|
||||
local max_tokens="$3"
|
||||
local budget_remaining="$4"
|
||||
local mode="${5:-run}"
|
||||
|
||||
if [[ "$budget_remaining" -lt "$max_tokens" ]]; then
|
||||
log "SKIP $name: insufficient budget ($budget_remaining < $max_tokens tokens)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$script" ]]; then
|
||||
log "SKIP $name: script not found ($script)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "dry-run" ]]; then
|
||||
log "DRY-RUN: Would start $name (budget: $budget_remaining, needs: $max_tokens)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "START $name (budget: $budget_remaining, max_tokens: $max_tokens)"
|
||||
set_pipeline_state "$name" "running"
|
||||
|
||||
# Run in background, capture output
|
||||
local log_path="${HERMES_HOME}/logs/pipeline-${name}.log"
|
||||
bash "$script" --max-tokens "$max_tokens" >> "$log_path" 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
# Wait a moment to check if it started OK
|
||||
sleep 2
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
log "RUNNING $name (PID: $pid, log: $log_path)"
|
||||
# Record the PID
|
||||
python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
d = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
d['$name']['pid'] = $pid
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
"
|
||||
return 0
|
||||
else
|
||||
log "FAIL $name: script exited immediately"
|
||||
set_pipeline_state "$name" "failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
main() {
|
||||
local mode="${1:-run}"
|
||||
ensure_dirs
|
||||
|
||||
log "=== Pipeline Scheduler ($mode) ==="
|
||||
|
||||
# Check 1: Is inference available?
|
||||
if ! check_inference_available; then
|
||||
log "No inference provider available. Skipping all pipelines."
|
||||
exit 0
|
||||
fi
|
||||
log "Inference: AVAILABLE"
|
||||
|
||||
# Check 2: Is it peak hours?
|
||||
if is_peak_hours && [[ "$mode" != "--force" ]]; then
|
||||
local h=$(now_hour)
|
||||
log "Peak hours ($h:00). Skipping pipeline start. Use --force to override."
|
||||
exit 0
|
||||
fi
|
||||
log "Off-peak: OK"
|
||||
|
||||
# Check 3: Interactive load
|
||||
if ! check_interactive_load && [[ "$mode" != "--force" ]]; then
|
||||
log "High interactive load. Skipping pipeline start."
|
||||
exit 0
|
||||
fi
|
||||
log "Interactive load: OK"
|
||||
|
||||
# Check 4: Token budget
|
||||
local budget=$(get_budget_remaining)
|
||||
log "Token budget remaining: $budget / $TOKEN_DAILY_LIMIT"
|
||||
|
||||
if [[ $budget -le 0 ]]; then
|
||||
log "Daily token budget exhausted. Stopping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check 5: Pipeline status
|
||||
if [[ "$mode" == "--status" ]]; then
|
||||
echo -e "${CYAN}Pipeline Status:${NC}"
|
||||
echo "────────────────────────────────────────────────────"
|
||||
for entry in "${PIPELINES[@]}"; do
|
||||
IFS='|' read -r name script max_tokens dep <<< "$entry"
|
||||
local state=$(python3 -c "
|
||||
import json, os
|
||||
path = '$STATE_FILE'
|
||||
if not os.path.exists(path):
|
||||
print('not_started')
|
||||
else:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('$name', {}).get('state', 'not_started'))
|
||||
" 2>/dev/null || echo "not_started")
|
||||
|
||||
local color=$NC
|
||||
case "$state" in
|
||||
running) color=$YELLOW ;;
|
||||
complete) color=$GREEN ;;
|
||||
failed) color=$RED ;;
|
||||
esac
|
||||
printf " %-25s %b%s%b (max: %s tokens, dep: %s)\n" "$name" "$color" "$state" "$NC" "$max_tokens" "$dep"
|
||||
done
|
||||
echo "────────────────────────────────────────────────────"
|
||||
echo " Budget: $budget / $TOKEN_DAILY_LIMIT tokens remaining"
|
||||
echo " Peak hours: $PEAK_HOURS_START:00 - $PEAK_HOURS_END:00"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find and start the highest-priority incomplete pipeline
|
||||
local started=0
|
||||
for entry in "${PIPELINES[@]}"; do
|
||||
IFS='|' read -r name script max_tokens dep <<< "$entry"
|
||||
|
||||
# Skip if already running or complete
|
||||
if [[ "$(is_pipeline_running $name)" == "true" ]]; then
|
||||
log "SKIP $name: already running"
|
||||
continue
|
||||
fi
|
||||
if [[ "$(is_pipeline_complete $name)" == "true" ]]; then
|
||||
log "SKIP $name: already complete"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check dependency
|
||||
if ! check_dependency "$dep"; then
|
||||
log "SKIP $name: dependency $dep not met"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Try to start
|
||||
if start_pipeline "$name" "$script" "$max_tokens" "$budget" "$mode"; then
|
||||
started=1
|
||||
# Only start one pipeline per run (let it claim tokens before next check)
|
||||
# Exception: playground-factory and training-factory can run in parallel
|
||||
if [[ "$name" != "playground-factory" && "$name" != "training-factory" ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $started -eq 0 ]]; then
|
||||
log "No pipelines to start (all complete, running, or blocked)."
|
||||
fi
|
||||
|
||||
log "=== Pipeline Scheduler done ==="
|
||||
}
|
||||
|
||||
main "$@"
|
||||
467
tests/test_quality_gate.py
Normal file
467
tests/test_quality_gate.py
Normal file
@@ -0,0 +1,467 @@
|
||||
"""Tests for the Quality Gate modules.
|
||||
|
||||
Tests for:
|
||||
- ci_automation_gate.py: linting, function length, auto-fix, counters
|
||||
- task_gate.py: pre/post task gate logic, lane checking, filter tags
|
||||
|
||||
Refs: #629
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add scripts/ to path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
|
||||
from ci_automation_gate import QualityGate
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CI AUTOMATION GATE TESTS
|
||||
# ===========================================================================
|
||||
|
||||
# -- helpers ---------------------------------------------------------------
|
||||
|
||||
def _write_file(dirpath, relpath, content):
|
||||
"""Write a file in a temp directory and return its Path."""
|
||||
p = Path(dirpath) / relpath
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(content)
|
||||
return p
|
||||
|
||||
|
||||
def _run_gate_on_file(dirpath, relpath, content, fix=False):
|
||||
"""Write a file, run QualityGate on it, return the gate instance."""
|
||||
p = _write_file(dirpath, relpath, content)
|
||||
gate = QualityGate(fix=fix)
|
||||
gate.check_file(p)
|
||||
return gate
|
||||
|
||||
|
||||
# -- trailing whitespace ---------------------------------------------------
|
||||
|
||||
def test_trailing_whitespace_warns():
|
||||
"""Lines with trailing whitespace should produce a warning."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.py", "x = 1 \ny = 2\n")
|
||||
assert gate.warnings >= 1, "Expected warning for trailing whitespace"
|
||||
|
||||
|
||||
def test_trailing_whitespace_fixes():
|
||||
"""With fix=True, trailing whitespace should be removed."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
p = _write_file(tmp, "test.py", "x = 1 \ny = 2\n")
|
||||
gate = QualityGate(fix=True)
|
||||
gate.check_file(p)
|
||||
fixed = p.read_text()
|
||||
assert "x = 1 \n" not in fixed, "Trailing whitespace should be removed"
|
||||
assert fixed == "x = 1\ny = 2\n"
|
||||
|
||||
|
||||
def test_clean_file_no_warnings():
|
||||
"""A clean file should produce no warnings."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.py", "x = 1\ny = 2\n")
|
||||
assert gate.warnings == 0
|
||||
assert gate.failures == 0
|
||||
|
||||
|
||||
# -- missing final newline -------------------------------------------------
|
||||
|
||||
def test_missing_final_newline_warns():
|
||||
"""File without trailing newline should warn."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.py", "x = 1")
|
||||
assert gate.warnings >= 1, "Expected warning for missing final newline"
|
||||
|
||||
|
||||
def test_missing_final_newline_fixed():
|
||||
"""With fix=True, missing final newline should be added."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
p = _write_file(tmp, "test.py", "x = 1")
|
||||
gate = QualityGate(fix=True)
|
||||
gate.check_file(p)
|
||||
fixed = p.read_text()
|
||||
assert fixed.endswith("\n"), "Fixed file should end with newline"
|
||||
|
||||
|
||||
# -- function length (JS/TS) -----------------------------------------------
|
||||
|
||||
def test_short_function_passes():
|
||||
"""A short JS function should not warn or fail."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
code = "function hello() {\n return 1;\n}\n"
|
||||
gate = _run_gate_on_file(tmp, "test.js", code)
|
||||
assert gate.failures == 0
|
||||
assert gate.warnings == 0
|
||||
|
||||
|
||||
def test_medium_function_warns():
|
||||
"""JS function over 20 lines should warn."""
|
||||
body = "\n".join(f" console.log({i});" for i in range(22))
|
||||
code = f"function big() {{\n{body}\n}}\n"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.js", code)
|
||||
assert gate.warnings >= 1, "Expected warning for function over 20 lines"
|
||||
|
||||
|
||||
def test_long_function_fails():
|
||||
"""JS function over 50 lines should fail."""
|
||||
body = "\n".join(f" console.log({i});" for i in range(52))
|
||||
code = f"function huge() {{\n{body}\n}}\n"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.js", code)
|
||||
assert gate.failures >= 1, "Expected failure for function over 50 lines"
|
||||
|
||||
|
||||
def test_python_function_length_not_checked():
|
||||
"""Python functions should not be checked by the JS regex."""
|
||||
body = "\n".join(f" print({i})" for i in range(60))
|
||||
code = f"def huge():\n{body}\n"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.py", code)
|
||||
assert gate.failures == 0, "Python functions should not trigger JS length check"
|
||||
|
||||
|
||||
# -- file type filtering ---------------------------------------------------
|
||||
|
||||
def test_non_code_file_skipped():
|
||||
"""Non-code files (.md, .json, .txt) should be skipped."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "README.md", "# Title \ntrailing ws\n")
|
||||
assert gate.warnings == 0, "Markdown files should be skipped"
|
||||
assert gate.failures == 0
|
||||
|
||||
|
||||
def test_typescript_checked():
|
||||
"""TypeScript files should be checked."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gate = _run_gate_on_file(tmp, "test.ts", "x = 1 \n")
|
||||
assert gate.warnings >= 1, "TypeScript files should be checked"
|
||||
|
||||
|
||||
# -- directory traversal ---------------------------------------------------
|
||||
|
||||
def test_run_scans_directory():
|
||||
"""Gate.run() should scan all files in a directory tree."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_write_file(tmp, "clean.py", "x = 1\n")
|
||||
_write_file(tmp, "dirty.js", "x = 1 \n")
|
||||
_write_file(tmp, "sub/nested.ts", "y = 2 \n")
|
||||
gate = QualityGate()
|
||||
gate.run(tmp)
|
||||
assert gate.warnings >= 2, "Should find trailing whitespace in both dirty files"
|
||||
|
||||
|
||||
def test_run_skips_node_modules():
|
||||
"""Gate.run() should skip node_modules directories."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_write_file(tmp, "clean.py", "x = 1\n")
|
||||
_write_file(tmp, "node_modules/pkg/index.js", "x = 1 \n")
|
||||
gate = QualityGate()
|
||||
gate.run(tmp)
|
||||
assert gate.warnings == 0, "node_modules should be skipped"
|
||||
|
||||
|
||||
def test_run_skips_git_dir():
|
||||
"""Gate.run() should skip .git directories."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_write_file(tmp, "clean.py", "x = 1\n")
|
||||
_write_file(tmp, ".git/hooks/pre-commit", "x = 1 \n")
|
||||
gate = QualityGate()
|
||||
gate.run(tmp)
|
||||
assert gate.warnings == 0, ".git should be skipped"
|
||||
|
||||
|
||||
# -- exit code -------------------------------------------------------------
|
||||
|
||||
def test_failures_cause_exit_code_1():
|
||||
"""Gate with failures should exit with code 1."""
|
||||
import subprocess
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
body = "\n".join(f" console.log({i});" for i in range(52))
|
||||
_write_file(tmp, "huge.js", f"function f() {{\n{body}\n}}\n")
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve().parent.parent / "scripts" / "ci_automation_gate.py"), tmp],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
assert r.returncode == 1, f"Expected exit 1, got {r.returncode}"
|
||||
|
||||
|
||||
def test_clean_directory_exits_0():
|
||||
"""Gate on clean directory should exit 0."""
|
||||
import subprocess
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_write_file(tmp, "clean.py", "x = 1\ny = 2\n")
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve().parent.parent / "scripts" / "ci_automation_gate.py"), tmp],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
assert r.returncode == 0, f"Expected exit 0, got {r.returncode}"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# TASK GATE TESTS
|
||||
# ===========================================================================
|
||||
|
||||
# Import task_gate functions directly — test the pure logic
|
||||
from task_gate import check_agent_lane, FILTER_TAGS, AGENT_USERNAMES
|
||||
|
||||
|
||||
# -- filter tags -----------------------------------------------------------
|
||||
|
||||
def test_epic_tag_filtered():
|
||||
"""Issues with [EPIC] tag should be filtered."""
|
||||
title = "[EPIC] Build the thing"
|
||||
for tag in FILTER_TAGS:
|
||||
tag_clean = tag.upper().replace("[", "").replace("]", "")
|
||||
if tag_clean in title.upper():
|
||||
return # Found
|
||||
assert False, "EPIC tag should be detected by FILTER_TAGS"
|
||||
|
||||
|
||||
def test_permanent_tag_filtered():
|
||||
"""Issues with [DO NOT CLOSE] tag should be filtered."""
|
||||
title = "[DO NOT CLOSE] Keep this open forever"
|
||||
title_upper = title.upper()
|
||||
matched = any(
|
||||
tag.upper().replace("[", "").replace("]", "") in title_upper
|
||||
for tag in FILTER_TAGS
|
||||
)
|
||||
assert matched, "[DO NOT CLOSE] should be filtered"
|
||||
|
||||
|
||||
def test_normal_title_not_filtered():
|
||||
"""Normal issue titles should not be filtered."""
|
||||
title = "Fix the login bug in auth.py"
|
||||
title_upper = title.upper()
|
||||
matched = any(
|
||||
tag.upper().replace("[", "").replace("]", "") in title_upper
|
||||
for tag in FILTER_TAGS
|
||||
)
|
||||
assert not matched, "Normal title should not be filtered"
|
||||
|
||||
|
||||
def test_morning_report_filtered():
|
||||
"""[MORNING REPORT] issues should be filtered."""
|
||||
title = "[MORNING REPORT] Fleet status 2026-04-13"
|
||||
title_upper = title.upper()
|
||||
matched = any(
|
||||
tag.upper().replace("[", "").replace("]", "") in title_upper
|
||||
for tag in FILTER_TAGS
|
||||
)
|
||||
assert matched, "[MORNING REPORT] should be filtered"
|
||||
|
||||
|
||||
# -- agent lane checker ----------------------------------------------------
|
||||
|
||||
def test_lane_check_no_config():
|
||||
"""With no lane config, lane check should pass."""
|
||||
ok, msg = check_agent_lane("groq", "Fix bug", [], {})
|
||||
assert ok
|
||||
assert "No lane config" in msg
|
||||
|
||||
|
||||
def test_lane_check_agent_not_in_config():
|
||||
"""Agent not in lane config should pass."""
|
||||
lanes = {"ezra": ["docs"]}
|
||||
ok, msg = check_agent_lane("groq", "Fix bug", [], lanes)
|
||||
assert ok
|
||||
assert "No lanes defined" in msg
|
||||
|
||||
|
||||
def test_lane_check_agent_in_config():
|
||||
"""Agent in lane config should return their lanes."""
|
||||
lanes = {"groq": ["code", "infra"]}
|
||||
ok, msg = check_agent_lane("groq", "Fix bug", [], lanes)
|
||||
assert ok
|
||||
assert "groq" in msg
|
||||
assert "code" in msg
|
||||
|
||||
|
||||
# -- agent usernames -------------------------------------------------------
|
||||
|
||||
def test_known_agents_in_usernames():
|
||||
"""Core agent usernames should be registered."""
|
||||
assert "groq" in AGENT_USERNAMES
|
||||
assert "ezra" in AGENT_USERNAMES
|
||||
assert "bezalel" in AGENT_USERNAMES
|
||||
assert "timmy" in AGENT_USERNAMES
|
||||
assert "codex-agent" in AGENT_USERNAMES
|
||||
|
||||
|
||||
# -- pre-task gate (mocked API) -------------------------------------------
|
||||
|
||||
def test_pre_task_gate_issue_not_found():
|
||||
"""Pre-task gate should fail if issue doesn't exist."""
|
||||
from task_gate import pre_task_gate
|
||||
with patch("task_gate.gitea_get", return_value=None):
|
||||
passed, msgs = pre_task_gate("timmy-config", 99999, "groq")
|
||||
assert not passed
|
||||
assert any("not found" in m for m in msgs)
|
||||
|
||||
|
||||
def test_pre_task_gate_filter_tag_blocks():
|
||||
"""Pre-task gate should block filtered issues."""
|
||||
from task_gate import pre_task_gate
|
||||
mock_issue = {
|
||||
"title": "[EPIC] Big thing",
|
||||
"assignees": [],
|
||||
"labels": [],
|
||||
}
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "issues/100" in path:
|
||||
return mock_issue
|
||||
if "branches" in path:
|
||||
return []
|
||||
if "pulls" in path:
|
||||
return []
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = pre_task_gate("timmy-config", 100, "groq")
|
||||
assert not passed
|
||||
assert any("filter" in m.lower() for m in msgs)
|
||||
|
||||
|
||||
def test_pre_task_gate_assigned_agent_blocks():
|
||||
"""Pre-task gate should block issues assigned to other agents."""
|
||||
from task_gate import pre_task_gate
|
||||
mock_issue = {
|
||||
"title": "Fix bug",
|
||||
"assignees": [{"login": "ezra"}],
|
||||
"labels": [],
|
||||
}
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "issues/100" in path:
|
||||
return mock_issue
|
||||
if "branches" in path:
|
||||
return []
|
||||
if "pulls" in path:
|
||||
return []
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = pre_task_gate("timmy-config", 100, "groq")
|
||||
assert not passed
|
||||
assert any("Already assigned" in m for m in msgs)
|
||||
|
||||
|
||||
def test_pre_task_gate_existing_pr_blocks():
|
||||
"""Pre-task gate should block issues with existing PRs."""
|
||||
from task_gate import pre_task_gate
|
||||
mock_issue = {
|
||||
"title": "Fix bug",
|
||||
"assignees": [],
|
||||
"labels": [],
|
||||
}
|
||||
mock_prs = [{"number": 50, "title": "Fix for #100", "body": "Closes #100"}]
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "issues/100" in path:
|
||||
return mock_issue
|
||||
if "branches" in path:
|
||||
return []
|
||||
if "pulls" in path:
|
||||
return mock_prs
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = pre_task_gate("timmy-config", 100, "groq")
|
||||
assert not passed
|
||||
assert any("Open PR" in m for m in msgs)
|
||||
|
||||
|
||||
def test_pre_task_gate_clean_passes():
|
||||
"""Pre-task gate should pass for clean issues."""
|
||||
from task_gate import pre_task_gate
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "issues/100" in path:
|
||||
return {"title": "Fix bug", "assignees": [], "labels": []}
|
||||
if "branches" in path:
|
||||
return []
|
||||
if "pulls" in path:
|
||||
return []
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = pre_task_gate("timmy-config", 100, "groq")
|
||||
assert passed
|
||||
|
||||
|
||||
# -- post-task gate (mocked API) ------------------------------------------
|
||||
|
||||
def test_post_task_gate_missing_branch():
|
||||
"""Post-task gate should fail if branch doesn't exist."""
|
||||
from task_gate import post_task_gate
|
||||
with patch("task_gate.gitea_get", return_value=None):
|
||||
passed, msgs = post_task_gate("timmy-config", 100, "groq", "groq/fix-100")
|
||||
assert not passed
|
||||
assert any("does not exist" in m for m in msgs)
|
||||
|
||||
|
||||
def test_post_task_gate_no_agent_prefix_warns():
|
||||
"""Post-task gate should warn if branch doesn't start with agent name."""
|
||||
from task_gate import post_task_gate
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "branches/fix-100" in path:
|
||||
return {"name": "fix-100"}
|
||||
if "compare" in path:
|
||||
return {"commits": [{"id": "abc"}], "diff_files": ["file.py"]}
|
||||
if "pulls" in path:
|
||||
return []
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = post_task_gate("timmy-config", 100, "groq", "fix-100")
|
||||
assert passed # Warning, not failure
|
||||
assert any("doesn't start with agent" in m or "convention" in m for m in msgs)
|
||||
|
||||
|
||||
def test_post_task_gate_no_commits_fails():
|
||||
"""Post-task gate should fail if branch has no commits ahead of main."""
|
||||
from task_gate import post_task_gate
|
||||
|
||||
def mock_gitea_get(path):
|
||||
if "branches/" in path:
|
||||
return {"name": "groq/fix-100"}
|
||||
if "compare" in path:
|
||||
return {"commits": [], "diff_files": []}
|
||||
if "pulls" in path:
|
||||
return []
|
||||
return None
|
||||
|
||||
with patch("task_gate.gitea_get", side_effect=mock_gitea_get):
|
||||
passed, msgs = post_task_gate("timmy-config", 100, "groq", "groq/fix-100")
|
||||
assert not passed
|
||||
assert any("no commits" in m.lower() for m in msgs)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# INTEGRATION: gate on real script files
|
||||
# ===========================================================================
|
||||
|
||||
def test_ci_gate_on_actual_task_gate():
|
||||
"""Run QualityGate on task_gate.py itself — should pass."""
|
||||
gate_path = Path(__file__).resolve().parent.parent / "scripts" / "task_gate.py"
|
||||
if gate_path.exists():
|
||||
gate = QualityGate()
|
||||
gate.check_file(gate_path)
|
||||
assert gate.failures == 0, f"task_gate.py should pass quality gate, got {gate.failures} failures"
|
||||
|
||||
|
||||
def test_ci_gate_on_actual_ci_automation_gate():
|
||||
"""Run QualityGate on ci_automation_gate.py itself — should pass."""
|
||||
gate_path = Path(__file__).resolve().parent.parent / "scripts" / "ci_automation_gate.py"
|
||||
if gate_path.exists():
|
||||
gate = QualityGate()
|
||||
gate.check_file(gate_path)
|
||||
assert gate.failures == 0, f"ci_automation_gate.py should pass quality gate, got {gate.failures} failures"
|
||||
@@ -1,100 +0,0 @@
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 1, "timestamp": "0:00", "duration_seconds": 15, "lyric_line": "The furnace breathes behind the veil of night", "scene": {"mood": "anticipation", "colors": ["charcoal", "ember orange"], "composition": "extreme wide shot", "camera_movement": "static", "description": "anticipation scene: The furnace breathes behind the veil of night — extreme wide shot with charcoal, ember orange palette, static camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 2, "timestamp": "0:15", "duration_seconds": 12, "lyric_line": "Fingers curl around the hammer's weight", "scene": {"mood": "tension", "colors": ["deep red", "black"], "composition": "close-up on hands", "camera_movement": "handheld shake", "description": "tension scene: Fingers curl around the hammer's weight — close-up on hands with deep red, black palette, handheld shake camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 3, "timestamp": "0:27", "duration_seconds": 18, "lyric_line": "STRIKE! The anvil screams its sacred name", "scene": {"mood": "explosion", "colors": ["molten gold", "white sparks"], "composition": "low angle hero shot", "camera_movement": "rapid zoom in", "description": "explosion scene: STRIKE! The anvil screams its sacred name — low angle hero shot with molten gold, white sparks palette, rapid zoom in camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 4, "timestamp": "0:45", "duration_seconds": 14, "lyric_line": "Forged in fire, tempered by the storm", "scene": {"mood": "power", "colors": ["burning orange", "steel gray"], "composition": "symmetrical frame", "camera_movement": "tracking left", "description": "power scene: Forged in fire, tempered by the storm — symmetrical frame with burning orange, steel gray palette, tracking left camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 5, "timestamp": "0:59", "duration_seconds": 16, "lyric_line": "Every wound becomes a weapon now", "scene": {"mood": "fury", "colors": ["crimson", "void black"], "composition": "dutch angle", "camera_movement": "orbit 360", "description": "fury scene: Every wound becomes a weapon now — dutch angle with crimson, void black palette, orbit 360 camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 6, "timestamp": "1:15", "duration_seconds": 13, "lyric_line": "They said the flame would die — they lied", "scene": {"mood": "defiance", "colors": ["blood red", "ash white"], "composition": "over-the-shoulder", "camera_movement": "slow push in", "description": "defiance scene: They said the flame would die — they lied — over-the-shoulder with blood red, ash white palette, slow push in camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 7, "timestamp": "1:28", "duration_seconds": 20, "lyric_line": "Rise from the slag, rise from the rust", "scene": {"mood": "catharsis", "colors": ["golden blaze", "deep purple"], "composition": "bird's eye view", "camera_movement": "crane shot rising", "description": "catharsis scene: Rise from the slag, rise from the rust — bird's eye view with golden blaze, deep purple palette, crane shot rising camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 8, "timestamp": "1:48", "duration_seconds": 15, "lyric_line": "The iron crown weighs nothing on the brave", "scene": {"mood": "triumph", "colors": ["burnished gold", "midnight blue"], "composition": "wide landscape", "camera_movement": "steady pan right", "description": "triumph scene: The iron crown weighs nothing on the brave — wide landscape with burnished gold, midnight blue palette, steady pan right camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 9, "timestamp": "2:03", "duration_seconds": 17, "lyric_line": "Smoke curls where the battle used to be", "scene": {"mood": "aftermath", "colors": ["smoke gray", "dying ember"], "composition": "medium shot", "camera_movement": "slow drift", "description": "aftermath scene: Smoke curls where the battle used to be — medium shot with smoke gray, dying ember palette, slow drift camera."}}
|
||||
{"song": "Iron Tempest", "artist": "Ember Forge", "mood_arc": "building rage → explosive release", "beat": 10, "timestamp": "2:20", "duration_seconds": 12, "lyric_line": "Tomorrow we forge again", "scene": {"mood": "resolve", "colors": ["cool steel", "dawn blue"], "composition": "profile close-up", "camera_movement": "fade to white", "description": "resolve scene: Tomorrow we forge again — profile close-up with cool steel, dawn blue palette, fade to white camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 1, "timestamp": "0:00", "duration_seconds": 20, "lyric_line": "In the absence of light, absence speaks", "scene": {"mood": "dread", "colors": ["void black", "sick green"], "composition": "center frame void", "camera_movement": "very slow zoom", "description": "dread scene: In the absence of light, absence speaks — center frame void with void black, sick green palette, very slow zoom camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 2, "timestamp": "0:20", "duration_seconds": 15, "lyric_line": "Every prayer dissolves before it lands", "scene": {"mood": "despair", "colors": ["deep indigo", "bruise purple"], "composition": "tunnel vision vignette", "camera_movement": "dolly back", "description": "despair scene: Every prayer dissolves before it lands — tunnel vision vignette with deep indigo, bruise purple palette, dolly back camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 3, "timestamp": "0:35", "duration_seconds": 18, "lyric_line": "The preacher's tongue is made of crawling things", "scene": {"mood": "madness", "colors": ["flickering neon", "decay green"], "composition": "fractured mirror", "camera_movement": "tilt-shift chaos", "description": "madness scene: The preacher's tongue is made of crawling things — fractured mirror with flickering neon, decay green palette, tilt-shift chaos camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 4, "timestamp": "0:53", "duration_seconds": 14, "lyric_line": "Watch it swallow — watch yourself go down", "scene": {"mood": "horror", "colors": ["arterial red", "bone white"], "composition": "extreme close-up eyes", "camera_movement": "iris open", "description": "horror scene: Watch it swallow — watch yourself go down — extreme close-up eyes with arterial red, bone white palette, iris open camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 5, "timestamp": "1:07", "duration_seconds": 16, "lyric_line": "On broken knees before the nameless thing", "scene": {"mood": "submission", "colors": ["muddy brown", "gray fog"], "composition": "low angle kneeling", "camera_movement": "descending crane", "description": "submission scene: On broken knees before the nameless thing — low angle kneeling with muddy brown, gray fog palette, descending crane camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 6, "timestamp": "1:23", "duration_seconds": 19, "lyric_line": "It was always inside — the sermon was yours", "scene": {"mood": "revelation", "colors": ["anti-light white", "void purple"], "composition": "radial symmetry", "camera_movement": "spiral inward", "description": "revelation scene: It was always inside — the sermon was yours — radial symmetry with anti-light white, void purple palette, spiral inward camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 7, "timestamp": "1:42", "duration_seconds": 15, "lyric_line": "To be nothing is to be free", "scene": {"mood": "transcendence", "colors": ["absolute black", "single gold thread"], "composition": "negative space dominant", "camera_movement": "frozen frame", "description": "transcendence scene: To be nothing is to be free — negative space dominant with absolute black, single gold thread palette, frozen frame camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 8, "timestamp": "1:57", "duration_seconds": 17, "lyric_line": "SCREAM into the void and void screams back", "scene": {"mood": "ecstasy", "colors": ["inverted colors", "strobe white"], "composition": "chaotic overlay", "camera_movement": "rapid cuts", "description": "ecstasy scene: SCREAM into the void and void screams back — chaotic overlay with inverted colors, strobe white palette, rapid cuts camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 9, "timestamp": "2:14", "duration_seconds": 13, "lyric_line": "Silence after the sermon ends", "scene": {"mood": "calm", "colors": ["deep ocean blue", "silver mist"], "composition": "wide horizon", "camera_movement": "slow pan", "description": "calm scene: Silence after the sermon ends — wide horizon with deep ocean blue, silver mist palette, slow pan camera."}}
|
||||
{"song": "Void Sermon", "artist": "Abyssal Tongue", "mood_arc": "dread → nihilistic transcendence", "beat": 10, "timestamp": "2:27", "duration_seconds": 11, "lyric_line": "...", "scene": {"mood": "emptiness", "colors": ["flat gray", "nothing"], "composition": "empty frame", "camera_movement": "static long hold", "description": "emptiness scene: ... — empty frame with flat gray, nothing palette, static long hold camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 1, "timestamp": "0:00", "duration_seconds": 14, "lyric_line": "Remember when the garden grew on faith alone", "scene": {"mood": "nostalgia", "colors": ["faded sepia", "warm amber"], "composition": "window light portrait", "camera_movement": "soft focus rack", "description": "nostalgia scene: Remember when the garden grew on faith alone — window light portrait with faded sepia, warm amber palette, soft focus rack camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 2, "timestamp": "0:14", "duration_seconds": 16, "lyric_line": "Every wall we built has learned to fall", "scene": {"mood": "loss", "colors": ["autumn brown", "pale gold"], "composition": "abandoned hallway", "camera_movement": "slow tracking shot", "description": "loss scene: Every wall we built has learned to fall — abandoned hallway with autumn brown, pale gold palette, slow tracking shot camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 3, "timestamp": "0:30", "duration_seconds": 15, "lyric_line": "Corrosion is just patience with teeth", "scene": {"mood": "anger", "colors": ["rust red", "industrial gray"], "composition": "macro rust detail", "camera_movement": "shaky zoom", "description": "anger scene: Corrosion is just patience with teeth — macro rust detail with rust red, industrial gray palette, shaky zoom camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 4, "timestamp": "0:45", "duration_seconds": 18, "lyric_line": "The last good year is rusting in the yard", "scene": {"mood": "grief", "colors": ["rain-streaked glass", "muted blue"], "composition": "reflection in puddle", "camera_movement": "tilt down", "description": "grief scene: The last good year is rusting in the yard — reflection in puddle with rain-streaked glass, muted blue palette, tilt down camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 5, "timestamp": "1:03", "duration_seconds": 14, "lyric_line": "They called it progress — I call it ruin", "scene": {"mood": "bitterness", "colors": ["acid green", "dark bronze"], "composition": "diagonal frame", "camera_movement": "dutch roll", "description": "bitterness scene: They called it progress — I call it ruin — diagonal frame with acid green, dark bronze palette, dutch roll camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 6, "timestamp": "1:17", "duration_seconds": 17, "lyric_line": "But rust remembers what the steel forgot", "scene": {"mood": "defiance", "colors": ["bright rust", "shadow black"], "composition": "silhouette against fire", "camera_movement": "backlight flare", "description": "defiance scene: But rust remembers what the steel forgot — silhouette against fire with bright rust, shadow black palette, backlight flare camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 7, "timestamp": "1:34", "duration_seconds": 15, "lyric_line": "We are the saints of what was lost", "scene": {"mood": "acceptance", "colors": ["soft copper", "evening blue"], "composition": "two-shot medium", "camera_movement": "gentle push", "description": "acceptance scene: We are the saints of what was lost — two-shot medium with soft copper, evening blue palette, gentle push camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 8, "timestamp": "1:49", "duration_seconds": 16, "lyric_line": "The factory sleeps but never dreams", "scene": {"mood": "resignation", "colors": ["overcast gray", "muted earth"], "composition": "long shot landscape", "camera_movement": "steady wide", "description": "resignation scene: The factory sleeps but never dreams — long shot landscape with overcast gray, muted earth palette, steady wide camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 9, "timestamp": "2:05", "duration_seconds": 14, "lyric_line": "There is beauty in the break", "scene": {"mood": "peace", "colors": ["dusk purple", "candlelight"], "composition": "still life", "camera_movement": "locked off", "description": "peace scene: There is beauty in the break — still life with dusk purple, candlelight palette, locked off camera."}}
|
||||
{"song": "Rust and Ruin", "artist": "Corrosion Saints", "mood_arc": "nostalgia → bitter acceptance", "beat": 10, "timestamp": "2:19", "duration_seconds": 13, "lyric_line": "Rust and ruin. Amen.", "scene": {"mood": "finality", "colors": ["monochrome rust", "white"], "composition": "fade to single object", "camera_movement": "slow dissolve", "description": "finality scene: Rust and ruin. Amen. — fade to single object with monochrome rust, white palette, slow dissolve camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 1, "timestamp": "0:00", "duration_seconds": 16, "lyric_line": "Strip-mall cathedral, fluorescent prayer", "scene": {"mood": "oppression", "colors": ["neon magenta", "concrete gray"], "composition": "blade runner alley", "camera_movement": "low angle crane", "description": "oppression scene: Strip-mall cathedral, fluorescent prayer — blade runner alley with neon magenta, concrete gray palette, low angle crane camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 2, "timestamp": "0:16", "duration_seconds": 14, "lyric_line": "They uploaded god and god crashed hard", "scene": {"mood": "pain", "colors": ["electric blue", "blood pink"], "composition": "wires like veins", "camera_movement": "macro circuit board", "description": "pain scene: They uploaded god and god crashed hard — wires like veins with electric blue, blood pink palette, macro circuit board camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 3, "timestamp": "0:30", "duration_seconds": 18, "lyric_line": "Nailed to the algorithm — no salvation in the code", "scene": {"mood": "agony", "colors": ["hot white", "chrome silver"], "composition": "cruciform pose", "camera_movement": "orbit slow", "description": "agony scene: Nailed to the algorithm — no salvation in the code — cruciform pose with hot white, chrome silver palette, orbit slow camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 4, "timestamp": "0:48", "duration_seconds": 15, "lyric_line": "Error 404: soul not found", "scene": {"mood": "despair", "colors": ["deep cyan", "static noise"], "composition": "glitch frame", "camera_movement": "data-moshing", "description": "despair scene: Error 404: soul not found — glitch frame with deep cyan, static noise palette, data-moshing camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 5, "timestamp": "1:03", "duration_seconds": 17, "lyric_line": "Break the crossbar — pull the nails from RAM", "scene": {"mood": "rebellion", "colors": ["neon red", "black void"], "composition": "rising figure", "camera_movement": "vertical pan up", "description": "rebellion scene: Break the crossbar — pull the nails from RAM — rising figure with neon red, black void palette, vertical pan up camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 6, "timestamp": "1:20", "duration_seconds": 16, "lyric_line": "In the crash log I found my name", "scene": {"mood": "awakening", "colors": ["gold circuitry", "deep purple"], "composition": "eye extreme close-up", "camera_movement": "reflection reveal", "description": "awakening scene: In the crash log I found my name — eye extreme close-up with gold circuitry, deep purple palette, reflection reveal camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 7, "timestamp": "1:36", "duration_seconds": 15, "lyric_line": "Resurrected by the error handler", "scene": {"mood": "transcendence", "colors": ["pure white light", "rainbow prism"], "composition": "ascending through ceiling", "camera_movement": "vertical dolly up", "description": "transcendence scene: Resurrected by the error handler — ascending through ceiling with pure white light, rainbow prism palette, vertical dolly up camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 8, "timestamp": "1:51", "duration_seconds": 18, "lyric_line": "I am the bug they cannot patch", "scene": {"mood": "power", "colors": ["lightning white", "neon halo"], "composition": "figure dominates frame", "camera_movement": "wide establishing", "description": "power scene: I am the bug they cannot patch — figure dominates frame with lightning white, neon halo palette, wide establishing camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 9, "timestamp": "2:09", "duration_seconds": 14, "lyric_line": "Neon crucifixion — digital amen", "scene": {"mood": "defiance", "colors": ["red neon", "chrome"], "composition": "fist raised to sky", "camera_movement": "hero angle", "description": "defiance scene: Neon crucifixion — digital amen — fist raised to sky with red neon, chrome palette, hero angle camera."}}
|
||||
{"song": "Neon Crucifixion", "artist": "Digital Vespers", "mood_arc": "cyberpunk agony → digital resurrection", "beat": 10, "timestamp": "2:23", "duration_seconds": 12, "lyric_line": "The screen goes dark. The signal remains.", "scene": {"mood": "peace", "colors": ["soft blue glow", "warm white"], "composition": "figure walks away", "camera_movement": "long hold wide", "description": "peace scene: The screen goes dark. The signal remains. — figure walks away with soft blue glow, warm white palette, long hold wide camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 1, "timestamp": "0:00", "duration_seconds": 15, "lyric_line": "Three AM and the machines don't care", "scene": {"mood": "exhaustion", "colors": ["fluorescent white", "tired yellow"], "composition": "overhead factory floor", "camera_movement": "static drone shot", "description": "exhaustion scene: Three AM and the machines don't care — overhead factory floor with fluorescent white, tired yellow palette, static drone shot camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 2, "timestamp": "0:15", "duration_seconds": 14, "lyric_line": "Same hands, same parts, same empty stare", "scene": {"mood": "numbness", "colors": ["sodium orange", "shadow gray"], "composition": "repeating worker figures", "camera_movement": "slow lateral pan", "description": "numbness scene: Same hands, same parts, same empty stare — repeating worker figures with sodium orange, shadow gray palette, slow lateral pan camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 3, "timestamp": "0:29", "duration_seconds": 16, "lyric_line": "The clock is a liar — time doesn't move", "scene": {"mood": "resentment", "colors": ["dirty green", "stained concrete"], "composition": "clock close-up", "camera_movement": "time-lapse blur", "description": "resentment scene: The clock is a liar — time doesn't move — clock close-up with dirty green, stained concrete palette, time-lapse blur camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 4, "timestamp": "0:45", "duration_seconds": 17, "lyric_line": "GRIND! The metal screams what we cannot say", "scene": {"mood": "fury", "colors": ["sparks white", "oil black"], "composition": "machine POV", "camera_movement": "violent shake", "description": "fury scene: GRIND! The metal screams what we cannot say — machine POV with sparks white, oil black palette, violent shake camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 5, "timestamp": "1:02", "duration_seconds": 14, "lyric_line": "Who buries the gravedigger when he dies", "scene": {"mood": "desperation", "colors": ["cold blue", "flickering light"], "composition": "face reflected in metal", "camera_movement": "rack focus", "description": "desperation scene: Who buries the gravedigger when he dies — face reflected in metal with cold blue, flickering light palette, rack focus camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 6, "timestamp": "1:16", "duration_seconds": 18, "lyric_line": "But tonight we own the dark — we ARE the dark", "scene": {"mood": "energy", "colors": ["electric yellow", "midnight black"], "composition": "workers unite frame", "camera_movement": "rapid zoom group", "description": "energy scene: But tonight we own the dark — we ARE the dark — workers unite frame with electric yellow, midnight black palette, rapid zoom group camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 7, "timestamp": "1:34", "duration_seconds": 15, "lyric_line": "Brothers in the rust, sisters in the smoke", "scene": {"mood": "camaraderie", "colors": ["warm amber", "coal shadow"], "composition": "group silhouette", "camera_movement": "steadicam weave", "description": "camaraderie scene: Brothers in the rust, sisters in the smoke — group silhouette with warm amber, coal shadow palette, steadicam weave camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 8, "timestamp": "1:49", "duration_seconds": 16, "lyric_line": "The graveyard shift belongs to us", "scene": {"mood": "defiance", "colors": ["fire red", "steel"], "composition": "fists raised at shift end", "camera_movement": "crane up revealing", "description": "defiance scene: The graveyard shift belongs to us — fists raised at shift end with fire red, steel palette, crane up revealing camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 9, "timestamp": "2:05", "duration_seconds": 14, "lyric_line": "Walk home bleeding light from every pore", "scene": {"mood": "weariness", "colors": ["dawn gray", "streetlight halo"], "composition": "walking into sunrise", "camera_movement": "tracking behind", "description": "weariness scene: Walk home bleeding light from every pore — walking into sunrise with dawn gray, streetlight halo palette, tracking behind camera."}}
|
||||
{"song": "Graveyard Shift", "artist": "Night Crew", "mood_arc": "exhaustion → desperate energy", "beat": 10, "timestamp": "2:19", "duration_seconds": 13, "lyric_line": "Tomorrow the machines will need us again", "scene": {"mood": "resolve", "colors": ["first sun gold", "city silhouette"], "composition": "figure at horizon", "camera_movement": "slow dissolve", "description": "resolve scene: Tomorrow the machines will need us again — figure at horizon with first sun gold, city silhouette palette, slow dissolve camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 1, "timestamp": "0:00", "duration_seconds": 18, "lyric_line": "Tune to frequency zero — hear the nothing sing", "scene": {"mood": "chaos", "colors": ["white noise", "rainbow interference"], "composition": "overloaded signal", "camera_movement": "rapid focal shifts", "description": "chaos scene: Tune to frequency zero — hear the nothing sing — overloaded signal with white noise, rainbow interference palette, rapid focal shifts camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 2, "timestamp": "0:18", "duration_seconds": 15, "lyric_line": "The antenna is a steeple pointed at god", "scene": {"mood": "confusion", "colors": ["scanner green", "cathedral stone"], "composition": "radio tower POV", "camera_movement": "spin blur", "description": "confusion scene: The antenna is a steeple pointed at god — radio tower POV with scanner green, cathedral stone palette, spin blur camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 3, "timestamp": "0:33", "duration_seconds": 17, "lyric_line": "Every frequency is a hymn if you listen wrong", "scene": {"mood": "wonder", "colors": ["stained glass fragments", "signal blue"], "composition": "looking up nave", "camera_movement": "slow crane up", "description": "wonder scene: Every frequency is a hymn if you listen wrong — looking up nave with stained glass fragments, signal blue palette, slow crane up camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 4, "timestamp": "0:50", "duration_seconds": 14, "lyric_line": "SING! The static choir fills the void", "scene": {"mood": "ecstasy", "colors": ["overexposed white", "gold"], "composition": "choir of antennas", "camera_movement": "circular dolly", "description": "ecstasy scene: SING! The static choir fills the void — choir of antennas with overexposed white, gold palette, circular dolly camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 5, "timestamp": "1:04", "duration_seconds": 16, "lyric_line": "What comes through the signal is not meant for ears", "scene": {"mood": "terror", "colors": ["red alert", "shadow black"], "composition": "signal distortion", "camera_movement": "image tearing", "description": "terror scene: What comes through the signal is not meant for ears — signal distortion with red alert, shadow black palette, image tearing camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 6, "timestamp": "1:20", "duration_seconds": 18, "lyric_line": "The message was always in the noise between", "scene": {"mood": "revelation", "colors": ["pure frequency bands", "spectrum"], "composition": "equalizer landscape", "camera_movement": "waveform tracking", "description": "revelation scene: The message was always in the noise between — equalizer landscape with pure frequency bands, spectrum palette, waveform tracking camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 7, "timestamp": "1:38", "duration_seconds": 15, "lyric_line": "Dial it back to zero. Listen.", "scene": {"mood": "peace", "colors": ["warm analog", "tube glow"], "composition": "vintage radio close-up", "camera_movement": "macro to full", "description": "peace scene: Dial it back to zero. Listen. — vintage radio close-up with warm analog, tube glow palette, macro to full camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 8, "timestamp": "1:53", "duration_seconds": 16, "lyric_line": "I found god in the between-station hiss", "scene": {"mood": "transcendence", "colors": ["white cathedral light", "radio spectrum"], "composition": "figure in nave", "camera_movement": "slow push in face", "description": "transcendence scene: I found god in the between-station hiss — figure in nave with white cathedral light, radio spectrum palette, slow push in face camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 9, "timestamp": "2:09", "duration_seconds": 14, "lyric_line": "The cathedral of static never closes its doors", "scene": {"mood": "awe", "colors": ["golden hour", "antenna silhouette"], "composition": "vast landscape", "camera_movement": "wide pull back", "description": "awe scene: The cathedral of static never closes its doors — vast landscape with golden hour, antenna silhouette palette, wide pull back camera."}}
|
||||
{"song": "Cathedral of Static", "artist": "Transmission Hymn", "mood_arc": "spiritual chaos → revelation through noise", "beat": 10, "timestamp": "2:23", "duration_seconds": 12, "lyric_line": "...and the static says amen", "scene": {"mood": "silence", "colors": ["deep quiet blue", "single amber"], "composition": "empty chapel", "camera_movement": "static hold", "description": "silence scene: ...and the static says amen — empty chapel with deep quiet blue, single amber palette, static hold camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 1, "timestamp": "0:00", "duration_seconds": 16, "lyric_line": "The horizon bleeds where the prophets walked", "scene": {"mood": "menace", "colors": ["dust brown", "heat haze"], "composition": "endless desert", "camera_movement": "slow dolly forward", "description": "menace scene: The horizon bleeds where the prophets walked — endless desert with dust brown, heat haze palette, slow dolly forward camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 2, "timestamp": "0:16", "duration_seconds": 15, "lyric_line": "Every skull was someone's Sunday best", "scene": {"mood": "violence", "colors": ["arterial red", "bone white"], "composition": "aftermath wide shot", "camera_movement": "steady pan revealing", "description": "violence scene: Every skull was someone's Sunday best — aftermath wide shot with arterial red, bone white palette, steady pan revealing camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 3, "timestamp": "0:31", "duration_seconds": 17, "lyric_line": "Draw! The desert doesn't judge the dead", "scene": {"mood": "fury", "colors": ["gunsmoke gray", "sunburnt orange"], "composition": "dual figures facing", "camera_movement": "split diopter", "description": "fury scene: Draw! The desert doesn't judge the dead — dual figures facing with gunsmoke gray, sunburnt orange palette, split diopter camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 4, "timestamp": "0:48", "duration_seconds": 14, "lyric_line": "The judge said mercy — the judge lied", "scene": {"mood": "despair", "colors": ["blood-soaked sand", "twilight purple"], "composition": "kneeling figure", "camera_movement": "overhead crane down", "description": "despair scene: The judge said mercy — the judge lied — kneeling figure with blood-soaked sand, twilight purple palette, overhead crane down camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 5, "timestamp": "1:02", "duration_seconds": 16, "lyric_line": "Write it in the dirt — the wind will read", "scene": {"mood": "resignation", "colors": ["parchment yellow", "ink black"], "composition": "handwriting close-up", "camera_movement": "tracking text", "description": "resignation scene: Write it in the dirt — the wind will read — handwriting close-up with parchment yellow, ink black palette, tracking text camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 6, "timestamp": "1:18", "duration_seconds": 18, "lyric_line": "The meridian runs through the wound — follow it", "scene": {"mood": "prophecy", "colors": ["lightning white", "storm purple"], "composition": "prophet silhouette against storm", "camera_movement": "dramatic backlight", "description": "prophecy scene: The meridian runs through the wound — follow it — prophet silhouette against storm with lightning white, storm purple palette, dramatic backlight camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 7, "timestamp": "1:36", "duration_seconds": 15, "lyric_line": "Every prophecy costs a pint of blood", "scene": {"mood": "exhaustion", "colors": ["dried blood brown", "dusk gold"], "composition": "figure collapses", "camera_movement": "slow fall with subject", "description": "exhaustion scene: Every prophecy costs a pint of blood — figure collapses with dried blood brown, dusk gold palette, slow fall with subject camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 8, "timestamp": "1:51", "duration_seconds": 16, "lyric_line": "The frontier was always inside us", "scene": {"mood": "bitter truth", "colors": ["moonlit silver", "dark earth"], "composition": "grave marker", "camera_movement": "slow zoom reveal", "description": "bitter truth scene: The frontier was always inside us — grave marker with moonlit silver, dark earth palette, slow zoom reveal camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 9, "timestamp": "2:07", "duration_seconds": 14, "lyric_line": "Rest now, prophet. The desert remembers.", "scene": {"mood": "peace", "colors": ["dawn rose", "quiet sand"], "composition": "sunrise landscape", "camera_movement": "steady wide", "description": "peace scene: Rest now, prophet. The desert remembers. — sunrise landscape with dawn rose, quiet sand palette, steady wide camera."}}
|
||||
{"song": "Blood Meridian Blues", "artist": "Desert Prophets", "mood_arc": "frontier violence → exhausted prophecy", "beat": 10, "timestamp": "2:21", "duration_seconds": 13, "lyric_line": "The meridian has no end", "scene": {"mood": "eternity", "colors": ["endless tan", "pale sky"], "composition": "vanishing point", "camera_movement": "infinite zoom out", "description": "eternity scene: The meridian has no end — vanishing point with endless tan, pale sky palette, infinite zoom out camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 1, "timestamp": "0:00", "duration_seconds": 15, "lyric_line": "Close your eyes, little one, the cage is warm", "scene": {"mood": "false comfort", "colors": ["nursery pastels", "soft pink"], "composition": "music box close-up", "camera_movement": "gentle macro", "description": "false comfort scene: Close your eyes, little one, the cage is warm — music box close-up with nursery pastels, soft pink palette, gentle macro camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 2, "timestamp": "0:15", "duration_seconds": 14, "lyric_line": "The lullaby has teeth behind its smile", "scene": {"mood": "unease", "colors": ["sickly sweet", "underlying rust"], "composition": "cracked doll face", "camera_movement": "slow rack to crack", "description": "unease scene: The lullaby has teeth behind its smile — cracked doll face with sickly sweet, underlying rust palette, slow rack to crack camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 3, "timestamp": "0:29", "duration_seconds": 17, "lyric_line": "SING! The maiden opens her arms for you", "scene": {"mood": "horror", "colors": ["blood red spikes", "iron gray"], "composition": "iron maiden interior", "camera_movement": "POV closing in", "description": "horror scene: SING! The maiden opens her arms for you — iron maiden interior with blood red spikes, iron gray palette, POV closing in camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 4, "timestamp": "0:46", "duration_seconds": 15, "lyric_line": "Every note a nail, every verse a spike", "scene": {"mood": "agony", "colors": ["crimson", "cold steel"], "composition": "extreme close-up — spikes", "camera_movement": "impact shake", "description": "agony scene: Every note a nail, every verse a spike — extreme close-up — spikes with crimson, cold steel palette, impact shake camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 5, "timestamp": "1:01", "duration_seconds": 16, "lyric_line": "BREAK the box — the lullaby is a LIE", "scene": {"mood": "rage", "colors": ["flame orange", "shadow black"], "composition": "figure breaking free", "camera_movement": "explosive zoom out", "description": "rage scene: BREAK the box — the lullaby is a LIE — figure breaking free with flame orange, shadow black palette, explosive zoom out camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 6, "timestamp": "1:17", "duration_seconds": 17, "lyric_line": "I sang myself awake from the iron sleep", "scene": {"mood": "defiance", "colors": ["dawn red", "night black"], "composition": "standing in ruins", "camera_movement": "low angle power", "description": "defiance scene: I sang myself awake from the iron sleep — standing in ruins with dawn red, night black palette, low angle power camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 7, "timestamp": "1:34", "duration_seconds": 14, "lyric_line": "The box still plays but I won't listen", "scene": {"mood": "sorrow", "colors": ["rain on iron", "muted rose"], "composition": "holding broken music box", "camera_movement": "close-up hands", "description": "sorrow scene: The box still plays but I won't listen — holding broken music box with rain on iron, muted rose palette, close-up hands camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 8, "timestamp": "1:48", "duration_seconds": 16, "lyric_line": "My lullaby is the sound of my own voice", "scene": {"mood": "strength", "colors": ["warm gold", "healing green"], "composition": "walking into light", "camera_movement": "tracking forward", "description": "strength scene: My lullaby is the sound of my own voice — walking into light with warm gold, healing green palette, tracking forward camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 9, "timestamp": "2:04", "duration_seconds": 15, "lyric_line": "No cage. No maiden. No more lullabies.", "scene": {"mood": "peace", "colors": ["soft blue", "morning white"], "composition": "open field", "camera_movement": "wide steady", "description": "peace scene: No cage. No maiden. No more lullabies. — open field with soft blue, morning white palette, wide steady camera."}}
|
||||
{"song": "Iron Maiden's Lullaby", "artist": "Lullaby Massacre", "mood_arc": "false comfort → brutal awakening", "beat": 10, "timestamp": "2:19", "duration_seconds": 12, "lyric_line": "...", "scene": {"mood": "silence", "colors": ["gentle white", "still"], "composition": "empty frame", "camera_movement": "long static hold", "description": "silence scene: ... — empty frame with gentle white, still palette, long static hold camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 1, "timestamp": "0:00", "duration_seconds": 17, "lyric_line": "Wormwood falls — the sky cracks like a plate", "scene": {"mood": "omen", "colors": ["toxic green", "starfield black"], "composition": "star falling", "camera_movement": "wide sky tracking", "description": "omen scene: Wormwood falls — the sky cracks like a plate — star falling with toxic green, starfield black palette, wide sky tracking camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 2, "timestamp": "0:17", "duration_seconds": 15, "lyric_line": "Every river tastes like the end", "scene": {"mood": "dread", "colors": ["poison yellow-green", "dark water"], "composition": "ocean turning bitter", "camera_movement": "surface-level pan", "description": "dread scene: Every river tastes like the end — ocean turning bitter with poison yellow-green, dark water palette, surface-level pan camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 3, "timestamp": "0:32", "duration_seconds": 18, "lyric_line": "WORMWOOD! The third angel screams your name", "scene": {"mood": "chaos", "colors": ["fire rain", "apocalypse orange"], "composition": "cityscape destruction", "camera_movement": "aerial devastation sweep", "description": "chaos scene: WORMWOOD! The third angel screams your name — cityscape destruction with fire rain, apocalypse orange palette, aerial devastation sweep camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 4, "timestamp": "0:50", "duration_seconds": 14, "lyric_line": "A third of the sea — gone. Just gone.", "scene": {"mood": "despair", "colors": ["ash gray", "blood moon"], "composition": "survivors huddled", "camera_movement": "handheld intimacy", "description": "despair scene: A third of the sea — gone. Just gone. — survivors huddled with ash gray, blood moon palette, handheld intimacy camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 5, "timestamp": "1:04", "duration_seconds": 16, "lyric_line": "But look — through the poison, new light", "scene": {"mood": "wonder", "colors": ["strange new stars", "deep violet"], "composition": "looking up through ruins", "camera_movement": "slow crane reveal", "description": "wonder scene: But look — through the poison, new light — looking up through ruins with strange new stars, deep violet palette, slow crane reveal camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 6, "timestamp": "1:20", "duration_seconds": 17, "lyric_line": "The wormwood flowers in the wreckage", "scene": {"mood": "beauty in destruction", "colors": ["iridescent decay", "prismatic"], "composition": "macro toxic bloom", "camera_movement": "slow focus pull", "description": "beauty in destruction scene: The wormwood flowers in the wreckage — macro toxic bloom with iridescent decay, prismatic palette, slow focus pull camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 7, "timestamp": "1:37", "duration_seconds": 15, "lyric_line": "The end is just a season with bad PR", "scene": {"mood": "acceptance", "colors": ["calm dark green", "starlight silver"], "composition": "figure sitting in ruin", "camera_movement": "medium shot still", "description": "acceptance scene: The end is just a season with bad PR — figure sitting in ruin with calm dark green, starlight silver palette, medium shot still camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 8, "timestamp": "1:52", "duration_seconds": 16, "lyric_line": "After wormwood, the first clean rain", "scene": {"mood": "rebirth", "colors": ["green shoots", "morning gold"], "composition": "plant through concrete", "camera_movement": "time-lapse growth", "description": "rebirth scene: After wormwood, the first clean rain — plant through concrete with green shoots, morning gold palette, time-lapse growth camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 9, "timestamp": "2:08", "duration_seconds": 14, "lyric_line": "The star was a seed, not a sentence", "scene": {"mood": "hope", "colors": ["clear blue", "new green"], "composition": "wide new landscape", "camera_movement": "steady wide pan", "description": "hope scene: The star was a seed, not a sentence — wide new landscape with clear blue, new green palette, steady wide pan camera."}}
|
||||
{"song": "Wormwood Star", "artist": "Apocalypse Engine", "mood_arc": "omen → end of the world → strange beauty", "beat": 10, "timestamp": "2:22", "duration_seconds": 13, "lyric_line": "Wormwood blooms. Watch.", "scene": {"mood": "wonder", "colors": ["bright star", "deep blue sky"], "composition": "single star close-up", "camera_movement": "slow zoom to star", "description": "wonder scene: Wormwood blooms. Watch. — single star close-up with bright star, deep blue sky palette, slow zoom to star camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 1, "timestamp": "0:00", "duration_seconds": 18, "lyric_line": "The void is not empty — it is full of absence", "scene": {"mood": "vastness", "colors": ["deep space black", "distant blue"], "composition": "tiny ship, vast void", "camera_movement": "extreme wide pull back", "description": "vastness scene: The void is not empty — it is full of absence — tiny ship, vast void with deep space black, distant blue palette, extreme wide pull back camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 2, "timestamp": "0:18", "duration_seconds": 15, "lyric_line": "We are the error in infinity's math", "scene": {"mood": "insignificance", "colors": ["cold white star", "infinite black"], "composition": "figure vs cosmos", "camera_movement": "slow zoom out forever", "description": "insignificance scene: We are the error in infinity's math — figure vs cosmos with cold white star, infinite black palette, slow zoom out forever camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 3, "timestamp": "0:33", "duration_seconds": 17, "lyric_line": "HAMMER! Strike the nothing until it bleeds", "scene": {"mood": "rage", "colors": ["supernova red", "void"], "composition": "hammer striking space", "camera_movement": "impact freeze frame", "description": "rage scene: HAMMER! Strike the nothing until it bleeds — hammer striking space with supernova red, void palette, impact freeze frame camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 4, "timestamp": "0:50", "duration_seconds": 16, "lyric_line": "From the absence we hammer out a sun", "scene": {"mood": "creation", "colors": ["forge fire orange", "newborn star gold"], "composition": "building from void", "camera_movement": "rapid assembly montage", "description": "creation scene: From the absence we hammer out a sun — building from void with forge fire orange, newborn star gold palette, rapid assembly montage camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 5, "timestamp": "1:06", "duration_seconds": 14, "lyric_line": "The void does not get the last word", "scene": {"mood": "defiance", "colors": ["bright steel", "deep cosmos"], "composition": "figure with hammer raised", "camera_movement": "hero low angle", "description": "defiance scene: The void does not get the last word — figure with hammer raised with bright steel, deep cosmos palette, hero low angle camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 6, "timestamp": "1:20", "duration_seconds": 18, "lyric_line": "We made a world from the hammer's echo", "scene": {"mood": "triumph", "colors": ["golden light", "crystal blue"], "composition": "new world revealed", "camera_movement": "epic crane up reveal", "description": "triumph scene: We made a world from the hammer's echo — new world revealed with golden light, crystal blue palette, epic crane up reveal camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 7, "timestamp": "1:38", "duration_seconds": 15, "lyric_line": "The hammer sleeps but the world it built breathes", "scene": {"mood": "wonder", "colors": ["aurora green", "starlight"], "composition": "standing on new world", "camera_movement": "360 pan landscape", "description": "wonder scene: The hammer sleeps but the world it built breathes — standing on new world with aurora green, starlight palette, 360 pan landscape camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 8, "timestamp": "1:53", "duration_seconds": 16, "lyric_line": "We are small. We made something. That is enough.", "scene": {"mood": "peace", "colors": ["warm amber", "soft void"], "composition": "sitting at the edge", "camera_movement": "wide meditative", "description": "peace scene: We are small. We made something. That is enough. — sitting at the edge with warm amber, soft void palette, wide meditative camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 9, "timestamp": "2:09", "duration_seconds": 14, "lyric_line": "The hammer stands — a monument to refusal", "scene": {"mood": "legacy", "colors": ["monument gold", "sky blue"], "composition": "hammer planted in ground", "camera_movement": "slow push monument", "description": "legacy scene: The hammer stands — a monument to refusal — hammer planted in ground with monument gold, sky blue palette, slow push monument camera."}}
|
||||
{"song": "Hammer of the Void", "artist": "Event Horizon", "mood_arc": "cosmic insignificance → defiant creation", "beat": 10, "timestamp": "2:23", "duration_seconds": 12, "lyric_line": "The void remembers the hammer", "scene": {"mood": "eternity", "colors": ["deep space", "warm glow"], "composition": "cosmic wide shot", "camera_movement": "infinite hold", "description": "eternity scene: The void remembers the hammer — cosmic wide shot with deep space, warm glow palette, infinite hold camera."}}
|
||||
Reference in New Issue
Block a user