Add REST API for preset-based generation and fallback cover images
REST API (routes/api.py): Three endpoints behind API key auth for programmatic image generation via presets — list presets, queue generation with optional overrides, and poll job status. Shared generation logic extracted from routes/presets.py into services/generation.py so both web UI and API use the same code path. Fallback covers: library index pages now show a random generated image at reduced opacity when no cover is assigned, instead of "No Image". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ action.slug }}" src="{{ url_for('static', filename='uploads/' + action.image_path) }}" alt="{{ action.name }}">
|
||||
<span id="no-img-{{ action.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('actions', action.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ action.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ action.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ action.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ action.slug }}" src="" alt="{{ action.name }}" class="d-none">
|
||||
<span id="no-img-{{ action.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ action.name }}</h5>
|
||||
|
||||
@@ -28,9 +28,15 @@
|
||||
<img id="img-{{ ckpt.slug }}" src="{{ url_for('static', filename='uploads/' + ckpt.image_path) }}" alt="{{ ckpt.name }}">
|
||||
<span id="no-img-{{ ckpt.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('checkpoints', ckpt.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ ckpt.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ ckpt.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ ckpt.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ ckpt.slug }}" src="" alt="{{ ckpt.name }}" class="d-none">
|
||||
<span id="no-img-{{ ckpt.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ ckpt.name }}</h5>
|
||||
|
||||
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ detailer.slug }}" src="{{ url_for('static', filename='uploads/' + detailer.image_path) }}" alt="{{ detailer.name }}">
|
||||
<span id="no-img-{{ detailer.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('detailers', detailer.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ detailer.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ detailer.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ detailer.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ detailer.slug }}" src="" alt="{{ detailer.name }}" class="d-none">
|
||||
<span id="no-img-{{ detailer.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ detailer.name }}</h5>
|
||||
|
||||
@@ -22,9 +22,15 @@
|
||||
<img id="img-{{ char.slug }}" src="{{ url_for('static', filename='uploads/' + char.image_path) }}" alt="{{ char.name }}">
|
||||
<span id="no-img-{{ char.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('characters', char.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ char.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ char.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ char.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ char.slug }}" src="" alt="{{ char.name }}" class="d-none">
|
||||
<span id="no-img-{{ char.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ char.name }}</h5>
|
||||
|
||||
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ look.slug }}" src="{{ url_for('static', filename='uploads/' + look.image_path) }}" alt="{{ look.name }}">
|
||||
<span id="no-img-{{ look.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('looks', look.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ look.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ look.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ look.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ look.slug }}" src="" alt="{{ look.name }}" class="d-none">
|
||||
<span id="no-img-{{ look.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if look_assignments.get(look.look_id, 0) > 0 %}
|
||||
<span class="assignment-badge" title="Assigned to {{ look_assignments.get(look.look_id, 0) }} character(s)">{{ look_assignments.get(look.look_id, 0) }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ outfit.slug }}" src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}">
|
||||
<span id="no-img-{{ outfit.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('outfits', outfit.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ outfit.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ outfit.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ outfit.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ outfit.slug }}" src="" alt="{{ outfit.name }}" class="d-none">
|
||||
<span id="no-img-{{ outfit.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if outfit.data.lora and outfit.data.lora.lora_name and lora_assignments.get(outfit.data.lora.lora_name, 0) > 0 %}
|
||||
<span class="assignment-badge" title="Assigned to {{ lora_assignments.get(outfit.data.lora.lora_name, 0) }} character(s)">{{ lora_assignments.get(outfit.data.lora.lora_name, 0) }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -27,8 +27,13 @@
|
||||
{% if preset.image_path %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + preset.image_path) }}" alt="{{ preset.name }}">
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('presets', preset.slug) %}
|
||||
{% if fallback %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ preset.name }}" class="fallback-cover">
|
||||
{% else %}
|
||||
<span class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<h6 class="card-title text-center mb-1">{{ preset.name }}</h6>
|
||||
|
||||
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ scene.slug }}" src="{{ url_for('static', filename='uploads/' + scene.image_path) }}" alt="{{ scene.name }}">
|
||||
<span id="no-img-{{ scene.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('scenes', scene.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ scene.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ scene.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ scene.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ scene.slug }}" src="" alt="{{ scene.name }}" class="d-none">
|
||||
<span id="no-img-{{ scene.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ scene.name }}</h5>
|
||||
|
||||
@@ -141,6 +141,25 @@
|
||||
<button type="submit" class="btn btn-primary btn-lg">Save All Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5 class="mb-3 text-primary">REST API Key</h5>
|
||||
<p class="text-muted small">Generate an API key to use the <code>/api/v1/</code> endpoints for programmatic image generation.</p>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control font-monospace" id="api-key-display"
|
||||
value="{{ settings.api_key or '' }}" readonly
|
||||
placeholder="No API key generated yet">
|
||||
<button class="btn btn-outline-secondary" type="button" id="api-key-toggle" title="Show/hide key">
|
||||
<i class="bi bi-eye"></i> Show
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="api-key-copy" title="Copy to clipboard">
|
||||
<i class="bi bi-clipboard"></i> Copy
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" type="button" id="api-key-regen">Regenerate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,6 +251,54 @@
|
||||
});
|
||||
}
|
||||
|
||||
// API Key Management
|
||||
const apiKeyDisplay = document.getElementById('api-key-display');
|
||||
const apiKeyToggle = document.getElementById('api-key-toggle');
|
||||
const apiKeyCopy = document.getElementById('api-key-copy');
|
||||
const apiKeyRegen = document.getElementById('api-key-regen');
|
||||
|
||||
if (apiKeyToggle) {
|
||||
apiKeyToggle.addEventListener('click', () => {
|
||||
if (apiKeyDisplay.type === 'password') {
|
||||
apiKeyDisplay.type = 'text';
|
||||
apiKeyToggle.innerHTML = '<i class="bi bi-eye-slash"></i> Hide';
|
||||
} else {
|
||||
apiKeyDisplay.type = 'password';
|
||||
apiKeyToggle.innerHTML = '<i class="bi bi-eye"></i> Show';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyCopy) {
|
||||
apiKeyCopy.addEventListener('click', () => {
|
||||
if (apiKeyDisplay.value) {
|
||||
navigator.clipboard.writeText(apiKeyDisplay.value);
|
||||
apiKeyCopy.innerHTML = '<i class="bi bi-check"></i> Copied';
|
||||
setTimeout(() => { apiKeyCopy.innerHTML = '<i class="bi bi-clipboard"></i> Copy'; }, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyRegen) {
|
||||
apiKeyRegen.addEventListener('click', async () => {
|
||||
if (!confirm('Generate a new API key? Any existing key will stop working.')) return;
|
||||
apiKeyRegen.disabled = true;
|
||||
try {
|
||||
const resp = await fetch('/api/key/regenerate', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.api_key) {
|
||||
apiKeyDisplay.value = data.api_key;
|
||||
apiKeyDisplay.type = 'text';
|
||||
apiKeyToggle.innerHTML = '<i class="bi bi-eye-slash"></i> Hide';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to regenerate API key.');
|
||||
} finally {
|
||||
apiKeyRegen.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Local Model Loading
|
||||
const connectLocalBtn = document.getElementById('connect-local-btn');
|
||||
const localModelSelect = document.getElementById('local_model');
|
||||
|
||||
@@ -29,9 +29,15 @@
|
||||
<img id="img-{{ style.slug }}" src="{{ url_for('static', filename='uploads/' + style.image_path) }}" alt="{{ style.name }}">
|
||||
<span id="no-img-{{ style.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
{% set fallback = random_gen_image('styles', style.slug) %}
|
||||
{% if fallback %}
|
||||
<img id="img-{{ style.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ style.name }}" class="fallback-cover">
|
||||
<span id="no-img-{{ style.slug }}" class="text-muted d-none">No Image</span>
|
||||
{% else %}
|
||||
<img id="img-{{ style.slug }}" src="" alt="{{ style.name }}" class="d-none">
|
||||
<span id="no-img-{{ style.slug }}" class="text-muted">No Image</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ style.name }}</h5>
|
||||
|
||||
Reference in New Issue
Block a user