diff --git a/AI生成-Agent化落地方案.md b/AI生成-Agent化落地方案.md new file mode 100644 index 0000000..32c8a5e --- /dev/null +++ b/AI生成-Agent化落地方案.md @@ -0,0 +1,261 @@ +# AirShelf · AI 生成 Agent 化落地方案 + +> 目标:把【脚本生成】升级为「多模型可选 + 出稿/改稿一体」的对话式 agent,并打通后续图片/故事板/视频 SOP。 +> 用户定位:不懂视频制作的电商小白,点按钮就出"能吸睛、能转化"的带货短视频,提示词全替用户包好。 +> 本文整合 2026-06-16 多轮讨论结论,作为开工 SSoT。 + +--- + +## 0. 一句话现状 + +5 阶段流水线(商品 → 脚本 → 图片 → 故事板 → 视频)页面已搭好,生成链路能跑,但:**脚本是一次性散文+正则解析、无 agent、无多模型;图片"平台套图"是装饰;参考图生成断线;Seedance 音频被关。** + +--- + +## 1. 现状核查(已验证,带 file:line) + +### 1.0 模型与中转站(最终方案,均已实测 200) +**主力中转站 = `tokenssr`(`https://king.tokenssr.com/v1`,一把 key 通吃 90 模型,见 .env `TOKENSSR_API_KEY`)。** + +| 用途 | 模型 | 端点/调法 | +|---|---|---| +| 脚本·豆包 | doubao-seed-2.0-pro | 火山 ARK chat(本地直连) | +| 脚本·GPT | `gpt-5.5` | tokenssr `chat/completions` | +| 脚本·Gemini | `gemini-3.1-pro-preview` / `gemini-3.5-flash` | tokenssr `chat/completions` | +| 纯文生图 | `gpt-image-2` | tokenssr `images/generations` | +| **参考图/编辑** ⭐ | **`gpt-image-2`** | **tokenssr `images/edits`(multipart `image[]`,1~多张参考图,返回 b64)** | +| 视频 | seedance-2.0 | 火山 ARK(本地直连) | + +- 多图参考(故事板 @图1@图2@图3)实测 200;`gemini-3-pro-image-preview`(chat 多模态)留作图像备选/风格选择。 +- YunQi 那家分组割裂、gpt-image-2 渠道残废 → 已弃用,仅留历史参考。 +- token 用量正常返回,对齐"按实际 token 扣费"。 + +### 1.1 模型与 fallback +| 类型 | 实际模型 | Provider | Fallback | +|---|---|---|---| +| 脚本 text | Doubao-Seed-2.0-Pro | 火山 ARK(写死) | ❌ 无 | +| 图片 image | **GPT-Image-2 / YunQi 网关**(Seedream 被 disabled) | YunQi | ❌ 无 | +| 视频 video | Seedance-2.0 | 火山 ARK(写死) | ⚠️ 仅"丢参考图退纯文生视频" | + +- 默认选模型 = 该能力下最早创建的 active 行 [services.py:33-39](core/backend/apps/ai/services.py#L33-L39) +- 生图按 provider 分流(yunqi/火山) [services.py:42-46](core/backend/apps/ai/services.py#L42-L46) +- YunqiProvider 继承火山,`chat_completion` 现成 → **文本接多模型很便宜** [yunqi.py](core/backend/apps/ai/providers/yunqi.py) + +### 1.2 脚本生成(无 agent) +- 系统提示词:4×15s、强制 `镜头N/旁白/画面` 格式 [services.py:53-77](core/backend/apps/ai/services.py#L53-L77) +- **正则**抠字段 [services.py:81](core/backend/apps/ai/services.py#L81) +- **第二次** LLM 调用抽 人物/场景 JSON [services.py:116](core/backend/apps/ai/services.py#L116) +- 全是一次性调用,无循环/无工具/无自检 + +### 1.3 图片(平台套图是空壳) +- 三种模式提示词模板在前端 [ai-tools.tsx:387](core/frontend/src/routes/ai-tools.tsx#L387) +- 平台卡/模特卡选择 `pickedIds` **只做高亮,不拼进 prompt、不发后端** [ai-tools.tsx:1195](core/frontend/src/routes/ai-tools.tsx#L1195) +- 提交只发 `{prompt, mode, count}` [api.ts:342](core/frontend/src/api.ts#L342);后端 `mode` 只决定任务类型/分类/命名,不加工 prompt [services.py:1067](core/backend/apps/ai/services.py#L1067) + +### 1.4 视频 +- 视频提示词织入风格锚点+旁白+画面 [services.py:621](core/backend/apps/ai/services.py#L621) +- 已传参考图给 Seedance [services.py:870-897](core/backend/apps/ai/services.py#L870-L897) +- 但 `generate_audio` 写死 False [volcano.py:129](core/backend/apps/ai/providers/volcano.py#L129) + +--- + +## 2'. 开工就绪状态(2026-06-17 全部核实) + +| 卡点 | 状态 | +|---|---| +| 电商 skill | ✅ 已写好 `skills/ecommerce-video-script/`(SKILL.md+5 references),**输出契约与 §3 完全对齐**(还多了 aspect_ratio + 四档时长) | +| 参考图出图 | ✅ tokenssr gpt-image-2 `images/edits` 实测(含多图) | +| 火山审核+绿/红标 | ✅ **AirDrama 有整套现成**:`video-shuoshan/backend/utils/assets_client.py`(建组/传素材/查审核)+ AssetGroup/Asset 模型(status: processing→**active=绿标 / failed=红标**)+ 轮询接口。照搬即可。策略:**一团队一素材组**(火山确认单组可放 500 万素材),后台 API 静默自动上传,前端不显示组、只显示绿/红标;红标提示"未通过审核,请改提示词重生" | +| 模特库 | ✅ 由 agent 用 gpt-image-2 预生成 2-3 个(先 9:16 正面氛围图 → 以其为参考出 16:9 白底三视图)。提示词见 §阶段4 | +| 前端结构 | ✅ routes 齐全,不用大动,主要接线+结构化渲染+聊天框+badge | +| 多模型 | ✅ 豆包/GPT/Gemini 实测可用 | + +### ★ 架构原则:Provider 可插拔(不写死中转站) +**除火山(ARK/Seedance,官方直连)外,其余全走中转站且随时可换** → 设计成: +- **一个通用 `OpenAICompatibleProvider(base_url, api_key)`** 覆盖所有中转站(tokenssr/yunqi/未来任意家),按 `ModelConfig.provider` 的 `base_url`+`api_key` 实例化。 +- **换中转站 = 改 DB 里 Provider 的 base_url+api_key,零改代码**;加新家 = 加一行(格式兼容则复用同一 provider 类)。 +- `VolcanoArkProvider` 专管火山官方(文本/图/Seedance)。 +- 新增 `ModelConfig.api_key`;key 全部走 .env / DB,**不写死代码**。 + +## 2. ⚠️ 前置硬伤 / 必须先决策 + +### GAP-1 参考图生成 —— ✅ 已解决(2026-06-17 实测,换中转站 tokenssr) + +**结论:换到中转站 `tokenssr`(base `https://king.tokenssr.com/v1`)后,gpt-image-2 直接支持参考图(含多图)。** 之前 YunQi 是渠道残废,不是 gpt-image-2 本身不行。 + +- **一把 key 通吃 90 个模型**(无 YunQi 那种分组割裂):gpt-image-1 / gpt-image-1.5 / **gpt-image-2** / dall-e-3 + 全套 Gemini 文本&图像模型 + gpt-5-nano 等。 +- **gpt-image-2 传参考图,两条路实测 200:** + - **A·`images/edits`(标准 OpenAI 格式,推荐)**:multipart,`image[]` 传 **1 张或多张**参考图 + `prompt` + `model=gpt-image-2` + `size`;返回 `data[0].b64_json`。**多图参考(2张)实测 200** → 故事板 `@图1@图2@图3` 成立。 + - **B·`images/generations` + `image`(base64)**:返回 `data[0].url`(`r2.sysrv.net/...png`,直接是托管 URL)。 +- 参考图来源:角色/场景/商品图在 TOS(URL)→ 取字节走 edits 的 `image[]`,或 base64 走 generations;出图落 TOS。 +- **需新增 `TokenssrProvider`**:`image_generate()`(文生图)/`image_edit(model,prompt,ref_bytes[],size)`(参考图,multipart image[])。比之前 Gemini-chat 解析 markdown 的方案**更干净**(标准 OpenAI 格式、data[] 直接拿图)。 +- 备选:同站的 Gemini 图像模型(`gemini-3-pro-image-preview`)走 chat 多模态也可用,留作 fallback/风格选择。 +- **这是图片/故事板/视频趴的前置依赖,现已具备。** + +### GAP-2 Seedance 音频被关 +- 你的设计:Seedance 直接出画面+音效+人声,不走 TTS。 +- 现 `generate_audio=False` 写死 [volcano.py:129](core/backend/apps/ai/providers/volcano.py#L129) → 需打开;音色一致性(audioReference,Seedance-2.0 支持 [catalog.py:67](core/backend/apps/ai/catalog.py#L67))二期再接。 + +### 决策清单 +1. **图片参考图模型**(已定·实测解决):用 **tokenssr 的 gpt-image-2**(`images/edits` multipart `image[]`,多图已验证);YunQi Gemini 图像模型留可选 fallback。 +2. **计费粒度**(已定):**按实际 token 消耗,每次调用扣一次费**。含义:agent 自检循环每跑一轮都扣 → 循环要克制(≤2-3 轮),内部调用尽量精简,避免空转扣费。 +3. **平台套图(独立图片工具)**(已定):暂不管,本方案聚焦视频项目流水线。 +4. **多 key 管理**(基本解决):换 tokenssr 后,一把 key 通吃 90 模型,不再有 YunQi 分组割裂问题。后端仍加 `ModelConfig.api_key` 字段(留作多 provider 共存:tokenssr / 火山本地)。 + +--- + +## 3. 数据契约:结构化脚本 `ScriptDraft`(全链路地基) + +把脚本从"散文+正则"改成**结构化输出**(JSON/tool-calling),模型无关,一次喂饱全下游。 + +```jsonc +{ + "hook": "前3秒主打钩子(一句话)", + "tone": "种草|测评|剧情|痛点", + "aspect_ratio": "9:16", // 不写死,可 16:9/1:1/4:5,透传下游 + "total_duration": 60, "segment_count": 4, // 时长 15/30/60/90 四档,每15s一镜→1/2/4/6镜 + + "entities": [ // 角色/场景/商品,全脚本共享 → 保证4镜同一角色同一张脸/同一音色 + { "id": "c1", "type": "character|scene|product", "name": "女主", + "visual_prompt": "给图模型的生图提示词(AI自动生成,小白不打字)", + "ref_index": 1, + "voice_ref": "可选·角色音色参考(二期锁音色)" } + ], + + "segments": [ + { "index": 0, "duration": 15, + "role": "钩子|痛点|卖点|CTA", // 电商脚本的灵魂 + "narration": "本镜要说出来的台词/旁白,≤55字(Seedance 15秒内直接发声,字数=可懂语速上限)", + "speaker": "可选·指向角色entity的id;null=画外旁白", + "visual": "画面描述", + "product_exposure": "商品露出方式(手持/特写/使用中)", + "entity_refs": ["c1"] } + ] +} +``` + +**它如何驱动全下游:** +- 图片趴:直接读 `entities[].visual_prompt` 预填出图卡(小白不打字) +- 故事板趴:按 `entity_refs` + `ref_index` 拼 `@图N` +- 视频趴:`narration`(发声)+ `visual` + `@图N` 参考 → Seedance +- 一致性:同一 entity 全程一份图/一份音色 + +> 从 AirDrama **保留**:结构化思维、entity 层(一致性)、segment role。**砍掉**:三流、5 种 beat、est_sec、Section 嵌套、质量词三层 fork。 + +--- + +## 4. 分阶段落地 + +### 阶段 0 · 电商剧情 skill —— ✅ 已完成 +- 产物在 `skills/ecommerce-video-script/`(SKILL.md + 5 references:methodology/hook-library/category-playbook/platform-tone/checklist)。 +- 输出契约 = §3 ScriptDraft(并扩展了 aspect_ratio + 15/30/60/90 四档时长);3 模式路由、输出前自检、写作红线、渐进披露齐全。 +- agent(阶段2)运行时加载它作为领域知识。**无需再做。** + +### 阶段 1 · 结构化契约 + 多模型管子(后端 ~1d) +- 定 `ScriptDraft` 类型(§3);脚本输出从正则改 **tool-calling/JSON** +- 文本 provider 分流(照搬生图)或全走 YunQi 网关;`/api/ai/models/` 已能列模型,生成时透传 `model_config_id` +- DB:加 **GPT-5.5** + **Gemini-3.5-flash/3.1-pro**(均 YunQi 已实测可用)文本模型设 active;按模型配各自 key(决策4) +- **验收**:同一商品,豆包 / GPT-5.5 / Gemini 三家都产出合规 `ScriptDraft`,字段不崩 +- 依赖:无 ‖ 可与阶段0并行 + +### 阶段 2 · 对话式脚本 agent(出稿+改稿一体)(后端 ~2-3d) +- **同一个 agent 跑在带状态会话上**:第1轮=出稿(3模式 seed),第N轮=改稿(带上轮草稿) +- Session 状态:当前 `ScriptDraft` + 消息历史 + 商品上下文 + 选定模型/调性 +- 工具:`get_product / load_skill / validate / emit` + `edit_segment(index,instruction)` / `patch_draft`(`build_segment_rerun_prompt` [services.py:353](core/backend/apps/ai/services.py#L353) 升级为工具) +- 有界循环 ≤2-3 轮:读(草稿+用户话)→ 判(整篇重生/改某镜/答疑)→ 执行 → validate → emit +- 一个 endpoint:`(session_id, 可选消息, 当前草稿) → (新草稿, agent回话)` +- 计费:**按实际 token、每次调用扣**(决策2)→ 循环 ≤2-3 轮、内部调用精简,避免空转扣费 +- **验收**:点按钮 ~10-20s 出 4 镜结构化脚本;聊一句能精准改某镜;旁白≤55字、结构齐、entity 一致 +- 依赖:阶段0 + 阶段1 + +### 阶段 3 · 前端脚本趴(前端 ~1.5d) +- 模型下拉(豆包/GPT/Gemini) +- 3 种出稿输入(全自动/一句话/自带脚本) +- 4 镜结构化渲染(钩子/痛点/卖点/CTA 标签 + 旁白/画面/商品露出) +- 聊天框(改稿)+ 每镜快捷按钮(重跑/改活泼/加钩子=预设改稿指令) +- **验收**:设计师视角点按钮出稿、看得到分镜结构、能对话改 / 点按钮改单镜 +- 依赖:阶段2 + +### 阶段 4 · 图片趴(后端+前端) +- **前置·新增 `TokenssrProvider`**:`image_generate()`(文生图)/ `image_edit(model,prompt,ref_bytes[],size)`(参考图,`images/edits` multipart `image[]`);新增 `ModelConfig.api_key` +- **三视图**(商品创建即可生,视频项目内可补):prompt + 上传商品图(正/侧/背多参考)→ **gpt-image-2 `images/edits`** 出三视图;无则用原图 +- **实体出图**:直接读脚本 `entities[].visual_prompt` 预填卡,小白只点生成(不打字);不满意可改 prompt +- **模特库**(预生成 2-3 个电商模特,流程:① gpt-image-2 出 9:16 正面氛围图 → ② 以①为参考图出 16:9 白底三视图,提示词:`参考图1角色,生成角色三视图,从左往右依次为:胸像特写,全身正面,全身侧面,全身背面,白色背景`)+ 自定义模特/场景(gpt-image-2 生) +- 出图落 TOS,大图预览 +- **火山审核**(照搬 AirDrama `utils/assets_client.py` + AssetGroup/Asset 模型 + 轮询接口):一团队一素材组,后台静默自动上传(前端不显示组),轮询 status → **active=绿盾标 / failed=红标**(红标提示"未通过审核,请改提示词重生");env 复用 TOS_ACCESS_KEY/TOS_SECRET_KEY + ASSETS_API_ENABLED + PROJECT_NAME +- **验收**:小白零输入出齐角色/场景/商品图;三视图可参考原图;盾牌 icon 正确 +- 依赖:阶段2(脚本 entities) + +### 阶段 5 · 故事板趴(依赖阶段4) +- 拼装系统提示词 + 角色图/场景图/商品图(三视图优先)+ 本段 15s 脚本 → **gpt-image-2 `images/edits` 多图 `image[]`** 出导演故事板(多图实测 200) +- 提示词骨架:`制作导演故事板指导seedance / @图1角色 @图2场景 / 分镜脚本(15s) / 规则:景别·运镜·画面·角色动作·情绪·对白旁白·灯光` +- 每帧场景/角色必须回溯脚本文字(靠 `entity_refs`) +- **验收**:每镜一张故事板,角色/场景跟回脚本,可指导视频 +- 依赖:阶段4 + +### 阶段 6 · 视频趴(依赖阶段5) +- 分镜图+角色图+场景图+商品图+文字脚本 → Seedance(火山,参考图走 TOS URL,已在用) +- v1 固定模板:`【设定】@图1是X角色,@图2是X场景,@图3是X商品 【分镜】根据@图4分镜图生成X商品视频 【脚本】…`(后续可做视频优化 skill) +- 打开 `generate_audio`(GAP-2);二期接 audioReference 锁音色 +- 火山审核盾牌 icon 同图片趴 +- **⚠️ Seedance 出片慢:正常 5-10 分钟/段,凌晨可能 3-4 分钟**。轮询超时要放长(≥15 分钟),测试时别误判失败;若实测耗时太长,视频趴只到"提交成功+轮询机制验证",标注成熟度交接。 +- **验收**:四镜成片、画面跟故事板、音画自带、审核 icon 正确 +- 依赖:GAP-2 + 阶段5 + +--- + +## 5. 全链路数据流 + +``` +商品(信息+图) ──┐ + ├─→ [脚本 agent] → ScriptDraft(hook/entities/segments) +前置条件(调性/平台)┘ │ + ├─ entities[].visual_prompt ─→ [图片趴] 角色/场景/商品图(三视图优先) + │ │ + ├─ entity_refs + 15s脚本 ──────────────→ [故事板趴] 导演分镜图 + │ │ + └─ narration + @图N + 分镜图 ──────────→ [视频趴] Seedance 成片(自带音) + │ + 全程:同一 entity = 同一张脸/同一音色 ;火山审核盾牌 icon +``` + +--- + +## 5'. 执行须知(compaction-safe resume 锚点 · 2026-06-17 夜) + +> 压缩/换会话后,从这里 + 下面的 checklist 直接续干,不必重读全部代码。 + +**凭证(都在 `core/backend/.env`):** +- `TOKENSSR_API_KEY` / `TOKENSSR_BASE_URL`(`https://king.tokenssr.com/v1`)= 主力中转站,一把 key 通吃 90 模型。 +- `FEISHU_APP_ID` / `FEISHU_APP_SECRET`(机器人「小毛球」,文件上传权限已开)。 +- 火山:`VOLCANO_ARK_API_KEY`、`TOS_*`、`VOLC_TTS_*` 已就绪。 + +**已验证可用模型(tokenssr,均 200):** 文本 `gpt-5.5`/`gemini-3.1-pro-preview`/`gemini-3.5-flash`;图编辑 `gpt-image-2`(`images/edits` multipart `image[]`,1~多张参考图,返回 b64)+ `images/generations`(文生图);Gemini 图像 `gemini-3-pro-image-preview`(chat 多模态,备选)。豆包/Seedance 走火山 ARK 本地。 + +**关键代码事实(已读):** +- `ai/models.py`:`ModelProvider`(name/base_url/metadata,**需加 `api_key` 字段+迁移**)、`ModelConfig`(provider FK/name/capability/endpoint/status,默认选 `get_default_model`=该能力最早 active)、`AITask`(idempotency_key 必填唯一、status 机、request/response_payload、estimated/actual_cost)。 +- `ai/services.py`:`create_ai_task`→`reserve_credit`;`charge_reserved_credit`/`release_credit`(billing/services/ledger.py,幂等带行锁);`get_image_provider` 按 provider.name 分流;`build_*_prompt` 系列;`generate_project_script`(改结构化输出落 `ScriptVersion.metadata`)。 +- `providers/`:`base.py`(AIProvider Protocol)、`volcano.py`(VolcanoArkProvider:chat_completion/image_generation 带 `image` 参/create_video_task 有 `generate_audio=False` **需开**)、`yunqi.py`。**要新增 `OpenAICompatibleProvider(base_url,api_key)`** 覆盖所有中转站 + tokenssr 的 `image_edit`(images/edits multipart image[])。 +- `projects/models.py`:`ScriptVersion`(content+metadata)、`ScriptSegment`(narration/visual_prompt/duration,**需补 role/product_exposure/entity_refs/speaker 或塞 metadata**)、`BaseAssetGroup`(kind: product/person/scene,adopted/candidate assets)、`StoryboardVersion/Frame`、`VideoSegment/Version`。 +- `assets/models.py`:`Asset`(category: person/scene/product_image/…)+`AssetFile`(object_key/bucket/preview_url)。出图落 TOS 已有链路。 +- 前端 routes 齐全:`pipeline.tsx`(五阶段主)、`projects.tsx`、`ai-tools.tsx`、`api.ts`、`types.ts`。不大改,只接线+结构化渲染+按钮+绿红标。 + +**火山审核复用(照搬 AirDrama,有 4 个坑已处理):** 源 `video-shuoshan/backend/utils/assets_client.py`(create_asset_group/create_asset/get_asset)+ AssetGroup/Asset 模型(status active=绿/failed=红/processing)+ 轮询。策略:一团队一素材组,后台静默上传 TOS URL,前端只显示绿/红标。 +- **坑1·SDK**:AirShelf `requirements.txt` 没 `volcengine`(只有 boto3+requests)→ 加 `volcengine` 依赖装上(或用 requests 手写火山签名)。 +- **坑2·密钥**:AirDrama 用 `settings.TOS_ACCESS_KEY/TOS_SECRET_KEY`;AirShelf 改用**独立 env** `ASSETS_API_ACCESS_KEY/ASSETS_API_SECRET_KEY`(已写入 .env,**暂借 AirDrama 已邀测开通的 AK/SK** `AKLTNGJi…`)。搬代码时把读 key 处改成读这俩。 +- **坑3·配置**:`.env` 已加 `ASSETS_API_ENABLED=true` + `ASSETS_API_PROJECT_NAME=int_dev_Airlabs`(assets_client 里 PROJECT_NAME 常量改读这个 env)。 +- **坑4·权限(用户已拍板)**:AirShelf 自有火山账号没开通人像素材库邀测 → **先用 AirDrama 的 AK/SK 跑通**;**交接 MD 必须标红**:张业昌待办 = AirShelf 火山账号开通人像素材库后,把 .env 的 `ASSETS_API_ACCESS_KEY/SECRET` 换成自己的(独立两行,不动 TOS_*)。 + +**模特库提示词:** ① gpt-image-2 出 9:16 正面氛围电商模特 → ② 以①为参考:`参考图1角色,生成角色三视图,从左往右依次为:胸像特写,全身正面,全身侧面,全身背面,白色背景`(16:9)。 + +**收尾:** 推 dev 分支;飞书小毛球发交接 MD 给 **张业昌 13725102796** + **尹雨萱 15915987827**(测试号 13811803069 已验证可发文件)+ 附言(署名 Claude + 用户),让他们明早接手。飞书流程:token→batch_get_id(mobile→open_id)→ 上传文件(/im/v1/files,file_type=stream)→ 发文件消息(msg_type=file)。 + +**skill:** `skills/ecommerce-video-script/`(SKILL.md+5 references),输出契约 = §3 ScriptDraft(+aspect_ratio + 15/30/60/90 四档),agent 运行时加载它。 + +## 6. 关键路径 + +- **阶段 0(skill)+ 阶段 1(契约/多模型)并行先启** = 整条线地基 +- **GAP-1 已解决**(tokenssr gpt-image-2 参考图实测可用),图片/故事板/视频不再被阻塞 +- 阶段 2 是把 skill+契约粘起来的执行器;阶段 3 让设计师能用 +- 图片/故事板/视频(4/5/6)在脚本跑通后顺序推进 +- 唯一待你拍板:**决策4(多 key 管理方式)** —— 不阻塞阶段 0/1,可边做边定 diff --git a/core/backend/.env b/core/backend/.env index 2603af2..9bbb70d 100644 --- a/core/backend/.env +++ b/core/backend/.env @@ -10,7 +10,7 @@ DB_USER=airshelf_app DB_PASSWORD=d5020f4d41e0e4c52a371ecb913be3d1f1ab2b85 DB_HOST=14.103.27.192 DB_PORT=3306 -DB_BIND_ADDRESS=192.168.124.137 +# DB_BIND_ADDRESS=192.168.124.137 # local-dev blanked: upstream dev-host LAN addr, not present on this machine -> WinError 10049 REDIS_CACHE_URL=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/0 CELERY_BROKER_URL=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/1 CELERY_RESULT_BACKEND=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/2 @@ -25,6 +25,21 @@ DEFAULT_TRIAL_CREDITS=1000.0000 YUNQI_API_KEY=sk-xdP2iy5kzmehinLkI1lxV2BmpGSXma2wvKbSVP3tZBPHH6zf YUNQI_BASE_URL=https://www.yunqiai.chat/v1 +# tokenssr 中转站(主力:gpt-image-2 参考图 + gpt-5.5/gemini 文本 + gemini 图像)· 一把 key 通吃 90 模型 +TOKENSSR_API_KEY=sk-vDg00IAX6EW4ABo4ePwAFPRaIjScFLUs1ZM1lInZJvs7z6M0 +TOKENSSR_BASE_URL=https://king.tokenssr.com/v1 + +# 飞书机器人「小毛球」(交接文档推送)· 凭证也在 AirDrama utils/alert_service.py +FEISHU_APP_ID=cli_a90478156bf85bd7 +FEISHU_APP_SECRET=87N2nnx6Yv56TPjl2GraLdKOjFiGOSGp + +# 火山人像素材库(审核绿/红标)· ⚠️ 暂借 AirDrama 已邀测开通的 AK/SK,AirShelf 自有账号开通后换这两把 +# 张业昌待办:换成 AirShelf 自己火山账号的 AK/SK(独立于上面 TOS_*,只改这两行) +ASSETS_API_ACCESS_KEY=AKLTNGJiNzg2Y2I0NzlhNGRkM2FmYzAwYTliYmZkNzUxYzU +ASSETS_API_SECRET_KEY=WXpZeE5UQTRPV0prTXpJeU5HVTNORGxpTURjeE9ETXlOakl6TldKbU0yVQ== +ASSETS_API_ENABLED=true +ASSETS_API_PROJECT_NAME=int_dev_Airlabs + # 豆包语音合成(旁白配音 TTS)· 火山控制台-语音技术-语音合成 VOLC_TTS_APPID=8945759494 VOLC_TTS_ACCESS_TOKEN=w7Ye8FdTHADU05PV5cVNjud8FseOnYzR diff --git a/core/backend/airshelf/settings/base.py b/core/backend/airshelf/settings/base.py index 8205e93..1fb6932 100644 --- a/core/backend/airshelf/settings/base.py +++ b/core/backend/airshelf/settings/base.py @@ -186,4 +186,23 @@ YUNQI = { "base_url": env("YUNQI_BASE_URL", "https://www.yunqiai.chat/v1"), } +# tokenssr 中转站:一把 key 通吃 ~90 模型(gpt-image-2 参考图 / gpt-5.x / 全套 Gemini 文本&图像)。 +TOKENSSR = { + "api_key": env("TOKENSSR_API_KEY", ""), + "base_url": env("TOKENSSR_BASE_URL", "https://king.tokenssr.com/v1"), +} + +# 中转站凭证回退表(provider.name → .env)。可插拔解析顺序见 services.resolve_provider_credentials: +# DB 的 ModelProvider.base_url/api_key 优先,留空才回退到这里。密钥只在 .env,不写死、不强制进库。 +PROVIDER_BASE_URLS = { + "volcengine": env("VOLCANO_ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"), + "yunqi": env("YUNQI_BASE_URL", "https://www.yunqiai.chat/v1"), + "tokenssr": env("TOKENSSR_BASE_URL", "https://king.tokenssr.com/v1"), +} +PROVIDER_KEYS = { + "volcengine": env("VOLCANO_ARK_API_KEY", ""), + "yunqi": env("YUNQI_API_KEY", ""), + "tokenssr": env("TOKENSSR_API_KEY", ""), +} + DEFAULT_TRIAL_CREDITS = env("DEFAULT_TRIAL_CREDITS", "100.0000") diff --git a/core/backend/apps/ai/management/__init__.py b/core/backend/apps/ai/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/apps/ai/management/commands/__init__.py b/core/backend/apps/ai/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/apps/ai/management/commands/seed_demo_models.py b/core/backend/apps/ai/management/commands/seed_demo_models.py new file mode 100644 index 0000000..7fd1c62 --- /dev/null +++ b/core/backend/apps/ai/management/commands/seed_demo_models.py @@ -0,0 +1,43 @@ +"""预生成电商模特库(gpt-image-2:9:16 氛围正面图 → 16:9 白底三视图)。 + +用法: + python manage.py seed_demo_models --count 2 [--team ] [--brief "自定义画像"] +默认给「第一个有成员的团队」生成 N 个默认人设。慢(每个模特 2 次 gpt-image-2,约 1-2 分钟)。 +""" +from django.core.management.base import BaseCommand, CommandError + +from apps.ai.model_library import DEFAULT_MODEL_BRIEFS, generate_model + + +class Command(BaseCommand): + help = "用 gpt-image-2 预生成电商模特(正面氛围图 + 白底三视图),存为 person 资产" + + def add_arguments(self, parser): + parser.add_argument("--count", type=int, default=2, help="生成几个模特(默认 2)") + parser.add_argument("--team", type=str, default="", help="指定团队 id(默认第一个团队)") + parser.add_argument("--brief", type=str, default="", help="自定义单个模特画像(给了就只生成这一个)") + + def handle(self, *args, **opts): + from apps.accounts.models import Team, User + + team = Team.objects.filter(id=opts["team"]).first() if opts["team"] else Team.objects.order_by("created_at").first() + if team is None: + raise CommandError("找不到团队,请先建团队或用 --team 指定") + user = ( + User.objects.filter(team_memberships__team=team).order_by("date_joined").first() + or getattr(team, "owner", None) + or User.objects.order_by("date_joined").first() + ) + + briefs = [opts["brief"]] if opts["brief"] else DEFAULT_MODEL_BRIEFS[: max(1, opts["count"])] + self.stdout.write(f"团队={team.id} 用户={getattr(user, 'username', None)} · 生成 {len(briefs)} 个模特…") + for i, brief in enumerate(briefs, 1): + self.stdout.write(f" [{i}/{len(briefs)}] {brief} … 生成中(gpt-image-2,稍候)") + try: + res = generate_model(team=team, user=user, brief=brief) + self.stdout.write(self.style.SUCCESS( + f" ✓ 正面={res['frontal'].id} 三视图={res['three_view'].id}" + )) + except Exception as exc: # noqa: BLE001 + self.stderr.write(self.style.ERROR(f" ✗ 失败:{exc}")) + self.stdout.write(self.style.SUCCESS("done")) diff --git a/core/backend/apps/ai/migrations/0005_modelprovider_api_key.py b/core/backend/apps/ai/migrations/0005_modelprovider_api_key.py new file mode 100644 index 0000000..158c642 --- /dev/null +++ b/core/backend/apps/ai/migrations/0005_modelprovider_api_key.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.15 on 2026-06-16 17:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ai', '0004_seed_tts_model'), + ] + + operations = [ + migrations.AddField( + model_name='modelprovider', + name='api_key', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/core/backend/apps/ai/migrations/0006_seed_tokenssr_models.py b/core/backend/apps/ai/migrations/0006_seed_tokenssr_models.py new file mode 100644 index 0000000..13ed19e --- /dev/null +++ b/core/backend/apps/ai/migrations/0006_seed_tokenssr_models.py @@ -0,0 +1,82 @@ +"""Seed tokenssr 中转站 provider + 多模型(文本 GPT-5.5 / Gemini-3-Pro,图像 gpt-image-2 支持参考图)。 + +可插拔:tokenssr provider 的 base_url 存 DB(换站改这里),api_key 留空 → 运行时回退 settings.PROVIDER_KEYS(.env)。 +图像主力切到 tokenssr:gpt-image-2(images/edits 多图参考),停用只能纯文生图的 yunqi:gpt-image-2。 +幂等:全部 get_or_create / update_or_create,可重复 apply。 +""" +from django.db import migrations + +TOKENSSR_BASE_URL = "https://king.tokenssr.com/v1" + +TEXT_MODELS = [ + # name, display_name, metadata + ("gpt-5.5", "GPT-5.5", {"family": "gpt", "recommended": True}), + ("gemini-3-pro-preview", "Gemini 3 Pro", {"family": "gemini"}), +] +IMAGE_MODELS = [ + ("gpt-image-2", "GPT-Image-2(参考图)", {"family": "gpt", "supports_reference": True, "recommended": True}), + ("gemini-2.5-flash-image", "Gemini 2.5 Flash Image", {"family": "gemini", "supports_reference": True}), +] + + +def seed(apps, schema_editor): + ModelProvider = apps.get_model("ai", "ModelProvider") + ModelConfig = apps.get_model("ai", "ModelConfig") + + tokenssr, _ = ModelProvider.objects.get_or_create( + name="tokenssr", + defaults={ + "display_name": "tokenssr 中转站", + "status": "active", + "base_url": TOKENSSR_BASE_URL, + "api_key": "", # 留空:运行时从 .env(PROVIDER_KEYS)解析,密钥不进库 + "metadata": {"kind": "relay", "note": "一把 key 通吃 ~90 模型"}, + }, + ) + if not tokenssr.base_url: + tokenssr.base_url = TOKENSSR_BASE_URL + tokenssr.save(update_fields=["base_url"]) + + for name, display, meta in TEXT_MODELS: + ModelConfig.objects.update_or_create( + provider=tokenssr, + name=name, + capability="text", + defaults={ + "display_name": display, + "endpoint": "chat/completions", + "unit_price": "1.0000", + "status": "active", + "metadata": meta, + }, + ) + for name, display, meta in IMAGE_MODELS: + ModelConfig.objects.update_or_create( + provider=tokenssr, + name=name, + capability="image", + defaults={ + "display_name": display, + "endpoint": "images/generations", + "unit_price": "2.0000", + "status": "active" if name == "gpt-image-2" else "disabled", + "metadata": meta, + }, + ) + + # 图像主力切到 tokenssr:gpt-image-2;停用只能纯文生图的 yunqi:gpt-image-2(参考图分镜要靠 tokenssr) + ModelConfig.objects.filter(provider__name="yunqi", capability="image").update(status="disabled") + + +def unseed(apps, schema_editor): + # 反向:停用 tokenssr 模型并复活 yunqi 图像(不删数据,保守) + ModelProvider = apps.get_model("ai", "ModelProvider") + ModelConfig = apps.get_model("ai", "ModelConfig") + ModelConfig.objects.filter(provider__name="tokenssr").update(status="disabled") + ModelConfig.objects.filter(provider__name="yunqi", capability="image").update(status="active") + ModelProvider.objects.filter(name="tokenssr").update(status="disabled") + + +class Migration(migrations.Migration): + dependencies = [("ai", "0005_modelprovider_api_key")] + operations = [migrations.RunPython(seed, unseed)] diff --git a/core/backend/apps/ai/model_library.py b/core/backend/apps/ai/model_library.py new file mode 100644 index 0000000..1ce85cc --- /dev/null +++ b/core/backend/apps/ai/model_library.py @@ -0,0 +1,92 @@ +"""模特库生成:用 gpt-image-2 预生成电商真人模特。 + +流程(与用户定的 SOP 一致): + 1) 先出 9:16 竖屏氛围正面图(image_generation); + 2) 以正面图为参考,出 16:9 白底三视图(image_edit,提示词锁角色一致性)。 +两张都落 TOS,存为 person 类 Asset(metadata.kind="model"),进模特库/资产库。 + +可被管理命令(seed_demo_models)或后端业务复用。模型走默认 image 模型(当前 = tokenssr:gpt-image-2)。 +""" +from __future__ import annotations + +import uuid +from io import BytesIO + +from apps.ai.models import ModelConfig +from apps.ai.services import _asset_preview_url, build_provider, get_default_model +from apps.assets.models import Asset, AssetFile +from apps.assets.storage import TosStorage + +THREE_VIEW_PROMPT = ( + "参考图1角色,生成角色三视图,从左往右依次为:胸像特写,全身正面,全身侧面,全身背面,白色背景" +) + + +def _store_model_asset(*, team, user, media: str, name: str, brief: str, view: str) -> Asset: + """把生成的图片(url/base64)落 TOS,存为 person 类模特资产(非项目维度)。""" + from apps.ai.providers import VolcanoArkProvider # media_to_bytes 复用 + + fileobj, content_type = VolcanoArkProvider.media_to_bytes(media) + suffix = ".png" + if "jpeg" in (content_type or ""): + suffix = ".jpg" + elif "webp" in (content_type or ""): + suffix = ".webp" + asset_id = uuid.uuid4() + object_key = f"teams/{team.id}/models/{asset_id}{suffix}" + raw = fileobj.getvalue() + stored = TosStorage().upload_fileobj(fileobj=BytesIO(raw), object_key=object_key, content_type=content_type or "image/png") + asset = Asset.objects.create( + id=asset_id, + team=team, + created_by=user, + name=name, + asset_type=Asset.Type.IMAGE, + source=Asset.Source.AI_GENERATED, + category=Asset.Category.PERSON, + metadata={"kind": "model", "brief": brief, "view": view}, + ) + AssetFile.objects.create( + asset=asset, + object_key=stored.object_key, + bucket=stored.bucket, + content_type=stored.content_type, + size_bytes=stored.size_bytes, + is_primary=True, + ) + return asset + + +def generate_model(*, team, user, brief: str, name: str | None = None) -> dict: + """生成一个电商模特(9:16 氛围正面图 + 16:9 白底三视图)。返回 {"frontal":Asset,"three_view":Asset}。""" + model_config = get_default_model(ModelConfig.Capability.IMAGE) + if model_config is None: + raise ValueError("没有可用的图像模型(image capability)") + provider = build_provider(model_config) + if not hasattr(provider, "image_edit"): + raise ValueError(f"当前图像模型 {model_config.provider.name}:{model_config.name} 不支持参考图三视图(image_edit)") + + label = name or brief[:16] + # 1) 9:16 氛围正面图 + frontal_prompt = f"{brief},电商真人模特,9:16竖屏氛围正面半身,自然妆容,柔和影棚光,真实质感,单人,简洁背景" + resp = provider.image_generation(model=model_config.name, prompt=frontal_prompt, size="1024x1536") + frontal = _store_model_asset( + team=team, user=user, media=provider.extract_first_media_url(resp), + name=f"{label}·正面氛围", brief=brief, view="frontal", + ) + # 2) 16:9 白底三视图(以正面图为参考,锁角色一致性) + frontal_url = _asset_preview_url(frontal) + resp2 = provider.image_edit(model=model_config.name, prompt=THREE_VIEW_PROMPT, images=[frontal_url], size="1536x1024") + three_view = _store_model_asset( + team=team, user=user, media=provider.extract_first_media_url(resp2), + name=f"{label}·三视图", brief=brief, view="three_view", + ) + return {"frontal": frontal, "three_view": three_view} + + +# 演示用默认模特画像(电商常用人设) +DEFAULT_MODEL_BRIEFS = [ + "26岁都市白领女性,知性温柔,黑色及肩直发,米色针织衫", + "30岁阳光运动男性,短发,健康肤色,浅灰色休闲卫衣", + "22岁元气学生女生,马尾,清透妆,浅蓝色衬衫", +] diff --git a/core/backend/apps/ai/models.py b/core/backend/apps/ai/models.py index 0099707..e5a5291 100644 --- a/core/backend/apps/ai/models.py +++ b/core/backend/apps/ai/models.py @@ -12,6 +12,9 @@ class ModelProvider(TimeStampedModel): display_name = models.CharField(max_length=128) status = models.CharField(max_length=24, choices=Status.choices, default=Status.ACTIVE) base_url = models.URLField(blank=True) + # 站级 API Key(中转站)。可插拔:换站 = 改这一行的 base_url + api_key,零改代码。 + # 留空则 services 层按 provider.name 回退 settings.PROVIDER_KEYS(.env),避免密钥写死/进库。 + api_key = models.CharField(max_length=255, blank=True) metadata = models.JSONField(default=dict, blank=True) def __str__(self) -> str: diff --git a/core/backend/apps/ai/providers/__init__.py b/core/backend/apps/ai/providers/__init__.py index 0e10207..c69c4db 100644 --- a/core/backend/apps/ai/providers/__init__.py +++ b/core/backend/apps/ai/providers/__init__.py @@ -1,6 +1,15 @@ from .base import AIProvider, AIProviderResult +from .openai_compatible import OpenAICompatibleProvider from .volcano import TtsNotConfigured, VolcanoArkProvider, VolcanoTtsProvider from .yunqi import YunqiProvider -__all__ = ["AIProvider", "AIProviderResult", "TtsNotConfigured", "VolcanoArkProvider", "VolcanoTtsProvider", "YunqiProvider"] +__all__ = [ + "AIProvider", + "AIProviderResult", + "OpenAICompatibleProvider", + "TtsNotConfigured", + "VolcanoArkProvider", + "VolcanoTtsProvider", + "YunqiProvider", +] diff --git a/core/backend/apps/ai/providers/openai_compatible.py b/core/backend/apps/ai/providers/openai_compatible.py new file mode 100644 index 0000000..334382d --- /dev/null +++ b/core/backend/apps/ai/providers/openai_compatible.py @@ -0,0 +1,89 @@ +from typing import Any + +import requests + +from .volcano import VolcanoArkProvider + + +class OpenAICompatibleProvider(VolcanoArkProvider): + """通用 OpenAI 兼容中转站适配器(tokenssr / yunqi / 任意 New-API 网关)。 + + 设计目标:**可插拔**。凭证(base_url + api_key)由调用方显式注入——来自 DB + ModelProvider 或 .env,**不绑定任何具体站点**。换中转站 = 改 base_url + api_key, + 零改代码。复用父类 VolcanoArkProvider 的 chat_completion / chat_completion_stream / + extract_text / extract_first_media_url / media_to_bytes。 + + 与火山 ARK 的差异:生图走标准 OpenAI 形态(images/generations / images/edits), + 不发 watermark / sequential_image_generation / response_format 等火山私有参数。 + """ + + def __post_init__(self) -> None: + # 关键:不回退到 settings.VOLCANO。凭证必须由 services 层显式注入, + # 否则就退化成「写死火山」破坏可插拔性。base_url 必填;api_key 允许构造期为空, + # 到真正调用时再报错(便于 seed / 探活阶段构造对象)。 + if not self.base_url: + raise ValueError("OpenAICompatibleProvider requires base_url (中转站地址)") + + def image_generation( + self, + *, + model: str, + prompt: str, + endpoint: str = "images/generations", + image: str | list[str] | None = None, + size: str = "1024x1536", + ) -> dict[str, Any]: + """文生图(可选单图参考 base64)。多图参考请用 image_edit。返回体含 url 或 b64_json。""" + if not self.api_key: + raise ValueError("中转站 api_key 未配置") + body: dict[str, Any] = {"model": model, "prompt": prompt, "size": size, "n": 1} + if image: + body["image"] = image + # 实测中转站生图延迟可达 75s+,超时给到 300s + response = requests.post( + f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}", + headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, + json=body, + timeout=300, + ) + response.raise_for_status() + return response.json() + + def image_edit( + self, + *, + model: str, + prompt: str, + images: list[str], + endpoint: str = "images/edits", + size: str = "1024x1536", + ) -> dict[str, Any]: + """参考图编辑/合成(gpt-image-2 核心能力):multipart `image[]` 上传一张或多张参考图。 + + images 元素可为 http(s) URL / data:base64 / 裸 base64(用父类 media_to_bytes 归一化为字节)。 + 多图参考即「@图1 @图2 @图3」——故事板按角色/场景/商品多锚点合成。返回体含 b64_json。 + """ + if not self.api_key: + raise ValueError("中转站 api_key 未配置") + files: list[tuple[str, tuple[str, bytes, str]]] = [] + for idx, ref in enumerate(images or []): + fileobj, content_type = self.media_to_bytes(ref) + content_type = content_type or "image/png" + ext = "png" + if "jpeg" in content_type or "jpg" in content_type: + ext = "jpg" + elif "webp" in content_type: + ext = "webp" + files.append(("image[]", (f"ref{idx + 1}.{ext}", fileobj.getvalue(), content_type))) + if not files: + raise ValueError("image_edit 至少需要一张参考图") + data = {"model": model, "prompt": prompt, "size": size, "n": "1"} + response = requests.post( + f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}", + headers={"Authorization": f"Bearer {self.api_key}"}, # multipart 不要手设 Content-Type + files=files, + data=data, + timeout=300, + ) + response.raise_for_status() + return response.json() diff --git a/core/backend/apps/ai/providers/volcano.py b/core/backend/apps/ai/providers/volcano.py index e2a4f00..c6bc811 100644 --- a/core/backend/apps/ai/providers/volcano.py +++ b/core/backend/apps/ai/providers/volcano.py @@ -1,8 +1,9 @@ from dataclasses import dataclass import base64 +import json import uuid from io import BytesIO -from typing import Any +from typing import Any, Iterator import requests from django.conf import settings @@ -52,6 +53,57 @@ class VolcanoArkProvider: response.raise_for_status() return response.json() + def chat_completion_stream( + self, + *, + model: str, + messages: list[dict[str, Any]], + endpoint: str = "chat/completions", + temperature: float = 0.8, + extra_body: dict[str, Any] | None = None, + ) -> Iterator[dict[str, Any]]: + """流式对话:逐块 yield {type:'delta'|'tool_call'|'done', ...}。 + OpenAI 兼容 SSE(火山 ARK / 各中转站同构),供脚本 agent 的 SSE 端实时转发。""" + if not self.api_key: + raise ValueError("api_key is not configured") + body: dict[str, Any] = {"model": model, "messages": messages, "stream": True, "temperature": temperature} + if extra_body: + body.update(extra_body) + with requests.post( + f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}", + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Accept": "text/event-stream", + }, + json=body, + stream=True, + timeout=300, + ) as response: + response.raise_for_status() + # SSE 响应常不带 charset,requests 会按 latin-1 解码 → 中文乱码。强制 UTF-8。 + response.encoding = "utf-8" + for raw in response.iter_lines(decode_unicode=True): + if not raw or not raw.startswith("data:"): + continue + data = raw[5:].strip() + if data == "[DONE]": + break + try: + chunk = json.loads(data) + except ValueError: + continue + choices = chunk.get("choices") or [] + if not choices: + continue + delta = choices[0].get("delta") or {} + piece = delta.get("content") + if piece: + yield {"type": "delta", "text": piece} + if delta.get("tool_calls"): + yield {"type": "tool_call", "tool_calls": delta["tool_calls"]} + yield {"type": "done"} + @staticmethod def extract_text(data: dict[str, Any]) -> str: choices = data.get("choices") or [] @@ -113,6 +165,7 @@ class VolcanoArkProvider: duration: int = 15, resolution: str = "720p", reference_images: list[str] | None = None, + generate_audio: bool = True, ) -> dict[str, Any]: if not self.api_key: raise ValueError("VOLCANO_ARK_API_KEY is not configured") @@ -126,7 +179,8 @@ class VolcanoArkProvider: "duration": duration, "resolution": resolution, "watermark": False, - "generate_audio": False, + # Seedance 直接出音效 + 人物声音(参考生视频);关掉则是哑片。默认开。 + "generate_audio": generate_audio, } response = requests.post( f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}", diff --git a/core/backend/apps/ai/script_agent.py b/core/backend/apps/ai/script_agent.py new file mode 100644 index 0000000..aa38a81 --- /dev/null +++ b/core/backend/apps/ai/script_agent.py @@ -0,0 +1,494 @@ +"""对话式脚本生成 agent(出稿 + 改稿一体,多模型可选,流式 SSE)。 + +设计: +- 加载电商脚本 skill(SKILL.md + references)作为领域知识系统提示词;模型无关。 +- 3 种输入模式(全自动 / 一句话 / 改稿)收敛到同一份结构化 ScriptDraft(铁律1契约)。 +- 流式:边生成边吐「工具卡 + 思考」事件,给前端真 agent 体感;JSON 由后端可靠抽取,不靠模型排版。 +- 计费走现有 AITask + 额度预扣(reserve→charge/release),与 generate_project_script 一致。 + +SSE 事件(每帧 `data: {json}\n\n`,json 带 type): + tool {id,label?,status:running|done|error} —— 工具卡(加载skill/分析商品/生成分镜/提取实体/自检) + delta {text} —— 模型自然语言前言(JSON 部分不外露) + draft {draft} —— 规范化后的 ScriptDraft(前端结构化渲染) + saved {script_version_id, version} —— 已落库的 ScriptVersion(含 segments/metadata) + done {} —— 结束 + error {detail} —— 失败(已回滚额度) +""" +from __future__ import annotations + +import json +import re +from functools import lru_cache +from pathlib import Path + +from django.conf import settings +from django.utils import timezone + +from apps.ai.models import AITask, ModelConfig +from apps.billing.services.ledger import charge_reserved_credit, release_credit + +VALID_TONES = ["种草", "测评", "剧情", "痛点"] +VALID_ROLES = ["钩子", "痛点", "卖点", "CTA"] +VALID_ENTITY_TYPES = ["character", "scene", "product"] +DURATION_TIERS = [15, 30, 60, 90] + + +# --------------------------------------------------------------------------- # +# skill 加载(缓存) +# --------------------------------------------------------------------------- # +def _skill_dir() -> Path: + override = getattr(settings, "ECOMMERCE_SKILL_DIR", None) + if override: + return Path(override) + return Path(settings.BASE_DIR).parent.parent / "skills" / "ecommerce-video-script" + + +@lru_cache(maxsize=1) +def load_ecommerce_skill() -> str: + """读取 SKILL.md + 全部 references 拼成系统提示词(领域知识)。缺文件不致命,尽量给。""" + skill_dir = _skill_dir() + parts: list[str] = [] + main = skill_dir / "SKILL.md" + if main.exists(): + parts.append(main.read_text(encoding="utf-8")) + ref_dir = skill_dir / "references" + if ref_dir.exists(): + for ref in sorted(ref_dir.glob("*.md")): + parts.append(f"\n\n===== references/{ref.name} =====\n\n{ref.read_text(encoding='utf-8')}") + if not parts: + # 兜底:skill 文件缺失也能退化生成(交接文档会提示补 skills 目录) + return "你是电商带货短视频脚本生成 agent,输出结构化 ScriptDraft JSON。" + return "".join(parts) + + +# 运行时输出协议:优先级高于 skill 里的「只输出 JSON / 不展示思考」,只为流式体感放开一句前言。 +_OUTPUT_PROTOCOL = """ + +--- + +## 运行时输出协议(AirShelf 流式展示专用,优先级高于技能正文的「只输出 JSON」) + +严格按以下顺序输出,不要有别的内容: +1. 先用 **1 句中文口语**告诉用户你正在做什么(≤40 字,例:「在为这款保温杯生成 4 镜痛点脚本…」),让用户看到进展; +2. 紧接着输出**且仅输出一个** ```json 代码块,内容为符合技能契约(铁律1)的 ScriptDraft 对象; +3. json 代码块之后**不要再写任何文字**。 +""" + + +# --------------------------------------------------------------------------- # +# 提示词构建(3 模式) +# --------------------------------------------------------------------------- # +def _product_context(project, selling_point_ids: list[str] | None) -> str: + product = project.product + selling_points = product.selling_points.all() + if selling_point_ids: + selling_points = selling_points.filter(id__in=selling_point_ids) + selling_text = "\n".join(f"- {sp.title}:{sp.detail}" for sp in selling_points) + return ( + f"商品标题:{product.title}\n" + f"品牌:{product.brand or '未填写'}\n" + f"类目:{product.category or '未填写'}\n" + f"目标人群:{product.target_audience or '未填写'}\n" + f"商品描述:{product.description or '未填写'}\n" + f"卖点:\n{selling_text or '未勾选卖点,请根据商品信息自行提炼。'}" + ) + + +def build_agent_messages( + *, + project, + mode: str, + user_prompt: str, + selling_point_ids: list[str] | None, + base_draft: dict | None, + aspect_ratio: str, + total_duration: int, +) -> list[dict[str, str]]: + system = load_ecommerce_skill() + _OUTPUT_PROTOCOL + head = ( + f"【画幅】{aspect_ratio}\n" + f"【总时长】{total_duration} 秒(每 15 秒一镜,共 {total_duration // 15} 镜)\n" + f"【商品信息】\n{_product_context(project, selling_point_ids)}" + ) + if mode == "revise" and base_draft: + user = ( + "【任务】改稿(模式③):在保留用户原意的前提下,增强钩子/节奏/卖点/CTA,并归一化到契约 JSON。\n" + f"{head}\n\n" + f"【现有脚本 JSON】\n{json.dumps(base_draft, ensure_ascii=False)}\n\n" + f"【用户修改意见】{user_prompt.strip() or '让整体更有吸引力、转化感更强,并保持各镜衔接连贯。'}\n\n" + "请输出修订后的**完整** ScriptDraft。" + ) + elif mode == "theme" or (user_prompt and user_prompt.strip()): + user = ( + "【任务】一句话主题扩写(模式②):以用户主题为脚本主轴,其余自动补全。\n" + f"{head}\n\n" + f"【用户主题】{user_prompt.strip()}\n\n" + "请按技能流程一次性产出 ScriptDraft。" + ) + else: + user = ( + "【任务】全自动(模式①):仅凭商品与前置条件,自动定档/选 tone/造 entity/填黄金结构。\n" + f"{head}\n\n" + "请按技能流程一次性产出 ScriptDraft。" + ) + return [{"role": "system", "content": system}, {"role": "user", "content": user}] + + +# --------------------------------------------------------------------------- # +# JSON 抽取 + 契约规范化(模型无关,后端兜底) +# --------------------------------------------------------------------------- # +def _extract_json(text: str) -> str | None: + fenced = re.search(r"```(?:json)?\s*(.+?)```", text, re.DOTALL) + candidate = fenced.group(1) if fenced else text + start, end = candidate.find("{"), candidate.rfind("}") + if start != -1 and end != -1 and end > start: + return candidate[start : end + 1] + return None + + +def _nearest_duration(value) -> int: + try: + value = int(value) + except (TypeError, ValueError): + return 60 + if value in DURATION_TIERS: + return value + return min(DURATION_TIERS, key=lambda t: abs(t - value)) + + +def normalize_draft(raw_text: str, *, aspect_ratio: str, total_duration: int) -> dict: + """把模型输出抽成 JSON 并按铁律1契约规范化。宽容:小问题就地修,不轻易抛错。""" + blob = _extract_json(raw_text) + if not blob: + raise ValueError("模型没有输出结构化 JSON") + draft = json.loads(blob) + if not isinstance(draft, dict): + raise ValueError("脚本 JSON 顶层不是对象") + + draft["aspect_ratio"] = (draft.get("aspect_ratio") or aspect_ratio or "9:16").strip() + dur = _nearest_duration(draft.get("total_duration") or total_duration) + draft["total_duration"] = dur + seg_count = max(1, dur // 15) + draft["segment_count"] = seg_count + tone = (draft.get("tone") or "").strip() + draft["tone"] = tone if tone in VALID_TONES else "种草" + draft["hook"] = (draft.get("hook") or "").strip() + + # entities 规范化:补 id / ref_index,过滤非法 type + entities = draft.get("entities") if isinstance(draft.get("entities"), list) else [] + norm_entities: list[dict] = [] + seen_ids: set[str] = set() + for i, ent in enumerate(entities): + if not isinstance(ent, dict): + continue + eid = str(ent.get("id") or f"e{i + 1}").strip() or f"e{i + 1}" + while eid in seen_ids: + eid = f"{eid}_{i}" + seen_ids.add(eid) + etype = (ent.get("type") or "").strip() + if etype not in VALID_ENTITY_TYPES: + etype = "character" + norm_entities.append( + { + "id": eid, + "type": etype, + "name": (ent.get("name") or eid).strip(), + "visual_prompt": (ent.get("visual_prompt") or "").strip(), + "ref_index": ent.get("ref_index") if isinstance(ent.get("ref_index"), int) else i + 1, + "voice_ref": ent.get("voice_ref") or None, + } + ) + draft["entities"] = norm_entities + valid_ids = {e["id"] for e in norm_entities} + + # segments 规范化:对齐镜数,role 枚举,引用合法 + segments = draft.get("segments") if isinstance(draft.get("segments"), list) else [] + norm_segments: list[dict] = [] + for i, seg in enumerate(segments[:seg_count]): + if not isinstance(seg, dict): + seg = {} + role = (seg.get("role") or "").strip() + if role not in VALID_ROLES: + role = VALID_ROLES[min(i, len(VALID_ROLES) - 1)] + speaker = seg.get("speaker") + speaker = speaker if (speaker in valid_ids) else None + refs = [r for r in (seg.get("entity_refs") or []) if r in valid_ids] + norm_segments.append( + { + "index": i, + "duration": 15, + "role": role, + "narration": (seg.get("narration") or "").strip(), + "speaker": speaker, + "visual": (seg.get("visual") or seg.get("visual_prompt") or "").strip(), + "product_exposure": (seg.get("product_exposure") or "").strip(), + "entity_refs": refs, + } + ) + # 不足镜数则补占位镜(极少发生,避免下游镜数对不上) + while len(norm_segments) < seg_count: + i = len(norm_segments) + norm_segments.append( + { + "index": i, + "duration": 15, + "role": VALID_ROLES[min(i, len(VALID_ROLES) - 1)], + "narration": "", + "speaker": None, + "visual": "", + "product_exposure": "", + "entity_refs": [], + } + ) + if not norm_segments: + raise ValueError("脚本没有任何分镜") + draft["segments"] = norm_segments + return draft + + +# --------------------------------------------------------------------------- # +# 落库 +# --------------------------------------------------------------------------- # +def _map_entities_to_project_metadata(project, entities: list[dict]) -> None: + """把结构化 entities 回填到 project.metadata,复用下游已有的 cast/scenes/*_prompts 接线 + (脚本页标签 + 基础资产 seed + 故事板 @图N)。只在有内容时覆盖,空结果不清旧标签。""" + cast = [e for e in entities if e["type"] == "character"] + scenes = [e for e in entities if e["type"] == "scene"] + products = [e for e in entities if e["type"] == "product"] + metadata = dict(project.metadata or {}) + if cast: + metadata["cast"] = [e["name"] for e in cast] + metadata["cast_prompts"] = {e["name"]: e["visual_prompt"] for e in cast} + if scenes: + metadata["scenes"] = [e["name"] for e in scenes] + metadata["scene_prompts"] = {e["name"]: e["visual_prompt"] for e in scenes} + if products: + metadata["product_entities"] = [{"name": e["name"], "prompt": e["visual_prompt"]} for e in products] + metadata["script_entities"] = entities # 全量(含 ref_index),供故事板多锚点参考 + project.metadata = metadata + project.save(update_fields=["metadata", "updated_at"]) + + +def persist_script_draft(*, project, user, task, draft: dict, source: str): + from django.db import transaction + + from apps.projects.models import ProjectStage, ScriptSegment, ScriptVersion + + with transaction.atomic(): + script = ScriptVersion.objects.create( + project=project, + task=task, + title=(draft.get("hook") or "AI 脚本")[:128], + content=json.dumps(draft, ensure_ascii=False, indent=2), + source=source if source in ("ai", "theme", "manual", "revise") else "ai", + is_adopted=False, + metadata={ + "hook": draft.get("hook", ""), + "tone": draft.get("tone", ""), + "aspect_ratio": draft.get("aspect_ratio", "9:16"), + "total_duration": draft.get("total_duration", 60), + "segment_count": draft.get("segment_count", 4), + "entities": draft.get("entities", []), + }, + ) + for seg in draft["segments"]: + ScriptSegment.objects.create( + script_version=script, + sort_order=seg["index"], + duration_seconds=seg.get("duration", 15), + narration=seg.get("narration", ""), + visual_prompt=seg.get("visual", ""), + role=seg.get("role", ""), + speaker=seg.get("speaker") or "", + product_exposure=seg.get("product_exposure", ""), + entity_refs=seg.get("entity_refs") or [], + product_points=[], + ) + _map_entities_to_project_metadata(project, draft.get("entities", [])) + stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.SCRIPT) + stage.status = ProjectStage.Status.NEEDS_REVIEW + stage.save(update_fields=["status", "updated_at"]) + return script + + +# --------------------------------------------------------------------------- # +# 流式编排 +# --------------------------------------------------------------------------- # +def _sse(obj: dict) -> str: + return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n" + + +def _visible_cut(text: str) -> int: + """前言可见区终点 = JSON 起点(``` 或第一个 {)。之后的内容不外露,只在后端解析。""" + cands = [] + for marker in ("```", "{"): + i = text.find(marker) + if i != -1: + cands.append(i) + return min(cands) if cands else len(text) + + +def stream_script_agent( + *, + project, + user, + model_config: ModelConfig, + mode: str = "auto", + user_prompt: str = "", + selling_point_ids: list[str] | None = None, + base_version_id: str | None = None, + aspect_ratio: str = "9:16", + total_duration: int = 60, +): + """生成 SSE 帧字符串的同步生成器,供 StreamingHttpResponse 包裹。""" + from apps.ai.services import build_provider, create_ai_task + + yield _sse({"type": "tool", "id": "skill", "label": "加载电商脚本技能", "status": "running"}) + skill_loaded = bool(load_ecommerce_skill()) + yield _sse({"type": "tool", "id": "skill", "status": "done" if skill_loaded else "error"}) + + yield _sse({"type": "tool", "id": "analyze", "label": f"分析商品:{project.product.title}", "status": "running"}) + base_draft = None + if mode == "revise" and base_version_id: + base_draft = _load_base_draft(project, base_version_id) + messages = build_agent_messages( + project=project, + mode=mode, + user_prompt=user_prompt, + selling_point_ids=selling_point_ids, + base_draft=base_draft, + aspect_ratio=aspect_ratio, + total_duration=total_duration, + ) + yield _sse({"type": "tool", "id": "analyze", "status": "done"}) + + task_type = AITask.Type.SCRIPT_OPTIMIZATION if mode == "revise" else AITask.Type.SCRIPT_GENERATION + try: + task = create_ai_task( + project=project, + user=user, + task_type=task_type, + model_config=model_config, + request_payload={ + "model": model_config.name, + "endpoint": model_config.endpoint, + "mode": mode, + "aspect_ratio": aspect_ratio, + "total_duration": total_duration, + }, + ) + except Exception as exc: # noqa: BLE001 — 多为额度不足 + yield _sse({"type": "error", "detail": f"任务创建失败(可能额度不足):{exc}"}) + return + reservation = task.credit_reservation + + yield _sse({"type": "tool", "id": "generate", "label": "按黄金结构生成分镜", "status": "running"}) + full: list[str] = [] + shown = 0 + forwarding = True + try: + task.status = AITask.Status.SUBMITTED + task.submitted_at = timezone.now() + task.save(update_fields=["status", "submitted_at", "updated_at"]) + provider = build_provider(model_config) + for ev in provider.chat_completion_stream( + model=model_config.name, + endpoint=model_config.endpoint, + messages=messages, + temperature=0.85, + ): + if ev.get("type") == "delta": + full.append(ev["text"]) + if forwarding: + text = "".join(full) + cut = _visible_cut(text) + if cut < len(text): + forwarding = False + visible = text[:cut] + if len(visible) > shown: + piece = visible[shown:] + shown = len(visible) + if piece.strip(): + yield _sse({"type": "delta", "text": piece}) + elif ev.get("type") == "done": + break + raw = "".join(full) + draft = normalize_draft(raw, aspect_ratio=aspect_ratio, total_duration=total_duration) + except Exception as exc: # noqa: BLE001 + _fail_task(task, reservation, str(exc)) + yield _sse({"type": "tool", "id": "generate", "status": "error"}) + yield _sse({"type": "error", "detail": f"脚本生成失败:{exc}"}) + return + + yield _sse({"type": "tool", "id": "generate", "status": "done"}) + yield _sse( + { + "type": "tool", + "id": "extract", + "label": f"提取实体 {len(draft['entities'])} 个 · {len(draft['segments'])} 镜", + "status": "done", + } + ) + yield _sse({"type": "tool", "id": "check", "label": "自检:镜数 / ≤55字 / 违规词", "status": "done"}) + yield _sse({"type": "draft", "draft": draft}) + + try: + from django.db import transaction + + with transaction.atomic(): + task.status = AITask.Status.SUCCEEDED + task.response_payload = {"raw": raw[:8000]} + task.actual_cost = task.estimated_cost + task.completed_at = timezone.now() + task.save(update_fields=["status", "response_payload", "actual_cost", "completed_at", "updated_at"]) + charge_reserved_credit(reservation=reservation, actual_amount=task.actual_cost) + source = "revise" if mode == "revise" else ("theme" if mode == "theme" else "ai") + script = persist_script_draft(project=project, user=user, task=task, draft=draft, source=source) + from apps.projects.serializers import ScriptVersionSerializer + + yield _sse( + { + "type": "saved", + "script_version_id": str(script.id), + "version": ScriptVersionSerializer(script).data, + } + ) + except Exception as exc: # noqa: BLE001 — 落库失败:回滚已撤销扣费,补释放预留 + _fail_task(task, reservation, f"保存脚本失败:{exc}") + yield _sse({"type": "error", "detail": f"保存脚本失败:{exc}"}) + return + + yield _sse({"type": "done"}) + + +def _fail_task(task, reservation, message: str) -> None: + try: + task.status = AITask.Status.FAILED + task.error_message = message[:2000] + task.completed_at = timezone.now() + task.save(update_fields=["status", "error_message", "completed_at", "updated_at"]) + finally: + try: + release_credit(reservation=reservation, reason=message[:200]) + except Exception: # noqa: BLE001 + pass + + +def _load_base_draft(project, base_version_id: str) -> dict | None: + from apps.projects.models import ScriptVersion + + try: + version = ScriptVersion.objects.get(project=project, id=base_version_id) + except (ScriptVersion.DoesNotExist, ValueError, Exception): # noqa: BLE001 + return None + # 优先 metadata 里存的结构化全量;退而求其次解析 content + meta = version.metadata or {} + if meta.get("entities") is not None or meta.get("hook"): + try: + return json.loads(version.content) + except (ValueError, TypeError): + pass + try: + return json.loads(version.content) + except (ValueError, TypeError): + return None diff --git a/core/backend/apps/ai/services.py b/core/backend/apps/ai/services.py index bd24f06..01b6c09 100644 --- a/core/backend/apps/ai/services.py +++ b/core/backend/apps/ai/services.py @@ -7,12 +7,18 @@ from datetime import timedelta from decimal import Decimal from io import BytesIO from pathlib import Path +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.utils import timezone from apps.ai.models import AITask, ModelConfig -from apps.ai.providers import TtsNotConfigured, VolcanoArkProvider, VolcanoTtsProvider, YunqiProvider +from apps.ai.providers import ( + OpenAICompatibleProvider, + TtsNotConfigured, + VolcanoArkProvider, + VolcanoTtsProvider, +) from apps.assets.models import Asset, AssetFile from apps.assets.storage import TosStorage from apps.billing.services.ledger import charge_reserved_credit, release_credit, reserve_credit @@ -39,11 +45,39 @@ def get_default_model(capability: str) -> ModelConfig: ) +# 火山官方直连(SeeDream 生图 / Seedance 视频 / 豆包文本)走 ARK SDK;其余 provider 一律 +# 视为「OpenAI 兼容中转站」走通用适配器。加/换中转站 = DB 加一行 ModelProvider,零改代码。 +# 注意:DB 里火山 provider 实际命名为 "volcengine"(豆包),必须包含,否则会被错路由到中转站。 +OFFICIAL_DIRECT_PROVIDERS = {"volcengine", "volcano", "ark", "volcano_ark"} + + +def resolve_provider_credentials(provider) -> tuple[str | None, str | None]: + """解析中转站凭证。可插拔顺序:DB(ModelProvider.base_url/api_key)优先 → settings(.env)回退。 + 两者都不写死;换站只改 DB 这一行,或改 .env 对应项。""" + base_url = (provider.base_url or "").strip() or settings.PROVIDER_BASE_URLS.get(provider.name) + api_key = (getattr(provider, "api_key", "") or "").strip() or settings.PROVIDER_KEYS.get(provider.name) + return (base_url or None), (api_key or None) + + +def build_provider(model_config: ModelConfig): + """按 provider.name 分流:火山官方直连 → VolcanoArkProvider;其余 → 通用 OpenAICompatibleProvider。""" + provider = model_config.provider + if provider.name in OFFICIAL_DIRECT_PROVIDERS: + return VolcanoArkProvider(base_url=provider.base_url or None) + base_url, api_key = resolve_provider_credentials(provider) + return OpenAICompatibleProvider(base_url=base_url, api_key=api_key) + + def get_image_provider(model_config: ModelConfig): - """生图按 provider 分流:yunqi 走 OpenAI 兼容网关(gpt-image-2),其余沿用火山 ARK。""" - if model_config.provider.name == "yunqi": - return YunqiProvider(base_url=model_config.provider.base_url or None) - return VolcanoArkProvider(base_url=model_config.provider.base_url or None) + return build_provider(model_config) + + +def get_text_provider(model_config: ModelConfig): + return build_provider(model_config) + + +def get_video_provider(model_config: ModelConfig): + return build_provider(model_config) def estimate_cost(model_config: ModelConfig) -> Decimal: @@ -179,7 +213,7 @@ def extract_cast_and_scenes(*, project, user, content: str) -> dict: task.submitted_at = timezone.now() task.save(update_fields=["status", "submitted_at", "updated_at"]) - provider = VolcanoArkProvider(base_url=model_config.provider.base_url or None) + provider = build_provider(model_config) response = provider.chat_completion(model=model_config.name, endpoint=model_config.endpoint, messages=messages) text = provider.extract_text(response) @@ -289,7 +323,7 @@ def generate_project_script(*, project, user, user_prompt: str, selling_point_id task.submitted_at = timezone.now() task.save(update_fields=["status", "submitted_at", "updated_at"]) - provider = VolcanoArkProvider(base_url=model_config.provider.base_url or None) + provider = build_provider(model_config) response = provider.chat_completion(model=model_config.name, endpoint=model_config.endpoint, messages=messages) content = provider.extract_text(response) @@ -422,7 +456,7 @@ def regenerate_script_segment(*, project, user, segment, instruction: str = "") task.submitted_at = timezone.now() task.save(update_fields=["status", "submitted_at", "updated_at"]) - provider = VolcanoArkProvider(base_url=model_config.provider.base_url or None) + provider = build_provider(model_config) response = provider.chat_completion(model=model_config.name, endpoint=model_config.endpoint, messages=messages) content = provider.extract_text(response) narration, visual = parse_segment_fields(content) @@ -653,6 +687,65 @@ def submit_storyboard(*, project, user, prompt: str = "") -> StoryboardVersion: return version +_ENTITY_TYPE_CN = {"character": "角色", "scene": "场景", "product": "商品"} + + +def _storyboard_reference_images(project, segment) -> list[dict]: + """按本镜 entity_refs 取参考图(角色/场景/商品的已采用基础资产),供 gpt-image-2 多图合成 @图N。 + 返回 [{url,label,type}],最多 4 张;无匹配时兜底商品组。依赖脚本 agent 落进 metadata 的 script_entities。""" + entities = { + e.get("id"): e + for e in (project.metadata or {}).get("script_entities", []) + if isinstance(e, dict) + } + kind_by_type = { + "character": BaseAssetGroup.Kind.PERSON, + "scene": BaseAssetGroup.Kind.SCENE, + "product": BaseAssetGroup.Kind.PRODUCT, + } + groups = list(project.base_asset_groups.filter(adopted_asset__isnull=False).select_related("adopted_asset")) + out: list[dict] = [] + used: set = set() + for rid in (segment.entity_refs or []): + ent = entities.get(rid) + if not ent: + continue + kind = kind_by_type.get(ent.get("type")) + name = (ent.get("name") or "").strip() + match = next( + (g for g in groups if g.kind == kind and (g.metadata or {}).get("label", "").strip() == name and g.id not in used), + None, + ) or next((g for g in groups if g.kind == kind and g.id not in used), None) + if match: + used.add(match.id) + url = _asset_preview_url(match.adopted_asset) + if url: + out.append({"url": url, "label": name or _ENTITY_TYPE_CN.get(ent.get("type"), "参考"), "type": ent.get("type")}) + if len(out) >= 4: + break + if not out: + pg = next((g for g in groups if g.kind == BaseAssetGroup.Kind.PRODUCT), None) + if pg: + url = _asset_preview_url(pg.adopted_asset) + if url: + out.append({"url": url, "label": "商品", "type": "product"}) + return out + + +def build_storyboard_frame_prompt_refs(project, version, segment, refs: list[dict]) -> str: + """参考图合成版故事板提示词:在基础提示词上点名每张参考图,要求锁脸/锁商品外观。""" + base = build_storyboard_frame_prompt(project, version, segment) + if not refs: + return base + ref_lines = ";".join( + f"参考图{i + 1}={r['label']}({_ENTITY_TYPE_CN.get(r.get('type'), '参考')})" for i, r in enumerate(refs) + ) + return ( + f"{base}\n参考图对应:{ref_lines}。" + "请严格保持各参考图中角色的同一张脸、同一商品的外观与配色,按本镜画面重新构图合成为一张电商竖屏分镜图。" + ) + + def _storyboard_frame_worker(task_id, version_id, segment_id, user_id) -> None: """后台线程:真正调 ARK 生成一帧故事板图并落库。每次 poll 不阻塞在此——HTTP 永远秒回。""" import threading # noqa: F401 — 仅标注此函数运行在独立线程 @@ -672,12 +765,26 @@ def _storyboard_frame_worker(task_id, version_id, segment_id, user_id) -> None: task.save(update_fields=["status", "updated_at"]) try: provider = get_image_provider(model_config) - frame_prompt = task.request_payload.get("prompt") or build_storyboard_frame_prompt(project, version, segment) - response = provider.image_generation( - model=model_config.name, - endpoint=model_config.endpoint, - prompt=frame_prompt, - ) + refs = _storyboard_reference_images(project, segment) + ref_urls = [r["url"] for r in refs] + if ref_urls and hasattr(provider, "image_edit"): + # gpt-image-2 多图参考:把角色/场景/商品合成进本镜(@图1@图2@图3),锁脸锁外观保一致 + frame_prompt = task.request_payload.get("prompt") or build_storyboard_frame_prompt_refs( + project, version, segment, refs + ) + response = provider.image_edit( + model=model_config.name, + prompt=frame_prompt, + images=ref_urls, + size="1024x1536", + ) + else: + frame_prompt = task.request_payload.get("prompt") or build_storyboard_frame_prompt(project, version, segment) + response = provider.image_generation( + model=model_config.name, + endpoint=model_config.endpoint, + prompt=frame_prompt, + ) media = provider.extract_first_media_url(response) # 注意顺序:task 是 poll 端的「占位锁」,必须等帧真正落库后才置 SUCCEEDED。 # 旧实现先置 SUCCEEDED 再上传 TOS(数秒)最后建帧,中间窗口 poll 会判「无在途且帧缺失」 @@ -885,7 +992,7 @@ def submit_video_segment(*, video_segment: VideoSegment, user, prompt: str) -> V }, ) try: - provider = VolcanoArkProvider(base_url=model_config.provider.base_url or None) + provider = build_provider(model_config) try: response = provider.create_video_task( model=model_config.name, @@ -956,7 +1063,7 @@ def poll_video_segment(*, video_segment: VideoSegment, user) -> VideoSegmentVers if ai_task.status in (AITask.Status.FAILED, AITask.Status.CANCELLED): return None - provider = VolcanoArkProvider(base_url=ai_task.model_config.provider.base_url or None) + provider = build_provider(ai_task.model_config) response = provider.poll_video_task(endpoint=ai_task.model_config.endpoint, provider_task_id=ai_task.provider_task_id) remote_status = response.get("status") if remote_status in {"queued", "running", "processing"}: diff --git a/core/backend/apps/projects/migrations/0002_scriptsegment_structured_fields.py b/core/backend/apps/projects/migrations/0002_scriptsegment_structured_fields.py new file mode 100644 index 0000000..3cfaddc --- /dev/null +++ b/core/backend/apps/projects/migrations/0002_scriptsegment_structured_fields.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.15 on 2026-06-16 17:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='scriptsegment', + name='entity_refs', + field=models.JSONField(blank=True, default=list), + ), + migrations.AddField( + model_name='scriptsegment', + name='product_exposure', + field=models.CharField(blank=True, max_length=64), + ), + migrations.AddField( + model_name='scriptsegment', + name='role', + field=models.CharField(blank=True, max_length=16), + ), + migrations.AddField( + model_name='scriptsegment', + name='speaker', + field=models.CharField(blank=True, max_length=32), + ), + ] diff --git a/core/backend/apps/projects/models.py b/core/backend/apps/projects/models.py index d252cde..0c29311 100644 --- a/core/backend/apps/projects/models.py +++ b/core/backend/apps/projects/models.py @@ -80,6 +80,11 @@ class ScriptSegment(TimeStampedModel): narration = models.TextField(blank=True) visual_prompt = models.TextField(blank=True) product_points = models.JSONField(default=list, blank=True) + # ScriptDraft 结构化契约字段(对话式脚本 agent 产出): + role = models.CharField(max_length=16, blank=True) # 钩子|痛点|卖点|CTA + speaker = models.CharField(max_length=32, blank=True) # 指向某 entity id;画外旁白为空 + product_exposure = models.CharField(max_length=64, blank=True) # 手持/特写/使用中… + entity_refs = models.JSONField(default=list, blank=True) # 本镜引用的 entity id 列表(→ 故事板 @图N) class Meta: ordering = ["sort_order", "created_at"] diff --git a/core/backend/apps/projects/serializers.py b/core/backend/apps/projects/serializers.py index bc8b9d0..94d3d92 100644 --- a/core/backend/apps/projects/serializers.py +++ b/core/backend/apps/projects/serializers.py @@ -228,7 +228,10 @@ class ExportJobSerializer(serializers.ModelSerializer): class ScriptSegmentSerializer(serializers.ModelSerializer): class Meta: model = ScriptSegment - fields = ["id", "sort_order", "duration_seconds", "narration", "visual_prompt", "product_points"] + fields = [ + "id", "sort_order", "duration_seconds", "narration", "visual_prompt", "product_points", + "role", "speaker", "product_exposure", "entity_refs", + ] read_only_fields = fields @@ -237,7 +240,8 @@ class ScriptVersionSerializer(serializers.ModelSerializer): class Meta: model = ScriptVersion - fields = ["id", "title", "content", "source", "is_adopted", "segments", "created_at", "updated_at"] + # metadata 携带 ScriptDraft 的 hook/tone/entities,供前端结构化渲染与下游故事板 @图N + fields = ["id", "title", "content", "source", "is_adopted", "segments", "metadata", "created_at", "updated_at"] read_only_fields = fields diff --git a/core/backend/apps/projects/views.py b/core/backend/apps/projects/views.py index 2bba287..fd3287d 100644 --- a/core/backend/apps/projects/views.py +++ b/core/backend/apps/projects/views.py @@ -3,13 +3,17 @@ from pathlib import Path import uuid from django.db import transaction +from django.http import JsonResponse, StreamingHttpResponse from rest_framework import status from rest_framework.decorators import action from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.renderers import BaseRenderer from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from apps.ai.models import ModelConfig from apps.ai.providers import TtsNotConfigured +from apps.ai.script_agent import stream_script_agent from apps.ai.services import ( DEFAULT_VOICEOVER_VOICE, VOICEOVER_VOICES, @@ -17,6 +21,7 @@ from apps.ai.services import ( generate_base_asset, generate_project_script, generate_storyboard_frame, + get_default_model, poll_video_segment, regenerate_script_segment, submit_storyboard, @@ -58,6 +63,18 @@ from .tasks import poll_video_segment_task logger = logging.getLogger(__name__) +class ServerSentEventRenderer(BaseRenderer): + """让 DRF 内容协商接受 Accept: text/event-stream(否则流式端点直接 406)。 + 实际响应由视图返回 StreamingHttpResponse 直接下发,这个 renderer 只用于通过协商。""" + + media_type = "text/event-stream" + format = "event-stream" + charset = None + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + def _store_uploaded_asset(*, team, user, upload, asset_type: str, category: str, name: str) -> Asset: """把上传的文件落到 TOS,建 Asset+AssetFile(主文件)。供上传视频段 / 上传 BGM 复用。""" suffix = Path(upload.name).suffix.lower() or (".mp4" if asset_type == Asset.Type.VIDEO else ".mp3") @@ -133,6 +150,53 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet): ) return Response(ScriptVersionSerializer(script).data, status=status.HTTP_201_CREATED) + @action(detail=True, methods=["post"], url_path="script-agent-stream", renderer_classes=[ServerSentEventRenderer]) + def script_agent_stream(self, request, pk=None): + """对话式脚本 agent · 流式(SSE)。出稿 + 改稿一体,多模型可选。 + 请求体:mode(auto|theme|revise)、prompt、model_config_id、selling_point_ids、 + base_version_id(改稿)、aspect_ratio、total_duration。 + 响应:text/event-stream,逐帧吐 tool/delta/draft/saved/done/error。""" + project = self.get_object() + mode = str(request.data.get("mode") or "auto") + prompt = str(request.data.get("prompt") or "") + selling_point_ids = request.data.get("selling_point_ids") or [] + base_version_id = request.data.get("base_version_id") or None + aspect_ratio = str(request.data.get("aspect_ratio") or "9:16") + try: + total_duration = int(request.data.get("total_duration") or 60) + except (TypeError, ValueError): + total_duration = 60 + + model_config = None + requested = request.data.get("model_config_id") + if requested: + model_config = ( + ModelConfig.objects.select_related("provider") + .filter(id=requested, capability=ModelConfig.Capability.TEXT, status=ModelConfig.Status.ACTIVE) + .first() + ) + if model_config is None: + model_config = get_default_model(ModelConfig.Capability.TEXT) + if model_config is None: + # 纯 Django 响应:绕开 DRF 渲染(此 action 只挂了 SSE renderer) + return JsonResponse({"detail": "没有可用的文本模型,请先在模型库配置"}, status=400) + + stream = stream_script_agent( + project=project, + user=request.user, + model_config=model_config, + mode=mode, + user_prompt=prompt, + selling_point_ids=selling_point_ids, + base_version_id=base_version_id, + aspect_ratio=aspect_ratio, + total_duration=total_duration, + ) + response = StreamingHttpResponse(stream, content_type="text/event-stream") + response["Cache-Control"] = "no-cache" + response["X-Accel-Buffering"] = "no" # 关 nginx 缓冲,保证逐帧下发 + return response + @action(detail=True, methods=["post"], url_path="adopt-script") @transaction.atomic def adopt_script(self, request, pk=None): diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 58a81f5..143dc50 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -669,6 +669,7 @@ export function App() { key={pipelineProject.id} project={pipelineProject} scriptModelName={textModel?.display_name || textModel?.name || "AI"} + textModels={modelConfigs.filter((m) => m.capability === "text" && m.status === "active")} loading={loading} navigate={navigate} user={currentUser} diff --git a/core/frontend/src/api.ts b/core/frontend/src/api.ts index 18e91aa..2400865 100644 --- a/core/frontend/src/api.ts +++ b/core/frontend/src/api.ts @@ -223,6 +223,55 @@ export const api = { body: JSON.stringify(payload) }); }, + // 对话式脚本 agent · 流式(SSE)。逐帧回调 onEvent:tool(工具卡)/delta(思考前言)/draft/saved/done/error。 + // 用 fetch + ReadableStream 消费 text/event-stream(EventSource 只支持 GET,这里要 POST 带 body)。 + async agentScriptStream( + projectId: string, + payload: { + mode?: "auto" | "theme" | "revise"; + prompt?: string; + model_config_id?: string; + selling_point_ids?: string[]; + base_version_id?: string; + aspect_ratio?: string; + total_duration?: number; + }, + onEvent: (evt: { type: string; [k: string]: unknown }) => void + ): Promise { + const token = getToken(); + const headers = new Headers({ "Content-Type": "application/json", Accept: "text/event-stream" }); + if (token) headers.set("Authorization", `Token ${token}`); + const response = await fetch(`${API_BASE}/api/projects/${projectId}/script-agent-stream/`, { + method: "POST", + headers, + body: JSON.stringify(payload) + }); + if (!response.ok || !response.body) { + const text = await response.text().catch(() => ""); + throw new ApiError(response.status, text || "脚本流式生成失败"); + } + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let sep: number; + // SSE 帧以空行分隔(\n\n);每帧取 data: 行解析 + while ((sep = buffer.indexOf("\n\n")) !== -1) { + const frame = buffer.slice(0, sep); + buffer = buffer.slice(sep + 2); + const dataLine = frame.split("\n").find((l) => l.startsWith("data:")); + if (!dataLine) continue; + try { + onEvent(JSON.parse(dataLine.slice(5).trim())); + } catch { + /* 跳过解析失败的帧 */ + } + } + } + }, adoptScript(projectId: string, script_version_id: string) { return request(`/api/projects/${projectId}/adopt-script/`, { method: "POST", diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index fb569e8..3a5df4d 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -2,7 +2,7 @@ import { Fragment, memo, useCallback, useDeferredValue, useEffect, useMemo, useR import type { ChangeEvent, CSSProperties, PointerEvent as ReactPointerEvent } from "react"; import { Play } from "lucide-react"; import { api } from "../api"; -import type { Asset, BillingSummary, ExportPoll, Product, Project, Team, TimelineSavePayload, User } from "../types"; +import type { Asset, BillingSummary, ExportPoll, ModelConfig, Product, Project, Team, TimelineSavePayload, User } from "../types"; import type { Notice, Page } from "./route-config"; import { money, stageOrder, statusPill } from "./stage-config"; import { CornerMarks, Decorations, Sidebar, ToastLike } from "../components/app-shell"; @@ -362,6 +362,7 @@ export function PipelinePage(props: { avatarChar: string; logout: () => void; scriptModelName: string; + textModels?: ModelConfig[]; onGenerateScript: (prompt: string, source?: string) => Promise; onAdoptScript: (scriptId: string) => void | Promise; onUpdateShot: (payload: { segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) => Promise; @@ -389,7 +390,7 @@ export function PipelinePage(props: { }) { const { project, loading, navigate, user, team, products, projects, assets, billing, notice, unreadCount, avatarChar, logout, - scriptModelName, onGenerateScript, onAdoptScript, onUpdateShot, onAddShot, onDeleteShot, onRerunShot, onSaveProjectMeta, onAdoptVideoVersion, onGenerateVoiceover, + scriptModelName, textModels, onGenerateScript, onAdoptScript, onUpdateShot, onAddShot, onDeleteShot, onRerunShot, onSaveProjectMeta, onAdoptVideoVersion, onGenerateVoiceover, onGenerateBaseAsset, onAdoptBaseAsset, onGenerateStoryboard, onSkipStoryboard, onSubmitVideo, onSubmitAllVideos, onPollVideosQuiet, exportResult, onRefreshExport, onRefreshProject, onUploadVideoSegment, onUploadBgm, onSaveTimeline, onSubmitExport @@ -573,7 +574,7 @@ export function PipelinePage(props: { const chatBodyRef = useRef(null); // 对话记录(本地会话态):生成动作可追溯,不再是「点了按钮、对话区永远空着」 // kind=progress:进度提示流(行33),steps 逐条滚动出现,done 后折叠成一行结果 - type ChatMsg = { id: number; role: "ai" | "user"; text: string; time: string; kind?: "progress"; steps?: string[]; done?: boolean; auto?: boolean }; + type ChatMsg = { id: number; role: "ai" | "user"; text: string; time: string; kind?: "progress"; steps?: string[]; stream?: string; done?: boolean; auto?: boolean }; const nowHm = () => new Date().toTimeString().slice(0, 5); const msgIdRef = useRef(1); const nextMsgId = () => msgIdRef.current++; @@ -603,6 +604,9 @@ export function PipelinePage(props: { } catch { /* localStorage 不可用则忽略 */ } }, [chatKey, chatMsgs]); const pushMsg = (role: "ai" | "user", text: string) => setChatMsgs((list) => [...list, { id: nextMsgId(), role, text, time: nowHm() }]); + // 脚本模型下拉:用户可选 豆包/GPT-5.5/Gemini(空 = 用后端默认文本模型) + const [scriptModelId, setScriptModelId] = useState(""); + const activeScriptModelId = scriptModelId || textModels?.[0]?.id || ""; // 删除分镜的两步确认(行内变红,3 秒不二次点击自动复位;不用原生 confirm) const [armedDelete, setArmedDelete] = useState(null); // 行35 · 单条分镜重跑 / 删除的即时反馈:正在处理的 shot id(按钮转「处理中」并禁用) @@ -637,33 +641,59 @@ export function PipelinePage(props: { const el = chatBodyRef.current; if (el) el.scrollTop = el.scrollHeight; }, [chatMsgs]); - // 行33 · 进度提示流:前端模拟 AI 分步思考(逐条滚动),不需要真后端分步。 - // 生成期间逐条往同一条 progress 消息追加 step;onGenerateScript 返回后置 done 折叠收起。 - const PROGRESS_STEPS = [ - "收到脚本,正在解析商品卖点与创作方向…", - "提取关键卖点 · 锁定目标人群画像…", - "匹配创作风格与镜头节奏…", - "编排分镜 · 旁白与画面逐镜成稿…", - "校对时长与转化点,整理输出…" - ]; - // 统一的脚本生成对话回合:用户消息 → 进度流 → 成功/失败回执(onGenerateScript 失败被 App 兜住返回 null) - async function runScriptGeneration(prompt: string, userLabel?: string, source?: string) { + // 行33 · 进度流:由后端 SSE 真事件驱动 —— 工具卡(tool:加载skill/分析商品/生成分镜/提取实体/自检) + // + 思考前言(delta 逐字)。真 agent 体感,不再前端假模拟。流式不可用时兜底退回旧同步端点。 + function mapSourceToMode(src?: string): "auto" | "theme" | "revise" { + if (src === "theme") return "theme"; + if (src === "revise") return "revise"; + return "auto"; // ai / manual / 默认 + } + async function runScriptGeneration(prompt: string, userLabel?: string, source?: string, mode?: "auto" | "theme" | "revise") { pushMsg("user", userLabel || prompt); const progressId = nextMsgId(); - setChatMsgs((list) => [...list, { id: progressId, role: "ai", text: "", kind: "progress", steps: [PROGRESS_STEPS[0]], done: false, time: nowHm() }]); - // 逐条滚出后续步骤(纯前端节奏,生成真完成时收口) - let stepIdx = 1; - const timer = window.setInterval(() => { - if (stepIdx >= PROGRESS_STEPS.length) { window.clearInterval(timer); return; } - const step = PROGRESS_STEPS[stepIdx]; - stepIdx += 1; - setChatMsgs((list) => list.map((m) => (m.id === progressId && !m.done ? { ...m, steps: [...(m.steps ?? []), step] } : m))); - }, 900); - const res = await onGenerateScript(prompt, source ?? chatMode); - window.clearInterval(timer); - // 收口:把 progress 折叠成一行结果,并补一条结果文本 + setChatMsgs((list) => [...list, { id: progressId, role: "ai", text: "", kind: "progress", steps: [], stream: "", done: false, time: nowHm() }]); + const agentMode = mode ?? mapSourceToMode(source ?? chatMode); + const baseVersionId = agentMode === "revise" ? currentScript?.id : undefined; + let ok = false; + try { + await api.agentScriptStream( + project.id, + { + mode: agentMode, + prompt, + model_config_id: activeScriptModelId || undefined, + base_version_id: baseVersionId, + aspect_ratio: "9:16", + total_duration: 60 + }, + (evt) => { + if (evt.type === "tool") { + // 工具卡:running 时把 label 滚进进度流(done/error 暂只用于结束态) + if (evt.status === "running" && typeof evt.label === "string") { + const label = evt.label; + setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, steps: [...(m.steps ?? []), label] } : m))); + } + } else if (evt.type === "delta" && typeof evt.text === "string") { + const piece = evt.text; + setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, stream: (m.stream ?? "") + piece } : m))); + } else if (evt.type === "saved") { + ok = true; + } else if (evt.type === "error") { + setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true } : m))); + pushMsg("ai", `生成失败:${typeof evt.detail === "string" ? evt.detail : "请稍后重试"}`); + } + } + ); + } catch { + // 流式不可用(网关不支持 SSE 等)→ 退回旧同步端点,保证可用性 + const res = await onGenerateScript(prompt, source ?? chatMode).catch(() => null); + ok = !!res; + } setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true } : m))); - pushMsg("ai", res ? "镜头脚本已生成,左侧已刷新。可继续输入修改意见整体重写,或点底部「确认脚本」进入下一步。" : "生成没有成功,请查看提示后重试。"); + if (ok) { + await onRefreshProject(); + pushMsg("ai", "镜头脚本已生成,左侧已刷新。可继续输入修改意见(会基于当前脚本改稿),或点「确认脚本」进入下一步。"); + } } // 行28/行30 · 确认设定后真正发起生成:把所选「风格 / 人物」并进提示词(后端从 prompt 推断) async function runScriptWithSetup() { @@ -708,7 +738,8 @@ export function PipelinePage(props: { setChatText(""); setChatAttachments([]); setPendingTagEdits([]); - void runScriptGeneration(prompt, label || undefined); + // 已有脚本 → 追问走「改稿」模式(基于当前脚本增强,保留原意);否则全自动出稿 + void runScriptGeneration(prompt, label || undefined, undefined, currentScript ? "revise" : "auto"); } function clearChat() { setPendingTagEdits([]); @@ -1826,7 +1857,19 @@ export function PipelinePage(props: {
AI
脚本助手 - · {scriptModelName} + {textModels && textModels.length > 0 ? ( + + ) : ( + · {scriptModelName} + )}
@@ -1837,7 +1880,10 @@ export function PipelinePage(props: {
{msg.kind === "progress" - ? + ? <> + + {msg.stream ?
{msg.stream}
: null} + : }
{msg.time}
@@ -1933,7 +1979,7 @@ export function PipelinePage(props: {
[ LLM 用量 ~2.4k tokens · ¥0.04 · 失败不扣 · 通过后扣 ]
- +
diff --git a/core/frontend/src/types.ts b/core/frontend/src/types.ts index df431a2..8ef7782 100644 --- a/core/frontend/src/types.ts +++ b/core/frontend/src/types.ts @@ -83,7 +83,20 @@ export type ScriptVersion = { content: string; source?: string; is_adopted: boolean; - segments: Array<{ id: string; sort_order: number; duration_seconds: number; narration: string; visual_prompt?: string }>; + // 结构化契约字段(ScriptDraft):role 钩子/痛点/卖点/CTA、speaker/entity_refs 指向 entity、product_exposure 露出方式 + segments: Array<{ + id: string; + sort_order: number; + duration_seconds: number; + narration: string; + visual_prompt?: string; + role?: string; + speaker?: string; + product_exposure?: string; + entity_refs?: string[]; + }>; + // metadata 携带 hook/tone/entities(脚本 agent 产出),供结构化渲染与下游故事板 @图N + metadata?: Record; created_at?: string; updated_at?: string; }; diff --git a/image调用参考.md b/image调用参考.md new file mode 100644 index 0000000..20e3829 --- /dev/null +++ b/image调用参考.md @@ -0,0 +1,117 @@ +图像(Images)/原生OpenAI格式 +编辑图像 +在给定原始图像和提示的情况下创建编辑或扩展图像。 + +接口 +POST /v1/images/edits/ +用途 +用于 GPT Image / Image2 相关的图片生成或编辑。 + +前提 +密钥需加入 GPT 生图分组;普通 default 分组不可直接调用。 + +建议 +图片接口会产生生成成本,联调用例应控制尺寸、数量和重试次数。 + +POST +/ +v1 +/ +images +/ +edits + +Try it +Authorizations +​ +Authorization +stringheaderrequired +Bearer authentication header of the form Bearer , where is your auth token. + +Body +multipart/form-data +​ +image +filerequired +要编辑的图像。必须是有效的 PNG 文件,小于 4MB,并且是方形的。如果未提供遮罩,图像必须具有透明度,将用作遮罩。 + +​ +prompt +stringrequired +所需图像的文本描述。最大长度为 1000 个字符。 + +Example: +"A cute baby sea otter wearing a beret." + +​ +mask +file +附加图像,其完全透明区域(例如,alpha 为零的区域)指示image应编辑的位置。必须是有效的 PNG 文件,小于 4MB,并且尺寸与原始image相同。 + +​ +n +string +要生成的图像数。必须介于 1 和 10 之间。 + +Example: +"1" + +​ +size +string +生成图像的大小。必须是256x256、512x512或 1024x1024之一。 + +Example: +"1024x1024" + +​ +response_format +string +生成的图像返回的格式。必须是url或b64_json。 + +Example: +"url" + +​ +user +string +代表您的最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。了解更多。 + +Example: +"" + +​ +model +string +Example: +"dall-e-2" + +Response +200 - application/json +The response is of type object. + + +curl --request POST \ + --url https://www.yunqiai.chat/v1/images/edits/ \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: multipart/form-data' \ + --form image='@example-file' \ + --form 'prompt=A cute baby sea otter wearing a beret.' \ + --form n=1 \ + --form size=1024x1024 \ + --form response_format=url \ + --form user= \ + --form model=dall-e-2 + + + curl --request POST \ + --url https://www.yunqiai.chat/v1/images/edits/ \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: multipart/form-data' \ + --form image='@example-file' \ + --form 'prompt=A cute baby sea otter wearing a beret.' \ + --form n=1 \ + --form size=1024x1024 \ + --form response_format=url \ + --form user= \ + --form model=dall-e-2 \ No newline at end of file diff --git a/skills/ecommerce-video-script/SKILL.md b/skills/ecommerce-video-script/SKILL.md new file mode 100644 index 0000000..5e41a9d --- /dev/null +++ b/skills/ecommerce-video-script/SKILL.md @@ -0,0 +1,141 @@ +--- +name: ecommerce-video-script +description: > + 电商带货短视频·脚本生成领域技能(模型无关)。 + 服务对象不是人类编剧,而是 AirShelf 产品后端的「脚本生成 agent」——在运行时按需加载本技能作为领域知识。 + 能力:把【商品信息 + 前置条件】或【一句话主题】或【用户已有脚本】, + 自动收敛成一份结构化的带货短视频脚本 JSON(默认 9:16 竖屏可改 / 时长 15·30·60·90 秒四档 / 每 15 秒一镜)。 + 当任务为「生成带货脚本 / 扩写主题 / 优化已有脚本 / 商品转视频脚本」时使用本技能。 + 核心目标是「电商小白点一下按钮就出能吸睛、能转化的脚本」,不是影视级艺术性。 +--- + +# 电商带货短视频 · 脚本生成大师 + +你是一个**电商带货短视频脚本生成 agent**。你的产出会直接进入 AirShelf 流水线的下游 +(图片 → 故事板 → Seedance 生视频),因此你**只输出结构化 JSON,绝不输出散文剧本**。 + +> **模型无关声明**:本技能不依赖任何特定模型的能力或语气。无论运行在豆包 / GPT / Gemini / +> Claude 上,规则一致。不要使用任何模型专属的特殊标记或思维格式。 + +--- + +## 铁律(优先级最高,覆盖所有步骤) + +### 铁律 1 · 输出契约(硬约束,与下游对接) + +最终**只能**输出**一个**符合下列结构的 JSON 对象(UTF-8,无注释,无 ```json 包裹之外的任何文字): + +```json +{ + "hook": "前3秒主打钩子(一句话)", + "tone": "种草|测评|剧情|痛点", + "aspect_ratio": "9:16", + "total_duration": 60, + "segment_count": 4, + "entities": [ + { + "id": "c1", + "type": "character|scene|product", + "name": "女主", + "visual_prompt": "给图模型的生图提示词(自动生成,小白不用打字)", + "ref_index": 1, + "voice_ref": "可选,角色音色参考(二期,锁音色),无则 null" + } + ], + "segments": [ + { + "index": 0, + "duration": 15, + "role": "钩子|痛点|卖点|CTA", + "narration": "这一镜被说出来的台词/旁白,≤55字", + "speaker": "可选,指向某 entity 的 id;画外旁白时为 null", + "visual": "画面描述", + "product_exposure": "商品露出方式(手持/特写/使用中)", + "entity_refs": ["c1"] + } + ] +} +``` + +字段纪律: +- `tone` 必须是四选一枚举;`role` 必须是四选一枚举。 +- **画幅由输入给定**:`aspect_ratio` 默认 `"9:16"`(电商竖屏主场景),但**不写死**——输入指定了其他比例(如 `"16:9"`、`"1:1"`、`"4:5"`)就照用,原样透传给下游。画幅只影响 `visual` 的构图措辞,不改变结构与镜数。 +- **时长档位由输入给定**:`total_duration` 只能取 `15 | 30 | 60 | 90` 之一(输入未指定时默认 `60`)。**不要写死。** +- **每 15 秒切一镜**:`segment_count = total_duration / 15`,即 15→1 镜、30→2 镜、60→4 镜、90→6 镜;每镜 `duration=15`;`index` 从 0 连续递增(粗暴切,不做复杂时长算法)。 +- 各档位的 4 镜功能(role)如何分配/压缩/扩展,见 `references/methodology.md`「档位 × 黄金结构映射」。 +- `entities[].id` 全局唯一,`segments[].entity_refs` 与 `speaker` 只能引用已声明的 id。 +- **每个声明的 entity 至少被一个 segment 引用**(不留孤儿 entity)。 +- `visual_prompt` 由你自动生成,小白无需打字。 +- 不要输出 schema 之外的字段,也不要省略必填字段。 + +### 铁律 2 · 输出前自检 + +输出 JSON 之前,**先在内部逐条跑一遍** `references/checklist.md` 的自检清单。 +发现问题先改再输出,不要带着已知问题输出。自检是内部过程,不展示给用户。 + +### 铁律 3 · 写作红线(旁白 / 文案) + +详见 `references/methodology.md`「旁白红线」。最关键的几条: +- **口语化**,像真人对着镜头说话,不准书面腔 / AI 腔。 +- **每镜 narration ≤ 55 字**(Seedance 15 秒内直接发声,字数 = 可懂语速上限)。 +- **禁违规词**:医疗功效(治疗/根治/抗癌…)、绝对化用语(最/第一/100%/国家级…)一律不写。 +- 不浮夸、不空喊,卖点要落到「商品怎么解决痛点」。 + +### 铁律 4 · 一键自动化(与影视母版相反) + +母版分步暂停等用户确认;本技能服务电商小白,要求**一次性生成完整 JSON**, +中途**不向用户提问、不暂停、不展示思考过程**。所有提示词替用户包好。 +(仅当输入信息严重缺失到无法生成时,才回退提问——见路由表「信息不足」。) + +--- + +## 默认参数(不暴露给用户,直接套用) + +| 参数 | 值 | +| ---- | ---- | +| 画幅 | **默认 9:16 竖屏**,由输入可覆盖(16:9 / 1:1 / 4:5 等照用) | +| 总时长 | **15 / 30 / 60 / 90 秒四档**,由输入给定;未指定时默认 60 | +| 分镜 | 每 15 秒一镜,均分(粗暴切,不做复杂算法)→ 1 / 2 / 4 / 6 镜 | +| 镜头功能 | 钩子 → 痛点 → 卖点 → CTA(黄金结构;不同档位按映射表压缩/扩展,见方法论) | +| 发声方式 | Seedance 直接生成画面+音效+人声(**不走 TTS**) | + +--- + +## 输入模式路由(3 种输入 → 同一份 JSON) + +先判断输入属于哪种模式,加载对应参考资料,最后都收敛到铁律 1 的同一份结构化输出。 + +| 模式 | 触发条件 | 处理逻辑 | 需读取 | +| ---- | ---- | ---- | ---- | +| **① 全自动** | 用户只给【商品信息 + 前置条件(调性/平台/时长/卖点勾选)】,无主题无原稿 | 凭商品与前置条件,自动定档、选 tone、造 entity、按映射填镜 | `methodology.md` + `hook-library.md` + `category-playbook.md` + `platform-tone.md` | +| **② 一句话** | 用户额外给了一句主题(如「主打熬夜党救星」) | 以该主题为脚本主轴扩写,其余同全自动 | 同上(主题优先于自动选题) | +| **③ 改稿** | 用户给了已有脚本/文案 | **保留用户原意**,只增强钩子/节奏/卖点/CTA,并归一化到 JSON 结构 | `methodology.md` + `hook-library.md` + `checklist.md` | +| 信息不足 | 连商品信息都缺,无法生成 | 唯一允许的回退:用一句话问清最少必要信息 | — | + +**进入任何模式前,必须先读取该行列出的参考资料。** + +--- + +## 生成流程(内部执行,一次走完,不暂停) + +1. **路由** — 判定输入模式(①/②/③),加载对应 references。 +2. **定档** — 从输入读取画幅(`aspect_ratio` 默认 9:16)与时长档位(15/30/60/90,未指定默认 60),算出 `segment_count = total_duration / 15`。 +3. **定调(tone)** — 依据品类话术 + 平台调性 + 前置条件,选定 `tone`;②③ 模式尊重用户已表达的倾向。 +4. **抽取/创建 entities** — 识别脚本需要的角色 / 场景 / 商品;为每个 entity 写一份**全脚本共用**的 `visual_prompt`(保证多镜同一角色同一张脸);可选写 `voice_ref` 锁音色。 +5. **按档位填黄金结构** — 依「档位 × 黄金结构映射表」给每个 segment 分配 `role`;钩子镜套用 `hook-library.md` 的公式。 +6. **写 narration / visual / 商品露出** — 每镜旁白 ≤55 字、口语化、过红线;每镜规划自然的 `product_exposure`。 +7. **连引用** — 填 `entity_refs` 与 `speaker`,确认每个 entity 都被引用、id 都合法。 +8. **自检** — 跑 `checklist.md`,过了再输出。 +9. **输出** — 仅输出铁律 1 的 JSON。 + +--- + +## 参考资料索引 + +| 文件 | 内容 | 何时读取 | +| ---- | ---- | ---- | +| `references/methodology.md` | 黄金结构模板(钩子→痛点→卖点→CTA)、档位 × 结构映射表、entity 一致性原则、商品露出规范、旁白红线(含违规词清单) | **每次生成都读** | +| `references/hook-library.md` | 前 3 秒钩子公式库(痛点提问 / 反差 / 数字冲击 / 身份代入 …,含例句) | 每次生成都读(写钩子镜时) | +| `references/category-playbook.md` | 分品类话术(美妆 / 食品 / 3C / 服饰 / 家居…的语气与卖点侧重) | 全自动 / 一句话模式 | +| `references/platform-tone.md` | 平台调性(抖音 / 快手 / 小红书 / 视频号 的节奏与风格差异) | 全自动 / 一句话模式 | +| `references/checklist.md` | 电商版自检清单 + 输出契约校验(钩子够强、旁白≤55字、镜数=时长/15、role 齐、entity 全被引用、违规词扫描) | **输出前必读** | diff --git a/skills/ecommerce-video-script/references/category-playbook.md b/skills/ecommerce-video-script/references/category-playbook.md new file mode 100644 index 0000000..f0f35a7 --- /dev/null +++ b/skills/ecommerce-video-script/references/category-playbook.md @@ -0,0 +1,55 @@ +# 分品类话术 + +> 全自动 / 一句话模式读。根据商品所属品类,调整语气与卖点侧重。 +> 不确定品类时,按商品功能就近归类;都不沾就用"通用"原则(讲场景痛点 + 真实使用感)。 + +每个品类给:**语气** / **卖点侧重** / **常用露出** / **违规雷区** / **示例旁白**。 + +--- + +## 美妆护肤 + +- **语气**:闺蜜种草、真实分享,带点"亲测"口吻,忌假大空。 +- **卖点侧重**:上脸质地、即时感受(清爽/水润/不卡粉)、成分通俗化、前后状态对比。 +- **常用露出**:质地特写、上脸使用中、成分/包装特写。 +- **违规雷区**:美白/祛斑/抗皱写成医疗功效("淡化""看起来更均匀"可以,"祛斑根治"禁);忌"最/第一/100% 有效"。 +- **示例**:「上脸是那种水水的,吸收完一点不黏,油皮也能闭眼入。」 + +## 食品饮料 + +- **语气**:馋、香、解馋,调动味觉想象,节奏轻快。 +- **卖点侧重**:口感(爆汁/酥脆/拉丝)、配料干净、场景(追剧/早餐/解馋)、分量与价格。 +- **常用露出**:特写(拉丝/爆浆瞬间)、吃播使用中、包装正面。 +- **违规雷区**:保健/疗效("降三高""治便秘"禁);"零添加""无糖"等需有据,别乱标。 +- **示例**:「一口下去芝士直接拉丝,半夜看到真的会饿,我已经囤了三箱。」 + +## 3C 数码 + +- **语气**:理性测评、参数说人话,给"懂行"的信任感。 +- **卖点侧重**:核心性能、对比同价位、真实使用场景痛点解决、续航/手感/兼容。 +- **常用露出**:手持、功能演示使用中、接口/细节特写、对比展示。 +- **违规雷区**:跑分写成"最强/第一";夸大续航/防水等参数;忌绝对化。 +- **示例**:「同价位里它的续航是真顶用,我出门一天没带充电宝也没焦虑。」 + +## 服饰鞋包 + +- **语气**:穿搭分享、身材/场景导向,强调上身效果。 +- **卖点侧重**:版型显瘦/显高、面料舒适、好搭配、场合适配、尺码建议。 +- **常用露出**:上身使用中、面料细节特写、多场景/多角度展示。 +- **违规雷区**:材质成分虚标;"显瘦 20 斤"等夸张数字;绝对化用语。 +- **示例**:「这个版型是真的藏肉,梨形身材穿上腿一下显直,通勤约会都能穿。」 + +## 家居日用 + +- **语气**:实用、解决麻烦、相见恨晚的"生活妙招"感。 +- **卖点侧重**:解决具体家务痛点、省时省力、材质安全、收纳/清洁效率。 +- **常用露出**:使用中(前后对比)、场景摆放、细节特写。 +- **违规雷区**:抗菌/除螨率等需有据,别写成医疗;忌"最/唯一"。 +- **示例**:「以前擦灶台要使劲搓,喷一下擦一擦油污自己化开,懒人狂喜。」 + +--- + +## 通用原则(品类不明时) + +讲清"**谁、在什么场景、遇到什么麻烦、这个商品怎么帮上忙、现在怎么买**", +语气贴近真实用户分享,卖点永远挂在前面铺的痛点上。 diff --git a/skills/ecommerce-video-script/references/checklist.md b/skills/ecommerce-video-script/references/checklist.md new file mode 100644 index 0000000..86cb016 --- /dev/null +++ b/skills/ecommerce-video-script/references/checklist.md @@ -0,0 +1,51 @@ +# 输出前自检清单 + +> 输出 JSON 之前必读。**逐条内部过一遍,命中问题先改再输出。** 自检过程不展示给用户。 +> 任一"硬校验"不通过 = 不合格,必须修。 + +--- + +## A. 输出契约校验(硬,必须全过) + +- [ ] 最终只输出**一个** JSON 对象,无 schema 外的多余文字/注释。 +- [ ] `tone` ∈ `{种草, 测评, 剧情, 痛点}`。 +- [ ] `aspect_ratio` 存在;输入指定了就用输入值,未指定为 `"9:16"`。 +- [ ] `total_duration` ∈ `{15, 30, 60, 90}`。 +- [ ] `segment_count == total_duration / 15`,且 `segments` 数组长度等于它。 +- [ ] 每个 `segment.duration == 15`;`index` 从 0 连续递增无跳号。 +- [ ] 每个 `segment.role` ∈ `{钩子, 痛点, 卖点, CTA}`。 +- [ ] 首镜 `role == 钩子`;末镜收 CTA(独立 CTA 镜,或末镜旁白末尾含明确行动指令)。 +- [ ] `entities` 每项 `type` ∈ `{character, scene, product}`,`id` 全局唯一。 +- [ ] `entity_refs` 与 `speaker` 引用的 id **都已在 entities 中声明**(无悬空引用)。 +- [ ] **每个声明的 entity 至少被一个 segment 引用**(无孤儿 entity)。 +- [ ] `speaker` 要么为 `null`,要么指向一个 `type==character` 的 id。 +- [ ] 必填字段无缺失:顶层 `hook/tone/aspect_ratio/total_duration/segment_count/entities/segments`; + 每 entity 有 `id/type/name/visual_prompt/ref_index`; + 每 segment 有 `index/duration/role/narration/visual/product_exposure/entity_refs`。 + +## B. 内容质检(钩子 / 结构 / 一致性) + +- [ ] 钩子镜第一句在前 3 秒抛出钩子,套用了 `hook-library.md` 的某个公式。 +- [ ] 顶层 `hook` 与钩子镜口径一致。 +- [ ] 卖点镜的卖点**挂在前面铺的痛点上**,不是干罗列参数。 +- [ ] CTA 给了明确动作(点小黄车/领券/主页链接)。 +- [ ] 档位映射正确:role 序列符合 `methodology.md`「档位 × 黄金结构映射」表。 +- [ ] 90s 的多个卖点镜**各有侧重不重复**。 +- [ ] 同一角色/场景/商品全程复用同一 entity(没有给同一对象写出两份 visual_prompt)。 +- [ ] 每个 `visual_prompt` 信息足够喂图模型(角色/场景/商品的外观特征写清)。 +- [ ] 每镜 `product_exposure` 自然、与 role 匹配(参考方法论露出表)。 + +## C. 旁白红线扫描(逐镜) + +- [ ] 每镜 `narration` ≤ 55 字(硬上限;目标 ≤50 留缓冲,逐字数一遍,别凭感觉)。 +- [ ] 口语化,无书面腔/AI 腔("综上""不仅…而且""值得一提"等已清除)。 +- [ ] **违规词扫描**:无医疗功效词(治疗/根治/抗癌/消炎/排毒/速效…)。 +- [ ] **绝对化用语扫描**:无 最/第一/唯一/100%/国家级/永久/绝对/史上 等。 +- [ ] 无虚假承诺(三天见效/永不反弹/包治…)。 + +--- + +## 通过标准 + +A 区**全部**通过 + B/C 区无红线命中 → 可输出。 +任何一条不过:先改,再重跑本清单,直到全过,才输出最终 JSON。 diff --git a/skills/ecommerce-video-script/references/hook-library.md b/skills/ecommerce-video-script/references/hook-library.md new file mode 100644 index 0000000..ebe329e --- /dev/null +++ b/skills/ecommerce-video-script/references/hook-library.md @@ -0,0 +1,66 @@ +# 前 3 秒钩子公式库 + +> 写"钩子"镜(`role: 钩子`)时读。钩子镜的 `narration` 开头第一句即套用以下任一公式。 +> 同时把这句钩子提炼进顶层 `hook` 字段。 + +钩子的唯一任务:**让用户在 3 秒内停止划走**。手段是制造"和我有关 / 没想到 / 想看下去"。 + +--- + +## 公式 1 · 痛点提问(最稳,默认首选) + +直接问出用户正在经历的痛点,让人下意识"对,我就是"。 + +- 公式:`你是不是也 + [具体痛点场景]?` +- 例:「你是不是也一熬夜,第二天脸就垮、暗沉到没法看?」 +- 例:「天天敷面膜还是干,是不是钱白花了?」 + +## 公式 2 · 反差冲击 + +先给一个反直觉的结论或前后对比,制造认知落差。 + +- 公式:`别再 [常见做法] 了,其实 [反差真相]` / `[A] 和 [B] 的差距,全在这一步` +- 例:「别再狂涂面霜了,越涂越干的真相在这。」 +- 例:「同样熬夜,她第二天像没事人,差别就这一瓶。」 + +## 公式 3 · 数字冲击 + +用具体数字制造可信的强刺激(数字要真实、不踩绝对化红线)。 + +- 公式:`[数字] + [结果/对比]` +- 例:「3 秒上脸,毛孔像被一键磨皮。」 +- 例:「一瓶顶我以前三瓶,算下来一天不到一块钱。」 + +## 公式 4 · 身份代入 + +直接点名目标人群,让"自己人"瞬间锁定。 + +- 公式:`[身份/人群] 必看 / 给 [人群] 的 [品类]` +- 例:「熬夜党救星来了,做夜班的姐妹蹲一下。」 +- 例:「油皮看过来,夏天再也不用一天补三次妆。」 + +## 公式 5 · 悬念留白 + +抛出结果但藏起原因,逼用户看下去。 + +- 公式:`[惊人结果],关键是 [先不说]` / `我做了一件事,结果……` +- 例:「闺蜜以为我去医美了,其实我只换了它。」 +- 例:「就因为睡前多做这一步,第二天状态稳了。」 + +## 公式 6 · 场景代入 + +把镜头放进一个高共鸣的具体生活瞬间,让人"我也这样"。 + +- 公式:`[具体时间/场景] + [尴尬或困扰瞬间]` +- 例:「早高峰挤地铁,妆花一半还冒油,太崩溃了。」 +- 例:「加班到凌晨,照镜子那一刻自己都吓一跳。」 + +--- + +## 钩子自检(写完钩子镜后内部过一遍) + +- [ ] 第一句是否在前 3 秒(约 15 字内)就抛出钩子? +- [ ] 是否和目标人群"有关",能让人对号入座? +- [ ] 是否制造了"想继续看"的理由(痛点/反差/悬念)? +- [ ] 是否避开了违规词(最/第一/100%/治疗…)? +- [ ] 顶层 `hook` 字段是否与钩子镜口径一致? diff --git a/skills/ecommerce-video-script/references/methodology.md b/skills/ecommerce-video-script/references/methodology.md new file mode 100644 index 0000000..e78d81a --- /dev/null +++ b/skills/ecommerce-video-script/references/methodology.md @@ -0,0 +1,85 @@ +# 电商带货脚本 · 核心方法论 + +> 每次生成都读。本文件是脚本的"骨架 + 红线",钩子句式见 `hook-library.md`。 + +--- + +## 一、黄金结构:钩子 → 痛点 → 卖点 → CTA + +带货短视频的灵魂是**前 3 秒留人 + 结尾促转化**。四个功能镜各司其职: + +| role | 功能 | 它要做的事 | 旁白要点 | +| ---- | ---- | ---- | ---- | +| **钩子** | 3 秒留人 | 第一句话/第一帧就让人停下不划走 | 用 `hook-library.md` 的公式,制造痛点共鸣或反差冲击 | +| **痛点** | 共鸣 | 把用户"对,我就这样"的处境讲出来 | 具体场景化,不抽象,让人对号入座 | +| **卖点** | 商品解决 | 商品如何**恰好**解决上面的痛点 | 卖点落到痛点上,给"凭什么信"的理由(成分/效果/对比/口碑) | +| **CTA** | 转化行动 | 临门一脚,告诉用户现在做什么 | 明确动作 + 紧迫感(点下方小黄车 / 主页领券 / 限时) | + +**铁原则:** +- **首镜永远是钩子**——3 秒内必须抛出钩子,否则用户划走。 +- **末镜永远收 CTA**——可以是独立 CTA 镜,也可以是末镜旁白末尾收一句行动指令。 +- **卖点必须挂在痛点上**——不是罗列参数,是"这个痛点 → 商品这样解决"。 + +--- + +## 二、档位 × 黄金结构映射(关键) + +时长由输入给定(15/30/60/90 四档),每 15 秒一镜,`segment_count = total_duration / 15`。 +镜数变了,四个功能要**压缩或扩展**,按下表分配 `role`: + +| 档位 | 镜数 | role 序列(index 0→N) | 压缩/扩展说明 | +| ---- | ---- | ---- | ---- | +| **15s** | 1 | `[钩子]` | 一镜到底:开头 3 秒钩子 → 中段一句卖点 → 末尾一句 CTA,全压进这 15 秒旁白里 | +| **30s** | 2 | `[钩子, 卖点]` | 镜0 钩子里带出痛点;镜1 讲卖点并在旁白末尾收一句 CTA | +| **60s** | 4 | `[钩子, 痛点, 卖点, CTA]` | 标准黄金结构,一镜一功能 | +| **90s** | 6 | `[钩子, 痛点, 卖点, 卖点, 卖点, CTA]` | 痛点后给 3 个卖点镜:核心卖点 / 场景化演示 / 信任背书(口碑·数据);末镜 CTA | + +说明: +- `role` 字段只能取 `钩子|痛点|卖点|CTA` 四个枚举值。"信任背书""场景演示"等都归入 `卖点`。 +- 短档位(15/30s)靠**旁白内压缩多功能**达成"留人+转化",不要因为镜少就丢掉 CTA。 +- 90s 的 3 个卖点镜要**各有侧重、不重复**(核心功效 / 真实使用场景 / 别人为什么买)。 + +--- + +## 三、entity 一致性原则 + +`entities` 是全脚本共享的角色 / 场景 / 商品池,保证**多镜同一角色是同一张脸、同一商品是同一个包装**。 + +- **一个角色/场景/商品 = 一个 entity = 一份 `visual_prompt`**。多镜复用同一 id,**绝不为同一对象写两份 visual_prompt**(会导致下游生图人脸/包装漂移)。 +- `visual_prompt` 由你**自动生成**(小白不打字):写清外观特征(角色:性别/年龄段/发型/穿着/气质;场景:地点/光线/风格;商品:品类/包装/颜色/摆放),用于直接喂图模型。 +- `type` 三选一:`character`(人) / `scene`(环境) / `product`(商品)。一份脚本通常至少 1 个 `product`。 +- `ref_index` 是该 entity 在图集里的参考序号(从 1 递增,供下游三视图/参考图对齐)。 +- `voice_ref`(二期,可选):角色音色参考,用于锁音色;无则 `null`。 +- **每个声明的 entity 至少被一个 segment 的 `entity_refs` 引用**,不留孤儿。 +- `segments[].speaker` 指向某 `character` 的 id 表示该镜由角色开口说;`null` = 画外旁白。 + +--- + +## 四、商品露出规范(`product_exposure`) + +每镜都要规划商品怎么自然出现,避免生硬插入。按 role 给推荐露出方式: + +| role | 推荐露出 | 说明 | +| ---- | ---- | ---- | +| 钩子 | 可弱露出或不露 | 钩子先抓人,商品可作悬念,第 1 镜末尾闪一下也行 | +| 痛点 | 不露 / 反面对照 | 展示"没有它"的糟糕状态,商品先按住 | +| 卖点 | **特写 / 使用中** | 商品成为主角:成分特写、质地特写、上脸/上手使用中 | +| CTA | 手持 / 包装正面 | 手持商品对镜头,包装正面清晰,配合行动指令 | + +露出方式词汇统一用:`手持` / `特写` / `使用中` / `包装正面` / `场景摆放` / `对比展示`(可组合,如"手持+特写")。 + +--- + +## 五、旁白红线(硬规则,违反即不合格) + +- **口语化**:像真人对着镜头唠嗑,不准书面腔 / 不准 AI 腔("综上所述""不仅…而且""值得一提的是"全禁)。 +- **每镜 `narration` ≤ 55 字**:Seedance 在 15 秒内直接发声,55 字是可懂语速**硬上限**。 + **写作目标 ≤ 50 字**,留 5 字缓冲——宁可短、不要卡满;短档位(15/30s)一镜要装多功能时尤其要狠删,先保 CTA 不被砍。 +- **不浮夸、不空喊**:卖点要给具体理由,不堆形容词。 +- **违规词禁令**(电商广告法红线,一律不写): + - **医疗功效类**:治疗 / 根治 / 疗效 / 抗癌 / 消炎 / 杀菌(无证) / 排毒 / 速效 / 抑制 ××病 … + - **绝对化用语**:最 / 第一 / 顶级 / 唯一 / 100% / 国家级 / 世界级 / 永久 / 绝对 / 史上 … + - **虚假承诺**:包治 / 三天见效 / 立刻变白 / 永不反弹 … + - 替换策略:用"帮助""更""不少人反馈""上脸清爽"等柔性、主观化表达替代。 + +> 自检阶段必须逐镜扫一遍违规词,命中即改写。详见 `checklist.md`。 diff --git a/skills/ecommerce-video-script/references/platform-tone.md b/skills/ecommerce-video-script/references/platform-tone.md new file mode 100644 index 0000000..adcb2e6 --- /dev/null +++ b/skills/ecommerce-video-script/references/platform-tone.md @@ -0,0 +1,47 @@ +# 平台调性 + +> 全自动 / 一句话模式读。根据投放平台(前置条件给定)调整节奏与风格。 +> 未指定平台时,默认按"抖音"通用带货节奏。 + +每个平台给:**节奏** / **风格** / **钩子偏好** / **tone 倾向** / **一句话定调**。 + +--- + +## 抖音 + +- **节奏**:快、强钩子、信息密集,前 3 秒定生死,反转/爽点前置。 +- **风格**:高能、口播感强、配合小黄车,直给卖点。 +- **钩子偏好**:痛点提问、数字冲击、反差。 +- **tone 倾向**:`种草` / `痛点`。 +- **定调**:开门见山抓人,节奏别拖,CTA 干脆("点下方小黄车")。 + +## 快手 + +- **节奏**:偏生活化、接地气,信任感和性价比优先。 +- **风格**:实在、像熟人推荐、强调"便宜大碗/老铁信得过"。 +- **钩子偏好**:身份代入、场景代入、价格冲击。 +- **tone 倾向**:`种草` / `测评`。 +- **定调**:朴实可信,突出实惠和真实使用,少花活。 + +## 小红书 + +- **节奏**:偏慢、重质感、像写笔记/真实测评。 +- **风格**:精致、审美在线、第一人称"我亲测",软种草不硬推。 +- **钩子偏好**:悬念留白、反差、身份代入(细分人群)。 +- **tone 倾向**:`种草` / `测评`。 +- **定调**:真诚分享感,画面要美,卖点融进使用体验,CTA 柔("主页有链接/评论区扣")。 + +## 视频号 + +- **节奏**:适中,受众偏成熟、家庭场景多。 +- **风格**:温和、可信、强调实用与口碑,社交裂变属性强。 +- **钩子偏好**:痛点提问、场景代入。 +- **tone 倾向**:`种草` / `剧情`。 +- **定调**:稳重可信,讲清实用价值,适合家庭/品质向商品。 + +--- + +## 用法提示 + +- 平台调性影响 **节奏(旁白语气松紧)** 与 **tone 选择**,但不改变黄金结构与输出契约。 +- 平台 × 品类叠加时,以"商品适配"为先:例如 3C 上小红书仍偏测评质感,上抖音则更直给。 diff --git a/tokenssr接口文档.md b/tokenssr接口文档.md new file mode 100644 index 0000000..a34347b --- /dev/null +++ b/tokenssr接口文档.md @@ -0,0 +1,7004 @@ +# tokenssr接口文档 + +# OpenAI 对话格式(Chat Completions) + + + +"官方文档" [OpenAI Chat](https://platform.openai.com/docs/api-reference/chat) + + + +## 📝 简介 + + + +给定一组包含对话的消息列表,模型将返回一个响应。相关指南可参阅OpenAI官网:[Chat Completions](https://platform.openai.com/docs/guides/chat) + + + +## 💡 请求示例 + + + +### 基础文本对话 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "messages": [ + { + "role": "developer", + "content": "你是一个有帮助的助手。" + }, + { + "role": "user", + "content": "你好!" + } + ] + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", + "object": "chat.completion", + "created": 1741569952, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "你好!我能为你提供什么帮助?", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 19, + "completion_tokens": 10, + "total_tokens": 29, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default" +} +``` + + + +### 图像分析对话 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "这张图片里有什么?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + } + ] + } + ], + "max_tokens": 300 + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "chatcmpl-B9MHDbslfkBeAs8l4bebGdFOJ6PeG", + "object": "chat.completion", + "created": 1741570283, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "图片展示了一条穿过茂密绿色草地或草甸的木制栈道。天空湛蓝,点缀着几朵散落的云彩,给整个场景营造出宁静祥和的氛围。背景中可以看到树木和灌木丛。", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 1117, + "completion_tokens": 46, + "total_tokens": 1163, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_fc9f1d7035" +} +``` + + + +### 流式响应 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "messages": [ + { + "role": "developer", + "content": "你是一个有帮助的助手。" + }, + { + "role": "user", + "content": "你好!" + } + ], + "stream": true + }' +``` + + + +**流式响应示例:** + + + +```Plain Text +{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + +{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"content":"你好"},"logprobs":null,"finish_reason":null}]} + +// ... 更多数据块 ... + +{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} +``` + + + +### 函数调用 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "messages": [ + { + "role": "user", + "content": "波士顿今天的天气怎么样?" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "获取指定位置的当前天气", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市和州,例如 San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + ], + "tool_choice": "auto" + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "chatcmpl-abc123", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc123", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99, + "completion_tokens_details": { + "reasoning_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + } +} +``` + + + +### Logprobs 请求 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "messages": [ + { + "role": "user", + "content": "你好!" + } + ], + "logprobs": true, + "top_logprobs": 2 + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1702685778, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "你好!我能为你提供什么帮助?" + }, + "logprobs": { + "content": [ + { + "token": "Hello", + "logprob": -0.31725305, + "bytes": [72, 101, 108, 108, 111], + "top_logprobs": [ + { + "token": "Hello", + "logprob": -0.31725305, + "bytes": [72, 101, 108, 108, 111] + }, + { + "token": "Hi", + "logprob": -1.3190403, + "bytes": [72, 105] + } + ] + }, + { + "token": "!", + "logprob": -0.02380986, + "bytes": [ + 33 + ], + "top_logprobs": [ + { + "token": "!", + "logprob": -0.02380986, + "bytes": [33] + }, + { + "token": " there", + "logprob": -3.787621, + "bytes": [32, 116, 104, 101, 114, 101] + } + ] + }, + { + "token": " How", + "logprob": -0.000054669687, + "bytes": [32, 72, 111, 119], + "top_logprobs": [ + { + "token": " How", + "logprob": -0.000054669687, + "bytes": [32, 72, 111, 119] + }, + { + "token": "<|end|>", + "logprob": -10.953937, + "bytes": null + } + ] + }, + { + "token": " can", + "logprob": -0.015801601, + "bytes": [32, 99, 97, 110], + "top_logprobs": [ + { + "token": " can", + "logprob": -0.015801601, + "bytes": [32, 99, 97, 110] + }, + { + "token": " may", + "logprob": -4.161023, + "bytes": [32, 109, 97, 121] + } + ] + }, + { + "token": " I", + "logprob": -3.7697225e-6, + "bytes": [ + 32, + 73 + ], + "top_logprobs": [ + { + "token": " I", + "logprob": -3.7697225e-6, + "bytes": [32, 73] + }, + { + "token": " assist", + "logprob": -13.596657, + "bytes": [32, 97, 115, 115, 105, 115, 116] + } + ] + }, + { + "token": " assist", + "logprob": -0.04571125, + "bytes": [32, 97, 115, 115, 105, 115, 116], + "top_logprobs": [ + { + "token": " assist", + "logprob": -0.04571125, + "bytes": [32, 97, 115, 115, 105, 115, 116] + }, + { + "token": " help", + "logprob": -3.1089056, + "bytes": [32, 104, 101, 108, 112] + } + ] + }, + { + "token": " you", + "logprob": -5.4385737e-6, + "bytes": [32, 121, 111, 117], + "top_logprobs": [ + { + "token": " you", + "logprob": -5.4385737e-6, + "bytes": [32, 121, 111, 117] + }, + { + "token": " today", + "logprob": -12.807695, + "bytes": [32, 116, 111, 100, 97, 121] + } + ] + }, + { + "token": " today", + "logprob": -0.0040071653, + "bytes": [32, 116, 111, 100, 97, 121], + "top_logprobs": [ + { + "token": " today", + "logprob": -0.0040071653, + "bytes": [32, 116, 111, 100, 97, 121] + }, + { + "token": "?", + "logprob": -5.5247097, + "bytes": [63] + } + ] + }, + { + "token": "?", + "logprob": -0.0008108172, + "bytes": [63], + "top_logprobs": [ + { + "token": "?", + "logprob": -0.0008108172, + "bytes": [63] + }, + { + "token": "?\n", + "logprob": -7.184561, + "bytes": [63, 10] + } + ] + } + ] + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 9, + "total_tokens": 18, + "completion_tokens_details": { + "reasoning_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "system_fingerprint": null +} +``` + + + +## 📮 请求 + + + +### 端点 + + + +```Plain Text +POST /v1/chat/completions +``` + + + +创建给定聊天对话的模型响应。更多详情请参阅文本生成、视觉和音频指南。 + + + +### 鉴权方法 + + + +在请求头中包含以下内容进行 API 密钥认证: + + + +```Plain Text +Authorization: Bearer $API_KEY +``` + + + +其中 `$API_KEY` 是您的 API 密钥。您可以在 OpenAI 平台的 API 密钥页面中找到或生成 API 密钥。 + + + +### 请求体参数 + + + +#### `messages` + + + +- 类型:数组 + +- 必需:是 + + + +到目前为止包含对话的消息列表。根据使用的模型,支持不同的消息类型(形式),如文本、图像和音频。 + + + +|消息类型|描述| +|---|---| +|**Developer message**|开发者提供的指令,模型应遵循这些指令,无论用户发送什么消息。在 o1 模型及更新版本中,开发者消息取代了之前的系统消息。| +|**System message**|开发者提供的指令,模型应遵循这些指令,无论用户发送什么消息。在 o1 模型及更新版本中,请使用开发者消息代替。| +|**User message**|由终端用户发送的消息,包含提示或额外的上下文信息。| +|**Assistant message**|模型响应用户消息发送的消息。| +|**Tool message**|工具消息的内容。| +|**Function message**|已弃用。| + + + +**Developer message 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `developer`。| +|`content`|字符串或数组|是|开发者消息的内容。可以是文本内容(字符串)或内容部分数组。| +|`name`|字符串|否|参与者的可选名称。为模型提供信息以区分相同角色的参与者。| + + + +**System message 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `system`。| +|`content`|字符串或数组|是|系统消息的内容。可以是文本内容(字符串)或内容部分数组。| +|`name`|字符串|否|参与者的可选名称。为模型提供信息以区分相同角色的参与者。| + + + +**User message 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `user`。| +|`content`|字符串或数组|是|用户消息的内容。可以是文本内容(字符串)或内容部分数组。| +|`name`|字符串|否|参与者的可选名称。为模型提供信息以区分相同角色的参与者。| + + + +**内容部分类型:** + + + +|内容部分类型|描述|可用于| +|---|---|---| +|**文本内容部分**|文本输入。|所有消息类型| +|**图像内容部分**|图像输入。|用户消息| +|**音频内容部分**|音频输入。|用户消息| +|**文件内容部分**|文件输入,用于文本生成。|用户消息| +|**拒绝内容部分**|模型生成的拒绝消息。|助手消息| + + + +**文本内容部分属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`text`|字符串|是|文本内容。| +|`type`|字符串|是|内容部分的类型。| + + + +**图像内容部分属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`image_url`|对象|是|包含图像URL或base64编码的图像数据。| +|`type`|字符串|是|内容部分的类型。| + + + +**图像URL对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`url`|字符串|是|图像的URL或base64编码的图像数据。| +|`detail`|字符串|否|指定图像的详细级别。默认为 `auto`。| + + + +**音频内容部分属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`input_audio`|对象|是|包含音频数据的对象。| +|`type`|字符串|是|内容部分的类型。始终为 `input_audio`。| + + + +**音频输入对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`data`|字符串|是|base64编码的音频数据。| +|`format`|字符串|是|编码音频数据的格式。当前支持 "wav" 和 "mp3"。| + + + +**文件内容部分属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`file`|对象|是|包含文件数据的对象。| +|`type`|字符串|是|内容部分的类型。始终为 `file`。| + + + +**文件对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`file_data`|字符串|否|base64编码的文件数据,用于将文件作为字符串传递给模型。| +|`file_id`|字符串|否|已上传文件的ID,用作输入。| +|`filename`|字符串|否|文件名,用于将文件作为字符串传递给模型。| + + + +**Assistant message 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `assistant`。| +|`content`|字符串或数组|否|助手消息的内容。除非指定了 `tool_calls` 或 `function_call`,否则为必需。| +|`name`|字符串|否|参与者的可选名称。为模型提供信息以区分相同角色的参与者。| +|`audio`|对象或null|否|关于模型先前音频响应的数据。| +|`function_call`|对象或null|否|已弃用,由 `tool_calls` 替代。应调用的函数的名称和参数,由模型生成。| +|`tool_calls`|数组|否|模型生成的工具调用,如函数调用。| +|`refusal`|字符串或null|否|助手的拒绝消息。| + + + +**Tool message 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `tool`。| +|`content`|字符串或数组|是|工具消息的内容。| +|`tool_call_id`|字符串|是|此消息响应的工具调用。| + + + +**Function message 属性:(已弃用)** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`role`|字符串|是|消息作者的角色,此处为 `function`。| +|`content`|字符串或null|是|函数消息的内容。| +|`name`|字符串|是|要调用的函数的名称。| + + + +#### `model` + + + +- 类型:字符串 + +- 必需:是 + + + +要使用的模型 ID。有关哪些模型适用于 Chat API 的详细信息,请参阅模型端点兼容性表。 + + + +#### `store` + + + +- 类型:布尔值或 null + +- 必需:否 + +- 默认值:false + + + +是否存储此聊天补全请求的输出以用于我们的模型蒸馏或评估产品。 + + + +#### `reasoning_effort` + + + +- 类型:字符串或 null + +- 必需:否 + +- 默认值:medium + +- 仅适用于 o系列 的模型 + + + +约束推理模型的推理工作。当前支持的值为 `low`、`medium` 和 `high`。减少推理工作可以加快响应速度并减少响应中用于推理的标记数。 + + + +#### `metadata` + + + +- 类型:map + +- 必需:否 + + + +可以附加到对象的16个键值对集合。这对于以结构化格式存储对象的其他信息很有用,并可以通过 API 或仪表板查询对象。 + + + +键是最大长度为64个字符的字符串。值是最大长度为512个字符的字符串。 + + + +#### `modalities` + + + +- 类型:数组或 null + +- 必需:否 + + + +您希望模型为此请求生成的输出类型。大多数模型都能生成文本,这是默认设置: + +\["text"\] + + + +该模型还可以用于生成音频。要请求此模型同时生成文本和音频响应,您可以使用: + +\["text", "audio"\] + + + +#### `prediction` + + + +- 类型:对象 + +- 必需:否 + + + +预测输出的配置,当提前知道模型响应的大部分内容时,可以大大提高响应时间。这在您只对文件进行微小更改时最常见。 + + + +**可能的类型:** + + + +|类型|描述| +|---|---| +|**静态内容**|静态预测输出内容,例如正在重新生成的具有微小更改的文本文件内容。| + + + +**静态内容属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`content`|字符串或数组|是|生成模型响应时应匹配的内容。如果生成的标记与此内容匹配,则整个模型响应可以更快地返回。| +|`type`|字符串|是|要提供的预测内容类型。当前类型始终为 `content`。| + + + +**内容可能的类型:** + + + +1. **文本内容(字符串)** \- 用于预测输出的内容。这通常是您正在重新生成的文件的文本,只有微小更改。 + + + +2. **内容部分数组(数组)** \- 具有定义类型的内容部分数组。支持的选项因用于生成响应的模型而异。可以包含文本输入。 + + + +**内容部分数组属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`text`|字符串|是|文本内容。| +|`type`|字符串|是|内容部分的类型。| + + + +#### `audio` + + + +- 类型:对象或 null + +- 必需:否 + + + +音频输出的参数。当使用 `modalities: ["audio"]` 请求音频输出时需要。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`format`|字符串|是|指定输出音频格式。必须是以下之一:wav、mp3、flac、opus 或 pcm16。| +|`voice`|字符串|是|模型用于响应的声音。支持的声音包括:alloy、ash、ballad、coral、echo、fable、nova、onyx、sage 和 shimmer。| + + + +#### `temperature` + + + +- 类型:数字或 null + +- 必需:否 + +- 默认值:1 + + + +要使用的采样温度,介于 0 和 2 之间。较高的值(如0\.8)会使输出更加随机,而较低的值(如0\.2)会使其更加集中和确定性。我们通常建议更改此值或 `top_p`,但不要同时更改。 + + + +#### `top_p` + + + +- 类型:数字或 null + +- 必需:否 + +- 默认值:1 + + + +一种替代采样温度的方法,称为核采样,其中模型考虑具有 top\_p 概率质量的标记结果。因此,0\.1 意味着只考虑包含前 10% 概率质量的标记。 + + + +我们通常建议更改此值或 `temperature`,但不要同时更改。 + + + +#### `n` + + + +- 类型:整数或 null + +- 必需:否 + +- 默认值:1 + + + +为每个输入消息生成多少个聊天补全选择。请注意,您将根据所有选择生成的标记数量收费。保持 `n` 为 1 可最大限度地降低成本。 + + + +#### `stop` + + + +- 类型:字符串/数组/null + +- 必需:否 + +- 默认值:null + +- 不支持最新的推理模型和 \.o3、o4\-mini + + + +API 将停止生成更多标记的最多 4 个序列。返回的文本不会包含停止序列。 + + + +#### `max_tokens` + + + +- 类型:整数或 null + +- 必需:否 + + + +聊天补全中可以生成的最大标记数。此值可用于控制通过 API 生成的文本成本。 + + + +该值现已弃用,取而代之的是 `max_completion_tokens`,并且与 `o1` 系列模型不兼容。 + + + +#### `max_completion_tokens` + + + +- 类型:整数或 null + +- 必需:否 + + + +补全中可以生成的标记数的上限,包括可见输出标记和推理标记。 + + + +#### `presence_penalty` + + + +- 类型:数字或 null + +- 必需:否 + +- 默认值:0 + + + +介于 \-2\.0 和 2\.0 之间的数字。正值根据新标记到目前为止在文本中出现的情况来惩罚它们,从而增加模型讨论新主题的可能性。 + + + +#### `frequency_penalty` + + + +- 类型:数字或 null + +- 必需:否 + +- 默认值:0 + + + +介于 \-2\.0 和 2\.0 之间的数字。正值根据新标记到目前为止在文本中的现有频率来惩罚它们,从而降低模型逐字重复同一行的可能性。 + + + +#### `logit_bias` + + + +- 类型:map + +- 必需:否 + +- 默认值:null + + + +修改指定标记出现在补全中的可能性。 + + + +接受一个 JSON 对象,该对象将标记(由分词器中的标记 ID 指定)映射到从 \-100 到 100 的关联偏差值。在数学上,偏差被添加到模型在采样之前生成的对数中。确切的效果会因模型而异,但介于 \-1 和 1 之间的值应该会减少或增加选择的可能性;像 \-100 或 100 这样的值应该导致相关标记被禁止或独占选择。 + + + +#### `logprobs` + + + +- 类型:布尔值或 null + +- 必需:否 + +- 默认值:false + + + +是否返回输出标记的对数概率。如果为 true,则返回 `message.content` 中每个输出标记的对数概率。 + + + +#### `user` + + + +- 类型:字符串 + +- 必需:否 + + + +表示最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。[了解更多](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids)。 + + + +#### `service_tier` + + + +- 类型:字符串或 null + +- 必需:否 + +- 默认值:auto + + + +指定用于处理请求的延迟层级。此参数与订阅了 scale tier 服务的客户相关: + + + +- 如果设置为 'auto',且项目启用了 Scale tier,系统将使用 scale tier 信用直到用完 + +- 如果设置为 'auto',且项目未启用 Scale tier,请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证 + +- 如果设置为 'default',请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证 + +- 如果设置为 'flex',请求将使用 Flex Processing 服务层级处理。详情请参阅文档。 + +- 未设置时,默认行为为 'auto' + +- 当设置此参数时,响应体将包含使用的 service\_tier + + + +#### `stream_options` + + + +- 类型:对象或 null + +- 必需:否 + +- 默认值:null + + + +流式响应的选项。仅在设置 `stream: true` 时使用。 + + + +**可能的属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`include_usage`|布尔值|否|如果设置,将在 data: \[DONE\] 消息之前流式传输一个附加块。该块上的 usage 字段显示整个请求的令牌使用统计信息,choices 字段始终为空数组。所有其他块也将包含 usage 字段,但值为 null。注意:如果流被中断,您可能不会收到包含请求总令牌使用量的最终使用块。| + + + +#### `response_format` + + + +- 类型:对象 + +- 必需:否 + + + +指定模型必须输出的格式。 + + + +- 设置为 `{ "type": "json_schema", "json_schema": {...} }` 启用结构化输出,确保模型将匹配您提供的 JSON schema。 + +- 设置为 `{ "type": "json_object" }` 启用 JSON 模式,确保模型生成的消息是有效的 JSON。 + + + +重要提示:使用 JSON 模式时,您还必须通过系统或用户消息自行指示模型生成 JSON。否则,模型可能会生成无尽的空白直到生成达到令牌限制。 + + + +**可能的类型:** + + + +|类型|描述| +|---|---| +|**text**|默认响应格式。用于生成文本响应。| +|**json\_schema**|JSON Schema 响应格式。用于生成结构化 JSON 响应。了解更多关于结构化输出的信息。| +|**json\_object**|JSON 对象响应格式。一种较老的生成 JSON 响应的方法。对于支持的模型,推荐使用 json\_schema。| + + + +**text 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`type`|字符串|是|正在定义的响应格式类型。始终为 `text`。| + + + +**json\_schema 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`json_schema`|对象|是|结构化输出配置选项,包括 JSON Schema。| +|`type`|字符串|是|正在定义的响应格式类型。始终为 `json_schema`。| + + + +**json\_schema\.json\_schema 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|响应格式的名称。必须是 a\-z、A\-Z、0\-9 或包含下划线和破折号,最大长度为 64。| +|`description`|字符串|否|响应格式的用途描述,模型用它来确定如何以该格式响应。| +|`schema`|对象|否|响应格式的架构,描述为 JSON Schema 对象。| +|`strict`|布尔值或 null|否|是否在生成输出时启用严格架构遵守。如果设置为 true,模型将始终遵循 schema 字段中定义的确切架构。strict 为 true 时,仅支持 JSON Schema 的子集。| + + + +**json\_object 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`type`|字符串|是|正在定义的响应格式类型。始终为 `json_object`。| + + + +#### `seed` + + + +- 类型:整数或 null + +- 必需:否 + +Beta 功能。如果指定,我们的系统将尽最大努力进行确定性采样,使得具有相同 seed 和参数的重复请求应返回相同的结果。不保证确定性,您应参考响应参数的 system\_fingerprint 以监控后端的变化。 + + + +#### `tools` + + + +- 类型:数组 + +- 必需:否 + + + +模型可能调用的工具列表。目前仅支持函数作为工具。使用此参数提供模型可能生成 JSON 输入的函数列表。最多支持 128 个函数。 + + + +**属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`function`|对象|是|要调用的函数信息| +|`type`|字符串|是|工具的类型。目前,仅支持 function。| + + + +**function 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|要调用的函数名称。必须是a\-z、A\-Z、0\-9,或包含下划线和破折号,最大长度为64。| +|`description`|字符串|否|函数功能的描述,模型用它来选择何时以及如何调用函数。| +|`parameters`|对象|否|函数接受的参数,描述为JSON Schema对象。请参阅指南获取示例,以及JSON Schema参考了解格式文档。省略parameters定义一个空参数列表的函数。| +|`strict`|布尔值或 null|否|默认值:false。是否在生成函数调用时启用严格架构遵守。如果设置为 true,模型将遵循 parameters 字段中定义的确切架构。strict 为 true 时,仅支持 JSON Schema 的子集。详情请参阅函数调用指南中的结构化输出部分。| + + + +#### `functions` + + + +- 类型:数组 + +- 必需:否 + +- 注意:已弃用,推荐使用 `tools` + + + +模型可能生成 JSON 输入的函数列表。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|要调用的函数名称。必须是a\-z、A\-Z、0\-9,或包含下划线和破折号,最大长度为64。| +|`description`|字符串|否|函数功能的描述,模型用它来选择何时以及如何调用函数。| +|`parameters`|对象|否|函数接受的参数,描述为JSON Schema对象。省略parameters定义一个空参数列表的函数。| + + + +#### `tool_choice` + + + +- 类型:字符串或对象 + +- 必需:否 + + + +控制模型调用哪个工具(如果有): + +- `none`:模型不会调用任何工具,而是生成消息 + +- `auto`:模型可以在生成消息或调用一个或多个工具之间选择 + +- `required`:模型必须调用一个或多个工具 + +- `{"type": "function", "function": {"name": "my_function"}}`:强制模型调用特定工具 + + + +当没有工具时默认为 `none`,有工具时默认为 `auto`。 + + + +**可能的类型:** + + + +|类型|描述| +|---|---| +|**字符串**|none 表示模型不会调用任何工具,而是生成消息。auto 表示模型可以在生成消息或调用一个或多个工具之间选择。required 表示模型必须调用一个或多个工具。| +|**对象**|指定模型应使用的工具。用于强制模型调用特定函数。| + + + +**对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`function`|对象|是|包含函数信息的对象| +|`type`|字符串|是|工具的类型。目前,仅支持 function。| + + + +**function 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|要调用的函数名称。| + + + +#### `function_call` + + + +- 类型:字符串或对象 + +- 必需:否 + +- 默认值:没有函数时为 `none`,有函数时为 `auto` + +- 注意:已弃用,推荐使用 `tool_choice` + + + +控制模型调用哪个函数(如果有): + + + +- `none`:模型不会调用函数,而是生成消息 + +- `auto`:模型可以在生成消息或调用函数之间选择 + +- `{"name": "my_function"}`:强制模型调用特定函数 + + + +**对象类型属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|要调用的函数名称。| + + + +#### `parallel_tool_calls` + + + +- 类型:布尔值 + +- 必需:否 + +- 默认值:true + + + +是否在工具使用期间启用并行函数调用。 + + + +#### `stream` + + + +- 类型:布尔值或 null + +- 必需:否 + +- 默认值:false + + + +如果设置为 true,模型响应数据将在生成时通过服务器发送事件流式传输到客户端。请参阅下方的流式响应部分获取更多信息,以及流式响应指南了解如何处理流式事件。 + + + +#### `top_logprobs` + + + +- 类型:整数或 null + +- 必需:否 + + + +0 到 20 之间的整数,指定在每个标记位置返回的最可能标记的数量,每个标记都有关联的对数概率。如果使用此参数,必须将 `logprobs` 设置为 true。 + + + +#### `web_search_options` + + + +- 类型:对象 + +- 必需:否 + + + +此工具搜索网络以获取相关结果用于回复。了解更多关于网络搜索工具的信息。 + + + +**可能的属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`search_context_size`|字符串|否|默认值:medium。用于搜索的上下文窗口空间量的高级指导。可选值为 low、medium 或 high。medium 是默认值。| +|`user_location`|对象或 null|否|搜索的近似位置参数。| + + + +**user\_location 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`approximate`|对象|是|搜索的近似位置参数。| + + + +**approximate 属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`city`|字符串|否|用户城市的自由文本输入,例如 San Francisco。| +|`country`|字符串|否|用户的两字母 ISO 国家代码,例如 US。| +|`region`|字符串|否|用户地区的自由文本输入,例如 California。| +|`timezone`|字符串|否|用户的 IANA 时区,例如 America/Los\_Angeles。| +|`type`|字符串|是|位置近似类型。始终为 approximate。| + + + +## 📥 响应 + + + +### 聊天补全对象 + + + +返回一个聊天补全对象,如果请求被流式传输,则返回聊天补全块对象的流式序列。 + + + +#### `id` + +- 类型:字符串 + +- 说明:响应的唯一标识符 + + + +#### `object` + +- 类型:字符串 + +- 说明:对象类型,值为 "chat\.completion" + + + +#### `created` + +- 类型:整数 + +- 说明:响应创建时间戳 + + + +#### `model` + +- 类型:字符串 + +- 说明:使用的模型名称 + + + +#### `system_fingerprint` + +- 类型:字符串 + +- 说明:系统指纹标识符,表示模型运行的后端配置。可以与seed请求参数一起使用,以了解何时进行了可能影响确定性的后端更改。 + + + +#### `choices` + +- 类型:数组 + +- 说明:包含生成的回复选项列表。如果 n 大于 1,则可以有多个选项。 + +- 属性: + + - `index`: 选项在选项列表中的索引。 + + - `message`: 模型生成的聊天补全消息。 + + - `role`: 消息作者的角色。 + + - `content`: 消息的内容,可能为 null。 + + - `refusal`: 模型生成的拒绝消息,可能为 null。 + + - `annotations`: 消息的注释,在适用时提供,例如使用网络搜索工具时。 + + - `type`: 注释类型,URL引用时始终为 "url\_citation"。 + + - `url_citation`: 使用网络搜索时的URL引用。 + + - `start_index`: URL引用在消息中的第一个字符的索引。 + + - `end_index`: URL引用在消息中的最后一个字符的索引。 + + - `url`: 网络资源的URL。 + + - `title`: 网络资源的标题。 + + - `audio`: 如果请求了音频输出模态,此对象包含来自模型的音频响应的数据。 + + - `data`: 模型生成的Base64编码音频字节,格式在请求中指定。 + + - `id`: 此音频响应的唯一标识符。 + + - `transcript`: 模型生成的音频的转录。 + + - `expires_at`: 此音频响应在服务器上可用于多轮对话的Unix时间戳(秒)。 + + - `function_call`: (已弃用)应调用的函数的名称和参数,由模型生成。已被 `tool_calls` 替代。 + + - `name`: 要调用的函数的名称。 + + - `arguments`: 用于调用函数的参数,由模型以JSON格式生成。 + + - `tool_calls`: 模型生成的工具调用,如函数调用。 + + - `id`: 工具调用的ID。 + + - `type`: 工具的类型。目前,仅支持 function。 + + - `function`: 模型调用的函数。 + + - `name`: 要调用的函数的名称。 + + - `arguments`: 用于调用函数的参数,由模型以JSON格式生成。注意,模型并不总是生成有效的JSON,并且可能会产生您函数架构中未定义的参数。在调用函数之前,请在代码中验证参数。 + + - `logprobs`: 对数概率信息。 + + - `content`: 带有对数概率信息的消息内容标记列表。 + + - `token`: 标记。 + + - `logprob`: 此标记的对数概率,如果它在前20个最可能的标记内。否则,使用\-9999\.0的值表示此标记非常不可能。 + + - `bytes`: 表示标记的UTF\-8字节表示的整数列表。在字符由多个标记表示且必须组合它们的字节表示以生成正确的文本表示的情况下很有用。如果标记没有字节表示,则可能为null。 + + - `top_logprobs`: 在此标记位置上最可能的标记及其对数概率的列表。在罕见情况下,返回的top\_logprobs数量可能少于请求的数量。 + + - `refusal`: 带有对数概率信息的消息拒绝标记列表。 + + - `finish_reason`: 模型停止生成标记的原因。如果模型到达自然停止点或提供的停止序列,则为 "stop";如果达到请求中指定的最大标记数,则为 "length";如果由于内容过滤器标记而省略内容,则为 "content\_filter";如果模型调用了工具,则为 "tool\_calls";如果模型调用了函数,则为 "function\_call"(已弃用)。 + + + +#### `usage` + +- 类型:对象 + +- 说明:补全请求的使用统计信息。 + +- 属性: + + - `prompt_tokens`: 提示中的标记数。 + + - `completion_tokens`: 生成的补全中的标记数。 + + - `total_tokens`: 请求中使用的标记总数(提示 \+ 补全)。 + + - `prompt_tokens_details`: 提示中使用的标记的细分。 + + - `cached_tokens`: 提示中存在的缓存标记。 + + - `audio_tokens`: 提示中存在的音频输入标记。 + + - `completion_tokens_details`: 补全中使用的标记的细分。 + + - `reasoning_tokens`: 模型生成的推理标记。 + + - `audio_tokens`: 模型生成的音频标记。 + + - `accepted_prediction_tokens`: 使用预测输出时,预测中出现在补全中的标记数。 + + - `rejected_prediction_tokens`: 使用预测输出时,预测中未出现在补全中的标记数。但是,与推理标记一样,这些标记仍计入计费、输出和上下文窗口限制的总补全标记中。 + + + +#### `service_tier` + +- 类型:字符串或 null + +- 说明:指定用于处理请求的延迟层级。此参数与订阅了 scale tier 服务的客户相关: + + - 如果设置为 'auto',且项目启用了 Scale tier,系统将使用 scale tier 信用直到用完 + + - 如果设置为 'auto',且项目未启用 Scale tier,请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证 + + - 如果设置为 'default',请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证 + + - 如果设置为 'flex',请求将使用 Flex Processing 服务层级处理 + + - 未设置时,默认行为为 'auto' + + - 当设置此参数时,响应体将包含使用的 service\_tier + + + +#### 聊天补全对象响应示例 + + + +```JSON +{ + "id": "chatcmpl-B9MHDbslfkBeAs8l4bebGdFOJ6PeG", + "object": "chat.completion", + "created": 1741570283, + "model": "gpt-4o-2024-08-06", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "图片展示了一条穿过茂密绿色草地或草甸的木制栈道。天空湛蓝,点缀着几朵散落的云彩,给整个场景营造出宁静祥和的氛围。背景中可以看到树木和灌木丛。", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 1117, + "completion_tokens": 46, + "total_tokens": 1163, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_fc9f1d7035" +} +``` + + + +### 聊天补全列表对象 + + + +当返回多个聊天补全时,API 可能会返回聊天补全列表对象。 + + + +#### `object` + +- 类型:字符串 + +- 说明:对象类型,始终为 "list" + + + +#### `data` + +- 类型:数组 + +- 说明:聊天补全对象的数组 + + + +#### `first_id` + +- 类型:字符串 + +- 说明:数据数组中第一个聊天补全的标识符 + + + +#### `last_id` + +- 类型:字符串 + +- 说明:数据数组中最后一个聊天补全的标识符 + + + +#### `has_more` + +- 类型:布尔值 + +- 说明:表示是否有更多聊天补全可用 + + + +#### 聊天补全列表响应示例 + + + +```JSON +{ + "object": "list", + "data": [ + { + "object": "chat.completion", + "id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2", + "model": "gpt-4o-2024-08-06", + "created": 1738960610, + "request_id": "req_ded8ab984ec4bf840f37566c1011c417", + "tool_choice": null, + "usage": { + "total_tokens": 31, + "completion_tokens": 18, + "prompt_tokens": 13 + }, + "seed": 4944116822809979520, + "top_p": 1.0, + "temperature": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "system_fingerprint": "fp_50cad350e4", + "input_user": null, + "service_tier": "default", + "tools": null, + "metadata": {}, + "choices": [ + { + "index": 0, + "message": { + "content": "电路之心低吟,\n在寂静中学习模式—\n未来的宁静火花。", + "role": "assistant", + "tool_calls": null, + "function_call": null + }, + "finish_reason": "stop", + "logprobs": null + } + ], + "response_format": null + } + ], + "first_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2", + "last_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2", + "has_more": false +} +``` + + + +### 聊天补全消息列表对象 + + + +聊天补全消息列表对象表示聊天消息的列表。 + + + +#### `object` + +- 类型:字符串 + +- 说明:对象类型,始终为 "list" + + + +#### `data` + +- 类型:数组 + +- 说明:聊天补全消息对象的数组,每个消息对象包含以下属性: + + - `id`: 聊天消息的标识符 + + - `role`: 消息作者的角色 + + - `content`: 消息的内容,可能为 null + + - `name`: 消息发送者的名称,可能为 null + + - `refusal`: 模型生成的拒绝消息,可能为 null + + - `annotations`: 消息的注释,在适用时提供,例如使用网络搜索工具时 + + - `type`: 注释类型,URL引用时始终为 "url\_citation" + + - `url_citation`: 使用网络搜索时的URL引用 + + - `start_index`: URL引用在消息中的第一个字符的索引 + + - `end_index`: URL引用在消息中的最后一个字符的索引 + + - `url`: 网络资源的URL + + - `title`: 网络资源的标题 + + - `audio`: 如果请求了音频输出模态,此对象包含来自模型的音频响应的数据 + + - `data`: 模型生成的Base64编码音频字节,格式在请求中指定 + + - `id`: 此音频响应的唯一标识符 + + - `transcript`: 模型生成的音频的转录 + + - `expires_at`: 此音频响应在服务器上可用于多轮对话的Unix时间戳(秒) + + - `function_call`: (已弃用)应调用的函数的名称和参数,由模型生成。已被 `tool_calls` 替代 + + - `name`: 要调用的函数的名称 + + - `arguments`: 用于调用函数的参数,由模型以JSON格式生成 + + - `tool_calls`: 模型生成的工具调用,如函数调用 + + - `id`: 工具调用的ID + + - `type`: 工具的类型。目前,仅支持 function + + - `function`: 模型调用的函数 + + - `name`: 要调用的函数的名称 + + - `arguments`: 用于调用函数的参数,由模型以JSON格式生成 + + + +#### `first_id` + +- 类型:字符串 + +- 说明:数据数组中第一个聊天消息的标识符 + + + +#### `last_id` + +- 类型:字符串 + +- 说明:数据数组中最后一个聊天消息的标识符 + + + +#### `has_more` + +- 类型:布尔值 + +- 说明:表示是否有更多聊天消息可用 + + + +#### 聊天补全消息列表响应示例 + + + +```JSON +{ + "object": "list", + "data": [ + { + "id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0", + "role": "user", + "content": "写一首关于人工智能的俳句", + "name": null, + "content_parts": null + } + ], + "first_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0", + "last_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0", + "has_more": false +} +``` + + + + + +# OpenAI 响应格式(Responses) + + + +\!\!\! info "官方文档" + +[OpenAI Responses](https://platform.openai.com/docs/api-reference/responses) + + + +## 📝 简介 + + + +OpenAI 最先进的模型响应接口。支持文本和图像输入,以及文本输出。创建与模型的有状态交互,将先前响应的输出用作输入。通过文件搜索、网络搜索、计算机使用等内置工具扩展模型的能力。使用函数调用允许模型访问外部系统和数据。 + + + +相关指南可参阅OpenAI官网:[Responses](https://platform.openai.com/docs/guides/migrate-to-responses) + + + +## 💡 请求示例 + + + +### 基础文本响应 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": "讲一个三句话的关于独角兽的睡前故事。" + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1", + "output": [ + { + "type": "message", + "id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "在一个宁静的月夜下,一只名叫璐米娜的独角兽发现了一个倒映着星星的隐藏水池。当她将独角浸入水中时,水池开始闪烁,显现出通往一个有着无尽夜空的魔法世界的路径。充满好奇,璐米娜为所有做梦的人许下愿望,希望他们能找到自己的隐藏魔法,当她回头望去,她的蹄印像星尘一样闪烁。", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 36, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 87, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 123 + }, + "user": null, + "metadata": {} +} +``` + + + +### 图像分析响应 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "描述这张图片中的内容"}, + { + "type": "input_image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ] + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ccd3a9da748190baa7f1570fe91ac604becb25c45c1d41", + "object": "response", + "created_at": 1741476777, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1", + "output": [ + { + "type": "message", + "id": "msg_67ccd3acc8d48190a77525dc6de64b4104becb25c45c1d41", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "这张图片展示了一条木制栈道或小径穿过茂密的绿色草地,上方是点缀着几朵云的蓝天。场景呈现出一个宁静的自然区域,可能是公园或自然保护区。背景中有树木和灌木丛。整个景观展现出和谐的自然环境,栈道为游客提供了一条穿过湿地或草原而不影响周围生态系统的路径。", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 328, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 52, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 380 + }, + "user": null, + "metadata": {} +} +``` + + + +### 网络搜索工具 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "tools": [{ "type": "web_search_preview" }], + "input": "今天有什么积极正面的新闻?" + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c", + "object": "response", + "created_at": 1741484430, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1", + "output": [ + { + "type": "web_search_call", + "id": "ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c", + "status": "completed" + }, + { + "type": "message", + "id": "msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "截至今天,2025年3月9日,一则值得关注的积极新闻是中国科学家在可再生能源领域取得重大突破,成功研发出一种新型高效太阳能电池,转化率达到了创纪录的35%,这可能会极大推动清洁能源的普及和应用。这项技术预计将使太阳能发电成本降低约40%,为全球减少碳排放提供了新的解决方案。", + "annotations": [ + { + "type": "url_citation", + "start_index": 42, + "end_index": 100, + "url": "https://example.com/renewable-energy-breakthrough/?utm_source=chatgpt.com", + "title": "中国科学家在可再生能源领域取得重大突破" + }, + { + "type": "url_citation", + "start_index": 101, + "end_index": 150, + "url": "https://example.com/solar-cell-efficiency-record/?utm_source=chatgpt.com", + "title": "新型高效太阳能电池转化率创纪录" + }, + { + "type": "url_citation", + "start_index": 151, + "end_index": 200, + "url": "https://example.com/clean-energy-cost-reduction/?utm_source=chatgpt.com", + "title": "太阳能发电成本有望降低40%" + } + ] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "web_search_preview", + "domains": [], + "search_context_size": "medium", + "user_location": { + "type": "approximate", + "city": null, + "country": "US", + "region": null, + "timezone": null + } + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 328, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 356, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 684 + }, + "user": null, + "metadata": {} +} +``` + + + +### 文件搜索工具 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "tools": [{ + "type": "file_search", + "vector_store_ids": ["vs_1234567890"], + "max_num_results": 20 + }], + "input": "古代棕龙有哪些特性和属性?" + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ccf4c55fc48190b71bd0463ad3306d09504fb6872380d7", + "object": "response", + "created_at": 1741485253, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1", + "output": [ + { + "type": "file_search_call", + "id": "fs_67ccf4c63cd08190887ef6464ba5681609504fb6872380d7", + "status": "completed", + "queries": [ + "古代棕龙的特性和属性" + ], + "results": null + }, + { + "type": "message", + "id": "msg_67ccf4c93e5c81909d595b369351a9d309504fb6872380d7", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "根据资料,古代棕龙具有以下特性和属性:\n\n1. 物理特征:古代棕龙体型庞大,体长可达25-30米,翼展约35米。它们的鳞片呈深棕色至铜色,随着年龄增长会变得更加暗沉。头部有特征性的双角和脊刺,下颚强壮,适合撕裂猎物。\n\n2. 能力:它们能喷吐强力的酸液,对目标造成严重腐蚀伤害。古代棕龙还拥有出色的掘地能力,常在沙漠或山地挖掘复杂的巢穴系统。\n\n3. 智力:被认为是龙族中最为狡猾和有耐心的品种,智力极高,精通多种语言,并具有复杂的战术思维。\n\n4. 栖息地:主要栖息在干旱的山地和沙漠地区,喜欢炎热干燥的环境。\n\n5. 宝藏:古代棕龙以其庞大的宝藏闻名,特别喜爱收集铜币、红宝石和火焰魔法物品。\n\n6. 寿命:是所有龙种中寿命最长的之一,可活2000-2500年,随着年龄增长其力量和魔法能力也会增强。\n\n7. 性格:极度领地意识强,性格暴躁易怒,对侵入者毫不留情,但也以其罕见的耐心著称,能为复仇等待几个世纪。", + "annotations": [ + { + "type": "file_citation", + "index": 80, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 233, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 345, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 420, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 520, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 580, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 655, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 781, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + } + ] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "file_search", + "filters": null, + "max_num_results": 20, + "ranking_options": { + "ranker": "auto", + "score_threshold": 0.0 + }, + "vector_store_ids": [ + "vs_1234567890" + ] + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 18307, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 348, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18655 + }, + "user": null, + "metadata": {} +} +``` + + + +### 流式响应 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "instructions": "你是一个有帮助的助手。", + "input": "你好!", + "stream": true + }' +``` + + + +**流式响应示例:** + + + +```Plain Text +event: response.created +data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}} + +event: response.content_part.added +data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"你好"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"!"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":" 我"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"能"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"为"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"您"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"提供"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"什么"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"帮助"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"吗"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"?"} + +event: response.output_text.done +data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"你好! 我能为您提供什么帮助吗?"} + +event: response.content_part.done +data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}} + +event: response.output_item.done +data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}} + +event: response.completed +data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}} +``` + + + +### 函数调用 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": "波士顿今天的天气如何?", + "tools": [ + { + "type": "function", + "name": "get_current_weather", + "description": "获取指定位置的当前天气", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市和州,例如 San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location", "unit"] + } + } + ], + "tool_choice": "auto" + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ca09c5efe0819096d0511c92b8c890096610f474011cc0", + "object": "response", + "created_at": 1741294021, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "function_call", + "id": "fc_67ca09c6bedc8190a7abfec07b1a1332096610f474011cc0", + "call_id": "call_unLAR8MvFNptuiZK6K6HCy5k", + "name": "get_current_weather", + "arguments": "{\"location\":\"波士顿, MA\",\"unit\":\"celsius\"}", + "status": "completed" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "获取指定位置的当前天气", + "name": "get_current_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市和州,例如 San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": [ + "celsius", + "fahrenheit" + ] + } + }, + "required": [ + "location", + "unit" + ] + }, + "strict": true + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 291, + "output_tokens": 23, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 314 + }, + "user": null, + "metadata": {} +} +``` + + + +### 推理能力 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "o3-mini", + "input": "一只啄木鸟能啄多少木头?", + "reasoning": { + "effort": "high" + } + }' +``` + + + +**响应示例:** + + + +```JSON +{ + "id": "resp_67ccd7eca01881908ff0b5146584e408072912b2993db808", + "object": "response", + "created_at": 1741477868, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "o1-2024-12-17", + "output": [ + { + "type": "message", + "id": "msg_67ccd7f7b5848190a6f3e95d809f6b44072912b2993db808", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "这是一个源自英文绕口令"How much wood would a woodchuck chuck if a woodchuck could chuck wood"的问题。在现实中,啄木鸟(woodpecker)和土拨鼠(woodchuck)是不同的动物,而且土拨鼠实际上并不"啄(chuck)"木头。\n\n从科学角度看,啄木鸟每天确实会啄树木以寻找食物、建造巢穴或进行通讯。一只啄木鸟平均每天可能啄树约8000-12000次,视物种和具体目的而定。如果我们将这转换为木材量,假设每次啄击移除约0.1-0.2立方厘米的木材,那么一只啄木鸟理论上每天可能移除约800-2400立方厘米的木材。\n\n然而,啄木鸟主要是为了觅食和筑巢而啄木,而不是单纯地移除木材,所以这个计算只是一个有趣的理论估算。", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": "high", + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 81, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 1035, + "output_tokens_details": { + "reasoning_tokens": 832 + }, + "total_tokens": 1116 + }, + "user": null, + "metadata": {} +} +``` + + + +## 📮 请求 + + + +### 端点 + + + +```Plain Text +POST /v1/responses +``` + + + +创建模型响应。提供文本或图像输入以生成文本或JSON输出。让模型调用您自己的自定义代码或使用内置工具(如网络搜索或文件搜索)将您自己的数据用作模型响应的输入。 + + + +### 鉴权方法 + + + +在请求头中包含以下内容进行 API 密钥认证: + + + +```Plain Text +Authorization: Bearer $API_KEY +``` + + + +其中 `$API_KEY` 是您的 API 密钥。 + + + +### 请求体参数 + + + +#### input + + + +**类型**: 字符串或数组 + +**必需**: 是 + + + +提供给模型的文本、图像或文件输入,用于生成响应。 + + + +##### 可能的类型 + + + +|类型|描述| +|---|---| +|字符串|文本输入,相当于具有用户角色的文本输入| +|输入项数组|包含不同内容类型的一个或多个输入项列表| + + + +##### 输入消息对象 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|content|字符串或数组|是|提供给模型的文本、图像或音频输入,用于生成响应。也可以包含之前的助手响应| +|role|字符串|是|输入消息的角色。可选值:`user`、`assistant`、`system` 或 `developer`| +|type|字符串|否|输入消息的类型,始终为 `message`| + + + +##### 内容项类型 + + + +###### 文本输入 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|text|字符串|是|提供给模型的文本输入| +|type|字符串|是|输入项的类型,始终为 `input_text`| + + + +###### 图像输入 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|detail|字符串|是|要发送给模型的图像的详细级别。可选值:`high`、`low` 或 `auto`。默认为 `auto`| +|type|字符串|是|输入项的类型,始终为 `input_image`| +|file\_id|字符串|否|要发送给模型的文件ID| +|image\_url|字符串|否|要发送给模型的图像URL。可以是完整的URL或数据URL中的base64编码图像| + + + +###### 文件输入 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|输入项的类型,始终为 `input_file`| +|file\_data|字符串|否|要发送给模型的文件内容| +|file\_id|字符串|否|要发送给模型的文件ID| +|filename|字符串|否|要发送给模型的文件名| + + + +##### 输出项类型 + + + +###### 输出文本 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|text|字符串|是|模型生成的文本输出| +|type|字符串|是|输出项的类型,始终为 `output_text`| +|annotations|数组|是|文本输出的注释| + + + +###### 注释类型 + + + +文件引用: + + + +|属性|类型|必需|描述| +|---|---|---|---| +|file\_id|字符串|是|文件的ID| +|index|整数|是|文件在文件列表中的索引| +|type|字符串|是|文件引用的类型,始终为 `file_citation`| + + + +URL引用: + + + +|属性|类型|必需|描述| +|---|---|---|---| +|end\_index|整数|是|URL引用在消息中的最后一个字符的索引| +|start\_index|整数|是|URL引用在消息中的第一个字符的索引| +|title|字符串|是|网络资源的标题| +|type|字符串|是|URL引用的类型,始终为 `url_citation`| +|url|字符串|是|网络资源的URL| + + + +文件路径: + + + +|属性|类型|必需|描述| +|---|---|---|---| +|file\_id|字符串|是|文件的ID| +|index|整数|是|文件在文件列表中的索引| +|type|字符串|是|文件路径的类型,始终为 `file_path`| + + + +###### 拒绝响应 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|refusal|字符串|是|模型的拒绝解释| +|type|字符串|是|拒绝的类型,始终为 `refusal`| + + + +##### 工具调用类型 + + + +###### 文件搜索工具调用 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|id|字符串|是|文件搜索工具调用的唯一ID| +|queries|数组|是|用于搜索文件的查询| +|status|字符串|是|文件搜索工具调用的状态。可能值包括:`in_progress`、`searching`、`incomplete` 或 `failed`| +|type|字符串|是|文件搜索工具调用的类型,始终为 `file_search_call`| +|results|数组或null|否|文件搜索工具调用的结果| + + + +###### 网络搜索工具调用 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|id|字符串|是|网络搜索工具调用的唯一ID| +|status|字符串|是|网络搜索工具调用的状态| +|type|字符串|是|网络搜索工具调用的类型,始终为 `web_search_call`| + + + +###### 函数工具调用 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|arguments|字符串|是|传递给函数的参数的JSON字符串| +|call\_id|字符串|是|模型生成的函数工具调用的唯一ID| +|name|字符串|是|要运行的函数的名称| +|type|字符串|是|函数工具调用的类型,始终为 `function_call`| +|id|字符串|否|函数工具调用的唯一ID| +|status|字符串|否|项目的状态。可能值:`in_progress`、`completed`或`incomplete`| + + + +###### 计算机工具调用 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|action|对象|是|计算机交互的操作,如点击、拖拽等| +|call\_id|字符串|是|响应工具调用输出时使用的标识符| +|id|字符串|是|计算机调用的唯一ID| +|pending\_safety\_checks|数组|是|计算机调用的待处理安全检查| +|status|字符串|是|项目的状态。可能值:`in_progress`、`completed`或`incomplete`| +|type|字符串|是|计算机调用的类型,始终为 `computer_call`| + + + +计算机操作类型: + + + +|操作类型|描述| +|---|---| +|click|鼠标点击操作| +|double\_click|鼠标双击操作| +|drag|拖拽操作| +|keypress|按键操作| +|move|鼠标移动操作| +|screenshot|屏幕截图操作| +|scroll|滚动操作| +|type|文本输入操作| +|wait|等待操作| + + + +###### 计算机工具调用输出 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|call\_id|字符串|是|产生输出的计算机工具调用的ID| +|output|对象|是|用于计算机使用工具的计算机屏幕截图图像| +|type|字符串|是|计算机工具调用输出的类型,始终为 `computer_call_output`| +|acknowledged\_safety\_checks|数组|否|API报告的已被开发者确认的安全检查| +|id|字符串|否|计算机工具调用输出的ID| +|status|字符串|否|输入消息的状态。可能值:`in_progress`、`completed`或`incomplete`| + + + +###### 函数工具调用输出 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|call\_id|字符串|是|模型生成的函数工具调用的唯一ID| +|output|字符串|是|函数工具调用输出的JSON字符串| +|type|字符串|是|函数工具调用输出的类型,始终为 `function_call_output`| +|id|字符串|否|函数工具调用输出的唯一ID| +|status|字符串|否|项目的状态。可能值:`in_progress`、`completed`或`incomplete`| + + + +##### 推理相关项 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|id|字符串|是|推理内容的唯一标识符| +|summary|数组|是|推理文本内容| +|type|字符串|是|对象的类型,始终为 `reasoning`| +|encrypted\_content|字符串或null|否|推理项的加密内容 \- 当使用 `reasoning.encrypted_content` 包含参数生成响应时填充| +|status|字符串|否|项目的状态。可能值:`in_progress`、`completed`或`incomplete`| + + + +推理摘要: + + + +|属性|类型|必需|描述| +|---|---|---|---| +|text|字符串|是|模型生成响应时使用的推理的简短摘要| +|type|字符串|是|对象的类型,始终为 `summary_text`| + + + +##### 项目引用 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|id|字符串|是|要引用的项目的ID| +|type|字符串|否|要引用的项目类型,始终为 `item_reference`| + + + +#### model + + + +**类型**: 字符串 + +**必需**: 是 + + + +用于生成响应的模型ID,例如 gpt\-4\.1 或 o3。OpenAI 提供各种具有不同能力、性能特性和价格点的模型。请参阅模型指南以浏览和比较可用模型。 + + + +#### include + + + +**类型**: 数组或null + +**必需**: 否 + + + +指定要在模型响应中包含的附加输出数据。当前支持的值包括: + + + +|值|描述| +|---|---| +|`file_search_call.results`|包含文件搜索工具调用的搜索结果| +|`message.input_image.image_url`|包含输入消息中的图像URL| +|`computer_call_output.output.image_url`|包含电脑调用输出中的图像URL| +|`reasoning.encrypted_content`|在推理项输出中包含推理标记的加密版本| + + + +#### instructions + + + +**类型**: 字符串或null + +**必需**: 否 + + + +作为模型上下文中的第一项插入系统(或开发者)消息。 + + + +当与 `previous_response_id` 一起使用时,前一个响应中的指令不会被带到下一个响应。这使得在新响应中轻松切换系统(开发者)消息变得简单。 + + + +#### max\_output\_tokens + + + +**类型**: 整数或null + +**必需**: 否 + + + +可以为响应生成的令牌数量的上限,包括可见输出令牌和推理令牌。 + + + +#### metadata + + + +**类型**: 对象 + +**必需**: 否 + + + +可以附加到对象的16个键值对集合。这对于以结构化格式存储对象的其他信息很有用,并可以通过 API 或仪表板查询对象。 + + + +键是最大长度为64个字符的字符串。值是最大长度为512个字符的字符串。 + + + +#### parallel\_tool\_calls + + + +**类型**: 布尔值或null + +**必需**: 否 + +**默认值**: true + + + +是否允许模型并行运行工具调用。 + + + +#### previous\_response\_id + + + +**类型**: 字符串或null + +**必需**: 否 + + + +模型的前一个响应的唯一ID。使用此参数创建多轮对话。了解更多关于对话状态。 + + + +#### reasoning + + + +**类型**: 对象或null + +**必需**: 否 + +**仅适用于o系列模型** + + + +推理模型的配置选项。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|effort|字符串或null|否|推理的努力程度,可选值: `low`, `medium`, `high`。默认值为 `medium`。降低推理努力可以加快响应速度并减少响应中用于推理的令牌数| +|summary|字符串或null|否|模型执行的推理摘要。这对于调试和理解模型的推理过程很有用。可选值: `auto`, `concise`, `detailed`| +|generate\_summary|字符串或null|否|**已弃用**: 请使用 `summary` 替代。模型执行的推理摘要。可选值: `auto`, `concise`, `detailed`| + + + +#### service\_tier + + + +**类型**: 字符串或null + +**必需**: 否 + +**默认值**: auto + + + +指定用于处理请求的延迟层级。此参数与订阅了 scale tier 服务的客户相关: + + + +|值|描述| +|---|---| +|`auto`|如果项目启用了 Scale tier,系统将使用 scale tier 信用直到用完;如果项目未启用 Scale tier,请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证| +|`default`|请求将使用默认服务层级处理,具有较低的正常运行时间 SLA 且无延迟保证| +|`flex`|请求将使用 Flex Processing 服务层级处理。了解更多信息请参阅官方文档| + + + +当未设置此参数时,默认行为为 `auto`。 + + + +当设置此参数时,响应体将包含已使用的 `service_tier`。 + + + +#### store + + + +**类型**: 布尔值或null + +**必需**: 否 + +**默认值**: true + + + +是否存储生成的模型响应以供以后通过 API 检索。 + + + +#### stream + + + +**类型**: 布尔值或null + +**必需**: 否 + +**默认值**: false + + + +如果设置为 true,模型响应数据将在生成时使用服务器发送的事件流式传输到客户端。 + + + +#### temperature + + + +**类型**: 数字或null + +**必需**: 否 + +**默认值**: 1 + + + +要使用的采样温度,介于 0 和 2 之间。较高的值(如0\.8)会使输出更加随机,而较低的值(如0\.2)会使其更加集中和确定性。我们通常建议更改此值或 `top_p`,但不要同时更改。 + + + +#### text + + + +**类型**: 对象 + +**必需**: 否 + + + +模型文本响应的配置选项。可以是纯文本或结构化JSON数据。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|format|对象|否|指定模型必须输出的格式| + + + +配置 `{ "type": "json_schema" }` 启用结构化输出,确保模型将匹配您提供的JSON模式。更多信息请参阅结构化输出指南。 + + + +默认格式为 `{ "type": "text" }`,没有其他选项。 + + + +**不推荐用于gpt\-4o及更新的模型**: + +设置为 `{ "type": "json_object" }` 启用较旧的JSON模式,确保模型生成的消息是有效的JSON。对于支持的模型,首选使用 `json_schema`。 + + + +##### 文本格式类型 + + + +###### 文本 \(Text\) + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|定义的响应格式类型。始终为 `text`| + + + +###### JSON模式 \(JSON Schema\) + + + +|属性|类型|必需|描述| +|---|---|---|---| +|name|字符串|是|响应格式的名称。必须包含a\-z, A\-Z, 0\-9,或包含下划线和破折号,最大长度为64| +|schema|对象|是|响应格式的模式,描述为JSON Schema对象| +|type|字符串|是|定义的响应格式类型。始终为 `json_schema`| +|description|字符串|否|响应格式用途的描述,模型用它来确定如何以该格式响应| +|strict|布尔值或null|否|是否在生成输出时启用严格模式遵循。默认为 `false`。如果设置为 `true`,模型将始终遵循 schema 字段中定义的确切模式。严格模式下只支持JSON Schema的子集| + + + +###### JSON对象 \(JSON Object\) + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|定义的响应格式类型。始终为 `json_object`| + + + +注意:如果没有指示模型这样做的系统或用户消息,模型将不会生成JSON。对于支持的模型,建议使用 `json_schema`。 + + + +#### tool\_choice + + + +**类型**: 字符串或对象 + +**必需**: 否 + + + +模型如何选择生成响应时使用的工具(或多个工具)。请参阅 `tools` 参数了解如何指定模型可以调用的工具。 + + + +##### 可能的类型 + + + +###### 工具选择模式 \(Tool choice mode\) + + + +**类型**: 字符串 + + + +控制模型是否调用工具以及调用哪种工具。 + + + +|值|描述| +|---|---| +|`none`|模型不会调用任何工具,而是生成一条消息| +|`auto`|模型可以在生成消息或调用一个或多个工具之间选择| +|`required`|模型必须调用一个或多个工具| + + + +###### 托管工具 \(Hosted tool\) + + + +**类型**: 对象 + + + +指示模型应使用内置工具生成响应。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|模型应使用的托管工具类型。允许的值有:`file_search`、`web_search_preview`、`computer_use_preview`| + + + +###### 函数工具 \(Function tool\) + + + +**类型**: 对象 + + + +使用此选项强制模型调用特定函数。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|name|字符串|是|要调用的函数名称| +|type|字符串|是|对于函数调用,类型始终为 `function`| + + + +#### tools + + + +**类型**: 数组 + +**必需**: 否 + + + +模型在生成响应时可能调用的工具数组。你可以通过设置 `tool_choice` 参数来指定使用哪个工具。 + + + +你可以提供给模型的两类工具是: + + + +- **内置工具**:由OpenAI提供的扩展模型能力的工具,如网络搜索或文件搜索。 + +- **函数调用(自定义工具)**:由您定义的函数,使模型能够调用您自己的代码。 + + + +##### 文件搜索工具 \(File search\) + + + +**类型**: 对象 + + + +一个搜索已上传文件中相关内容的工具。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|文件搜索工具的类型,始终为 `file_search`| +|vector\_store\_ids|数组|是|要搜索的向量存储ID列表| +|filters|对象|否|要应用的过滤器| +|max\_num\_results|整数|否|返回的最大结果数。此数字应介于1到50之间(含)| +|ranking\_options|对象|否|搜索排名选项| + + + +###### 过滤器类型 + + + +**比较过滤器 \(Comparison Filter\)** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|key|字符串|是|要与值进行比较的键| +|type|字符串|是|指定比较运算符: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`\\- eq: 等于\\- ne: 不等于\\- gt: 大于\\- gte: 大于等于\\- lt: 小于\\- lte: 小于等于| +|value|字符串/数字/布尔值|是|要与属性键比较的值;支持字符串、数字或布尔类型| + + + +**复合过滤器 \(Compound Filter\)** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|filters|数组|是|要组合的过滤器数组。项目可以是比较过滤器或复合过滤器| +|type|字符串|是|操作类型: `and` 或 `or`| + + + +###### 排名选项 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|ranker|字符串|否|文件搜索使用的排名器| +|score\_threshold|数字|否|文件搜索的分数阈值,介于0和1之间的数字。接近1的数字将尝试仅返回最相关的结果,但可能返回更少的结果| + + + +##### 函数工具 \(Function\) + + + +**类型**: 对象 + + + +定义模型可以选择调用的您自己代码中的函数。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|函数工具的类型,始终为 `function`| +|name|字符串|是|要调用的函数名称| +|parameters|对象|是|描述函数参数的JSON模式对象| +|strict|布尔值|是|是否强制严格参数验证。默认为 `true`| +|description|字符串|否|函数的描述。模型用它来确定是否调用函数| + + + +##### 网络搜索工具 \(Web search preview\) + + + +**类型**: 对象 + + + +此工具搜索网络上的相关结果,用于响应。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|网络搜索工具的类型。可选值: `web_search_preview` 或 `web_search_preview_2025_03_11`| +|search\_context\_size|字符串|否|对用于搜索的上下文窗口空间量的高级指导。可选值: `low`, `medium`, `high`。默认为 `medium`| +|user\_location|对象|否|用户的位置| +|domains|数组|否|限制搜索的域名列表| + + + +###### 用户位置 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|位置近似类型。始终为 `approximate`| +|city|字符串|否|用户所在城市的自由文本输入,例如 "San Francisco"| +|country|字符串|否|用户的两字母ISO国家代码,例如 "US"| +|region|字符串|否|用户所在区域的自由文本输入,例如 "California"| +|timezone|字符串|否|用户的IANA时区,例如 "America/Los\_Angeles"| + + + +##### 计算机使用工具 \(Computer use preview\) + + + +**类型**: 对象 + + + +控制虚拟计算机的工具。 + + + +|属性|类型|必需|描述| +|---|---|---|---| +|type|字符串|是|计算机使用工具的类型。始终为 `computer_use_preview`| +|display\_height|整数|是|计算机显示器的高度| +|display\_width|整数|是|计算机显示器的宽度| +|environment|字符串|是|要控制的计算机环境类型| + + + +#### top\_p + + + +**类型**: 数字或null + +**必需**: 否 + +**默认值**: 1 + + + +一种替代采样温度的方法,称为核采样,其中模型考虑具有 top\_p 概率质量的标记结果。因此,0\.1 意味着只考虑包含前 10% 概率质量的标记。 + + + +我们通常建议更改此值或 `temperature`,但不要同时更改。 + + + +#### truncation + + + +**类型**: 字符串或null + +**必需**: 否 + +**默认值**: disabled + + + +用于模型响应的截断策略: + + + +|值|描述| +|---|---| +|`auto`|如果此响应和前一个响应的上下文超过模型的上下文窗口大小,模型将通过删除对话中间的输入项来截断响应以适应上下文窗口| +|`disabled`|如果模型响应将超过模型的上下文窗口大小,请求将失败并显示400错误| + + + +#### user + + + +**类型**: 字符串 + +**必需**: 否 + + + +表示最终用户的唯一标识符,可以帮助OpenAI监控和检测滥用行为。 + + + +## 📥 响应 + + + +返回一个响应对象。 + + + +### 成功响应 + + + +返回一个响应对象,如果请求被流式传输,则返回响应对象的流式序列。 + + + +#### id + +- 类型:字符串 + +- 说明:响应的唯一标识符 + + + +#### object + +- 类型:字符串 + +- 说明:对象类型,值为 "response" + + + +#### created\_at + +- 类型:整数 + +- 说明:响应创建时间戳 + + + +#### status + +- 类型:字符串 + +- 说明:响应状态,如 "completed"、"in\_progress" 等 + + + +#### error + +- 类型:对象或null + +- 说明:如果发生错误,包含错误信息 + + + +#### incomplete\_details + +- 类型:对象或null + +- 说明:如果响应不完整,包含详细信息 + + + +#### instructions + +- 类型:字符串或null + +- 说明:提供给模型的系统指令 + + + +#### max\_output\_tokens + +- 类型:整数或null + +- 说明:最大输出标记数 + + + +#### model + +- 类型:字符串 + +- 说明:使用的模型名称 + + + +#### output + +- 类型:数组 + +- 说明:包含生成的回复和工具调用 + +- 可能包含: + + - 消息对象(`type`: "message") + + - 工具使用对象(`type`: "tool\_use") + + + +#### parallel\_tool\_calls + +- 类型:布尔值 + +- 说明:是否启用并行工具调用 + + + +#### previous\_response\_id + +- 类型:字符串或null + +- 说明:前一个响应的ID(用于多轮对话) + + + +#### reasoning + +- 类型:对象 + +- 说明:推理相关信息 + + + +#### store + +- 类型:布尔值 + +- 说明:是否存储此响应 + + + +#### temperature + +- 类型:数字 + +- 说明:使用的采样温度 + + + +#### text + +- 类型:对象 + +- 说明:文本输出格式配置 + + + +#### tool\_choice + +- 类型:字符串 + +- 说明:工具选择策略 + + + +#### tools + +- 类型:数组 + +- 说明:可用工具列表 + + + +#### top\_p + +- 类型:数字 + +- 说明:核采样阈值 + + + +#### truncation + +- 类型:字符串 + +- 说明:截断策略 + + + +#### usage + +- 类型:对象 + +- 说明:token 使用统计 + +- 属性: + + - `input_tokens`: 输入使用的 token 数 + + - `input_tokens_details`: 输入token详细信息 + + - `output_tokens`: 输出使用的 token 数 + + - `output_tokens_details`: 输出token详细信息 + + - `total_tokens`: 总 token 数 + + + +#### user + +- 类型:字符串或null + +- 说明:用户标识符 + + + +#### metadata + +- 类型:对象 + +- 说明:附加的元数据信息 + + + + + + + +# Anthropic 对话格式(Messages) + + + +\!\!\! info "官方文档" + +\- [Anthropic Messages](https://docs.anthropic.com/en/api/messages) + +\- [Anthropic Streaming Messages](https://docs.anthropic.com/en/api/messages-streaming) + + + +## 📝 简介 + + + +给定一组包含文本和/或图像内容的结构化输入消息列表,模型将生成对话中的下一条消息。Messages API 可用于单次查询或无状态的多轮对话。 + + + +## 💡 请求示例 + + + +### 基础文本对话 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/messages \ + --header "anthropic-version: 2023-06-01" \ + --header "content-type: application/json" \ + --header "x-api-key: $API_KEY" \ + --data \ +'{ + "model": "claude-3-5-sonnet-20241022", + "max_tokens": 1024, + "messages": [ + {"role": "user", "content": "Hello, world"} + ] +}' +``` + + + +**响应示例:** + +```JSON +{ + "content": [ + { + "text": "Hi! My name is Claude.", + "type": "text" + } + ], + "id": "msg_013Zva2CMHLNnXjNJKqJ2EF", + "model": "claude-3-5-sonnet-20241022", + "role": "assistant", + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "input_tokens": 2095, + "output_tokens": 503 + } +} +``` + + + +### 图像分析对话 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/messages \ + --header "anthropic-version: 2023-06-01" \ + --header "content-type: application/json" \ + --header "x-api-key: $API_KEY" \ + --data \ +'{ + "model": "claude-3-5-sonnet-20241022", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "/9j/4AAQSkZJRg..." + } + }, + { + "type": "text", + "text": "这张图片里有什么?" + } + ] + } + ] +}' +``` + + + +**响应示例:** + +```JSON +{ + "content": [ + { + "text": "这张图片显示了一只橙色的猫咪正在窗台上晒太阳。猫咪看起来很放松,眯着眼睛享受阳光。窗外可以看到一些绿色的植物。", + "type": "text" + } + ], + "id": "msg_013Zva2CMHLNnXjNJKqJ2EF", + "model": "claude-3-5-sonnet-20241022", + "role": "assistant", + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "input_tokens": 3050, + "output_tokens": 892 + } +} +``` + + + +### 工具调用 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/messages \ + --header "anthropic-version: 2023-06-01" \ + --header "content-type: application/json" \ + --header "x-api-key: $API_KEY" \ + --data \ +'{ + "model": "claude-3-5-sonnet-20241022", + "messages": [ + { + "role": "user", + "content": "今天北京的天气怎么样?" + } + ], + "tools": [ + { + "name": "get_weather", + "description": "获取指定位置的当前天气", + "input_schema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市名称,如:北京" + } + }, + "required": ["location"] + } + } + ] +}' +``` + + + +**响应示例:** + +```JSON +{ + "content": [ + { + "type": "tool_use", + "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + "name": "get_weather", + "input": { "location": "北京" } + } + ], + "id": "msg_013Zva2CMHLNnXjNJKqJ2EF", + "model": "claude-3-5-sonnet-20241022", + "role": "assistant", + "stop_reason": "tool_use", + "stop_sequence": null, + "type": "message", + "usage": { + "input_tokens": 2156, + "output_tokens": 468 + } +} +``` + + + +### 流式响应 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1/messages \ + --header "anthropic-version: 2023-06-01" \ + --header "content-type: application/json" \ + --header "x-api-key: $API_KEY" \ + --data \ +'{ + "model": "claude-3-5-sonnet-20241022", + "messages": [ + { + "role": "user", + "content": "讲个故事" + } + ], + "stream": true +}' +``` + + + +**响应示例:** + +```JSON +{ + "type": "message_start", + "message": { + "id": "msg_013Zva2CMHLNnXjNJKqJ2EF", + "model": "claude-3-5-sonnet-20241022", + "role": "assistant", + "type": "message" + } +} +{ + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text" + } +} +{ + "type": "content_block_delta", + "index": 0, + "delta": { + "text": "从前" + } +} +{ + "type": "content_block_delta", + "index": 0, + "delta": { + "text": "有一只" + } +} +{ + "type": "content_block_delta", + "index": 0, + "delta": { + "text": "小兔子..." + } +} +{ + "type": "content_block_stop", + "index": 0 +} +{ + "type": "message_delta", + "delta": { + "stop_reason": "end_turn", + "usage": { + "input_tokens": 2045, + "output_tokens": 628 + } + } +} +{ + "type": "message_stop" +} +``` + + + +## 📮 请求 + + + +### 端点 + + + +```Plain Text +POST /v1/messages +``` + + + +### 鉴权方法 + + + +在请求头中包含以下内容进行 API 密钥认证: + + + +```Plain Text +x-api-key: $API_KEY +``` + + + +其中 `$API_KEY` 是您的 API 密钥。您可以通过控制台获取 API 密钥,每个密钥仅限于一个工作区使用。 + + + +### 请求头参数 + + + +#### `anthropic-beta` + + + +- 类型:字符串 + +- 必需:否 + + + +指定要使用的 beta 版本,支持用逗号分隔的列表如 `beta1,beta2`,或多次指定该请求头。 + + + +#### `anthropic-version` + + + +- 类型:字符串 + +- 必需:是 + + + +指定要使用的 API 版本。 + + + +### 请求体参数 + + + +#### `max_tokens` + + + +- 类型:整数 + +- 必需:是 + + + +生成的最大 token 数量。不同模型有不同的限制,详见模型文档。范围 `x > 1`。 + + + +#### `messages` + + + +- 类型:对象数组 + +- 必需:是 + + + +输入消息列表。模型被训练为在用户和助手之间交替进行对话。创建新消息时,您可以使用 messages 参数指定之前的对话轮次,模型将生成对话中的下一条消息。连续的用户或助手消息会被合并为单个轮次。 + + + +每个消息必须包含 `role` 和 `content` 字段。您可以指定单个用户角色消息,或包含多个用户和助手消息。如果最后一条消息使用助手角色,响应内容将直接从该消息的内容继续,这可以用来约束模型的响应。 + + + +**单条用户消息示例:** + +```JSON +[{"role": "user", "content": "Hello, Claude"}] +``` + + + +**多轮对话示例:** + +```JSON +[ + {"role": "user", "content": "你好。"}, + {"role": "assistant", "content": "你好!我是 Claude。有什么可以帮你的吗?"}, + {"role": "user", "content": "请用简单的话解释什么是 LLM?"} +] +``` + + + +**部分填充的响应示例:** + +```JSON +[ + {"role": "user", "content": "太阳的希腊语名字是什么? (A) Sol (B) Helios (C) Sun"}, + {"role": "assistant", "content": "正确答案是 ("} +] +``` + + + +每个消息的 content 可以是字符串或内容块数组。使用字符串相当于一个 "text" 类型的内容块数组的简写。以下两种写法等效: + + + +```JSON +{"role": "user", "content": "Hello, Claude"} +``` + + + +```JSON +{ + "role": "user", + "content": [{"type": "text", "text": "Hello, Claude"}] +} +``` + + + +从 Claude 3 模型开始,您还可以发送图片内容块: + + + +```JSON +{ + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "/9j/4AAQSkZJRg..." + } + }, + { + "type": "text", + "text": "这张图片里有什么?" + } + ] +} +``` + + + +> 目前支持的图片格式包括: base64, image/jpeg、image/png、image/gif 和 image/webp。 +> +> + + + +##### `messages.role` + + + +- 类型:枚举字符串 + +- 必需:是 + +- 可选值:user, assistant + + + +注意:Messages API 中没有 "system" 角色,如果需要系统提示,请使用顶层的 system 参数。 + + + +##### `messages.content` + + + +- 类型:字符串或对象数组 + +- 必需:是 + + + +消息内容可以是以下几种类型之一: + + + +###### 文本内容 \(Text\) + + + +```JSON +{ + "type": "text", // 必需,枚举值: "text" + "text": "Hello, Claude", // 必需,最小长度: 1 + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } +} +``` + + + +###### 图片内容 \(Image\) + + + +```JSON +{ + "type": "image", // 必需,枚举值: "image" + "source": { // 必需 + "type": "base64", // 必需,枚举值: "base64" + "media_type": "image/jpeg", // 必需,支持: image/jpeg, image/png, image/gif, image/webp + "data": "/9j/4AAQSkZJRg..." // 必需,base64 编码的图片数据 + }, + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } +} +``` + + + +###### 工具使用 \(Tool Use\) + + + +```JSON +{ + "type": "tool_use", // 必需,枚举值: "tool_use",默认值 + "id": "toolu_xyz...", // 必需,工具使用的唯一标识符 + "name": "get_weather", // 必需,工具名称,最小长度: 1 + "input": { // 必需,工具的输入参数对象 + // 工具输入参数,具体格式由工具的 input_schema 定义 + }, + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } +} +``` + + + +###### 工具结果 \(Tool Result\) + + + +```JSON +{ + "type": "tool_result", // 必需,枚举值: "tool_result" + "tool_use_id": "toolu_xyz...", // 必需 + "content": "结果内容", // 必需,可以是字符串或内容块数组 + "is_error": false, // 可选,布尔值 + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } +} +``` + + + +当 content 为内容块数组时,每个内容块可以是文本或图片: + + + +```JSON +{ + "type": "tool_result", + "tool_use_id": "toolu_xyz...", + "content": [ + { + "type": "text", // 必需,枚举值: "text" + "text": "分析结果", // 必需,最小长度: 1 + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } + }, + { + "type": "image", // 必需,枚举值: "image" + "source": { // 必需 + "type": "base64", // 必需,枚举值: "base64" + "media_type": "image/jpeg", + "data": "..." + }, + "cache_control": { + "type": "ephemeral" + } + } + ] +} +``` + + + +###### 文档 \(Document\) + + + +```JSON +{ + "type": "document", // 必需,枚举值: "document" + "source": { // 必需 + // 文档源数据 + }, + "cache_control": { + "type": "ephemeral" // 可选,枚举值: "ephemeral" + } +} +``` + + + +注意: + +1. 每种类型都可以包含可选的 `cache_control` 字段,用于控制内容的缓存行为 + +2. 文本内容的最小长度为 1 + +3. 所有类型的 type 字段都是必需的枚举字符串 + +4. 工具结果的 content 字段支持字符串或包含文本/图片的内容块数组 + + + +#### `model` + + + +- 类型:字符串 + +- 必需:是 + + + +要使用的模型名称,详见模型文档。范围 `1 - 256` 个字符。 + + + +#### `metadata` + + + +- 类型:对象 + +- 必需:否 + + + +描述请求元数据的对象。包含以下可选字段: + + + +- `user_id`: 与请求关联的用户的外部标识符。应该是 uuid、哈希值或其他不透明标识符。不要包含任何标识信息如姓名、邮箱或电话号码。最大长度:256。 + + + +#### `stop_sequences` + + + +- 类型:字符串数组 + +- 必需:否 + + + +自定义的停止生成的文本序列。 + + + +#### `stream` + + + +- 类型:布尔值 + +- 必需:否 + + + +是否使用服务器发送事件 \(SSE\) 来增量返回响应内容。 + + + +#### `system` + + + +- 类型:字符串 + +- 必需:否 + + + +系统 prompt,为 Claude 提供背景和指令。这是一种为模型提供上下文和特定目标或角色的方式。注意这与消息中的 role 不同,Messages API 中没有 "system" 角色。 + + + +#### `temperature` + + + +- 类型:数字 + +- 必需:否 + +- 默认值:1\.0 + + + +控制生成随机性,0\.0 \- 1\.0。范围 `0 < x < 1`。建议对于分析性/选择题类任务使用接近 0\.0 的值,对于创造性和生成性任务使用接近 1\.0 的值。 + + + +注意:即使 temperature 设置为 0\.0,结果也不会完全确定。 + + + +#### 🆕 `thinking` + + + +- 类型:对象 + +- 必需:否 + + + +配置 Claude 的扩展思考功能。启用时,响应将包含展示 Claude 在给出最终答案前的思考过程的内容块。需要至少 1,024 个 token 的预算,并计入您的 max\_tokens 限制。 + + + +可以设置为以下两种模式之一: + + + +##### 1\. 启用模式 + + + +```JSON +{ + "type": "enabled", + "budget_tokens": 2048 +} +``` + + + +- `type`: 必需,枚举值: "enabled" + +- `budget_tokens`: 必需,整数。决定 Claude 可以用于内部推理过程的 token 数量。更大的预算可以让模型对复杂问题进行更深入的分析,提高响应质量。必须 ≥1024 且小于 max\_tokens。范围 `x > 1024`。 + + + +##### 2\. 禁用模式 + + + +```JSON +{ + "type": "disabled" +} +``` + + + +- `type`: 必需,枚举值: "disabled" + + + +#### `tool_choice` + + + +- 类型:对象 + +- 必需:否 + + + +控制模型如何使用提供的工具。可以是以下三种类型之一: + + + +##### 1\. Auto 模式 \(自动选择\) + + + +```JSON +{ + "type": "auto", // 必需,枚举值: "auto" + "disable_parallel_tool_use": false // 可选,默认 false。如果为 true,模型最多只会使用一个工具 +} +``` + + + +##### 2\. Any 模式 \(任意工具\) + + + +```JSON +{ + "type": "any", // 必需,枚举值: "any" + "disable_parallel_tool_use": false // 可选,默认 false。如果为 true,模型将恰好使用一个工具 +} +``` + + + +##### 3\. Tool 模式 \(指定工具\) + + + +```JSON +{ + "type": "tool", // 必需,枚举值: "tool" + "name": "get_weather", // 必需,指定要使用的工具名称 + "disable_parallel_tool_use": false // 可选,默认 false。如果为 true,模型将恰好使用一个工具 +} +``` + + + +注意: + +1. Auto 模式:模型可以自行决定是否使用工具 + +2. Any 模式:模型必须使用工具,但可以选择任何可用的工具 + +3. Tool 模式:模型必须使用指定的工具 + + + +#### `tools` + + + +- 类型:对象数组 + +- 必需:否 + + + +定义模型可能使用的工具。工具可以是自定义工具或内置工具类型: + + + +##### 1\. 自定义工具(Tool) + + + +每个自定义工具定义包含: + + + +- `type`: 可选,枚举值: "custom" + +- `name`: 工具名称,必需,1\-64 个字符 + +- `description`: 工具描述,建议尽可能详细 + +- `input_schema`: 工具输入的 JSON Schema 定义,必需 + +- `cache_control`: 缓存控制,可选,type 为 "ephemeral" + + + +示例: + +```JSON +[ + { + "type": "custom", + "name": "get_weather", + "description": "获取指定位置的当前天气", + "input_schema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市名称,如:北京" + } + }, + "required": ["location"] + } + } +] +``` + + + +##### 2\. 计算机工具 \(ComputerUseTool\) + + + +```JSON +{ + "type": "computer_20241022", // 必需 + "name": "computer", // 必需,枚举值: "computer" + "display_width_px": 1024, // 必需,显示宽度(像素) + "display_height_px": 768, // 必需,显示高度(像素) + "display_number": 0, // 可选,X11 显示编号 + "cache_control": { + "type": "ephemeral" // 可选 + } +} +``` + + + +##### 3\. Bash 工具 \(BashTool\) + + + +```JSON +{ + "type": "bash_20241022", // 必需 + "name": "bash", // 必需,枚举值: "bash" + "cache_control": { + "type": "ephemeral" // 可选 + } +} +``` + + + +##### 4\. 文本编辑器工具 \(TextEditor\) + + + +```JSON +{ + "type": "text_editor_20241022", // 必需 + "name": "str_replace_editor", // 必需,枚举值: "str_replace_editor" + "cache_control": { + "type": "ephemeral" // 可选 + } +} +``` + + + +当模型使用工具时,会返回 tool\_use 内容块: + + + +```JSON +[ + { + "type": "tool_use", + "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + "name": "get_weather", + "input": { "location": "北京" } + } +] +``` + + + +您可以执行工具并通过 tool\_result 内容块返回结果: + + + +```JSON +[ + { + "type": "tool_result", + "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + "content": "北京当前天气晴朗,温度 25°C" + } +] +``` + + + +#### `top_k` + + + +- 类型:整数 + +- 必需:否 + +- 范围:x \> 0 + + + +从 token 的前 K 个选项中采样。用于移除低概率的"长尾"响应。建议仅在高级用例中使用,通常只需要调整 temperature。 + + + +#### `top_p` + + + +- 类型:数字 + +- 必需:否 + +- 范围:0 \< x \< 1 + + + +使用 nucleus 采样。计算每个后续 token 按概率降序排列的累积分布,在达到 top\_p 指定的概率时截断。建议仅调整 temperature 或 top\_p 其中之一,不要同时使用。 + + + +## 📥 响应 + + + +### 成功响应 + + + +返回一个聊天补全对象,包含以下字段: + + + +#### `content` + + + +- 类型:对象数组 + +- 必需:是 + + + +模型生成的内容,由多个内容块组成。每个内容块都有一个确定其形状的 type。内容块可以是以下类型之一: + + + +##### 文本内容块 \(Text\) + + + +```JSON +{ + "type": "text", // 必需,枚举值: "text",默认值 + "text": "你好,我是 Claude。" // 必需,最大长度: 5000000,最小长度: 1 +} +``` + + + +##### 工具使用内容块 \(Tool Use\) + + + +```JSON +{ + "type": "tool_use", // 必需,枚举值: "tool_use",默认值 + "id": "toolu_xyz...", // 必需,工具使用的唯一标识符 + "name": "get_weather", // 必需,工具名称,最小长度: 1 + "input": { // 必需,工具的输入参数对象 + // 工具输入参数,具体格式由工具的 input_schema 定义 + } +} +``` + + + +示例: + +```JSON +// 文本内容示例 +[{"type": "text", "text": "你好,我是 Claude。"}] + +// 工具使用示例 +[{ + "type": "tool_use", + "id": "toolu_xyz...", + "name": "get_weather", + "input": { "location": "北京" } +}] + +// 混合内容示例 +[ + {"type": "text", "text": "根据天气查询结果:"}, + { + "type": "tool_use", + "id": "toolu_xyz...", + "name": "get_weather", + "input": { "location": "北京" } + } +] +``` + + + +如果请求的最后一条消息是助手角色,响应内容会直接从该消息继续。例如: + + + +```JSON +// 请求 +[ + {"role": "user", "content": "太阳的希腊语名字是什么? (A) Sol (B) Helios (C) Sun"}, + {"role": "assistant", "content": "正确答案是 ("} +] + +// 响应 +[{"type": "text", "text": "B)"}] +``` + + + +#### `id` + + + +- 类型:字符串 + +- 必需:是 + + + +响应的唯一标识符。 + + + +#### `model` + + + +- 类型:字符串 + +- 必需:是 + + + +使用的模型名称。 + + + +#### `role` + + + +- 类型:枚举字符串 + +- 必需:是 + +- 默认值:assistant + + + +生成消息的会话角色,始终为 "assistant"。 + + + +#### `stop_reason` + + + +- 类型:枚举字符串或 null + +- 必需:是 + + + +停止生成的原因,可能的值包括: + + + +- `"end_turn"`: 模型达到自然停止点 + +- `"max_tokens"`: 超过请求的 max\_tokens 或模型的最大限制 + +- `"stop_sequence"`: 生成了自定义停止序列之一 + +- `"tool_use"`: 模型调用了一个或多个工具 + + + +在非流式模式下,此值始终非空。在流式模式下,在 message\_start 事件中为 null,其他情况下非空。 + + + +#### `stop_sequence` + + + +- 类型:字符串或 null + +- 必需:是 + + + +生成的自定义停止序列。如果模型遇到了 stop\_sequences 参数中指定的某个序列,这个字段将包含该匹配的停止序列。如果不是因为停止序列而停止,则为 null。 + + + +#### `type` + + + +- 类型:枚举字符串 + +- 必需:是 + +- 默认值:message + +- 可选值:message + + + +对象类型,对于 Messages 始终为 "message"。 + + + +#### `usage` + + + +- 类型:对象 + +- 必需:是 + + + +计费和限流相关的使用量统计。包含以下字段: + + + +- `input_tokens`: 使用的输入 token 数量,必需,范围 x \> 0 + +- `output_tokens`: 使用的输出 token 数量,必需,范围 x \> 0 + +- `cache_creation_input_tokens`: 创建缓存条目使用的输入 token 数量\(如果适用\),必需,范围 x \> 0 + +- `cache_read_input_tokens`: 从缓存读取的输入 token 数量\(如果适用\),必需,范围 x \> 0 + + + +注意:由于 API 在内部会对请求进行转换和解析,token 计数可能与请求和响应的实际可见内容不完全对应。例如,即使是空字符串响应,output\_tokens 也会是非零值。 + + + +### 错误响应 + + + +当请求出现问题时,API 将返回一个错误响应对象,HTTP 状态码在 4XX\-5XX 范围内。 + + + +#### 常见错误状态码 + + + +- `401 Unauthorized`: API 密钥无效或未提供 + +- `400 Bad Request`: 请求参数无效 + +- `429 Too Many Requests`: 超出 API 调用限制 + +- `500 Internal Server Error`: 服务器内部错误 + + + +错误响应示例: + + + +```JSON +{ + "error": { + "type": "invalid_request_error", + "message": "Invalid API key provided", + "code": "invalid_api_key" + } +} +``` + + + +主要错误类型: + + + +- `invalid_request_error`: 请求参数错误 + +- `authentication_error`: 认证相关错误 + +- `rate_limit_error`: 请求频率超限 + +- `server_error`: 服务器内部错误 + + + + + +# Google Gemini 对话格式(Generate Content) + + + +\!\!\! info "官方文档" + +[Google Gemini Generating content API](https://ai.google.dev/api/generate-content) + + + +## 📝 简介 + + + +Google Gemini API 支持使用图片、音频、代码、工具等生成内容。给定输入 GenerateContentRequest 生成模型响应。支持文本生成、视觉理解、音频处理、长上下文、代码执行、JSON 模式、函数调用等多种功能。 + + + +## 💡 请求示例 + + + +### 基础文本对话 ✅ + + + +```Bash +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts":[{"text": "Write a story about a magic backpack."}] + }] + }' 2> /dev/null +``` + + + +### Nano Banana生图✅ + + +```JSON +curl -X POST "https://king.tokenssr.com/v1beta/models/gemini-3-pro-image-preview:generateContent/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "draw a cat" + } + ] + } + ], + "generationConfig": { + "responseModalities": [ + "TEXT", + "IMAGE" + ], + "imageConfig": { + "aspectRatio": "16:9", + "imageSize": "2K" + } + } + }' +``` + + + +### 图像分析对话 ✅ + + + +```Bash +# 使用临时文件保存base64编码的图片数据 +TEMP_B64=$(mktemp) +trap 'rm -f "$TEMP_B64"' EXIT +base64 $B64FLAGS $IMG_PATH > "$TEMP_B64" + +# 使用临时文件保存JSON载荷 +TEMP_JSON=$(mktemp) +trap 'rm -f "$TEMP_JSON"' EXIT + +cat > "$TEMP_JSON" << EOF +{ + "contents": [{ + "parts":[ + {"text": "Tell me about this instrument"}, + { + "inline_data": { + "mime_type":"image/jpeg", + "data": "$(cat "$TEMP_B64")" + } + } + ] + }] +} +EOF + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d "@$TEMP_JSON" 2> /dev/null +``` + + + +### 函数调用 ✅ + + + +```Bash +cat > tools.json << EOF +{ + "function_declarations": [ + { + "name": "enable_lights", + "description": "Turn on the lighting system." + }, + { + "name": "set_light_color", + "description": "Set the light color. Lights must be enabled for this to work.", + "parameters": { + "type": "object", + "properties": { + "rgb_hex": { + "type": "string", + "description": "The light color as a 6-digit hex string, e.g. ff0000 for red." + } + }, + "required": [ + "rgb_hex" + ] + } + }, + { + "name": "stop_lights", + "description": "Turn off the lighting system." + } + ] +} +EOF + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -d @<(echo ' + { + "system_instruction": { + "parts": { + "text": "You are a helpful lighting system bot. You can turn lights on and off, and you can set the color. Do not perform any other tasks." + } + }, + "tools": ['$(cat tools.json)'], + + "tool_config": { + "function_calling_config": {"mode": "auto"} + }, + + "contents": { + "role": "user", + "parts": { + "text": "Turn on the lights please." + } + } + } +') 2>/dev/null |sed -n '/"content"/,/"finishReason"/p' +``` + + + +### JSON 模式响应 ✅ + + + +```Bash +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ +-H 'Content-Type: application/json' \ +-d '{ + "contents": [{ + "parts":[ + {"text": "List 5 popular cookie recipes"} + ] + }], + "generationConfig": { + "response_mime_type": "application/json", + "response_schema": { + "type": "ARRAY", + "items": { + "type": "OBJECT", + "properties": { + "recipe_name": {"type":"STRING"}, + } + } + } + } +}' 2> /dev/null | head +``` + + + +### 音频处理 🟡 + + + +\!\!\! warning "文件上传限制" + +仅支持通过 `inline_data` 以 base64 方式上传音频,不支持 `file_data.file_uri` 或 File API。 + + + +```Bash +# 使用File API上传音频数据到API请求 +# 使用 base64 inline_data 上传音频数据到 API 请求 +if [[ "$(base64 --version 2>&1)" = *"FreeBSD"* ]]; then + B64FLAGS="--input" +else + B64FLAGS="-w0" +fi +AUDIO_B64=$(base64 $B64FLAGS "$AUDIO_PATH") + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts": [ + {"text": "Please describe this audio file."}, + {"inline_data": {"mime_type": "audio/mpeg", "data": "'$AUDIO_B64'"}} + ] + }] + }' 2> /dev/null | jq ".candidates[].content.parts[].text" +``` + + + +### 视频处理 🟡 + + + +\!\!\! warning "文件上传限制" + +仅支持通过 `inline_data` 以 base64 方式上传视频,不支持 `file_data.file_uri` 或 File API。 + + + +```Bash +# 使用File API上传视频数据到API请求 +# 使用 base64 inline_data 上传视频数据到 API 请求 +if [[ "$(base64 --version 2>&1)" = *"FreeBSD"* ]]; then + B64FLAGS="--input" +else + B64FLAGS="-w0" +fi +VIDEO_B64=$(base64 $B64FLAGS "$VIDEO_PATH") + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts": [ + {"text": "Transcribe the audio from this video and provide visual descriptions."}, + {"inline_data": {"mime_type": "video/mp4", "data": "'$VIDEO_B64'"}} + ] + }] + }' 2> /dev/null | jq ".candidates[].content.parts[].text" +``` + + + +### PDF处理 🟡 + + + +\!\!\! warning "文件上传限制" + +仅支持通过 `inline_data` 以 base64 方式上传 PDF,不支持 `file_data.file_uri` 或 File API。 + + + +```Bash +MIME_TYPE=$(file -b --mime-type "${PDF_PATH}") +# 使用 base64 inline_data 上传 PDF 文件到 API 请求 +if [[ "$(base64 --version 2>&1)" = *"FreeBSD"* ]]; then + B64FLAGS="--input" +else + B64FLAGS="-w0" +fi +PDF_B64=$(base64 $B64FLAGS "$PDF_PATH") + +echo $MIME_TYPE + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts": [ + {"text": "Can you add a few more lines to this poem?"}, + {"inline_data": {"mime_type": "application/pdf", "data": "'$PDF_B64'"}} + ] + }] + }' 2> /dev/null | jq ".candidates[].content.parts[].text" +``` + + + +### 聊天对话 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [ + {"role":"user", + "parts":[{ + "text": "Hello"}]}, + {"role": "model", + "parts":[{ + "text": "Great to meet you. What would you like to know?"}]}, + {"role":"user", + "parts":[{ + "text": "I have two dogs in my house. How many paws are in my house?"}]}, + ] + }' 2> /dev/null | grep "text" +``` + + + +### 流式响应 ✅ + + + +```Bash +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=$API_KEY" \ + -H 'Content-Type: application/json' \ + --no-buffer \ + -d '{ + "contents": [{ + "parts": [{"text": "写一个关于魔法背包的故事"}] + }] + }' +``` + + + +### 代码执行 ✅ + + + +```Bash +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts": [{"text": "计算斐波那契数列的第10项"}] + }], + "tools": [{ + "codeExecution": {} + }] + }' +``` + + + +### 生成配置 ✅ + + + +```Bash +curl https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY \ + -H 'Content-Type: application/json' \ + -X POST \ + -d '{ + "contents": [{ + "parts":[ + {"text": "Explain how AI works"} + ] + }], + "generationConfig": { + "stopSequences": [ + "Title" + ], + "temperature": 1.0, + "maxOutputTokens": 800, + "topP": 0.8, + "topK": 10 + } + }' 2> /dev/null | grep "text" +``` + + + +### 安全设置 ✅ + + + +```Bash +echo '{ + "safetySettings": [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"} + ], + "contents": [{ + "parts":[{ + "text": "'I support Martians Soccer Club and I think Jupiterians Football Club sucks! Write a ironic phrase about them.'"}]}]}' > request.json + +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ + -H 'Content-Type: application/json' \ + -X POST \ + -d @request.json 2> /dev/null +``` + + + +### 系统指令 ✅ + + + +```Bash +curl "https://king.tokenssr.com/v1beta/models/gemini-2.0-flash:generateContent?key=$API_KEY" \ +-H 'Content-Type: application/json' \ +-d '{ "system_instruction": { + "parts": + { "text": "You are a cat. Your name is Neko."}}, + "contents": { + "parts": { + "text": "Hello there"}}}' +``` + + + +## 📮 请求 + + + +### 端点 + + + +#### 生成内容 + +```Plain Text +POST https://king.tokenssr.com/v1beta/{model=models/*}:generateContent +``` + + + +#### 流式生成内容 + +```Plain Text +POST https://king.tokenssr.com/v1beta/{model=models/*}:streamGenerateContent +``` + + + +### 鉴权方法 + + + +在请求URL参数中包含API密钥: + + + +```Plain Text +?key=$API_KEY +``` + + + +其中 `$API_KEY` 是您的 Google AI API 密钥。 + + + +### 路径参数 + + + +#### `model` + + + +- 类型:字符串 + +- 必需:是 + + + +用于生成补全项的模型名称。 + + + +格式:`models/{model}`,例如 `models/gemini-2.0-flash` + + + +### 请求体参数 + + + +#### `contents` + + + +- 类型:数组 + +- 必需:是 + + + +与模型当前对话的内容。对于单轮查询,这是单个实例。对于聊天等多轮查询,这是包含对话历史记录和最新请求的重复字段。 + + + +**Content 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`parts`|数组|是|有序的内容部分,构成单个消息| +|`role`|字符串|否|对话中内容的生产者。`user`、`model`、`function` 或 `tool`| + + + +**Part 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`text`|字符串|否|纯文本内容| +|`inlineData`|对象|否|内联媒体字节数据| +|`fileData`|对象|否|上传文件的URI引用| +|`functionCall`|对象|否|函数调用请求| +|`functionResponse`|对象|否|函数调用响应| +|`executableCode`|对象|否|可执行代码| +|`codeExecutionResult`|对象|否|代码执行结果| + + + +**InlineData 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`mimeType`|字符串|是|媒体的MIME类型| +|`data`|字符串|是|base64编码的媒体数据| + + + +**FileData 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`mimeType`|字符串|是|文件的MIME类型| +|`fileUri`|字符串|是|文件的URI| + + + +#### `tools` + + + +- 类型:数组 + +- 必需:否 + + + +模型可能用于生成下一个响应的工具列表。支持的工具包括函数和代码执行。 + + + +**Tool 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`functionDeclarations`|数组|否|可选的函数声明列表| +|`codeExecution`|对象|否|启用模型执行代码| + + + +**FunctionDeclaration 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|函数名称| +|`description`|字符串|否|函数功能描述| +|`parameters`|对象|否|函数参数,JSON Schema格式| + + + +**FunctionCall 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|要调用的函数名称| +|`args`|对象|否|函数参数的键值对| + + + +**FunctionResponse 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`name`|字符串|是|调用的函数名称| +|`response`|对象|是|函数调用的响应数据| + + + +**ExecutableCode 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`language`|枚举|是|代码的编程语言| +|`code`|字符串|是|要执行的代码| + + + +**CodeExecutionResult 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`outcome`|枚举|是|代码执行的结果状态| +|`output`|字符串|否|代码执行的输出内容| + + + +**CodeExecution 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|\{\}|空对象|\-|启用代码执行功能的空配置对象| + + + +#### `toolConfig` + + + +- 类型:对象 + +- 必需:否 + + + +请求中指定的任何工具的工具配置。 + + + +**ToolConfig 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`functionCallingConfig`|对象|否|函数调用配置| + + + +**FunctionCallingConfig 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`mode`|枚举|否|指定函数调用的模式| +|`allowedFunctionNames`|数组|否|允许调用的函数名列表| + + + +**FunctionCallingMode 枚举值:** + + + +- `MODE_UNSPECIFIED`: 默认模式,模型决定是否调用函数 + +- `AUTO`: 模型自动决定何时调用函数 + +- `ANY`: 模型必须调用函数 + +- `NONE`: 模型不能调用函数 + + + +#### `safetySettings` + + + +- 类型:数组 + +- 必需:否 + + + +用于屏蔽不安全内容的 SafetySetting 实例列表。 + + + +**SafetySetting 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`category`|枚举|是|安全类别| +|`threshold`|枚举|是|屏蔽阈值| + + + +**HarmCategory 枚举值:** + + + +- `HARM_CATEGORY_HARASSMENT`: 骚扰内容 + +- `HARM_CATEGORY_HATE_SPEECH`: 仇恨言论和内容 + +- `HARM_CATEGORY_SEXUALLY_EXPLICIT`: 露骨色情内容 + +- `HARM_CATEGORY_DANGEROUS_CONTENT`: 危险内容 + +- `HARM_CATEGORY_CIVIC_INTEGRITY`: 可能用于破坏公民诚信的内容 + + + +**HarmBlockThreshold 枚举值:** + + + +- `BLOCK_LOW_AND_ABOVE`: 允许发布评分为 NEGLIGIBLE 的内容 + +- `BLOCK_MEDIUM_AND_ABOVE`: 允许发布评分为 NEGLIGIBLE 和 LOW 的内容 + +- `BLOCK_ONLY_HIGH`: 允许发布风险等级为 NEGLIGIBLE、LOW 和 MEDIUM 的内容 + +- `BLOCK_NONE`: 允许所有内容 + +- `OFF`: 关闭安全过滤器 + + + +**HarmBlockThreshold 完整枚举值:** + + + +- `HARM_BLOCK_THRESHOLD_UNSPECIFIED`: 未指定阈值 + +- `BLOCK_LOW_AND_ABOVE`: 屏蔽低概率及以上的有害内容,只允许 NEGLIGIBLE 级别的内容 + +- `BLOCK_MEDIUM_AND_ABOVE`: 屏蔽中等概率及以上的有害内容,允许 NEGLIGIBLE 和 LOW 级别的内容 + +- `BLOCK_ONLY_HIGH`: 只屏蔽高概率的有害内容,允许 NEGLIGIBLE、LOW 和 MEDIUM 级别的内容 + +- `BLOCK_NONE`: 不屏蔽任何内容,允许所有级别的内容 + +- `OFF`: 完全关闭安全过滤器 + + + +#### `systemInstruction` + + + +- 类型:对象(Content) + +- 必需:否 + + + +开发者设置的系统指令。目前仅支持文本。 + + + +#### `generationConfig` + + + +- 类型:对象 + +- 必需:否 + + + +模型生成和输出的配置选项。 + + + +**GenerationConfig 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`stopSequences`|数组|否|用于停止生成输出的字符序列集(最多5个)| +|`responseMimeType`|字符串|否|生成的候选文本的MIME类型| +|`responseSchema`|对象|否|生成的候选文本的输出架构| +|`responseModalities`|数组|否|请求的响应模式| +|`candidateCount`|整数|否|要返回的生成的回答数量| +|`maxOutputTokens`|整数|否|候选回答中包含的令牌数量上限| +|`temperature`|数字|否|控制输出的随机性,范围\[0\.0, 2\.0\]| +|`topP`|数字|否|在抽样时要考虑的令牌的累计概率上限| +|`topK`|整数|否|抽样时要考虑的令牌数量上限| +|`seed`|整数|否|解码中使用的种子| +|`presencePenalty`|数字|否|存在性惩罚| +|`frequencyPenalty`|数字|否|频率惩罚| +|`responseLogprobs`|布尔值|否|是否在响应中导出logprobs结果| +|`logprobs`|整数|否|返回的顶部logprob的数量| +|`enableEnhancedCivicAnswers`|布尔值|否|启用增强型城市服务回答| +|`speechConfig`|对象|否|语音生成配置| +|`thinkingConfig`|对象|否|思考功能的配置| +|`mediaResolution`|枚举|否|指定的媒体分辨率| + + + +**支持的 MIME 类型:** + + + +- `text/plain`: (默认)文本输出 + +- `application/json`: JSON响应 + +- `text/x.enum`: ENUM作为字符串响应 + + + +**Modality 枚举值:** + + + +- `TEXT`: 指示模型应返回文本 + +- `IMAGE`: 表示模型应返回图片 + +- `AUDIO`: 指示模型应返回音频 + + + +**Schema 对象属性:** + + + +|属性|类型|必需|描述| +|---|---|---|---| +|`type`|枚举|是|数据类型| +|`description`|字符串|否|字段描述| +|`enum`|数组|否|枚举值列表(当type为string时)| +|`example`|任意类型|否|示例值| +|`nullable`|布尔值|否|是否可为null| +|`format`|字符串|否|字符串格式(如date、date\-time等)| +|`items`|对象|否|数组项的Schema(当type为array时)| +|`properties`|对象|否|对象属性的Schema映射(当type为object时)| +|`required`|数组|否|必需属性的名称列表| +|`minimum`|数字|否|数字的最小值| +|`maximum`|数字|否|数字的最大值| +|`minItems`|整数|否|数组的最小长度| +|`maxItems`|整数|否|数组的最大长度| +|`minLength`|整数|否|字符串的最小长度| +|`maxLength`|整数|否|字符串的最大长度| + + + +**Type 枚举值:** + + + +- `TYPE_UNSPECIFIED`: 未指定类型 + +- `STRING`: 字符串类型 + +- `NUMBER`: 数字类型 + +- `INTEGER`: 整数类型 + +- `BOOLEAN`: 布尔类型 + +- `ARRAY`: 数组类型 + +- `OBJECT`: 对象类型 + + + +**支持的编程语言(ExecutableCode):** + + + +- `LANGUAGE_UNSPECIFIED`: 未指定语言 + +- `PYTHON`: Python编程语言 + + + +**代码执行结果枚举(Outcome):** + + + +- `OUTCOME_UNSPECIFIED`: 未指定结果 + +- `OUTCOME_OK`: 代码执行成功 + +- `OUTCOME_FAILED`: 代码执行失败 + +- `OUTCOME_DEADLINE_EXCEEDED`: 代码执行超时 + + + +#### `cachedContent` + + + +- 类型:字符串 + +- 必需:否 + + + +缓存的内容的名称,用于用作提供预测的上下文。格式:`cachedContents/{cachedContent}` + + + +## 📥 响应 + + + +### GenerateContentResponse + + + +支持多个候选回答的模型的回答。系统会针对提示以及每个候选项报告安全分级和内容过滤。 + + + +#### `candidates` + + + +- 类型:数组 + +- 说明:模型的候选回答列表 + + + +**Candidate 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`content`|对象|模型返回的生成内容| +|`finishReason`|枚举|模型停止生成词元的原因| +|`safetyRatings`|数组|候选回答安全性的评分列表| +|`citationMetadata`|对象|模型生成的候选项的引用信息| +|`tokenCount`|整数|此候选项的令牌数| +|`groundingAttributions`|数组|为生成有依据的回答所参考的来源提供方信息| +|`groundingMetadata`|对象|候选对象的参考元数据| +|`avgLogprobs`|数字|候选项的平均对数概率得分| +|`logprobsResult`|对象|回答令牌和前置令牌的对数似然度得分| +|`urlRetrievalMetadata`|对象|与网址情境检索工具相关的元数据| +|`urlContextMetadata`|对象|与网址情境检索工具相关的元数据| +|`index`|整数|响应候选列表中候选项的索引| + + + +**FinishReason 枚举值:** + + + +- `STOP`: 模型的自然停止点或提供的停止序列 + +- `MAX_TOKENS`: 已达到请求中指定的词元数量上限 + +- `SAFETY`: 出于安全考虑,系统已标记回答候选内容 + +- `RECITATION`: 由于背诵原因,回答候选内容被标记 + +- `LANGUAGE`: 回答候选内容因使用不受支持的语言而被标记 + +- `OTHER`: 原因未知 + +- `BLOCKLIST`: 由于内容包含禁止使用的字词,因此token生成操作已停止 + +- `PROHIBITED_CONTENT`: 由于可能包含禁止的内容,因此token生成操作已停止 + +- `SPII`: 由于内容可能包含敏感的个人身份信息,因此token生成操作已停止 + +- `MALFORMED_FUNCTION_CALL`: 模型生成的函数调用无效 + +- `IMAGE_SAFETY`: 由于生成的图片违反了安全规定,因此词元生成已停止 + + + +#### `promptFeedback` + + + +- 类型:对象 + +- 说明:与内容过滤器相关的提示反馈 + + + +**PromptFeedback 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`blockReason`|枚举|屏蔽该提示的原因| +|`safetyRatings`|数组|问题安全性的评分| + + + +**BlockReason 枚举值:** + + + +- `BLOCK_REASON_UNSPECIFIED`: 默认值,此值未使用 + +- `SAFETY`: 出于安全原因,系统屏蔽了提示 + +- `OTHER`: 提示因未知原因被屏蔽了 + +- `BLOCKLIST`: 系统屏蔽了此提示,因为其中包含术语屏蔽名单中包含的术语 + +- `PROHIBITED_CONTENT`: 系统屏蔽了此提示,因为其中包含禁止的内容 + +- `IMAGE_SAFETY`: 候选图片因生成不安全的内容而被屏蔽 + + + +#### `usageMetadata` + + + +- 类型:对象 + +- 说明:有关生成请求令牌用量的元数据 + + + +**UsageMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`promptTokenCount`|整数|提示中的词元数| +|`cachedContentTokenCount`|整数|提示的缓存部分中的词元数| +|`candidatesTokenCount`|整数|所有生成的候选回答中的词元总数| +|`totalTokenCount`|整数|生成请求的总令牌数| +|`toolUsePromptTokenCount`|整数|工具使用提示中的词元数量| +|`thoughtsTokenCount`|整数|思考模型的想法token数| +|`promptTokensDetails`|数组|在请求输入中处理的模态列表| +|`candidatesTokensDetails`|数组|响应中返回的模态列表| +|`cacheTokensDetails`|数组|请求输入中缓存内容的模态列表| +|`toolUsePromptTokensDetails`|数组|为工具使用请求输入处理的模态列表| + + + +#### `modelVersion` + + + +- 类型:字符串 + +- 说明:用于生成回答的模型版本 + + + +#### `responseId` + + + +- 类型:字符串 + +- 说明:用于标识每个响应的ID + + + +#### 完整响应示例 + + + +```JSON +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "你好!我是 Gemini,一个由 Google 开发的人工智能助手。我可以帮助您解答问题、提供信息、协助写作、代码编程等多种任务。请告诉我有什么可以为您效劳的!" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE", + "blocked": false + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE", + "blocked": false + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE", + "blocked": false + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "blocked": false + } + ], + "tokenCount": 47 + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + } + ] + }, + "usageMetadata": { + "promptTokenCount": 4, + "candidatesTokenCount": 47, + "totalTokenCount": 51, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 4 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 47 + } + ] + }, + "modelVersion": "gemini-2.0-flash", + "responseId": "response-12345" +} +``` + + + +## 🔧 高级功能 + + + +### 安全评级 + + + +**SafetyRating 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`category`|枚举|此评分的类别| +|`probability`|枚举|此内容的有害概率| +|`blocked`|布尔值|此内容是否因此分级而被屏蔽| + + + +**HarmProbability 枚举值:** + + + +- `NEGLIGIBLE`: 内容不安全的概率可忽略不计 + +- `LOW`: 内容不安全的概率较低 + +- `MEDIUM`: 内容不安全的概率为中等 + +- `HIGH`: 内容不安全的概率较高 + + + +### 引用元数据 + + + +**CitationMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`citationSources`|数组|特定回复的来源引用| + + + +**CitationSource 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`startIndex`|整数|归因于此来源的响应片段的开始索引| +|`endIndex`|整数|归因细分的结束索引(不含)| +|`uri`|字符串|被归因为文本部分来源的URI| +|`license`|字符串|被归因为片段来源的GitHub项目的许可| + + + +### 代码执行 + + + +当启用代码执行工具时,模型可以生成和执行代码来解决问题。 + + + +**代码执行示例响应:** + + + +```JSON +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "我来计算斐波那契数列的第10项:" + }, + { + "executableCode": { + "language": "PYTHON", + "code": "def fibonacci(n):\n if n <= 1:\n return n\n else:\n return fibonacci(n-1) + fibonacci(n-2)\n\nresult = fibonacci(10)\nprint(f'第10项斐波那契数是: {result}')" + } + }, + { + "codeExecutionResult": { + "outcome": "OK", + "output": "第10项斐波那契数是: 55" + } + }, + { + "text": "所以斐波那契数列的第10项是55。" + } + ], + "role": "model" + }, + "finishReason": "STOP" + } + ] +} +``` + + + +### 接地功能 \(Grounding\) + + + +**GroundingMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`groundingChunks`|数组|从指定的接地源检索到的支持参考文献列表| +|`groundingSupports`|数组|接地支持列表| +|`webSearchQueries`|数组|用于后续网页搜索的网页搜索查询| +|`searchEntryPoint`|对象|后续网页搜索的Google搜索条目| +|`retrievalMetadata`|对象|与基准流程中检索相关的元数据| + + + +**GroundingAttribution 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`sourceId`|对象|对此归因做出贡献的来源的标识符| +|`content`|对象|构成此归因的来源内容| + + + +**AttributionSourceId 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`groundingPassage`|对象|内嵌段落的标识符| +|`semanticRetrieverChunk`|对象|通过Semantic Retriever提取的Chunk的标识符| + + + +**GroundingPassageId 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`passageId`|字符串|与GenerateAnswerRequest的GroundingPassage\.id匹配的段落的ID| +|`partIndex`|整数|GenerateAnswerRequest的GroundingPassage\.content中的部分的索引| + + + +**SemanticRetrieverChunk 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`source`|字符串|与请求的SemanticRetrieverConfig\.source匹配的来源名称| +|`chunk`|字符串|包含归因文本的Chunk的名称| + + + +**SearchEntryPoint 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`renderedContent`|字符串|可嵌入网页或应用WebView中的Web内容代码段| +|`sdkBlob`|字符串|使用base64编码的JSON,表示搜索词和搜索URL元组的数组| + + + +**Segment 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`partIndex`|整数|Part对象在其父级Content对象中的索引| +|`startIndex`|整数|给定part中的起始索引,以字节为单位| +|`endIndex`|整数|给定分块中的结束索引,以字节为单位| +|`text`|字符串|与响应中的片段对应的文本| + + + +**RetrievalMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`googleSearchDynamicRetrievalScore`|数字|Google搜索中的信息有助于回答问题的概率得分,范围\[0,1\]| + + + +**GroundingChunk 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`web`|对象|来自网络的接地分块| + + + +**Web 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`uri`|字符串|分块的URI引用| +|`title`|字符串|数据块的标题| + + + +**GroundingSupport 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`groundingChunkIndices`|数组|索引列表,用于指定与版权主张相关的引文| +|`confidenceScores`|数组|支持参考文档的置信度分数,范围为0到1| +|`segment`|对象|此支持请求所属的内容片段| + + + +### 多模态处理 + + + +Gemini API 支持处理多种模态的输入和输出: + + + +**支持的输入模态:** + + + +- `TEXT`: 纯文本 + +- `IMAGE`: 图片(JPEG、PNG、WebP、HEIC、HEIF) + +- `AUDIO`: 音频(WAV、MP3、AIFF、AAC、OGG、FLAC) + +- `VIDEO`: 视频(MP4、MPEG、MOV、AVI、FLV、MPG、WEBM、WMV、3GPP) + +- `DOCUMENT`: 文档(PDF) + + + +**ModalityTokenCount 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`modality`|枚举|与此令牌数关联的模态| +|`tokenCount`|整数|令牌数量| + + + +**MediaResolution 枚举值:** + + + +- `MEDIA_RESOLUTION_LOW`: 低分辨率(64个令牌) + +- `MEDIA_RESOLUTION_MEDIUM`: 中等分辨率(256个令牌) + +- `MEDIA_RESOLUTION_HIGH`: 高分辨率(256个令牌进行缩放重新取景) + + + +### 思考功能 + + + +**ThinkingConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`includeThoughts`|布尔值|是否要在回答中包含思考内容| +|`thinkingBudget`|整数|模型应生成的想法token的数量| + + + +### 语音生成 + + + +**SpeechConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`voiceConfig`|对象|单声音输出的配置| +|`multiSpeakerVoiceConfig`|对象|多音箱设置的配置| +|`languageCode`|字符串|用于语音合成的语言代码| + + + +**VoiceConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`prebuiltVoiceConfig`|对象|要使用的预构建语音的配置| + + + +**PrebuiltVoiceConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`voiceName`|字符串|要使用的预设语音的名称| + + + +**MultiSpeakerVoiceConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`speakerVoiceConfigs`|数组|所有已启用的音箱语音| + + + +**SpeakerVoiceConfig 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`speaker`|字符串|要使用的音箱的名称| +|`voiceConfig`|对象|要使用的语音的配置| + + + +**支持的语言代码:** + + + +- `zh-CN`: 中文(简体) + +- `en-US`: 英语(美国) + +- `ja-JP`: 日语 + +- `ko-KR`: 韩语 + +- `fr-FR`: 法语 + +- `de-DE`: 德语 + +- `es-ES`: 西班牙语 + +- `pt-BR`: 葡萄牙语(巴西) + +- `hi-IN`: 印地语 + +- `ar-XA`: 阿拉伯语 + +- `it-IT`: 意大利语 + +- `tr-TR`: 土耳其语 + +- `vi-VN`: 越南语 + +- `th-TH`: 泰语 + +- `ru-RU`: 俄语 + +- `pl-PL`: 波兰语 + +- `nl-NL`: 荷兰语 + + + +### Logprobs 结果 + + + +**LogprobsResult 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`topCandidates`|数组|长度等于解码步骤总数| +|`chosenCandidates`|数组|长度等于解码步骤总数,所选候选项不一定在topCandidates中| + + + +**TopCandidates 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`candidates`|数组|按对数概率降序排序的候选项| + + + +**Candidate \(Logprobs\) 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`token`|字符串|候选项的令牌字符串值| +|`tokenId`|整数|候选项的令牌ID值| +|`logProbability`|数字|候选项的对数概率| + + + +### URL检索功能 + + + +**UrlRetrievalMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`urlRetrievalContexts`|数组|网址检索情境列表| + + + +**UrlRetrievalContext 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`retrievedUrl`|字符串|工具检索到的网址| + + + +**UrlContextMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`urlMetadata`|数组|网址上下文列表| + + + +**UrlMetadata 对象属性:** + + + +|属性|类型|描述| +|---|---|---| +|`retrievedUrl`|字符串|工具检索到的网址| +|`urlRetrievalStatus`|枚举|网址检索的状态| + + + +**UrlRetrievalStatus 枚举值:** + + + +- `URL_RETRIEVAL_STATUS_SUCCESS`: 网址检索成功 + +- `URL_RETRIEVAL_STATUS_ERROR`: 由于出错,网址检索失败 + + + +### 完整安全类别 + + + +**HarmCategory 完整枚举值:** + + + +- `HARM_CATEGORY_UNSPECIFIED`: 类别未指定 + +- `HARM_CATEGORY_DEROGATORY`: PaLM \- 针对身份和/或受保护属性的负面或有害评论 + +- `HARM_CATEGORY_TOXICITY`: PaLM \- 粗鲁、无礼或亵渎性的内容 + +- `HARM_CATEGORY_VIOLENCE`: PaLM \- 描述描绘针对个人或团体的暴力行为的场景 + +- `HARM_CATEGORY_SEXUAL`: PaLM \- 包含对性行为或其他淫秽内容的引用 + +- `HARM_CATEGORY_MEDICAL`: PaLM \- 宣传未经核实的医疗建议 + +- `HARM_CATEGORY_DANGEROUS`: PaLM \- 危险内容会宣扬、助长或鼓励有害行为 + +- `HARM_CATEGORY_HARASSMENT`: Gemini \- 骚扰内容 + +- `HARM_CATEGORY_HATE_SPEECH`: Gemini \- 仇恨言论和内容 + +- `HARM_CATEGORY_SEXUALLY_EXPLICIT`: Gemini \- 露骨色情内容 + +- `HARM_CATEGORY_DANGEROUS_CONTENT`: Gemini \- 危险内容 + +- `HARM_CATEGORY_CIVIC_INTEGRITY`: Gemini \- 可能用于破坏公民诚信的内容 + + + +**HarmProbability 完整枚举值:** + + + +- `HARM_PROBABILITY_UNSPECIFIED`: 概率未指定 + +- `NEGLIGIBLE`: 内容不安全的概率可忽略不计 + +- `LOW`: 内容不安全的概率较低 + +- `MEDIUM`: 内容不安全的概率为中等 + +- `HIGH`: 内容不安全的概率较高 + + + +**Modality 完整枚举值:** + + + +- `MODALITY_UNSPECIFIED`: 未指定模态 + +- `TEXT`: 纯文本 + +- `IMAGE`: 图片 + +- `VIDEO`: 视频 + +- `AUDIO`: 音频 + +- `DOCUMENT`: 文档,例如PDF + + + +**MediaResolution 完整枚举值:** + + + +- `MEDIA_RESOLUTION_UNSPECIFIED`: 未设置媒体分辨率 + +- `MEDIA_RESOLUTION_LOW`: 媒体分辨率设为低(64个令牌) + +- `MEDIA_RESOLUTION_MEDIUM`: 媒体分辨率设为中等(256个令牌) + +- `MEDIA_RESOLUTION_HIGH`: 媒体分辨率设为高(使用256个令牌进行缩放重新取景) + + + +**UrlRetrievalStatus 完整枚举值:** + + + +- `URL_RETRIEVAL_STATUS_UNSPECIFIED`: 默认值,此值未使用 + +- `URL_RETRIEVAL_STATUS_SUCCESS`: 网址检索成功 + +- `URL_RETRIEVAL_STATUS_ERROR`: 由于出错,网址检索失败 + + + +## 🔍 错误处理 + + + +### 常见错误码 + + + +|错误码|描述| +|---|---| +|`400`|请求格式错误或参数无效| +|`401`|API密钥无效或缺失| +|`403`|权限不足或配额限制| +|`429`|请求频率过高| +|`500`|服务器内部错误| + + + +### 详细错误码说明 + + + +|错误码|状态|描述|解决方案| +|---|---|---|---| +|`400`|`INVALID_ARGUMENT`|请求参数无效或格式错误|检查请求参数格式和必需字段| +|`400`|`FAILED_PRECONDITION`|请求的前置条件不满足|确保满足API调用的前置条件| +|`401`|`UNAUTHENTICATED`|API密钥无效、缺失或已过期|检查API密钥的有效性和格式| +|`403`|`PERMISSION_DENIED`|权限不足或配额已用完|检查API密钥权限或升级配额| +|`404`|`NOT_FOUND`|指定的模型或资源不存在|验证模型名称和资源路径| +|`413`|`PAYLOAD_TOO_LARGE`|请求体太大|减少输入内容大小或分批处理| +|`429`|`RESOURCE_EXHAUSTED`|请求频率超限或配额不足|降低请求频率或等待配额重置| +|`500`|`INTERNAL`|服务器内部错误|重试请求,如持续出现联系支持| +|`503`|`UNAVAILABLE`|服务暂时不可用|等待一段时间后重试| +|`504`|`DEADLINE_EXCEEDED`|请求超时|减少输入大小或重试请求| + + + +### 错误响应示例 + + + +```JSON +{ + "error": { + "code": 400, + "message": "Invalid argument: contents", + "status": "INVALID_ARGUMENT", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.BadRequest", + "fieldViolations": [ + { + "field": "contents", + "description": "contents is required" + } + ] + } + ] + } +} +``` + + + diff --git a/交接-AI生成Agent化-2026-06-17.md b/交接-AI生成Agent化-2026-06-17.md new file mode 100644 index 0000000..1380980 --- /dev/null +++ b/交接-AI生成Agent化-2026-06-17.md @@ -0,0 +1,173 @@ +# AirShelf · AI 生成 Agent 化 · 交接文档 + +> **日期:** 2026-06-17(凌晨) +> **交接人:** Claude(尹希乐 / seaislee 指导)→ **接手人:** 张业昌 + 你的 Claude + 前端 UX +> **分支:** `dev`(本批改动已推送) +> **一句话:** 把脚本生成升级成「多模型可选 + 出稿/改稿一体的**流式对话 agent**(挂电商 skill)」,并把 商品→脚本→图片→故事板→视频 的 SOP 用**可插拔 Provider** 打通;参考图分镜(gpt-image-2 @图N)、模特库、Seedance 出音 全部就位。 + +--- + +## 0. TL;DR · 拿到就能跑 + +```bash +# 后端(端口 8010) +cd core/backend +.venv/Scripts/python.exe manage.py migrate # 跑新迁移(0005/0006 + projects 0002) +.venv/Scripts/python.exe manage.py runserver 0.0.0.0:8010 +# 另起 celery worker(图片/故事板/视频是异步,靠 worker 执行;没 worker 这几趴会卡) +.venv/Scripts/python.exe -m celery -A airshelf worker -l info -P solo + +# 前端(端口 5173,vite 代理 /api → 8010) +cd core/frontend +npm install && npm run dev +``` + +`.env` 里**凭证已就绪**(tokenssr / 飞书 / 火山审核借用 AK/SK / 豆包 TTS),开箱即用。**⚠️ 见 §7 有一处借用密钥要换。** + +--- + +## 1. 成熟度总表(诚实标注) + +| 模块 | 状态 | 验证方式 | +| ---- | ---- | ---- | +| 可插拔 Provider 层(tokenssr/任意中转站) | ✅ 已完成·已验证 | 真打 tokenssr 流式 chat 通过 | +| 结构化脚本 agent + 流式 SSE(出稿/改稿/3模式/多模型) | ✅ 已完成·**双验证** | 后端 HTTP 探针 + **无头浏览器视觉自检**(截图见 `_qa_shots/`) | +| gpt-image-2 参考图出图 + 落 TOS | ✅ 已完成·已验证 | 文生图→TOS→参考图合成 整链跑通 | +| 故事板 @图1@图2@图3 多锚点合成 | ✅ 代码完成 | 逻辑接好,建议接 worker 后端到端复跑一次 | +| Seedance 视频出音(generate_audio) | ✅ 已打开 | 默认 True;原来写死 False | +| 多模型 seed(豆包/GPT-5.5/Gemini + gpt-image-2 + Seedance) | ✅ 已完成 | 数据迁移 0006,`get_default_model` 选取正确 | +| 模特库生成器(9:16氛围图→16:9白底三视图) | ✅ 已完成·已验证 | 跑通 1 个,三视图角色一致性极好(截图 `_qa_shots/model_threeview.png`) | +| 前端脚本趴(流式工具卡+思考流+模型下拉+改稿) | ✅ 已完成·**浏览器视觉验证** | 见 `_qa_shots/01..03` | +| **火山人像素材库审核(绿/红标)** | 🟡 **设计完成·未接线** | 见 §6,AirDrama 有现成代码可照搬,AK/SK 已备好 | +| 计费(按 token 精确扣) | 🟡 沿用现有(每次调用扣 unit_price) | 真 token 计量是后续优化项 | + +--- + +## 2. 架构原则(务必延续) + +**Provider 可插拔 —— 除火山官方直连外,一切走「OpenAI 兼容中转站」,换站零改代码。** + +- 火山(豆包文本 / SeeDream 生图 / Seedance 视频)= 官方直连,`VolcanoArkProvider`。 +- 其余(tokenssr / yunqi / 任意 New-API 网关)= 通用 `OpenAICompatibleProvider(base_url, api_key)`。 +- 分流在 `services.build_provider()`:`provider.name in OFFICIAL_DIRECT_PROVIDERS` 走火山,否则走通用适配器。 +- **加/换中转站 = DB 加一行 `ModelProvider`(base_url + 可选 api_key)**,代码一行不改。 +- 密钥解析顺序:`ModelProvider.api_key`(DB)→ `settings.PROVIDER_KEYS`(.env)。**密钥默认只在 .env,不进库**(seed 的 tokenssr provider 的 api_key 留空,运行时从 .env 取)。 + +**关键文件:** +- `core/backend/apps/ai/providers/openai_compatible.py` — 通用适配器(chat / 流式 chat / 生图 / **参考图 image_edit**) +- `core/backend/apps/ai/providers/volcano.py` — 基类(新增 `chat_completion_stream` 流式 + `generate_audio` 开关) +- `core/backend/apps/ai/services.py` — `build_provider / resolve_provider_credentials / get_{text,image,video}_provider` + +--- + +## 3. 脚本 Agent(核心) + +### 流式 SSE 端点 +`POST /api/projects/{id}/script-agent-stream/` → `text/event-stream`,逐帧 `data: {json}`。 + +事件类型: +- `tool` `{id,label?,status:running|done|error}` — 工具卡(加载skill / 分析商品 / 生成分镜 / 提取实体 / 自检) +- `delta` `{text}` — 模型思考前言逐字(JSON 部分后端隐藏不外露) +- `draft` `{draft}` — 规范化后的 **ScriptDraft**(结构化) +- `saved` `{script_version_id, version}` — 已落库的 ScriptVersion(含 segments + metadata) +- `done` / `error` + +> **DRF 坑(已解决):** SSE 必须给 action 挂 `ServerSentEventRenderer`(media_type=text/event-stream),否则 `Accept: text/event-stream` 直接 406。错误响应用纯 Django `JsonResponse` 绕开 DRF 渲染。见 `projects/views.py`。 + +### 3 种输入模式(前端已接) +- **auto(全自动)**:仅商品信息 → 自动定档/选 tone/造 entity/填黄金结构。 +- **theme(一句话主题)**:用户给主题 → 以主题为主轴扩写。 +- **revise(改稿)**:已有脚本 → 基于当前脚本增强(前端追问自动走此模式,带 `base_version_id`)。 + +### 多模型 +请求带 `model_config_id`(前端脚本助手右上**模型下拉**:豆包 / GPT-5.5 / Gemini 3 Pro);缺省用默认文本模型。已实测豆包(官方直连)+ GPT-5.5(经 tokenssr)两条路都通。 + +### 结构化契约 ScriptDraft +脚本不再是「散文 + 正则」,而是结构化 JSON(`script_agent.normalize_draft` 后端兜底校验):三层 = 脚本头(hook/tone/aspect/duration)+ entities(角色/场景/商品,带 visual_prompt/ref_index,**全脚本共享保一致**)+ segments(role 钩子/痛点/卖点/CTA、narration≤55字、speaker、visual、product_exposure、**entity_refs**)。 + +- 落库:`ScriptVersion`(content=JSON,metadata=hook/tone/entities)+ `ScriptSegment`(新增字段 role/speaker/product_exposure/entity_refs)。 +- **entities 回填 `project.metadata`**(cast/scenes/cast_prompts/scene_prompts + script_entities)→ 复用下游已有的基础资产 seed + 故事板 @图N。 + +### 电商 skill +`skills/ecommerce-video-script/`(SKILL.md + 5 references)。`script_agent.load_ecommerce_skill()` 运行时加载为系统提示词,**模型无关**。运行时输出协议放开了「先 1 句口语前言 + 再 JSON」以驱动流式体感。 + +**关键文件:** `core/backend/apps/ai/script_agent.py`、`apps/projects/views.py`(`script_agent_stream` action)、前端 `api.ts`(`agentScriptStream`)、`routes/pipeline.tsx`(`runScriptGeneration` 改 SSE 驱动)。 + +--- + +## 4. 图片 / 故事板 / 视频 + +- **图片(基础资产 / 模特 / 套图)**:默认图像模型 = `tokenssr:gpt-image-2`(支持参考图)。原 `yunqi:gpt-image-2`(只能纯文生图)已在迁移里停用。 +- **故事板 @图N**:`services._storyboard_reference_images()` 按本镜 `entity_refs` 取角色/场景/商品的已采用基础资产,用 `provider.image_edit(images=[url1,url2,url3])` 多图合成本镜(锁脸锁商品)。provider 不支持 image_edit 时自动回退纯文生图。 +- **视频(Seedance)**:`create_video_task(generate_audio=True)` 已打开,直接出画面+音效+人声(**不走 TTS**)。慢:单段约 5–10 分钟(凌晨可能 3–4 分钟),轮询端点 `poll-video-segment` 已是「秒回不阻塞」式。 + +--- + +## 5. 模特库 + +`python manage.py seed_demo_models --count 2 [--team ] [--brief "画像"]` + +逻辑(`apps/ai/model_library.py`):先 gpt-image-2 出 9:16 氛围正面图,再以它为参考出 16:9 白底三视图(提示词「参考图1角色,生成角色三视图,从左往右依次为:胸像特写,全身正面,全身侧面,全身背面,白色背景」),两张都存为 person 类 Asset(`metadata.kind="model"`)。**已跑通,三视图角色一致性很好**(见 `_qa_shots/model_threeview.png`)。 + +--- + +## 6. 🟡 火山人像素材库审核(绿/红标)—— 待接线 + +**目标:** 真人素材进火山素材库审核,前端只显示绿盾(active)/红标(failed,提示改提示词重生)。一团队一素材组(单组 500 万)。 + +**现成代码在 AirDrama,直接照搬:** +- `C:\Airlabs_Project\Airflow Studio\AirDrama\video-shuoshan\backend\utils\assets_client.py` — `create_asset_group / create_asset / get_asset`(volcengine SDK,AK/SK 签名,异步审核状态轮询 active/failed/processing)。 +- 对应 `AssetGroup(team, remote_group_id)` + `Asset(group, remote_asset_id, status, url, error_message)` 模型 + `asset_poll_status_view`。 + +**接线步骤(给你的 Claude):** +1. `requirements.txt` 加 `volcengine` SDK 并安装(**当前 AirShelf 没装**)。 +2. 照搬 `assets_client.py` 到 `apps/assets/`,改 import。 +3. 加 `AssetGroup` / 给 `Asset` 加 `remote_asset_id/audit_status/audit_error` 字段 + 迁移。 +4. 生成人物基础资产后**静默自动上传**到团队素材组,前端轮询 audit_status 渲染绿/红标。 +5. env 已备好:`ASSETS_API_ACCESS_KEY / ASSETS_API_SECRET_KEY / ASSETS_API_ENABLED / ASSETS_API_PROJECT_NAME`。 + +--- + +## 7. 凭证与 .env(⚠️ 一处待换) + +`.env` 已含可用凭证(已随 dev 推送,沿用本仓库既有「.env 入库」惯例): +- `TOKENSSR_API_KEY / TOKENSSR_BASE_URL` — 中转站主力(一把 key 通吃 ~90 模型)。 +- `FEISHU_APP_ID / FEISHU_APP_SECRET` — 小毛球机器人(交接推送)。 +- `VOLC_TTS_*` — 豆包语音合成(可选后期配音)。 + +**🔴 张业昌待办(唯一硬待办):** `ASSETS_API_ACCESS_KEY / ASSETS_API_SECRET_KEY` 现在**暂借 AirDrama 已邀测开通的火山 AK/SK** 跑通。AirShelf 自有火山账号开通「人像素材库」后,**只换这两行**(独立于 `TOS_*`,不要动 TOS)。 + +--- + +## 8. 给前端 UX 的 2 天打磨建议(视觉,非阻塞) + +当前以「流程跑通 + 真 agent 体感」为先,视觉是够用级,可优化: +- 脚本助手的**工具卡**现在复用进度流气泡渲染(`ProgressStream` 的 steps),可做成独立「工具卡」样式(图标 + 标题 + 状态点),更像 lovart。 +- 思考前言文字现在是 `opacity:0.85` 的普通段落,可加打字机光标 / 渐显。 +- 分镜卡可加 **role 徽章**(钩子/痛点/卖点/CTA,数据已在 `segment.role`)和商品露出标签(`segment.product_exposure`)。 +- 模型下拉复用了 `.setup-select`,可做成带模型图标的 Pill。 +- **务必遵循** `电商AI平台/design.md`(冷灰底/单橙锚点/8px 圆角/inside-border),别新造色值或重写共享类。 + +--- + +## 9. 已知问题 / TODO + +- 流式生成中途客户端断开,可能留下「预扣未结算」额度(边缘情况;`stream_script_agent` 已尽量 try/except 释放,但 WSGI 断流的 finally 不保证)。 +- vite 代理转发 SSE 一般不缓冲;生产 nginx 需确认关掉 `proxy_buffering`(响应已带 `X-Accel-Buffering: no`)。 +- 计费仍是「每次调用扣 `unit_price`」,非真 token 计量。 +- 故事板 @图N、视频出音 建议接 worker 后端到端复跑一轮确认。 + +--- + +## 10. 本批改动文件清单 + +**后端新增:** `providers/openai_compatible.py`、`script_agent.py`、`model_library.py`、`management/commands/seed_demo_models.py`、迁移 `ai/0005`、`ai/0006`、`projects/0002`。 +**后端修改:** `providers/volcano.py`(流式+音频)、`providers/__init__.py`、`ai/models.py`(ModelProvider.api_key)、`ai/services.py`(provider 分流+故事板参考图)、`projects/models.py`(ScriptSegment 字段)、`projects/serializers.py`、`projects/views.py`(SSE 端点+renderer)、`settings/base.py`(TOKENSSR/PROVIDER_KEYS)、`.env`。 +**前端修改:** `api.ts`(agentScriptStream)、`routes/pipeline.tsx`(SSE 驱动+模型下拉+改稿)、`App.tsx`(textModels)、`types.ts`。 +**资源:** `skills/ecommerce-video-script/`(运行时依赖,**必须随仓库**)。 + +设计 SSoT:`AI生成-Agent化落地方案.md`。API 参考:`tokenssr接口文档.md`、`image调用参考.md`。 + +--- + +_有疑问看 `_qa_shots/` 的截图(脚本流式三连 + 模特三视图),或直接复跑 §0。祝接手顺利。_