Fix analysis display for recipe-based runs

Add get_run_analysis() to RunService to load per-input analysis from
CACHE_DIR/analysis/{hash}.json files. Update runs router and template
to display tempo, beats, energy, and beat timeline visualization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-01-11 08:05:40 +00:00
parent 29d8d06d76
commit 945fb3b413
3 changed files with 126 additions and 7 deletions

View File

@@ -56,7 +56,7 @@
onclick="showTab('plan')">Plan</a>
<a href="#artifacts" class="tab-link border-b-2 border-transparent text-gray-400 hover:text-white pb-3 px-1"
onclick="showTab('artifacts')">Artifacts</a>
{% if run.analysis %}
{% if analysis %}
<a href="#analysis" class="tab-link border-b-2 border-transparent text-gray-400 hover:text-white pb-3 px-1"
onclick="showTab('analysis')">Analysis</a>
{% endif %}
@@ -158,13 +158,89 @@
<!-- Analysis Tab -->
<div id="tab-analysis" class="tab-content hidden">
{% if run.analysis %}
<div class="bg-gray-800 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Audio Analysis</h3>
<pre class="text-sm text-gray-300 overflow-x-auto">{{ run.analysis | tojson(indent=2) }}</pre>
{% if analysis %}
<div class="space-y-6">
{% for item in analysis %}
<div class="bg-gray-800 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">{{ item.input_name }}</h3>
<a href="/cache/{{ item.input_hash }}" class="font-mono text-xs text-blue-400 hover:text-blue-300">
{{ item.input_hash[:16] }}...
</a>
</div>
{% if item.has_analysis %}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
{% if item.tempo %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Tempo</div>
<div class="text-2xl font-bold text-pink-400">{{ item.tempo | round(1) }}</div>
<div class="text-gray-500 text-xs">BPM</div>
</div>
{% endif %}
{% if item.beat_times %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Beats</div>
<div class="text-2xl font-bold text-blue-400">{{ item.beat_times | length }}</div>
<div class="text-gray-500 text-xs">detected</div>
</div>
{% endif %}
{% if item.energy is not none %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Energy</div>
<div class="text-2xl font-bold text-green-400">{{ (item.energy * 100) | round(1) }}%</div>
<div class="text-gray-500 text-xs">average</div>
</div>
{% endif %}
{% if item.beat_times %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Duration</div>
<div class="text-2xl font-bold text-purple-400">
{{ (item.beat_times[-1] | default(0)) | round(1) }}s
</div>
<div class="text-gray-500 text-xs">analyzed</div>
</div>
{% endif %}
</div>
<!-- Beat visualization -->
{% if item.beat_times and item.beat_times | length > 0 %}
<div class="mt-4">
<div class="text-gray-500 text-sm mb-2">Beat Timeline</div>
<div class="bg-gray-900 rounded h-8 relative overflow-hidden">
{% set max_time = item.beat_times[-1] if item.beat_times else 1 %}
{% for beat_time in item.beat_times[:100] %}
<div class="absolute top-0 bottom-0 w-px bg-pink-500 opacity-60"
style="left: {{ (beat_time / max_time * 100) | round(2) }}%"></div>
{% endfor %}
</div>
<div class="flex justify-between text-xs text-gray-600 mt-1">
<span>0s</span>
<span>{{ max_time | round(1) }}s</span>
</div>
</div>
{% endif %}
<!-- Raw data toggle -->
<details class="mt-4">
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm">
Show Raw Analysis Data
</summary>
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4 mt-2 overflow-x-auto">
<pre class="text-sm text-gray-300 whitespace-pre-wrap">{{ item.raw | tojson(indent=2) }}</pre>
</div>
</details>
{% else %}
<p class="text-gray-500">No analysis data available for this input.</p>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500">No analysis data available.</p>
<p class="text-gray-500">No inputs with analysis data.</p>
{% endif %}
</div>