feat: connect subject template library
This commit is contained in:
192
api/main.py
192
api/main.py
@@ -38,6 +38,10 @@ CHARACTER_LIBRARY_DIR = Path(
|
||||
os.getenv("CHARACTER_LIBRARY_DIR", Path(__file__).resolve().parent / "character_library" / "skg-characters")
|
||||
).resolve()
|
||||
CHARACTER_LIBRARY_MANIFEST = CHARACTER_LIBRARY_DIR / "manifest.json"
|
||||
SUBJECT_TEMPLATE_DIR = Path(os.getenv("SUBJECT_TEMPLATE_DIR", JOBS_DIR / "_subject_templates")).resolve()
|
||||
SUBJECT_TEMPLATE_IMAGE_DIR = SUBJECT_TEMPLATE_DIR / "images"
|
||||
SUBJECT_TEMPLATE_MANIFEST = SUBJECT_TEMPLATE_DIR / "manifest.json"
|
||||
SUBJECT_TEMPLATE_IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "").strip()
|
||||
LLM_API_KEY = os.getenv("LLM_API_KEY", "").strip()
|
||||
@@ -452,6 +456,38 @@ class CharacterLibraryItem(BaseModel):
|
||||
images: list[CharacterLibraryImage] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SubjectTemplateImage(BaseModel):
|
||||
id: str
|
||||
view: str
|
||||
label: str = ""
|
||||
filename: str
|
||||
url: str = ""
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
background: AssetBackground = "white"
|
||||
quality: AssetQuality = "hd"
|
||||
size: AssetSize = "source"
|
||||
source_asset_id: str = ""
|
||||
source_frame_indices: list[int] = Field(default_factory=list)
|
||||
created_at: float = 0.0
|
||||
|
||||
|
||||
class SubjectTemplateItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
note: str = ""
|
||||
source: Literal["database"] = "database"
|
||||
source_job_id: str = ""
|
||||
source_frame_idx: int = -1
|
||||
source_element_id: str = ""
|
||||
subject_style: Literal["transparent_human", "source_actor"] = "transparent_human"
|
||||
primary_image: str = ""
|
||||
images: list[SubjectTemplateImage] = Field(default_factory=list)
|
||||
created_at: float = 0.0
|
||||
updated_at: float = 0.0
|
||||
|
||||
|
||||
class ProductFusionRegion(BaseModel):
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
@@ -802,6 +838,50 @@ def character_library_file(filename: str) -> Path:
|
||||
return p
|
||||
|
||||
|
||||
def load_subject_template_items() -> list[SubjectTemplateItem]:
|
||||
if not SUBJECT_TEMPLATE_MANIFEST.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(SUBJECT_TEMPLATE_MANIFEST.read_text(encoding="utf-8"))
|
||||
items: list[SubjectTemplateItem] = []
|
||||
for raw in data.get("templates", []):
|
||||
item = SubjectTemplateItem(**raw)
|
||||
for image in item.images:
|
||||
image.url = f"/subject-templates/images/{image.filename}"
|
||||
items.append(item)
|
||||
items.sort(key=lambda item: item.updated_at or item.created_at, reverse=True)
|
||||
return items
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"subject template manifest invalid: {e}")
|
||||
|
||||
|
||||
def save_subject_template_items(items: list[SubjectTemplateItem]) -> None:
|
||||
SUBJECT_TEMPLATE_MANIFEST.parent.mkdir(parents=True, exist_ok=True)
|
||||
SUBJECT_TEMPLATE_MANIFEST.write_text(
|
||||
json.dumps({"templates": [item.model_dump() for item in items]}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def find_subject_template_item(template_id: str) -> SubjectTemplateItem:
|
||||
template_id = template_id.strip()
|
||||
for item in load_subject_template_items():
|
||||
if item.id == template_id:
|
||||
return item
|
||||
raise HTTPException(404, "subject template not found")
|
||||
|
||||
|
||||
def subject_template_image_file(filename: str) -> Path:
|
||||
p = (SUBJECT_TEMPLATE_IMAGE_DIR / filename).resolve()
|
||||
try:
|
||||
p.relative_to(SUBJECT_TEMPLATE_IMAGE_DIR)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "invalid subject template image path")
|
||||
if not p.exists():
|
||||
raise HTTPException(404, "subject template image missing")
|
||||
return p
|
||||
|
||||
|
||||
def storyboard_ref_url(job_id: str, ref: dict | None) -> str:
|
||||
if not ref:
|
||||
return ""
|
||||
@@ -3783,6 +3863,7 @@ class GenerateSubjectAssetsReq(BaseModel):
|
||||
source_frame_indices: list[int] | None = None
|
||||
views: list[str] | None = None
|
||||
character_id: str = ""
|
||||
subject_template_id: str = ""
|
||||
subject_style: Literal["transparent_human", "source_actor"] = "transparent_human"
|
||||
reconstruction_mode: Literal["same", "similar"] = "same"
|
||||
prompt: str = ""
|
||||
@@ -4230,8 +4311,21 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
|
||||
character_reference_paths: list[Path] = []
|
||||
character_reference_clause = ""
|
||||
character_label = ""
|
||||
subject_template_id = (req.subject_template_id or "").strip()
|
||||
character_id = (req.character_id or "").strip()
|
||||
if character_id:
|
||||
if subject_template_id:
|
||||
template = find_subject_template_item(subject_template_id)
|
||||
character_label = template.name
|
||||
for image in template.images[:10]:
|
||||
character_reference_paths.append(subject_template_image_file(image.filename))
|
||||
character_reference_clause = (
|
||||
f"Selected reusable subject template from database: {template.name}. "
|
||||
"Use these saved generated subject views as a high-quality creative direction and identity bible only; "
|
||||
"do not copy pixels, file artifacts, exact pose, labels, or accidental defects. "
|
||||
"Create a new innovative variation that keeps the same broad subject type, transparent wellness character language, "
|
||||
"camera readability, shoulder/neck product compatibility, and commercial role. "
|
||||
)
|
||||
elif character_id:
|
||||
character = find_character_library_item(character_id)
|
||||
character_label = character.name
|
||||
for image in character.images[:7]:
|
||||
@@ -4854,6 +4948,15 @@ class AnalyzeProductViewsReq(BaseModel):
|
||||
refs: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SaveSubjectTemplateReq(BaseModel):
|
||||
name: str
|
||||
note: str = ""
|
||||
frame_idx: int
|
||||
element_id: str
|
||||
asset_ids: list[str] = Field(default_factory=list)
|
||||
subject_style: Literal["transparent_human", "source_actor"] = "transparent_human"
|
||||
|
||||
|
||||
@app.get("/product-library/skg", response_model=list[ProductLibraryItem])
|
||||
def list_skg_product_library() -> list[ProductLibraryItem]:
|
||||
"""内置 SKG 白底产品图库。来源是本地筛选后的产品图 manifest。"""
|
||||
@@ -4882,6 +4985,93 @@ def get_skg_character_library_image(filename: str):
|
||||
return FileResponse(p, media_type=media_type)
|
||||
|
||||
|
||||
@app.get("/subject-templates", response_model=list[SubjectTemplateItem])
|
||||
def list_subject_templates() -> list[SubjectTemplateItem]:
|
||||
"""数据库化主体模板库。保存后的相似主体可被后续任务复用为创意参考。"""
|
||||
return load_subject_template_items()
|
||||
|
||||
|
||||
@app.get("/subject-templates/images/{filename:path}")
|
||||
def get_subject_template_image(filename: str):
|
||||
p = subject_template_image_file(filename)
|
||||
return FileResponse(p, media_type="image/jpeg")
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/subject-templates", response_model=SubjectTemplateItem)
|
||||
def save_subject_template(job_id: str, req: SaveSubjectTemplateReq) -> SubjectTemplateItem:
|
||||
"""把当前 job 里已确认的相似主体视图复制到主体模板库。"""
|
||||
import time as _time
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "job not found")
|
||||
name = req.name.strip()
|
||||
if not name:
|
||||
raise HTTPException(400, "template name required")
|
||||
frame = _find_frame(job, req.frame_idx)
|
||||
element = next((e for e in frame.elements if e.id == req.element_id), None)
|
||||
if not element:
|
||||
raise HTTPException(404, "element not found")
|
||||
|
||||
requested_ids = [x.strip() for x in req.asset_ids if x.strip()]
|
||||
selected_assets = [asset for asset in (element.subject_assets or []) if not requested_ids or asset.id in requested_ids]
|
||||
if requested_ids:
|
||||
selected_assets.sort(key=lambda asset: requested_ids.index(asset.id) if asset.id in requested_ids else 999)
|
||||
else:
|
||||
selected_assets.sort(key=lambda asset: asset.created_at, reverse=True)
|
||||
if not selected_assets:
|
||||
raise HTTPException(400, "no subject assets to save")
|
||||
|
||||
template_id = f"subject-template-{uuid.uuid4().hex[:10]}"
|
||||
template_dir = SUBJECT_TEMPLATE_IMAGE_DIR / template_id
|
||||
template_dir.mkdir(parents=True, exist_ok=True)
|
||||
now = _time.time()
|
||||
images: list[SubjectTemplateImage] = []
|
||||
for asset in selected_assets:
|
||||
src = job_dir(job_id) / "assets" / f"{asset.id}.jpg"
|
||||
if not src.exists():
|
||||
continue
|
||||
image_id = f"{asset.view}_{uuid.uuid4().hex[:8]}"
|
||||
filename = f"{template_id}/{image_id}.jpg"
|
||||
dst = SUBJECT_TEMPLATE_IMAGE_DIR / filename
|
||||
shutil.copy2(src, dst)
|
||||
images.append(SubjectTemplateImage(
|
||||
id=image_id,
|
||||
view=asset.view,
|
||||
label=asset.label or asset.view,
|
||||
filename=filename,
|
||||
url=f"/subject-templates/images/{filename}",
|
||||
width=asset.width,
|
||||
height=asset.height,
|
||||
background=asset.background,
|
||||
quality=asset.quality,
|
||||
size=asset.size,
|
||||
source_asset_id=asset.id,
|
||||
source_frame_indices=asset.source_frame_indices,
|
||||
created_at=asset.created_at or now,
|
||||
))
|
||||
if not images:
|
||||
raise HTTPException(404, "subject asset files missing")
|
||||
|
||||
primary = next((image.id for image in images if image.view == "front"), images[0].id)
|
||||
item = SubjectTemplateItem(
|
||||
id=template_id,
|
||||
name=name,
|
||||
description=req.note.strip(),
|
||||
note=req.note.strip(),
|
||||
source_job_id=job_id,
|
||||
source_frame_idx=frame.index,
|
||||
source_element_id=element.id,
|
||||
subject_style=req.subject_style,
|
||||
primary_image=primary,
|
||||
images=images,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
items = [item] + [existing for existing in load_subject_template_items() if existing.id != item.id]
|
||||
save_subject_template_items(items)
|
||||
return item
|
||||
|
||||
|
||||
def normalize_product_asset_image(src: Path, out: Path) -> dict:
|
||||
original_bytes = src.stat().st_size if src.exists() else 0
|
||||
actions: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user