refactor: 应用设置中配置语音输入和播放
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
CaptainB 2024-09-04 17:02:15 +08:00 committed by 刘瑞斌
parent 38565aff11
commit c50da4ef46
9 changed files with 526 additions and 13 deletions

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.15 on 2024-09-05 14:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('setting', '0006_alter_model_status'),
('application', '0011_application_model_params_setting'),
]
operations = [
migrations.AddField(
model_name='application',
name='stt_model',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stt_model_id', to='setting.model'),
),
migrations.AddField(
model_name='application',
name='stt_model_enable',
field=models.BooleanField(default=False, verbose_name='语音识别模型是否启用'),
),
migrations.AddField(
model_name='application',
name='tts_model',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tts_model_id', to='setting.model'),
),
migrations.AddField(
model_name='application',
name='tts_model_enable',
field=models.BooleanField(default=False, verbose_name='语音合成模型是否启用'),
),
]

View File

@ -54,6 +54,10 @@ class Application(AppModelMixin):
work_flow = models.JSONField(verbose_name="工作流数据", default=dict)
type = models.CharField(verbose_name="应用类型", choices=ApplicationTypeChoices.choices,
default=ApplicationTypeChoices.SIMPLE, max_length=256)
tts_model = models.ForeignKey(Model, related_name='tts_model_id', on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True)
stt_model = models.ForeignKey(Model, related_name='stt_model_id', on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True)
tts_model_enable = models.BooleanField(verbose_name="语音合成模型是否启用", default=False)
stt_model_enable = models.BooleanField(verbose_name="语音识别模型是否启用", default=False)
@staticmethod
def get_default_model_prompt():

View File

@ -516,7 +516,7 @@ class ApplicationSerializer(serializers.Serializer):
@staticmethod
def reset_application(application: Dict):
application['multiple_rounds_dialogue'] = True if application.get('dialogue_number') > 0 else False
del application['dialogue_number']
if 'dataset_setting' in application:
application['dataset_setting'] = {'search_mode': 'embedding', 'no_references_setting': {
'status': 'ai_questioning',
@ -711,21 +711,39 @@ class ApplicationSerializer(serializers.Serializer):
raise AppApiException(500, "模型不存在")
if not model.is_permission(application.user_id):
raise AppApiException(500, f"沒有权限使用该模型:{model.name}")
if instance.get('stt_model_id') is None or len(instance.get('stt_model_id')) == 0:
application.stt_model_id = None
else:
model = QuerySet(Model).filter(
id=instance.get('stt_model_id')).first()
if model is None:
raise AppApiException(500, "模型不存在")
if not model.is_permission(application.user_id):
raise AppApiException(500, f"沒有权限使用该模型:{model.name}")
if instance.get('tts_model_id') is None or len(instance.get('tts_model_id')) == 0:
application.tts_model_id = None
else:
model = QuerySet(Model).filter(
id=instance.get('tts_model_id')).first()
if model is None:
raise AppApiException(500, "模型不存在")
if not model.is_permission(application.user_id):
raise AppApiException(500, f"沒有权限使用该模型:{model.name}")
if 'work_flow' in instance:
# 当前用户可修改关联的知识库列表
application_dataset_id_list = [str(dataset_dict.get('id')) for dataset_dict in
self.list_dataset(with_valid=False)]
self.update_reverse_search_node(instance.get('work_flow'), application_dataset_id_list)
# 找到语音配置相关
self.get_work_flow_model(instance)
update_keys = ['name', 'desc', 'model_id', 'multiple_rounds_dialogue', 'prologue', 'status',
'dataset_setting', 'model_setting', 'problem_optimization',
'dataset_setting', 'model_setting', 'problem_optimization', 'dialogue_number',
'stt_model_id', 'tts_model_id', 'tts_model_enable', 'stt_model_enable',
'api_key_is_active', 'icon', 'work_flow', 'model_params_setting']
for update_key in update_keys:
if update_key in instance and instance.get(update_key) is not None:
if update_key == 'multiple_rounds_dialogue':
application.__setattr__('dialogue_number', 0 if not instance.get(update_key) else 3)
else:
application.__setattr__(update_key, instance.get(update_key))
application.__setattr__(update_key, instance.get(update_key))
application.save()
if 'dataset_id_list' in instance:
@ -825,6 +843,27 @@ class ApplicationSerializer(serializers.Serializer):
application.save()
@staticmethod
def get_work_flow_model(instance):
nodes = instance.get('work_flow')['nodes']
for node in nodes:
if node['id'] == 'base-node':
instance['stt_model_id'] = node['properties']['node_data']['stt_model_id']
instance['tts_model_id'] = node['properties']['node_data']['tts_model_id']
instance['stt_model_enable'] = node['properties']['node_data']['stt_model_enable']
instance['tts_model_enable'] = node['properties']['node_data']['tts_model_enable']
break
def speech_to_text(self, filelist):
# todo 找到模型 mp3转text
print(self.application_id)
print(filelist)
def text_to_speech(self, text):
# todo 找到模型 text转bytes
print(self.application_id)
print(text)
class ApplicationKeySerializerModel(serializers.ModelSerializer):
class Meta:
model = ApplicationApiKey

View File

@ -63,5 +63,8 @@ urlpatterns = [
path(
'application/<str:application_id>/chat/<chat_id>/chat_record/<str:chat_record_id>/dataset/<str:dataset_id>/document_id/<str:document_id>/improve/<str:paragraph_id>',
views.ChatView.ChatRecord.Improve.Operate.as_view(),
name='')
name=''),
path('application/<str:application_id>/<str:model_id>/speech_to_text', views.Application.SpeechToText.as_view(), name='application/audio'),
path('application/<str:application_id>/<str:model_id>/text_to_speech', views.Application.TextToSpeech.as_view(), name='application/audio'),
]

View File

@ -534,3 +534,35 @@ class Application(APIView):
ApplicationSerializer.Query(
data={**query_params_to_single_dict(request.query_params), 'user_id': request.user.id}).page(
current_page, page_size))
class SpeechToText(APIView):
authentication_classes = [TokenAuth]
@action(methods=['POST'], detail=False)
@has_permissions(ViewPermission([RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION,
operate=Operate.USE,
dynamic_tag=keywords.get(
'application_id'))],
compare=CompareConstants.AND))
def post(self, request: Request, application_id: str, model_id: str):
return result.success(
ApplicationSerializer.Operate(
data={'application_id': application_id, 'user_id': request.user.id, 'model_id': model_id})
.speech_to_text(request.FILES.getlist('file')[0]))
class TextToSpeech(APIView):
authentication_classes = [TokenAuth]
@action(methods=['POST'], detail=False)
@has_permissions(ViewPermission([RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION,
operate=Operate.USE,
dynamic_tag=keywords.get(
'application_id'))],
compare=CompareConstants.AND))
def post(self, request: Request, application_id: str, model_id: str):
return result.success(
ApplicationSerializer.Operate(
data={'application_id': application_id, 'user_id': request.user.id, 'model_id': model_id})
.text_to_speech(request.data.get('text')))

View File

@ -250,6 +250,35 @@ const getApplicationRerankerModel: (
) => Promise<Result<Array<any>>> = (application_id, loading) => {
return get(`${prefix}/${application_id}/model`, { model_type: 'RERANKER' }, loading)
}
/**
* 使
* @param application_id
* @param loading
* @query { query_text: string, top_number: number, similarity: number }
* @returns
*/
const getApplicationSTTModel: (
application_id: string,
loading?: Ref<boolean>
) => Promise<Result<Array<any>>> = (application_id, loading) => {
return get(`${prefix}/${application_id}/model`, { model_type: 'STT' }, loading)
}
/**
* 使
* @param application_id
* @param loading
* @query { query_text: string, top_number: number, similarity: number }
* @returns
*/
const getApplicationTTSModel: (
application_id: string,
loading?: Ref<boolean>
) => Promise<Result<Array<any>>> = (application_id, loading) => {
return get(`${prefix}/${application_id}/model`, { model_type: 'TTS' }, loading)
}
/**
*
* @param
@ -324,5 +353,7 @@ export default {
listFunctionLib,
getFunctionLib,
getModelParamsForm,
getApplicationRerankerModel
getApplicationRerankerModel,
getApplicationSTTModel,
getApplicationTTSModel,
}

View File

@ -14,6 +14,10 @@ interface ApplicationFormType {
type?: string
work_flow?: any
model_params_setting?: any
stt_model_id?: string
tts_model_id?: string
stt_model_enable?: boolean
tts_model_enable?: boolean
}
interface chatType {
id: string

View File

@ -288,6 +288,147 @@
</template>
<el-switch size="small" v-model="applicationForm.problem_optimization"></el-switch>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">语音输入</span>
<el-tooltip
effect="dark"
content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
<el-switch v-model="applicationForm.stt_model_enable"/>
</div>
</template>
<el-select
v-model="applicationForm.stt_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in sttModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.stt_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.stt_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">语音播放</span>
<el-switch v-model="applicationForm.tts_model_enable"/>
</div>
</template>
<el-select
v-model="applicationForm.tts_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in ttsModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.tts_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.tts_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
@ -411,6 +552,10 @@ const applicationForm = ref<ApplicationFormType>({
},
model_params_setting: {},
problem_optimization: false,
stt_model_id: '',
tts_model_id: '',
stt_model_enable: false,
tts_model_enable: false,
type: 'SIMPLE'
})
@ -440,6 +585,8 @@ const rules = reactive<FormRules<ApplicationFormType>>({
const modelOptions = ref<any>(null)
const providerOptions = ref<Array<Provider>>([])
const datasetList = ref([])
const sttModelOptions = ref<any>(null)
const ttsModelOptions = ref<any>(null)
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
@ -508,6 +655,8 @@ function getDetail() {
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
applicationForm.value = res.data
applicationForm.value.model_id = res.data.model
applicationForm.value.stt_model_id = res.data.stt_model
applicationForm.value.tts_model_id = res.data.tts_model
})
}
@ -530,6 +679,32 @@ function getModel() {
})
}
function getSTTModel() {
loading.value = true
applicationApi
.getApplicationSTTModel(id)
.then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function getTTSModel() {
loading.value = true
applicationApi
.getApplicationTTSModel(id)
.then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function getProvider() {
loading.value = true
model
@ -552,6 +727,8 @@ onMounted(() => {
getModel()
getDataset()
getDetail()
getSTTModel()
getTTSModel()
})
</script>
<style lang="scss" scoped>

View File

@ -49,28 +49,186 @@
<template #defFooters>
<el-button text type="info" @click="openDialog">
<AppIcon iconName="app-magnify" style="font-size: 16px"></AppIcon>
</el-button> </template
></MdEditor>
</el-button>
</template
>
</MdEditor>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">语音输入</span>
<el-tooltip
effect="dark"
content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
<el-switch v-model="form_data.stt_model_enable" />
</div>
</template>
<el-select
v-model="form_data.stt_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in sttModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">语音播放</span>
<el-switch v-model="form_data.tts_model_enable" />
</div>
</template>
<el-select
v-model="form_data.tts_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in ttsModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
<!-- 回复内容弹出层 -->
<el-dialog v-model="dialogVisible" title="开场白" append-to-body>
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"> </MdEditor>
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor>
<template #footer>
<div class="dialog-footer mt-24">
<el-button type="primary" @click="submitDialog"> 确认 </el-button>
<el-button type="primary" @click="submitDialog"> 确认</el-button>
</div>
</template>
</el-dialog>
</NodeContainer>
</template>
<script setup lang="ts">
import { set } from 'lodash'
import { app } from '@/main'
import { groupBy, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import { relatedObject } from '@/utils/utils'
import useStore from '@/stores'
import applicationApi from '@/api/application'
const { model } = useStore()
const {
params: { id }
} = app.config.globalProperties.$route as any
const props = defineProps<{ nodeModel: any }>()
const sttModelOptions = ref<any>(null)
const ttsModelOptions = ref<any>(null)
const providerOptions = ref<any>(null)
const form = {
name: '',
desc: '',
@ -120,8 +278,38 @@ const validate = () => {
})
}
function getProvider() {
model
.asyncGetProvider()
.then((res: any) => {
providerOptions.value = res?.data
})
}
function getSTTModel() {
applicationApi
.getApplicationSTTModel(id)
.then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
})
}
function getTTSModel() {
applicationApi
.getApplicationTTSModel(id)
.then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
})
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
getProvider()
getTTSModel()
getSTTModel()
})
</script>
<style lang="scss" scoped>