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:
105
routes/api.py
Normal file
105
routes/api.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
from flask import request, jsonify
|
||||
from models import Preset, Settings
|
||||
from services.generation import generate_from_preset
|
||||
from services.job_queue import _job_queue_lock, _job_history
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
|
||||
def require_api_key(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
api_key = request.headers.get('X-API-Key') or request.args.get('api_key')
|
||||
if not api_key:
|
||||
return jsonify({'error': 'Missing API key. Provide X-API-Key header or api_key query parameter.'}), 401
|
||||
settings = Settings.query.first()
|
||||
if not settings or not settings.api_key or api_key != settings.api_key:
|
||||
return jsonify({'error': 'Invalid API key.'}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
@app.route('/api/v1/presets')
|
||||
@require_api_key
|
||||
def api_v1_presets():
|
||||
presets = Preset.query.order_by(Preset.name).all()
|
||||
return jsonify({
|
||||
'presets': [
|
||||
{
|
||||
'preset_id': p.preset_id,
|
||||
'slug': p.slug,
|
||||
'name': p.name,
|
||||
'has_cover': bool(p.image_path),
|
||||
}
|
||||
for p in presets
|
||||
]
|
||||
})
|
||||
|
||||
@app.route('/api/v1/generate/<path:preset_slug>', methods=['POST'])
|
||||
@require_api_key
|
||||
def api_v1_generate(preset_slug):
|
||||
preset = Preset.query.filter_by(slug=preset_slug).first()
|
||||
if not preset:
|
||||
return jsonify({'error': f'Preset not found: {preset_slug}'}), 404
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
|
||||
count = body.get('count', 1)
|
||||
try:
|
||||
count = int(count)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': 'count must be an integer'}), 400
|
||||
if count < 1 or count > 20:
|
||||
return jsonify({'error': 'count must be between 1 and 20'}), 400
|
||||
|
||||
seed = body.get('seed')
|
||||
if seed is not None:
|
||||
try:
|
||||
seed = int(seed)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': 'seed must be an integer'}), 400
|
||||
|
||||
width = body.get('width')
|
||||
height = body.get('height')
|
||||
if (width is None) != (height is None):
|
||||
return jsonify({'error': 'width and height must both be provided or both omitted'}), 400
|
||||
|
||||
overrides = {
|
||||
'action': 'preview',
|
||||
'checkpoint': body.get('checkpoint', ''),
|
||||
'extra_positive': body.get('extra_positive', ''),
|
||||
'extra_negative': body.get('extra_negative', ''),
|
||||
'seed': seed,
|
||||
'width': int(width) if width else None,
|
||||
'height': int(height) if height else None,
|
||||
}
|
||||
|
||||
try:
|
||||
jobs = []
|
||||
for _ in range(count):
|
||||
job = generate_from_preset(preset, overrides)
|
||||
jobs.append({'job_id': job['id'], 'status': 'queued'})
|
||||
return jsonify({'jobs': jobs}), 202
|
||||
except Exception as e:
|
||||
logger.exception("API generation error (preset %s): %s", preset_slug, e)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/job/<job_id>')
|
||||
@require_api_key
|
||||
def api_v1_job_status(job_id):
|
||||
with _job_queue_lock:
|
||||
job = _job_history.get(job_id)
|
||||
if not job:
|
||||
return jsonify({'error': 'Job not found'}), 404
|
||||
resp = {
|
||||
'id': job['id'],
|
||||
'label': job['label'],
|
||||
'status': job['status'],
|
||||
'error': job['error'],
|
||||
}
|
||||
if job.get('result'):
|
||||
resp['result'] = job['result']
|
||||
return jsonify(resp)
|
||||
Reference in New Issue
Block a user