feat: 功能优化

This commit is contained in:
2026-01-27 01:39:08 +08:00
parent bf12e70339
commit 24f66c8e81
24 changed files with 1570 additions and 133 deletions

View File

@@ -22,6 +22,7 @@ export const VoiceService = {
* @param {string} data.language - 语言(可选)
* @param {string} data.gender - 音色类型(可选)
* @param {string} data.note - 备注(可选)
* @param {string} data.providerType - 供应商类型可选cosyvoice-阿里云siliconflow-硅基流动
* @returns {Promise}
*/
create(data) {
@@ -88,8 +89,9 @@ export const VoiceService = {
},
/**
* 文本转语音CosyVoice
* 文本转语音
* @param {Object} data
* @param {string} data.providerType - 供应商类型可选cosyvoice-阿里云siliconflow-硅基流动
* @returns {Promise}
*/
synthesize(data) {
@@ -99,6 +101,7 @@ export const VoiceService = {
/**
* 我的音色试听
* @param {Object} data
* @param {string} data.providerType - 供应商类型可选cosyvoice-阿里云siliconflow-硅基流动
* @returns {Promise}
*/
preview(data) {
@@ -107,4 +110,3 @@ export const VoiceService = {
}
export default VoiceService

View File

@@ -1,21 +1,18 @@
/**
* TTS (Text-to-Speech) 公共Hook
* 支持多个供应商:Qwen, Azure, AWS等
* 支持多个供应商:CosyVoice, SiliconFlow, Azure, AWS等
*/
import { ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import { VoiceService } from '@/api/voice'
import { normalizeProviderType, VOICE_PROVIDER_TYPES } from '@/config/voiceConfig'
// 供应商配置
const TTS_PROVIDERS = {
QWEN: 'qwen',
AZURE: 'azure',
AWS: 'aws'
}
// 兼容旧代码的导出
const TTS_PROVIDERS = VOICE_PROVIDER_TYPES
// 默认配置
// 供应商默认配置(使用标准化后的键名)
const DEFAULT_CONFIG = {
qwen: {
cosyvoice: {
apiEndpoint: '/api/tik/voice/tts',
audioFormat: 'mp3',
supportedFormats: ['mp3', 'wav']
@@ -32,16 +29,9 @@ const DEFAULT_CONFIG = {
}
}
/**
* TTS Hook主函数
* @param {Object} options 配置选项
* @param {string} options.provider 供应商名称,默认'qwen'
* @param {Object} options.customConfig 自定义配置
* @returns {Object} TTS相关的方法和状态
*/
export function useTTS(options = {}) {
const {
provider = TTS_PROVIDERS.QWEN,
provider = VOICE_PROVIDER_TYPES.COSYVOICE,
customConfig = {}
} = options
@@ -59,7 +49,8 @@ export function useTTS(options = {}) {
// 获取当前供应商配置
const getProviderConfig = () => {
const config = DEFAULT_CONFIG[provider] || DEFAULT_CONFIG[TTS_PROVIDERS.QWEN]
const normalizedProvider = normalizeProviderType(provider)
const config = DEFAULT_CONFIG[normalizedProvider] || DEFAULT_CONFIG.cosyvoice
return { ...config, ...customConfig }
}
@@ -202,7 +193,7 @@ export function useTTS(options = {}) {
speechRate: speechRate.value || 1.0,
audioFormat: providerConfig.audioFormat,
timestamp: Date.now(),
provider: provider
providerType: normalizeProviderType(provider)
}
}
@@ -285,7 +276,7 @@ export function useTTS(options = {}) {
voiceConfigId: params.voiceConfigId,
speechRate: params.speechRate || speechRate.value,
audioFormat: params.audioFormat || providerConfig.audioFormat,
provider: provider
providerType: normalizeProviderType(provider)
}
return await VoiceService.synthesize(ttsParams)

View File

@@ -0,0 +1,61 @@
/**
* 语音供应商统一配置
*/
// 供应商类型枚举
export const VOICE_PROVIDER_TYPES = {
COSYVOICE: 'cosyvoice',
SILICONFLOW: 'siliconflow',
QWEN: 'qwen',
AZURE: 'azure',
AWS: 'aws'
}
// 默认供应商
export const DEFAULT_VOICE_PROVIDER = VOICE_PROVIDER_TYPES.COSYVOICE
// 供应商选项(用于下拉选择)
export const VOICE_PROVIDER_OPTIONS = [
{ label: '阿里云 CosyVoice', value: VOICE_PROVIDER_TYPES.COSYVOICE },
{ label: '硅基流动 SiliconFlow', value: VOICE_PROVIDER_TYPES.SILICONFLOW }
]
// 供应商别名映射(兼容旧名称)
export const PROVIDER_ALIAS_MAP = {
[VOICE_PROVIDER_TYPES.QWEN]: VOICE_PROVIDER_TYPES.COSYVOICE
}
/**
* 标准化供应商类型(处理别名映射)
*/
export function normalizeProviderType(providerType) {
if (!providerType) return DEFAULT_VOICE_PROVIDER
return PROVIDER_ALIAS_MAP[providerType] || providerType
}
/**
* 获取供应商显示名称
*/
export function getProviderLabel(providerType) {
const option = VOICE_PROVIDER_OPTIONS.find(opt => opt.value === providerType)
return option?.label || providerType
}
/**
* 检查供应商是否支持
*/
export function isProviderSupported(providerType) {
const normalized = normalizeProviderType(providerType)
return Object.values(VOICE_PROVIDER_TYPES).includes(normalized)
}
// 默认导出配置对象
export default {
VOICE_PROVIDER_TYPES,
DEFAULT_VOICE_PROVIDER,
VOICE_PROVIDER_OPTIONS,
PROVIDER_ALIAS_MAP,
normalizeProviderType,
getProviderLabel,
isProviderSupported
}

View File

@@ -14,6 +14,7 @@ import { MaterialService } from '@/api/material'
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
import { extractVideoCover } from '@/utils/video-cover'
import { useUpload } from '@/composables/useUpload'
import { DEFAULT_VOICE_PROVIDER } from '@/config/voiceConfig'
// 导入 voiceStore 用于获取用户音色
import { useVoiceCopyStore } from '@/stores/voiceCopy'
@@ -249,41 +250,38 @@ const resetPreviewState = () => {
}
const buildPreviewParams = (voice) => {
// 公共参数
const baseParams = {
inputText: ttsText.value,
speechRate: speechRate.value || 1.0,
audioFormat: 'mp3',
timestamp: Date.now(),
providerType: DEFAULT_VOICE_PROVIDER
}
if (voice.source === 'user') {
// 用户音色使用voiceConfigId不传instruction
// 用户音色使用voiceConfigId
const configId = voice.rawId || extractIdFromString(voice.id)
if (!configId) {
message.error('配音配置无效')
return null
}
return {
voiceConfigId: configId,
inputText: ttsText.value,
speechRate: speechRate.value || 1.0,
audioFormat: 'mp3',
timestamp: Date.now() // 添加时间戳确保每次请求不同
}
} else {
// 系统音色根据是否选择instruction或emotion来决定传递哪个参数
const params = {
voiceId: voice.voiceId,
inputText: ttsText.value,
speechRate: speechRate.value || 1.0,
audioFormat: 'mp3',
timestamp: Date.now() // 添加时间戳确保每次请求不同
}
// instruction和emotion只能选一个传递
if (instruction.value && instruction.value !== 'neutral') {
params.instruction = instruction.value
} else if (emotion.value && emotion.value !== 'neutral') {
params.emotion = emotion.value
} else if (voice.defaultInstruction) {
params.instruction = voice.defaultInstruction
}
return params
return { ...baseParams, voiceConfigId: configId }
}
// 系统音色使用voiceId可能包含instruction/emotion
const params = { ...baseParams, voiceId: voice.voiceId }
// instruction和emotion只能选一个传递
if (instruction.value && instruction.value !== 'neutral') {
params.instruction = instruction.value
} else if (emotion.value && emotion.value !== 'neutral') {
params.emotion = emotion.value
} else if (voice.defaultInstruction) {
params.instruction = voice.defaultInstruction
}
return params
}
const extractIdFromString = (idStr) => {
@@ -303,7 +301,8 @@ const handleSynthesizeVoice = async () => {
const params = {
inputText: ttsText.value,
speechRate: speechRate.value,
audioFormat: 'mp3'
audioFormat: 'mp3',
providerType: DEFAULT_VOICE_PROVIDER
}
if (voice.source === 'user') {

View File

@@ -113,8 +113,11 @@ import { MaterialService } from '@/api/material'
import { useUpload } from '@/composables/useUpload'
import dayjs from 'dayjs'
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 = {
id: null,
name: '',
@@ -122,7 +125,8 @@ const DEFAULT_FORM_DATA = {
autoTranscribe: true,
language: 'zh-CN',
gender: 'female',
note: ''
note: '',
providerType: DEFAULT_VOICE_PROVIDER
}
// ========== 响应式数据 ==========
@@ -183,7 +187,8 @@ const fillFormData = (data) => {
fileId: data.fileId || null,
language: data.language || 'zh-CN',
gender: data.gender || 'female',
note: data.note || ''
note: data.note || '',
providerType: data.providerType || DEFAULT_VOICE_PROVIDER
})
}
@@ -363,7 +368,8 @@ const handleSubmit = async () => {
autoTranscribe: formData.autoTranscribe,
language: formData.language,
gender: formData.gender,
note: formData.note
note: formData.note,
providerType: formData.providerType
}
: {
id: formData.id,

View File

@@ -13,6 +13,7 @@ import type {
} from '../types/identify-face'
// @ts-ignore
import { VoiceService } from '@/api/voice'
import { DEFAULT_VOICE_PROVIDER } from '@/config/voiceConfig'
/**
* 语音生成 Hook
@@ -76,6 +77,7 @@ export function useVoiceGeneration(): UseVoiceGeneration {
voiceConfigId: voice.rawId || extractIdFromString(voice.id),
speechRate: speechRate.value || 1.0,
audioFormat: 'mp3' as const,
providerType: DEFAULT_VOICE_PROVIDER,
}
const res = await VoiceService.synthesize(params)

View File

@@ -0,0 +1,133 @@
# Technical Design: Voice Clone Provider Refactoring
## Context
当前语音克隆功能直接依赖阿里云 CosyVoice 的 SDK 和 API。Service 层直接调用 `CosyVoiceClient`,导致:
1. **强耦合**:无法轻松切换或添加其他供应商
2. **测试困难**:难以 mock 外部依赖
3. **扩展性差**:添加新供应商需要修改 Service 层
## Goals / Non-Goals
### Goals
- 解耦 Service 层与具体供应商实现
- 支持多供应商并存和动态切换
- 保持现有功能完全兼容
- 为添加硅基流动 IndexTTS-2 打下基础
### Non-Goals
- 不改变现有 API 行为
- 不修改数据库结构
- 不改变前端交互
## Decisions
### 1. 采用策略模式 + 工厂模式
**Why**:
- 策略模式:定义统一接口,各供应商独立实现
- 工厂模式:根据配置动态获取 Provider 实例
- 符合开闭原则,扩展时无需修改现有代码
**架构**:
```
VoiceCloneProvider (interface)
├── CosyVoiceProvider (impl) - 阿里云 CosyVoice (DashScope)
├── SiliconFlowProvider (impl) - 阶段二:硅基流动 IndexTTS-2
└── VoiceCloneProviderFactory
```
**说明**:
- `CosyVoiceProvider` 对应阿里云 DashScope 的语音服务
- 默认模型:`cosyvoice-v3-flash`
- 扩展时添加新的 Provider 实现
### 2. 统一 DTO 设计
**Why**: 屏蔽不同供应商的 API 差异
```java
// 统一请求
VoiceCloneRequest {
String audioUrl; // 音频 URL
String prefix; // 音色前缀
String targetModel; // 目标模型
}
// 统一响应
VoiceCloneResult {
String voiceId; // 生成的音色 ID
String requestId; // 请求 ID
}
```
### 3. 配置结构设计
**新配置结构**:
```yaml
yudao:
voice:
# 默认供应商
default-provider: cosyvoice
# 供应商配置
providers:
cosyvoice: # 阿里云 CosyVoice
enabled: true
api-key: ${DASHSCOPE_API_KEY}
default-model: cosyvoice-v3-flash
# ... 其他配置
siliconflow: # 阶段二添加
enabled: false
api-key: ${SILICONFLOW_API_KEY}
base-url: https://api.siliconflow.cn
default-model: indextts-2
```
**向后兼容**:
- 读取旧配置 `yudao.cosyvoice.*` 并合并到新结构
- 优先使用新配置,旧配置作为 fallback
### 4. 错误处理策略
- Provider 调用失败时,记录详细日志
- 返回统一的业务异常 `VOICE_TTS_FAILED`
- 不暴露底层供应商的技术细节
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| 破坏现有功能 | 充分测试,保持 DTO 兼容 |
| 配置迁移复杂 | 支持旧配置自动映射 |
| 性能开销 | 工厂缓存 Provider 实例 |
## Migration Plan
### 阶段一CosyVoice 重构
1. 创建接口和工厂
2. 重构 CosyVoice 为 Provider 实现
3. 更新 Service 层使用接口
4. 测试验证
### 阶段二:添加 SiliconFlow
1. 实现 SiliconFlowProvider
2. 添加配置支持
3. 集成测试
### 回滚方案
- 保留原有配置支持
- Feature Flag 控制新逻辑
## Open Questions
1. **Q**: 是否需要支持运行时动态切换供应商?
**A**: 初期不支持,通过配置切换即可
2. **Q**: 是否需要 Provider 健康检查?
**A**: 阶段二考虑添加
3. **Q**: DTO 字段差异如何处理?
**A**: 使用公共字段,扩展字段放 `Map<String, Object> extensions`

View File

@@ -0,0 +1,35 @@
# Change: Refactor Voice Clone Provider
## Why
当前语音克隆功能直接依赖阿里云 CosyVoice 实现,代码强耦合,扩展性差。添加新供应商(如硅基流动 IndexTTS-2需要修改 Service 层代码,违反开闭原则。
**说明**: CosyVoice 是阿里云的语音合成服务DashScope 平台),支持语音克隆和 TTS。当前代码使用 `cosyvoice-v3-flash` 模型。
## What Changes
- **ADDED** 引入策略模式,定义 `VoiceCloneProvider` 统一接口
- **ADDED** 创建工厂类 `VoiceCloneProviderFactory` 管理多供应商
- **MODIFIED** 将现有 `CosyVoiceClient` 改造为 `CosyVoiceProvider`
- **MODIFIED** 更新 `TikUserVoiceServiceImpl` 使用 Provider 接口
- **ADDED** 新增配置类支持多供应商配置和切换
- **BREAKING** 配置项从 `yudao.cosyvoice` 迁移到 `yudao.voice.providers`
## Impact
- **Affected specs**:
- `voice-clone` (新增能力规范)
- **Affected code**:
- `TikUserVoiceServiceImpl.java` - Service 层改为依赖注入 Provider
- `CosyVoiceClient.java``CosyVoiceProvider.java` - 重命名并实现接口
- `CosyVoiceProperties.java``VoiceProviderProperties.java` - 配置结构重组
- 新增 `VoiceCloneProvider.java` - 统一接口定义
- 新增 `VoiceCloneProviderFactory.java` - 工厂类
- 新增 `SiliconFlowProvider.java` - 硅基流动实现(阶段二)
## Migration
- 现有配置自动迁移:`yudao.cosyvoice.*``yudao.voice.providers.cosyvoice.*`
- 默认供应商保持为 `cosyvoice`
- 默认行为保持不变,向后兼容
- 支持通过配置切换供应商:`yudao.voice.default-provider`

View File

@@ -0,0 +1,132 @@
# Voice Clone Capability Specification
## ADDED Requirements
### Requirement: Provider Abstraction Layer
The system SHALL provide a unified provider abstraction layer for voice cloning services, supporting multiple vendors through a common interface.
#### Scenario: Get provider by type
- **GIVEN** the system is configured with multiple voice clone providers
- **WHEN** requesting a provider by type
- **THEN** the system SHALL return the corresponding provider instance
- **AND** the provider SHALL implement the `VoiceCloneProvider` interface
#### Scenario: Provider not found
- **GIVEN** the system is configured with a default provider
- **WHEN** requesting a non-existent provider type
- **THEN** the system SHALL fallback to the default provider
- **AND** log a warning message
### Requirement: Voice Cloning
The system SHALL support voice cloning through the provider interface, accepting an audio file URL and returning a unique voice ID.
#### Scenario: Successful voice cloning with CosyVoice
- **GIVEN** a valid CosyVoice provider is configured
- **WHEN** submitting a voice clone request with audio URL
- **THEN** the system SHALL return a voice ID
- **AND** the voice ID SHALL be usable for subsequent TTS synthesis
#### Scenario: Voice cloning failure
- **GIVEN** the provider API is unavailable or returns an error
- **WHEN** submitting a voice clone request
- **THEN** the system SHALL throw a `VOICE_TTS_FAILED` exception
- **AND** log the error details for debugging
### Requirement: Text-to-Speech Synthesis
The system SHALL support TTS synthesis through cloned voices or system voices, accepting text input and returning audio data.
#### Scenario: TTS with cloned voice
- **GIVEN** a valid voice ID from a previous clone operation
- **WHEN** submitting a TTS request with text and voice ID
- **THEN** the system SHALL return audio data in the specified format
- **AND** the audio SHALL match the cloned voice characteristics
#### Scenario: TTS with system voice
- **GIVEN** a system voice ID is configured
- **WHEN** submitting a TTS request with text and system voice ID
- **THEN** the system SHALL return audio data using the system voice
- **AND** the audio SHALL match the system voice characteristics
#### Scenario: TTS with reference audio (file URL)
- **GIVEN** a reference audio URL and transcription text
- **WHEN** submitting a TTS request with file URL
- **THEN** the system SHALL perform on-the-fly voice cloning
- **AND** return audio data matching the reference voice
### Requirement: Configuration Management
The system SHALL support multi-provider configuration through a unified configuration structure.
#### Scenario: Configure multiple providers
- **GIVEN** the application configuration file
- **WHEN** configuring multiple voice providers
- **THEN** each provider SHALL have independent `enabled` flag
- **AND** the system SHALL only use enabled providers
#### Scenario: Default provider selection
- **GIVEN** the configuration specifies a `default-provider`
- **WHEN** no provider is explicitly specified
- **THEN** the system SHALL use the default provider
- **AND** fallback to `cosyvoice` if default is not configured
#### Scenario: Backward compatibility
- **GIVEN** existing configuration using `yudao.cosyvoice.*`
- **WHEN** the system starts
- **THEN** the system SHALL automatically migrate to new config structure
- **AND** existing functionality SHALL remain unchanged
### Requirement: Provider Factory
The system SHALL provide a factory component for managing provider instances and resolving providers by type.
#### Scenario: Factory resolves provider
- **GIVEN** the factory is initialized with provider configurations
- **WHEN** calling `factory.getProvider("cosyvoice")`
- **THEN** the factory SHALL return the CosyVoiceProvider instance
- **AND** cache the instance for subsequent requests
#### Scenario: Factory returns default
- **GIVEN** the factory is configured with default provider
- **WHEN** calling `factory.getProvider(null)`
- **THEN** the factory SHALL return the default provider instance
## MODIFIED Requirements
### Requirement: Voice Creation Flow
The voice creation process SHALL use the provider abstraction layer instead of directly calling CosyVoice client.
#### Scenario: Create voice with CosyVoice
- **GIVEN** a user uploads a voice audio file
- **WHEN** creating a voice configuration through the API
- **THEN** the system SHALL:
1. Validate the file exists and belongs to voice category
2. Call `provider.cloneVoice()` with the audio URL
3. Store the returned `voiceId` in the database
4. Return success response with voice configuration ID
#### Scenario: Create voice with transcription
- **GIVEN** a voice configuration is created without transcription
- **WHEN** the user triggers transcription
- **THEN** the system SHALL:
1. Fetch the audio file URL
2. Call the transcription service
3. Store the transcription text
4. Update the voice configuration
### Requirement: Voice Preview
The voice preview functionality SHALL work with both cloned voices (voiceId) and reference audio (file URL).
#### Scenario: Preview cloned voice
- **GIVEN** a voice configuration with a valid `voiceId`
- **WHEN** requesting a preview with custom text
- **THEN** the system SHALL call `provider.synthesize()` with the voiceId
- **AND** return audio data in Base64 format
#### Scenario: Preview with reference audio
- **GIVEN** a voice configuration without `voiceId` but with audio file
- **WHEN** requesting a preview
- **THEN** the system SHALL call `provider.synthesize()` with the file URL
- **AND** use the stored transcription as reference text
- **AND** return audio data in Base64 format
## REMOVED Requirements
None. This change is additive and refactoring only.

View File

@@ -0,0 +1,53 @@
# Implementation Tasks
## 1. 接口与基础结构
- [ ] 1.1 创建 `VoiceCloneProvider` 接口
- 定义 `cloneVoice(VoiceCloneRequest)` 方法
- 定义 `synthesize(VoiceTtsRequest)` 方法
- 定义 `supports(String providerType)` 方法
- [ ] 1.2 创建统一 DTO 类
- `VoiceCloneRequest` - 语音克隆请求
- `VoiceCloneResult` - 语音克隆响应
- `VoiceTtsRequest` - 语音合成请求
- `VoiceTtsResult` - 语音合成响应
- [ ] 1.3 创建 `VoiceCloneProviderFactory` 工厂类
- 根据配置获取 Provider 实例
- 支持动态切换供应商
## 2. CosyVoice 重构(保持现有功能)
- [ ] 2.1 重命名 `CosyVoiceClient``CosyVoiceProvider`
- [ ] 2.2 `CosyVoiceProvider` 实现 `VoiceCloneProvider` 接口
- [ ] 2.3 适配现有 DTO 到新的统一 DTO
- [ ] 2.4 保持现有 DashScope SDK 调用逻辑不变
## 3. 配置重构
- [ ] 3.1 创建 `VoiceProviderProperties` 配置类
- 支持多供应商配置结构
- 添加 `default-provider` 配置项
- [ ] 3.2 创建 `CosyVoiceProviderConfig` (嵌套配置)
- [ ] 3.3 保持向后兼容:支持读取旧的 `yudao.cosyvoice.*` 配置
## 4. Service 层改造
- [ ] 4.1 修改 `TikUserVoiceServiceImpl`
- 注入 `VoiceCloneProvider` 而非 `CosyVoiceClient`
- 使用工厂获取 Provider 实例
- [ ] 4.2 更新方法调用
- `createVoice()` - 使用 `provider.cloneVoice()`
- `synthesizeVoice()` - 使用 `provider.synthesize()`
- `previewVoice()` - 使用 `provider.synthesize()`
## 5. 测试与验证
- [ ] 5.1 单元测试CosyVoiceProvider
- [ ] 5.2 单元测试VoiceCloneProviderFactory
- [ ] 5.3 集成测试TikUserVoiceServiceImpl
- [ ] 5.4 验证现有功能正常运行
## 6. 文档与配置迁移
- [ ] 6.1 更新 `application.yaml` 配置示例
- [ ] 6.2 添加配置迁移说明文档
---
**总计**: 20 项任务
**预计工作量**: 2-3 天

View File

@@ -0,0 +1,296 @@
-- Yudao 风格建表语句
-- 包含多租户概念,使用 TenantBaseDO
-- ===============================================
-- 1. 积分管理模块
-- ===============================================
-- 积分兑换配置表
CREATE TABLE `member_point_exchange_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`exchange_rate` int NOT NULL DEFAULT 1 COMMENT '兑换比例(1元兑换多少积分)',
`adjust_reason` varchar(200) NOT NULL DEFAULT '' COMMENT '调整原因',
`operator_id` bigint NOT NULL DEFAULT 0 COMMENT '操作人用户编号',
`operator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人账号',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分兑换配置表';
-- 积分签到配置表
CREATE TABLE `member_point_signin_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`daily_points` int NOT NULL DEFAULT 0 COMMENT '每日签到赠送积分',
`continuous_days` int NOT NULL DEFAULT 0 COMMENT '连续签到天数',
`bonus_points` int NOT NULL DEFAULT 0 COMMENT '连续签到奖励积分',
`reset_days` int NOT NULL DEFAULT 0 COMMENT '重置签到天数(0表示不重置)',
`adjust_reason` varchar(200) NOT NULL DEFAULT '' COMMENT '调整原因',
`operator_id` bigint NOT NULL DEFAULT 0 COMMENT '操作人用户编号',
`operator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人账号',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_continuous_days` (`continuous_days`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分签到配置表';
-- 积分充值配置表
CREATE TABLE `member_point_recharge_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`recharge_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '充值金额',
`bonus_points` int NOT NULL DEFAULT 0 COMMENT '赠送积分数',
`adjust_reason` varchar(200) NOT NULL DEFAULT '' COMMENT '调整原因',
`operator_id` bigint NOT NULL DEFAULT 0 COMMENT '操作人用户编号',
`operator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人账号',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_recharge_amount` (`recharge_amount`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分充值配置表';
-- 积分记录表
CREATE TABLE `member_point_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`type` varchar(20) NOT NULL DEFAULT '' COMMENT '变动类型(increase-增加 decrease-减少)',
`point_amount` int NOT NULL DEFAULT 0 COMMENT '变动积分数量(正数为增加,负数为减少)',
`balance` int NOT NULL DEFAULT 0 COMMENT '变动后余额',
`reason` varchar(100) NOT NULL DEFAULT '' COMMENT '变动原因',
`biz_type` varchar(50) NOT NULL DEFAULT '' COMMENT '业务类型(signin-签到 recharge-充值 exchange-兑换 admin-后台调整 gift-礼包赠送)',
`biz_id` varchar(64) NOT NULL DEFAULT '' COMMENT '业务关联ID',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id_user_id` (`tenant_id`, `user_id`) USING BTREE,
KEY `idx_user_id_create_time` (`user_id`, `create_time`) USING BTREE,
KEY `idx_type` (`type`) USING BTREE,
KEY `idx_biz_type` (`biz_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分记录表';
-- ===============================================
-- 2. 客户管理模块
-- ===============================================
-- 会员用户表
CREATE TABLE `member_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`user_id` varchar(32) NOT NULL COMMENT '用户ID',
`mobile` varchar(20) NOT NULL COMMENT '手机号',
`register_time` datetime NOT NULL COMMENT '注册时间',
`last_login_time` datetime NOT NULL COMMENT '最后登录时间',
`total_points` int NOT NULL DEFAULT 0 COMMENT '账户总积分',
`used_points` int NOT NULL DEFAULT 0 COMMENT '账户消耗积分',
`remaining_points` int NOT NULL DEFAULT 0 COMMENT '账户剩余积分',
`total_storage` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '云空间总容量(GB)',
`used_storage` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '云空间已用容量(GB)',
`remaining_storage` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '云空间剩余容量(GB)',
`total_recharge` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '总充值金额',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_user_id` (`user_id`) USING BTREE,
UNIQUE KEY `uk_mobile` (`mobile`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_register_time` (`register_time`) USING BTREE,
KEY `idx_last_login_time` (`last_login_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员用户表';
-- 充值记录表
CREATE TABLE `member_recharge_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号',
`mobile` varchar(20) NOT NULL COMMENT '手机号',
`recharge_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '充值金额',
`recharge_type` varchar(20) NOT NULL DEFAULT '' COMMENT '充值方式(alipay-支付宝 wechat-微信 admin-人工)',
`order_type` varchar(50) NOT NULL DEFAULT '' COMMENT '订单类型(purchase-权限购买 exchange-积分兑换)',
`permission_type` varchar(100) NOT NULL DEFAULT '' COMMENT '购买权限类型',
`bonus_points` int NOT NULL DEFAULT 0 COMMENT '获得积分',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-失败 1-成功)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id_user_id` (`tenant_id`, `user_id`) USING BTREE,
KEY `idx_user_id_create_time` (`user_id`, `create_time`) USING BTREE,
KEY `idx_recharge_type` (`recharge_type`) USING BTREE,
KEY `idx_order_type` (`order_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值记录表';
-- ===============================================
-- 3. 礼包管理模块
-- ===============================================
-- 礼包表
CREATE TABLE `member_gift_package` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`package_id` varchar(32) NOT NULL COMMENT '礼包ID',
`package_name` varchar(100) NOT NULL COMMENT '礼包名称',
`sort_order` int NOT NULL DEFAULT 0 COMMENT 'C端展示排序',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '购买价格',
`validity_days` int NOT NULL DEFAULT 0 COMMENT '有效期(天)',
`bonus_points` int NOT NULL DEFAULT 0 COMMENT '赠送积分',
`applications` text NOT NULL COMMENT '关联应用(JSON格式)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`operator_id` bigint NOT NULL DEFAULT 0 COMMENT '操作人用户编号',
`operator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人账号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_package_id` (`package_id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_sort_order` (`sort_order`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼包表';
-- ===============================================
-- 4. 模型管理模块
-- ===============================================
-- AI模型表
CREATE TABLE `ai_model` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`model_name` varchar(100) NOT NULL COMMENT '模型名称',
`model_code` varchar(100) NOT NULL COMMENT '模型标识/编码',
`platform` varchar(50) NOT NULL COMMENT '所属平台',
`api_key` varchar(200) NOT NULL COMMENT 'API秘钥',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`temperature` decimal(3,2) NOT NULL DEFAULT 0.70 COMMENT '温度参数',
`max_tokens` int NOT NULL DEFAULT 0 COMMENT '回复数Token数',
`daily_limit` int NOT NULL DEFAULT 0 COMMENT '每日请求次数',
`model_type` varchar(50) NOT NULL COMMENT '模型类型(image-图像 text-文本 video-视频 audio-音频)',
`consume_points` int NOT NULL DEFAULT 0 COMMENT '消耗积分',
`max_text_length` int NOT NULL DEFAULT 0 COMMENT '最大文本数量',
`max_image_size` varchar(50) NOT NULL DEFAULT '' COMMENT '图片最大像素',
`max_video_duration` int NOT NULL DEFAULT 0 COMMENT '视频最大时长(秒)',
`max_video_quality` varchar(20) NOT NULL DEFAULT '' COMMENT '视频最大质量',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_platform` (`platform`) USING BTREE,
KEY `idx_model_type` (`model_type`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI模型表';
-- ===============================================
-- 5. 应用功能管理模块
-- ===============================================
-- 应用功能表
CREATE TABLE `ai_application` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`app_id` varchar(32) NOT NULL COMMENT '应用ID',
`app_name` varchar(100) NOT NULL COMMENT '应用名称',
`api_key` varchar(200) NOT NULL COMMENT '第三方API秘钥',
`consume_points` int NOT NULL DEFAULT 0 COMMENT '单位消耗积分',
`unit_type` varchar(20) NOT NULL COMMENT '消耗单位(time-时长 count-次数)',
`unit_value` varchar(50) NOT NULL COMMENT '单位值(如1min、20次)',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_app_id` (`app_id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_app_name` (`app_name`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应用功能表';
-- ===============================================
-- 6. 智能体配置模块
-- ===============================================
-- 智能体表
CREATE TABLE `ai_agent` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`agent_id` varchar(32) NOT NULL COMMENT '智能体ID',
`agent_name` varchar(100) NOT NULL COMMENT '智能体名称',
`icon` varchar(200) NOT NULL DEFAULT '' COMMENT '图标URL',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-禁用 1-启用)',
`description` text NOT NULL COMMENT '设定描述',
`system_prompt` text NOT NULL COMMENT '预置提示词',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`operator_id` bigint NOT NULL DEFAULT 0 COMMENT '操作人用户编号',
`operator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人账号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`updater` varchar(64) NOT NULL DEFAULT '' COMMENT '更新者',
`deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_agent_id` (`agent_id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_agent_name` (`agent_name`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='智能体表';
-- ===============================================
-- 7. 权限管理表
-- ===============================================
-- 用户权限表
CREATE TABLE `member_user_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号',
`permission_type` varchar(100) NOT NULL COMMENT '权限类型',
`package_id` bigint NOT NULL DEFAULT 0 COMMENT '礼包ID',
`validity_start` datetime NOT NULL COMMENT '有效期开始时间',
`validity_end` datetime NOT NULL COMMENT '有效期结束时间',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0-过期 1-有效)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id_user_id` (`tenant_id`, `user_id`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE,
KEY `idx_package_id` (`package_id`) USING BTREE,
KEY `idx_validity_end` (`validity_end`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户权限表';

View File

@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.tik.voice.client;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneResult;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsResult;
import cn.iocoder.yudao.module.tik.voice.config.CosyVoiceProperties;
import cn.iocoder.yudao.module.tik.voice.config.CosyVoiceProviderConfig;
import cn.iocoder.yudao.module.tik.voice.config.VoiceProviderProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* CosyVoice Provider 实现
*
* <p>阿里云 CosyVoice 语音服务的 Provider 实现。
* 内部委托给 {@link CosyVoiceClient} 进行实际的API调用。
*
* @author 芋道源码
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CosyVoiceProvider implements VoiceCloneProvider {
private final CosyVoiceClient cosyVoiceClient;
/**
* 新配置(支持多供应商)
*/
private final VoiceProviderProperties voiceProviderProperties;
/**
* 旧配置(向后兼容)
*/
private final CosyVoiceProperties cosyVoiceProperties;
/**
* 获取 CosyVoice 配置
* 优先使用新配置,如果不存在则使用旧配置(向后兼容)
*/
private CosyVoiceProviderConfig getConfig() {
// 尝试从新配置获取
var baseConfig = voiceProviderProperties.getProviderConfig("cosyvoice");
if (baseConfig instanceof CosyVoiceProviderConfig cosyConfig) {
return cosyConfig;
}
// 回退到旧配置(向后兼容)
if (cosyVoiceProperties != null && cosyVoiceProperties.isEnabled()) {
return migrateFromLegacyConfig(cosyVoiceProperties);
}
// 返回空配置
return new CosyVoiceProviderConfig();
}
/**
* 从旧配置迁移到新配置格式
*/
private CosyVoiceProviderConfig migrateFromLegacyConfig(CosyVoiceProperties legacy) {
var config = new CosyVoiceProviderConfig();
config.setEnabled(true);
config.setApiKey(legacy.getApiKey());
config.setDefaultModel(legacy.getDefaultModel());
config.setDefaultVoiceId(legacy.getDefaultVoiceId());
config.setSampleRate(legacy.getSampleRate());
config.setAudioFormat(legacy.getAudioFormat());
config.setPreviewText(legacy.getPreviewText());
config.setTtsUrl(legacy.getTtsUrl());
config.setVoiceEnrollmentUrl(legacy.getVoiceEnrollmentUrl());
config.setConnectTimeout(legacy.getConnectTimeout());
config.setReadTimeout(legacy.getReadTimeout());
return config;
}
@Override
public VoiceCloneResult cloneVoice(VoiceCloneRequest request) {
log.info("[CosyVoiceProvider][语音克隆][audioUrl={}, model={}]",
request.getAudioUrl(), request.getModel());
// 适配到 CosyVoiceCloneRequest
cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceCloneRequest cosyRequest =
new cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceCloneRequest();
cosyRequest.setUrl(request.getAudioUrl());
cosyRequest.setTargetModel(request.getModel());
cosyRequest.setPrefix(request.getPrefix());
if (request.getSampleRate() != null) {
cosyRequest.setSampleRate(request.getSampleRate());
}
if (request.getAudioFormat() != null) {
cosyRequest.setAudioFormat(request.getAudioFormat());
}
// 调用底层 Client
cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceCloneResult cosyResult =
cosyVoiceClient.cloneVoice(cosyRequest);
// 适配到统一 Result
VoiceCloneResult result = new VoiceCloneResult();
result.setVoiceId(cosyResult.getVoiceId());
result.setRequestId(cosyResult.getRequestId());
log.info("[CosyVoiceProvider][语音克隆成功][voiceId={}]", result.getVoiceId());
return result;
}
@Override
public VoiceTtsResult synthesize(VoiceTtsRequest request) {
log.info("[CosyVoiceProvider][语音合成][voiceId={}, textLength={}, model={}]",
request.getVoiceId(),
request.getText() != null ? request.getText().length() : 0,
request.getModel());
// 适配到 CosyVoiceTtsRequest
cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceTtsRequest cosyRequest =
cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceTtsRequest.builder()
.text(request.getText())
.voiceId(request.getVoiceId())
.fileUrl(request.getFileUrl())
.referenceText(request.getReferenceText())
.model(request.getModel())
.speechRate(request.getSpeechRate())
.volume(request.getVolume())
.instruction(request.getInstruction())
.sampleRate(request.getSampleRate())
.audioFormat(request.getAudioFormat())
.preview(request.isPreview())
.build();
// 调用底层 Client
cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceTtsResult cosyResult =
cosyVoiceClient.synthesize(cosyRequest);
// 适配到统一 Result
VoiceTtsResult result = new VoiceTtsResult();
result.setRequestId(cosyResult.getRequestId());
result.setFormat(cosyResult.getFormat());
result.setSampleRate(cosyResult.getSampleRate());
result.setAudio(cosyResult.getAudio());
result.setVoiceId(cosyResult.getVoiceId());
log.info("[CosyVoiceProvider][语音合成成功][format={}, audioSize={}]",
result.getFormat(), result.getAudio() != null ? result.getAudio().length : 0);
return result;
}
@Override
public boolean supports(String providerType) {
return "cosyvoice".equalsIgnoreCase(providerType);
}
@Override
public String getProviderType() {
return "cosyvoice";
}
}

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.tik.voice.client;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneResult;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsResult;
/**
* 语音克隆 Provider 统一接口
*
* <p>支持多供应商实现的语音克隆和语音合成服务。
* 通过工厂类 {@link VoiceCloneProviderFactory} 获取具体实现。
*
* @author 芋道源码
*/
public interface VoiceCloneProvider {
/**
* 语音克隆
*
* <p>根据提供的音频文件URL克隆目标音色。
* 不同供应商的实现细节被此接口屏蔽。
*
* @param request 语音克隆请求
* @return 语音克隆结果,包含生成的 voiceId
* @throws RuntimeException 当克隆失败时抛出
*/
VoiceCloneResult cloneVoice(VoiceCloneRequest request);
/**
* 文本转语音合成
*
* <p>将文本转换为语音,支持使用已克隆的音色或系统音色。
*
* @param request 语音合成请求
* @return 语音合成结果,包含音频数据
* @throws RuntimeException 当合成失败时抛出
*/
VoiceTtsResult synthesize(VoiceTtsRequest request);
/**
* 检查是否支持指定的供应商类型
*
* @param providerType 供应商类型(如 "cosyvoice", "siliconflow"
* @return true 如果支持false 否则
*/
boolean supports(String providerType);
/**
* 获取供应商类型标识
*
* @return 供应商类型,如 "cosyvoice", "siliconflow"
*/
String getProviderType();
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.tik.voice.client;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.VOICE_TTS_FAILED;
/**
* 语音克隆 Provider 工厂
*
* <p>负责管理和获取不同的语音克隆 Provider 实现。
* 支持多供应商配置和动态切换。
*
* @author 芋道源码
*/
@Slf4j
@Component
public class VoiceCloneProviderFactory {
private final Map<String, VoiceCloneProvider> providers = new ConcurrentHashMap<>();
@Autowired
public VoiceCloneProviderFactory(List<VoiceCloneProvider> providerList) {
// 自动注册所有 Provider 实现类
for (VoiceCloneProvider provider : providerList) {
registerProvider(provider);
log.info("[VoiceCloneProviderFactory][注册Provider][type={}]", provider.getProviderType());
}
}
/**
* 注册 Provider
*
* @param provider Provider 实例
*/
public void registerProvider(VoiceCloneProvider provider) {
String type = provider.getProviderType();
if (providers.containsKey(type)) {
log.warn("[VoiceCloneProviderFactory][Provider已存在覆盖][type={}]", type);
}
providers.put(type, provider);
}
/**
* 获取默认 Provider
*
* @return 默认的 Provider 实例
* @throws ServiceException 当没有可用的 Provider 时抛出
*/
public VoiceCloneProvider getDefaultProvider() {
if (providers.isEmpty()) {
throw exception0(VOICE_TTS_FAILED.getCode(), "未配置任何语音克隆 Provider");
}
// 返回第一个注册的 Provider 作为默认
return providers.values().iterator().next();
}
/**
* 根据类型获取 Provider
*
* @param providerType 供应商类型(如 "cosyvoice", "siliconflow"
* @return 对应的 Provider 实例
* @throws ServiceException 当 Provider 不存在时抛出
*/
public VoiceCloneProvider getProvider(String providerType) {
if (providerType == null || providerType.trim().isEmpty()) {
return getDefaultProvider();
}
VoiceCloneProvider provider = providers.get(providerType);
if (provider == null) {
throw exception0(VOICE_TTS_FAILED.getCode(), "不支持的语音克隆供应商: " + providerType);
}
return provider;
}
/**
* 检查是否支持指定的供应商类型
*
* @param providerType 供应商类型
* @return true 如果支持false 否则
*/
public boolean hasProvider(String providerType) {
return providerType != null && providers.containsKey(providerType);
}
/**
* 获取所有已注册的 Provider 类型
*
* @return 供应商类型列表
*/
public List<String> getAvailableProviderTypes() {
return List.copyOf(providers.keySet());
}
}

View File

@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.tik.voice.client.dto;
import lombok.Data;
/**
* 语音克隆请求统一DTO
*
* <p>屏蔽不同供应商API差异提供统一的请求结构。
* 各Provider实现负责将此DTO转换为供应商特定格式。
*
* @author 芋道源码
*/
@Data
public class VoiceCloneRequest {
/**
* 音频文件公网URL
*
* <p>CosyVoice: 对应 {@code url} 字段</p>
* <p>SiliconFlow: 对应 {@code audio} 字段需base64编码</p>
*/
private String audioUrl;
/**
* 模型名称
*
* <p>CosyVoice: 对应 {@code targetModel},如 {@code cosyvoice-v3-flash}</p>
* <p>SiliconFlow: 对应 {@code model},如 {@code indextts-2}</p>
*/
private String model;
/**
* 音色自定义前缀(可选)
*
* <p>CosyVoice: 必填,仅允许数字和小写字母,长度<10字符</p>
* <p>SiliconFlow: 不适用</p>
*/
private String prefix;
/**
* 采样率默认24000
*/
private Integer sampleRate;
/**
* 音频格式默认mp3
*
* <p>可选值: mp3, wav, flac</p>
*/
private String audioFormat;
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.tik.voice.client.dto;
import lombok.Data;
/**
* 语音克隆结果统一DTO
*
* @author 芋道源码
*/
@Data
public class VoiceCloneResult {
/**
* 生成的音色ID
*
* <p>后续TTS合成时使用此ID</p>
*/
private String voiceId;
/**
* 请求ID用于追踪
*/
private String requestId;
}

View File

@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.tik.voice.client.dto;
import lombok.Builder;
import lombok.Data;
/**
* 文本转语音请求统一DTO
*
* <p>屏蔽不同供应商API差异提供统一的请求结构。
*
* @author 芋道源码
*/
@Data
@Builder
public class VoiceTtsRequest {
/**
* 待合成文本
*/
private String text;
/**
* 音色ID可选默认使用配置
*
* <p>使用语音克隆生成的voiceId</p>
*/
private String voiceId;
/**
* 语音文件URL当使用语音URL合成时使用替代voiceId
*
* <p>用于实时语音克隆,无需提前克隆</p>
*/
private String fileUrl;
/**
* 参考音频文本当使用fileUrl时用于提高克隆质量
*/
private String referenceText;
/**
* 模型(默认使用供应商默认模型)
*/
private String model;
/**
* 语速0.5 - 2.0默认1.0
*/
private Float speechRate;
/**
* 音量(-100 - 100默认0
*/
private Float volume;
/**
* 指令(用于控制音色风格),可选
*/
private String instruction;
/**
* 采样率默认24000
*/
private Integer sampleRate;
/**
* 音频格式默认mp3
*
* <p>可选值: mp3, wav, flac</p>
*/
private String audioFormat;
/**
* 是否仅用于试听(方便服务侧做限流)
*/
private boolean preview;
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.tik.voice.client.dto;
import lombok.Data;
/**
* 文本转语音结果统一DTO
*
* @author 芋道源码
*/
@Data
public class VoiceTtsResult {
/**
* 请求ID用于追踪
*/
private String requestId;
/**
* 返回的音频格式
*
* <p>mp3, wav, flac 等</p>
*/
private String format;
/**
* 采样率
*/
private Integer sampleRate;
/**
* 音频二进制内容
*/
private byte[] audio;
/**
* 音频所使用的 voiceId
*/
private String voiceId;
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.tik.voice.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.Duration;
/**
* CosyVoice 供应商配置
*
* <p>继承通用配置,添加 CosyVoice 特有字段。
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CosyVoiceProviderConfig extends VoiceProviderProperties.ProviderConfig {
/**
* 默认模型
*/
private String defaultModel = "cosyvoice-v3-flash";
/**
* 默认 voiceId可选
*/
private String defaultVoiceId;
/**
* 默认采样率
*/
private Integer sampleRate = 24000;
/**
* 默认音频格式
*/
private String audioFormat = "mp3";
/**
* 试听默认示例文本
*/
private String previewText = "您好,欢迎体验专属音色。";
/**
* TTS 接口地址
*/
private String ttsUrl = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/speech-synthesis";
/**
* 语音复刻接口地址(声音注册)
*/
private String voiceEnrollmentUrl = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/voice-enrollment";
/**
* 连接超时时间
*/
private Duration connectTimeout = Duration.ofSeconds(10);
/**
* 读取超时时间3分钟提升语音合成成功率
*/
private Duration readTimeout = Duration.ofSeconds(180);
}

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.tik.voice.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 语音 Provider 统一配置
*
* <p>支持多供应商配置,默认供应商选择。
*
* @author 芋道源码
*/
@Data
@Component
@ConfigurationProperties(prefix = "yudao.voice")
public class VoiceProviderProperties {
/**
* 默认供应商类型
*
* <p>可选值: cosyvoice, siliconflow 等
*/
private String defaultProvider = "cosyvoice";
/**
* 各供应商配置
*
* <p>key 为供应商类型(如 cosyvoice, siliconflow
*/
private Map<String, ProviderConfig> providers = new HashMap<>();
/**
* 供应商通用配置基类
*/
@Data
public static class ProviderConfig {
/**
* 是否启用
*/
private boolean enabled = true;
/**
* API Key
*/
private String apiKey;
/**
* 优先级(数字越小优先级越高,用于故障转移)
*/
private Integer priority = 100;
}
/**
* 获取指定供应商配置
*
* @param providerType 供应商类型
* @return 配置对象,不存在返回 null
*/
public ProviderConfig getProviderConfig(String providerType) {
return providers.get(providerType);
}
/**
* 检查供应商是否启用
*
* @param providerType 供应商类型
* @return true 如果启用且配置存在
*/
public boolean isProviderEnabled(String providerType) {
ProviderConfig config = getProviderConfig(providerType);
return config != null && config.isEnabled();
}
}

View File

@@ -19,12 +19,14 @@ import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
import cn.iocoder.yudao.module.tik.file.service.TikUserFileService;
import cn.iocoder.yudao.module.tik.tikhup.service.TikHupService;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.voice.client.CosyVoiceClient;
import cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceCloneRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceCloneResult;
import cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceTtsRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.CosyVoiceTtsResult;
import cn.iocoder.yudao.module.tik.voice.client.VoiceCloneProvider;
import cn.iocoder.yudao.module.tik.voice.client.VoiceCloneProviderFactory;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceCloneResult;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsRequest;
import cn.iocoder.yudao.module.tik.voice.client.dto.VoiceTtsResult;
import cn.iocoder.yudao.module.tik.voice.config.CosyVoiceProperties;
import cn.iocoder.yudao.module.tik.voice.config.VoiceProviderProperties;
import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikUserVoiceDO;
import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikUserVoiceMapper;
import cn.iocoder.yudao.module.tik.voice.vo.AppTikUserVoiceCreateReqVO;
@@ -84,11 +86,14 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
private TikHupService tikHupService;
@Resource
private CosyVoiceClient cosyVoiceClient;
private VoiceCloneProviderFactory voiceProviderFactory;
@Resource
private CosyVoiceProperties cosyVoiceProperties;
@Resource
private VoiceProviderProperties voiceProviderProperties;
@Resource
private StringRedisTemplate stringRedisTemplate;
@@ -139,17 +144,20 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
.setTranscription(null); // 初始为空,表示未识别
voiceMapper.insert(voice);
// 4. 调用阿里云语音复刻服务,生成 voice_id
// 4. 调用语音克隆服务,生成 voice_id
try {
log.info("[createVoice][开始语音复刻,配音编号({})文件ID({})]", voice.getId(), fileDO.getId());
log.info("[createVoice][开始语音复刻,配音编号({})文件ID({}),供应商({})]",
voice.getId(), fileDO.getId(), createReqVO.getProviderType());
String fileAccessUrl = fileApi.presignGetUrl(fileDO.getUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
CosyVoiceCloneRequest cloneRequest = new CosyVoiceCloneRequest();
cloneRequest.setTargetModel("cosyvoice-v3-flash"); // 使用v3-flash模型
// 使用 Provider 接口(支持前端选择供应商,不传则使用默认)
VoiceCloneProvider provider = voiceProviderFactory.getProvider(createReqVO.getProviderType());
VoiceCloneRequest cloneRequest = new VoiceCloneRequest();
cloneRequest.setAudioUrl(fileAccessUrl);
cloneRequest.setModel("cosyvoice-v3-flash"); // 使用v3-flash模型
cloneRequest.setPrefix("voice" + voice.getId()); // 音色前缀,格式要求
cloneRequest.setUrl(fileAccessUrl);
CosyVoiceCloneResult cloneResult = cosyVoiceClient.cloneVoice(cloneRequest);
VoiceCloneResult cloneResult = provider.cloneVoice(cloneRequest);
String voiceId = cloneResult.getVoiceId();
// 更新配音记录,保存 voice_id
@@ -432,22 +440,26 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
return buildSynthResponseFromCache(reqVO, synthCache);
}
CosyVoiceTtsResult ttsResult = cosyVoiceClient.synthesize(buildTtsRequest(
finalText,
voiceId,
fileUrl,
transcriptionText,
reqVO.getModel(),
reqVO.getSpeechRate(),
reqVO.getVolume(),
reqVO.getInstruction(),
reqVO.getSampleRate(),
reqVO.getAudioFormat(),
false
));
// 使用 Provider 接口进行 TTS 合成(支持前端选择供应商,不传则使用默认)
VoiceCloneProvider provider = voiceProviderFactory.getProvider(reqVO.getProviderType());
VoiceTtsRequest ttsRequest = VoiceTtsRequest.builder()
.text(finalText)
.voiceId(voiceId)
.fileUrl(fileUrl)
.referenceText(transcriptionText)
.model(reqVO.getModel())
.speechRate(reqVO.getSpeechRate())
.volume(reqVO.getVolume())
.instruction(reqVO.getInstruction())
.sampleRate(reqVO.getSampleRate())
.audioFormat(reqVO.getAudioFormat())
.preview(false)
.build();
VoiceTtsResult ttsResult = provider.synthesize(ttsRequest);
String format = defaultFormat(ttsResult.getFormat(), reqVO.getAudioFormat());
String finalVoiceId = StrUtil.blankToDefault(voiceId, cosyVoiceProperties.getDefaultVoiceId());
String finalVoiceId = StrUtil.blankToDefault(voiceId, getDefaultVoiceId());
// 【安全方案】不暴露OSS链接直接返回Base64编码的音频数据
String audioBase64 = Base64.getEncoder().encodeToString(ttsResult.getAudio());
@@ -527,7 +539,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
voiceId = voice.getVoiceId();
// 注意:使用 voiceId 时,不依赖 transcriptionText直接使用前端传入的 inputText
transcriptionText = null; // 清除 transcriptionText
inputText = StrUtil.blankToDefault(reqVO.getInputText(), cosyVoiceProperties.getPreviewText());
inputText = StrUtil.blankToDefault(reqVO.getInputText(), getPreviewText());
} else {
log.info("[previewVoice][使用文件URL试听配音编号({})]", voiceConfigId);
// 获取文件信息用于获取文件URL
@@ -543,17 +555,17 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
throw exception(VOICE_NOT_EXISTS, "配音识别文本为空,请先进行语音识别");
}
inputText = StrUtil.blankToDefault(reqVO.getInputText(),
StrUtil.blankToDefault(transcriptionText, cosyVoiceProperties.getPreviewText()));
StrUtil.blankToDefault(transcriptionText, getPreviewText()));
}
}
// 3. 如果没有配置ID使用系统配音配置需要前端传voiceId
else {
log.info("[previewVoice][开始试听,使用系统配音配置,用户({})]", userId);
voiceId = StrUtil.blankToDefault(reqVO.getVoiceId(), cosyVoiceProperties.getDefaultVoiceId());
voiceId = StrUtil.blankToDefault(reqVO.getVoiceId(), getDefaultVoiceId());
if (StrUtil.isBlank(voiceId)) {
throw exception(VOICE_NOT_EXISTS, "系统配音音色ID不能为空");
}
inputText = StrUtil.blankToDefault(reqVO.getInputText(), cosyVoiceProperties.getPreviewText());
inputText = StrUtil.blankToDefault(reqVO.getInputText(), getPreviewText());
}
String finalText = determineSynthesisText(
@@ -588,21 +600,26 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
return buildPreviewResp(cachedBase64, previewCache.getFormat(), voiceId);
}
log.info("[previewVoice][调用CosyVoice合成,配音编号({})voiceId({})fileUrl({}),文本长度({})]",
voiceConfigId, voiceId, fileUrl, finalText.length());
CosyVoiceTtsResult ttsResult = cosyVoiceClient.synthesize(buildTtsRequest(
finalText,
voiceId,
fileUrl,
transcriptionText, // 参考音频文本,用于提高克隆质量
null, // 使用默认模型
speechRate,
volume,
instruction,
null,
audioFormat,
true
));
log.info("[previewVoice][调用语音合成服务,配音编号({})voiceId({})fileUrl({}),文本长度({}),供应商({})]",
voiceConfigId, voiceId, fileUrl, finalText.length(), reqVO.getProviderType());
// 使用 Provider 接口进行 TTS 合成(支持前端选择供应商,不传则使用默认)
VoiceCloneProvider provider = voiceProviderFactory.getProvider(reqVO.getProviderType());
VoiceTtsRequest ttsRequest = VoiceTtsRequest.builder()
.text(finalText)
.voiceId(voiceId)
.fileUrl(fileUrl)
.referenceText(transcriptionText)
.model(null) // 使用默认模型
.speechRate(speechRate)
.volume(volume)
.instruction(instruction)
.sampleRate(null)
.audioFormat(audioFormat)
.preview(true)
.build();
VoiceTtsResult ttsResult = provider.synthesize(ttsRequest);
String format = defaultFormat(ttsResult.getFormat(), audioFormat);
String identifier = StrUtil.isNotBlank(voiceId) ? voiceId : "voice";
@@ -622,35 +639,53 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
return buildPreviewResp(audioBase64, format, voiceId);
}
private CosyVoiceTtsRequest buildTtsRequest(String text,
String voiceId,
String fileUrl,
String referenceText,
String model,
Float speechRate,
Float volume,
String instruction,
Integer sampleRate,
String audioFormat,
boolean preview) {
return CosyVoiceTtsRequest.builder()
.text(text)
.voiceId(voiceId)
.fileUrl(fileUrl)
.referenceText(referenceText)
.model(model)
.speechRate(speechRate)
.volume(volume)
.instruction(instruction)
.sampleRate(sampleRate)
.audioFormat(audioFormat)
.preview(preview)
.build();
/**
* 获取 CosyVoice 配置(统一入口)
* 优先使用新配置,回退到旧配置
*/
private cn.iocoder.yudao.module.tik.voice.config.CosyVoiceProviderConfig getCosyVoiceConfig() {
if (voiceProviderProperties != null) {
var config = voiceProviderProperties.getProviderConfig("cosyvoice");
if (config instanceof cn.iocoder.yudao.module.tik.voice.config.CosyVoiceProviderConfig cosyConfig) {
return cosyConfig;
}
}
return null;
}
/**
* 获取默认音频格式
* 优先使用新配置,回退到旧配置
*/
private String getDefaultFormat() {
var config = getCosyVoiceConfig();
if (config != null) {
return config.getAudioFormat();
}
if (cosyVoiceProperties != null) {
return cosyVoiceProperties.getAudioFormat();
}
return "mp3";
}
/**
* 获取默认采样率
* 优先使用新配置,回退到旧配置
*/
private Integer getDefaultSampleRate() {
var config = getCosyVoiceConfig();
if (config != null) {
return config.getSampleRate();
}
if (cosyVoiceProperties != null) {
return cosyVoiceProperties.getSampleRate();
}
return 24000;
}
private String defaultFormat(String responseFormat, String requestFormat) {
return StrUtil.blankToDefault(responseFormat,
StrUtil.blankToDefault(requestFormat, cosyVoiceProperties.getAudioFormat()));
StrUtil.blankToDefault(requestFormat, getDefaultFormat()));
}
private String buildFileName(String voiceId, String format) {
@@ -687,7 +722,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
return builder.toString();
}
if (allowFallback) {
return cosyVoiceProperties.getPreviewText();
return getPreviewText();
}
throw exception(VOICE_TTS_FAILED, "请提供需要合成的文本内容");
}
@@ -750,15 +785,19 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
} else {
identifier = "no-voice";
}
// 获取默认配置
String defaultFormat = getDefaultFormat();
Integer defaultSampleRate = getDefaultSampleRate();
String payload = StrUtil.join("|",
identifier,
text,
speechRate != null ? speechRate : "1.0",
volume != null ? volume : "0",
instruction,
StrUtil.blankToDefault(audioFormat, cosyVoiceProperties.getAudioFormat()),
sampleRate != null ? sampleRate : cosyVoiceProperties.getSampleRate());
StrUtil.blankToDefault(audioFormat, defaultFormat),
sampleRate != null ? sampleRate : defaultSampleRate);
String hash = cn.hutool.crypto.SecureUtil.sha256(payload);
return prefix + hash;
}
@@ -1123,5 +1162,35 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
builder.append(normalized);
}
/**
* 获取默认音色ID
* 优先使用新配置,回退到旧配置
*/
private String getDefaultVoiceId() {
var config = getCosyVoiceConfig();
if (config != null) {
return config.getDefaultVoiceId();
}
if (cosyVoiceProperties != null) {
return cosyVoiceProperties.getDefaultVoiceId();
}
return null;
}
/**
* 获取试听文本
* 优先使用新配置,回退到旧配置
*/
private String getPreviewText() {
var config = getCosyVoiceConfig();
if (config != null) {
return config.getPreviewText();
}
if (cosyVoiceProperties != null) {
return cosyVoiceProperties.getPreviewText();
}
return "您好,欢迎体验专属音色。";
}
}

View File

@@ -34,5 +34,7 @@ public class AppTikUserVoiceCreateReqVO {
@Schema(description = "备注", example = "这是一个测试配音")
private String note;
}
@Schema(description = "供应商类型cosyvoice-阿里云siliconflow-硅基流动(不传则使用默认)", example = "cosyvoice")
private String providerType;
}

View File

@@ -42,6 +42,8 @@ public class AppTikVoicePreviewReqVO {
@Schema(description = "指令(用于控制音色风格)", example = "请用温柔专业的语调朗读")
private String instruction;
@Schema(description = "供应商类型cosyvoice-阿里云siliconflow-硅基流动(不传则使用默认)", example = "cosyvoice")
private String providerType;
}

View File

@@ -44,6 +44,8 @@ public class AppTikVoiceTtsReqVO {
@Schema(description = "音频格式,默认 wav可选 mp3")
private String audioFormat;
@Schema(description = "供应商类型cosyvoice-阿里云siliconflow-硅基流动(不传则使用默认)", example = "cosyvoice")
private String providerType;
}