From 82d94f6e0e072d7f477953b89ec50b4d36ba9870 Mon Sep 17 00:00:00 2001 From: gilesb Date: Mon, 12 Jan 2026 00:20:26 +0000 Subject: [PATCH] Add inline media previews for runs list and detail page - Run card shows thumbnail previews for inputs and output - Run detail shows output media inline (image/video/audio) - Add audio detection (MP3, FLAC, OGG, WAV) to detect_media_type - Add debug logging for recipe count on home page - Add console.log debugging for DAG elements Co-Authored-By: Claude Opus 4.5 --- app/routers/home.py | 8 +++-- app/routers/runs.py | 43 +++++++++++++++++++++++ app/services/run_service.py | 15 +++++++- app/templates/runs/_run_card.html | 57 ++++++++++++++++++++++++++----- app/templates/runs/detail.html | 25 +++++++++++++- 5 files changed, 136 insertions(+), 12 deletions(-) diff --git a/app/routers/home.py b/app/routers/home.py index 3d5e293..5b19d37 100644 --- a/app/routers/home.py +++ b/app/routers/home.py @@ -43,11 +43,15 @@ async def home(request: Request): try: from ..services.recipe_service import RecipeService from ..dependencies import get_redis_client, get_cache_manager + import logging + logger = logging.getLogger(__name__) recipe_service = RecipeService(get_redis_client(), get_cache_manager()) recipes = await recipe_service.list_recipes(user.actor_id) stats["recipes"] = len(recipes) - except Exception: - pass + logger.info(f"Home page: found {len(recipes)} recipes for {user.actor_id}") + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to get recipe count: {e}") try: from ..services.run_service import RunService from ..dependencies import get_redis_client, get_cache_manager diff --git a/app/routers/runs.py b/app/routers/runs.py index eda4cda..1e4e219 100644 --- a/app/routers/runs.py +++ b/app/routers/runs.py @@ -182,6 +182,7 @@ async def get_run( # Build artifacts list from output and inputs artifacts = [] + output_media_type = None if run.get("output_hash"): # Detect media type using magic bytes output_hash = run["output_hash"] @@ -192,6 +193,7 @@ async def get_run( if cache_path and cache_path.exists(): simple_type = detect_media_type(cache_path) media_type = type_to_mime(simple_type) + output_media_type = media_type except Exception: pass artifacts.append({ @@ -263,6 +265,7 @@ async def get_run( artifacts=artifacts, run_inputs=run_inputs, dag_elements=dag_elements, + output_media_type=output_media_type, active_tab="runs", ) @@ -308,6 +311,46 @@ async def list_runs( if wants_json(request): return {"runs": runs, "offset": offset, "limit": limit, "has_more": has_more} + # Add media info for inline previews (only for HTML) + cache_manager = get_cache_manager() + from ..services.run_service import detect_media_type + + def type_to_mime(simple_type: str) -> str: + if simple_type == "video": + return "video/mp4" + elif simple_type == "image": + return "image/jpeg" + elif simple_type == "audio": + return "audio/mpeg" + return None + + for run in runs: + # Add output media info + if run.get("output_hash"): + try: + cache_path = cache_manager.get_by_content_hash(run["output_hash"]) + if cache_path and cache_path.exists(): + simple_type = detect_media_type(cache_path) + run["output_media_type"] = type_to_mime(simple_type) + except Exception: + pass + + # Add input media info (first 3 inputs) + input_previews = [] + inputs = run.get("inputs", []) + if isinstance(inputs, list): + for input_hash in inputs[:3]: + preview = {"hash": input_hash, "media_type": None} + try: + cache_path = cache_manager.get_by_content_hash(input_hash) + if cache_path and cache_path.exists(): + simple_type = detect_media_type(cache_path) + preview["media_type"] = type_to_mime(simple_type) + except Exception: + pass + input_previews.append(preview) + run["input_previews"] = input_previews + templates = get_templates(request) return render(templates, "runs/list.html", request, runs=runs, diff --git a/app/services/run_service.py b/app/services/run_service.py index 749755f..207e5e9 100644 --- a/app/services/run_service.py +++ b/app/services/run_service.py @@ -50,7 +50,10 @@ def detect_media_type(cache_path: Path) -> str: # Video signatures if header[:4] == b'\x1a\x45\xdf\xa3': # WebM/MKV return "video" - if len(header) > 8 and header[4:8] == b'ftyp': # MP4/MOV + if len(header) > 8 and header[4:8] == b'ftyp': # MP4/MOV/M4A + # Check for audio-only M4A + if len(header) > 11 and header[8:12] in (b'M4A ', b'm4a '): + return "audio" return "video" if header[:4] == b'RIFF' and len(header) > 12 and header[8:12] == b'AVI ': # AVI return "video" @@ -65,6 +68,16 @@ def detect_media_type(cache_path: Path) -> str: if header[:4] == b'RIFF' and len(header) > 12 and header[8:12] == b'WEBP': # WebP return "image" + # Audio signatures + if header[:3] == b'ID3' or header[:2] == b'\xff\xfb': # MP3 + return "audio" + if header[:4] == b'fLaC': # FLAC + return "audio" + if header[:4] == b'OggS': # Ogg (could be audio or video, assume audio) + return "audio" + if header[:4] == b'RIFF' and len(header) > 12 and header[8:12] == b'WAVE': # WAV + return "audio" + return "unknown" diff --git a/app/templates/runs/_run_card.html b/app/templates/runs/_run_card.html index a987fce..88e03db 100644 --- a/app/templates/runs/_run_card.html +++ b/app/templates/runs/_run_card.html @@ -23,7 +23,7 @@ {{ run.created_at }} -
+
Recipe: {{ run.recipe_name or (run.recipe[:12] ~ '...' if run.recipe and run.recipe|length > 12 else run.recipe) or 'Unknown' }} @@ -34,15 +34,56 @@ {% endif %}
+
+ + {# Media previews row #} +
+ {# Input previews #} + {% if run.input_previews %} +
+ In: + {% for inp in run.input_previews %} + {% if inp.media_type and inp.media_type.startswith('image/') %} + + {% elif inp.media_type and inp.media_type.startswith('video/') %} + + {% else %} +
?
+ {% endif %} + {% endfor %} + {% if run.inputs and run.inputs|length > 3 %} + +{{ run.inputs|length - 3 }} + {% endif %} +
+ {% elif run.inputs %} +
+ {{ run.inputs|length }} input(s) +
+ {% endif %} + + {# Arrow #} + -> + + {# Output preview #} + {% if run.output_hash %} +
+ Out: + {% if run.output_media_type and run.output_media_type.startswith('image/') %} + + {% elif run.output_media_type and run.output_media_type.startswith('video/') %} + + {% else %} +
?
+ {% endif %} +
+ {% else %} + No output yet + {% endif %} + +
{% if run.output_hash %} - {{ run.output_hash[:16] }}... + {{ run.output_hash[:12] }}... {% endif %}
- - {% if run.inputs %} -
- Inputs: {{ run.inputs | length }} file(s) -
- {% endif %} diff --git a/app/templates/runs/detail.html b/app/templates/runs/detail.html index 4f90904..cd9c32f 100644 --- a/app/templates/runs/detail.html +++ b/app/templates/runs/detail.html @@ -351,8 +351,29 @@ {% if run.output_hash %}

Output

+ + {# Inline media preview #} +
+ {% if output_media_type and output_media_type.startswith('image/') %} + + Output + + {% elif output_media_type and output_media_type.startswith('video/') %} + + {% elif output_media_type and output_media_type.startswith('audio/') %} + + {% else %} +
+
?
+
{{ output_media_type or 'Unknown media type' }}
+
+ {% endif %} +
+
- + {{ run.output_hash }} {% if run.output_ipfs_cid %} @@ -394,6 +415,8 @@ document.querySelectorAll('.step-item').forEach(el => { config: JSON.parse(el.dataset.stepConfig || '{}') }; }); +console.log('stepData loaded:', Object.keys(stepData).length, 'steps'); +console.log('dag_elements:', {{ dag_elements | tojson }}); let cy = null; let selectedNode = null;