auto-save 2026-05-17 19:48 (~4)

This commit is contained in:
2026-05-17 19:48:24 +08:00
parent 5c6a16ddd8
commit 9cfb633365
4 changed files with 109 additions and 21 deletions

View File

@@ -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
}
]
}

View File

@@ -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}pxJPEG 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,
}

View File

@@ -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 1600pxJPEG 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}

View File

@@ -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 {