auto-save 2026-05-18 20:57 (~3)
This commit is contained in:
@@ -1,19 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "0abb1a2",
|
|
||||||
"message": "auto-save 2026-05-16 15:26 (~1)",
|
|
||||||
"ts": "2026-05-16T15:26:26+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "11506f6",
|
|
||||||
"message": "auto-save 2026-05-16 15:32 (~1)",
|
|
||||||
"ts": "2026-05-16T15:32:12+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 1,
|
"files_changed": 1,
|
||||||
"hash": "59687a5",
|
"hash": "59687a5",
|
||||||
@@ -3214,6 +3200,19 @@
|
|||||||
"type": "session-heartbeat",
|
"type": "session-heartbeat",
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:docs: document skg brand workbench theme",
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:docs: document skg brand workbench theme",
|
||||||
"files_changed": 1
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-18T20:51:56+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-18 20:51 (~2)",
|
||||||
|
"hash": "58fe17c",
|
||||||
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-18T12:52:20Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-18 20:51 (~2)",
|
||||||
|
"files_changed": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
283
api/main.py
283
api/main.py
@@ -1396,6 +1396,289 @@ def auth_logout(response: Response) -> dict:
|
|||||||
return {"ok": True}
|
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 实现 ----------
|
# ---------- Pipeline 实现 ----------
|
||||||
|
|
||||||
def _binary_works(path: str) -> bool:
|
def _binary_works(path: str) -> bool:
|
||||||
|
|||||||
192
web/lib/api.ts
192
web/lib/api.ts
@@ -64,7 +64,7 @@ export interface KeyElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageRef {
|
export interface ImageRef {
|
||||||
kind: "keyframe" | "cutout" | "asset"
|
kind: "keyframe" | "cutout" | "asset" | "library_subject" | "library_product" | "library_scene"
|
||||||
frame_idx: number
|
frame_idx: number
|
||||||
element_id?: string | null
|
element_id?: string | null
|
||||||
cutout_id?: string | null
|
cutout_id?: string | null
|
||||||
@@ -87,6 +87,69 @@ export interface ImageRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PromptLibraryCategory = "scene_desc" | "video_desc" | "subject_desc" | "skg_script" | "product_angle"
|
||||||
|
export type AssetLibraryKind = "subjects" | "products" | "scenes" | "videos"
|
||||||
|
|
||||||
|
export interface PromptLibraryItem {
|
||||||
|
id: string
|
||||||
|
category: PromptLibraryCategory
|
||||||
|
name: string
|
||||||
|
tags: string[]
|
||||||
|
prompt_en: string
|
||||||
|
prompt_zh: string
|
||||||
|
use_count: number
|
||||||
|
source_job_id: string
|
||||||
|
created_at: number
|
||||||
|
updated_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetLibraryImage {
|
||||||
|
id: string
|
||||||
|
view: string
|
||||||
|
label: string
|
||||||
|
filename: string
|
||||||
|
url: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
created_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetLibraryItem {
|
||||||
|
id: string
|
||||||
|
kind: AssetLibraryKind
|
||||||
|
name: string
|
||||||
|
name_zh?: string
|
||||||
|
note?: string
|
||||||
|
tags: string[]
|
||||||
|
source_job_id?: string
|
||||||
|
use_count: number
|
||||||
|
created_at: number
|
||||||
|
updated_at: number
|
||||||
|
is_official?: boolean
|
||||||
|
prompt_brief?: string
|
||||||
|
prompt_brief_zh?: string
|
||||||
|
subject_style?: "transparent_human" | "source_actor"
|
||||||
|
product_type?: string
|
||||||
|
views?: AssetLibraryImage[]
|
||||||
|
images?: AssetLibraryImage[]
|
||||||
|
asset_role?: string
|
||||||
|
aspect_ratio?: string
|
||||||
|
image?: AssetLibraryImage | null
|
||||||
|
duration?: number
|
||||||
|
poster?: AssetLibraryImage | null
|
||||||
|
video_url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceLibraryRecentItem {
|
||||||
|
type: "prompt" | "asset"
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
category?: PromptLibraryCategory
|
||||||
|
kind?: AssetLibraryKind
|
||||||
|
created_at: number
|
||||||
|
item: PromptLibraryItem | AssetLibraryItem
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProductFusionRegion {
|
export interface ProductFusionRegion {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
@@ -209,6 +272,9 @@ export function resolveImageRefUrl(jobId: string, ref: ImageRef): string {
|
|||||||
if (ref.kind === "asset" && ref.element_id) {
|
if (ref.kind === "asset" && ref.element_id) {
|
||||||
return `${API_BASE}/jobs/${jobId}/assets/${ref.element_id}.jpg`
|
return `${API_BASE}/jobs/${jobId}/assets/${ref.element_id}.jpg`
|
||||||
}
|
}
|
||||||
|
if (ref.kind === "library_subject" || ref.kind === "library_product" || ref.kind === "library_scene") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
if (ref.element_id && ref.cutout_id) {
|
if (ref.element_id && ref.cutout_id) {
|
||||||
if (ref.cutout_id === ref.element_id) {
|
if (ref.cutout_id === ref.element_id) {
|
||||||
// legacy v1
|
// legacy v1
|
||||||
@@ -402,6 +468,130 @@ export async function saveSubjectTemplate(
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assetLibraryFileUrl(kind: AssetLibraryKind, itemId: string, filename: string): string {
|
||||||
|
return `${API_BASE}/asset-library/${kind}/${itemId}/file/${filename}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPromptLibrary(category?: PromptLibraryCategory, q = ""): Promise<PromptLibraryItem[]> {
|
||||||
|
const qs = new URLSearchParams()
|
||||||
|
if (category) qs.set("category", category)
|
||||||
|
if (q.trim()) qs.set("q", q.trim())
|
||||||
|
const res = await fetch(`${API_BASE}/prompt-library${qs.toString() ? `?${qs}` : ""}`, { cache: "no-store" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`listPromptLibrary ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPromptLibraryItem(body: {
|
||||||
|
category: PromptLibraryCategory
|
||||||
|
name: string
|
||||||
|
tags?: string[]
|
||||||
|
prompt_en: string
|
||||||
|
prompt_zh?: string
|
||||||
|
source_job_id?: string
|
||||||
|
}): Promise<PromptLibraryItem> {
|
||||||
|
const res = await fetch(`${API_BASE}/prompt-library`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
category: body.category,
|
||||||
|
name: body.name,
|
||||||
|
tags: body.tags ?? [],
|
||||||
|
prompt_en: body.prompt_en,
|
||||||
|
prompt_zh: body.prompt_zh ?? "",
|
||||||
|
source_job_id: body.source_job_id ?? "",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`createPromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usePromptLibraryItem(id: string): Promise<PromptLibraryItem> {
|
||||||
|
const res = await fetch(`${API_BASE}/prompt-library/${id}/use`, { method: "POST" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`usePromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePromptLibraryItem(id: string): Promise<{ ok: boolean }> {
|
||||||
|
const res = await fetch(`${API_BASE}/prompt-library/${id}`, { method: "DELETE" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`deletePromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAssetLibrary(kind: AssetLibraryKind, q = ""): Promise<AssetLibraryItem[]> {
|
||||||
|
const qs = new URLSearchParams()
|
||||||
|
if (q.trim()) qs.set("q", q.trim())
|
||||||
|
const res = await fetch(`${API_BASE}/asset-library/${kind}${qs.toString() ? `?${qs}` : ""}`, { cache: "no-store" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`listAssetLibrary ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssetLibraryItem(
|
||||||
|
kind: AssetLibraryKind,
|
||||||
|
metadata: Record<string, unknown>,
|
||||||
|
files: File[],
|
||||||
|
): Promise<AssetLibraryItem> {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("metadata", JSON.stringify(metadata))
|
||||||
|
for (const file of files) fd.append("files", file)
|
||||||
|
const res = await fetch(`${API_BASE}/asset-library/${kind}`, { method: "POST", body: fd })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`createAssetLibraryItem ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssetLibraryRefs(kind: AssetLibraryKind, id: string): Promise<{ count: number; jobs: string[] }> {
|
||||||
|
const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}/refs`, { cache: "no-store" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`getAssetLibraryRefs ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAssetLibraryItem(kind: AssetLibraryKind, id: string, force = false): Promise<{ ok: boolean }> {
|
||||||
|
const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}${force ? "?force=true" : ""}`, { method: "DELETE" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`deleteAssetLibraryItem ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyAssetLibraryToJob(kind: AssetLibraryKind, id: string, jobId: string): Promise<ImageRef | { kind: "video"; video_id: string; url: string; label?: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}/copy-to-job/${jobId}`, { method: "POST" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`copyAssetLibraryToJob ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResourceLibraryRecent(hours = 24): Promise<{ items: ResourceLibraryRecentItem[] }> {
|
||||||
|
const res = await fetch(`${API_BASE}/resource-library/recent?hours=${encodeURIComponent(String(hours))}`, { cache: "no-store" })
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`getResourceLibraryRecent ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
export async function copyCharacterLibraryAssets(jobId: string, characterId: string): Promise<{ character_id: string; character_name: string; images: ImageRef[] }> {
|
export async function copyCharacterLibraryAssets(jobId: string, characterId: string): Promise<{ character_id: string; character_name: string; images: ImageRef[] }> {
|
||||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/character-library`, {
|
const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/character-library`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user