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:
@@ -172,9 +172,10 @@ async def run_detail(
|
|||||||
if not run:
|
if not run:
|
||||||
raise HTTPException(404, f"Run {run_id} not found")
|
raise HTTPException(404, f"Run {run_id} not found")
|
||||||
|
|
||||||
# Get plan and artifacts
|
# Get plan, artifacts, and analysis
|
||||||
plan = await run_service.get_run_plan(run_id)
|
plan = await run_service.get_run_plan(run_id)
|
||||||
artifacts = await run_service.get_run_artifacts(run_id)
|
artifacts = await run_service.get_run_artifacts(run_id)
|
||||||
|
analysis = await run_service.get_run_analysis(run_id)
|
||||||
|
|
||||||
# Build DAG elements for visualization
|
# Build DAG elements for visualization
|
||||||
dag_elements = []
|
dag_elements = []
|
||||||
@@ -208,6 +209,7 @@ async def run_detail(
|
|||||||
"run": run,
|
"run": run,
|
||||||
"plan": plan,
|
"plan": plan,
|
||||||
"artifacts": artifacts,
|
"artifacts": artifacts,
|
||||||
|
"analysis": analysis,
|
||||||
}
|
}
|
||||||
|
|
||||||
templates = get_templates(request)
|
templates = get_templates(request)
|
||||||
@@ -215,6 +217,7 @@ async def run_detail(
|
|||||||
run=run,
|
run=run,
|
||||||
plan=plan,
|
plan=plan,
|
||||||
artifacts=artifacts,
|
artifacts=artifacts,
|
||||||
|
analysis=analysis,
|
||||||
dag_elements=dag_elements,
|
dag_elements=dag_elements,
|
||||||
user=ctx,
|
user=ctx,
|
||||||
active_tab="runs",
|
active_tab="runs",
|
||||||
|
|||||||
@@ -225,3 +225,43 @@ class RunService:
|
|||||||
artifacts.append(info)
|
artifacts.append(info)
|
||||||
|
|
||||||
return artifacts
|
return artifacts
|
||||||
|
|
||||||
|
async def get_run_analysis(self, run_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Get analysis data for each input in a run."""
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
run = await self.get_run(run_id)
|
||||||
|
if not run:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cache_dir = Path(os.environ.get("CACHE_DIR", "/tmp/artdag-cache"))
|
||||||
|
analysis_dir = cache_dir / "analysis"
|
||||||
|
|
||||||
|
results = []
|
||||||
|
inputs = run.get("inputs", [])
|
||||||
|
if isinstance(inputs, dict):
|
||||||
|
inputs = list(inputs.values())
|
||||||
|
|
||||||
|
for i, input_hash in enumerate(inputs):
|
||||||
|
analysis_path = analysis_dir / f"{input_hash}.json"
|
||||||
|
analysis_data = None
|
||||||
|
|
||||||
|
if analysis_path.exists():
|
||||||
|
try:
|
||||||
|
with open(analysis_path) as f:
|
||||||
|
analysis_data = json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"input_hash": input_hash,
|
||||||
|
"input_name": f"Input {i + 1}",
|
||||||
|
"has_analysis": analysis_data is not None,
|
||||||
|
"tempo": analysis_data.get("tempo") if analysis_data else None,
|
||||||
|
"beat_times": analysis_data.get("beat_times", []) if analysis_data else [],
|
||||||
|
"energy": analysis_data.get("energy") if analysis_data else None,
|
||||||
|
"raw": analysis_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
onclick="showTab('plan')">Plan</a>
|
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"
|
<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>
|
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"
|
<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>
|
onclick="showTab('analysis')">Analysis</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -158,13 +158,89 @@
|
|||||||
|
|
||||||
<!-- Analysis Tab -->
|
<!-- Analysis Tab -->
|
||||||
<div id="tab-analysis" class="tab-content hidden">
|
<div id="tab-analysis" class="tab-content hidden">
|
||||||
{% if run.analysis %}
|
{% if analysis %}
|
||||||
<div class="bg-gray-800 rounded-lg p-6">
|
<div class="space-y-6">
|
||||||
<h3 class="text-lg font-semibold mb-4">Audio Analysis</h3>
|
{% for item in analysis %}
|
||||||
<pre class="text-sm text-gray-300 overflow-x-auto">{{ run.analysis | tojson(indent=2) }}</pre>
|
<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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-gray-500">No analysis data available.</p>
|
<p class="text-gray-500">No inputs with analysis data.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user