@henjicc/ai-sdk 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +231 -211
- package/README.md +2 -2
- package/dist/capabilities/speech-recognition/bailian/module.js +29 -14
- package/dist/capabilities/speech-recognition/bailian/types.d.ts +2 -0
- package/dist/catalog/apimart/gemini-omni-flash.model.js +3 -14
- package/dist/catalog/bailian/cosyvoice-v3.5.model.js +1 -1
- package/dist/catalog/bailian/minimax-speech-2.8.model.js +1 -1
- package/dist/catalog/bailian/qwen-audio-3.1-tts-flash.model.js +1 -1
- package/dist/catalog/fal/eleven-v3-tts.model.js +1 -1
- package/dist/catalog/fal/minimax-speech-2.8.model.js +1 -1
- package/dist/catalog/kie/eleven-turbo-2.5-tts.model.js +1 -1
- package/dist/catalog/kie/eleven-v3-dialogue.model.js +1 -1
- package/dist/catalog/kie/gemini-3.1-flash-tts.model.js +1 -1
- package/dist/catalog/kie/gemini-omni-video.model.js +5 -26
- package/dist/catalog/ppio/minimax-speech.model.js +1 -1
- package/dist/catalog/siliconflow/cosyvoice2-tts.model.js +1 -1
- package/dist/catalog/siliconflow/moss-ttsd-0.5.model.js +1 -1
- package/dist/catalog/volcengine-speech/seed-icl-2.0.model.d.ts +1 -0
- package/dist/catalog/volcengine-speech/seed-icl-2.0.model.js +52 -10
- package/dist/catalog/volcengine-speech/seed-tts-2.0-voices.d.ts +2 -0
- package/dist/catalog/volcengine-speech/seed-tts-2.0-voices.js +438 -0
- package/dist/catalog/volcengine-speech/seed-tts-2.0.model.js +3 -3
- package/dist/generation/core.d.ts +6 -0
- package/dist/generation/core.js +10 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/tts-voices.d.ts +9 -0
- package/dist/providers/tts-voices.js +139 -0
- package/dist/providers/volcengine-speech.d.ts +1 -1
- package/dist/providers/volcengine-speech.js +9 -3
- package/dist/providers/volcengine-voice-clone.d.ts +3 -0
- package/dist/providers/volcengine-voice-clone.js +129 -0
- package/dist/types/runtime.d.ts +3 -0
- package/dist/upload/media-binary.js +1 -0
- package/docs/consumers.md +27 -3
- package/docs/model-adaptation/Gemini-Omni-Flash/Gemini-Omni-Flash_APIMart.md +1 -0
- package/docs/model-adaptation/Gemini-Omni-Flash/Gemini-Omni-Flash_KIE.md +1 -0
- package/docs/model-adaptation/README.md +11 -7
- package/docs/model-adaptation/Seed-ICL-2.0/Seed-ICL-2.0_/347/201/253/345/261/261/345/274/225/346/223/216.md +65 -0
- package/docs/model-adaptation/TTS-/345/244/232/344/276/233/345/272/224/345/225/206-2026-09.md +69 -55
- package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/231/276/347/202/274.md +6 -0
- package/docs/model-adaptation//346/226/207/346/241/243/351/207/207/351/233/206/346/211/213/345/206/214.md +6 -5
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { fetchProvider } from './provider-fetch.js';
|
|
2
|
+
const VOICE_PROVIDERS = {
|
|
3
|
+
'bailian-minimax-speech-2.8': 'bailian',
|
|
4
|
+
'bailian-cosyvoice-v3.5': 'bailian',
|
|
5
|
+
'bailian-qwen-audio-3.1-tts-flash': 'bailian',
|
|
6
|
+
'siliconflow-cosyvoice2-tts': 'siliconflow',
|
|
7
|
+
};
|
|
8
|
+
function object(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
function string(value) {
|
|
14
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
15
|
+
}
|
|
16
|
+
function voiceDescription(value) {
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value.trim() || undefined;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
const joined = value.filter((item) => typeof item === 'string' && item.trim().length > 0).join(' · ');
|
|
21
|
+
return joined || undefined;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function uniqueVoices(voices) {
|
|
26
|
+
return [...new Map(voices.map((voice) => [voice.id, voice])).values()];
|
|
27
|
+
}
|
|
28
|
+
async function requestJson(providerId, url, apiKey, runtime, body) {
|
|
29
|
+
const response = await fetchProvider(providerId, url, {
|
|
30
|
+
method: body ? 'POST' : 'GET',
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${apiKey}`,
|
|
33
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
34
|
+
},
|
|
35
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
36
|
+
}, { transport: runtime.transport, retryPreconnectOnce: false });
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`${providerId} 音色列表请求失败(HTTP ${response.status})`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return await response.json();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`${providerId} 音色列表响应格式错误`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function listBailianMinimax(apiKey, runtime) {
|
|
48
|
+
const payload = object(await requestJson('bailian', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', apiKey, runtime, { model: 'MiniMax/speech-2.8-turbo', input: { action: 'get_voice', voice_type: 'all' } }));
|
|
49
|
+
const output = object(payload?.output);
|
|
50
|
+
const baseResp = object(output?.base_resp);
|
|
51
|
+
if (!output || baseResp?.status_code !== 0 || (!Array.isArray(output.system_voice) && !Array.isArray(output.voice_cloning))) {
|
|
52
|
+
throw new Error('百炼 MiniMax 音色列表返回错误');
|
|
53
|
+
}
|
|
54
|
+
const voices = [];
|
|
55
|
+
for (const source of ['system_voice', 'voice_cloning']) {
|
|
56
|
+
const entries = output[source];
|
|
57
|
+
if (!Array.isArray(entries))
|
|
58
|
+
continue;
|
|
59
|
+
for (const item of entries) {
|
|
60
|
+
const entry = object(item);
|
|
61
|
+
const id = string(entry?.voice_id);
|
|
62
|
+
if (!id)
|
|
63
|
+
continue;
|
|
64
|
+
voices.push({
|
|
65
|
+
id,
|
|
66
|
+
name: string(entry?.voice_name) || id,
|
|
67
|
+
description: voiceDescription(entry?.description),
|
|
68
|
+
source: source === 'system_voice' ? 'system' : 'clone',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return uniqueVoices(voices);
|
|
73
|
+
}
|
|
74
|
+
async function listBailianEnrollment(modelId, apiKey, runtime) {
|
|
75
|
+
const prefix = modelId === 'bailian-cosyvoice-v3.5' ? 'cosyvoice-v3.5-' : 'qwen-audio-3.1-tts-flash-';
|
|
76
|
+
const voices = [];
|
|
77
|
+
const pageSize = 50;
|
|
78
|
+
for (let pageIndex = 0; pageIndex < 20; pageIndex += 1) {
|
|
79
|
+
const payload = object(await requestJson('bailian', 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization', apiKey, runtime, { model: 'voice-enrollment', input: { action: 'list_voice', page_index: pageIndex, page_size: pageSize } }));
|
|
80
|
+
const output = object(payload?.output);
|
|
81
|
+
const entries = output?.voice_list;
|
|
82
|
+
if (!Array.isArray(entries))
|
|
83
|
+
throw new Error('百炼复刻音色列表响应格式错误');
|
|
84
|
+
for (const item of entries) {
|
|
85
|
+
const entry = object(item);
|
|
86
|
+
const id = string(entry?.voice_id);
|
|
87
|
+
if (!id.startsWith(prefix) || entry?.status !== 'OK')
|
|
88
|
+
continue;
|
|
89
|
+
voices.push({ id, name: id, source: 'clone' });
|
|
90
|
+
}
|
|
91
|
+
if (entries.length < pageSize)
|
|
92
|
+
return uniqueVoices(voices);
|
|
93
|
+
}
|
|
94
|
+
throw new Error('百炼复刻音色超过查询页数限制');
|
|
95
|
+
}
|
|
96
|
+
async function listSiliconflow(apiKey, runtime) {
|
|
97
|
+
const payload = await requestJson('siliconflow', 'https://api.siliconflow.cn/v1/audio/voice/list', apiKey, runtime);
|
|
98
|
+
const root = object(payload);
|
|
99
|
+
const data = object(root?.data);
|
|
100
|
+
const entries = Array.isArray(payload) ? payload
|
|
101
|
+
: Array.isArray(root?.data) ? root.data
|
|
102
|
+
: Array.isArray(data?.voices) ? data.voices
|
|
103
|
+
: Array.isArray(root?.voices) ? root.voices
|
|
104
|
+
: null;
|
|
105
|
+
if (!entries)
|
|
106
|
+
throw new Error('硅基流动音色列表响应格式错误');
|
|
107
|
+
const voices = [];
|
|
108
|
+
for (const item of entries) {
|
|
109
|
+
const entry = object(item);
|
|
110
|
+
const id = string(entry?.uri);
|
|
111
|
+
if (!id)
|
|
112
|
+
continue;
|
|
113
|
+
voices.push({ id, name: string(entry?.customName) || string(entry?.name) || id, source: 'clone' });
|
|
114
|
+
}
|
|
115
|
+
return uniqueVoices(voices);
|
|
116
|
+
}
|
|
117
|
+
/** 查询供应商账号中的可用音色;静态系统音色由宿主的展示目录提供。 */
|
|
118
|
+
export async function listTtsVoices(modelId, runtime) {
|
|
119
|
+
if (!Object.prototype.hasOwnProperty.call(VOICE_PROVIDERS, modelId))
|
|
120
|
+
throw new Error(`不支持查询 ${modelId} 的音色`);
|
|
121
|
+
const providerId = VOICE_PROVIDERS[modelId];
|
|
122
|
+
const apiKey = await runtime.credentials.get('generation', providerId);
|
|
123
|
+
if (!apiKey)
|
|
124
|
+
throw new Error(`请先设置${providerId} API Key`);
|
|
125
|
+
runtime.logger?.info('tts_voice.list.start', { providerId, modelId });
|
|
126
|
+
try {
|
|
127
|
+
const voices = modelId === 'bailian-minimax-speech-2.8'
|
|
128
|
+
? await listBailianMinimax(apiKey, runtime)
|
|
129
|
+
: modelId === 'siliconflow-cosyvoice2-tts'
|
|
130
|
+
? await listSiliconflow(apiKey, runtime)
|
|
131
|
+
: await listBailianEnrollment(modelId, apiKey, runtime);
|
|
132
|
+
runtime.logger?.info('tts_voice.list.completed', { providerId, modelId, context: { count: voices.length } });
|
|
133
|
+
return voices;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
runtime.logger?.warn('tts_voice.list.failed', { providerId, modelId, error });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ProviderContinuePollingInput, ProviderExecutionInput, ProviderExecutionResult } from '../types/runtime.js';
|
|
2
2
|
export declare function execute(input: ProviderExecutionInput): Promise<ProviderExecutionResult>;
|
|
3
|
-
export declare function continuePolling(
|
|
3
|
+
export declare function continuePolling(input: ProviderContinuePollingInput): Promise<ProviderExecutionResult>;
|
|
@@ -5,6 +5,7 @@ import { fromBase64 } from '../upload/base64.js';
|
|
|
5
5
|
import { audioBytesToDataUri } from './audio-data.js';
|
|
6
6
|
import { isJsonObject, normalizeEndpoint } from './helpers.js';
|
|
7
7
|
import { fetchProvider } from './provider-fetch.js';
|
|
8
|
+
import { executeVoiceClone, pollVoiceClone } from './volcengine-voice-clone.js';
|
|
8
9
|
const BASE_URL = 'https://openspeech.bytedance.com';
|
|
9
10
|
const ENDPOINT = '/api/v3/tts/unidirectional';
|
|
10
11
|
function decodeChunk(encoded) {
|
|
@@ -76,6 +77,8 @@ function consumeFrames(buffer, audio, usage) {
|
|
|
76
77
|
return buffer.slice(start < 0 ? consumed : start);
|
|
77
78
|
}
|
|
78
79
|
export async function execute(input) {
|
|
80
|
+
if (input.route === '/api/v3/tts/voice_clone')
|
|
81
|
+
return executeVoiceClone(input);
|
|
79
82
|
if (!isJsonObject(input.body) || !isJsonObject(input.body.req_params)) {
|
|
80
83
|
throw new AiRuntimeError('invalid_request', 'Volcengine speech requires req_params');
|
|
81
84
|
}
|
|
@@ -135,9 +138,12 @@ export async function execute(input) {
|
|
|
135
138
|
return {
|
|
136
139
|
status: 'completed',
|
|
137
140
|
url: audioBytesToDataUri(joined, 'mp3'),
|
|
138
|
-
metadata: { byteLength: total, ...(usage.textWords === undefined ? {} : { textWords: usage.textWords })
|
|
141
|
+
metadata: { byteLength: total, ...(usage.textWords === undefined ? {} : { textWords: usage.textWords }),
|
|
142
|
+
...(resourceId === 'seed-icl-2.0' && typeof input.body.req_params.speaker === 'string'
|
|
143
|
+
? { activatedVoiceId: input.body.req_params.speaker } : {}),
|
|
144
|
+
},
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
|
-
export async function continuePolling(
|
|
142
|
-
|
|
147
|
+
export async function continuePolling(input) {
|
|
148
|
+
return pollVoiceClone(input);
|
|
143
149
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProviderExecutionInput, ProviderContinuePollingInput, ProviderExecutionResult } from '../types/runtime.js';
|
|
2
|
+
export declare function executeVoiceClone(input: ProviderExecutionInput): Promise<ProviderExecutionResult>;
|
|
3
|
+
export declare function pollVoiceClone(input: ProviderContinuePollingInput): Promise<ProviderExecutionResult>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { AiRuntimeError } from '../runtime/AiRuntimeError.js';
|
|
2
|
+
import { volcengineRequestId } from '../capabilities/speech-recognition/volcengine/request-id.js';
|
|
3
|
+
import { pollUntilResult } from '../protocols/polling.js';
|
|
4
|
+
import { parseDataUri } from '../upload/media-binary.js';
|
|
5
|
+
import { toBase64 } from '../upload/base64.js';
|
|
6
|
+
import { isJsonObject } from './helpers.js';
|
|
7
|
+
import { fetchProvider } from './provider-fetch.js';
|
|
8
|
+
const TASK_PREFIX = 'volc-voice:';
|
|
9
|
+
const MAX_AUDIO_BYTES = 10_000_000;
|
|
10
|
+
function taskId(identity) { return TASK_PREFIX + encodeURIComponent(JSON.stringify(identity)); }
|
|
11
|
+
function decodeTask(value) {
|
|
12
|
+
try {
|
|
13
|
+
if (!value.startsWith(TASK_PREFIX))
|
|
14
|
+
throw new Error('prefix');
|
|
15
|
+
const data = JSON.parse(decodeURIComponent(value.slice(TASK_PREFIX.length)));
|
|
16
|
+
if (!isJsonObject(data) || typeof data.speaker !== 'string' || !data.speaker.trim()
|
|
17
|
+
|| typeof data.name !== 'string' || !data.name.trim() || typeof data.postpaid !== 'boolean')
|
|
18
|
+
throw new Error('identity');
|
|
19
|
+
return { speaker: data.speaker, name: data.name, postpaid: data.postpaid };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new AiRuntimeError('invalid_task_id', '无效的音色训练任务');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function speakerBody(identity) {
|
|
26
|
+
return identity.postpaid
|
|
27
|
+
? { speaker_id: 'custom_speaker_id', custom_speaker_id: identity.speaker }
|
|
28
|
+
: { speaker_id: identity.speaker };
|
|
29
|
+
}
|
|
30
|
+
async function request(input, route, body) {
|
|
31
|
+
input.signal?.throwIfAborted();
|
|
32
|
+
const response = await fetchProvider('Volcengine Speech', `https://openspeech.bytedance.com${route}`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json', 'X-Api-Key': input.apiKey, 'X-Api-Request-Id': volcengineRequestId(input.requestId, undefined) },
|
|
35
|
+
body: JSON.stringify(body), signal: input.signal,
|
|
36
|
+
}, { transport: input.runtime.transport, retryPreconnectOnce: false });
|
|
37
|
+
const payload = await response.json();
|
|
38
|
+
if (!isJsonObject(payload))
|
|
39
|
+
throw new AiRuntimeError('invalid_response', '音色训练返回了无效响应');
|
|
40
|
+
if (!response.ok || (payload.code !== undefined && payload.code !== 0)) {
|
|
41
|
+
throw new AiRuntimeError('provider_task_failed', typeof payload.message === 'string' ? payload.message : `音色训练请求失败(HTTP ${response.status})`);
|
|
42
|
+
}
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
function parseResult(payload, identity) {
|
|
46
|
+
if (typeof payload.speaker_id !== 'string' || !payload.speaker_id.trim()
|
|
47
|
+
|| ![0, 1, 2, 3, 4].includes(payload.status)) {
|
|
48
|
+
throw new AiRuntimeError('invalid_response', '音色训练响应缺少有效音色或状态');
|
|
49
|
+
}
|
|
50
|
+
// 后付费响应可返回固定代号;实际自定义代号来自已提交任务身份。
|
|
51
|
+
if (payload.speaker_id !== identity.speaker && !(identity.postpaid && payload.speaker_id === 'custom_speaker_id')) {
|
|
52
|
+
throw new AiRuntimeError('invalid_response', '音色查询返回了其他音色');
|
|
53
|
+
}
|
|
54
|
+
if (payload.status === 0 || payload.status === 3) {
|
|
55
|
+
throw new AiRuntimeError(payload.status === 0 ? 'voice_not_found' : 'voice_training_failed', typeof payload.message === 'string' ? payload.message : payload.status === 0 ? '音色不存在或尚未训练' : '音色训练失败', { retryable: false });
|
|
56
|
+
}
|
|
57
|
+
const ready = payload.status === 2 || payload.status === 4;
|
|
58
|
+
const samples = Array.isArray(payload.speaker_status) ? payload.speaker_status : [];
|
|
59
|
+
const sample = samples.find(item => isJsonObject(item) && item.model_type === 5);
|
|
60
|
+
const demo = isJsonObject(sample) && typeof sample.demo_audio === 'string' ? sample.demo_audio : '';
|
|
61
|
+
if (demo && !/^https:\/\//i.test(demo))
|
|
62
|
+
throw new AiRuntimeError('invalid_response', '音色试听地址无效');
|
|
63
|
+
return {
|
|
64
|
+
status: ready ? 'completed' : 'pending', url: ready ? demo : '', taskId: taskId(identity),
|
|
65
|
+
metadata: {
|
|
66
|
+
...payload,
|
|
67
|
+
clonedVoice: { id: identity.speaker, name: identity.name, status: ready ? 'ready' : 'training',
|
|
68
|
+
postpaid: identity.postpaid, activated: payload.status === 4,
|
|
69
|
+
...(typeof payload.create_time === 'number' ? { createdAt: payload.create_time } : {}) },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function executeVoiceClone(input) {
|
|
74
|
+
if (!isJsonObject(input.body))
|
|
75
|
+
throw new AiRuntimeError('invalid_request', '音色克隆参数无效');
|
|
76
|
+
const body = input.body;
|
|
77
|
+
if (typeof body.clone_audio !== 'string' || typeof body.voice_name !== 'string' || !body.voice_name.trim()) {
|
|
78
|
+
throw new AiRuntimeError('invalid_request', '请填写音色名称并上传声音样本');
|
|
79
|
+
}
|
|
80
|
+
const existing = typeof body.speaker_id === 'string' ? body.speaker_id.trim() : '';
|
|
81
|
+
const custom = typeof body.custom_speaker_id === 'string' ? body.custom_speaker_id.trim() : '';
|
|
82
|
+
const identity = {
|
|
83
|
+
speaker: existing || custom || `henji${volcengineRequestId(input.requestId, undefined).replaceAll('-', '')}`,
|
|
84
|
+
name: body.voice_name.trim(), postpaid: !existing,
|
|
85
|
+
};
|
|
86
|
+
const audio = parseDataUri(body.clone_audio) ?? await input.runtime.media.read(body.clone_audio);
|
|
87
|
+
if (!audio.bytes.length || audio.bytes.length > MAX_AUDIO_BYTES)
|
|
88
|
+
throw new AiRuntimeError('invalid_media', '声音样本必须非空且不超过 10 MB');
|
|
89
|
+
const formats = { 'audio/wav': 'wav', 'audio/x-wav': 'wav', 'audio/wave': 'wav', 'audio/mpeg': 'mp3', 'audio/mp3': 'mp3', 'audio/ogg': 'ogg', 'audio/mp4': 'm4a', 'audio/x-m4a': 'm4a', 'audio/aac': 'aac' };
|
|
90
|
+
const format = formats[audio.mimeType];
|
|
91
|
+
if (!format)
|
|
92
|
+
throw new AiRuntimeError('invalid_media', '请上传 WAV、MP3、OGG、M4A 或 AAC 音频');
|
|
93
|
+
const payload = {
|
|
94
|
+
...speakerBody(identity), audio: { data: toBase64(audio.bytes), format },
|
|
95
|
+
language: body.language ?? 0, extra_params: body.extra_params ?? {},
|
|
96
|
+
...(typeof body.text === 'string' && body.text.trim() ? { text: body.text } : {}),
|
|
97
|
+
};
|
|
98
|
+
// Existing slots are never overwritten blindly (activation makes training unavailable).
|
|
99
|
+
if (existing) {
|
|
100
|
+
const current = await request(input, '/api/v3/tts/get_voice', speakerBody(identity));
|
|
101
|
+
if (current.speaker_id !== existing || ![0, 1, 2, 3, 4].includes(current.status)) {
|
|
102
|
+
throw new AiRuntimeError('invalid_response', '无法确认已有音色的训练状态');
|
|
103
|
+
}
|
|
104
|
+
if (current.status === 4 || current.status === 1 || current.available_training_times === 0) {
|
|
105
|
+
throw new AiRuntimeError('invalid_request', '该音色已锁定、正在训练或没有剩余训练次数,请选择其他音色');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return parseResult(await request(input, '/api/v3/tts/voice_clone', payload), identity);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// A connection loss after sending is ambiguous. Keep a queryable identity;
|
|
113
|
+
// never send a second training request to guess whether the first succeeded.
|
|
114
|
+
if (error instanceof AiRuntimeError && error.code === 'provider_network_error' && error.details?.submissionState === 'unknown') {
|
|
115
|
+
return { status: 'pending', url: '', taskId: taskId(identity), metadata: {
|
|
116
|
+
submissionUnconfirmed: true,
|
|
117
|
+
clonedVoice: { id: identity.speaker, name: identity.name, status: 'training', postpaid: identity.postpaid, activated: false },
|
|
118
|
+
} };
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function pollVoiceClone(input) {
|
|
124
|
+
const identity = decodeTask(input.taskId);
|
|
125
|
+
return pollUntilResult(input, async () => {
|
|
126
|
+
const result = parseResult(await request(input, '/api/v3/tts/get_voice', speakerBody(identity)), identity);
|
|
127
|
+
return result.status === 'pending' ? undefined : result;
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -57,7 +57,10 @@ export interface AiRuntimeTrace {
|
|
|
57
57
|
}
|
|
58
58
|
export interface AiGenerateResponseDto {
|
|
59
59
|
status: GenerateStatus;
|
|
60
|
+
/** 兼容字段:多个输出时用 `|||` 拼接;新代码请读 `urls`。 */
|
|
60
61
|
url: string;
|
|
62
|
+
/** 全部输出地址,按供应商输出顺序(SDK 生成结果总会给出;宿主自建的 DTO 可省略)。 */
|
|
63
|
+
urls?: string[];
|
|
61
64
|
filePath?: string;
|
|
62
65
|
/** 宿主本次响应中新建的受管媒体;消费方完成转移后必须释放。 */
|
|
63
66
|
createdFilePaths?: string[];
|
|
@@ -42,6 +42,7 @@ export function inferMimeFromPath(filePath, mediaKind = 'unknown') {
|
|
|
42
42
|
case 'webm': return 'video/webm';
|
|
43
43
|
case 'mov': return 'video/quicktime';
|
|
44
44
|
case 'mp3': return 'audio/mpeg';
|
|
45
|
+
case 'aac': return 'audio/aac';
|
|
45
46
|
case 'm4a': return 'audio/mp4';
|
|
46
47
|
case 'wav': return 'audio/wav';
|
|
47
48
|
case 'flac': return 'audio/flac';
|
package/docs/consumers.md
CHANGED
|
@@ -3,7 +3,31 @@
|
|
|
3
3
|
本清单是 `@henjicc/ai-sdk` 消费方的唯一维护入口,用于 SDK 发布后的跨仓升级协调。
|
|
4
4
|
绝对路径仅描述当前开发机上的仓库位置,不进入 SDK 运行时代码、发布包或用户配置。
|
|
5
5
|
|
|
6
|
-
最后核对日期:2026-09-
|
|
6
|
+
最后核对日期:2026-09-28
|
|
7
|
+
|
|
8
|
+
同日后续修正:豆包入口改名“豆包语音 2.0”,补全 431 个官方系统音色并按系统/克隆音色选择 TTS/ICL 资源;克隆提交在宿主共享生成入口要求用户二次确认。仍为 0.9.0 未发布开发候选。下方 `9e52c4df` 的包哈希与全量门禁仅证明此前候选,不能作为本次修订包的发布证据;待用户测试并明确要求发布后重新执行适用发布门禁与打包。
|
|
9
|
+
|
|
10
|
+
本次修正定向证据:9 个测试文件共 111 项通过,SDK/渲染层类型、SDK 可移植性、能力目录、模型 i18n 与局部 lint 通过;系统音色误路由到 ICL 的临时变异被测试拦截。正式 Electron `generation-voice-clone` 场景验证了系统音色列表、双模式、克隆提交确认和取消后零请求,已目视清晰截图,场景错误/警告为零。供应商调用仅在 IPC 边界使用受控替身,没有真实付费训练或激活。
|
|
11
|
+
|
|
12
|
+
`0.9.0` 候选(发布源码 `9e52c4df`):新增 Seed-ICL 2.0 V3 声音复刻、可恢复音色查询及首次正式合成费用确认,更新火山 TTS 公开字符价。SDK 86 文件 / 1050 项测试、可移植性、构建、固定候选包仓外 Node ESM/严格 TypeScript/56 个 Vite 入口和受限宿主通过;Henji-AI 相关 11 文件 / 45 项测试、类型、能力与界面静态检查通过。训练失败校验临时撤销时定向测试失败,恢复后通过。[发布源码必需 CI 36360451658](https://github.com/henjicc/Henji-AI/actions/runs/36360451658) 全部成功,普通测试 859 文件通过,另 1 文件按显式分层跳过。尚未执行真实付费训练或激活。按用户 2026-09-28 的新要求,本版本保持已接入痕迹AI、待用户测试状态;用户测试确认没问题并明确要求发版后才发布。此前 npm 401 仅为历史鉴权记录,恢复登录也不触发自动发布,不能声称已公开安装。
|
|
13
|
+
|
|
14
|
+
- 最终候选 shasum:`f415cd32b1a650e2da550ea851fb8bb3cda06ccc`
|
|
15
|
+
- 最终候选 integrity:`sha512-Amr2o5Yjz+W/lC6ZKWDBQhObpZ9nGhwY1vFTwmQJfQThorJ5dLQN/REU7Wpj1ETrsB8FrwKt/1yeGp9OxosqHw==`
|
|
16
|
+
- Henji-AI workspace 已升级 `0.9.0`。说吧仓库经模型调用入口检查仅使用 ASR/翻译/LLM;三个示例不使用本次克隆能力,保持现有版本。Photoshop 不在本机,其已有清单只登记图片模型,未声称本轮实际验证或升级。
|
|
17
|
+
- 迁移约束:非官方系统音色且非 `S_` 的后付费克隆音色正式合成需 `volcIclActivationConsent: true`;克隆成功可以没有试听 URL,使用 `metadata.clonedVoice` 保存音色,不能把它误报为音频生成失败。
|
|
18
|
+
- 首发界面:正式 `test:reality --suite ui --only generation-voice-clone --size 1440x900 --build` 通过,实际 Electron 完成模型选择、模式切换、录音上传和更多设置开关,截图已目视检查;应用错误、警告和浏览器错误均为零。仅使用隔离资料与合成录音夹具,未提交生成。
|
|
19
|
+
|
|
20
|
+
`0.8.0` 已发布:TTS 音色参数支持选择器,SDK 新增百炼 MiniMax / Qwen Audio / CosyVoice 与硅基流动 CosyVoice2 的账号音色查询。发布源码 `add89fea` 的必需 CI [36329516045](https://github.com/henjicc/Henji-AI/actions/runs/36329516045) 全部成功;SDK 85 文件 / 1036 项测试、可移植性、构建、56 个仓外 Vite 入口、受限宿主、Henji-AI Electron bundle 与宿主精确测试通过。MiniMax 错误码校验临时撤销后定向测试失败,恢复后通过;未执行付费生成。公共 npm 版本经隔离配置、缓存和令牌的仓外匿名安装,确认公开 `listTtsVoices` 与 registry integrity。Henji-AI 使用仓内 workspace `0.8.0`;现有外部消费者与三个示例未使用本次 TTS 音色查询,不机械升级。
|
|
21
|
+
|
|
22
|
+
- `0.8.0` tarball:[公共 npm 包](https://registry.npmjs.org/@henjicc/ai-sdk/-/ai-sdk-0.8.0.tgz)
|
|
23
|
+
- `0.8.0` shasum:`3361d472c10b7f41c40e26eb2d2c7a1965c0c9f2`
|
|
24
|
+
- `0.8.0` integrity:`sha512-fruZHU2jwnMlo1tpQMR44G+YpIODTJVevB4BN9aup+Y/vlFHDANnBqIliUbSBvWASnSxM1h2AfIRXIlWtOxGFw==`
|
|
25
|
+
|
|
26
|
+
`0.7.0` 已发布:新增 Fal、百炼、KIE、硅基流动和火山语音共 13 个 TTS 模型。发布源码 `9785e964` 的必需 CI [36316096650](https://github.com/henjicc/Henji-AI/actions/runs/36316096650) 全部成功;SDK 84 文件 / 1032 项测试、可移植性、打包、受限宿主、仓外 Vite 56 个入口与 Henji-AI Electron bundle / 宿主集成测试通过。火山截断帧校验撤销后定向测试失败、恢复后通过。固定候选包发布后,公共 npm 匿名安装和新供应商 pack 导入通过;未执行付费语音生成。Henji-AI 使用仓内 workspace `0.7.0`;现有外部消费者和三个示例未使用新增 TTS,不机械升级。
|
|
27
|
+
|
|
28
|
+
- `0.7.0` tarball:[公共 npm 包](https://registry.npmjs.org/@henjicc/ai-sdk/-/ai-sdk-0.7.0.tgz)
|
|
29
|
+
- `0.7.0` shasum:`d1a8e84d16ef4a02cf81d1318e7af9a3d1debdd0`
|
|
30
|
+
- `0.7.0` integrity:`sha512-IP+0VChzt2XjNZjiG0/wtXkHWZOxciZcGA70q7wMhp7T9nw//OGlbnLSl6hvy1I3FrSZOl6qLXcgZtwriQWnrA==`
|
|
7
31
|
|
|
8
32
|
`0.6.2` 已发布:聊天/翻译流终态、百炼/火山实时 ASR、共享会话终止与脱敏诊断已完成修复及撤销修复后的失败验证。SDK 83 文件 / 1023 项测试、可移植性、构建、49 个公开入口、严格类型和受限宿主门禁通过;Henji-AI 8 项相关宿主测试、主进程类型检查及 Electron bundle 通过。发布源码 `9e836def` 的必需 CI [35931001436](https://github.com/henjicc/Henji-AI/actions/runs/35931001436) 全部成功。固定候选包在仓外回装后发布,用户完成 npm 网页认证;待公共分发可读后,隔离配置、缓存与令牌的匿名重新安装通过,正式 tarball 和 lockfile integrity 与候选一致。Node ESM、严格 TypeScript、Vite 49 个公开入口、无 TextEncoder/TextDecoder 的受限宿主通过;llm-chat 类型构建、dry-run、bundle 边界及包内合法空事件/提前结束/后台故障与单次释放探针通过。本轮未执行真实付费模型请求。Henji-AI workspace 和 llm-chat 示例使用 `0.6.2`,minimal-node/form-renderer 保持 `0.6.0`。说吧仍为 `0.6.1`,本次未修改外部仓库,需在消费项目另行升级和验证;Photoshop 未在本机验证。
|
|
9
33
|
|
|
@@ -32,7 +56,7 @@
|
|
|
32
56
|
|
|
33
57
|
Henji-AI 安装包的 GPT Image 2.5 高分辨率报错归属:宿主智能比例预处理曾忽略联动过滤,将 `smart` 转成 KIE 仅限 1K 的 `27:16`,SDK 在请求前正确拒绝。修复位于应用公共预处理,按当前分辨率/渠道的合法选项匹配;Flare/Sunburst 的 1K/2K/4K 及 APIMart、Grsai 同类筛选已有定向覆盖。无需放宽 SDK 契约或发布 SDK;安装包需随应用更新才包含修复,现有版本可手动选合法比例规避。
|
|
34
58
|
|
|
35
|
-
|
|
59
|
+
当前仓内 SDK:`0.9.0` 候选;公共 npm 最新已核实版本为 `0.8.0`(2026-09-27 已发布并完成公共匿名回装)。以下为历史发布证据。
|
|
36
60
|
|
|
37
61
|
`0.5.2` 完善轻量 `@henjicc/ai-sdk/llm/streaming`:正式类型支持文本、JSON Object 与 JSON Schema,Chat Completions 的最终请求体写入 `response_format`,OpenAI 输出上限写入 `max_completion_tokens`;结构化输出、思考模式和模型能力的非法组合会在请求前返回结构化错误。流式正文与思考继续分离,保留外部 `AbortSignal`,不设置默认总时限;未显式配置输出上限时不再注入 4096,服务端以 `length` 等原因结束时返回 `truncated: true`。发布前 77 个测试文件 / 909 项测试、可移植性、构建、49 个 Vite 公开入口和受限宿主门禁通过;故意撤销结构化参数、输出上限、OpenAI 字段映射和截断标记后,8 项定向断言失败,恢复后通过。公共索引可读后,已使用隔离 npm 配置和缓存匿名安装精确版本,并从已发布的轻量入口验证最终请求体包含 JSON Schema、思考强度和 24,000 token 上限,SSE 正文/思考事件分离且未截断。
|
|
38
62
|
|
|
@@ -94,7 +118,7 @@ Henji-AI workspace 与三个仓内示例 manifest 均锁定 `0.4.0`;下表原
|
|
|
94
118
|
|
|
95
119
|
| 仓库 / 开发路径 | 宿主类型 | 当前精确版本 | SDK 入口与构建方式 | 凭据 / transport 责任 | 需同步的变更类型 | 验证命令 | 同步证据与边界 | 最后核对 |
|
|
96
120
|
|---|---|---|---|---|---|---|---|---|
|
|
97
|
-
| `Henji-AI`<br>`D:/VibeCode/Henji-AI` | Electron 42 主进程 + React/Vite;SDK 主开发、首发验证宿主 | workspace
|
|
121
|
+
| `Henji-AI`<br>`D:/VibeCode/Henji-AI` | Electron 42 主进程 + React/Vite;SDK 主开发、首发验证宿主 | workspace `0.9.0` 候选;公共 npm `0.8.0` | 包根、`provider-packs/*`、`tool-packs/*`;根构建先执行 `build:sdk`,再构建 Electron | Electron 主进程注入 HTTP transport、凭据、媒体读取、日志、trace、取消与落盘;渲染层不直接持有密钥 | 公共类型/目录、provider preset、凭据坐标、transport、媒体、包导出、LLM/生成执行协议与音色查询 | SDK 发布门禁、相关宿主 Vitest、渲染层/主进程类型检查与模型 i18n | 本轮 1050 项 SDK、45 项宿主、类型与 Electron bundle 通过;固定候选包及克隆公开入口回装通过;新版本已通过功能提交 CI,公共发布待用户测试与明确发版指令 | 2026-09-28 |
|
|
98
122
|
| `henji-ai-ps`<br>`/Users/henji/Documents/VibeCode/henji-ai-ps` | Photoshop UXP 插件 + React/Vite IIFE(pnpm) | manifest/lock 均精确锁定公共 npm `0.2.8`;lock integrity 与上方正式产物一致 | `generation/core`、单模型/供应商 pack、LLM streaming;Vite 构建与 UXP smoke bundle | UXP 宿主注入受限 `fetch`、provider 凭据、媒体编码读取和脱敏日志;SDK 不读取 Node/文件系统 | 生成 pack/exports、受限环境可移植性、RuntimeContext、凭据 scope、媒体与流式 LLM;不因版本同步自动引入 GLM | `pnpm typecheck:uxp-smoke && pnpm check:uxp-sdk && pnpm smoke:uxp:build && pnpm check:uxp-smoke`;完整 `pnpm check` | `e509716`;无用户 npm 凭据的 frozen lock 安装通过;Node 22 下四项门禁通过,正式产品仍仅 39 个 generation/erase packs,网络调用与受限环境风险均为 0 | 2026-08-31 |
|
|
99
123
|
| `say-it`<br>`D:/VibeCode/说吧` | Tauri 2 + Rust 管理 QuickJS;WebView 不运行 SDK | manifest/lock 精确锁定公共 npm `0.6.1`;bundle 版本、resolved、shasum、integrity 一致 | 按需打包 capability、Bailian/火山实时 ASR、SiliconFlow/Groq 文件 ASR、translation、LLM modules 为相互隔离 IIFE;Rust 加载 bundle | Rust Host API 注入 HTTP 字节流、WS、media-ref、CredentialStore、日志/trace、Abort/timeout/cancel;QuickJS/插件/WebView 不直取密钥 | capability/LLM 协议、按需 exports、descriptor source/坐标、QuickJS 可移植性、bundle 隔离;不因版本同步自动增加未采用模型 | `npm run sdk-runtime:typecheck && npm run sdk-runtime:build`;`npm run test:ui`;Rust 定向/全量测试;`npm run ui:build` | `18bad6d`;QuickJS/WS 空中间帧回归及 35 项 SDK 宿主本地测试通过,runtime 类型检查通过;听写/历史/UI 定向回归与前端类型构建通过,桌面开发应用已重启,实际麦克风交互待用户验证 | 2026-09-24 |
|
|
100
124
|
|
|
@@ -108,6 +108,7 @@ Google 官方 Gemini Omni Flash 全能多模态视频生成模型。支持文生
|
|
|
108
108
|
|
|
109
109
|
- 本项目默认**绝对不显示**:`seed`、负面提示词。APIMart 这两条渠道都没有这两个字段。
|
|
110
110
|
- **两条渠道差异极大**:官方渠道只有 720p、时长 3–10 秒、参考图最多 16 张、支持 `extend_from_task_id` 多轮编辑;Ext 渠道有 1080p/4k、时长只能 4/6/8/10、参考图只能 0/1/3 张、没有多轮编辑。产品入口可以合并,但渠道选择必须驱动参数显隐、校验、请求、轮询与计价,不能把两套契约混用。
|
|
111
|
+
- `extend_from_task_id`(多轮延续)需要填供应商侧的上一次任务 ID,本应用界面不展示任务 ID、用户拿不到:当前**不注册、不下发**(界面计划 5.8,P-17)。以后要支持,应做成生成记录上的“继续编辑”动作,由应用自己带上任务 ID,而不是参数输入框。
|
|
111
112
|
- Ext 渠道**不支持 2 张图的首尾帧**——这是与几乎所有其他视频模型都不同的限制,UI 必须挡住。
|
|
112
113
|
- Ext 渠道传参考视频时**不能同时传 `duration`**。
|
|
113
114
|
- 官方渠道传 `video_urls` 后 `aspect_ratio` 失效。
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
|
|
111
111
|
- 本项目默认**绝对不显示**:`seed`、负面提示词。KIE 的 `gemini-omni-video` **有 `seed`**,必须主动不注册、不下发。
|
|
112
112
|
- **这是一套三端点体系**,语音与角色是可复用资产(先创建拿 ID,再在视频里引用),需要独立的资产管理交互,不能塞进单次生成的参数面板。
|
|
113
|
+
- 当前状态(界面计划 5.8,P-17):应用还没有语音 / 角色资产的创建与管理入口,用户拿不到 `audio_ids` / `character_ids`,所以这两个字段**不注册、不下发**;参考图上限按“无视频 7 张、有视频 5 张”校验。
|
|
113
114
|
- **image slot 是共享额度**(图片 1 / 角色 1 / 视频 2,总计 7),前端计数必须统一。
|
|
114
115
|
- `duration` 是**字符串**且只有 4/6/8/10;**有视频输入时该参数静默失效**。
|
|
115
116
|
- **按次计费**:720p 与 1080p 同价,4k 更贵;有视频输入时价格固定且明显更高。
|
|
@@ -5,22 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
| 项目 | 内容 |
|
|
7
7
|
|---|---|
|
|
8
|
-
| 最后更新 | 2026-09-
|
|
8
|
+
| 最后更新 | 2026-09-28 |
|
|
9
9
|
| 模型数量 | 生成主清单 20(图片 11 / 视频 9)+ 主清单外存量 12 + SDK 跨项目能力模型 20(ASR 15 / 翻译 3 / LLM 2) |
|
|
10
10
|
| 模型供应商文档数量 | 生成/存量 84 + SDK 跨项目能力 20(另有 Fal 工具 12) |
|
|
11
11
|
| 覆盖供应商 | 火山引擎(官方生成/ASR)、百炼(官方)、硅基流动(官方 ASR)、智谱(官方 LLM)、Groq(官方 LLM/ASR)、APIMart、KIE、Fal、派欧云、魔搭、Grsai |
|
|
12
12
|
|
|
13
|
-
另有 12 个按能力按需分发的 Fal 图像工具,不进入默认 124 模型兼容目录:
|
|
13
|
+
另有 12 个按能力按需分发的 Fal 图像工具,不进入默认 124 模型兼容目录:
|
|
14
14
|
|
|
15
15
|
- 消除:[Flux Pro Erase](Flux-Pro-Erase/Flux-Pro-Erase_Fal.md)、[Bria Eraser](Bria-Eraser/Bria-Eraser_Fal.md)、[Finegrain Eraser](Finegrain-Eraser/Finegrain-Eraser_Fal.md)。
|
|
16
16
|
- 图片实用工具:[Image Apps v2 重打光](Image-Apps-v2-重打光/Image-Apps-v2-重打光_Fal.md)、[ControlLight](ControlLight/ControlLight_Fal.md)、[Image Apps v2 扩图](Image-Apps-v2-扩图/Image-Apps-v2-扩图_Fal.md)、[Image Apps v2 商品摄影](Image-Apps-v2-商品摄影/Image-Apps-v2-商品摄影_Fal.md)、[Image Apps v2 照片修复](Image-Apps-v2-照片修复/Image-Apps-v2-照片修复_Fal.md)、[Pixelcut 背景移除](Pixelcut-背景移除/Pixelcut-背景移除_Fal.md)。
|
|
17
17
|
- 多角度:[Qwen Image Edit 2511 多角度](Qwen-Image-Edit-2511-多角度/Qwen-Image-Edit-2511-多角度_Fal.md)、[透视变换](透视变换/透视变换_Fal.md)、[FLUX 2 多角度](FLUX-2-多角度/FLUX-2-多角度_Fal.md)。
|
|
18
18
|
|
|
19
|
-
工具均复用 Fal 队列、上传与结果解析;它们是独立的按需 SDK 入口,不是新的执行内核。Henji-AI 运行客户端显式装载三个 pack,但普通模型选择器为 124 个默认模型:六个图片实用工具通过图片能力菜单创建固定模型节点,三个多角度工具由专用多角度节点选择,消除工具保留给后续遮罩能力接入。真实质量、延迟和账单仍待获得付费授权后验证。
|
|
20
|
-
|
|
21
|
-
## 2026-09-27 TTS 适配
|
|
22
|
-
|
|
23
|
-
[
|
|
19
|
+
工具均复用 Fal 队列、上传与结果解析;它们是独立的按需 SDK 入口,不是新的执行内核。Henji-AI 运行客户端显式装载三个 pack,但普通模型选择器为 124 个默认模型:六个图片实用工具通过图片能力菜单创建固定模型节点,三个多角度工具由专用多角度节点选择,消除工具保留给后续遮罩能力接入。真实质量、延迟和账单仍待获得付费授权后验证。
|
|
20
|
+
|
|
21
|
+
## 2026-09-27 TTS 适配
|
|
22
|
+
|
|
23
|
+
2026-09-28:[豆包语音 2.0](Seed-ICL-2.0/Seed-ICL-2.0_火山引擎.md) 提供语音合成 / 克隆声音两模式,内置官方 431 个系统音色,并按系统/克隆音色选择 TTS/ICL 资源。接入官方 V3 训练与查询、自动后付费音色编号、克隆提交费用二次确认、试听和首次合成费用确认;Seed-TTS / ICL 2.0 公开价格更新为 3 元/万字符。
|
|
24
|
+
|
|
25
|
+
[TTS 多供应商适配契约](TTS-多供应商-2026-09.md):新增 13 个文字转语音模型,覆盖 Fal、百炼、KIE、硅基流动与火山语音;派欧云 MiniMax Speech 2.8 继续使用已有接入。记录官方接口、价格、音色前提、流式响应和测试样本来源。
|
|
26
|
+
|
|
27
|
+
2026-09-27 补充官方音色目录及账号查询接口:Qwen Audio 3.1、Seed-TTS 2.0 使用核对后的预置清单;百炼 MiniMax / Qwen / CosyVoice 与硅基流动 CosyVoice2 支持按账号查询可用音色。接口字段、适用模型和不可查询渠道的边界见上方契约。
|
|
24
28
|
|
|
25
29
|
## 2026-09-24 已适配更新
|
|
26
30
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Seed-ICL 2.0 · 火山引擎
|
|
2
|
+
|
|
3
|
+
最后更新:2026-09-28。官方文档公开可读;控制台、实际 API 需要账号与权限。
|
|
4
|
+
|
|
5
|
+
## 产品与接入
|
|
6
|
+
|
|
7
|
+
同一 Seed-ICL 2.0 模型提供“语音合成”和“克隆声音”模式;已有模型 ID 与语音密钥槽保持兼容。界面按火山引擎品牌归组,方舟和豆包语音分别配置凭据。
|
|
8
|
+
|
|
9
|
+
新接入采用 V3:`POST https://openspeech.bytedance.com/api/v3/tts/voice_clone` 训练,`POST /api/v3/tts/get_voice` 查询,`POST /api/v3/tts/unidirectional` 合成。训练、查询使用 `X-Api-Key`、UUID `X-Api-Request-Id` 和 JSON Content-Type;合成还带 `X-Api-Resource-Id: seed-icl-2.0`。旧 V1 训练不再迭代,不复用旧协议。
|
|
10
|
+
|
|
11
|
+
## 输入
|
|
12
|
+
|
|
13
|
+
- 预付费音色传已有 `speaker_id`;后付费传 `speaker_id: custom_speaker_id` 和唯一的 `custom_speaker_id`。应用自动生成后者,无需手填。
|
|
14
|
+
- 自定义代号长 8–256,仅字母、数字、横线、下划线;字母开头,不以横线/下划线结尾,不使用官方保留前后缀。SDK 自动值使用 `henji` 加 UUID。
|
|
15
|
+
- `audio.data` 为 Base64 原始字节,单文件不超过 10 MB;支持 wav/mp3/ogg/m4a/aac/pcm,PCM 仅 24k 单声道。应用上传复用受管文件,SDK 通过媒体读取器取字节,直接发官方 JSON 接口,不要求公网 URL。应用优先接受可识别容器格式,不接受没有采样元信息的裸 PCM。
|
|
16
|
+
- `audio.format` 总是显式填写(文档字段标必填,正文又说部分格式可省略,显式传递满足两者)。可选 `text` 是录音原文。
|
|
17
|
+
- `language` 数字枚举 0–17、19–21;默认 0 中文。
|
|
18
|
+
- `extra_params.demo_text` 为试听文本,4–300 字,语言应匹配录音;`enable_audio_denoise` 默认 false;`disable_volume_normalization` 默认 false。
|
|
19
|
+
- 合成用真实音色代号作为 `req_params.speaker`,`model: seed-tts-2.0-standard`。
|
|
20
|
+
|
|
21
|
+
## 状态、错误与恢复
|
|
22
|
+
|
|
23
|
+
| 状态 | 响应约束 | 行为 | 终态 |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| 0 NotFound | `speaker_id` 字符串、`status` 整数 | 未训练/找不到音色,明确报错 | 是 |
|
|
26
|
+
| 1 Training | 同上 | 保留任务身份,只查询同一个音色 | 否 |
|
|
27
|
+
| 2 Success | 同上;`speaker_status[]` 中 `model_type=5` 对应复刻 2.0,`demo_audio` 为可选试听 URL | 音色可用,保存到音色库;有试听则返回试听 | 是 |
|
|
28
|
+
| 3 Failed | 同上,可选 message | 明确训练失败 | 是 |
|
|
29
|
+
| 4 Active | 同上 | 音色可合成,已激活不可再训练 | 是 |
|
|
30
|
+
| HTTP 失败 / 非成功 code | 可带 `code/message` | 报供应商错误,不把错误当成空音色或成功 | 是 |
|
|
31
|
+
| 缺失/非法 speaker_id 或 status | 不符合字段表 | 拒绝响应 | 是 |
|
|
32
|
+
|
|
33
|
+
官方字面成功响应没有 `code`,不能强制要求 code 存在。成功必须同时具备有效音色与合法状态;不因为 HTTP 200 就判成功。`available_training_times/create_time/language` 可选;试听 URL 有效一小时,不作为永久音色身份。
|
|
34
|
+
|
|
35
|
+
训练没有公开幂等保证,不自动重放;取消只终止本地等待,不能声称撤销远端训练。任务身份包含音色代号和名称,继续/恢复只调用查询接口。训练响应为处理中时返回 pending;提交后网络结果不确定时保留可查询身份并标记 submissionUnconfirmed,不假定已训练成功。查询复用现有轮询,正常处理中持续等待,取消或明确终态结束,连续查询失败最多 20 次。未知状态、空响应不会当成成功。官方未说明乱序、重复、远端取消、连接复用保证。普通 HTTP 由 transport 管理,响应完整消费,轮询计时器在取消时释放。
|
|
36
|
+
|
|
37
|
+
## 费用
|
|
38
|
+
|
|
39
|
+
公开刊例:Seed-TTS 2.0 / Seed-ICL 2.0 后付费合成均为 **3 元/万字符**;资源包另有折扣。后付费克隆音色 **138 元/音色**,首次正式语音合成时收费并固定音色;训练试听按复刻 2.0 字符费收取,七天未正式调用会删除。预付费槽位按购买数量分档,基础档 138 元/音色,每槽位可训练 15 次。客户端不自动购买或激活槽位。训练与正式合成必须分别显示其费用后果,不把训练试听当成免费,不把槽位费用隐藏在字符估价里。
|
|
40
|
+
|
|
41
|
+
## 音色列表
|
|
42
|
+
|
|
43
|
+
2026-09-28 复核:产品入口命名为“豆包语音 2.0”,语音合成模式同时提供系统音色和本地克隆音色。官方在线音色表的两个“豆包语音合成模型2.0”章节共 431 个去重音色(包含 `ICL_uranus_*` 命名的官方系统音色);不采集后续 S2S 专用与 1.0 章节。旧清单 230 个漏掉了大小写不同的 ID,本次补齐。系统音色必须使用 `seed-tts-2.0` 资源;用户克隆音色使用 `seed-icl-2.0`,不能根据 ID 是否以 ICL 开头推断为收费克隆音色。
|
|
44
|
+
|
|
45
|
+
官方确有控制面 `ListSpeakers`(2025-05-20)接口,`ResourceIDs: ["seed-tts-2.0"]`、Page、Limit;响应 Speakers 包含 VoiceType、Name 等。官方示例要求 `Authorization: HMAC-SHA256` AK/SK 签名,并非当前配置的语音 `X-Api-Key`。因此本次使用官方文档内置列表,不要求用户为音色选择额外配置控制面凭据。
|
|
46
|
+
|
|
47
|
+
克隆提交前宿主必须弹出费用二次确认:训练试听 3 元/万字符,新建后付费音色首次正式合成另收 138 元且锁定,7 天未正式使用会删除;使用已有预付费音色会覆盖旧效果并消耗训练次数。取消、关闭或任务中止均不上传样本、不发训练请求;确认仅对本次提交有效。
|
|
48
|
+
|
|
49
|
+
新版按 ID 查询不提供目录。控制面分页接口 `BatchListMegaTTSTrainStatus`(2025-05-21)要求 ProjectName,官方示例使用 HMAC AK/SK;本次不假定语音 Key 能代替控制面签名。已有槽位可导入一次,应用自动保存之后无需重复输入;新建后付费音色自动产生 ID。本地记录与语音密钥的 provider 隔离,不能混入方舟或派欧云音色。
|
|
50
|
+
|
|
51
|
+
## Fixture
|
|
52
|
+
|
|
53
|
+
`tests/fixtures/volcengine-speech/voice-clone.json` 保存训练/查询页面嵌入 API 示例的原样成功响应(仅替换 ID/URL 为不可联网值),以及字段表构造的各状态。负例由测试单独构造。验收覆盖无 code 的官方响应、训练→成功、错误、缺失字段、超限音频、取消与不重放。
|
|
54
|
+
|
|
55
|
+
## 原始链接
|
|
56
|
+
|
|
57
|
+
- [系统音色表](https://docs.volcengine.com/docs/DoubaoVoice/Tonelist-1?lang=zh):2.0 中文与外语章节;公开。
|
|
58
|
+
- [ListSpeakers](https://www.volcengine.com/docs/6561/2160690):系统音色分页、资源筛选、HMAC 签名示例;公开。
|
|
59
|
+
- [V3 训练](https://docs.volcengine.com/docs/DoubaoVoice/tone-training-http?lang=zh):请求、状态、后付费 ID 规则;公开。
|
|
60
|
+
- [V3 查询](https://docs.volcengine.com/docs/DoubaoVoice/tone-query-http?lang=zh):状态、试听有效期;公开。
|
|
61
|
+
- [合成](https://docs.volcengine.com/docs/DoubaoVoice/unidirectional-streaming-text-to-speech-http?lang=zh):合成请求;公开。
|
|
62
|
+
- [购买与使用](https://docs.volcengine.com/docs/DoubaoVoice/Soundreplicationorderingandusageguide?lang=zh):激活、训练次数、有效期;公开。
|
|
63
|
+
- [价格](https://docs.volcengine.com/docs/DoubaoVoice/Billinginstructions-21?lang=zh):正式后付费、槽位与试听计费;公开。
|
|
64
|
+
- [分页音色查询](https://www.volcengine.com/docs/6561/1801953):控制面签名、项目与分页;公开。
|
|
65
|
+
- [错误](https://www.volcengine.com/docs/6561/2534853):供应商错误码;公开。
|