feat: 支持音色切换
This commit is contained in:
@@ -12,7 +12,7 @@ export interface Message {
|
||||
role?: string;
|
||||
usage?: UsageInfo;
|
||||
id?: string;
|
||||
type?: 'chat' | 'voice';
|
||||
type?: "chat" | "voice";
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
@@ -32,3 +32,97 @@ export interface UsageInfo {
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaker 语音合成器基本信息
|
||||
*/
|
||||
export interface Speaker {
|
||||
/** speaker唯一标识ID */
|
||||
speaker_id: string;
|
||||
/** speaker显示名称 */
|
||||
speaker_name: string;
|
||||
/** 支持的语言/口音 */
|
||||
language: string;
|
||||
/** 支持的平台列表 */
|
||||
platforms: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaker分类信息
|
||||
*/
|
||||
export interface CategorySpeakers {
|
||||
/** 分类名称 */
|
||||
category: string;
|
||||
/** 该分类下的speaker列表 */
|
||||
speakers: Speaker[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaker分类枚举
|
||||
*/
|
||||
export enum SpeakerCategory {
|
||||
/** 趣味口音 */
|
||||
ACCENT = "趣味口音",
|
||||
/** 角色扮演 */
|
||||
ROLE_PLAY = "角色扮演"
|
||||
}
|
||||
|
||||
/**
|
||||
* 常用平台枚举
|
||||
*/
|
||||
export enum SpeakerPlatform {
|
||||
DOUYIN = "抖音",
|
||||
DOUBAO = "豆包",
|
||||
CICI = "Cici",
|
||||
JIANYING = "剪映",
|
||||
JIANYING_C = "剪映C端",
|
||||
WEB_DEMO = "web demo",
|
||||
STORY_AI = "StoryAi",
|
||||
MAOXIANG = "猫箱"
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaker选择器组件Props
|
||||
*/
|
||||
export interface SpeakerSelectorProps {
|
||||
/** 当前选中的speaker */
|
||||
selectedSpeaker?: Speaker;
|
||||
/** speaker选择回调 */
|
||||
onSpeakerChange: (speaker: Speaker) => void;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 过滤特定分类 */
|
||||
filterCategories?: SpeakerCategory[];
|
||||
/** 过滤特定平台 */
|
||||
filterPlatforms?: SpeakerPlatform[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音合成参数
|
||||
*/
|
||||
export interface VoiceSynthesisParams {
|
||||
/** 使用的speaker */
|
||||
speaker: Speaker;
|
||||
/** 要合成的文本 */
|
||||
text: string;
|
||||
/** 语速 (0.5-2.0) */
|
||||
speed?: number;
|
||||
/** 音调 (0.5-2.0) */
|
||||
pitch?: number;
|
||||
/** 音量 (0.0-1.0) */
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音合成响应
|
||||
*/
|
||||
export interface VoiceSynthesisResponse {
|
||||
/** 音频文件URL */
|
||||
audio_url: string;
|
||||
/** 音频时长(秒) */
|
||||
duration: number;
|
||||
/** 合成状态 */
|
||||
status: "success" | "error";
|
||||
/** 错误信息 */
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
@@ -137,4 +137,9 @@ export class ChatService {
|
||||
public static GetModelList(config?: AxiosRequestConfig<any>) {
|
||||
return BaseClientService.get(`${this.basePath}/model/list`, config);
|
||||
}
|
||||
|
||||
// 获取音色列表
|
||||
public static GetSpeakerList(config?: AxiosRequestConfig<any>) {
|
||||
return BaseClientService.get(`${this.basePath}/speaker/list`, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useWebSocketStore } from "@/services";
|
||||
import { convertToPCM16 } from "@/utils";
|
||||
import { useChatStore } from "./chat_store";
|
||||
|
||||
export const useAsrStore = defineStore("asr", () => {
|
||||
// 是否正在录音
|
||||
@@ -125,6 +126,7 @@ export const useAsrStore = defineStore("asr", () => {
|
||||
if (router.currentRoute.value.path === "/voice") {
|
||||
msg.messageId = messageId;
|
||||
msg.voiceConversation = true;
|
||||
msg.speaker = useChatStore().speakerInfo?.speaker_id;
|
||||
}
|
||||
sendMessage(JSON.stringify(msg));
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type {
|
||||
CategorySpeakers,
|
||||
IChatWithLLMRequest,
|
||||
ModelInfo,
|
||||
ModelListInfo,
|
||||
Speaker,
|
||||
UsageInfo
|
||||
} from "@/interfaces";
|
||||
import { ChatService } from "@/services";
|
||||
@@ -20,6 +22,10 @@ export const useChatStore = defineStore("chat", () => {
|
||||
const thinking = ref<boolean>(false);
|
||||
// 模型列表
|
||||
const modelList = ref<ModelListInfo[]>([]);
|
||||
// 音色列表
|
||||
const speakerList = ref<CategorySpeakers[]>([]);
|
||||
// 当前音色信息
|
||||
const speakerInfo = ref<Speaker | null>(null);
|
||||
// 在线人数
|
||||
const onlineCount = ref<number>(0);
|
||||
|
||||
@@ -151,6 +157,16 @@ export const useChatStore = defineStore("chat", () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取音色列表
|
||||
const getSpeakerList = async () => {
|
||||
try {
|
||||
const response = await ChatService.GetSpeakerList();
|
||||
speakerList.value = response.data.data;
|
||||
} catch (error) {
|
||||
console.error("获取音色·列表失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
completing,
|
||||
@@ -162,6 +178,9 @@ export const useChatStore = defineStore("chat", () => {
|
||||
addMessageToHistory,
|
||||
clearHistoryMessages,
|
||||
getModelList,
|
||||
onlineCount
|
||||
onlineCount,
|
||||
speakerList,
|
||||
getSpeakerList,
|
||||
speakerInfo
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAudioWebSocket } from "@/services";
|
||||
import { createAudioUrl, mergeAudioChunks } from "@/utils";
|
||||
import { useChatStore } from "./chat_store";
|
||||
|
||||
interface AudioState {
|
||||
isPlaying: boolean;
|
||||
@@ -12,6 +13,7 @@ interface AudioState {
|
||||
}
|
||||
|
||||
export const useTtsStore = defineStore("tts", () => {
|
||||
const chatStore = useChatStore();
|
||||
// 多音频状态管理 - 以消息ID为key
|
||||
const audioStates = ref<Map<string, AudioState>>(new Map());
|
||||
|
||||
@@ -65,7 +67,14 @@ export const useTtsStore = defineStore("tts", () => {
|
||||
hasActiveSession.value = true;
|
||||
|
||||
// 发送文本到TTS服务
|
||||
sendMessage(JSON.stringify({ type: "tts_text", text, messageId }));
|
||||
sendMessage(
|
||||
JSON.stringify({
|
||||
type: "tts_text",
|
||||
text,
|
||||
messageId,
|
||||
speaker: chatStore.speakerInfo?.speaker_id
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(`连接失败: ${error}`, messageId);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import markdown from "@/components/markdown.vue";
|
||||
import { useAsrStore, useChatStore, useLayoutStore } from "@/stores";
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { historyMessages, completing, modelList, modelInfo, thinking } =
|
||||
const { historyMessages, completing, speakerList, speakerInfo, thinking } =
|
||||
storeToRefs(chatStore);
|
||||
const asrStore = useAsrStore();
|
||||
const { isRecording } = storeToRefs(asrStore);
|
||||
@@ -58,39 +58,43 @@ const handleItemHeaderClick = (name: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选中模型的 ID
|
||||
const selectedModelId = computed({
|
||||
get: () => modelInfo.value?.model_id ?? null,
|
||||
// 处理选中speaker的 ID
|
||||
const selectedSpeakerId = computed({
|
||||
get: () => speakerInfo.value?.speaker_id ?? null,
|
||||
set: (id: string | null) => {
|
||||
for (const vendor of modelList.value) {
|
||||
const found = vendor.models.find((model) => model.model_id === id);
|
||||
for (const category of speakerList.value) {
|
||||
const found = category.speakers.find(
|
||||
(speaker) => speaker.speaker_id === id
|
||||
);
|
||||
if (found) {
|
||||
modelInfo.value = found;
|
||||
speakerInfo.value = found;
|
||||
return;
|
||||
}
|
||||
}
|
||||
modelInfo.value = null;
|
||||
speakerInfo.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听模型列表变化,更新选项
|
||||
// 监听speaker列表变化,更新选项
|
||||
watch(
|
||||
() => modelList.value,
|
||||
() => speakerList.value,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
options.value = newVal.map((vendor) => ({
|
||||
options.value = newVal.map((category) => ({
|
||||
type: "group",
|
||||
label: vendor.vendor,
|
||||
key: vendor.vendor,
|
||||
children: vendor.models.map((model) => ({
|
||||
label: model.model_name,
|
||||
value: model.model_id,
|
||||
type: model.model_type
|
||||
label: category.category,
|
||||
key: category.category,
|
||||
children: category.speakers.map((speaker) => ({
|
||||
label: speaker.speaker_name,
|
||||
value: speaker.speaker_id,
|
||||
language: speaker.language,
|
||||
platforms: speaker.platforms
|
||||
}))
|
||||
}));
|
||||
|
||||
if (newVal.length > 0 && newVal[0].models.length > 0) {
|
||||
modelInfo.value = newVal[0].models[0];
|
||||
// 默认选择第一个speaker
|
||||
if (newVal.length > 0 && newVal[0].speakers.length > 0) {
|
||||
speakerInfo.value = newVal[0].speakers[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -115,7 +119,7 @@ watch(completing, (newVal) => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
chatStore.getModelList();
|
||||
chatStore.getSpeakerList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -207,7 +211,7 @@ onMounted(() => {
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<NSelect
|
||||
v-model:value="selectedModelId"
|
||||
v-model:value="selectedSpeakerId"
|
||||
label-field="label"
|
||||
value-field="value"
|
||||
children-field="children"
|
||||
|
||||
Reference in New Issue
Block a user