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>
This commit is contained in:
172
.claude/skills/video-from-script/scripts/minimax-voice-clone.js
Normal file
172
.claude/skills/video-from-script/scripts/minimax-voice-clone.js
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/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) })
|
||||
Reference in New Issue
Block a user