# 声音分离 API 接入教程

> 在线效果体验：https://fenli.ftcxx.com/  
> 接口地址：`https://api.hihookeji.com/api/spleeteraudio/index`  
> 返回格式：`application/json`  
> 请求方式：`HTTP POST`

---

## 一、接口概述

本接口对音频做人声 / 伴奏 / 分轨 / 降噪 / 去混响等分离，异步提交后通过 `notify_url` 以 **文件流** 回调结果。

| 项目 | 说明 |
|------|------|
| 接口地址 | `https://api.hihookeji.com/api/spleeteraudio/index` |
| 认证方式 | 请求体字段 `key`（你的 API 密钥，与业务参数一并提交） |
| 请求示例 | `https://api.hihookeji.com/api/spleeteraudio/index` |
| Content-Type | `application/json`（推荐）或 `application/x-www-form-urlencoded` |
| 业务判断 | 统一用响应字段 **`code`**：`200` 成功，失败常见为 `500` |
| 音频限制 | 支持 **mp3 / wav**，时长 **不超过 12 分钟** |

---

## 二、请求参数

| 参数名 | 类型 | 必填 | 描述 | 示例 |
|--------|------|------|------|------|
| key | string | 是 | API 密钥，放在请求体中与其它参数一起提交 | `你的密钥` |
| audio_url | string | 是 | 待分离音频链接（公网可访问），**mp3 / wav**，不超过 **12 分钟**；建议 URL 以 `.mp3` 或 `.wav` 结尾 | `http://abc.com/1.mp3` |
| notify_url | string | 是 | 结果通知地址（需公网可达） | 见下方「回调测试」 |
| stype | number | 是 | `1`：试用，仅分离约 **30 秒**；`2`：分离完整版 | `2` |
| stems | string | 是 | 提取类型，见下方说明 | `Vocals` |
| mode | number | 是 | `1`：极速模式；`2`：高级模式（音质更好） | `2` |

### 2.1 stems 取值

| stems | 说明 |
|-------|------|
| Vocals | 人声 |
| Instrumental | 背景声 |
| Bass | 贝斯 |
| Drums | 鼓声 |
| Guitar | 吉他（高级模式） |
| Piano | 钢琴 |
| All | 所有分轨 |
| Vocals-Bass-Drums | 组合提取（示例：多轨用 `-` 连接） |
| DeEcho | 消除回声 |
| DeNoise | 降噪 |

**回调测试**：联调异步回调时，建议先打开 [https://webhook.site/](https://webhook.site/) 获取自己的唯一 URL，将该地址填入 `notify_url`。提交任务后可在网页上直接查看回调字段与返回的音频文件，无需先部署自己的回调服务。

---

## 三、返回结果说明

### 3.1 任务提交成功

立即返回任务 ID，分离完成后通过 `notify_url` 回调。请自行保存 `data.taskid`。

| 字段 | 说明 |
|------|------|
| code | `200` 表示任务正确提交 |
| msg | 说明信息，如 `ok` |
| data.taskid | 任务 ID |

```json
{
  "code": 200,
  "data": {
    "taskid": "172733860562215209765004"
  },
  "msg": "ok"
}
```

### 3.2 任务提交失败

```json
{
  "code": 500,
  "msg": "提取的类型：Vocals,不能为空"
}
```

### 3.3 异步回调 Notify

服务端分离完成后，向 `notify_url` **POST**（`multipart/form-data`）：表单字段 + 音频文件流，**不是** JSON Body。

公共表单字段：

| 字段 | 说明 |
|------|------|
| errcode | **`0` 成功**；**`1001` 失败**（无可用结果文件时也会是 `1001`） |
| taskid | 任务 ID，与提交返回一致 |

文件以 multipart 多文件字段形式推送，字段名随请求时的 `stems` 变化（见下表）。常见后缀为 `.mp3` / `.wav` 等。

#### 回调文件字段（与 stems 对应）

| 请求 stems | 回调文件字段（form name） | 说明 |
|------------|---------------------------|------|
| `Vocals` 或 `Instrumental` | `vocals` + `others` | 人声 + 背景声（伴奏） |
| `Bass` | `bass` + `others` | 贝斯 + 其余 |
| `Drums` | `drums` + `others` | 鼓声 + 其余 |
| `Guitar` | `guitar` + `others` | 吉他 + 其余 |
| `Piano` | `piano` + `others` | 钢琴 + 其余 |
| `All` / `all` | `vocals`、`drums`、`bass`、`guitar`、`piano`、`others` | 全分轨（`others` 对应其余轨） |
| `Vocals-Bass-Drums` 等用 `-` 组合 | `combined` | 所选分轨叠加后的合并音轨 |
| `DeNoise` | `nonoise` | 降噪结果（注意拼写是 **nonoise**） |
| `DeEcho` | `noreverb` | 去混响结果 |

> 接收端请按字段名取文件，例如 PHP：`$_FILES['vocals']`、`$_FILES['others']`、`$_FILES['nonoise']`；Flask：`request.files.get('vocals')`。失败时通常只有 `errcode`/`taskid`，无文件。

---

## 四、接入流程建议

1. 准备公网可访问的 mp3/wav（≤12 分钟，后缀建议正确）。
2. 申请 API Key，作为请求体字段 `key` 与其它参数一起提交。
3. 选择 `stype`（试用/完整）、`stems`、`mode`（极速/高级）。
4. 填入 `notify_url`；联调可用 [webhook.site](https://webhook.site/) 唯一地址。
5. 提交成功后保存 `data.taskid`；回调 `errcode=0` 时按 stems 类型保存对应文件字段。

---

## 五、Python 完整代码

依赖：`requests`（`pip install requests`）

```python
# -*- coding: utf-8 -*-
"""
声音分离 API - Python 完整示例
接口: https://api.hihookeji.com/api/spleeteraudio/index
"""

import json
import os

import requests

API_URL = "https://api.hihookeji.com/api/spleeteraudio/index"
API_KEY = "你的密钥"  # 替换为真实密钥

# 回调可能出现的文件字段名（与 stems 对应）
FILE_FIELDS = (
    "vocals",
    "others",
    "bass",
    "drums",
    "guitar",
    "piano",
    "combined",
    "nonoise",   # DeNoise
    "noreverb",  # DeEcho
)


def separate_audio(
    audio_url,
    notify_url,
    stems="Vocals",
    stype=2,
    mode=2,
    timeout=60,
):
    """
    提交声音分离任务（异步）。

    :param audio_url: 音频公网 URL（mp3/wav，≤12 分钟）
    :param notify_url: 结果回调地址
    :param stems: Vocals / Instrumental / Bass / Drums / Guitar / Piano / All /
                  组合如 Vocals-Bass-Drums / DeEcho / DeNoise
    :param stype: 1=试用约30秒，2=完整版
    :param mode: 1=极速，2=高级（音质更好）
    """
    payload = {
        "key": API_KEY,
        "audio_url": audio_url,
        "notify_url": notify_url,
        "stype": stype,
        "stems": stems,
        "mode": mode,
    }
    resp = requests.post(
        API_URL,
        json=payload,
        headers={"Content-Type": "application/json"},
        timeout=timeout,
    )
    resp.raise_for_status()
    return resp.json()


# ---------------------------------------------------------------------------
# Flask 异步回调接收示例（可选）
# 安装: pip install flask
# ---------------------------------------------------------------------------
def create_notify_app(save_dir="./callback_audio"):
    from flask import Flask, request, jsonify

    app = Flask(__name__)
    os.makedirs(save_dir, exist_ok=True)

    @app.route("/Notify", methods=["POST"])
    def notify():
        # 表单: errcode / taskid；文件字段随 stems 变化
        taskid = request.form.get("taskid")
        errcode = request.form.get("errcode")  # 成功 0，失败 1001
        saved = {}

        for name in FILE_FIELDS:
            if name in request.files:
                f = request.files[name]
                if f and f.filename:
                    ext = os.path.splitext(f.filename)[1] or ".mp3"
                    path = os.path.join(save_dir, f"{taskid}_{name}{ext}")
                    f.save(path)
                    saved[name] = path

        # 其它可能的轨道名（高级分轨等）也一并落盘
        for name, f in request.files.items():
            if name in saved or not f or not f.filename:
                continue
            ext = os.path.splitext(f.filename)[1] or ".mp3"
            path = os.path.join(save_dir, f"{taskid}_{name}{ext}")
            f.save(path)
            saved[name] = path

        return jsonify({
            "ok": str(errcode) == "0" and bool(saved),
            "taskid": taskid,
            "errcode": errcode,
            "saved": saved,
        })

    return app


if __name__ == "__main__":
    result = separate_audio(
        audio_url="http://abc.com/1.mp3",
        notify_url="https://webhook.site/你的唯一ID",
        stems="Vocals",
        stype=2,
        mode=2,
    )
    print("任务提交:", json.dumps(result, ensure_ascii=False, indent=2))
    # 成功时 code==200，记录 data.taskid；回调可在 webhook.site 查看文件

    # ---------- 启动回调服务（需要时取消注释）----------
    # app = create_notify_app()
    # app.run(host="0.0.0.0", port=8080)
```

---

## 六、PHP 完整代码

需开启 `curl` 扩展。

```php
<?php
/**
 * 声音分离 API - PHP 完整示例
 * 接口: https://api.hihookeji.com/api/spleeteraudio/index
 */

define('API_URL', 'https://api.hihookeji.com/api/spleeteraudio/index');
define('API_KEY', '你的密钥'); // 替换为真实密钥

/**
 * 提交声音分离任务
 *
 * @param array $params
 * @param int   $timeout
 * @return array
 * @throws Exception
 */
function separateAudio(array $params, $timeout = 60)
{
    $payload = [
        'key'        => API_KEY,
        'audio_url'  => $params['audio_url'],
        'notify_url' => $params['notify_url'],
        'stype'      => isset($params['stype']) ? (int)$params['stype'] : 2,
        'stems'      => isset($params['stems']) ? $params['stems'] : 'Vocals',
        'mode'       => isset($params['mode']) ? (int)$params['mode'] : 2,
    ];

    $ch = curl_init(API_URL);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $body = curl_exec($ch);
    if ($body === false) {
        $err = curl_error($ch);
        curl_close($ch);
        throw new Exception('请求失败: ' . $err);
    }
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = json_decode($body, true);
    if (!is_array($result)) {
        throw new Exception("响应非 JSON，HTTP={$httpCode}, body={$body}");
    }
    return $result;
}

try {
    $result = separateAudio([
        'audio_url'  => 'http://abc.com/1.mp3',
        'notify_url' => 'https://webhook.site/你的唯一ID',
        'stype'      => 2,
        'stems'      => 'Vocals',
        'mode'       => 2,
    ]);
    echo "任务提交:\n" . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
} catch (Exception $e) {
    echo '错误: ' . $e->getMessage() . "\n";
}

/*
======================== notify.php 异步回调接收 ========================
<?php
header('Content-Type: application/json; charset=utf-8');

$saveDir = __DIR__ . '/callback_audio';
if (!is_dir($saveDir)) {
    mkdir($saveDir, 0755, true);
}

$taskid  = isset($_POST['taskid']) ? $_POST['taskid'] : null;
$errcode = isset($_POST['errcode']) ? $_POST['errcode'] : null;
$fileKeys = ['vocals','others','bass','drums','guitar','piano','combined','nonoise','noreverb'];
$saved = [];

foreach ($_FILES as $name => $info) {
    if (empty($info['tmp_name']) || !is_uploaded_file($info['tmp_name'])) {
        continue;
    }
    $ext = pathinfo($info['name'], PATHINFO_EXTENSION);
    $ext = $ext ? ('.' . $ext) : '.mp3';
    $path = $saveDir . '/' . ($taskid ?: uniqid('task_', true)) . '_' . $name . $ext;
    move_uploaded_file($info['tmp_name'], $path);
    $saved[$name] = $path;
}

echo json_encode([
    'ok'      => ((string)$errcode === '0') && !empty($saved),
    'taskid'  => $taskid,
    'errcode' => $errcode,
    'saved'   => $saved,
], JSON_UNESCAPED_UNICODE);
*/
```

---

## 七、Java 完整代码

依赖：JDK 8+，使用内置 `HttpURLConnection`，无需第三方库。

```java
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

/**
 * 声音分离 API - Java 完整示例
 * 接口: https://api.hihookeji.com/api/spleeteraudio/index
 */
public class SpleeterAudioDemo {

    private static final String API_URL = "https://api.hihookeji.com/api/spleeteraudio/index";
    private static final String API_KEY = "你的密钥"; // 替换为真实密钥

    public static class SeparateRequest {
        public String audioUrl;
        public String notifyUrl;
        public int stype = 2;          // 1 试用约30秒，2 完整
        public String stems = "Vocals";
        public int mode = 2;           // 1 极速，2 高级
    }

    public static String separateAudio(SeparateRequest req, int timeoutMs) throws IOException {
        URL url = new URL(API_URL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setConnectTimeout(15000);
        conn.setReadTimeout(timeoutMs);
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        conn.setRequestProperty("Accept", "application/json");

        String json = toJson(req);
        byte[] body = json.getBytes(StandardCharsets.UTF_8);
        conn.setRequestProperty("Content-Length", String.valueOf(body.length));
        try (OutputStream os = conn.getOutputStream()) {
            os.write(body);
        }

        int httpCode = conn.getResponseCode();
        InputStream is = (httpCode >= 200 && httpCode < 300)
                ? conn.getInputStream()
                : conn.getErrorStream();
        String resp = readFully(is);
        conn.disconnect();
        return resp;
    }

    private static String toJson(SeparateRequest r) {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append("\"key\":").append(quote(API_KEY)).append(",");
        sb.append("\"audio_url\":").append(quote(r.audioUrl)).append(",");
        sb.append("\"notify_url\":").append(quote(r.notifyUrl)).append(",");
        sb.append("\"stype\":").append(r.stype).append(",");
        sb.append("\"stems\":").append(quote(r.stems)).append(",");
        sb.append("\"mode\":").append(r.mode);
        sb.append("}");
        return sb.toString();
    }

    private static String quote(String s) {
        if (s == null) {
            return "null";
        }
        String escaped = s
                .replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
        return "\"" + escaped + "\"";
    }

    private static String readFully(InputStream is) throws IOException {
        if (is == null) {
            return "";
        }
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buf = new byte[4096];
        int n;
        while ((n = is.read(buf)) != -1) {
            bos.write(buf, 0, n);
        }
        return new String(bos.toByteArray(), StandardCharsets.UTF_8);
    }

    /*
     * 回调 Servlet：multipart
     * 表单: errcode(0成功/1001失败), taskid
     * 文件字段随 stems:
     *   Vocals/Instrumental -> vocals + others
     *   Bass/Drums/Guitar/Piano -> 对应分轨 + others
     *   All -> vocals/drums/bass/guitar/piano/others
     *   组合 Vocals-Bass-... -> combined
     *   DeNoise -> nonoise；DeEcho -> noreverb
     */

    public static void main(String[] args) throws Exception {
        SeparateRequest req = new SeparateRequest();
        req.audioUrl = "http://abc.com/1.mp3";
        req.notifyUrl = "https://webhook.site/你的唯一ID";
        req.stype = 2;
        req.stems = "Vocals";
        req.mode = 2;

        System.out.println("任务提交: " + separateAudio(req, 60000));
        // 成功时 code==200，记录 data.taskid
    }
}
```

---

## 八、常见问题

| 问题 | 处理建议 |
|------|----------|
| 音频格式 / 时长 | 仅 **mp3 / wav**，不超过 **12 分钟**；URL 建议以 `.mp3` / `.wav` 结尾 |
| OSS 鉴权长链失败 | 无标准后缀的鉴权 URL 易失败，请转存为后缀正确的直链 |
| stems 填错 | 大小写按文档：`Vocals`、`Instrumental`、`DeNoise`、`DeEcho` 等 |
| Guitar 效果差 | Guitar 需走 **高级模式**（`mode=2`） |
| 只分离了很短一段 | 检查是否 `stype=1` 试用（约 30 秒）；完整版用 `stype=2` |
| 只看 HTTP 200 | 不够，必须判断响应体 `code == 200` |
| 异步收不到回调 | 确认 `notify_url` 公网可达、支持 multipart；联调可用 [webhook.site](https://webhook.site/) |
| 回调当成 JSON 解析失败 | 回调是 multipart 文件流 + 表单字段，用 `$_FILES` / `request.files` 接收 |
| 回调成功却判失败 | 回调成功 `errcode` 为 **`0`**，失败为 `1001` |
| 找不到文件字段 | 按 stems 对照：`vocals`/`others`、`bass|drums|guitar|piano`+`others`、`All` 六轨、`combined`、降噪 **`nonoise`**、去混响 **`noreverb`** |
| 降噪字段写成 nonise | 实际回调字段名为 **`nonoise`**（双 o） |

---

## 九、快速对照

| 项目 | 说明 |
|------|------|
| 提交 | `key` / `audio_url` / `notify_url` / `stype` / `stems` / `mode` |
| 立即响应 | `code:200` + `data.taskid` |
| 成果获取 | `notify_url`：表单 `errcode`/`taskid` + multipart 分轨文件（见 3.3） |
| stype | `1` 试用约 30 秒；`2` 完整 |
| mode | `1` 极速；`2` 高级音质更好 |
