Skip to content

Commit e2ff5aa

Browse files
committed
feat: text to speech node
1 parent 3a55c3e commit e2ff5aa

4 files changed

Lines changed: 148 additions & 0 deletions

File tree

ui/src/workflow-canvas/component/NodeMenu.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const workflowComponentGroups = [
2828
list: [
2929
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: 'AI 对话', value: WorkflowNodeType.AiChat },
3030
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '意图识别', value: WorkflowNodeType.IntentNode },
31+
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '文本转语音', value: WorkflowNodeType.TextToSpeechNode },
3132
],
3233
},
3334
{
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<el-avatar class="avatar-gradient" shape="square">
3+
<img src="@/assets/workflow/icon_ai_chat.svg" style="width: 75%" alt="" />
4+
</el-avatar>
5+
</template>
6+
<script setup lang="ts"></script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import TextToSpeechNodeVue from './index.vue'
2+
import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node'
3+
import { WorkflowNodeType } from '@/workflow-canvas/types.ts'
4+
5+
class TextToSpeechNodeView extends WorkflowNodeView {
6+
constructor(props: ConstructorParameters<typeof WorkflowNodeView>[0]) {
7+
super(props, TextToSpeechNodeVue)
8+
}
9+
}
10+
11+
export default {
12+
type: WorkflowNodeType.TextToSpeechNode,
13+
model: WorkflowNodeModel,
14+
view: TextToSpeechNodeView,
15+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<script setup lang="ts">
2+
import { computed, inject, onMounted, ref, useTemplateRef } from 'vue'
3+
import { set } from 'lodash'
4+
import type { FormInstance } from 'element-plus'
5+
import ModelSelect from '@/components/business/model-select/index.vue'
6+
import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue'
7+
import NodeContainer from '@/workflow-canvas/core/NodeContainer.vue'
8+
import { useWorkflowStore } from '@/workflow-canvas/store'
9+
import type { BaseNodeModel } from '@logicflow/core'
10+
import type { ModelItem, ModelProviderItem } from '@/api/types'
11+
12+
defineOptions({ name: 'WorkflowTextToSpeechNode' })
13+
const getModel = inject('getModel') as () => BaseNodeModel
14+
const apiType = (inject('apiType') as string) || 'workspace'
15+
const model = getModel()
16+
17+
interface TextToSpeechNodeForm {
18+
tts_model_id: string
19+
tts_model_id_type: 'custom' | 'reference'
20+
tts_model_id_reference: string[]
21+
content_list: string[]
22+
model_params_setting: Record<string, unknown>
23+
}
24+
25+
const formRef = useTemplateRef<FormInstance>('formRef')
26+
const modelCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('modelCascaderRef')
27+
const contentCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('contentCascaderRef')
28+
29+
const store = useWorkflowStore(apiType)
30+
const modelList = ref<Array<ModelItem>>([])
31+
const providerOptions = ref<Array<ModelProviderItem>>([])
32+
33+
const formData = computed<TextToSpeechNodeForm>({
34+
get: () => {
35+
if (!model.properties.node_data) {
36+
set(model.properties, 'node_data', {
37+
tts_model_id: '',
38+
tts_model_id_type: 'custom',
39+
tts_model_id_reference: [],
40+
content_list: [],
41+
model_params_setting: {},
42+
})
43+
}
44+
const data = model.properties.node_data as TextToSpeechNodeForm
45+
if (data.tts_model_id_type === undefined) set(data, 'tts_model_id_type', 'custom')
46+
if (!Array.isArray(data.tts_model_id_reference)) set(data, 'tts_model_id_reference', [])
47+
if (!Array.isArray(data.content_list)) set(data, 'content_list', [])
48+
if (!data.model_params_setting) set(data, 'model_params_setting', {})
49+
return data
50+
},
51+
set: (value) => (model.properties.node_data = value),
52+
})
53+
54+
function validate() {
55+
return Promise.all([
56+
formData.value.tts_model_id_type === 'reference' ? modelCascaderRef.value?.validate() : Promise.resolve(),
57+
contentCascaderRef.value?.validate(),
58+
formRef.value?.validate(),
59+
]).catch((error) => Promise.reject({ node: model, errMessage: error }))
60+
}
61+
62+
onMounted(() => {
63+
model.validate = validate
64+
store.getModelList({ model_type: 'TTS' }).then((data) => {
65+
modelList.value = data
66+
})
67+
store.getProviderList().then((data) => {
68+
providerOptions.value = data
69+
})
70+
})
71+
</script>
72+
73+
<template>
74+
<NodeContainer :node-model="model">
75+
<h6 class="mb-3">节点设置</h6>
76+
<el-form ref="formRef" :model="formData" label-position="top" require-asterisk-position="right" @submit.prevent>
77+
<el-form-item
78+
:prop="formData.tts_model_id_type === 'reference' ? 'tts_model_id_reference' : 'tts_model_id'"
79+
:rules="{ required: true, message: '请选择或填写语音合成模型', trigger: 'change' }"
80+
>
81+
<template #label>
82+
<div class="flex-between gap-3 w-full">
83+
<span>语音合成模型</span>
84+
<el-select
85+
v-model="formData.tts_model_id_type"
86+
:teleported="false"
87+
class="w-30!"
88+
size="small"
89+
@change="formData.tts_model_id_reference = []"
90+
>
91+
<el-option label="引用变量" value="reference" />
92+
<el-option label="自定义" value="custom" />
93+
</el-select>
94+
</div>
95+
</template>
96+
<NodeCascader
97+
v-if="formData.tts_model_id_type === 'reference'"
98+
ref="modelCascaderRef"
99+
v-model="formData.tts_model_id_reference"
100+
:node-model="model"
101+
class="w-full"
102+
placeholder="请选择变量"
103+
/>
104+
<ModelSelect
105+
v-else
106+
placeholder="请输入语音合成模型 ID"
107+
:options="modelList"
108+
:provider-options="providerOptions"
109+
v-model="formData.tts_model_id"
110+
></ModelSelect>
111+
</el-form-item>
112+
113+
<el-form-item prop="content_list" :rules="{ required: true, message: '请选择文本内容', trigger: 'change' }" label="文本内容">
114+
<NodeCascader ref="contentCascaderRef" v-model="formData.content_list" :node-model="model" class="w-full" placeholder="选择文本内容" />
115+
</el-form-item>
116+
</el-form>
117+
</NodeContainer>
118+
</template>
119+
<style lang="scss" scoped>
120+
:deep(.el-form-item__label) {
121+
width: 100%;
122+
}
123+
:deep(.el-form-item) {
124+
margin-bottom: 16px;
125+
}
126+
</style>

0 commit comments

Comments
 (0)