Qwen3-TTS-12Hz-1.7B-VoiceDesign与Vue3前端集成指南
Qwen3-TTS-12Hz-1.7B-VoiceDesign与Vue3前端集成指南
你是不是也想过,在自己的网页应用里加入一个能“开口说话”的AI助手?不是那种机械的电子音,而是可以根据你的描述,创造出各种生动声音的智能语音。比如让一个虚拟主播用“温柔知性的女声”介绍产品,或者让游戏角色用“粗犷豪迈的男声”念出台词。
以前要实现这种效果,要么得用昂贵的商业API,要么就得自己搭建复杂的后端服务。但现在不一样了,有了Qwen3-TTS-12Hz-1.7B-VoiceDesign这个开源模型,我们完全可以在前端项目里直接集成语音生成功能。
今天我就带你一步步实现这个功能。我会用Vue3作为前端框架,从零开始搭建一个完整的语音生成应用。你不需要是AI专家,只要会一点JavaScript和Vue3的基础知识,就能跟着做出来。
1. 准备工作:理解我们要做什么
在开始写代码之前,我们先搞清楚几个关键点。
Qwen3-TTS-12Hz-1.7B-VoiceDesign这个模型有个很酷的特点:它不需要你提供任何录音样本,只需要用文字描述你想要的声音,它就能“创造”出来。比如你说“年轻活泼的女声,语速快,音调上扬”,它就能生成符合这个描述的声音。
但是这里有个实际问题:这个模型本身是用Python写的,而且需要GPU才能跑得快。我们不可能让用户的浏览器直接运行这个模型。所以我们需要一个中间层——一个后端服务来负责实际的语音生成,前端只负责发送请求和播放结果。
我们的架构是这样的:
- 前端(Vue3应用):用户输入文字和声音描述,发送请求,播放生成的语音
- 后端(Python服务):接收请求,调用Qwen3-TTS模型生成语音,返回音频文件
- 两者通过HTTP API通信
听起来有点复杂?别担心,我会把每一步都拆解得清清楚楚。我们先从前端开始,因为这是用户直接接触的部分。
2. 搭建Vue3项目基础框架
首先,我们创建一个新的Vue3项目。如果你已经熟悉Vue3的创建过程,可以跳过这部分,直接看核心代码。
# 使用Vite创建Vue3项目
npm create vue@latest qwen-tts-frontend
# 按照提示选择配置
# ✔ Project name: qwen-tts-frontend
# ✔ Add TypeScript? No
# ✔ Add JSX Support? No
# ✔ Add Vue Router for Single Page Application development? No
# ✔ Add Pinia for state management? Yes
# ✔ Add Vitest for Unit Testing? No
# ✔ Add an End-to-End Testing Solution? No
# ✔ Add ESLint for code quality? Yes
# 进入项目目录并安装依赖
cd qwen-tts-frontend
npm install
# 安装我们需要的额外依赖
npm install axios element-plus
项目创建好后,我们先来设计一下界面。我们的应用需要几个核心功能:
- 输入要转换成语音的文字
- 输入声音描述(比如“温柔的女声,语速适中”)
- 选择语言
- 生成按钮和播放控制
打开 src/App.vue,我们先写一个简单的界面:
<template>
<div class="app-container">
<header class="app-header">
<h1>Qwen3-TTS 语音生成器</h1>
<p class="subtitle">用文字描述创造你想要的声音</p>
</header>
<main class="main-content">
<div class="input-section">
<!-- 这里我们会逐步添加输入组件 -->
</div>
<div class="control-section">
<!-- 控制按钮区域 -->
</div>
<div class="output-section">
<!-- 音频播放区域 -->
</div>
</main>
</div>
</template>
<script setup>
// 这里我们稍后会添加逻辑
</script>
<style scoped>
.app-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app-header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid #eaeaea;
}
.app-header h1 {
margin: 0;
color: #333;
font-size: 2.5rem;
}
.subtitle {
margin-top: 10px;
color: #666;
font-size: 1.1rem;
}
.main-content {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.input-section, .control-section, .output-section {
margin-bottom: 30px;
}
</style>
基础框架搭好了,现在我们来添加具体的功能组件。
3. 创建语音生成的核心组件
我们需要两个主要的输入区域:一个用于输入要转换的文字,另一个用于描述想要的声音。我们先创建一个 TextInput 组件:
<!-- src/components/TextInput.vue -->
<template>
<div class="text-input-container">
<label class="input-label">
{{ label }}
<span v-if="required" class="required-mark">*</span>
</label>
<textarea
v-model="localValue"
:placeholder="placeholder"
:rows="rows"
class="text-input"
@input="handleInput"
></textarea>
<div v-if="showCounter" class="char-counter">
已输入 {{ localValue.length }} / {{ maxLength }} 字符
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: '输入内容'
},
placeholder: {
type: String,
default: '请输入...'
},
rows: {
type: Number,
default: 4
},
maxLength: {
type: Number,
default: 1000
},
required: {
type: Boolean,
default: false
},
showCounter: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:modelValue'])
const localValue = ref(props.modelValue)
const handleInput = () => {
if (localValue.value.length > props.maxLength) {
localValue.value = localValue.value.substring(0, props.maxLength)
}
emit('update:modelValue', localValue.value)
}
watch(() => props.modelValue, (newVal) => {
if (newVal !== localValue.value) {
localValue.value = newVal
}
})
</script>
<style scoped>
.text-input-container {
margin-bottom: 20px;
}
.input-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 0.95rem;
}
.required-mark {
color: #f56c6c;
margin-left: 4px;
}
.text-input {
width: 100%;
padding: 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
transition: border-color 0.3s;
box-sizing: border-box;
}
.text-input:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.text-input::placeholder {
color: #c0c4cc;
}
.char-counter {
margin-top: 6px;
font-size: 0.85rem;
color: #909399;
text-align: right;
}
</style>
现在我们在主页面中使用这个组件。更新 src/App.vue:
<template>
<div class="app-container">
<header class="app-header">
<h1>Qwen3-TTS 语音生成器</h1>
<p class="subtitle">用文字描述创造你想要的声音</p>
</header>
<main class="main-content">
<div class="input-section">
<TextInput
v-model="text"
label="要转换的文字"
placeholder="请输入要转换成语音的文字内容..."
:rows="5"
:max-length="500"
required
/>
<TextInput
v-model="voiceDescription"
label="声音描述"
placeholder="例如:温柔的女声,语速适中,音调柔和..."
:rows="3"
:max-length="200"
/>
<div class="language-selector">
<label class="input-label">选择语言</label>
<select v-model="language" class="language-select">
<option value="Chinese">中文</option>
<option value="English">English</option>
<option value="Japanese">日本語</option>
<option value="Korean">한국어</option>
<option value="German">Deutsch</option>
<option value="French">Français</option>
<option value="Spanish">Español</option>
<option value="Italian">Italiano</option>
<option value="Russian">Русский</option>
<option value="Portuguese">Português</option>
</select>
</div>
</div>
<div class="control-section">
<button
@click="generateSpeech"
:disabled="isGenerating || !text"
class="generate-button"
>
<span v-if="isGenerating">生成中...</span>
<span v-else>生成语音</span>
</button>
</div>
<div v-if="audioUrl" class="output-section">
<h3>生成的语音</h3>
<audio :src="audioUrl" controls class="audio-player"></audio>
<div class="audio-actions">
<button @click="downloadAudio" class="action-button">
下载音频
</button>
<button @click="copyAudioUrl" class="action-button">
复制链接
</button>
</div>
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</main>
</div>
</template>
<script setup>
import { ref } from 'vue'
import TextInput from './components/TextInput.vue'
// 响应式数据
const text = ref('')
const voiceDescription = ref('温柔的女声,语速适中,音调柔和')
const language = ref('Chinese')
const audioUrl = ref('')
const isGenerating = ref(false)
const errorMessage = ref('')
// 生成语音的函数
const generateSpeech = async () => {
// 这里我们先留空,下一节实现
console.log('开始生成语音...')
}
// 下载音频
const downloadAudio = () => {
if (!audioUrl.value) return
const link = document.createElement('a')
link.href = audioUrl.value
link.download = 'generated-speech.wav'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
// 复制音频链接
const copyAudioUrl = async () => {
if (!audioUrl.value) return
try {
await navigator.clipboard.writeText(audioUrl.value)
alert('链接已复制到剪贴板')
} catch (err) {
console.error('复制失败:', err)
}
}
</script>
<style scoped>
/* 添加新样式 */
.language-selector {
margin-top: 20px;
}
.language-select {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 1rem;
background: white;
cursor: pointer;
transition: border-color 0.3s;
}
.language-select:focus {
outline: none;
border-color: #409eff;
}
.generate-button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #409eff, #337ecc);
color: white;
border: none;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.generate-button:hover:not(:disabled) {
background: linear-gradient(135deg, #66b1ff, #409eff);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.generate-button:disabled {
background: #c0c4cc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.output-section {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #eaeaea;
}
.output-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.audio-player {
width: 100%;
margin-bottom: 15px;
}
.audio-actions {
display: flex;
gap: 10px;
}
.action-button {
flex: 1;
padding: 10px;
background: white;
border: 1px solid #dcdfe6;
border-radius: 6px;
color: #606266;
cursor: pointer;
transition: all 0.2s;
}
.action-button:hover {
background: #f5f7fa;
border-color: #c0c4cc;
}
.error-message {
padding: 15px;
background: #fef0f0;
border: 1px solid #fde2e2;
border-radius: 8px;
color: #f56c6c;
margin-top: 20px;
}
</style>
现在界面已经基本完成了,但还缺少最重要的部分:和后端API的通信。接下来我们就来实现这个功能。
4. 实现API通信层
我们需要创建一个服务来管理所有和后端的通信。在 src 目录下创建 services 文件夹,然后创建 api.js:
// src/services/api.js
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:8000', // 后端服务地址
timeout: 300000, // 5分钟超时,因为语音生成可能需要较长时间
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在请求前添加token等
console.log('发送请求:', config.url)
return config
},
(error) => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
console.log('收到响应:', response.config.url)
return response
},
(error) => {
console.error('响应错误:', error)
if (error.response) {
// 服务器返回了错误状态码
switch (error.response.status) {
case 400:
error.message = '请求参数错误'
break
case 401:
error.message = '未授权,请检查API密钥'
break
case 404:
error.message = '请求的资源不存在'
break
case 500:
error.message = '服务器内部错误'
break
case 503:
error.message = '服务暂时不可用'
break
default:
error.message = `请求失败: ${error.response.status}`
}
} else if (error.request) {
// 请求发送了但没有收到响应
error.message = '网络错误,请检查网络连接'
} else {
// 请求配置出错
error.message = '请求配置错误'
}
return Promise.reject(error)
}
)
// API接口定义
export const ttsApi = {
// 生成语音
async generateSpeech(data) {
const response = await api.post('/api/generate', data)
return response.data
},
// 获取生成状态
async getStatus(taskId) {
const response = await api.get(`/api/status/${taskId}`)
return response.data
},
// 测试连接
async testConnection() {
const response = await api.get('/api/health')
return response.data
}
}
export default api
现在我们需要一个状态管理来管理应用的状态。我们使用Pinia,这是Vue3推荐的状态管理库。创建 src/stores/ttsStore.js:
// src/stores/ttsStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { ttsApi } from '@/services/api'
export const useTtsStore = defineStore('tts', () => {
// 状态
const text = ref('')
const voiceDescription = ref('温柔的女声,语速适中,音调柔和')
const language = ref('Chinese')
const audioUrl = ref('')
const isGenerating = ref(false)
const errorMessage = ref('')
const generationHistory = ref([])
const currentTaskId = ref('')
// 计算属性
const canGenerate = computed(() => {
return text.value.trim().length > 0 && !isGenerating.value
})
const historyCount = computed(() => {
return generationHistory.value.length
})
// 动作
const generateSpeech = async () => {
if (!canGenerate.value) return
isGenerating.value = true
errorMessage.value = ''
audioUrl.value = ''
try {
const requestData = {
text: text.value,
language: language.value,
instruct: voiceDescription.value
}
console.log('发送生成请求:', requestData)
const response = await ttsApi.generateSpeech(requestData)
if (response.success) {
if (response.audio_url) {
// 直接返回音频URL
audioUrl.value = response.audio_url
addToHistory({
text: text.value,
voiceDescription: voiceDescription.value,
language: language.value,
audioUrl: response.audio_url,
timestamp: new Date().toISOString()
})
} else if (response.task_id) {
// 需要轮询获取结果
currentTaskId.value = response.task_id
await pollGenerationStatus(response.task_id)
}
} else {
throw new Error(response.message || '生成失败')
}
} catch (error) {
console.error('生成语音失败:', error)
errorMessage.value = error.message || '生成语音时发生错误'
} finally {
isGenerating.value = false
}
}
const pollGenerationStatus = async (taskId) => {
const maxAttempts = 60 // 最多尝试60次
const interval = 2000 // 每2秒检查一次
for (let i = 0; i < maxAttempts; i++) {
try {
const status = await ttsApi.getStatus(taskId)
if (status.status === 'completed') {
audioUrl.value = status.audio_url
addToHistory({
text: text.value,
voiceDescription: voiceDescription.value,
language: language.value,
audioUrl: status.audio_url,
timestamp: new Date().toISOString()
})
return
} else if (status.status === 'failed') {
throw new Error(status.message || '生成失败')
}
// 如果还在处理中,等待后继续检查
await new Promise(resolve => setTimeout(resolve, interval))
} catch (error) {
errorMessage.value = error.message || '获取生成状态失败'
break
}
}
errorMessage.value = '生成超时,请稍后重试'
}
const addToHistory = (item) => {
generationHistory.value.unshift(item)
// 只保留最近10条记录
if (generationHistory.value.length > 10) {
generationHistory.value = generationHistory.value.slice(0, 10)
}
// 保存到localStorage
try {
localStorage.setItem('ttsHistory', JSON.stringify(generationHistory.value))
} catch (e) {
console.warn('无法保存历史记录到localStorage:', e)
}
}
const loadHistory = () => {
try {
const saved = localStorage.getItem('ttsHistory')
if (saved) {
generationHistory.value = JSON.parse(saved)
}
} catch (e) {
console.warn('无法从localStorage加载历史记录:', e)
}
}
const clearHistory = () => {
generationHistory.value = []
try {
localStorage.removeItem('ttsHistory')
} catch (e) {
console.warn('无法清除localStorage:', e)
}
}
const clearError = () => {
errorMessage.value = ''
}
const setText = (value) => {
text.value = value
}
const setVoiceDescription = (value) => {
voiceDescription.value = value
}
const setLanguage = (value) => {
language.value = value
}
// 初始化时加载历史记录
loadHistory()
return {
// 状态
text,
voiceDescription,
language,
audioUrl,
isGenerating,
errorMessage,
generationHistory,
currentTaskId,
// 计算属性
canGenerate,
historyCount,
// 动作
generateSpeech,
pollGenerationStatus,
addToHistory,
loadHistory,
clearHistory,
clearError,
setText,
setVoiceDescription,
setLanguage
}
})
现在更新 src/App.vue 来使用我们的store:
<template>
<div class="app-container">
<header class="app-header">
<h1>Qwen3-TTS 语音生成器</h1>
<p class="subtitle">用文字描述创造你想要的声音</p>
</header>
<main class="main-content">
<div class="input-section">
<TextInput
v-model="store.text"
label="要转换的文字"
placeholder="请输入要转换成语音的文字内容..."
:rows="5"
:max-length="500"
required
/>
<TextInput
v-model="store.voiceDescription"
label="声音描述"
placeholder="例如:温柔的女声,语速适中,音调柔和..."
:rows="3"
:max-length="200"
/>
<div class="language-selector">
<label class="input-label">选择语言</label>
<select v-model="store.language" class="language-select">
<option value="Chinese">中文</option>
<option value="English">English</option>
<option value="Japanese">日本語</option>
<option value="Korean">한국어</option>
<option value="German">Deutsch</option>
<option value="French">Français</option>
<option value="Spanish">Español</option>
<option value="Italian">Italiano</option>
<option value="Russian">Русский</option>
<option value="Portuguese">Português</option>
</select>
</div>
<div class="description-tips">
<h4>声音描述小贴士:</h4>
<ul>
<li>尽量具体:用"低沉"、"清脆"、"语速快"等明确描述</li>
<li>多维度结合:性别 + 年龄 + 情感 + 使用场景</li>
<li>示例:"年轻活泼的女声,语速快,音调上扬,适合产品介绍"</li>
</ul>
</div>
</div>
<div class="control-section">
<button
@click="store.generateSpeech"
:disabled="!store.canGenerate"
class="generate-button"
>
<span v-if="store.isGenerating">
<span class="loading-spinner"></span>
生成中...
</span>
<span v-else>生成语音</span>
</button>
</div>
<div v-if="store.audioUrl" class="output-section">
<h3>生成的语音</h3>
<audio :src="store.audioUrl" controls class="audio-player"></audio>
<div class="audio-actions">
<button @click="downloadAudio" class="action-button">
下载音频
</button>
<button @click="copyAudioUrl" class="action-button">
复制链接
</button>
<button @click="playInNewTab" class="action-button">
新标签页播放
</button>
</div>
</div>
<div v-if="store.errorMessage" class="error-message">
<div class="error-header">
<span class="error-icon"></span>
<span>出错了</span>
<button @click="store.clearError" class="close-error">×</button>
</div>
<p>{{ store.errorMessage }}</p>
</div>
<div v-if="store.historyCount > 0" class="history-section">
<div class="history-header">
<h3>生成历史 ({{ store.historyCount }})</h3>
<button @click="store.clearHistory" class="clear-history-btn">
清空历史
</button>
</div>
<div class="history-list">
<div
v-for="(item, index) in store.generationHistory"
:key="index"
class="history-item"
@click="playHistoryItem(item)"
>
<div class="history-text">
<strong>{{ item.text.substring(0, 50) }}{{ item.text.length > 50 ? '...' : '' }}</strong>
<small>{{ formatDate(item.timestamp) }}</small>
</div>
<div class="history-desc">
声音:{{ item.voiceDescription }}
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { useTtsStore } from '@/stores/ttsStore'
import TextInput from './components/TextInput.vue'
const store = useTtsStore()
const downloadAudio = () => {
if (!store.audioUrl) return
const link = document.createElement('a')
link.href = store.audioUrl
link.download = `tts-${Date.now()}.wav`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const copyAudioUrl = async () => {
if (!store.audioUrl) return
try {
await navigator.clipboard.writeText(store.audioUrl)
alert('链接已复制到剪贴板')
} catch (err) {
console.error('复制失败:', err)
}
}
const playInNewTab = () => {
if (!store.audioUrl) return
window.open(store.audioUrl, '_blank')
}
const playHistoryItem = (item) => {
store.audioUrl = item.audioUrl
// 自动播放
setTimeout(() => {
const audioElement = document.querySelector('.audio-player')
if (audioElement) {
audioElement.play().catch(e => console.log('自动播放被阻止:', e))
}
}, 100)
}
const formatDate = (timestamp) => {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
</script>
<style scoped>
/* 添加新样式 */
.description-tips {
margin-top: 20px;
padding: 15px;
background: #f0f9ff;
border-radius: 6px;
border-left: 4px solid #409eff;
}
.description-tips h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.description-tips ul {
margin: 0;
padding-left: 20px;
color: #666;
}
.description-tips li {
margin-bottom: 5px;
font-size: 0.9rem;
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.error-icon {
margin-right: 8px;
}
.close-error {
background: none;
border: none;
font-size: 1.5rem;
color: #f56c6c;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-error:hover {
background: rgba(245, 108, 108, 0.1);
}
.history-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eaeaea;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.clear-history-btn {
padding: 6px 12px;
background: #fef0f0;
border: 1px solid #fde2e2;
border-radius: 4px;
color: #f56c6c;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.clear-history-btn:hover {
background: #fde2e2;
}
.history-list {
max-height: 300px;
overflow-y: auto;
}
.history-item {
padding: 12px;
margin-bottom: 8px;
background: white;
border: 1px solid #eaeaea;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.history-item:hover {
background: #f8f9fa;
border-color: #409eff;
transform: translateX(4px);
}
.history-text {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.history-text strong {
flex: 1;
margin-right: 10px;
color: #333;
font-size: 0.95rem;
}
.history-text small {
color: #909399;
font-size: 0.8rem;
white-space: nowrap;
}
.history-desc {
color: #666;
font-size: 0.85rem;
line-height: 1.4;
}
</style>
5. 后端服务搭建指南
前端部分已经完成了,现在我们需要一个后端服务来处理实际的语音生成。这里我提供一个简单的FastAPI后端示例:
# backend/main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
import uuid
import asyncio
from datetime import datetime
import os
app = FastAPI(title="Qwen3-TTS API", version="1.0.0")
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # Vue开发服务器地址
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 数据模型
class TTSRequest(BaseModel):
text: str
language: str = "Chinese"
instruct: Optional[str] = None
class TTSResponse(BaseModel):
success: bool
message: str
task_id: Optional[str] = None
audio_url: Optional[str] = None
class StatusResponse(BaseModel):
task_id: str
status: str # pending, processing, completed, failed
message: Optional[str] = None
audio_url: Optional[str] = None
created_at: datetime
updated_at: datetime
# 模拟的任务存储
tasks = {}
@app.get("/api/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.now()}
@app.post("/api/generate", response_model=TTSResponse)
async def generate_tts(request: TTSRequest, background_tasks: BackgroundTasks):
# 这里应该是实际的Qwen3-TTS调用
# 由于模型需要GPU,这里我们先模拟一下
task_id = str(uuid.uuid4())
# 创建任务
tasks[task_id] = {
"status": "pending",
"request": request.dict(),
"created_at": datetime.now(),
"updated_at": datetime.now()
}
# 在后台处理任务
background_tasks.add_task(process_tts_task, task_id)
return TTSResponse(
success=True,
message="任务已开始处理",
task_id=task_id
)
@app.get("/api/status/{task_id}", response_model=StatusResponse)
async def get_status(task_id: str):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="任务不存在")
task = tasks[task_id]
return StatusResponse(
task_id=task_id,
status=task["status"],
message=task.get("message"),
audio_url=task.get("audio_url"),
created_at=task["created_at"],
updated_at=task["updated_at"]
)
async def process_tts_task(task_id: str):
"""模拟语音生成过程"""
try:
task = tasks[task_id]
task["status"] = "processing"
task["updated_at"] = datetime.now()
# 模拟处理时间
await asyncio.sleep(5)
# 这里应该是实际的Qwen3-TTS调用
# 示例代码:
# from qwen_tts import Qwen3TTSModel
# import torch
# import soundfile as sf
#
# model = Qwen3TTSModel.from_pretrained(
# "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
# device_map="cuda:0",
# dtype=torch.bfloat16
# )
#
# wavs, sr = model.generate_voice_design(
# text=task["request"]["text"],
# language=task["request"]["language"],
# instruct=task["request"].get("instruct", "")
# )
#
# # 保存音频文件
# filename = f"generated_{task_id}.wav"
# sf.write(f"static/{filename}", wavs[0], sr)
# 模拟生成成功
task["status"] = "completed"
task["audio_url"] = f"http://localhost:8000/static/sample_audio.wav" # 示例音频
task["message"] = "语音生成成功"
task["updated_at"] = datetime.now()
except Exception as e:
task["status"] = "failed"
task["message"] = str(e)
task["updated_at"] = datetime.now()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
要运行这个后端,你需要安装依赖:
pip install fastapi uvicorn python-multipart
然后运行:
python backend/main.py
6. 性能优化和错误处理
在实际使用中,我们还需要考虑一些优化和错误处理。让我们在前端添加一些改进:
// src/utils/audioUtils.js
export class AudioManager {
constructor() {
this.audioContext = null
this.audioCache = new Map()
this.maxCacheSize = 10
}
// 初始化AudioContext
initAudioContext() {
if (!this.audioContext && typeof window !== 'undefined') {
const AudioContext = window.AudioContext || window.webkitAudioContext
this.audioContext = new AudioContext()
}
return this.audioContext
}
// 预加载音频
async preloadAudio(url) {
if (this.audioCache.has(url)) {
return this.audioCache.get(url)
}
try {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioContext = this.initAudioContext()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
// 添加到缓存
this.audioCache.set(url, audioBuffer)
// 清理旧缓存
if (this.audioCache.size > this.maxCacheSize) {
const firstKey = this.audioCache.keys().next().value
this.audioCache.delete(firstKey)
}
return audioBuffer
} catch (error) {
console.error('预加载音频失败:', error)
throw error
}
}
// 播放音频
async playAudio(url) {
try {
const audioBuffer = await this.preloadAudio(url)
const audioContext = this.initAudioContext()
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(audioContext.destination)
source.start(0)
return {
stop: () => source.stop(),
duration: audioBuffer.duration
}
} catch (error) {
console.error('播放音频失败:', error)
throw error
}
}
// 下载音频
async downloadAudio(url, filename = 'audio.wav') {
try {
const response = await fetch(url)
const blob = await response.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 清理URL对象
setTimeout(() => URL.revokeObjectURL(link.href), 100)
return true
} catch (error) {
console.error('下载音频失败:', error)
throw error
}
}
// 获取音频时长
async getAudioDuration(url) {
try {
const audioBuffer = await this.preloadAudio(url)
return audioBuffer.duration
} catch (error) {
console.error('获取音频时长失败:', error)
return 0
}
}
}
// 创建单例实例
export const audioManager = new AudioManager()
现在更新我们的store来使用这个音频管理器:
// 在ttsStore.js中添加
import { audioManager } from '@/utils/audioUtils'
// 在store中添加新的action
const playAudio = async () => {
if (!audioUrl.value) return
try {
await audioManager.playAudio(audioUrl.value)
} catch (error) {
errorMessage.value = '播放音频失败: ' + error.message
}
}
const preloadAudio = async (url) => {
try {
await audioManager.preloadAudio(url)
} catch (error) {
console.warn('预加载音频失败:', error)
}
}
// 在generateSpeech成功生成后预加载音频
if (response.audio_url) {
audioUrl.value = response.audio_url
// 预加载音频以便快速播放
preloadAudio(response.audio_url)
// ... 其他逻辑
}
7. 部署和配置建议
当你的应用开发完成后,需要考虑部署问题。这里有一些建议:
前端部署
- 构建生产版本:
npm run build
这会生成 dist 文件夹,里面是优化过的静态文件。
- 部署到静态托管服务:
- Vercel:
vercel --prod - Netlify: 直接拖拽
dist文件夹 - GitHub Pages: 配置自动化部署
- Vercel:
后端部署
- 使用Docker容器化:
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 安装Qwen3-TTS
RUN pip install qwen-tts
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
- 云服务部署:
- 需要GPU的云服务器(AWS EC2 G4实例、Google Cloud GPU实例等)
- 配置环境变量和API密钥
- 设置反向代理(Nginx)
环境配置
创建 .env 文件:
VITE_API_BASE_URL=http://localhost:8000
VITE_MAX_TEXT_LENGTH=1000
VITE_DEFAULT_VOICE_DESCRIPTION=温柔的女声,语速适中
更新 api.js 使用环境变量:
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000',
// ... 其他配置
})
8. 实际应用场景
这个集成方案可以应用在很多实际场景中:
- 在线教育平台:为课程内容生成不同风格的讲解语音
- 游戏开发:快速生成游戏角色的对话语音
- 内容创作:为视频、播客生成配音
- 无障碍服务:为视障用户提供文本转语音
- 智能客服:创建个性化的客服语音
你可以根据具体需求扩展功能,比如:
- 添加语音克隆功能(需要上传参考音频)
- 实现批量生成
- 添加语音效果调节(语速、音调等)
- 集成到现有的CMS或电商平台
9. 总结
通过这个教程,我们完成了一个完整的Qwen3-TTS与Vue3前端集成方案。从界面设计到状态管理,从API通信到音频处理,我们覆盖了前端集成的所有关键环节。
实际用下来,这种前后端分离的架构确实比较灵活。前端负责用户体验和界面交互,后端专注处理复杂的AI模型推理。即使你不是AI专家,也能通过这个方案在自己的应用中添加高质量的语音生成功能。
当然,实际部署时可能会遇到一些挑战,比如后端服务的GPU资源、网络延迟、音频文件存储等。但这些问题都有成熟的解决方案,可以根据具体需求来选择。
如果你刚开始接触AI语音生成,建议先从简单的例子开始,熟悉了整个流程后再尝试更复杂的场景。这个方案提供了一个很好的起点,你可以基于它构建更强大的语音应用。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐

所有评论(0)