auto-save 2026-05-18 20:57 (~3)

This commit is contained in:
2026-05-18 20:57:23 +08:00
parent 58fe17c5e0
commit 32620af91d
3 changed files with 487 additions and 15 deletions

View File

@@ -1396,6 +1396,289 @@ def auth_logout(response: Response) -> dict:
return {"ok": True}
def _parse_library_metadata(raw: str) -> dict:
if not raw.strip():
return {}
try:
data = json.loads(raw)
return data if isinstance(data, dict) else {}
except Exception as e:
raise HTTPException(400, f"metadata json invalid: {e}")
async def _save_upload_to_path(upload: UploadFile, dst: Path) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)
data = await upload.read()
if not data:
raise HTTPException(400, f"{upload.filename or 'file'} is empty")
dst.write_bytes(data)
def _save_library_image(src: Path, dst: Path) -> AssetLibraryImage:
dst.parent.mkdir(parents=True, exist_ok=True)
try:
with Image.open(src) as im:
rgb = im.convert("RGB")
rgb.save(dst, "JPEG", quality=94)
except Exception:
shutil.copy2(src, dst)
width, height = _library_media_size(dst)
return AssetLibraryImage(
id=dst.stem,
view=dst.stem,
label=dst.stem,
filename=str(dst.relative_to(dst.parents[1])),
width=width,
height=height,
created_at=_now_ts(),
)
@app.get("/prompt-library", response_model=list[PromptLibraryItem])
def list_prompt_library(category: PromptLibraryCategory | None = None, q: str = "", sort: str = "") -> list[PromptLibraryItem]:
items = load_prompt_library_items()
if category:
items = [item for item in items if item.category == category]
items = _prompt_library_search(items, q)
if sort == "use_count":
items.sort(key=lambda item: (item.use_count, item.updated_at or item.created_at), reverse=True)
return items
@app.get("/prompt-library/{item_id}", response_model=PromptLibraryItem)
def get_prompt_library_item(item_id: str) -> PromptLibraryItem:
return find_prompt_library_item(item_id)
@app.post("/prompt-library", response_model=PromptLibraryItem)
def create_prompt_library_item(req: PromptLibraryWriteReq) -> PromptLibraryItem:
now = _now_ts()
name = req.name.strip()
prompt_en = _ensure_english(req.prompt_en.strip()) if req.prompt_en.strip() else ""
if not name:
raise HTTPException(400, "prompt name required")
if not prompt_en and not req.prompt_zh.strip():
raise HTTPException(400, "prompt content required")
item = PromptLibraryItem(
id=f"lib_prompt_{uuid.uuid4().hex[:12]}",
category=req.category,
name=name,
tags=_safe_tags(req.tags),
prompt_en=prompt_en or _ensure_english(req.prompt_zh.strip()),
prompt_zh=req.prompt_zh.strip(),
source_job_id=req.source_job_id.strip(),
created_at=now,
updated_at=now,
)
_write_prompt_item(item)
return item
@app.patch("/prompt-library/{item_id}", response_model=PromptLibraryItem)
def patch_prompt_library_item(item_id: str, req: PromptLibraryPatchReq) -> PromptLibraryItem:
item = find_prompt_library_item(item_id)
data = item.model_dump()
patch = req.model_dump(exclude_unset=True)
if "tags" in patch:
patch["tags"] = _safe_tags(patch["tags"])
if "name" in patch:
patch["name"] = str(patch["name"]).strip()
if "prompt_en" in patch and str(patch["prompt_en"]).strip():
patch["prompt_en"] = _ensure_english(str(patch["prompt_en"]).strip())
data.update(patch)
data["updated_at"] = _now_ts()
updated = PromptLibraryItem(**data)
if not updated.name.strip():
raise HTTPException(400, "prompt name required")
_write_prompt_item(updated)
return updated
@app.delete("/prompt-library/{item_id}")
def delete_prompt_library_item(item_id: str) -> dict:
item = find_prompt_library_item(item_id)
src = _prompt_item_file(item)
trash = LIBRARY_TRASH_DIR / "prompt_library" / f"{item.id}_{int(_now_ts())}.json"
trash.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(trash))
_write_prompt_library_index()
return {"ok": True, "id": item.id, "trashed": str(trash)}
@app.post("/prompt-library/{item_id}/use", response_model=PromptLibraryItem)
def use_prompt_library_item(item_id: str) -> PromptLibraryItem:
item = find_prompt_library_item(item_id)
item.use_count += 1
item.updated_at = _now_ts()
_write_prompt_item(item)
return item
@app.get("/asset-library/{kind}", response_model=list[AssetLibraryItem])
def list_asset_library(kind: AssetLibraryKind, q: str = "", sort: str = "") -> list[AssetLibraryItem]:
items = _asset_library_search(load_asset_library_items(kind), q)
if sort == "use_count":
items.sort(key=lambda item: (item.use_count, item.updated_at or item.created_at), reverse=True)
return items
@app.get("/asset-library/{kind}/{item_id}", response_model=AssetLibraryItem)
def get_asset_library_item(kind: AssetLibraryKind, item_id: str) -> AssetLibraryItem:
return find_asset_library_item(kind, item_id)
@app.post("/asset-library/{kind}", response_model=AssetLibraryItem)
async def create_asset_library_item(
kind: AssetLibraryKind,
metadata: str = Form("{}"),
files: list[UploadFile] = File(default=[]),
) -> AssetLibraryItem:
meta = _parse_library_metadata(metadata)
if not files:
raise HTTPException(400, "at least one file required")
now = _now_ts()
item_id = f"lib_{kind}_{uuid.uuid4().hex[:12]}"
item_dir = _asset_library_item_dir(kind, item_id)
tmp_dir = item_dir / "_tmp"
item_dir.mkdir(parents=True, exist_ok=True)
tmp_dir.mkdir(parents=True, exist_ok=True)
name = str(meta.get("name") or meta.get("name_zh") or kind).strip()
item = AssetLibraryItem(
id=item_id,
kind=kind,
name=name,
name_zh=str(meta.get("name_zh") or "").strip(),
note=str(meta.get("note") or "").strip(),
tags=_safe_tags(meta.get("tags")),
source_job_id=str(meta.get("source_job_id") or "").strip(),
is_official=bool(meta.get("is_official") or False),
prompt_brief=str(meta.get("prompt_brief") or "").strip(),
prompt_brief_zh=str(meta.get("prompt_brief_zh") or "").strip(),
subject_style=str(meta.get("subject_style") or "transparent_human") if kind == "subjects" else "transparent_human",
product_type=str(meta.get("product_type") or "").strip(),
asset_role=str(meta.get("asset_role") or "").strip(),
aspect_ratio=str(meta.get("aspect_ratio") or "").strip(),
created_at=now,
updated_at=now,
)
requested_views = [str(v).strip() for v in (meta.get("views") or []) if str(v).strip()]
saved_images: list[AssetLibraryImage] = []
for index, upload in enumerate(files):
suffix = Path(upload.filename or "").suffix.lower() or ".jpg"
tmp = tmp_dir / f"{index}{suffix}"
await _save_upload_to_path(upload, tmp)
if kind == "videos":
if suffix in {".jpg", ".jpeg", ".png", ".webp"}:
image = _save_library_image(tmp, item_dir / "poster.jpg")
image.id = "poster"
image.view = "poster"
image.label = "poster"
image.filename = "poster.jpg"
item.poster = image
else:
shutil.copy2(tmp, item_dir / "video.mp4")
item.video_url = f"/asset-library/videos/{item_id}/file/video.mp4"
continue
view = requested_views[index] if index < len(requested_views) else (Path(upload.filename or "").stem or f"view_{index + 1}")
safe_view = re.sub(r"[^a-zA-Z0-9_-]+", "_", view).strip("_") or f"view_{index + 1}"
image = _save_library_image(tmp, item_dir / "images" / f"{safe_view}.jpg")
image.id = safe_view
image.view = safe_view
image.label = view
image.filename = f"images/{safe_view}.jpg"
saved_images.append(image)
shutil.rmtree(tmp_dir, ignore_errors=True)
if kind == "subjects":
item.images = saved_images
item.views = saved_images
item.prompt_brief = _ensure_english(item.prompt_brief) if item.prompt_brief else item.note
elif kind == "products":
item.views = saved_images
item.images = saved_images
elif kind == "scenes":
item.image = saved_images[0] if saved_images else None
if item.image:
item.image.id = "image"
item.image.view = "scene"
item.image.label = "scene"
elif kind == "videos" and not item.video_url:
raise HTTPException(400, "video file required")
_hydrate_asset_library_urls(item)
_write_asset_item(item)
return item
@app.patch("/asset-library/{kind}/{item_id}", response_model=AssetLibraryItem)
def patch_asset_library_item(kind: AssetLibraryKind, item_id: str, req: AssetLibraryPatchReq) -> AssetLibraryItem:
item = find_asset_library_item(kind, item_id)
data = item.model_dump()
patch = req.model_dump(exclude_unset=True)
if "tags" in patch:
patch["tags"] = _safe_tags(patch["tags"])
data.update(patch)
data["updated_at"] = _now_ts()
updated = AssetLibraryItem(**data)
_hydrate_asset_library_urls(updated)
_write_asset_item(updated)
return updated
@app.get("/asset-library/{kind}/{item_id}/refs")
def asset_library_refs(kind: AssetLibraryKind, item_id: str) -> dict:
find_asset_library_item(kind, item_id)
return _library_ref_usage(kind, item_id)
@app.delete("/asset-library/{kind}/{item_id}")
def delete_asset_library_item(kind: AssetLibraryKind, item_id: str, force: bool = False) -> dict:
item = find_asset_library_item(kind, item_id)
refs = _library_ref_usage(kind, item_id)
if refs["count"] and not force:
raise HTTPException(409, {"message": "asset library item is referenced", **refs})
src = _asset_library_item_dir(kind, item.id)
trash = LIBRARY_TRASH_DIR / "asset_library" / kind / f"{item.id}_{int(_now_ts())}"
trash.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(trash))
_write_asset_library_index()
return {"ok": True, "id": item.id, "refs": refs, "trashed": str(trash)}
@app.post("/asset-library/{kind}/{item_id}/copy-to-job/{job_id}")
def copy_asset_library_to_job(kind: AssetLibraryKind, item_id: str, job_id: str) -> dict:
return _copy_library_to_job(kind, item_id, job_id)
@app.get("/asset-library/{kind}/{item_id}/file/{filename:path}")
def get_asset_library_file(kind: AssetLibraryKind, item_id: str, filename: str):
item = find_asset_library_item(kind, item_id)
p = _library_item_file_path(item, filename)
suffix = p.suffix.lower()
if suffix == ".mp4":
return FileResponse(p, media_type="video/mp4")
if suffix == ".png":
return FileResponse(p, media_type="image/png")
if suffix == ".webp":
return FileResponse(p, media_type="image/webp")
return FileResponse(p, media_type="image/jpeg")
@app.get("/resource-library/recent")
def resource_library_recent(hours: int = 24) -> dict:
cutoff = _now_ts() - max(1, min(hours, 24 * 30)) * 3600
prompts = [
{"type": "prompt", "category": item.category, "id": item.id, "name": item.name, "created_at": item.created_at, "item": item.model_dump()}
for item in load_prompt_library_items()
if item.created_at >= cutoff
]
assets = [
{"type": "asset", "kind": item.kind, "id": item.id, "name": item.name, "created_at": item.created_at, "item": item.model_dump()}
for item in load_asset_library_items()
if item.created_at >= cutoff
]
items = sorted(prompts + assets, key=lambda item: item.get("created_at", 0), reverse=True)
return {"items": items}
# ---------- Pipeline 实现 ----------
def _binary_works(path: str) -> bool: