Add phase 4: AI cleanup and photo/text import via OpenAI

- recipes/ai.py: Chat Completions with strict JSON schema (Structured
  Outputs), Pydantic re-validation, plausibility clamping, prompt treats
  input as data only
- "Mit KI aufbereiten" on create/edit form returns an editable draft
- Import page: photo/screenshot (vision, downscaled JPEG, not stored) and
  pasted text; drafts pre-fill the recipe form with a preview
- Clear UI messages for timeout, rate limit, quota, auth, model and
  connection errors; without OPENAI_API_KEY AI features show a notice
- Optional per-user and global daily AI limits (default unlimited)
- Tests for response parsing, schema strictness, error mapping, views

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kyle Dammann
2026-10-30 21:43:30 +01:00
co-authored by Claude Opus 5.5
parent 9256e94286
commit 1bf8ef8b65
17 changed files with 856 additions and 13 deletions
+23
View File
@@ -0,0 +1,23 @@
<div class="msg msg-warning draft-notice">
<strong>{{ draft_notice|default:"KI-Entwurf" }} – bitte prüfen.</strong>
Noch nichts ist gespeichert. Korrigiere die Felder unten bei Bedarf und tippe dann auf „Speichern“.
</div>
{% if ai_draft %}
<details class="card preview" open>
<summary><strong>Vorschau: {{ ai_draft.title|default:"Ohne Titel" }}</strong></summary>
{% if ai_draft.ingredients %}
<h3>Zutaten</h3>
<ul class="ingredients">
{% for ing in ai_draft.ingredients %}
<li><span class="amount">{% if ing.amount is not None %}{{ ing.amount|floatformat:"-2" }}{% if ing.amount_max is not None %}–{{ ing.amount_max|floatformat:"-2" }}{% endif %}{% endif %} {{ ing.unit }}</span>
<span>{% if ing.group %}<span class="muted">{{ ing.group }}: </span>{% endif %}{{ ing.name }}{% if ing.note %}<span class="muted">, {{ ing.note }}</span>{% endif %}</span></li>
{% endfor %}
</ul>
{% endif %}
{% if ai_draft.steps %}
<h3>Zubereitung</h3>
<ol class="steps">{% for step in ai_draft.steps %}<li>{{ step }}</li>{% endfor %}</ol>
{% endif %}
{% if ai_draft.tags %}<p class="chips">{% for tag in ai_draft.tags %}<span class="chip">#{{ tag }}</span>{% endfor %}</p>{% endif %}
</details>
{% endif %}