feat: enhance model caching and output modalities handling

- Updated `refresh_models_cache` to include output modalities in the models cache.
- Added `get_model_output_modalities` function to retrieve output modalities for a specific model.
- Modified tests to cover new functionality for output modalities.
- Updated OpenRouter video generation functions to support audio generation and improved error handling.
- Enhanced dashboard to display generated images and videos.
- Refactored frontend templates to accommodate new data structures for generated content.
- Adjusted tests to validate changes in model handling and dashboard rendering.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-29 15:20:48 +02:00
parent 3d32e6df74
commit 712c556032
15 changed files with 618 additions and 219 deletions
+7 -1
View File
@@ -178,7 +178,13 @@ def dashboard():
user = resp.json() if resp.status_code == 200 else {}
img_resp = _api("GET", "/images/", token=token)
images = img_resp.json() if img_resp.status_code == 200 else []
return render_template("dashboard.html", user=user, images=images)
gen_resp = _api("GET", "/generate/images", token=token)
generated_images = gen_resp.json() if gen_resp.status_code == 200 else []
vid_resp = _api("GET", "/generate/videos", token=token)
generated_videos = vid_resp.json() if vid_resp.status_code == 200 else []
return render_template("dashboard.html", user=user, images=images,
generated_images=generated_images,
generated_videos=generated_videos)
# ── Generate ──────────────────────────────────────────────────────────────
+53 -1
View File
@@ -6,7 +6,59 @@ endblock %} {% block content %}
<a href="{{ url_for('generate') }}" class="btn">Start generating</a>
</div>
{% if images %}
{% if generated_images %}
<div class="card mt-2">
<h2>Generated images</h2>
<div class="image-grid">
{% for img in generated_images %}
<div class="image-grid-item">
<img
src="{{ img.image_data }}"
alt="{{ img.prompt }}"
class="generated-image"
loading="lazy"
/>
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
<strong>{{ img.model_id }}</strong><br />{{ img.prompt[:80] }}{% if
img.prompt|length > 80 %}…{% endif %}
</p>
</div>
{% endfor %}
</div>
</div>
{% endif %} {% if generated_videos %}
<div class="card mt-2">
<h2>Generated videos</h2>
<div class="image-grid">
{% for vid in generated_videos %}
<div class="image-grid-item">
{% if vid.video_url %}
<video controls style="max-width: 100%; border-radius: 6px">
<source src="{{ vid.video_url }}" />
Your browser does not support the video tag.
</video>
{% else %}
<div
style="
background: #1a1a1a;
border-radius: 6px;
padding: 2rem;
text-align: center;
"
>
<span class="text-muted">{{ vid.status | capitalize }} &hellip;</span>
</div>
{% endif %}
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
<strong>{{ vid.model_id }}</strong><br />{{ vid.prompt[:80] }}{% if
vid.prompt|length > 80 %}…{% endif %}<br />
<em>{{ vid.status }}</em>
</p>
</div>
{% endfor %}
</div>
</div>
{% endif %} {% if images %}
<div class="card mt-2">
<h2>Uploaded reference images</h2>
<div class="image-grid">
+19 -32
View File
@@ -8,12 +8,12 @@
{% if models %}
<select id="model" name="model" required>
{% for m in models %}
<option value="{{ m.id }}" {% if request.form.get('model', '') == m.id %}selected{% endif %}>{{ m.name }}</option>
<option value="{{ m.id }}" {{ "selected" if request.form.get('model', '') == m.id else "" }}>{{ m.name }}</option>
{% endfor %}
</select>
{% else %}
<input id="model" name="model" type="text" required
placeholder="e.g. openai/dall-e-3"
placeholder="e.g. google/gemini-2.5-flash-image"
value="{{ request.form.get('model', '') }}">
{% endif %}
@@ -21,40 +21,25 @@
<textarea id="prompt" name="prompt" rows="4" required
placeholder="Describe the image you want…">{{ request.form.get('prompt', '') }}</textarea>
<label for="size">Size</label>
<select id="size" name="size">
<option value="1024x1024" {% if request.form.get('size','1024x1024')=='1024x1024' %}selected{% endif %}>1024×1024</option>
<option value="1792x1024" {% if request.form.get('size')=='1792x1024' %}selected{% endif %}>1792×1024 (landscape)</option>
<option value="1024x1792" {% if request.form.get('size')=='1024x1792' %}selected{% endif %}>1024×1792 (portrait)</option>
<option value="512x512" {% if request.form.get('size')=='512x512' %}selected{% endif %}>512×512</option>
</select>
<label for="aspect_ratio">Aspect ratio</label>
<select id="aspect_ratio" name="aspect_ratio">
<option value="">Auto (default)</option>
<option value="1:1" {% if request.form.get('aspect_ratio')=='1:1' %}selected{% endif %}>1:1 (square)</option>
<option value="16:9" {% if request.form.get('aspect_ratio')=='16:9' %}selected{% endif %}>16:9 (landscape)</option>
<option value="9:16" {% if request.form.get('aspect_ratio')=='9:16' %}selected{% endif %}>9:16 (portrait)</option>
<option value="4:3" {% if request.form.get('aspect_ratio')=='4:3' %}selected{% endif %}>4:3</option>
<option value="3:4" {% if request.form.get('aspect_ratio')=='3:4' %}selected{% endif %}>3:4</option>
<option value="3:2" {% if request.form.get('aspect_ratio')=='3:2' %}selected{% endif %}>3:2</option>
<option value="2:3" {% if request.form.get('aspect_ratio')=='2:3' %}selected{% endif %}>2:3</option>
<option value="">Auto (default 1:1)</option>
<option value="1:1" {{ "selected" if request.form.get('aspect_ratio')=='1:1' else "" }}>1:1 (square)</option>
<option value="16:9" {{ "selected" if request.form.get('aspect_ratio')=='16:9' else "" }}>16:9 (landscape)</option>
<option value="9:16" {{ "selected" if request.form.get('aspect_ratio')=='9:16' else "" }}>9:16 (portrait)</option>
<option value="4:3" {{ "selected" if request.form.get('aspect_ratio')=='4:3' else "" }}>4:3</option>
<option value="3:4" {{ "selected" if request.form.get('aspect_ratio')=='3:4' else "" }}>3:4</option>
<option value="3:2" {{ "selected" if request.form.get('aspect_ratio')=='3:2' else "" }}>3:2</option>
<option value="2:3" {{ "selected" if request.form.get('aspect_ratio')=='2:3' else "" }}>2:3</option>
</select>
<label for="image_size">Resolution</label>
<select id="image_size" name="image_size">
<option value="">Auto (default)</option>
<option value="0.5K" {% if request.form.get('image_size')=='0.5K' %}selected{% endif %}>0.5K (low)</option>
<option value="1K" {% if request.form.get('image_size')=='1K' %}selected{% endif %}>1K (standard)</option>
<option value="2K" {% if request.form.get('image_size')=='2K' %}selected{% endif %}>2K (high)</option>
<option value="4K" {% if request.form.get('image_size')=='4K' %}selected{% endif %}>4K (ultra)</option>
</select>
<label for="n">Number of images</label>
<select id="n" name="n">
<option value="1" {% if request.form.get('n','1')=='1' %}selected{% endif %}>1</option>
<option value="2" {% if request.form.get('n')=='2' %}selected{% endif %}>2</option>
<option value="4" {% if request.form.get('n')=='4' %}selected{% endif %}>4</option>
<option value="">Auto (default 1K)</option>
<option value="0.5K" {{ "selected" if request.form.get('image_size')=='0.5K' else "" }}>0.5K (low)</option>
<option value="1K" {{ "selected" if request.form.get('image_size')=='1K' else "" }}>1K (standard)</option>
<option value="2K" {{ "selected" if request.form.get('image_size')=='2K' else "" }}>2K (high)</option>
<option value="4K" {{ "selected" if request.form.get('image_size')=='4K' else "" }}>4K (ultra)</option>
</select>
<label for="reference_image">Reference image (optional)</label>
@@ -65,7 +50,7 @@
accept="image/png,image/jpeg,image/webp,image/gif"
>
<p class="text-muted mt-1" id="reference-image-help">
Upload image for visual reference in upcoming image-to-image flow.
Upload an image to use as visual reference (image-to-image).
</p>
<div class="image-upload-preview" id="image-upload-preview" hidden>
<p class="text-muted" id="image-upload-filename"></p>
@@ -83,7 +68,9 @@
<div class="result">
<h2>Generated image{{ 's' if result.images|length > 1 }}</h2>
{% for img in result.images %}
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
{% if img.url %}
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
{% endif %}
{% if img.revised_prompt %}
<p class="text-muted mt-1" style="font-size:0.8rem;">{{ img.revised_prompt }}</p>
{% endif %}
+28 -3
View File
@@ -151,7 +151,8 @@ def test_dashboard_renders_user_info(client):
me_mock = _mock_response(
200, {"id": "1", "email": "u@example.com", "role": "user"})
images_mock = _mock_response(200, [])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
gen_images_mock = _mock_response(200, [])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock]):
resp = client.get("/dashboard")
assert resp.status_code == 200
assert b"u@example.com" in resp.data
@@ -531,19 +532,43 @@ def test_dashboard_shows_uploaded_images(client):
{"id": "img-1", "filename": "cat.png", "content_type": "image/png",
"size_bytes": 1024, "created_at": "2026-04-29T10:00:00"},
])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
gen_images_mock = _mock_response(200, [])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock]):
resp = client.get("/dashboard")
assert resp.status_code == 200
assert b"cat.png" in resp.data
assert b"img-1" in resp.data
def test_dashboard_shows_generated_images(client):
_set_auth(client)
me_mock = _mock_response(
200, {"id": "1", "email": "u@example.com", "role": "user"})
images_mock = _mock_response(200, [])
gen_images_mock = _mock_response(200, [
{
"id": "gen-1",
"model_id": "google/gemini-2.5-flash-image",
"prompt": "A cat on the moon",
"image_data": "data:image/png;base64,abc123",
"created_at": "2026-04-29T10:00:00",
}
])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock]):
resp = client.get("/dashboard")
assert resp.status_code == 200
assert b"Generated images" in resp.data
assert b"A cat on the moon" in resp.data
assert b"data:image/png;base64,abc123" in resp.data
def test_dashboard_no_images_section_when_empty(client):
_set_auth(client)
me_mock = _mock_response(
200, {"id": "1", "email": "u@example.com", "role": "user"})
images_mock = _mock_response(200, [])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
gen_images_mock = _mock_response(200, [])
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock]):
resp = client.get("/dashboard")
assert resp.status_code == 200
assert b"Uploaded reference images" not in resp.data