feat: deduplicate image generation jobs
This commit is contained in:
parent
c6ce3e4803
commit
ebf7b8baa1
@ -23,6 +23,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
- **Server-side Provider Credentials.** Provider keys are now masked even for same-origin Settings responses, are
|
||||
preserved when masked/empty fields are saved, and model/health probes resolve configured keys inside the server.
|
||||
Browser localStorage is no longer used as a credential store.
|
||||
- **Image Job Deduplication.** Consistent portrait/reference generation accepts an `Idempotency-Key` and returns the
|
||||
existing job for repeated submissions; completed job mappings are evicted with the job cache.
|
||||
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
|
||||
rejection and oversized JSON request rejection.
|
||||
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.
|
||||
|
||||
4
TODO.md
4
TODO.md
@ -51,8 +51,8 @@ the first block before investing in architecture or polish.
|
||||
- [ ] **M — Make profile portraits and character sheets use one canonical identity.**
|
||||
Share age, hair, eyes, face, build, skin tone, marks and era; version the prompt and
|
||||
detect stale or inconsistent images.
|
||||
- [ ] **M — Add bounded duplicate-generation protection.** Ensure double-clicks and
|
||||
retried requests cannot create duplicate voice, image or casting jobs.
|
||||
- [ ] **M — Add bounded duplicate-generation protection.** Consistent portrait/reference jobs now deduplicate
|
||||
repeated requests with `Idempotency-Key`; extend the same bounded contract to voice and casting jobs.
|
||||
- [ ] **M — Improve large-table performance.** Virtualize/paginate character and voice
|
||||
tables, update only affected rows, and disable preloading on low-memory devices.
|
||||
- [ ] **M — Add explicit cache invalidation and eviction.** Cache `/api/voices` safely
|
||||
|
||||
@ -60,6 +60,7 @@ _attribution_llm_lock = threading.Lock()
|
||||
# entries are pruned an hour after completion so this can't grow unbounded.
|
||||
_consistent_image_jobs: dict[str, dict] = {}
|
||||
_consistent_image_jobs_lock = threading.Lock()
|
||||
_consistent_image_idempotency: dict[str, str] = {}
|
||||
|
||||
|
||||
def _cij_new(job_id: str, name: str) -> None:
|
||||
@ -72,6 +73,9 @@ def _cij_new(job_id: str, name: str) -> None:
|
||||
cutoff = now - 3600
|
||||
for stale_id in [j for j, v in _consistent_image_jobs.items() if v.get("done") and v.get("updated", 0) < cutoff]:
|
||||
_consistent_image_jobs.pop(stale_id, None)
|
||||
for key, mapped_id in list(_consistent_image_idempotency.items()):
|
||||
if mapped_id == stale_id:
|
||||
_consistent_image_idempotency.pop(key, None)
|
||||
|
||||
|
||||
def _cij_set(job_id: str, **fields) -> None:
|
||||
@ -3014,6 +3018,14 @@ async def character_generate_consistent_images(request: Request):
|
||||
the face consistent across both deliverables.
|
||||
"""
|
||||
data = await request.json()
|
||||
idempotency_key = str(request.headers.get("idempotency-key") or "").strip()
|
||||
if len(idempotency_key) > 200:
|
||||
raise HTTPException(400, "Idempotency-Key is too long")
|
||||
if idempotency_key:
|
||||
with _consistent_image_jobs_lock:
|
||||
previous_job_id = _consistent_image_idempotency.get(idempotency_key)
|
||||
if previous_job_id and previous_job_id in _consistent_image_jobs:
|
||||
return {"job_id": previous_job_id, "deduplicated": True}
|
||||
name = str(data.get("name") or "").strip()
|
||||
book = str(data.get("book") or "").strip()
|
||||
sheet: dict = data.get("sheet") or {}
|
||||
@ -3068,6 +3080,9 @@ async def character_generate_consistent_images(request: Request):
|
||||
# fetch that a page reload would then throw away mid-render.
|
||||
job_id = uuid.uuid4().hex
|
||||
_cij_new(job_id, name or "Character")
|
||||
if idempotency_key:
|
||||
with _consistent_image_jobs_lock:
|
||||
_consistent_image_idempotency[idempotency_key] = job_id
|
||||
|
||||
def _worker() -> None:
|
||||
try:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user