feat(flow): 更新声音类型检测功能并添加系统错误状态清除
- 修改声音事件识别为声音类型检测,调整相关参数配置 - 添加目标声音类型选择功能和预期标签匹配逻辑 - 新增机械臂故障清除API接口和操作按钮 - 实现工作流中清除机器人错误状态的功能和对话框 - 更新任务执行日志中声音分析结果显示逻辑 - 优化声音类型检测的输出参数和验证规则
This commit is contained in:
parent
f944d33d58
commit
eb056f097c
@ -15,4 +15,12 @@ export function status(data) {
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function clearFault(params) {
|
||||
return request({
|
||||
url: '/api/arm/clearFault',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,8 +1,33 @@
|
||||
<script setup>
|
||||
import { reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
|
||||
import { computed, reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { clearFault as clearArmFault } from "@/api/device/arm";
|
||||
import UrdfViewer from "./components/UrdfViewer.vue";
|
||||
import JointControl from "./components/JointControl.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const robotId = computed(() => String(route.query.robotId || ""));
|
||||
const deviceId = computed(() => String(route.query.deviceId || ""));
|
||||
const clearingFault = ref(false);
|
||||
|
||||
const handleClearFault = async () => {
|
||||
if (!robotId.value || !deviceId.value) {
|
||||
ElMessage.warning("缺少机器人或机械臂设备信息");
|
||||
return;
|
||||
}
|
||||
clearingFault.value = true;
|
||||
try {
|
||||
await clearArmFault({ robotId: robotId.value, deviceId: deviceId.value });
|
||||
ElMessage.success("机械臂故障状态已清除");
|
||||
} catch (error) {
|
||||
console.error("清除机械臂故障失败:", error);
|
||||
} finally {
|
||||
clearingFault.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const data = reactive({
|
||||
form: {
|
||||
x: 11,
|
||||
@ -160,6 +185,15 @@ onUnmounted(() => {
|
||||
>零位姿态</el-button
|
||||
>
|
||||
<el-button @click="handleResets('init')">初始位置</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:loading="clearingFault"
|
||||
:disabled="!robotId || !deviceId"
|
||||
@click="handleClearFault"
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>清除故障
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pose-info flex align-center">
|
||||
<div class="pose-item">X: {{ form.x.toFixed(6) }} m</div>
|
||||
|
||||
@ -17,8 +17,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="task-log__analysis-grid">
|
||||
<div v-if="analysisOf(log).targetLabel" class="task-log__analysis-field">
|
||||
<span>目标声音</span>
|
||||
<strong>{{ analysisOf(log).targetLabel }}</strong>
|
||||
</div>
|
||||
<div v-if="analysisOf(log).label" class="task-log__analysis-field">
|
||||
<span>识别结果</span>
|
||||
<span>{{ analysisOf(log).isAudio ? '实际声音' : '识别结果' }}</span>
|
||||
<strong>{{ analysisOf(log).label }}</strong>
|
||||
</div>
|
||||
<div v-if="analysisOf(log).matchLabel" class="task-log__analysis-field">
|
||||
@ -144,10 +148,11 @@ const analysisOf = (log) => {
|
||||
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
||||
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
||||
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
||||
'expectedLabel', 'expectedLabelName', 'decisionReason',
|
||||
].forEach((key) => delete detailSource[key])
|
||||
const hasDetails = Object.keys(detailSource).length > 0
|
||||
const result = {
|
||||
title: isAudio ? '声音分析' : '视频分析',
|
||||
title: isAudio ? '声音类型检测' : '视频分析',
|
||||
statusLabel,
|
||||
tagType: status === 'FAILED' || status === 'ERROR' || passed === false
|
||||
? 'danger'
|
||||
@ -157,10 +162,12 @@ const analysisOf = (log) => {
|
||||
: output.analysisModel?.name || '',
|
||||
timingMs: Number.isFinite(Number(output.analysisTimingMs)) ? Number(output.analysisTimingMs) : null,
|
||||
label: output.labelName || output.label || '',
|
||||
matchLabel: matched === true ? '匹配' : matched === false ? '未匹配' : '',
|
||||
targetLabel: output.expectedLabelName || output.expectedLabel || '',
|
||||
matchLabel: isAudio ? '' : matched === true ? '匹配' : matched === false ? '未匹配' : '',
|
||||
confidence,
|
||||
passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '',
|
||||
summary: output.summary || output.conclusion || '',
|
||||
summary: output.decisionReason || output.summary || output.conclusion || '',
|
||||
isAudio,
|
||||
events: Array.isArray(output.events)
|
||||
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
||||
: [],
|
||||
|
||||
@ -29,6 +29,26 @@
|
||||
<div class="field-hint">场景决定识别标签、判断阈值和分析要求。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!isVideo" label="目标声音类型" prop="expectedLabel">
|
||||
<el-select
|
||||
v-model="formData.expectedLabel"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请选择目标声音类型"
|
||||
style="width: 100%"
|
||||
@change="handleExpectedLabelChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in soundTypeOptions"
|
||||
: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>
|
||||
@ -126,6 +146,8 @@ const analysisPresets = {
|
||||
}
|
||||
const formData = reactive({
|
||||
profileCode: '',
|
||||
expectedLabel: '',
|
||||
expectedLabelName: '',
|
||||
mediaMode: 'quote',
|
||||
mediaQuote: [],
|
||||
mediaQuoteType: 'output',
|
||||
@ -136,10 +158,10 @@ const formData = reactive({
|
||||
})
|
||||
|
||||
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
||||
const title = computed(() => isVideo.value ? '视频智能分析' : '声音事件识别')
|
||||
const title = computed(() => isVideo.value ? '视频智能分析' : '声音类型检测')
|
||||
const description = computed(() => isVideo.value
|
||||
? '对录像抽帧并按所选场景生成结构化分析结果。'
|
||||
: '将录音与场景中的多类参考声音比较,低于阈值时返回“其他声音”。')
|
||||
: '判断录音是否属于指定的目标声音类型,并输出通过或不通过。')
|
||||
const mediaLabel = computed(() => isVideo.value ? '视频来源' : '音频来源')
|
||||
const mediaName = computed(() => isVideo.value ? 'videoUrl' : 'audioUrl')
|
||||
const mediaModes = [
|
||||
@ -151,6 +173,13 @@ const analysisModes = [
|
||||
{ label: '均衡', value: 'AUTO' },
|
||||
{ label: '精确', value: 'ACCURATE' },
|
||||
]
|
||||
const soundTypeOptions = [
|
||||
{ label: '开机', value: 'POWER_ON' },
|
||||
{ label: '关机', value: 'POWER_OFF' },
|
||||
{ label: '设防', value: 'ARMED' },
|
||||
{ label: '解防', value: 'DISARMED' },
|
||||
{ label: '鸣笛寻车', value: 'FIND_VEHICLE_HORN' },
|
||||
]
|
||||
const modeDescriptions = {
|
||||
FAST: '使用轻量模型直接判断,适合动作明显、批量执行的场景。',
|
||||
AUTO: '先快速判断,低置信度时自动切换精确分析。',
|
||||
@ -163,7 +192,7 @@ const isTuned = computed(() => {
|
||||
})
|
||||
const profileOptions = computed(() => isVideo.value
|
||||
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
||||
: [{ label: '爱玛车辆声音事件识别', value: 'aima.power_state.v1' }])
|
||||
: [{ label: '爱玛车辆声音类型检测', value: 'aima.power_state.v1' }])
|
||||
const outputSummary = computed(() => isVideo.value
|
||||
? [
|
||||
{ name: 'passed', desc: '是否通过' },
|
||||
@ -176,15 +205,23 @@ const outputSummary = computed(() => isVideo.value
|
||||
{ name: 'fallback', desc: '是否切换到精确分析' },
|
||||
]
|
||||
: [
|
||||
{ name: 'label', desc: '场景分类编码,未匹配时为 UNKNOWN' },
|
||||
{ name: 'labelName', desc: '结果名称' },
|
||||
{ name: 'matched', desc: '是否达到匹配阈值' },
|
||||
{ name: 'passed', desc: '是否检测到目标声音' },
|
||||
{ name: 'expectedLabelName', desc: '目标声音类型' },
|
||||
{ name: 'labelName', desc: '实际声音类型' },
|
||||
{ name: 'matched', desc: '是否识别到已知声音' },
|
||||
{ name: 'confidence', desc: '最高匹配度' },
|
||||
{ name: 'decisionReason', desc: '检测结论' },
|
||||
{ name: 'scores', desc: '各分类匹配度' },
|
||||
])
|
||||
|
||||
const rules = {
|
||||
profileCode: [{ required: true, message: '请选择或输入分析场景', trigger: 'change' }],
|
||||
expectedLabel: [{
|
||||
validator: (_, value) => isVideo.value || String(value || '').trim()
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请选择目标声音类型')),
|
||||
trigger: 'change',
|
||||
}],
|
||||
mediaQuote: [{
|
||||
validator: (_, value) => formData.mediaMode !== 'quote' || (Array.isArray(value) && value.length)
|
||||
? Promise.resolve()
|
||||
@ -211,6 +248,11 @@ const restorePreset = () => {
|
||||
|
||||
const handleAnalysisModeChange = () => restorePreset()
|
||||
|
||||
const handleExpectedLabelChange = (value) => {
|
||||
const option = soundTypeOptions.find(item => item.value === value)
|
||||
formData.expectedLabelName = option?.label || String(value || '').trim()
|
||||
}
|
||||
|
||||
const parseStoredTuning = (value) => {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
||||
if (typeof value !== 'string' || !value.trim()) return {}
|
||||
@ -251,11 +293,18 @@ const handleQuoteChange = () => {
|
||||
const initData = () => {
|
||||
const params = props.data?.properties?.nodeParams || []
|
||||
const profile = params.find(item => item.name === 'profileCode')
|
||||
const expectedLabel = params.find(item => item.name === 'expectedLabel')
|
||||
const expectedLabelName = params.find(item => item.name === 'expectedLabelName')
|
||||
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')
|
||||
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||
formData.expectedLabel = expectedLabel?.input || ''
|
||||
formData.expectedLabelName = expectedLabelName?.input || ''
|
||||
if (formData.expectedLabel && !formData.expectedLabelName) {
|
||||
handleExpectedLabelChange(formData.expectedLabel)
|
||||
}
|
||||
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
||||
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
||||
formData.mediaQuoteType = media?.quoteType || 'output'
|
||||
@ -292,6 +341,26 @@ const validateAndSave = async () => {
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
if (!isVideo.value) {
|
||||
nodeParams.splice(1, 0,
|
||||
{
|
||||
name: 'expectedLabel',
|
||||
type: 'input',
|
||||
input: formData.expectedLabel.trim(),
|
||||
required: true,
|
||||
disabled: true,
|
||||
componentType: 'select',
|
||||
selectOptions: soundTypeOptions,
|
||||
},
|
||||
{
|
||||
name: 'expectedLabelName',
|
||||
type: 'input',
|
||||
input: formData.expectedLabelName.trim() || formData.expectedLabel.trim(),
|
||||
required: false,
|
||||
disabled: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
if (isVideo.value) {
|
||||
nodeParams.push({
|
||||
name: 'instruction',
|
||||
|
||||
@ -516,21 +516,27 @@ function handler(params) {
|
||||
nodeList: [
|
||||
{
|
||||
icon: microphoneSvg,
|
||||
name: "声音事件识别",
|
||||
name: "声音类型检测",
|
||||
type: "mediaAnalysis",
|
||||
desc: "将录音与分析场景中的多类参考声音进行匹配",
|
||||
desc: "判断录音是否属于指定的声音类型",
|
||||
action: "AUDIO_EVENT_CLASSIFY",
|
||||
nodeType: "LLM",
|
||||
nodeParams: [
|
||||
{ name: "profileCode", type: "input", input: "aima.power_state.v1", required: true, disabled: true },
|
||||
{ name: "expectedLabel", type: "input", input: "", required: true, disabled: true },
|
||||
{ name: "expectedLabelName", type: "input", input: "", required: false, 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: "passed", type: "boolean", desc: "是否检测到目标声音", disabled: true },
|
||||
{ name: "expectedLabel", type: "string", desc: "目标声音类型编码", disabled: true },
|
||||
{ name: "expectedLabelName", type: "string", desc: "目标声音类型名称", disabled: true },
|
||||
{ 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: "decisionReason", type: "string", desc: "检测结论", disabled: true },
|
||||
{ name: "scores", type: "object", desc: "各分类匹配度", children: [], disabled: true },
|
||||
],
|
||||
},
|
||||
|
||||
@ -21,6 +21,15 @@
|
||||
<el-button v-if="hasRunResult" @click="clearRun">
|
||||
<el-icon><Delete /></el-icon>清除结果
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
:loading="recoverSafetyLoading"
|
||||
:disabled="flowState === 'testRunning'"
|
||||
@click="openRecoverSafetyDialog"
|
||||
>
|
||||
<el-icon><Warning /></el-icon>清除错误状态
|
||||
</el-button>
|
||||
<el-button :type="groupSelectionPending ? 'primary' : 'default'" @click="group">
|
||||
<el-icon><CollectionTag /></el-icon>{{ groupSelectionPending ? "取消分组" : "分组" }}
|
||||
</el-button>
|
||||
@ -163,6 +172,39 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="recoverSafetyDialogVisible" title="清除错误状态" width="480" append-to-body>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="机器人">
|
||||
<el-select
|
||||
v-model="recoverSafetyRobotIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="recoverRobotLoading"
|
||||
placeholder="请选择需要恢复的在线机器人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in recoverRobotOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="recoverSafetyDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="recoverSafetyLoading"
|
||||
:disabled="recoverSafetyRobotIds.length === 0"
|
||||
@click="confirmRecoverSafetyState"
|
||||
>确认清除</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="visible" :width="nodeAction === 'SYSTEM_EXECUTE_ACTION_QUEUE' ? '620' : '500'" class="single-node-dialog" append-to-body>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<div class="my-header">
|
||||
@ -279,7 +321,7 @@ import TestRun from "./components/TestRun.vue";
|
||||
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
||||
import Aside from "./components/Aside.vue";
|
||||
import ActionQueueSingleRun from "./components/ActionQueueSingleRun.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useFlowStore } from "@/store/modules/flow";
|
||||
import { emitter } from "@/utils/eventBus";
|
||||
import { convertToTree } from "@/utils/flow";
|
||||
@ -308,6 +350,7 @@ import {
|
||||
} from "@/api/flow/flow";
|
||||
import { getDetect } from "@/api/test/detect";
|
||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||
import { recoverSafetyState } from '@/api/device/system'
|
||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||
import {
|
||||
CircleClose,
|
||||
@ -321,6 +364,7 @@ import {
|
||||
Upload,
|
||||
VideoPause,
|
||||
VideoPlay,
|
||||
Warning,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
const flowContainerRef = ref(null);
|
||||
@ -333,6 +377,76 @@ const queueDropTargetId = ref("");
|
||||
const { onFlowEvent } = useFlowEvents();
|
||||
window.lf = lf;
|
||||
|
||||
const recoverSafetyDialogVisible = ref(false);
|
||||
const recoverRobotLoading = ref(false);
|
||||
const recoverSafetyLoading = ref(false);
|
||||
const recoverRobotOptions = ref([]);
|
||||
const recoverSafetyRobotIds = ref([]);
|
||||
|
||||
const openRecoverSafetyDialog = async () => {
|
||||
recoverSafetyDialogVisible.value = true;
|
||||
recoverRobotLoading.value = true;
|
||||
try {
|
||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||
recoverRobotOptions.value = (response.rows || []).filter((robot) => robot.robotId).map((robot) => ({
|
||||
value: robot.robotId,
|
||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||
}));
|
||||
recoverSafetyRobotIds.value = recoverSafetyRobotIds.value.filter((robotId) => (
|
||||
recoverRobotOptions.value.some((option) => option.value === robotId)
|
||||
));
|
||||
if (!recoverSafetyRobotIds.value.length && recoverRobotOptions.value.length === 1) {
|
||||
recoverSafetyRobotIds.value = [recoverRobotOptions.value[0].value];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载在线机器人失败:", error);
|
||||
recoverRobotOptions.value = [];
|
||||
} finally {
|
||||
recoverRobotLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmRecoverSafetyState = async () => {
|
||||
if (!recoverSafetyRobotIds.value.length) {
|
||||
ElMessage.warning("请选择需要恢复的机器人");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将清除 ${recoverSafetyRobotIds.value.length} 台机器人的可恢复错误状态,确认继续?`,
|
||||
"安全状态确认",
|
||||
{ type: "warning", confirmButtonText: "确认清除", cancelButtonText: "取消" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
recoverSafetyLoading.value = true;
|
||||
const robotIds = [...recoverSafetyRobotIds.value];
|
||||
try {
|
||||
const results = await Promise.allSettled(robotIds.map((robotId) => recoverSafetyState({
|
||||
robotId,
|
||||
reason: "工作流设计器手动清除错误状态",
|
||||
timeoutMs: 15000,
|
||||
})));
|
||||
const failedRobotIds = results.flatMap((result, index) => (
|
||||
result.status === "rejected" ? [robotIds[index]] : []
|
||||
));
|
||||
if (!failedRobotIds.length) {
|
||||
ElMessage.success(`已清除 ${robotIds.length} 台机器人的可恢复错误状态`);
|
||||
recoverSafetyDialogVisible.value = false;
|
||||
recoverSafetyRobotIds.value = [];
|
||||
} else {
|
||||
const successCount = robotIds.length - failedRobotIds.length;
|
||||
recoverSafetyRobotIds.value = failedRobotIds;
|
||||
ElMessage.error(successCount
|
||||
? `${successCount} 台成功,${failedRobotIds.length} 台失败,请重试失败项`
|
||||
: "清除错误状态失败,请检查机器人状态");
|
||||
}
|
||||
} finally {
|
||||
recoverSafetyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
type: "flow-edge",
|
||||
animated: true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user