auto-save 2026-05-13 09:59 (~4)
This commit is contained in:
35
api/main.py
35
api/main.py
@@ -641,6 +641,7 @@ class GenerateReq(BaseModel):
|
||||
negative_prompt: str = "" # ✗ 不需要的元素(负向)
|
||||
model: str = "" # 留空用 IMAGE_MODEL 默认
|
||||
mode: str = "edit" # "edit" 带参考图,"text" 纯文字
|
||||
from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference(迭代),否则原关键帧
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/generate", response_model=Job)
|
||||
@@ -656,6 +657,17 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
||||
if not frame_path.exists():
|
||||
raise HTTPException(404, "frame file missing")
|
||||
|
||||
# 决定 i2i 参考图:from_selected=True 且存在 selected 生成图 → 用它(迭代);否则原关键帧
|
||||
reference_path = frame_path
|
||||
reference_source = "keyframe"
|
||||
if req.from_selected:
|
||||
sel = next((g for g in frame.generated_images if g.selected), None)
|
||||
if sel:
|
||||
sel_path = job_dir(job_id) / "gen" / f"{idx:03d}_{sel.id}.jpg"
|
||||
if sel_path.exists():
|
||||
reference_path = sel_path
|
||||
reference_source = f"gen:{sel.id[:6]}"
|
||||
|
||||
full_prompt = req.prompt.strip()
|
||||
if req.extra_prompt.strip():
|
||||
full_prompt = f"{full_prompt}. Include: {req.extra_prompt.strip()}"
|
||||
@@ -673,14 +685,18 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
||||
|
||||
img_b64: str | None = None
|
||||
if req.mode == "edit":
|
||||
img_b64 = b64lib.b64encode(frame_path.read_bytes()).decode("ascii")
|
||||
img_b64 = b64lib.b64encode(reference_path.read_bytes()).decode("ascii")
|
||||
|
||||
MAX_ATTEMPTS = 3
|
||||
# 尝试 i2i 最多 3 次,全失败时降级 text-only 再试 1 次
|
||||
plan: list[str] = ([req.mode] * 3) if req.mode == "edit" else [req.mode]
|
||||
if req.mode == "edit":
|
||||
plan.append("text") # i2i 都失败时自动降级
|
||||
resp_data: dict = {}
|
||||
last_err = ""
|
||||
for attempt in range(MAX_ATTEMPTS):
|
||||
effective_mode = req.mode
|
||||
for attempt, current_mode in enumerate(plan):
|
||||
try:
|
||||
if req.mode == "edit":
|
||||
if current_mode == "edit":
|
||||
data_uri = f"data:image/jpeg;base64,{img_b64}"
|
||||
# OpenAI SDK 不直接支持 image 参数,用底层 httpx
|
||||
with httpx.Client(timeout=120) as client:
|
||||
@@ -705,6 +721,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
||||
resp_data = resp.model_dump() if hasattr(resp, "model_dump") else {"data": [{"b64_json": resp.data[0].b64_json}]}
|
||||
|
||||
if resp_data.get("data"):
|
||||
effective_mode = current_mode
|
||||
break
|
||||
err_obj = resp_data.get("error") or {}
|
||||
last_err = f"empty data · {err_obj.get('code', '')} · {str(err_obj.get('message', ''))[:200]}"
|
||||
@@ -722,13 +739,15 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
||||
except Exception as e:
|
||||
last_err = f"{type(e).__name__}: {e}"
|
||||
|
||||
if attempt < MAX_ATTEMPTS - 1:
|
||||
print(f"[image gen retry {attempt + 1}/{MAX_ATTEMPTS}] {last_err}", flush=True)
|
||||
if attempt < len(plan) - 1:
|
||||
next_mode = plan[attempt + 1]
|
||||
tag = f"fallback → {next_mode}" if next_mode != current_mode else f"retry {attempt + 1}/{len(plan)}"
|
||||
print(f"[image gen {tag}] {last_err}", flush=True)
|
||||
_time.sleep(1.5 * (attempt + 1))
|
||||
|
||||
data_arr = resp_data.get("data", [])
|
||||
if not data_arr:
|
||||
raise HTTPException(500, f"image gen failed after {MAX_ATTEMPTS} attempts: {last_err}")
|
||||
raise HTTPException(500, f"image gen failed after {len(plan)} attempts: {last_err}")
|
||||
|
||||
item = data_arr[0]
|
||||
b64 = item.get("b64_json")
|
||||
@@ -745,7 +764,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
||||
id=gen_id,
|
||||
prompt=full_prompt,
|
||||
model=model,
|
||||
mode=req.mode,
|
||||
mode=effective_mode,
|
||||
url=f"/jobs/{job_id}/frames/{idx}/gen/{gen_id}.jpg",
|
||||
selected=False,
|
||||
created_at=_time.time(),
|
||||
|
||||
Reference in New Issue
Block a user