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>
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
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)
|