import { useEffect, useMemo, useState } from "react"; import { Alert, Avatar, Button, Card, Col, DatePicker, Form, Input, Row, Select, Space, Spin, Statistic, Switch, Tag, Tooltip, Typography, message, } from "antd"; import { AudioOutlined, CheckCircleOutlined, LinkOutlined, MessageOutlined, QuestionCircleOutlined, RocketOutlined, TeamOutlined, UserOutlined, } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import dayjs from "dayjs"; import PageHeader from "../../components/shared/PageHeader"; import { listUsers } from "../../api"; import { getAiModelDefault, getAiModelPage, type AiModelVO } from "../../api/business/aimodel"; import { getHotWordPage, type HotWordVO } from "../../api/business/hotword"; import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt"; import { createRealtimeMeeting, type CreateRealtimeMeetingCommand } from "../../api/business/meeting"; import type { SysUser } from "../../types"; const { Title, Text } = Typography; const { Option } = Select; type RealtimeMeetingSessionDraft = { meetingId: number; meetingTitle: string; asrModelName: string; summaryModelName: string; asrModelId: number; mode: string; language: string; useSpkId: number; enablePunctuation: boolean; enableItn: boolean; enableTextRefine: boolean; saveAudio: boolean; hotwords: Array<{ hotword: string; weight: number }>; }; function resolveWsUrl(model?: AiModelVO | null) { if (model?.wsUrl) { return model.wsUrl; } if (model?.baseUrl) { return model.baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://"); } return ""; } function buildRealtimeProxyPreviewUrl() { const protocol = window.location.protocol === "https:" ? "wss" : "ws"; return `${protocol}://${window.location.host}/ws/meeting/realtime`; } function getSessionKey(meetingId: number) { return `realtimeMeetingSession:${meetingId}`; } export default function RealtimeAsr() { const navigate = useNavigate(); const [form] = Form.useForm(); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [asrModels, setAsrModels] = useState([]); const [llmModels, setLlmModels] = useState([]); const [prompts, setPrompts] = useState([]); const [hotwordList, setHotwordList] = useState([]); const [userList, setUserList] = useState([]); const watchedAsrModelId = Form.useWatch("asrModelId", form); const watchedSummaryModelId = Form.useWatch("summaryModelId", form); const watchedHotWords = Form.useWatch("hotWords", form) || []; const watchedParticipants = Form.useWatch("participants", form) || []; const watchedUseSpkId = Form.useWatch("useSpkId", form); const selectedAsrModel = useMemo( () => asrModels.find((item) => item.id === watchedAsrModelId) || null, [asrModels, watchedAsrModelId], ); const selectedSummaryModel = useMemo( () => llmModels.find((item) => item.id === watchedSummaryModelId) || null, [llmModels, watchedSummaryModelId], ); const selectedHotwordCount = watchedHotWords.length > 0 ? watchedHotWords.length : hotwordList.length; useEffect(() => { const loadInitialData = async () => { setLoading(true); try { const [asrRes, llmRes, promptRes, hotwordRes, users, defaultAsr, defaultLlm] = await Promise.all([ getAiModelPage({ current: 1, size: 100, type: "ASR" }), getAiModelPage({ current: 1, size: 100, type: "LLM" }), getPromptPage({ current: 1, size: 100 }), getHotWordPage({ current: 1, size: 1000 }), listUsers(), getAiModelDefault("ASR"), getAiModelDefault("LLM"), ]); const activeAsrModels = asrRes.data.data.records.filter((item) => item.status === 1); const activeLlmModels = llmRes.data.data.records.filter((item) => item.status === 1); const activePrompts = promptRes.data.data.records.filter((item) => item.status === 1); const activeHotwords = hotwordRes.data.data.records.filter((item) => item.status === 1); setAsrModels(activeAsrModels); setLlmModels(activeLlmModels); setPrompts(activePrompts); setHotwordList(activeHotwords); setUserList(users || []); form.setFieldsValue({ title: `实时会议 ${dayjs().format("MM-DD HH:mm")}`, meetingTime: dayjs(), asrModelId: defaultAsr.data.data?.id, summaryModelId: defaultLlm.data.data?.id, promptId: activePrompts[0]?.id, useSpkId: 1, mode: "2pass", language: "auto", enablePunctuation: true, enableItn: true, enableTextRefine: false, saveAudio: false, }); } catch { message.error("加载实时会议配置失败"); } finally { setLoading(false); } }; void loadInitialData(); }, [form]); const handleCreate = async () => { const values = await form.validateFields(); const wsUrl = resolveWsUrl(selectedAsrModel); if (!wsUrl) { message.error("当前 ASR 模型没有配置 WebSocket 地址"); return; } setSubmitting(true); try { const selectedHotwords = (values.hotWords?.length ? hotwordList.filter((item) => values.hotWords.includes(item.word)) : hotwordList ).map((item) => ({ hotword: item.word, weight: Number(item.weight || 2) / 10, })); const payload: CreateRealtimeMeetingCommand = { ...values, meetingTime: values.meetingTime.format("YYYY-MM-DD HH:mm:ss"), participants: values.participants?.join(",") || "", tags: values.tags?.join(",") || "", mode: values.mode || "2pass", language: values.language || "auto", useSpkId: values.useSpkId ? 1 : 0, enablePunctuation: values.enablePunctuation !== false, enableItn: values.enableItn !== false, enableTextRefine: !!values.enableTextRefine, saveAudio: !!values.saveAudio, hotWords: values.hotWords, }; const res = await createRealtimeMeeting(payload); const createdMeeting = res.data.data; const sessionDraft: RealtimeMeetingSessionDraft = { meetingId: createdMeeting.id, meetingTitle: createdMeeting.title, asrModelName: selectedAsrModel?.modelName || "ASR", summaryModelName: selectedSummaryModel?.modelName || "LLM", asrModelId: selectedAsrModel?.id || values.asrModelId, mode: values.mode || "2pass", language: values.language || "auto", useSpkId: values.useSpkId ? 1 : 0, enablePunctuation: values.enablePunctuation !== false, enableItn: values.enableItn !== false, enableTextRefine: !!values.enableTextRefine, saveAudio: !!values.saveAudio, hotwords: selectedHotwords, }; sessionStorage.setItem(getSessionKey(createdMeeting.id), JSON.stringify(sessionDraft)); message.success("会议已创建,进入实时识别"); navigate(`/meeting-live-session/${createdMeeting.id}`); } catch { message.error("创建实时会议失败"); } finally { setSubmitting(false); } }; return (
{loading ? (
) : (
创建实时会议 会前完成配置后再进入会中识别页。
单页配置 会中专注转写 异常关闭自动兜底
{asrModels.map((model) => ( ))} 热词增强{" "} } > 说话人区分 } valuePropName="checked" getValueProps={(value) => ({ checked: value === 1 || value === true })} normalize={(value) => (value ? 1 : 0)} > } readOnly />
} /> } /> } /> } />
本次识别摘要
ASR {selectedAsrModel?.modelName || "-"}
LLM {selectedSummaryModel?.modelName || "-"}
WebSocket {buildRealtimeProxyPreviewUrl()}
创建成功后会直接进入识别页,不会在当前页占用麦克风。
)}
); }