# 06 - OrangeStudio 创作工作室指南

> AI 音乐创作工作站 · v0.6 核心链路已上线（写词 / 音乐生成 / 人声伴奏分轨 / 工程保存）

## 一、概述

OrangeStudio 是 OrangeRadio 的核心差异化能力：一个**AI 音乐创作工作站**，由 **MiniMax music-2.6** 驱动。

```
灵感描述 ──► AI 写词 ──► 音乐生成（带词带唱） ──► 人声/伴奏分轨 ──► 工程保存
```

### 当前能力（v0.6）

| 能力 | 状态 | 说明 |
|---|---|---|
| AI 写词 | ✅ 已实现 | LLM 生成结构化歌词（主歌/副歌/桥段），可手动编辑 |
| 音乐生成 | ✅ 已实现 | MiniMax music_generation，一步出带词带唱完整歌曲 |
| 人声/伴奏分轨 | ✅ 已实现（双轨） | 同一 prompt 两次调用：带唱版 + 纯伴奏版 |
| 工程保存/加载 | ✅ 已实现 | `.orp` JSON 格式，本地路径缓存 |
| DAW 多轨编辑 | 🔜 待实现 | 时间线 / 钢琴卷帘 / 混音台 |
| 5 轨 STEM 分离 | 🔜 待实现 | 当前仅人声/伴奏双轨，5 轨需接 Demucs |
| AI 演唱（独立 TTS） | 🔜 待实现 | 音乐生成已含人声，独立 TTS 留后续 |
| 混音母带 | 🔜 待实现（v0.8） | 响度归一 / 立体声拓宽 |

## 二、MiniMax 集成

### Provider 抽象
创作 AI 通过 `AudioAIProvider` trait 抽象，当前主力实现为 MiniMax，但可替换为 Suno/Udio 等：

```rust
#[async_trait]
pub trait AudioAIProvider: Send + Sync {
    fn name(&self) -> &str;
    fn capabilities(&self) -> ProviderCapabilities;
    async fn generate(&self, request: &GenerationRequest) -> Result<GenerationResult>;
    async fn query(&self, task_id: &str) -> Result<GenerationResult>;
}
```

### MiniMax 能力映射
| OrangeStudio 能力 | MiniMax 服务 | 状态 |
|---|---|---|
| AI 写词 | MiniMax LLM（Anthropic 兼容端点 `/v1/messages`） | ✅ |
| 音乐生成 | MiniMax music_generation（`/v1/music_generation`，music-2.6 模型） | ✅ |
| 人声/伴奏分轨 | music_generation 双调用（`is_instrumental` 切换） | ✅ |
| 5 轨 STEM 分离 | MiniMax 暂无独立端点，需接 Demucs | 🔜 |
| AI 演唱（独立 TTS） | MiniMax TTS / 语音克隆 | 🔜 |

### 配置

在**设置面板**配置两组参数（共用同一个 API Key）：

**① AI 配置（MiniMax）** —— 用于歌词译注 + Studio 写词
| 字段 | localStorage 键 | 默认值 |
|---|---|---|
| API Key | `orangeradio_minimax_key` | （空） |
| API Base | `orangeradio_minimax_base` | `https://api.minimaxi.com/anthropic` |
| Model | `orangeradio_minimax_model` | `MiniMax-M1` |

**② AI 音乐生成（Studio 创作台）** —— 用于 music_generation 端点
| 字段 | localStorage 键 | 默认值 |
|---|---|---|
| Music Base | `orangeradio_minimax_music_base` | `https://api.minimaxi.com` |
| Music Model | `orangeradio_minimax_music_model` | `music-2.6-free` |

> 💡 `music-2.6-free` 是限免版（有 RPM 限制和每日额度）；额度耗尽可切 `music-2.6` 正式版。
> 💡 Key 仅存本地 localStorage，不进 git、不上传。

## 三、已实现流程详解

### Step 1: 提示词输入

在 Studio 页面输入一句自然语言描述，例如：
- "深夜电台感的 synthwave，BPM 108，女声，副歌有城市霓虹的画面"
- "中文 R&B，低频温暖，歌词关于错过的消息和凌晨三点的橙色路灯"

也提供 3 个预设提示词快速填充。

### Step 2: AI 写词（`LyricsGenerator`）

点击「AI 写词」按钮，调 MiniMax LLM 生成结构化歌词：

```rust
let draft = lyrics_gen.generate(&LyricsRequest {
    theme: "夏夜海边".into(),
    mood: "欢快怀旧".into(),
    style: "合成器流行".into(),
    language: "中文".into(),
    ..Default::default()
}).await?;
```

输出 `LyricsDraft`（标题 / 段落 / 主题 / 押韵方案），前端渲染为可编辑的文本框（`[Verse]\n歌词...` 格式），用户可在生成音乐前手动调整。

`LyricsDraft::to_minimax_lyrics()` 方法把结构化段落渲染为 MiniMax music_generation 期望的歌词格式。

### Step 3: 音乐生成（`MiniMaxProvider::generate`）

点击「开始创作」，调 MiniMax music_generation API：

```rust
let provider = MiniMaxProvider::new(api_key, music_base, music_model);
let result = provider.generate(&GenerationRequest {
    style_prompt: prompt,
    lyrics: Some(lyrics_text),
    params: json!({ "is_instrumental": false }),
    ..Default::default()
}).await?;
```

**接口语义**：music_generation 是**同步**接口，一次 POST 直接返回音频（耗时约 30-90 秒）。Rust 端把音频下载到输出目录 `{timestamp}-{task_id}.mp3`，前端用 `convertFileSrc` 播放。

**自动写词（`auto_lyrics`，默认开启）**：MiniMax `music_generation` 响应**不返回歌词**（官方文档确认，请求里传的词或 `lyrics_optimizer` 自动写的词都拿不回来）。为让用户「听到 = 看到」，当用户没传歌词且非纯伴奏时，`studio_generate_music` 命令会**先**调一次 LLM 写词（复用 `LyricsGenerator` + `orangeradio_minimax_*` 的 LLM 配置），把词同时用于：① 塞进 `GenerationRequest.lyrics` 喂给 MiniMax 演唱；② 写进返回 JSON 的 `lyrics` 字段回传前端展示，并同步存成 `{timestamp}-{task_id}.txt`。写词失败不阻断音乐生成——降级为让 MiniMax 自己 `lyrics_optimizer` 盲写（此时返回的 `lyrics` 为 null，`lyrics_note` 给出降级提示）。纯伴奏模式（`is_instrumental=true`）跳过自动写词。

**请求体关键字段**：
- `model`: `music-2.6-free` / `music-2.6`
- `prompt`: 风格描述
- `lyrics`: 歌词文本（为空且开启 auto_lyrics 时由后端先写词再传入；仍为空则 MiniMax 自动补词）
- `is_instrumental`: `true` = 纯伴奏，`false` = 带人声演唱
- `lyrics_optimizer`: `true`（MiniMax 服务端自动优化歌词，但产物不回传）
- `stream`: `false`（明确请求返回 URL 形式 `data.audio_url`，24h 内有效；`true` 才返回 hex 字节流）
- `audio_setting`: 44100Hz / 256kbps / mp3 格式 / 双声道

> ⚠ **`format` vs `stream`（曾踩坑）**：`audio_setting.format` 是「音频编码格式」（mp3/wav/pcm/flac），**不是**「返回方式」。控制返回方式的是请求体的 `stream` 字段：`false`→URL，`true`→hex。早期误把 `format` 当返回方式传 `"url"` 会触发服务端 `2013: invalid params, audio format: url is not allowed`。详见下方「踩坑记录」。

> ⚠ **响应双模兼容**：即便传 `stream:false`，部分账号/网关仍会把 hex 字节塞进 `data.audio` 返回。`minimax.rs` 的响应解析做双模兼容——按字符串内容（而非字段名）分发：以 `http(s)://`/`file://`/`/` 开头按 URL 处理，否则按 hex 解码落 temp 再以 `file://` 回传，由 `download_audio` 统一复制到输出目录。

### Step 4: 人声/伴奏分轨（`StemSeparator`）

点击「生成分轨」，**调用 MiniMax 两次**：
- 第一次 `is_instrumental=false` → 带人声演唱版
- 第二次 `is_instrumental=true` → 纯伴奏版

⚠ **重要说明**：这是「双轨试听」而非精确分离：
- 消耗**双倍额度**
- 两次生成基于同一 prompt，但旋律/编曲会有**随机差异**（适合试听人声/伴奏效果，不适合精确混音）
- 完整 5 轨 STEM（人声/鼓/贝斯/和声/其他）需后续接入 Demucs

### Step 5: 工程保存（`StudioProject`）

生成的工程可保存为 `.orp` 文件（JSON 格式）：

```rust
let project = StudioProject::new("夏夜回忆");
project.save_to_path(&Path::new(".orangeradio/studio/夏夜回忆.orp"))?;
```

`.orp` 文件保存工程元数据（BPM / 调性 / 轨道配置 / 歌词）。音频以**本地路径**形式存储，跨设备不可用（端云同步是 v0.7）。

## 四、Tauri 命令（IPC）

前端通过 5 个命令调用 Studio 能力：

| 命令 | 功能 | 返回 |
|---|---|---|
| `studio_generate_lyrics` | AI 写词 | `LyricsDraft` JSON |
| `studio_generate_music` | 音乐生成（含自动写词，下载到本地） | `{ audio_path, task_id, lyrics?, lyrics_note? }` |
| `studio_separate_vocal` | 人声/伴奏分轨（双调用） | `{ vocals_path, instrumental_path }` |
| `studio_project_save` | 保存工程 | 文件路径 |
| `studio_project_load` | 加载工程 | `StudioProject` JSON |

所有命令的 `api_base` / `api_key` / `model` 由前端从 localStorage 读取后作为参数传入（与现有 `lyric_annotate` 模式一致，不存 AppState）。

`studio_generate_music` 额外参数：
- `output_dir: Option<String>` — 用户配置的输出目录（为空回退 `app_data_dir/studio`）
- `auto_lyrics: Option<bool>` — 是否启用自动写词（默认 true；纯伴奏时自动跳过）
- `lyrics_api_base` / `lyrics_api_key` / `lyrics_model` — 写词用的 LLM 配置（复用 `orangeradio_minimax_*`，与 music 端点区分）

`studio_separate_vocal` 和 `studio_project_save` 同样接受 `output_dir` 参数。

### 创作输出目录

生成的音频、歌词、分轨、工程文件都落到「创作输出目录」：
- **默认**：`{app_data_dir}/studio/`（Windows 下 `%APPDATA%\com.orangeradio.app\studio\`）
- **自定义**：设置 → 📁 创作输出目录 → 「选择…」挑一个文件夹；留空则用默认值

配置存 localStorage key `orangeradio_studio_output_dir`，由 `getStudioConfig()` 读取后透传给后端命令。设置页同时提供「打开」按钮（调用 `@tauri-apps/plugin-shell` 的 `open()` 在资源管理器中定位）。生成结果区的「📂 打开目录」按钮同样打开该目录。

## 五、前端代码位置

## 五、前端代码位置

| 文件 | 作用 |
|---|---|
| `frontend/src/features/studio/StudioView.tsx` | 创作台主界面（提示词 / 写词 / 生成 / 分轨） |
| `frontend/src/stores/studioStore.ts` | Zustand store（prompt / lyrics / audioPath / stems 状态） |
| `frontend/src/lib/studio.ts` | invoke wrapper + localStorage 配置读取 |
| `frontend/src/components/SettingsModal.tsx` | AI 配置面板（两组配置） |
| `frontend/src/styles/studio.css` | 创作台样式 |

## 六、模块代码位置（Rust）

| 模块 | 文件 | 状态 |
|---|---|---|
| Provider 抽象 | `crates/orange-studio/src/provider.rs` | ✅ 稳定 |
| MiniMax 对接 | `crates/orange-studio/src/minimax.rs` | ✅ 已实现 |
| 写词 | `crates/orange-studio/src/lyrics.rs` | ✅ 已实现 |
| 分轨 | `crates/orange-studio/src/stems.rs` | ✅ 已实现（双轨） |
| 工程文件 | `crates/orange-studio/src/project.rs` | ✅ 已实现 |
| IPC 命令 | `crates/orange-tauri/src/commands.rs` | ✅ 已注册 |
| 作曲（胶水） | `crates/orange-studio/src/composition.rs` | 🟡 半骨架 |
| 演唱 | `crates/orange-studio/src/vocal.rs` | 🔜 骨架 |
| 渲染 | `crates/orange-studio/src/render.rs` | 🔜 骨架（v0.8） |

## 七、后续规划

- **5 轨 STEM 分离**：接入 Demucs 本地模型或等待 MiniMax 新端点
- **DAW 多轨编辑器**：时间线 / 钢琴卷帘 / 混音台（v0.7+）
- **AI 演唱独立 TTS**：基于已有音频替换人声（音色克隆）
- **混音母带**：响度归一 / 立体声拓宽（v0.8）
- **发布社区**：原创歌单 / Remix / 商用授权（v0.7 社交后端）

## 七·补、踩坑记录

### 1. `audio_setting.format` ≠ 返回方式（误传 `"url"` 触发 2013）

早期误以为 `audio_setting.format` 控制「返回方式」，传 `"url"` 想让 MiniMax 返回 URL，结果服务端报：

```
2013: invalid params, audio format: url is not allowed
```

**正解**：`format` 是音频编码格式（mp3/wav/pcm/flac），控制返回方式的是请求体的 `stream` 字段（`false`→URL，`true`→hex）。现在请求体显式传 `"stream": false` + `"format": "mp3"`。

### 2. MiniMax `music_generation` 响应不返回歌词

最初以为可以从生成结果里取歌词回传给前端展示，但抓官方文档（[英文](https://platform.minimax.io/docs/api-reference/music-generation)/[中文](https://platform.minimaxi.com/docs/api-reference/music-generation)）确认：响应 JSON 只有 `data.audio_url`、`data.audio`、`data.task_id`、`extra_info.audio_length` 等音频元信息，**没有任何歌词字段**。请求里传的 `lyrics` 不回显，`lyrics_optimizer` 自动写的词也不回传。

**对策**：用「自动写词」——`studio_generate_music` 命令在用户没传词时，先调一次 LLM（`LyricsGenerator`）写词，把词同时用于 MiniMax 演唱和前端展示，保证「听到 = 看到」。MiniMax 另有独立的 `lyrics_generation` 端点专门返回歌词，但本项目复用已有 LLM 写词能力，能力等价。

### 3. `data.audio` 既可能是 URL 也可能是 hex（响应双模兼容）

即便请求传 `stream:false`，部分账号/网关仍会把 hex 编码的音频字节流塞进 `data.audio` 字段返回（而非走 `data.audio_url`）。最初代码把 `data.audio` 当 URL 解析，hex 字符串（一大串 `0-9a-f`）既非 http(s) URL 也不以 `/` 开头，触发 `normalize_audio_url` 报错：

```
MiniMax 返回的音频 URL 非法（audio_url 既非绝对 URL 也非以 / 开头的相对路径）
```

**对策**：响应解析做双模兼容——`looks_like_url_or_path()` 按字符串**内容**（而非字段名）分发：以 `http(s)://`/`file://`/`/` 开头按 URL 处理；否则当 hex 解码到系统 temp 目录，以 `file://` URL 回传，由 `download_audio` 识别 `file://` 协议复制到最终输出目录。这样无论 MiniMax 返回哪种格式都能正确产出本地音频文件。

## 八、版权与署名

- AI 生成内容自动标注「AI 辅助创作」
- 记录创作者署名
- 商用授权管理（后端存证，v0.7）
- Remix 需遵循原作者授权条款
