feat: 添加腾讯离线和实时 ASR 模型配置并优化相关逻辑
- 在 `AiModelServiceImpl` 和 `AiTaskServiceImpl` 中添加处理腾讯离线和实时 ASR 模型的逻辑 - 更新前端 `AiModels` 页面,支持腾讯 ASR 配置表单字段 - 在 `RealtimeMeetingSocketSessionServiceImpl` 中添加解析腾讯实时模型代码的方法 - 更新单元测试以验证腾讯 ASR 模型保存逻辑dev_na
parent
982425e21e
commit
57f343ddfd
|
|
@ -205,7 +205,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
|
|
@ -281,7 +281,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
private static final String MEDIA_TENCENT_APP_ID = "tencentAppId";
|
||||
private static final String MEDIA_TENCENT_SECRET_ID = "tencentSecretId";
|
||||
private static final String MEDIA_TENCENT_SECRET_KEY = "tencentSecretKey";
|
||||
private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode";
|
||||
private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode";
|
||||
private static final int DEFAULT_SORT_ORDER = 0;
|
||||
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
|
||||
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
|
||||
|
|
@ -964,8 +966,11 @@ public class AiModelServiceImpl implements AiModelService {
|
|||
if (readConfigString(mediaConfig.get(MEDIA_TENCENT_SECRET_KEY)) == null) {
|
||||
throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentSecretKey");
|
||||
}
|
||||
if (dto.getModelCode() == null || dto.getModelCode().isBlank()) {
|
||||
throw new RuntimeException("腾讯 ASR 模型必须配置 modelCode");
|
||||
if (readConfigString(mediaConfig.get(MEDIA_TENCENT_OFFLINE_MODEL_CODE)) == null) {
|
||||
throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentOfflineModelCode");
|
||||
}
|
||||
if (readConfigString(mediaConfig.get(MEDIA_TENCENT_REALTIME_MODEL_CODE)) == null) {
|
||||
throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentRealtimeModelCode");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private static final String MEDIA_TENCENT_APP_ID = "tencentAppId";
|
||||
private static final String MEDIA_TENCENT_SECRET_ID = "tencentSecretId";
|
||||
private static final String MEDIA_TENCENT_SECRET_KEY = "tencentSecretKey";
|
||||
private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode";
|
||||
private static final String TENCENT_ASR_REGION = "ap-guangzhou";
|
||||
|
||||
private final MeetingMapper meetingMapper;
|
||||
|
|
@ -756,7 +757,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
protected Map<String, Object> buildTencentOfflineCreateRequest(Meeting meeting, AiTask taskRecord, AiModelVO asrModel) {
|
||||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("engineModelType", asrModel.getModelCode());
|
||||
req.put("engineModelType", resolveTencentOfflineModelCode(asrModel));
|
||||
req.put("channelNum", 1L);
|
||||
req.put("resTextFormat", 2L);
|
||||
req.put("sourceType", 0L);
|
||||
|
|
@ -1646,6 +1647,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
transcript.setEndTime(endTime);
|
||||
}
|
||||
|
||||
private String resolveTencentOfflineModelCode(AiModelVO asrModel) {
|
||||
Map<String, Object> mediaConfig = asrModel == null || asrModel.getMediaConfig() == null ? Map.of() : asrModel.getMediaConfig();
|
||||
String offlineModelCode = stringValue(mediaConfig.get(MEDIA_TENCENT_OFFLINE_MODEL_CODE));
|
||||
if (offlineModelCode != null && !offlineModelCode.isBlank()) {
|
||||
return offlineModelCode.trim();
|
||||
}
|
||||
return asrModel == null ? null : asrModel.getModelCode();
|
||||
}
|
||||
|
||||
private AiModelVO resolveAsrModelForRevision(AiTask asrTask) {
|
||||
if (asrTask == null || asrTask.getTaskConfig() == null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import java.util.UUID;
|
|||
public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingSocketSessionService {
|
||||
|
||||
private static final String WS_PATH = "/ws/meeting/realtime";
|
||||
private static final String TENCENT_PROVIDER = "tencent";
|
||||
private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode";
|
||||
|
||||
private final RealtimeMeetingSocketSessionCache socketSessionCache;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
|
|
@ -87,7 +89,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
sessionData.setAsrModelId(asrModelId);
|
||||
sessionData.setProvider(realtimeAsrChannelFactory.normalizeProvider(asrModel.getProvider()));
|
||||
sessionData.setTargetWsUrl(targetWsUrl);
|
||||
sessionData.setModelCode(asrModel.getModelCode());
|
||||
sessionData.setModelCode(resolveRealtimeModelCode(asrModel));
|
||||
sessionData.setMediaConfig(asrModel.getMediaConfig());
|
||||
|
||||
String sessionToken = UUID.randomUUID().toString().replace("-", "");
|
||||
|
|
@ -111,6 +113,25 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
return vo;
|
||||
}
|
||||
|
||||
private String resolveRealtimeModelCode(AiModelVO asrModel) {
|
||||
if (asrModel == null) {
|
||||
return null;
|
||||
}
|
||||
if (!TENCENT_PROVIDER.equalsIgnoreCase(asrModel.getProvider())) {
|
||||
return asrModel.getModelCode();
|
||||
}
|
||||
Map<String, Object> mediaConfig = asrModel.getMediaConfig();
|
||||
if (mediaConfig == null) {
|
||||
return asrModel.getModelCode();
|
||||
}
|
||||
Object realtimeModelCode = mediaConfig.get(MEDIA_TENCENT_REALTIME_MODEL_CODE);
|
||||
if (realtimeModelCode == null) {
|
||||
return asrModel.getModelCode();
|
||||
}
|
||||
String value = String.valueOf(realtimeModelCode).trim();
|
||||
return value.isEmpty() ? asrModel.getModelCode() : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealtimeSocketSessionData getSessionData(String sessionToken) {
|
||||
return socketSessionCache.get(sessionToken);
|
||||
|
|
|
|||
|
|
@ -397,6 +397,31 @@ class AiModelServiceImplTest {
|
|||
assertEquals("腾讯 ASR 模型必须配置 mediaConfig.tencentSecretKey", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveModelShouldRejectTencentAsrWithoutRealtimeModelCode() {
|
||||
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||
objectMapper,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
AiModelDTO dto = new AiModelDTO();
|
||||
dto.setModelType("ASR");
|
||||
dto.setModelName("tencent-asr");
|
||||
dto.setProvider("tencent");
|
||||
dto.setIsDefault(0);
|
||||
dto.setStatus(1);
|
||||
dto.setMediaConfig(Map.of(
|
||||
"tencentAppId", "app-id",
|
||||
"tencentSecretId", "secret-id",
|
||||
"tencentSecretKey", "secret-key",
|
||||
"tencentOfflineModelCode", "16k_zh"
|
||||
));
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> service.saveModel(dto));
|
||||
assertEquals("腾讯 ASR 模型必须配置 mediaConfig.tencentRealtimeModelCode", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveModelShouldPersistTencentAsrWithoutBaseUrl() {
|
||||
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||
|
|
@ -412,13 +437,14 @@ class AiModelServiceImplTest {
|
|||
dto.setModelType("ASR");
|
||||
dto.setModelName("tencent-asr");
|
||||
dto.setProvider("tencent");
|
||||
dto.setModelCode("16k_zh");
|
||||
dto.setIsDefault(0);
|
||||
dto.setStatus(1);
|
||||
dto.setMediaConfig(Map.of(
|
||||
"tencentAppId", "app-id",
|
||||
"tencentSecretId", "secret-id",
|
||||
"tencentSecretKey", "secret-key"
|
||||
"tencentSecretKey", "secret-key",
|
||||
"tencentOfflineModelCode", "16k_zh",
|
||||
"tencentRealtimeModelCode", "16k_zh_realtime"
|
||||
));
|
||||
|
||||
service.saveModel(dto);
|
||||
|
|
@ -426,7 +452,9 @@ class AiModelServiceImplTest {
|
|||
ArgumentCaptor<AsrModel> captor = ArgumentCaptor.forClass(AsrModel.class);
|
||||
verify(asrModelMapper, times(1)).insert(captor.capture());
|
||||
assertEquals("tencent", captor.getValue().getProvider());
|
||||
assertEquals("16k_zh", captor.getValue().getModelCode());
|
||||
assertNull(captor.getValue().getModelCode());
|
||||
assertEquals("16k_zh", captor.getValue().getMediaConfig().get("tencentOfflineModelCode"));
|
||||
assertEquals("16k_zh_realtime", captor.getValue().getMediaConfig().get("tencentRealtimeModelCode"));
|
||||
assertEquals("secret-key", captor.getValue().getMediaConfig().get("tencentSecretKey"));
|
||||
assertNull(captor.getValue().getBaseUrl());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,11 +133,13 @@ class AiTaskServiceImplTest {
|
|||
)));
|
||||
|
||||
AiModelVO model = new AiModelVO();
|
||||
model.setModelCode("16k_zh");
|
||||
model.setModelCode("legacy-model-code");
|
||||
model.setMediaConfig(Map.of(
|
||||
"tencentAppId", "123456",
|
||||
"tencentSecretId", "secret-id",
|
||||
"tencentSecretKey", "secret-key"
|
||||
"tencentSecretKey", "secret-key",
|
||||
"tencentOfflineModelCode", "16k_zh",
|
||||
"tencentRealtimeModelCode", "16k_zh_realtime"
|
||||
));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
@ -211,11 +213,13 @@ class AiTaskServiceImplTest {
|
|||
|
||||
AiModelVO model = new AiModelVO();
|
||||
model.setProvider("tencent");
|
||||
model.setModelCode("16k_zh");
|
||||
model.setModelCode("legacy-model-code");
|
||||
model.setMediaConfig(Map.of(
|
||||
"tencentAppId", "123456",
|
||||
"tencentSecretId", "secret-id",
|
||||
"tencentSecretKey", "secret-key"
|
||||
"tencentSecretKey", "secret-key",
|
||||
"tencentOfflineModelCode", "16k_zh",
|
||||
"tencentRealtimeModelCode", "16k_zh_realtime"
|
||||
));
|
||||
|
||||
TaskStatus doing = new TaskStatus();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
testLocalModelConnectivity,
|
||||
updateAiModel,
|
||||
} from "../../api/business/aimodel";
|
||||
import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting";
|
||||
import AppPagination from "../../components/shared/AppPagination";
|
||||
|
||||
const { Option } = Select;
|
||||
|
|
@ -31,6 +32,12 @@ const { Title } = Typography;
|
|||
|
||||
type ModelType = "ASR" | "LLM";
|
||||
|
||||
const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = {
|
||||
offlineEnabled: true,
|
||||
realtimeEnabled: false,
|
||||
offlineAudioMaxSizeMb: 1024,
|
||||
};
|
||||
|
||||
const PROVIDER_BASE_URL_MAP: Record<string, string> = {
|
||||
openai: "https://api.openai.com",
|
||||
deepseek: "https://api.deepseek.com",
|
||||
|
|
@ -64,6 +71,7 @@ const AiModels: React.FC = () => {
|
|||
const [connectivityLoading, setConnectivityLoading] = useState(false);
|
||||
const [remoteModels, setRemoteModels] = useState<string[]>([]);
|
||||
const [speakerModels, setSpeakerModels] = useState<string[]>([]);
|
||||
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>(DEFAULT_CREATE_CONFIG);
|
||||
const modelNameAutoFilledRef = useRef(false);
|
||||
const localProfileLoadedRef = useRef(false);
|
||||
|
||||
|
|
@ -86,6 +94,20 @@ const AiModels: React.FC = () => {
|
|||
void fetchData();
|
||||
}, [current, size, searchName, activeType]);
|
||||
|
||||
useEffect(() => {
|
||||
getMeetingCreateConfig()
|
||||
.then((res) => {
|
||||
const config = (res as any)?.data?.data ?? (res as any);
|
||||
setCreateConfig({
|
||||
...DEFAULT_CREATE_CONFIG,
|
||||
...(config || {}),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setCreateConfig(DEFAULT_CREATE_CONFIG);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible || !provider) {
|
||||
return;
|
||||
|
|
@ -137,6 +159,8 @@ const AiModels: React.FC = () => {
|
|||
const tencentAppId = record.mediaConfig?.tencentAppId;
|
||||
const tencentSecretId = record.mediaConfig?.tencentSecretId;
|
||||
const tencentSecretKey = record.mediaConfig?.tencentSecretKey;
|
||||
const tencentOfflineModelCode = record.mediaConfig?.tencentOfflineModelCode || record.modelCode;
|
||||
const tencentRealtimeModelCode = record.mediaConfig?.tencentRealtimeModelCode || record.modelCode;
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
modelType: record.modelType,
|
||||
|
|
@ -145,6 +169,8 @@ const AiModels: React.FC = () => {
|
|||
tencentAppId,
|
||||
tencentSecretId,
|
||||
tencentSecretKey,
|
||||
tencentOfflineModelCode,
|
||||
tencentRealtimeModelCode,
|
||||
isDefaultChecked: record.isDefault === 1,
|
||||
statusChecked: record.status === 1,
|
||||
});
|
||||
|
|
@ -261,8 +287,8 @@ const AiModels: React.FC = () => {
|
|||
baseUrl: values.baseUrl,
|
||||
apiPath: values.apiPath,
|
||||
apiKey: values.apiKey,
|
||||
modelCode: values.modelCode,
|
||||
wsUrl: values.wsUrl,
|
||||
modelCode: activeType === "ASR" && isTencentProvider ? values.tencentOfflineModelCode : values.modelCode,
|
||||
wsUrl: activeType === "ASR" ? values.wsUrl : undefined,
|
||||
mediaConfig:
|
||||
activeType === "ASR" && isLocalProvider
|
||||
? {
|
||||
|
|
@ -274,6 +300,8 @@ const AiModels: React.FC = () => {
|
|||
tencentAppId: values.tencentAppId,
|
||||
tencentSecretId: values.tencentSecretId,
|
||||
tencentSecretKey: values.tencentSecretKey,
|
||||
tencentOfflineModelCode: values.tencentOfflineModelCode,
|
||||
tencentRealtimeModelCode: values.tencentRealtimeModelCode,
|
||||
}
|
||||
: undefined,
|
||||
temperature: values.temperature,
|
||||
|
|
@ -567,13 +595,14 @@ const AiModels: React.FC = () => {
|
|||
<Form.Item
|
||||
label="模型名称"
|
||||
required={activeType === "LLM"}
|
||||
hidden={activeType === "ASR" && isTencentProvider}
|
||||
tooltip="可从远程列表选择,也可手动输入;值将作为模型 code 传给后端"
|
||||
>
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
name="modelCode"
|
||||
noStyle
|
||||
rules={activeType === "LLM" || isTencentProvider ? [{
|
||||
rules={activeType === "LLM" ? [{
|
||||
required: true,
|
||||
message: "请输入或选择模型名称"
|
||||
}] : []}
|
||||
|
|
@ -602,7 +631,8 @@ const AiModels: React.FC = () => {
|
|||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="wsUrl" label="WebSocket 地址" hidden>
|
||||
<Form.Item name="wsUrl" label="WebSocket 地址"
|
||||
hidden={!(activeType === "ASR" && createConfig.realtimeEnabled)}>
|
||||
<Input placeholder="wss://api.example.com/v1/ws" />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -660,6 +690,24 @@ const AiModels: React.FC = () => {
|
|||
<Input.Password/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="tencentOfflineModelCode"
|
||||
label="离线识别模型名"
|
||||
rules={[{required: true, message: "请输入离线识别模型名"}]}
|
||||
>
|
||||
<Input placeholder="例如:16k_zh"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="tencentRealtimeModelCode"
|
||||
label="实时识别模型名"
|
||||
rules={[{required: true, message: "请输入实时识别模型名"}]}
|
||||
>
|
||||
<Input placeholder="例如:16k_zh_realtime"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue