feat(flow): 添加媒体分析节点功能
- 新增声音事件识别节点,支持音频分类匹配功能 - 新增视频智能分析节点,支持视频内容理解与结构化输出 - 添加MediaAnalysisParams组件实现媒体分析参数配置界面 - 集成媒体分析节点到参数抽屉组件中 - 优化参数验证逻辑,添加required字段判断条件
This commit is contained in:
parent
3ccf3e2c00
commit
360a8b842f
@ -38,6 +38,7 @@ import RecognizeNodeParams from './params/RecognizeNodeParams.vue'
|
||||
import DeviceUniversal from './params/DeviceUniversal.vue'
|
||||
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
||||
import ActionQueueParams from './params/ActionQueueParams.vue'
|
||||
import MediaAnalysisParams from './params/MediaAnalysisParams.vue'
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
@ -65,6 +66,7 @@ const currentComponent = computed(() => {
|
||||
if (type === 'recognize') return RecognizeNodeParams
|
||||
if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal
|
||||
if (type === 'actionQueue') return ActionQueueParams
|
||||
if (type === 'mediaAnalysis') return MediaAnalysisParams
|
||||
if (type === 'branch') return SwitchNodeParams
|
||||
return null
|
||||
})
|
||||
|
||||
244
src/views/flow/components/params/MediaAnalysisParams.vue
Normal file
244
src/views/flow/components/params/MediaAnalysisParams.vue
Normal file
@ -0,0 +1,244 @@
|
||||
<!--
|
||||
* 文件说明:通用媒体分析节点配置,支持音频分类和视频分析场景。
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
-->
|
||||
<template>
|
||||
<div class="media-analysis-params">
|
||||
<div class="media-analysis-intro">
|
||||
<div class="media-analysis-intro__title">{{ title }}</div>
|
||||
<div class="media-analysis-intro__desc">{{ description }}</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||
<el-form-item label="分析场景" prop="profileCode">
|
||||
<el-select
|
||||
v-model="formData.profileCode"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入场景编码"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in profileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="field-hint">场景决定识别标签、判断阈值和分析要求。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="mediaLabel" prop="mediaMode">
|
||||
<el-segmented v-model="formData.mediaMode" :options="mediaModes" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.mediaMode === 'quote'" label="引用节点输出" prop="mediaQuote">
|
||||
<el-cascader
|
||||
ref="mediaCascaderRef"
|
||||
v-model="formData.mediaQuote"
|
||||
:options="quoteOptions"
|
||||
:props="{ checkStrictly: true }"
|
||||
placeholder="选择上游录音或录像输出"
|
||||
style="width: 100%"
|
||||
@visible-change="loadQuoteOptions"
|
||||
@change="handleQuoteChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-else :label="`${mediaLabel}地址`" prop="mediaUrl">
|
||||
<el-input v-model="formData.mediaUrl" type="textarea" :rows="3" placeholder="请输入 HTTP/HTTPS 媒体地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isVideo" label="补充分析要求">
|
||||
<el-input
|
||||
v-model="formData.instruction"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
placeholder="可选,例如:判断仪表是否点亮,并说明依据"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isVideo" label="分析模式">
|
||||
<el-segmented v-model="formData.analysisMode" :options="analysisModes" block />
|
||||
<div class="field-hint">自动模式优先快速分析,结果不完整时自动切换精确分析。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">节点输出</el-divider>
|
||||
<div class="output-list">
|
||||
<div v-for="item in outputSummary" :key="item.name" class="output-item">
|
||||
<code>{{ item.name }}</code>
|
||||
<span>{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { getInput } from '@/utils/flow'
|
||||
|
||||
const props = defineProps({ data: Object })
|
||||
const emit = defineEmits(['save-success', 'save-error'])
|
||||
|
||||
const formRef = ref()
|
||||
const mediaCascaderRef = ref()
|
||||
const quoteOptions = ref([])
|
||||
const formData = reactive({
|
||||
profileCode: '',
|
||||
mediaMode: 'quote',
|
||||
mediaQuote: [],
|
||||
mediaQuoteType: 'output',
|
||||
mediaUrl: '',
|
||||
instruction: '',
|
||||
analysisMode: 'AUTO',
|
||||
})
|
||||
|
||||
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
||||
const title = computed(() => isVideo.value ? '视频智能分析' : '声音事件识别')
|
||||
const description = computed(() => isVideo.value
|
||||
? '对录像抽帧并按所选场景生成结构化分析结果。'
|
||||
: '将录音与场景中的多类参考声音比较,低于阈值时返回“其他声音”。')
|
||||
const mediaLabel = computed(() => isVideo.value ? '视频来源' : '音频来源')
|
||||
const mediaName = computed(() => isVideo.value ? 'videoUrl' : 'audioUrl')
|
||||
const mediaModes = [
|
||||
{ label: '引用节点输出', value: 'quote' },
|
||||
{ label: '直接输入地址', value: 'input' },
|
||||
]
|
||||
const analysisModes = [
|
||||
{ label: '自动', value: 'AUTO' },
|
||||
{ label: '快速', value: 'FAST' },
|
||||
{ label: '精确', value: 'ACCURATE' },
|
||||
]
|
||||
const profileOptions = computed(() => isVideo.value
|
||||
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
||||
: [{ label: '爱玛车辆声音事件识别', value: 'aima.power_state.v1' }])
|
||||
const outputSummary = computed(() => isVideo.value
|
||||
? [
|
||||
{ name: 'passed', desc: '是否通过' },
|
||||
{ name: 'conclusion', desc: '分析结论' },
|
||||
{ name: 'summary', desc: '分析摘要' },
|
||||
{ name: 'events', desc: '关键事件列表' },
|
||||
{ name: 'analysisMode', desc: '实际使用的分析模式' },
|
||||
{ name: 'fallback', desc: '是否切换到精确分析' },
|
||||
]
|
||||
: [
|
||||
{ name: 'label', desc: '场景分类编码,未匹配时为 UNKNOWN' },
|
||||
{ name: 'labelName', desc: '结果名称' },
|
||||
{ name: 'matched', desc: '是否达到匹配阈值' },
|
||||
{ name: 'confidence', desc: '最高匹配度' },
|
||||
{ name: 'scores', desc: '各分类匹配度' },
|
||||
])
|
||||
|
||||
const rules = {
|
||||
profileCode: [{ required: true, message: '请选择或输入分析场景', trigger: 'change' }],
|
||||
mediaQuote: [{
|
||||
validator: (_, value) => formData.mediaMode !== 'quote' || (Array.isArray(value) && value.length)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请选择媒体输出')),
|
||||
trigger: 'change',
|
||||
}],
|
||||
mediaUrl: [{
|
||||
validator: (_, value) => formData.mediaMode !== 'input' || /^https?:\/\//i.test(String(value || '').trim())
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入有效的 HTTP/HTTPS 地址')),
|
||||
trigger: 'blur',
|
||||
}],
|
||||
}
|
||||
|
||||
const loadQuoteOptions = (visible) => {
|
||||
if (visible) quoteOptions.value = getInput(props.data.id) || []
|
||||
}
|
||||
|
||||
const handleQuoteChange = () => {
|
||||
const checked = mediaCascaderRef.value?.getCheckedNodes?.(true)?.[0]
|
||||
formData.mediaQuoteType = checked?.data?.type || 'output'
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
const params = props.data?.properties?.nodeParams || []
|
||||
const profile = params.find(item => item.name === 'profileCode')
|
||||
const media = params.find(item => item.name === mediaName.value)
|
||||
const instruction = params.find(item => item.name === 'instruction')
|
||||
const analysisMode = params.find(item => item.name === 'analysisMode')
|
||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
||||
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
||||
formData.mediaQuoteType = media?.quoteType || 'output'
|
||||
formData.mediaUrl = media?.input || ''
|
||||
formData.instruction = instruction?.input || ''
|
||||
formData.analysisMode = analysisMode?.input || 'AUTO'
|
||||
}
|
||||
|
||||
watch(() => props.data, initData, { immediate: true, deep: true })
|
||||
|
||||
const validateAndSave = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
const nodeParams = [
|
||||
{
|
||||
name: 'profileCode',
|
||||
type: 'input',
|
||||
input: formData.profileCode.trim(),
|
||||
required: true,
|
||||
disabled: true,
|
||||
componentType: 'select',
|
||||
selectOptions: profileOptions.value,
|
||||
},
|
||||
{
|
||||
name: mediaName.value,
|
||||
type: formData.mediaMode,
|
||||
input: formData.mediaMode === 'input' ? formData.mediaUrl.trim() : '',
|
||||
quote: formData.mediaMode === 'quote' ? [...formData.mediaQuote] : '',
|
||||
quoteType: formData.mediaQuoteType,
|
||||
required: true,
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
if (isVideo.value) {
|
||||
nodeParams.push({
|
||||
name: 'instruction',
|
||||
type: 'input',
|
||||
input: formData.instruction.trim(),
|
||||
required: false,
|
||||
disabled: true,
|
||||
})
|
||||
nodeParams.push({
|
||||
name: 'analysisMode',
|
||||
type: 'input',
|
||||
input: formData.analysisMode,
|
||||
required: false,
|
||||
disabled: true,
|
||||
componentType: 'select',
|
||||
selectOptions: analysisModes,
|
||||
})
|
||||
}
|
||||
const properties = { ...props.data.properties, nodeParams }
|
||||
delete properties.executionTarget
|
||||
lf.setProperties(props.data.id, properties)
|
||||
emit('save-success')
|
||||
} catch (error) {
|
||||
emit('save-error')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ validateAndSave })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-analysis-params { padding-top: 16px; }
|
||||
.media-analysis-intro {
|
||||
margin-bottom: 22px;
|
||||
padding: 14px 16px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.media-analysis-intro__title { color: var(--el-text-color-primary); font-weight: 650; }
|
||||
.media-analysis-intro__desc, .field-hint { margin-top: 5px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 18px; }
|
||||
.output-list { display: grid; gap: 8px; }
|
||||
.output-item { display: flex; gap: 12px; align-items: baseline; color: var(--el-text-color-regular); font-size: 13px; }
|
||||
.output-item code { min-width: 92px; color: var(--el-color-primary); }
|
||||
</style>
|
||||
@ -492,6 +492,49 @@ function handler(params) {
|
||||
{
|
||||
collapseTitle: "大模型",
|
||||
nodeList: [
|
||||
{
|
||||
icon: microphoneSvg,
|
||||
name: "声音事件识别",
|
||||
type: "mediaAnalysis",
|
||||
desc: "将录音与分析场景中的多类参考声音进行匹配",
|
||||
action: "AUDIO_EVENT_CLASSIFY",
|
||||
nodeType: "LLM",
|
||||
nodeParams: [
|
||||
{ name: "profileCode", type: "input", input: "aima.power_state.v1", required: true, disabled: true },
|
||||
{ name: "audioUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||
],
|
||||
outputType: "json",
|
||||
outputParams: [
|
||||
{ name: "label", type: "string", desc: "场景分类编码,未匹配时为UNKNOWN", disabled: true },
|
||||
{ name: "labelName", type: "string", desc: "分类名称", disabled: true },
|
||||
{ name: "matched", type: "boolean", desc: "是否达到匹配阈值", disabled: true },
|
||||
{ name: "confidence", type: "number", desc: "最高匹配度", disabled: true },
|
||||
{ name: "scores", type: "object", desc: "各分类匹配度", children: [], disabled: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: videoSvg,
|
||||
name: "视频智能分析",
|
||||
type: "mediaAnalysis",
|
||||
desc: "按分析场景理解测试录像并输出结构化结论",
|
||||
action: "VIDEO_ANALYZE",
|
||||
nodeType: "LLM",
|
||||
nodeParams: [
|
||||
{ name: "profileCode", type: "input", input: "aima.power_video.v1", required: true, disabled: true },
|
||||
{ name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||
{ name: "instruction", type: "input", input: "", required: false, disabled: true },
|
||||
{ name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true },
|
||||
],
|
||||
outputType: "json",
|
||||
outputParams: [
|
||||
{ name: "passed", type: "boolean", desc: "是否通过", disabled: true },
|
||||
{ name: "conclusion", type: "string", desc: "分析结论", disabled: true },
|
||||
{ name: "summary", type: "string", desc: "分析摘要", disabled: true },
|
||||
{ name: "events", type: "array<object>", desc: "关键事件", children: [], disabled: true },
|
||||
{ name: "analysisMode", type: "string", desc: "实际分析模式", disabled: true },
|
||||
{ name: "fallback", type: "boolean", desc: "是否切换精确分析", disabled: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: microphoneSvg,
|
||||
name: "获取触控坐标",
|
||||
|
||||
@ -1066,7 +1066,7 @@ const validateSingleNodeParams = () => {
|
||||
ElMessage.warning("请输入参数名");
|
||||
return false;
|
||||
}
|
||||
if (param.input === "" || param.input === null || param.input === undefined) {
|
||||
if ((param.input === "" || param.input === null || param.input === undefined) && param.required !== false) {
|
||||
ElMessage.warning(`请输入参数 ${param.name} 的值`);
|
||||
return false;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user