Files
video-create/.claude/skills/video-from-script/scripts/minimax-voice-clone.js
lc 1e110219ff feat: MiniMax TTS集成、口播批量剪辑流水线、执黑先行二号账号
- 新增 minimax-tts.js 和 minimax-voice-clone.js 脚本
- 新增口播批量剪辑流水线 (口播_assemble.js, 口播_pipeline.js)
- 更新 video-from-script 各阶段脚本 (kling, images, tts, videos)
- 新增执黑先行二号-风格延伸账号
- 更新执黑先行 account.json 配置
- 替换 ugc_product_seeding 参考图
- 更新 CLAUDE.md 和依赖配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 20:05:56 +08:00

173 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* MiniMax 音色克隆
*
* 用法:
* node minimax-voice-clone.js --file /path/to/audio.mp3 --voice-id my_voice_name
*
* 流程:
* 1. 上传音频文件 → 获取 file_id
* 2. 调用 voice_clone → 注册 voice_id
* 3. 输出 voice_id写入 config.json 的 minimaxVoices
*
* 选项:
* --file <path> 待克隆的音频文件MP3/WAV/M4A≥10s
* --voice-id <name> 自定义音色 ID字母数字下划线如 "snape_mm"
* --name <name> 音色中文名(写入 config默认同 voice-id
* --model <model> 模型(默认 speech-02-hd
* --save 自动写入 config.json 的 minimaxVoices
*/
const https = require('https')
const fs = require('fs')
const path = require('path')
const CONFIG_PATH = path.join(__dirname, '..', '..', 'config.json')
const BOUNDARY = `----FormBoundary${Date.now().toString(16)}`
function loadConfig() {
if (!fs.existsSync(CONFIG_PATH)) throw new Error(`config.json 不存在: ${CONFIG_PATH}`)
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
}
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
const chunks = []
res.on('data', c => chunks.push(c))
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')))
} catch (e) {
reject(new Error(`响应解析失败: ${e.message}`))
}
})
})
req.on('error', reject)
if (body) req.write(body)
req.end()
})
}
async function uploadFile(apiKey, filePath, baseUrl, purpose = 'voice_clone') {
const fileBuffer = fs.readFileSync(filePath)
const fileName = path.basename(filePath)
const bodyParts = [
`--${BOUNDARY}\r\nContent-Disposition: form-data; name="purpose"\r\n\r\n${purpose}`,
`--${BOUNDARY}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`,
]
const body = Buffer.concat([
Buffer.from(bodyParts[0] + '\r\n'),
Buffer.from(bodyParts[1]),
fileBuffer,
Buffer.from(`\r\n--${BOUNDARY}--\r\n`),
])
const url = new URL(`${baseUrl}/v1/files`)
const result = await httpsRequest({
hostname: url.hostname,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': `multipart/form-data; boundary=${BOUNDARY}`,
'Content-Length': body.length,
},
}, body)
if (result.base_resp?.status_code !== 0) {
throw new Error(`上传失败 [${result.base_resp?.status_code}]: ${result.base_resp?.status_msg}`)
}
return result.file.file_id
}
async function cloneVoice(apiKey, fileId, voiceId, model, baseUrl) {
const body = JSON.stringify({
file_id: fileId,
voice_id: voiceId,
text: '这是一段用于验证音色克隆效果的测试文本,请仔细聆听音色特征。',
model,
need_noise_reduction: true,
need_volume_normalization: true,
})
const url = new URL(`${baseUrl}/v1/voice_clone`)
const result = await httpsRequest({
hostname: url.hostname,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
}, body)
if (result.base_resp?.status_code !== 0) {
throw new Error(`克隆失败 [${result.base_resp?.status_code}]: ${result.base_resp?.status_msg}`)
}
return result
}
function parseArgs(argv) {
const args = {}
for (let i = 0; i < argv.length; i++) {
if (argv[i].startsWith('--')) {
const key = argv[i].slice(2)
args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true
}
}
return args
}
async function main() {
const args = parseArgs(process.argv.slice(2))
if (!args.file || !args['voice-id']) {
console.error('用法: node minimax-voice-clone.js --file <音频文件> --voice-id <自定义ID> [--name <中文名>] [--save]')
console.error('示例: node minimax-voice-clone.js --file snape.mp3 --voice-id snape_mm --name 斯内普MM --save')
process.exit(1)
}
const config = loadConfig()
const apiKey = config.minimaxApiKey
if (!apiKey) { console.error('config.json 中未配置 minimaxApiKey'); process.exit(1) }
const filePath = path.resolve(args.file)
if (!fs.existsSync(filePath)) { console.error(`文件不存在: ${filePath}`); process.exit(1) }
const voiceId = args['voice-id']
const voiceName = args.name || voiceId
const model = args.model || config.minimaxTtsModel || 'speech-2.8-hd'
const baseUrl = config.minimaxApiBase || 'https://api.yunwu.ai/minimax'
console.log(`\n[1/2] 上传音频文件: ${path.basename(filePath)}`)
const fileId = await uploadFile(apiKey, filePath, baseUrl)
console.log(` file_id: ${fileId}`)
console.log(`[2/2] 克隆音色: ${voiceId} (模型: ${model})`)
await cloneVoice(apiKey, fileId, voiceId, model, baseUrl)
console.log(` 克隆成功!`)
console.log(`\n✅ voice_id: ${voiceId}`)
if (args.save) {
const voices = config.minimaxVoices || {}
voices[voiceName] = voiceId
config.minimaxVoices = voices
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
console.log(` 已写入 config.json: minimaxVoices["${voiceName}"] = "${voiceId}"`)
} else {
console.log(`\n 手动写入 config.json:`)
console.log(` "minimaxVoices": { "${voiceName}": "${voiceId}" }`)
console.log(`\n 或运行时加 --save 参数自动写入`)
}
}
main().catch(err => { console.error('\n❌ 失败:', err.message); process.exit(1) })