feat: 语音

This commit is contained in:
2026-02-01 18:35:03 +08:00
parent 646a72de64
commit 003e55dccf
8 changed files with 45 additions and 67 deletions

View File

@@ -86,13 +86,6 @@ onMounted(async () => {})
display: flex; display: flex;
} }
.ant-select-selector {
background: var(--color-bg) !important;
border: 1px solid var(--color-border) !important;
border-radius: 4px !important;
transition: all 0.2s !important;
min-height: 32px !important;
}
.ant-select-selection-item, .ant-select-selection-item,
.ant-select-selection-placeholder { .ant-select-selection-placeholder {

View File

@@ -4,7 +4,7 @@
<div v-if="displayMode === 'select'" class="prompt-select-container"> <div v-if="displayMode === 'select'" class="prompt-select-container">
<a-select <a-select
v-model:value="selectedPromptId" v-model:value="selectedPromptId"
placeholder="选择提示词风格" placeholder="选择文案风格"
style="width: 100%" style="width: 100%"
@change="handleSelectChange" @change="handleSelectChange"
:loading="loading" :loading="loading"
@@ -56,7 +56,7 @@
<polyline points="21 15 16 10 5 21"></polyline> <polyline points="21 15 16 10 5 21"></polyline>
</svg> </svg>
</div> </div>
<p class="prompt-empty-text">没有找到提示词</p> <p class="prompt-empty-text">没有找到文案风格</p>
</div> </div>
<!-- 更多提示词弹窗 --> <!-- 更多提示词弹窗 -->

View File

@@ -12,7 +12,7 @@ export const VOICE_PROVIDER_TYPES = {
} }
// 默认供应商 // 默认供应商
export const DEFAULT_VOICE_PROVIDER = VOICE_PROVIDER_TYPES.COSYVOICE export const DEFAULT_VOICE_PROVIDER = VOICE_PROVIDER_TYPES.SILICONFLOW
// 供应商选项(用于下拉选择) // 供应商选项(用于下拉选择)
export const VOICE_PROVIDER_OPTIONS = [ export const VOICE_PROVIDER_OPTIONS = [

View File

@@ -174,14 +174,14 @@ async function generateCopywriting() {
return return
} }
// 检查是否选择了提示词风格 // 检查是否选择了风格
if (!form.value.prompt || !form.value.prompt.trim()) { if (!form.value.prompt || !form.value.prompt.trim()) {
message.warning('请先选择提示词风格') message.warning('请先选择文案风格')
return return
} }
if (!selectedPromptId.value) { if (!selectedPromptId.value) {
message.warning('请先选择提示词风格') message.warning('请先选择文案风格')
return return
} }
@@ -386,13 +386,6 @@ defineOptions({ name: 'ContentStyleCopywriting' })
<a-card class="form-card" :bordered="false" title="创作设置"> <a-card class="form-card" :bordered="false" title="创作设置">
<a-form :model="form" layout="vertical" class="form-container"> <a-form :model="form" layout="vertical" class="form-container">
<a-form-item class="form-item"> <a-form-item class="form-item">
<template #label>
<span>
选择提示词风格
<span class="form-tip-inline">从已保存的提示词中选择</span>
</span>
</template>
<!-- 使用 PromptSelector 组件 --> <!-- 使用 PromptSelector 组件 -->
<PromptSelector <PromptSelector
v-model="selectedPromptId" v-model="selectedPromptId"
@@ -403,10 +396,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
@update:searchKeyword="promptSearchKeyword = $event" @update:searchKeyword="promptSearchKeyword = $event"
/> />
<!-- 空状态提示 -->
<div v-if="!loadingPrompts && allPrompts.length === 0" class="prompt-empty" style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</a-form-item> </a-form-item>
<!-- 统一输入文本或视频链接 --> <!-- 统一输入文本或视频链接 -->

View File

@@ -68,18 +68,6 @@
<a-input v-model:value="formData.name" placeholder="请输入配音名称" /> <a-input v-model:value="formData.name" placeholder="请输入配音名称" />
</a-form-item> </a-form-item>
<a-form-item
v-if="isCreateMode"
label="语音供应商"
name="providerType"
>
<a-select
v-model:value="formData.providerType"
:options="PROVIDER_OPTIONS"
placeholder="请选择语音供应商"
/>
</a-form-item>
<a-form-item <a-form-item
v-if="isCreateMode" v-if="isCreateMode"
label="音频文件" label="音频文件"
@@ -125,10 +113,8 @@ import { MaterialService } from '@/api/material'
import { useUpload } from '@/composables/useUpload' import { useUpload } from '@/composables/useUpload'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import BasicLayout from '@/layouts/components/BasicLayout.vue' import BasicLayout from '@/layouts/components/BasicLayout.vue'
import { VOICE_PROVIDER_OPTIONS, DEFAULT_VOICE_PROVIDER } from '@/config/voiceConfig'
// ========== 常量 ========== // ========== 常量 ==========
const PROVIDER_OPTIONS = VOICE_PROVIDER_OPTIONS
const DEFAULT_FORM_DATA = { const DEFAULT_FORM_DATA = {
id: null, id: null,
@@ -137,8 +123,7 @@ const DEFAULT_FORM_DATA = {
autoTranscribe: true, autoTranscribe: true,
language: 'zh-CN', language: 'zh-CN',
gender: 'female', gender: 'female',
note: '', note: ''
providerType: DEFAULT_VOICE_PROVIDER
} }
// ========== 响应式数据 ========== // ========== 响应式数据 ==========
@@ -199,8 +184,7 @@ const fillFormData = (data) => {
fileId: data.fileId || null, fileId: data.fileId || null,
language: data.language || 'zh-CN', language: data.language || 'zh-CN',
gender: data.gender || 'female', gender: data.gender || 'female',
note: data.note || '', note: data.note || ''
providerType: data.providerType || DEFAULT_VOICE_PROVIDER
}) })
} }
@@ -269,6 +253,8 @@ const handleDelete = (record) => {
okText: '删除', okText: '删除',
okButtonProps: { danger: true }, okButtonProps: { danger: true },
cancelText: '取消', cancelText: '取消',
centered: true,
width: 420,
onOk: async () => { onOk: async () => {
try { try {
const res = await VoiceService.delete(record.id) const res = await VoiceService.delete(record.id)
@@ -380,8 +366,7 @@ const handleSubmit = async () => {
autoTranscribe: formData.autoTranscribe, autoTranscribe: formData.autoTranscribe,
language: formData.language, language: formData.language,
gender: formData.gender, gender: formData.gender,
note: formData.note, note: formData.note
providerType: formData.providerType
} }
: { : {
id: formData.id, id: formData.id,

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { message, Select } from 'ant-design-vue' import { message } from 'ant-design-vue'
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub' import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
import { CommonService } from '@/api/common' import { CommonService } from '@/api/common'
import { UserPromptApi } from '@/api/userPrompt' import { UserPromptApi } from '@/api/userPrompt'
@@ -166,7 +166,7 @@ async function handleGenerate() {
} }
if (!topicDetails.stylePrompt?.trim()) { if (!topicDetails.stylePrompt?.trim()) {
message.warning('请先选择提示词风格') message.warning('请先选择文案风格')
return return
} }
@@ -602,11 +602,9 @@ onMounted(async () => {
<!-- 热点标题 --> <!-- 热点标题 -->
<div> <div>
<label class="form-label">热点标题</label> <label class="form-label">热点标题</label>
<input <a-input
v-model="topicDetails.title" v-model:value="topicDetails.title"
type="text" placeholder="选择左侧热点或手动输入标题"
placeholder="选择左侧热点或手动输入标题"
class="form-input"
/> />
</div> </div>
@@ -619,18 +617,17 @@ onMounted(async () => {
<span class="analyzing-text">正在分析语音...</span> <span class="analyzing-text">正在分析语音...</span>
</span> </span>
</div> </div>
<textarea <a-textarea
v-model="topicDetails.copywriting" v-model:value="topicDetails.copywriting"
rows="5" :rows="5"
placeholder="输入或AI生成文案内容" placeholder="输入或AI生成文案内容"
class="form-textarea"
:disabled="isAnalyzing" :disabled="isAnalyzing"
></textarea> />
</div> </div>
<!-- 风格提示词 --> <!-- 风格提示词 -->
<div> <div>
<label class="form-label" style="display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: var(--color-text);">风格提示词</label> <label class="form-label" style="display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: var(--color-text);">风格</label>
<!-- 使用 PromptSelector 组件 --> <!-- 使用 PromptSelector 组件 -->
<PromptSelector <PromptSelector
@@ -642,10 +639,7 @@ onMounted(async () => {
@update:searchKeyword="promptSearchKeyword = $event" @update:searchKeyword="promptSearchKeyword = $event"
/> />
<!-- 空状态提示 -->
<div v-if="!loadingPrompts && allPrompts.length === 0" class="prompt-empty" style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div> </div>
<!-- 生成文案按钮 --> <!-- 生成文案按钮 -->
@@ -760,7 +754,6 @@ onMounted(async () => {
padding: 8px 12px; padding: 8px 12px;
font-size: 14px; font-size: 14px;
color: var(--color-text); color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 6px; border-radius: 6px;
transition: all 0.2s; transition: all 0.2s;
@@ -1005,7 +998,6 @@ onMounted(async () => {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
color: var(--color-text); color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 6px; border-radius: 6px;
transition: all 0.2s; transition: all 0.2s;

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.tik.voice.client; package cn.iocoder.yudao.module.tik.voice.client;
import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.tik.voice.config.VoiceProviderProperties;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -27,8 +28,11 @@ public class VoiceCloneProviderFactory {
private final Map<String, VoiceCloneProvider> providers = new ConcurrentHashMap<>(); private final Map<String, VoiceCloneProvider> providers = new ConcurrentHashMap<>();
private final VoiceProviderProperties properties;
@Autowired @Autowired
public VoiceCloneProviderFactory(List<VoiceCloneProvider> providerList) { public VoiceCloneProviderFactory(List<VoiceCloneProvider> providerList, VoiceProviderProperties properties) {
this.properties = properties;
// 自动注册所有 Provider 实现类 // 自动注册所有 Provider 实现类
for (VoiceCloneProvider provider : providerList) { for (VoiceCloneProvider provider : providerList) {
registerProvider(provider); registerProvider(provider);
@@ -59,7 +63,12 @@ public class VoiceCloneProviderFactory {
if (providers.isEmpty()) { if (providers.isEmpty()) {
throw exception0(VOICE_TTS_FAILED.getCode(), "未配置任何语音克隆 Provider"); throw exception0(VOICE_TTS_FAILED.getCode(), "未配置任何语音克隆 Provider");
} }
// 返回第一个注册的 Provider 作为默认 // 使用配置的默认供应商
String defaultProviderType = properties.getDefaultProvider();
if (defaultProviderType != null && providers.containsKey(defaultProviderType)) {
return providers.get(defaultProviderType);
}
// 回退到第一个注册的 Provider
return providers.values().iterator().next(); return providers.values().iterator().next();
} }

View File

@@ -223,8 +223,17 @@ wx:
# 芋道配置项,设置当前项目所有自定义的配置 # 芋道配置项,设置当前项目所有自定义的配置
yudao: yudao:
cosyvoice: voice:
api-key: sk-10c746f8cb8640738f8d6b71af699003 default-provider: siliconflow
cosyvoice:
enabled: true
api-key: sk-10c746f8cb8640738f8d6b71af699003
default-model: cosyvoice-v3-flash
siliconflow:
enabled: true
api-key: ${SILICONFLOW_API_KEY:}
base-url: https://api.siliconflow.cn
default-model: IndexTeam/IndexTTS-2
ice: ice:
access-key-id: LTAI5tPV9Ag3csf41GZjaLTA access-key-id: LTAI5tPV9Ag3csf41GZjaLTA
access-key-secret: kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs access-key-secret: kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs