feat: 语音
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<!-- 更多提示词弹窗 -->
|
<!-- 更多提示词弹窗 -->
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<!-- 统一输入:文本或视频链接 -->
|
<!-- 统一输入:文本或视频链接 -->
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user