# AirShelf 项目移交报告 · 2026-06-24 > 给接手的开发同学:这份是当前项目整体状况 + 我们这一程做了什么 + 还差什么的速览。 > 配合看仓库里已有的:[CLAUDE.md](CLAUDE.md)(工程约定/铁律)、[core/ARCHITECTURE.md](core/ARCHITECTURE.md)、[PRD.md](PRD.md)、[交接-AI生成Agent化-2026-06-17.md](交接-AI生成Agent化-2026-06-17.md)。 --- ## 0. 一句话结论 **核心「商品 → 视频」生成全流程已经端到端跑通**(实测项目「电子吧唧 · 短视频 · v3」四段视频全部出片)。 所以**视频这条线暂时不用再投入**,可以先去做别的。下面第 5、6 节列了几个**已知小问题 + 临时依赖**,不阻塞主流程,回头清。 --- ## 1. 项目是什么 - **AirShelf** —— AI 短视频带货生成平台。一条流水线把「一个商品」变成「可投放的竖屏带货短视频」。 - **真实开发目录在 [core/](core/)**(仓库根的 `电商AI平台/` 只是 UI 设计稿/视觉标准答案,不是运行代码): - [core/frontend/](core/frontend/) —— React 19 + Vite + TS,真网站前端。 - [core/backend/](core/backend/) —— Django + DRF + Celery,后端 + 平台后台。 - [core/qa/](core/qa/) —— 截图对比测试。 - **部署**:Gitea Actions([.gitea/workflows/deploy.yaml](.gitea/workflows/deploy.yaml))→ 打 Docker 镜像 → K8s。 - dev 分支 → **测试环境**:前端 `https://airshelf-web.test.airlabs.art`,推 dev 自动构建部署。 - 三个核心服务:`airshelf-core-api`(Django)、`airshelf-core-worker`(Celery)、`airshelf-core-web`(nginx 托管 SPA + 反代 /api)。 ### 生成流水线(5 阶段) ``` 商品(选品) → 脚本(脚本 Agent 流式对话) → 实体提取(角色/场景) → 基础资产(人物立绘/三视图 · 场景图 · 商品三视图) → 故事板(分镜图) → 视频(Seedance 逐段出片) ``` ### AI 链路(详见 CLAUDE.md「AI 生成 Agent 化架构」) - **可插拔 Provider**:火山官方直连 `VolcanoArkProvider`(豆包/SeeDream/Seedance)+ 通用中转站 `OpenAICompatibleProvider`(yunqi/tokenssr)。分流在 [services.py](core/backend/apps/ai/services.py) `build_provider()`。 - **默认模型**:图像 = `yunqi:gpt-image-2`;视频 = `doubao-seedance-2-0`;脚本/提取 = 豆包 doubao-seed-2.0-pro。 - **火山人像合规审核**:含人脸的素材(人物立绘/三视图/分镜图)自动送火山「人像素材库」审核,拿绿/红盾。 ### 系统架构图 ``` 浏览器(SPA) │ https ┌─────────────▼──────────────┐ │ airshelf-core-web (nginx) │ ← 托管打包好的 React 静态资源 │ ├─ /assets,/ → SPA(index.html, try_files 回落=客户端路由) │ ├─ /api/ → 反代 Django │ └─ /django-admin/,/static/ → 反代 Django(自带后台) └───────┬──────────────┬──────┘ /api │ │ /api(异步任务也读写同库) ┌────────────────▼───┐ ┌──────▼─────────────────┐ │ airshelf-core-api │ │ airshelf-core-worker │ │ Django + DRF │ │ Celery(出图/出片/审核轮询)│ │ (gunicorn) │ │ │ └──┬────┬────┬───┬───┘ └──┬─────┬────────┬───────┘ │ │ │ │ │ │ │ MySQL │ TOS│ 火山│中转站 Redis MySQL 火山/中转站 (远程 │(对象│ ARK │(yunqi/ (broker)(同一库) (同上) 测试库)│存储)│直连 │ tokenssr) │ │(豆包/ │ │ │ SeeDream/ │ │ │ Seedance) │ │ └─ 人像素材库 Assets API(审核 + asset:// 引用,与 Seedance 需同账号) ``` **后端 Django apps([core/backend/apps/](core/backend/apps/))**: `accounts`(团队/成员/邀请码/认证) · `products`(商品+商品图) · `projects`(项目/脚本/故事板/视频段/时间轴) · `assets`(资产+文件+模特库+审核) · `ai`(Provider/模型配置/AI任务/提示词模板/质量词/脚本Agent/各生成 service) · `billing`(团队账户余额/预扣/流水) · `adminpanel`(平台超管后台 API) · `common`(团队作用域 mixin/健康检查)。 **前端([core/frontend/src/](core/frontend/src/))**:`App.tsx`(壳+路由) · `routes/pipeline.tsx`(5 阶段流水线主页,最大的文件) · `routes/admin/*`(平台后台) · `routes/products/projects/library`(商品/项目/成品库) · `components/`(共享组件) · `design-restraint.css`(设计 token/组件)。 > 数据/计费动作统一走「团队作用域」(`TeamScopedViewSetMixin`);生成统一「预扣额度 → 成功扣费 / 失败释放」。 --- ## 1.5 产品视角:这到底是个什么产品 + 产品架构 ### 一句话定位 **AirShelf 是一个「把一个商品,一键变成可直接投放的竖屏 AI 带货短视频」的多租户 SaaS 工具**。卖家上传商品图 + 填基本信息,平台用 AI 跑完「脚本 → 角色/场景/商品素材 → 分镜 → 视频」整条产线,产出带口播/音效的成片。按**团队**开户、按**额度(credit)**计费。 ### 产品形态 - **多租户 SaaS**:邀请码注册 → 开团队(owner) → 拉成员(分角色/给额度) → 团队内共享商品/项目/素材/余额。 - **两层界面**:① 团队「工作台」(商品库 / 视频项目 / 图片创作 / 模特库 / 成品库);② 平台超管「治理后台」(`/admin`:邀请码/团队/用户/提示词/审核/任务/供应商/计费/额度/治理)。 ### 用户主路径(产品流) ``` 注册(邀请码) → 开/进团队 → 商品库传商品图 → 新建视频项目(选成片时长) → ① 脚本(脚本 Agent 对话出稿/改稿) → ② 提取角色·场景 → ③ 基础资产(立绘/三视图/场景图/商品三视图,过人像审核) → ④ 故事板(分镜图) → ⑤ 视频(Seedance 逐段出片) → 导出成片 ``` ### 产品功能模块(用户能看到的) ↔ 后端 app | 用户模块 | 是什么 | 后端 app | |---|---|---| | 商品库 | 选品 + 商品图(一切素材的源头) | `products` | | 视频项目(流水线) | 5 阶段把商品做成短视频 | `projects` + `ai` | | 图片创作 | 模特上身图 / 平台套图 / 自由创作 | `assets` + `ai` | | 模特库 | 复用的真人模特(立绘+三视图+声线) | `assets` | | 成品库(资产库) | 所有产出图片/视频成品,按项目打包 | `assets` | | 团队 & 计费 | 成员/角色/额度/余额/流水 | `accounts` + `billing` | | 平台超管后台 | 跨团队治理 | `adminpanel` | ### 功能模块间的架构(数据/依赖关系) ``` Team(团队 · 一切的归属) ┌───────────────┬───────────────┬──────────────┬───────────────┐ billing.Account 成员/邀请码 Product(商品) Asset(资产库) Model(模特库) 余额/预扣 owner/admin/member └─ 商品图 图片/视频成品 立绘+三视图+声线 │ │(源头) ▲ ▲ │ 每次生成:预扣→扣费/释放 ▼ │ │ 引用 │ Project(视频项目)────────┘ │ │ ├ ScriptVersion → ScriptSegment(脚本/分镜:narration·dialogue·entity_refs) │ ├ BaseAssetGroup(角色/场景/商品 基础资产,含人脸的送审) │ ├ StoryboardVersion → StoryboardFrame(分镜图,送审) │ ├ VideoSegment → VideoSegmentVersion(每段视频,可多版本) │ └ Timeline(拼接导出,V2 再启) └────────────────── ai.services 编排所有 AI 调用 ──────────────────┘ ModelConfig/Provider(火山/中转站) · AITask(每次生成一条,挂计费) · PromptTemplate(提示词) · 火山人像审核 ``` - **一切挂在 Team 下**(团队作用域)。`Product` 是源头;`Project` 串起脚本/资产/故事板/视频;`ai` 模块编排所有 AI 调用并挂计费;`assets` 管资产+审核+模特库;`billing` 管钱。 - ⭐ **产品的主数据线**:项目脚本提取出 `script_entities`(角色/场景/商品) → 决定要做哪些**基础资产** → 基础资产**过人像审核**拿 `asset://` 引用 → **故事板/视频**引用它**锁脸锁商品**。接手要懂的就是这一条「实体 → 资产 → 引用」贯穿始终。 --- ## 2. 视频生成流程现状:✅ 顺畅,已跑通 实测 `电子吧唧 v3` 四段视频提交火山全部 ACCEPTED、正常出片。让它跑通的**关键认知**(接手必懂): > **火山 Seedance 2.0 对「写实人脸」的原始图片直链会直接 400 拒**(`InputImageSensitiveContentDetected`)。 > 含人脸的参考图(人物立绘、分镜图)**必须走「素材库引用」`asset://asset-xxx`**,而不是原始 TOS 直链。 > 素材库引用要求:① 资产已送审进库(有 `review_remote_id`,processing 也行,不必等 active);② **素材库和 Seedance 必须同一个火山账号**(见第 5.2 节临时依赖)。 代码位置:[services.py](core/backend/apps/ai/services.py) `_seedance_ref_url()` / `_video_reference_images()`。 ### 视频提示词(admin 可改) 真正发给 Seedance 的正文长这样(admin「提示词」页的「视频」卡可改模板): ``` 【设定】@图1是学生女主(角色),@图2是室友(角色),@图3是女生宿舍书桌(场景),@图4是商品(商品)。 【分镜】根据@图5分镜图生成「电子吧唧」短视频。 【脚本】画面:… / 台词:室友:… / 学生女主:… 电商带货短视频,商品露出清晰,节奏有转化感。不要字幕,不要背景音乐,但是要有音效,逼真的音效。 ``` - 时长/比例(duration/ratio/resolution)**是 API 参数单独传**,不写进正文。 - 对白带说话人、照脚本原样放(不再拍扁成一行口播)。 --- ## 2.5 提示词体系(admin 6 条模板 + 数据结构/数组) 提示词**不再写死**:admin「提示词」页存「一段固定模板 + 几个 `{占位符}`」,生成时把角色/场景/商品/脚本等**自动塞进占位符**,拼成最终发给模型的正文。代码在 [services.py](core/backend/apps/ai/services.py) `render_prompt()` / `prompt_ratio_size()`(DB 模板覆盖代码写死默认;模板删了/停用就回落默认,零回归)。比例靠模板的 `ratio` 字段 → gpt-image 尺寸(`_RATIO_TO_SIZE`)。 ### 6 条模板(DB 当前值) | key | 名称 | 比例→尺寸 | 占位符 | 模板正文 | |---|---|---|---|---| | `person_portrait` | 人物立绘 | 竖2:3→1024×1536 | `{描述}` | 电商真人模特,氛围正面全身照,**{描述}**,自然妆容,柔和影棚光,真实质感,单人,纯色背景 | | `person_triview` | 人物三视图 | 16:9→1536×864 | (无,靠参考图1) | 参考图1角色,生成角色三视图:胸像特写/全身正面/侧面/背面,白色背景 | | `product_triview` | 商品三视图 | 16:9→1536×864 | `{商品}` `{补充}` | 参考图1是「**{商品}**」真实主图。严格参照生成三视图:正面/侧面/背面,纯白背景,**{补充}** | | `scene` | 场景图 | 16:9→1536×864 | `{场景描述}` | **{场景描述}**(外层风格可在 admin 包) | | `storyboard_frame` | 分镜图 | 竖2:3→1024×1536 | `{设定}{场景上下文}{时长}{脚本}{补充}` | **{设定}**根据脚本生成导演故事板…(保持各参考图同一张脸/同一商品);竖屏 9:16 | | `video_segment` | 视频 | 竖屏(参数) | `{设定}{分镜}{脚本}{时长}` | **{设定}{分镜}**【脚本】**{脚本}**\n电商带货短视频…不要字幕/背景音乐,要逼真音效 | > ⚠️ 占位符是「调用字段」,**改模板时别删它们**(删了运行时原样保留、不会崩,但就拼不进内容了)。视频的时长/比例是 **API 参数**(duration/ratio),不在正文。 ### 关键数据结构(占位符背后的「数组」) 脚本 Agent 产出结构化 `ScriptDraft`,落 `ScriptVersion.metadata` + 逐镜 `ScriptSegment`,并回填 `project.metadata` 供下游: - **`project.metadata.script_entities`** —— 提取出的角色/场景/商品实体数组(占位符 `{设定}`/参考图来源): ```json [{"id":"c1","type":"character","name":"学生女主"}, {"id":"c2","type":"character","name":"室友"}, {"id":"p1","type":"product","name":"电子吧唧"}, {"id":"s1","type":"scene","name":"女生宿舍书桌"}] ``` - **`ScriptSegment.entity_refs`** —— 本镜引用的实体 id 数组:`["c1","c2","p1","s1"]`(决定本镜带哪些参考图)。 - **`ScriptSegment.dialogue`** —— 结构化对白数组(视频 `{脚本}` 的台词,照原样带说话人): ```json [{"speaker":"c2","line":"你偷偷跟谁聊天呢?老实交代!"}, {"speaker":"c1","line":"哎呀你别瞎想!"}] ``` (`speaker` 是实体 id,渲染时用 `script_entities` 解析成名字;另有扁平 `narration` 兜底。) - **`ScriptSegment`** 其它:`visual_prompt`(画面)、`product_exposure`(商品露出)、`duration_seconds`、`sort_order`。 - **视频 `reference_images` 数组**(传给 Seedance,与提示词 `@图N` 顺序严格对齐): `["asset://asset-…(角色)", "asset://asset-…(角色)", "https://…(场景)", "https://…(商品)", "asset://asset-…(分镜图)"]` —— **含人脸的(角色/分镜图)走 `asset://` 素材库引用,无脸的(场景/商品)走原始直链**(见第 2 节)。 --- ## 2.6 脚本 Agent 怎么运行(出稿 + 改稿一体,流式对话) 代码:[script_agent.py](core/backend/apps/ai/script_agent.py)。端点 `POST /api/projects/{id}/script-agent-stream/` → **SSE 流**(`text/event-stream`,DRF 必须挂 `ServerSentEventRenderer` 否则 406)。 **它不是「调一次函数返回 JSON」,而是一个边想边吐的流式 agent**: 1. **系统提示词 = 电商脚本 skill**:加载 [core/backend/skills/ecommerce-video-script/](core/backend/skills/ecommerce-video-script/) 的 `SKILL.md + references` 当领域知识(模型无关)。⚠️ **skill 必须在 `core/backend/` 内**,否则不在镜像构建上下文里 → 系统提示词为空 → 退化乱出(这就是之前线上提取/脚本翻车的根因之一)。 2. **3 种输入模式,收敛到同一份结构化 `ScriptDraft`**: - `auto` 全自动(只给商品 + 卖点)· `theme` 一句话主题 · `revise` 改稿(基于某个基准版)。 - 改稿带 `target_index` = **精准只改第 N 镜**,后端 `_merge_single_segment` **强制保留其余镜原样**(`0` 是合法镜号,判断一律 `is None` 不用真值)。 3. **流式吐「工具卡 + 思考」给真 agent 体感**(SSE 事件,见 script_agent.py 头部): `tool`(加载skill→分析商品→生成分镜→提取实体)· `reasoning`(推理模型思考流,逐字、纯展示不进答案)· `delta`(自然语言前言)· `draft`(规范化后的稿)· `saved`(已落库)· `summary`(模型自己写的收尾交付语)· `done` / `error`。 > 其中 `reasoning` 是关键体验修复:推理模型出 JSON 前会先「想」很久,把思考逐字下发,避免前端「卡在生成分镜」假死。 4. **JSON 由后端可靠抽取,不靠模型排版**:`normalize_draft` / `_extract_json`(括号配平、字段模糊匹配 `_pick_field`、时长吸附到 `[15,30,60,90]`、抽实体)。模型排版再乱也能稳定拿到结构化稿。 5. **结构化 `ScriptDraft` 契约**:`narration`(扁平口播兜底)+ `dialogue:[{speaker,line}]`(带说话人对白)+ `role/tone/product_exposure/entity_refs/total_duration`。 6. **落库 + 回填**(`persist_script_draft`):写 `ScriptVersion.metadata`(hook/tone/entities)+ 逐镜 `ScriptSegment`,并把 `cast/scenes/script_entities` **回填 `project.metadata`** —— 下游(提取/资产/故事板/视频)全靠这份回填。 7. **计费 + 断连兜底**:走 `AITask` + 额度**预扣 → 成功扣费 / 失败释放**。客户端中途断连会抛 `GeneratorExit`(是 `BaseException`,普通 `except` 抓不到)→ 用 `try/finally` 释放预扣,**绝不冻结额度**。 8. **多模型可选**:`model_config_id` 切不同脚本模型(默认豆包 doubao-seed-2.0-pro)。 > 一句话:**前端点「生成脚本」→ 后端起一个流式 agent,边加载 skill / 分析商品 / 出分镜,边把工具卡和思考推给前端;最后后端把模型输出可靠抽成结构化脚本、落库、回填给下游**。整条产线后续所有阶段都吃这份脚本回填的 `script_entities`。 --- ## 3. 我们这一程做了什么(2026-06-22 ~ 06-24) 按模块归类(完整见 `git log`,均带 Claude 署名): **提取(角色/场景)** - 实体提取改**异步 worker + 轮询**,根治线上 502 / 刷新丢 loading / 重复扣费。 - skills 随后端打进镜像(之前放仓库根 → 镜像里没有 → 系统提示词为空 → 提取吐散文解析失败)。 - 锁定豆包 2.0 Pro + 写死 JSON 输出契约兜底。 **视频(本程重点,让它跑通)** - ⭐ **真人/分镜图走素材库 `asset://` 锁脸**,根治 Seedance「人物/场景全错」。 - 提交报错**接住返 JSON、不再 500 白屏**;分镜图 processing 态也能走 asset://。 - 视频卡交互反馈(重跑转圈、防连点双扣)、「全部重跑」真重跑已完成段、「共 N 版」入口。 - 视频卡用首帧当封面(`#t=0.1`),不再空白占位。 - 视频提示词:台词带说话人、时长/比例只走参数。 **故事板** - 整张生成时每张分镜转圈(不全屏遮挡)、未生成占位回 2:3 竖屏。 - ⭐ 分镜图挂**火山人像审核盾** + 红盾可「重新送审」(审核超时是误判,重提即过)。 **商品** - ⭐ 建商品改 **upload-first**(图先进桶拿 asset,创建时带 `cover_asset`+`images` 一块落库),根治「商品创建失败」。 - 商品图删除接通(之前叉子是纯 CSS 装饰没绑事件)+ 至少保留一张。 **图片比例 / admin** - admin「提示词」页:视频线 6 条生图/视频提示词正文 + 比例**可在后台改,不再写死**。 - 真 16:9 支持(`1536x864`,原来误当 3:2);人物三视图/商品三视图/场景图默认设成 16:9(分镜图/立绘/视频保持竖屏)。 - 脚本按向导选的时长出(之前写死 60 秒)。 --- ## 4. 怎么跑起来(接手速记) **本地后端(连真测试库,不是隔离 sqlite)** ```bash cd core/backend DJANGO_SETTINGS_MODULE=airshelf.settings.development .venv/bin/python manage.py runserver 127.0.0.1:8010 --noreload ``` **本地前端**:`cd core/frontend && npm run dev`(5173,HMR;/api 走 8010)。 **跑测试**(远程库无建库权限,必须 sqlite) ```bash cd core/backend DB_ENGINE=sqlite DJANGO_SETTINGS_MODULE=airshelf.settings.test .venv/bin/python manage.py test ``` **部署**:推 dev → Gitea Actions 自动构建 + 跑迁移 + 滚动部署。**默认不推,改完即停,明确说推才推**(CLAUDE.md Git 铁律)。 > 注:本机用的是 python-build-standalone 的 CPython 3.12(系统自带 3.9 跑不了 Django 5),venv 在 `core/backend/.venv`。 --- ## 5. 已知问题 / 待办(不阻塞主流程,回头清) ### 5.1 ⚠️ Admin 页刷新报「找不到网页」(404)—— 根因已定位,修法明确 **现象**:在 `/admin/...`(如 `/admin/prompts`)刷新页面 → 404。点链接进去没事(前端路由),一刷新就挂。 **根因**:路径冲突。 - React 平台后台用的是客户端路由 `/admin`、`/admin/prompts`、`/admin/teams` … - 但 nginx([core/frontend/nginx.conf](core/frontend/nginx.conf))里有 `location /admin/ { proxy_pass …api }`,把 `/admin/` **反代给了 Django**; - Django([core/backend/airshelf/urls.py](core/backend/airshelf/urls.py))只有 `path("admin/", admin.site.urls)`(Django 自带后台),找不到 `/admin/prompts` → 404。 **推荐修法**(把 Django 自带后台挪开,让 `/admin/*` 回落 SPA): 1. `urls.py`:`path("admin/", admin.site.urls)` → `path("django-admin/", admin.site.urls)`; 2. `nginx.conf`:`location /admin/` → `location /django-admin/`。 这样 `/admin/*` 落到底部 `try_files $uri $uri/ /index.html`(SPA 路由),刷新就正常了。 > 约 10 分钟的小改。要的话可以直接做。 ### 5.2 ⚠️ 临时借 AirDrama 火山账号(**待换自有,最该尽快处理的基建项**) - **真人素材库审核** 用的是借来的 AirDrama 账号 AK/SK(`.env` 的 `ASSETS_API_*`)。 - **视频 Seedance** 我们临时也指到 AirDrama 账号(`.env` 的 `VIDEO_ARK_API_KEY`,仅覆盖 video 能力,见 [services.py](core/backend/apps/ai/services.py) `build_provider`)—— 因为 `asset://` 素材库引用必须和 Seedance 同账号才解析得到。 - **终态**:等自有火山账号开通「人像素材库(Assets API)」后,把 `ASSETS_API_*` 换成自有账号 AK/SK,并删掉 `VIDEO_ARK_API_KEY` 这行(视频自动回落自有 `VOLCANO_ARK_API_KEY`)。CLAUDE.md 里记的「待张业昌换」就是这个。 ### 5.3 视频封面:后端抽帧没产出 poster(前端已兜底) - 后端 [services.py](core/backend/apps/ai/services.py) `_generate_video_poster` 本应抽视频首帧存成封面图,但当前环境**没产出 poster 文件**(大概率缺 ffmpeg/抽帧依赖)。 - 现在靠前端 `