tiny-army / web /ttsVoxcpm.js
polats's picture
Add VoxCPM reference audio cloning
8000b92
// TTS engine: VoxCPM2 via a ZeroGPU sidecar, proxied through /voxcpm-tts so
// tokens and sidecar details stay server-side. Like Qwen3, this designs a voice
// from each hero's free-form voice description.
import { decodeAudio } from '/web/ttsAudio.js'
let _desc = ''
const VOICES = [
{ id: 'persona', label: '✨ Persona voice (designed)', desc: () => _desc },
{ id: 'veteran', label: 'Gruff veteran', desc: () => 'A gravelly, battle-worn male baritone - slow, deliberate, weary, with a wry edge.' },
{ id: 'herald', label: 'Bright herald', desc: () => 'A clear, bright young male voice - brisk, energetic, projecting and confident.' },
{ id: 'medic', label: 'Steady medic', desc: () => 'A calm, warm female voice - measured pace, clear articulation, reassuring.' },
{ id: 'rogue', label: 'Sly rogue', desc: () => 'A low, smooth voice with a sly, amused lilt - unhurried, with a dangerous edge.' },
]
const get = (id) => VOICES.find((v) => v.id === id) || VOICES[0]
async function postSynthWav(text, voiceId) {
const instruct = (get(voiceId).desc() || '').trim()
const resp = await fetch('/voxcpm-tts', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, instruct, language: 'English' }),
})
if (!resp.ok) throw new Error(`VoxCPM ${resp.status}: ${(await resp.text()).slice(0, 140)}`)
return resp.arrayBuffer()
}
const postSynth = async (text, voiceId) => decodeAudio(await postSynthWav(text, voiceId))
function abToB64(ab) {
let s = ''; const u = new Uint8Array(ab); const C = 0x8000
for (let i = 0; i < u.length; i += C) s += String.fromCharCode.apply(null, u.subarray(i, i + C))
return btoa(s)
}
async function postCloneWav(text, refAb, refText, instruct) {
const resp = await fetch('/voxcpm-clone', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, ref_audio: abToB64(refAb), ref_text: refText || '', instruct: instruct || '', language: 'English' }),
})
if (!resp.ok) throw new Error(`VoxCPM clone ${resp.status}: ${(await resp.text()).slice(0, 140)}`)
return resp.arrayBuffer()
}
export const engine = {
mode: 'pcm', needsDownload: false, networked: true, design: true,
id: 'voxcpm',
label: 'VoxCPM2 · Voice Design (ZeroGPU)',
experimental: true,
available: () => true,
listVoices: () => VOICES,
defaultVoice: 'persona',
ensure: async () => {},
setDesc(d) { _desc = (d || '').trim() },
synth: (text, voiceId) => postSynth(text, voiceId),
synthWav: (text, voiceId) => postSynthWav(text, voiceId),
cloneWav: (text, refAb, refText, instruct) => postCloneWav(text, refAb, refText, instruct),
backendLabel: () => 'ZeroGPU VoxCPM2',
}