auto-save 2026-05-17 19:48 (~4)
This commit is contained in:
@@ -1,18 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 2,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-15 11:40 (~1)",
|
||||
"ts": "2026-05-15T03:44:44Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 2,
|
||||
"hash": "23b1def",
|
||||
"message": "auto-save 2026-05-15 11:45 (~2)",
|
||||
"ts": "2026-05-15T11:46:03+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 3,
|
||||
"hash": "de3cef4",
|
||||
@@ -3264,6 +3251,19 @@
|
||||
"message": "auto-save 2026-05-17 19:32 (~4)",
|
||||
"hash": "96c998c",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T19:37:40+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-17 19:37 (~4)",
|
||||
"hash": "5c6a16d",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T11:38:28Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 19:37 (~4)",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
72
api/main.py
72
api/main.py
@@ -51,6 +51,10 @@ TRANSLATE_MODEL = os.getenv("TRANSLATE_MODEL", "gemini-2.5-flash")
|
||||
REWRITE_MODEL = os.getenv("REWRITE_MODEL", "gemini-2.5-pro")
|
||||
VISION_MODEL = os.getenv("VISION_MODEL", "gemini-2.5-flash")
|
||||
IMAGE_MODEL = os.getenv("IMAGE_MODEL", "gemini-3-pro-image-preview")
|
||||
PRODUCT_ASSET_MAX_SIDE = max(1024, int(os.getenv("PRODUCT_ASSET_MAX_SIDE", "1600")))
|
||||
PRODUCT_ASSET_MIN_LONG_SIDE = max(512, int(os.getenv("PRODUCT_ASSET_MIN_LONG_SIDE", "900")))
|
||||
PRODUCT_ASSET_MIN_SHORT_SIDE = max(320, int(os.getenv("PRODUCT_ASSET_MIN_SHORT_SIDE", "600")))
|
||||
PRODUCT_ASSET_JPEG_QUALITY = max(80, min(95, int(os.getenv("PRODUCT_ASSET_JPEG_QUALITY", "92"))))
|
||||
VIDEO_MODEL = os.getenv("VIDEO_MODEL", "seedance").strip() or "seedance"
|
||||
AUDIO_PRODUCT_BRIEF = os.getenv(
|
||||
"AUDIO_PRODUCT_BRIEF",
|
||||
@@ -4234,6 +4238,64 @@ def get_skg_character_library_image(filename: str):
|
||||
return FileResponse(p, media_type=media_type)
|
||||
|
||||
|
||||
def normalize_product_asset_image(src: Path, out: Path) -> dict:
|
||||
original_bytes = src.stat().st_size if src.exists() else 0
|
||||
actions: list[str] = []
|
||||
warnings: list[str] = []
|
||||
with Image.open(src) as opened:
|
||||
img = ImageOps.exif_transpose(opened)
|
||||
original_width, original_height = img.size
|
||||
if img.mode in {"RGBA", "LA"} or ("transparency" in img.info):
|
||||
rgba = img.convert("RGBA")
|
||||
base = Image.new("RGB", img.size, (255, 255, 255))
|
||||
base.paste(rgba, mask=rgba.getchannel("A"))
|
||||
img = base
|
||||
actions.append("透明背景已铺白")
|
||||
elif img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
actions.append("已转 RGB/JPEG")
|
||||
|
||||
max_side = max(img.size)
|
||||
if max_side > PRODUCT_ASSET_MAX_SIDE:
|
||||
ratio = PRODUCT_ASSET_MAX_SIDE / max_side
|
||||
next_size = (max(1, round(img.width * ratio)), max(1, round(img.height * ratio)))
|
||||
img = img.resize(next_size, Image.Resampling.LANCZOS)
|
||||
actions.append(f"最长边压缩到 {PRODUCT_ASSET_MAX_SIDE}px")
|
||||
if max(original_width, original_height) >= 2400:
|
||||
warnings.append("原图过大已自动压缩;超高清不会提升识别稳定性")
|
||||
elif max_side < PRODUCT_ASSET_MIN_LONG_SIDE:
|
||||
ratio = PRODUCT_ASSET_MIN_LONG_SIDE / max_side
|
||||
next_size = (max(1, round(img.width * ratio)), max(1, round(img.height * ratio)))
|
||||
img = img.resize(next_size, Image.Resampling.LANCZOS)
|
||||
actions.append(f"低分辨率图已放大到最长边 {PRODUCT_ASSET_MIN_LONG_SIDE}px")
|
||||
warnings.append("原始分辨率偏低,已放大为工作图,但真实细节不会增加")
|
||||
|
||||
if min(img.size) < PRODUCT_ASSET_MIN_SHORT_SIDE:
|
||||
warnings.append(f"短边低于 {PRODUCT_ASSET_MIN_SHORT_SIDE}px,细节/比例识别可能不稳")
|
||||
if original_bytes >= 5 * 1024 * 1024:
|
||||
warnings.append("原文件较大,已生成轻量 AI 工作副本")
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
img.save(out, "JPEG", quality=PRODUCT_ASSET_JPEG_QUALITY, optimize=True, progressive=True, subsampling=0)
|
||||
|
||||
return {
|
||||
"standard": f"AI工作副本:最长边≤{PRODUCT_ASSET_MAX_SIDE}px,建议长边≥{PRODUCT_ASSET_MIN_LONG_SIDE}px,短边≥{PRODUCT_ASSET_MIN_SHORT_SIDE}px,JPEG q{PRODUCT_ASSET_JPEG_QUALITY}",
|
||||
"original_width": original_width,
|
||||
"original_height": original_height,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"original_bytes": original_bytes,
|
||||
"work_bytes": out.stat().st_size if out.exists() else 0,
|
||||
"max_side": PRODUCT_ASSET_MAX_SIDE,
|
||||
"min_long_side": PRODUCT_ASSET_MIN_LONG_SIDE,
|
||||
"min_short_side": PRODUCT_ASSET_MIN_SHORT_SIDE,
|
||||
"quality": PRODUCT_ASSET_JPEG_QUALITY,
|
||||
"actions": actions,
|
||||
"warnings": warnings,
|
||||
"normalized": bool(actions or warnings),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/assets")
|
||||
async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) -> dict:
|
||||
if job_id not in JOBS:
|
||||
@@ -4245,9 +4307,7 @@ async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) ->
|
||||
out = out_dir / f"{asset_id}.jpg"
|
||||
try:
|
||||
tmp.write_bytes(await file.read())
|
||||
img = Image.open(tmp).convert("RGB")
|
||||
img.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
|
||||
img.save(out, "JPEG", quality=94)
|
||||
asset_meta = normalize_product_asset_image(tmp, out)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"product image upload failed: {e}")
|
||||
finally:
|
||||
@@ -4261,6 +4321,7 @@ async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) ->
|
||||
"element_id": asset_id,
|
||||
"cutout_id": asset_id,
|
||||
"label": file.filename or "SKG 产品图",
|
||||
"asset_meta": asset_meta,
|
||||
}
|
||||
|
||||
|
||||
@@ -4482,9 +4543,7 @@ def copy_product_library_asset(job_id: str, req: CopyProductLibraryAssetReq) ->
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = out_dir / f"{asset_id}.jpg"
|
||||
try:
|
||||
img = Image.open(src).convert("RGB")
|
||||
img.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
|
||||
img.save(out, "JPEG", quality=94)
|
||||
asset_meta = normalize_product_asset_image(src, out)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"product library copy failed: {e}")
|
||||
label = f"产品融合 · {item.title} #{item.image_index}"
|
||||
@@ -4494,6 +4553,7 @@ def copy_product_library_asset(job_id: str, req: CopyProductLibraryAssetReq) ->
|
||||
"element_id": asset_id,
|
||||
"cutout_id": asset_id,
|
||||
"label": label,
|
||||
"asset_meta": asset_meta,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ type ProductRefItem = {
|
||||
note: string
|
||||
risk: string
|
||||
source: "upload" | "ai"
|
||||
assetMeta?: ImageRef["asset_meta"]
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
@@ -356,6 +357,11 @@ function productBackgroundLabel(background: string) {
|
||||
return PRODUCT_BACKGROUND_LABELS[background] ?? PRODUCT_BACKGROUND_LABELS.unknown
|
||||
}
|
||||
|
||||
function formatProductAssetSize(meta?: ImageRef["asset_meta"]) {
|
||||
if (!meta?.width || !meta?.height) return "AI工作图"
|
||||
return `${meta.width}x${meta.height}`
|
||||
}
|
||||
|
||||
function defaultProductUseTags(view: string) {
|
||||
const defaults: Record<string, string[]> = {
|
||||
front: ["hero_packshot", "asymmetry"],
|
||||
@@ -398,6 +404,7 @@ function createProductRefItem(
|
||||
note: note ?? targetSlot.hint,
|
||||
risk,
|
||||
source,
|
||||
assetMeta: ref.asset_meta,
|
||||
confidence,
|
||||
}
|
||||
}
|
||||
@@ -1343,7 +1350,7 @@ function AudioStoryboardPlanPanel({
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 max-w-[760px] text-[11px] leading-snug text-white/42">
|
||||
上传的图默认属于同一个产品;系统只标注背景、视角、用途和生成风险。每条视频生成时自动挑选最多 {MAX_PRODUCT_REFS_PER_VIDEO} 张相关产品图,避免把所有素材都塞给模型。
|
||||
上传原图不限尺寸,但系统会自动生成轻量 AI 工作副本:最长边 1600px、JPEG 92。超高清原图不会更稳;低分辨率会自动放大并标注风险。每条视频只挑最多 {MAX_PRODUCT_REFS_PER_VIDEO} 张相关产品图。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
@@ -1506,6 +1513,8 @@ function ProductReferenceCard({
|
||||
}) {
|
||||
const src = resolveImageRefUrl(job.id, item.ref)
|
||||
const tagLabels = item.useTags.map((tag) => PRODUCT_USE_TAG_LABELS[tag]).filter(Boolean)
|
||||
const assetWarnings = item.assetMeta?.warnings ?? []
|
||||
const assetActions = item.assetMeta?.actions ?? []
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-[74px_minmax(0,1fr)_28px] gap-2 rounded-md border border-white/10 bg-black/26 p-2">
|
||||
<div className="group relative h-[74px] w-[74px] overflow-visible rounded-md border border-white/10 bg-white">
|
||||
@@ -1517,6 +1526,7 @@ function ProductReferenceCard({
|
||||
<br />
|
||||
{item.note}
|
||||
{item.risk ? <><br />风险:{item.risk}</> : null}
|
||||
{assetWarnings.length ? <><br />规格:{assetWarnings.join(";")}</> : null}
|
||||
</div>
|
||||
</div>
|
||||
<span className="absolute left-1 top-1 rounded bg-black/70 px-1 text-[9px] text-white/75">{item.source === "ai" ? "AI" : "图"}</span>
|
||||
@@ -1529,11 +1539,13 @@ function ProductReferenceCard({
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex min-h-5 flex-wrap gap-1 overflow-hidden">
|
||||
<span className="rounded border border-white/10 bg-white/[0.045] px-1.5 py-0.5 text-[9.5px] leading-none text-white/44">{formatProductAssetSize(item.assetMeta)}</span>
|
||||
<span className="rounded border border-white/10 bg-white/[0.045] px-1.5 py-0.5 text-[9.5px] leading-none text-white/44">{productBackgroundLabel(item.background)}</span>
|
||||
{tagLabels.slice(0, 3).map((tag) => (
|
||||
<span key={tag} className="rounded border border-cyan-300/14 bg-cyan-300/[0.07] px-1.5 py-0.5 text-[9.5px] leading-none text-cyan-100/58">{tag}</span>
|
||||
))}
|
||||
{item.risk ? <span className="rounded border border-amber-300/18 bg-amber-300/[0.08] px-1.5 py-0.5 text-[9.5px] leading-none text-amber-100/68">有风险</span> : null}
|
||||
{item.risk || assetWarnings.length ? <span className="rounded border border-amber-300/18 bg-amber-300/[0.08] px-1.5 py-0.5 text-[9.5px] leading-none text-amber-100/68">需留意</span> : null}
|
||||
{assetActions.length ? <span className="rounded border border-emerald-300/14 bg-emerald-300/[0.07] px-1.5 py-0.5 text-[9.5px] leading-none text-emerald-100/58">已标准化</span> : null}
|
||||
</div>
|
||||
<input
|
||||
value={item.note}
|
||||
|
||||
@@ -56,6 +56,22 @@ export interface ImageRef {
|
||||
element_id?: string | null
|
||||
cutout_id?: string | null
|
||||
label?: string
|
||||
asset_meta?: {
|
||||
standard?: string
|
||||
original_width?: number
|
||||
original_height?: number
|
||||
width?: number
|
||||
height?: number
|
||||
original_bytes?: number
|
||||
work_bytes?: number
|
||||
max_side?: number
|
||||
min_long_side?: number
|
||||
min_short_side?: number
|
||||
quality?: number
|
||||
actions?: string[]
|
||||
warnings?: string[]
|
||||
normalized?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProductFusionRegion {
|
||||
|
||||
Reference in New Issue
Block a user