feat: MiniMax provider for image/video/podcast skills + new music-generation skill (#3437)

* docs(spec): MiniMax integration for generation skills + new music skill

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plan): MiniMax generation providers implementation plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(skills): add importlib loader + FakeResp for skill tests

* test(skills): register loaded module in sys.modules; raise requests.HTTPError in FakeResp

* feat(image-generation): add MiniMax provider with env auto-detect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(image-generation): guard unknown provider, derive ref MIME, strengthen tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(video-generation): add MiniMax provider with async poll/download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(video-generation): surface base_resp errors while polling; add timeout test

* feat(podcast-generation): add MiniMax t2a_v2 provider with env auto-detect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(podcast-generation): restore TTS credential guard; add volcengine + voice tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(music-generation): new MiniMax music skill via skill-creator

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(music-generation): treat empty lyrics as absent; test no-audio-data path

* refactor(skills): add request timeouts to MiniMax network calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix for pull request finding 'Explicit returns mixed with implicit (fall through) returns'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix(models): strip inconsistent user-message names for MiniMax chat

DeerFlow middlewares tag user messages with provenance names (user-input, summary, loop_warning); langchain serializes them into the OpenAI-compatible payload and MiniMax rejects mismatched user-message names with "user name must be consistent (2013)". PatchedChatMiniMax now drops the per-message name from user-role messages. Point the config.example MiniMax models at PatchedChatMiniMax so they also get reasoning_content mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(image-generation): MiniMax sends JSON prompt field, guard 1500-char limit

MiniMax image-01 takes one text string capped at 1500 chars, but the skill was sending the whole structured JSON. The MiniMax provider now extracts the JSON `prompt` field (relying on prompt_optimizer to expand it) and fails fast with a clear error before calling the API when that field exceeds 1500 chars. Authoring stays provider-agnostic; Gemini still receives the full JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(podcast-generation): per-provider TTS concurrency and retry/backoff

Each TTS provider owns its concurrency internally — MiniMax runs single-threaded to reduce rate-limit failures, Volcengine keeps 4 workers — with automatic retry and backoff on transient HTTP and base_resp errors. No caller-facing concurrency knob.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): address Copilot review comments on generation skills

- video: add raise_for_status + timeout to the Gemini download/POST/poll calls so non-2xx responses surface as clear HTTP errors instead of JSON/KeyError or hangs
- video: check the task Fail status before the generic base_resp check so the failure keeps its task_id context
- video/image: create the output file parent directory before writing (matching music-generation) so nested output paths do not raise FileNotFoundError
- music: require a non-empty prompt and fail fast with ValueError instead of sending an empty prompt to the API

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(scripts): reclaim dev ports across worktrees in make stop/dev

All deer-flow worktrees (main checkout + linked worktrees) hardcode the same dev ports (8001/3000/2026), so a service started from any worktree must be reclaimable from another. stop_all now resolves the set of worktree roots (DEERFLOW_ROOTS) and treats a process as deer-flow-owned when its open files live under any of them. It also force-kills survivors on 2026 alongside 8001/3000, fixing `make dev` aborting on the nginx port preflight when a prior nginx lingered on 2026.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(view-image): hide the injected image-context message from the UI

ViewImageMiddleware injects a HumanMessage (text + base64 images) so the vision model can see viewed images, but it was the only internal injector that set neither hide_from_ui nor a hidden name, so it leaked into the chat UI (and IM channels) as a user bubble reading "Here are the images you've viewed:". Mark it with additional_kwargs={"hide_from_ui": True}, matching todo/dynamic_context injections, which the frontend isHiddenFromUIMessage and the channel sender already honor. The model still receives the full content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(minimax): mark M2.7 models as text-only (no vision)

MiniMax M2.7 / M2.7-highspeed do not support vision; only M3 does. The
provider config asserted vision support for M2.7 in four places.

- config.example.yaml: 4 M2.7 entries -> supports_vision: false
- backend/docs/CONFIGURATION.md: M2.7 + highspeed -> supports_vision: false
- wizard: add LLMProvider.model_vision_overrides + extra_config_for() so
  selecting an M2.7 model writes supports_vision: false while M3 (default)
  keeps vision; wire it through setup_wizard.py
- tests: M2.7-highspeed fixture -> supports_vision=False; add
  test_minimax_vision_is_per_model

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
DanielWalnut
2026-06-08 22:04:38 +08:00
committed by GitHub
parent 1651d1f1f5
commit cd5bedaa74
27 changed files with 3564 additions and 365 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,175 @@
# MiniMax 接入生成类 Skill — 设计文档
- 日期:2026-06-08
- 分支:`worktree-feat-minimax-generation`
- 参考:MiniMax 开放平台 APIhttps://platform.minimaxi.com/docs/api-reference
## 1. 目标
1. 在现有 `image-generation``video-generation``podcast-generation` 三个 skill 中接入 MiniMax 作为可选 provider(与现有 Gemini / Volcengine 并存)。
2. 用项目自带的 `skill-creator` skill 新建一个 `music-generation` skill,对接 MiniMax 音乐生成 API。
## 2. 背景与现状
三个生成 skill 均位于 `skills/public/<name>/`,是**自包含目录**
- `SKILL.md`frontmatter`name``description` + 给 agent 的使用说明,运行时路径为 `/mnt/skills/public/<name>/...`、产物写到 `/mnt/user-data/...`
- `scripts/generate.py`(纯 `requests` 调用外部 API 的 CLI`argparse`
- 可选 `templates/`
现状 provider
| Skill | 现 provider | 端点 | 凭证 |
|---|---|---|---|
| image-generation | Gemini | `generativelanguage.googleapis.com/.../gemini-3-pro-image-preview:generateContent` | `GEMINI_API_KEY` |
| video-generation | Gemini Veo | `.../veo-3.1-generate-preview:predictLongRunning`(长任务轮询) | `GEMINI_API_KEY` |
| podcast-generation | Volcengine TTS | `openspeech.bytedance.com/api/v1/tts`(逐行多线程,base64 音频拼接) | `VOLCENGINE_TTS_APPID` + `VOLCENGINE_TTS_ACCESS_TOKEN`+ 可选 `VOLCENGINE_TTS_CLUSTER` |
MiniMax 已作为 **LLM chat provider** 接入(`config.example.yaml` + `patched_minimax.py`),但**未用于**图像/视频/音频生成。仓库中**无** music 生成功能。
沙箱中各 skill 目录隔离、互不 import → MiniMax 代码在每个 skill 内**各自内联**,不做跨 skill 共享模块(少量重复可接受)。
`skill-creator` 是仓库内真实公共 skill`skills/public/skill-creator/`,含 `scripts/init_skill.py` 脚手架)。前端 `frontend/src/app/mock/api/skills/route.ts` 维护着 UI 展示用的 skill 列表(mock)。
## 3. Provider 选择机制(已和用户确认)
每个被改造的脚本新增 `_resolve_provider()`,判定顺序:
1. **显式覆盖**:若环境变量 `<SKILL>_PROVIDER` 已设(如 `IMAGE_GENERATION_PROVIDER``VIDEO_GENERATION_PROVIDER``PODCAST_GENERATION_PROVIDER`,取值 `gemini`/`volcengine`/`minimax`),直接采用,覆盖自动判断。
2. **现有 provider 优先**:现 provider 凭证齐全 → 用现有 provider(保持完全向后兼容)。
3. **回退 MiniMax**:否则若 `MINIMAX_API_KEY` 已设 → 用 MiniMax。
4. 都不满足 → 抛出清晰错误,提示两套环境变量该如何配置。
> 设计含义:默认行为不变(已有用户配了 Gemini/Volcengine 的不受影响);只配了 MiniMax 的用户自动走 MiniMax;两者都配又想用 MiniMax 的用户用 `<SKILL>_PROVIDER` 强制。
## 4. MiniMax 接口对接细节
通用:
- Base URL 默认 `https://api.minimaxi.com`,可用 `MINIMAX_API_HOST` 覆盖(备用 `https://api-bj.minimaxi.com`)。
- Header`Authorization: Bearer $MINIMAX_API_KEY``Content-Type: application/json`
- 统一错误处理:响应体 `base_resp.status_code != 0` → 抛带 `status_msg` 的异常。
### 4.1 图像 `POST /v1/image_generation`(同步)
请求体:
```json
{
"model": "image-01",
"prompt": "<文本>",
"aspect_ratio": "16:9",
"response_format": "base64",
"n": 1,
"prompt_optimizer": true
}
```
- 参考图:转成 Data URL`data:image/jpeg;base64,...`),放入
`subject_reference: [{"type": "character", "image_file": "<data url>"}]`(仅 `image-01` 支持;用现有 `--reference-images` 的图片)。
- 响应:`data.image_base64[0]``base64.b64decode` 写出文件;`response_format:url` 时取 `data.image_urls[0]` 下载(实现选 base64,少一次下载)。
- 模型可用 `MINIMAX_IMAGE_MODEL` 覆盖(默认 `image-01`)。
### 4.2 视频(异步三步)
1. `POST /v1/video_generation`
```json
{ "model": "MiniMax-Hailuo-2.3", "prompt": "<文本>", "first_frame_image": "<data url,可选>" }
```
→ `{ "task_id": "...", "base_resp": {...} }`
2. 轮询 `GET /v1/query/video_generation?task_id=<id>` → `status ∈ {Preparing,Queueing,Processing,Success,Fail}``Success` 时返回 `file_id`。
3. `GET /v1/files/retrieve?file_id=<id>` → `file.download_url`;下载 mp4 写出。
- 参考图:第一张转 Data URL 作 `first_frame_image`。
- 视频无 `aspect_ratio` 概念(用 resolution/duration),MiniMax 路径忽略 `--aspect-ratio`,用默认 resolution。
- 轮询间隔 3s,设最大次数上限(如 120 次≈6 分钟)防止无限循环;`Fail`/超时报错。
- 模型可用 `MINIMAX_VIDEO_MODEL` 覆盖(默认 `MiniMax-Hailuo-2.3`)。
### 4.3 播客 TTS `POST /v1/t2a_v2`(同步)
沿用现有"逐行 + `ThreadPoolExecutor` 多线程 + 拼接"结构,仅替换单行合成函数:
```json
{
"model": "speech-2.6-hd",
"text": "<单行文本>",
"voice_setting": { "voice_id": "<male/female 预设>", "speed": 1.0, "vol": 1.0, "pitch": 0 },
"audio_setting": { "sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1 },
"output_format": "hex"
}
```
- 响应 `data.audio` 为 **hex 编码** → `bytes.fromhex(audio)`(区别于 Volcengine 的 base64)。
- 角色映射:`male`/`female` → MiniMax voice_id 预设,默认值可用 `MINIMAX_TTS_VOICE_MALE` / `MINIMAX_TTS_VOICE_FEMALE` 覆盖。
- 模型可用 `MINIMAX_TTS_MODEL` 覆盖(默认 `speech-2.6-hd`)。
### 4.4 音乐 `POST /v1/music_generation`(同步,新 skill
请求体:
```json
{
"model": "music-2.6-free",
"prompt": "<风格/情绪/场景>",
"lyrics": "[verse]\n...\n[chorus]\n...",
"output_format": "hex",
"audio_setting": { "sample_rate": 44100, "bitrate": 256000, "format": "mp3" }
}
```
- 响应 `data.audio` 为 **hex** → `bytes.fromhex` 写 mp3。
- 歌词规则:
- 提供 `lyrics`:直接用(含 `[Verse]`/`[Chorus]` 等结构标签,`\n` 分行)。
- 未提供且 `is_instrumental` 为真:`is_instrumental:true`(不需要 lyrics)。
- 未提供且非纯音乐:`lyrics_optimizer:true`(系统据 `prompt` 自动写词)。
- 仅用 `MINIMAX_API_KEY`(音乐只有 MiniMax 提供,无 provider 判断);模型可用 `MINIMAX_MUSIC_MODEL` 覆盖(默认 `music-2.6-free`,付费用户可设 `music-2.6`)。
## 5. 各组件改动清单
### 5.1 `skills/public/image-generation/scripts/generate.py`
- 抽出现有 Gemini 逻辑为 `_generate_image_gemini(...)`。
- 新增 `_generate_image_minimax(...)`、`_resolve_provider("image_generation", ...)`、`_to_data_url(path)`。
- `generate_image(...)` 顶层按 provider 路由;保留 CLI 与签名不变。
- `SKILL.md`:在说明里补充 MiniMax provider 与所需环境变量(不改变调用方式)。
### 5.2 `skills/public/video-generation/scripts/generate.py`
- 同上模式:`_generate_video_gemini`、`_generate_video_minimax`(三步轮询)、`_resolve_provider("video_generation", ...)`。
- `SKILL.md` 补充 MiniMax provider 说明。
### 5.3 `skills/public/podcast-generation/scripts/generate.py`
- `text_to_speech_volcengine`(现有改名)+ `text_to_speech_minimax``_process_line`/`tts_node` 内按 `_resolve_provider("podcast_generation", ...)` 选择合成函数与 voice 映射。
- 环境变量校验同时支持两套;`SKILL.md` 补充说明。
### 5.4 新增 `skills/public/music-generation/`(用 skill-creator
- 用 `skill-creator/scripts/init_skill.py` 脚手架生成目录骨架,再填充:
- `SKILL.md`frontmatter `name: music-generation` + description;说明输入 JSON 结构、调用方式、环境变量、示例(按现有生成 skill 的风格与运行时路径 `/mnt/skills/public/music-generation/...`)。
- `scripts/generate.py`CLI `--prompt-file <json> --output-file <mp3>`;读 JSON `{title, prompt, lyrics?, is_instrumental?}`;调 `/v1/music_generation`hex→mp3。
- `frontend/src/app/mock/api/skills/route.ts`:新增 `music-generation` 条目(按字母序,`category:"public"`、`enabled:true`),使其出现在 UI skill 列表。
## 6. 测试(TDD
- 框架:pytest。测试目录:仓库根 `tests/skills/`(**不放进会部署到沙箱的 skill 目录**)。
- 用 `importlib.util.spec_from_file_location` 按路径加载各 `generate.py`。
- `requests.post` / `requests.get` 全部用 `unittest.mock` 打桩,**不打真实 API**。
- 覆盖点:
- `_resolve_provider`:各环境变量组合(仅现有 key / 仅 MiniMax key / 两者 / 都无 / `<SKILL>_PROVIDER` 覆盖)→ 正确 provider 或正确报错。
- 请求体构造:image/video/podcast/music 各自 payload 字段、模型默认与 env 覆盖、参考图 Data URL 转换。
- 响应解析:image base64 解码写文件、music/podcast hex 解码、video 三步流转(mock task_id→Success→download_url→内容写出)。
- 错误:`base_resp.status_code != 0` 抛异常;video `Fail`/超时分支。
- 先写失败测试,再实现到通过。
## 7. 向后兼容性
- 现有 CLI 参数与默认行为完全不变;仅当现 provider 凭证缺失(或显式 `<SKILL>_PROVIDER`)时才走 MiniMax。
- 不改 LLM 侧已有的 MiniMax 接入。
## 8. 新增环境变量汇总
| 变量 | 用途 | 默认 |
|---|---|---|
| `MINIMAX_API_KEY` | 复用现有 LLM 同名 key | 必填(走 MiniMax 时) |
| `MINIMAX_API_HOST` | MiniMax base url | `https://api.minimaxi.com` |
| `IMAGE_GENERATION_PROVIDER` / `VIDEO_GENERATION_PROVIDER` / `PODCAST_GENERATION_PROVIDER` | 强制 provider | 不设(自动判断) |
| `MINIMAX_IMAGE_MODEL` | 图像模型 | `image-01` |
| `MINIMAX_VIDEO_MODEL` | 视频模型 | `MiniMax-Hailuo-2.3` |
| `MINIMAX_TTS_MODEL` | TTS 模型 | `speech-2.6-hd` |
| `MINIMAX_TTS_VOICE_MALE` / `MINIMAX_TTS_VOICE_FEMALE` | 播客音色 | 选定的男/女系统音色 |
| `MINIMAX_MUSIC_MODEL` | 音乐模型 | `music-2.6-free` |
## 9. 非目标(YAGNI
- 不做翻唱(`music-cover` / `music_cover_preprocess`)、独立歌词生成接口(`lyrics_generation`,音乐内置 `lyrics_optimizer` 已覆盖"自动写词")、音色复刻/设计、视频模板 Agent、流式合成。
- 不为各 skill 抽象统一 "GenerationProvider" 框架(沙箱隔离 + YAGNI)。