3 Commits

Author SHA1 Message Date
591bc37990 docs: record video error deployment 2026-05-26 09:43:02 +08:00
579e538aa7 fix: explain video generation failures 2026-05-26 09:41:03 +08:00
836a33e85b auto-save 2026-05-26 09:38 (~2) 2026-05-26 09:39:00 +08:00
4 changed files with 145 additions and 32 deletions

View File

@@ -1,30 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"message": "Claude 会话结束 · 持续 0 秒 · 最近命令claude · 分支 main · 1 项未提交变更 · 最近提交auto-save 2026-05-20 14:23 (+1, ~1)",
"ts": "2026-05-20T06:37:09Z",
"type": "session-end"
},
{
"files_changed": 1,
"message": "Claude 会话结束 · 持续 0 秒 · 最近命令claude · 分支 main · 1 项未提交变更 · 最近提交auto-save 2026-05-20 14:23 (+1, ~1)",
"ts": "2026-05-20T06:37:09Z",
"type": "session-end"
},
{
"files_changed": 2,
"hash": "16f78ba",
"message": "auto-save 2026-05-20 14:39 (+1, ~1)",
"ts": "2026-05-20T14:39:42+08:00",
"type": "commit"
},
{
"files_changed": 2,
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 2 项未提交变更 · 最近提交auto-save 2026-05-20 14:39 (+1, ~1)",
"ts": "2026-05-20T06:43:58Z",
"type": "session-heartbeat"
},
{
"files_changed": 3,
"hash": "d6bba9d",
@@ -3206,6 +3181,31 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交chore: disable password login in production",
"files_changed": 1
},
{
"ts": "2026-05-26T09:08:30+08:00",
"type": "commit",
"message": "chore: migrate legacy password data to Feishu owner",
"hash": "e0330bf",
"files_changed": 3
},
{
"ts": "2026-05-26T01:09:34Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交chore: migrate legacy password data to Feishu owner",
"files_changed": 1
},
{
"ts": "2026-05-26T01:19:34Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交chore: migrate legacy password data to Feishu owner",
"files_changed": 1
},
{
"ts": "2026-05-26T01:29:34Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交chore: migrate legacy password data to Feishu owner",
"files_changed": 1
}
]
}

View File

@@ -23,6 +23,7 @@
- 最近部署验证2026-05-26`c9d8fa7` 对应 Postgres 持久化代码已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`。生产新增 `skg-marketing-postgres` 容器,数据库持久化在服务器 `./data/postgres``DATABASE_URL` / `POSTGRES_PASSWORD` 只写服务器 `deploy/.env.production`。部署前脚本备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525225145.tgz`;生产 Docker 重建后脚本内验证通过web/API/Postgres 容器 Up、Postgres healthy、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok db connected``api:ytdlp_cookie_args []`)。文档/元数据同步后又执行 `./scripts/deploy-prod-safe.sh --no-build`,实际走过 Postgres `pg_dump` 备份路径并生成 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525230444.tgz`,复验同样通过。补验:容器内 `/health` 返回 `database.enabled=true``database.connected=true``/api/auth/config` 返回 `feishu_enabled=true``password_enabled=true``data_isolation_enabled=true`;画布项目 API 可创建、读取、软删除记录;数据库索引计数为 users=1、jobs=26、assets=129、canvas_active=0、canvas_deleted=1、audit=2。
- 生产登录收口2026-05-26已在服务器 `/opt/skg-marketing-studio/deploy/.env.production` 设置 `PASSWORD_AUTH_ENABLED=false` 并通过 `./scripts/deploy-prod-safe.sh` 重建生产。部署前脚本备份到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526003816.tgz`;脚本内首次验证在容器启动 3 秒时遇到根路径临时 500随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`)。公网复验:`/api/auth/config` 返回 `password_enabled=false``feishu_enabled=true``data_isolation_enabled=true``GET /api/auth/feishu/start?next=/` 返回 302 到飞书授权页;`POST /api/auth/login` 返回 503 `账号密码登录未配置`
- 旧密码账号归属迁移2026-05-26已把旧共享密码账号 `password:skg` 下的 22 个 job、3 个画布项目和对应生成资产索引迁到飞书用户 `万康``feishu:ou_78276b4fd9dd818d8f70bc00d03ddbdf`)。迁移前已备份数据库和 `data/jobs``/opt/skg-marketing-studio-backups/skg-marketing-owner-migration-20260526010622.sql.gz``/opt/skg-marketing-studio-backups/skg-marketing-owner-migration-jobs-20260526010622.tgz`。复验:`job_index` 中该飞书用户 24 个 job`canvas_projects` 中该飞书用户 3 个未删除私有画布,生成资产索引为 image completed=11、video completed=11、video failed=1无 owner 的 4 个更早旧 job 保持未迁移,后续再确认归属。
- 视频错误提示收口2026-05-26`579e538` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526014111.tgz`。本次把 Seedance / Doubao 视频上游错误转换为员工可读中文后再写入 `GeneratedVideo.error`,例如 `InputImageSensitiveContentDetected.PrivacyInformation` 会提示参考图含清晰人物或疑似真实人脸,需要换无脸首帧、裁切或模糊人物脸;原始上游错误只保留在 API 日志。脚本内首次验证在容器启动 3 秒时遇到根路径临时 500随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认隐私风控码会返回中文解释。
- 最近部署验证2026-05-25`cce9779` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,恢复 `chatfire-AI/huobao-canvas` 上游画布能力但保留 SKG 后端 `/api` 接入。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525102857.tgz`;生产 Docker 重建后脚本内验证通过web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok``api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/``https://marketing.skg.com/canvas/` 返回 308 到 `/``https://marketing.skg.com/p/test` 未登录返回 302 到 `/login/?next=/p/test`;容器内静态 bundle 命中 `AI 润色 / 自动执行 / 推荐: / 首帧 / 尾帧 / 多角度分镜 / 儿童绘本 / 工作流模板 / 批量下载素材`未命中上游注册链接、火宝欢迎文案、GitHub 入口或 `/huobao-canvas`
- 最近部署验证2026-05-25`e767d2b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,生产根域名改为直接进入个人生成画布,`/canvas/` 仅作为旧链接 308 跳转到 `/`。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525095839.tgz`;生产 Docker 重建后脚本内验证通过web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok``api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:容器内 `/usr/share/nginx/html/index.html` 为 Vue 画布产物,引用 `/assets/index-CioZwOvT.js` 且 title 为 `SKG`;静态 bundle 命中 `文生图 / 文生视频 / 图生视频`,未命中 `首帧生视频 / 首尾帧生视频 / 上传首帧 / 上传尾帧 / 推荐:`;外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/``https://marketing.skg.com/canvas/` 返回 308 到 `/``/p/test` 未登录返回 302 到 `/login/?next=/p/test`
- 最近部署验证2026-05-25`2a1ceee` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,可见品牌位从文字命名收敛为 logo-only首页、登录页和画布首页只显示 SKG logo网页 title 和画布 title 为 `SKG`,首页入口按钮文案为“画布”。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525092749.tgz`;生产 Docker 重建后脚本内验证通过web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok``api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。容器内静态产物复验:`index.html` 包含 `<title>SKG</title>``/skg-logo-black.svg`,首页入口包含“画布”,登录页只保留 logo当前 `_next``/canvas` 产物未再命中 `SKG 生图生视频``SKG 生成画布``营销内容生产平台``内容生产画布` 等旧可见文案。

View File

@@ -8035,6 +8035,92 @@ def video_url_from_response(data: dict) -> str:
return ""
def _video_public_error(raw: object) -> str:
text = str(raw or "").strip()
lower = text.lower()
if any(token.lower() in lower for token in (
"InputImageSensitiveContentDetected.PrivacyInformation".lower(),
"privacyinformation",
"privacy information",
"real person",
"input image may contain real person",
"human face",
"face detected",
"肖像",
"隐私",
"真人",
"人脸",
)):
return (
"视频生成失败:参考图里有清晰人物或疑似真实人脸,视频模型出于肖像/隐私风控拒绝生成。"
"请换成无可识别人脸的首帧,或先裁掉/模糊人物脸,再重新生成视频。"
)
if any(token in lower for token in (
"sensitivecontent",
"sensitive content",
"content policy",
"violate",
"violation",
"not allowed",
"risk control",
"moderation",
"敏感",
"安全审核",
"风控",
"违规",
)):
return (
"视频生成失败:参考图或提示词触发了视频模型的内容安全审核。"
"请换一张更中性的参考图,避免真实人物、暴露、医疗夸大、危险动作或敏感文字后重试。"
)
if any(token in lower for token in ("unauthorized", "invalid api key", "permission denied", "forbidden", "http 401", "http 403")):
return "视频生成失败:视频通道认证或权限异常,请联系管理员检查服务器上的视频 API Key 和模型权限。"
if any(token in lower for token in ("http 429", "rate limit", "too many requests", "quota", "insufficient", "balance", "限流", "额度", "余额")):
return "视频生成失败:视频模型当前限流或额度不足,请稍后重试;如果持续出现,请联系管理员检查视频通道额度。"
if any(token in lower for token in ("timeout", "timed out", "readtimeout", "connecttimeout", "超时")):
return "视频生成失败:视频模型响应超时,可能是上游繁忙或网络不稳定。请稍后重试,或缩短时长后再生成。"
if any(token in lower for token in (
"name or service not known",
"temporary failure in name resolution",
"nodename nor servname",
"connection refused",
"network is unreachable",
"connecterror",
"ssl:",
"网络",
"dns",
)):
return "视频生成失败:服务器连接视频模型网关异常,请稍后重试;如果连续失败,请联系管理员检查视频网关网络。"
if any(token in lower for token in ("http 404", "http 405", "unsupported", "not found", "method not allowed")):
return "视频生成失败:当前视频模型接口路径不可用,请联系管理员检查视频网关配置。"
if lower.startswith("video status: failed") or "video status: failed" in lower:
return "视频生成失败:视频模型返回生成失败。请换一张更清晰、主体更稳定的参考图,或简化提示词后重试。"
if text.startswith("视频生成失败:"):
return text[:500]
if text:
return f"视频生成失败:{text[:460]}"
return "视频生成失败:未知错误,请换一张参考图或稍后重试。"
def _video_create_failure_message(create_errors: list[str]) -> str:
raw = " | ".join(create_errors)
public = _video_public_error(raw)
if public.startswith("视频生成失败:当前视频模型接口路径不可用"):
return public
if public.startswith("视频生成失败:") and public != f"视频生成失败:{raw[:460]}":
return public
return "视频生成失败:视频模型没有接受本次请求。请换一张参考图或简化提示词后重试;如果持续失败,请联系管理员。"
def download_generated_video(client, base: str, headers: dict, provider_id: str, direct_url: str, out_mp4: Path) -> None:
if direct_url:
url = direct_url if direct_url.startswith("http") else f"{base}{direct_url if direct_url.startswith('/') else '/' + direct_url}"
@@ -8183,27 +8269,29 @@ def render_storyboard_video(
for create_path in VIDEO_CREATE_PATHS:
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs, primary_role)
if video_uses_ark() and source_ref and resp.status_code in {400, 422}:
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:160]}")
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:700]}")
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs, primary_role)
if video_uses_ark() and prepared_last_img and resp.status_code in {400, 422}:
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:160]}")
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:700]}")
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs, primary_role)
if video_uses_ark() and prepared_product_imgs and resp.status_code in {400, 422}:
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:160]}")
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:700]}")
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None, primary_role)
if resp.status_code < 400:
create = resp
break
create_errors.append(f"{video_path(create_path)} -> HTTP {resp.status_code}: {resp.text[:160]}")
create_errors.append(f"{video_path(create_path)} -> HTTP {resp.status_code}: {resp.text[:700]}")
if resp.status_code not in {400, 404, 405}:
resp.raise_for_status()
if create is None:
raise RuntimeError("视频模型已选择,但当前网关视频生成入口不可用;已尝试 " + " | ".join(create_errors))
print(f"[video create failed] job={job_id} video={local_id} errors={' | '.join(create_errors)[:1800]}", flush=True)
raise RuntimeError(_video_create_failure_message(create_errors))
data = create.json()
video_api_id = data.get("id") or provider_id or local_id
status = normalize_video_status(data.get("status"))
progress = video_progress(data, 5)
direct_url = video_url_from_response(data)
status_payload = data
update_generated_video(
job_id,
local_id,
@@ -8222,6 +8310,7 @@ def render_storyboard_video(
status = normalize_video_status(pdata.get("status"))
progress = video_progress(pdata, progress)
direct_url = video_url_from_response(pdata) or direct_url
status_payload = pdata
update_generated_video(
job_id,
local_id,
@@ -8231,7 +8320,17 @@ def render_storyboard_video(
)
if status != "completed":
update_generated_video(job_id, local_id, status="failed", error=f"video status: {status}", progress=progress, queue_message="")
raw_error = ""
if isinstance(status_payload, dict):
raw_error = str(
status_payload.get("error")
or status_payload.get("message")
or status_payload.get("reason")
or status_payload.get("fail_reason")
or status_payload
)
print(f"[video status failed] job={job_id} video={local_id} status={status} error={raw_error[:1200]}", flush=True)
update_generated_video(job_id, local_id, status="failed", error=_video_public_error(raw_error or f"video status: {status}"), progress=progress, queue_message="")
return
download_generated_video(client, base, headers, video_api_id, direct_url, out_mp4)
@@ -8247,7 +8346,8 @@ def render_storyboard_video(
queue_message="",
)
except Exception as e:
update_generated_video(job_id, local_id, status="failed", error=str(e)[:500], queue_message="")
print(f"[video task failed] job={job_id} video={local_id} error={str(e)[:1200]}", flush=True)
update_generated_video(job_id, local_id, status="failed", error=_video_public_error(e), queue_message="")
@app.post("/jobs/{job_id}/frames/{idx}/storyboard/quick-plan", response_model=StoryboardScene)

View File

@@ -1241,6 +1241,18 @@ ProductRefStateItem {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-26 · 视频生成失败改为员工可读提示</h3>
<span class="tag amber">API</span>
<span class="tag rose">UX</span>
</header>
<div class="body">
<p><strong>问题:</strong>Seedance / Doubao 视频上游返回 <code>InputImageSensitiveContentDetected.PrivacyInformation</code>、HTTP 400、429、timeout 等机器错误时,画布错误框原样展示会让员工误以为账号、模型或网关坏了,需要人工解释。</p>
<p><strong>改动:</strong><code>api/main.py</code> 新增视频错误归一化逻辑,提交失败、轮询失败和后台任务异常都会先转换成可读中文,再写入 <code>GeneratedVideo.error</code>。例如含疑似真实人脸的参考图会提示“参考图里有清晰人物或疑似真实人脸,视频模型出于肖像/隐私风控拒绝生成”,并给出换无脸首帧、裁掉或模糊人物脸的下一步。</p>
<p><strong>影响:</strong>前端现有视频失败卡、画布轮询错误框和详情里的 <code>video.error</code> 会自动显示中文解释;原始上游错误只写入 API 日志,方便管理员排查,不再要求用户把英文错误码发给开发者翻译。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-26 · 生产登录改为仅飞书</h3>