auto-save 2026-05-13 21:07 (~4)
This commit is contained in:
103
api/main.py
103
api/main.py
@@ -220,12 +220,30 @@ def public_api_base() -> str:
|
||||
return (LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
|
||||
|
||||
|
||||
def video_uses_poe() -> bool:
|
||||
if VIDEO_API_BASE_URL:
|
||||
return VIDEO_API_BASE_URL.rstrip("/") == POE_API_BASE_URL.rstrip("/")
|
||||
return bool(POE_API_KEY)
|
||||
|
||||
|
||||
def video_uses_ark() -> bool:
|
||||
return "ark.cn-beijing.volces.com" in video_api_base()
|
||||
|
||||
|
||||
def video_api_base() -> str:
|
||||
return (VIDEO_API_BASE_URL or LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
|
||||
if VIDEO_API_BASE_URL:
|
||||
return VIDEO_API_BASE_URL.rstrip("/")
|
||||
if POE_API_KEY:
|
||||
return POE_API_BASE_URL.rstrip("/")
|
||||
return (LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
|
||||
|
||||
|
||||
def video_api_key() -> str:
|
||||
return VIDEO_API_KEY or LLM_API_KEY
|
||||
if VIDEO_API_KEY:
|
||||
return VIDEO_API_KEY
|
||||
if video_uses_poe():
|
||||
return POE_API_KEY
|
||||
return LLM_API_KEY
|
||||
|
||||
|
||||
def video_path(template: str, **values: str) -> str:
|
||||
@@ -235,7 +253,7 @@ def video_path(template: str, **values: str) -> str:
|
||||
|
||||
def ensure_video_api_configured() -> None:
|
||||
if not video_api_key():
|
||||
raise HTTPException(503, "VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API")
|
||||
raise HTTPException(503, "POE_API_KEY、VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API")
|
||||
|
||||
|
||||
def storyboard_ref_path(job_id: str, ref: dict | None) -> Path | None:
|
||||
@@ -805,7 +823,8 @@ def health() -> dict:
|
||||
"rewrite": REWRITE_MODEL,
|
||||
"video": VIDEO_MODEL,
|
||||
"video_aliases": VIDEO_MODEL_ALIASES,
|
||||
"video_base_url": video_api_base() if VIDEO_API_BASE_URL else "",
|
||||
"video_provider": "poe" if video_uses_poe() else ("ark" if video_uses_ark() else "custom"),
|
||||
"video_base_url": video_api_base(),
|
||||
"video_configured": bool(video_api_key()),
|
||||
"video_create_paths": VIDEO_CREATE_PATHS,
|
||||
},
|
||||
@@ -1629,6 +1648,10 @@ class GenerateStoryboardVideoReq(BaseModel):
|
||||
|
||||
|
||||
def video_seconds(duration: float) -> str:
|
||||
if video_uses_ark():
|
||||
if duration <= 0:
|
||||
return "5"
|
||||
return str(max(4, min(15, round(duration))))
|
||||
if duration <= 6:
|
||||
return "4"
|
||||
if duration <= 10:
|
||||
@@ -1683,6 +1706,12 @@ def video_url_from_response(data: dict) -> str:
|
||||
v = output.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
return v
|
||||
content = data.get("content")
|
||||
if isinstance(content, dict):
|
||||
for key in ("video_url", "url", "download_url", "file_url"):
|
||||
v = content.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
return v
|
||||
return ""
|
||||
|
||||
|
||||
@@ -1696,6 +1725,64 @@ def download_generated_video(client, base: str, headers: dict, provider_id: str,
|
||||
out_mp4.write_bytes(r.content)
|
||||
|
||||
|
||||
def size_to_video_ratio(size: str) -> str:
|
||||
try:
|
||||
w, h = [int(x) for x in size.lower().replace(" ", "").split("x", 1)]
|
||||
except Exception:
|
||||
return "9:16"
|
||||
if w <= 0 or h <= 0:
|
||||
return "9:16"
|
||||
ratio = w / h
|
||||
known = {
|
||||
"16:9": 16 / 9,
|
||||
"9:16": 9 / 16,
|
||||
"1:1": 1,
|
||||
"4:3": 4 / 3,
|
||||
"3:4": 3 / 4,
|
||||
"21:9": 21 / 9,
|
||||
}
|
||||
return min(known, key=lambda key: abs(known[key] - ratio))
|
||||
|
||||
|
||||
def ark_reference_data_url(ref_img: Path) -> str:
|
||||
mime = "image/png" if ref_img.suffix.lower() == ".png" else "image/jpeg"
|
||||
return f"data:{mime};base64,{base64.b64encode(ref_img.read_bytes()).decode('ascii')}"
|
||||
|
||||
|
||||
def submit_video_create(client, url: str, headers: dict, ref_img: Path, payload: dict):
|
||||
if video_uses_ark():
|
||||
data = {
|
||||
"model": payload["model"],
|
||||
"content": [
|
||||
{"type": "text", "text": payload["prompt"]},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": ark_reference_data_url(ref_img)},
|
||||
"role": "first_frame",
|
||||
},
|
||||
],
|
||||
"ratio": size_to_video_ratio(str(payload.get("size", ""))),
|
||||
"duration": int(float(str(payload.get(VIDEO_DURATION_FIELD, 5)))),
|
||||
"watermark": False,
|
||||
"resolution": "720p",
|
||||
}
|
||||
return client.post(url, headers={**headers, "Content-Type": "application/json"}, json=data)
|
||||
|
||||
if video_uses_poe():
|
||||
data = dict(payload)
|
||||
data[VIDEO_DURATION_FIELD] = int(float(str(data.get(VIDEO_DURATION_FIELD, 4))))
|
||||
data["input_image"] = base64.b64encode(ref_img.read_bytes()).decode("ascii")
|
||||
return client.post(url, headers=headers, json=data)
|
||||
|
||||
with ref_img.open("rb") as fh:
|
||||
return client.post(
|
||||
url,
|
||||
headers=headers,
|
||||
data=payload,
|
||||
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
|
||||
)
|
||||
|
||||
|
||||
def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_path: Path, prompt: str, model: str, seconds: str, size: str) -> None:
|
||||
import httpx
|
||||
|
||||
@@ -1714,13 +1801,7 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa
|
||||
create = None
|
||||
create_errors: list[str] = []
|
||||
for create_path in VIDEO_CREATE_PATHS:
|
||||
with ref_img.open("rb") as fh:
|
||||
resp = client.post(
|
||||
f"{base}{video_path(create_path)}",
|
||||
headers=headers,
|
||||
data=payload,
|
||||
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
|
||||
)
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload)
|
||||
if resp.status_code < 400:
|
||||
create = resp
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user