配置ASR模型的页面优化
parent
349cdf26e6
commit
2d502751f5
|
|
@ -21,10 +21,45 @@ public class SpeechRecognitionSchemaInitializer implements ApplicationRunner {
|
|||
public void run(ApplicationArguments args) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("""
|
||||
create table if not exists speech_recognition_service (
|
||||
id bigint generated by default as identity primary key,
|
||||
service_type varchar(64) not null,
|
||||
service_name varchar(128) not null,
|
||||
built_in boolean not null default true,
|
||||
enabled boolean not null default true,
|
||||
base_url varchar(512),
|
||||
transcribe_path varchar(255) not null default '/v1/audio/transcriptions',
|
||||
task_status_path varchar(255) not null default '/v1/tasks/{task_id}',
|
||||
file_field_name varchar(64) not null default 'file',
|
||||
language varchar(32),
|
||||
response_format varchar(32) not null default 'json',
|
||||
model varchar(128),
|
||||
prompt text,
|
||||
hotwords text,
|
||||
timestamp_granularities varchar(128),
|
||||
enable_speaker_diarization boolean,
|
||||
enable_speaker_identification boolean,
|
||||
enable_text_cleanup boolean,
|
||||
temperature double precision,
|
||||
api_key varchar(512),
|
||||
api_key_header varchar(128) not null default 'X-API-Key',
|
||||
connect_timeout_seconds integer not null default 10,
|
||||
read_timeout_seconds integer not null default 300,
|
||||
task_timeout_seconds integer not null default 300,
|
||||
task_poll_interval_millis integer not null default 1500,
|
||||
sort_order integer not null default 100,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint uk_speech_recognition_service_type unique (service_type)
|
||||
)
|
||||
""");
|
||||
statement.execute("alter table if exists speech_recognition_service add column if not exists built_in boolean not null default true");
|
||||
statement.execute("""
|
||||
create table if not exists speech_recognition_config (
|
||||
id bigint generated by default as identity primary key,
|
||||
tenant_id bigint not null,
|
||||
service_type varchar(64) not null default 'CUSTOM',
|
||||
enabled boolean not null default true,
|
||||
base_url varchar(512),
|
||||
transcribe_path varchar(255) not null default '/v1/audio/transcriptions',
|
||||
|
|
@ -51,6 +86,7 @@ public class SpeechRecognitionSchemaInitializer implements ApplicationRunner {
|
|||
constraint uk_speech_recognition_config_tenant unique (tenant_id)
|
||||
)
|
||||
""");
|
||||
statement.execute("alter table if exists speech_recognition_config add column if not exists service_type varchar(64) not null default 'CUSTOM'");
|
||||
statement.execute("alter table if exists speech_recognition_config add column if not exists response_format varchar(32) not null default 'json'");
|
||||
statement.execute("alter table if exists speech_recognition_config add column if not exists model varchar(128)");
|
||||
statement.execute("alter table if exists speech_recognition_config add column if not exists prompt text");
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@ package com.unis.crm.controller;
|
|||
|
||||
import com.unis.crm.common.ApiResponse;
|
||||
import com.unis.crm.dto.speech.SpeechRecognitionConfigDTO;
|
||||
import com.unis.crm.dto.speech.SpeechRecognitionServiceDTO;
|
||||
import com.unis.crm.service.SpeechRecognitionConfigService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
|
@ -27,6 +32,22 @@ public class SpeechRecognitionAdminController {
|
|||
return ApiResponse.success(speechRecognitionConfigService.getConfig(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/speech-recognition-services")
|
||||
public ApiResponse<List<SpeechRecognitionServiceDTO>> listSpeechRecognitionServices() {
|
||||
return ApiResponse.success(speechRecognitionConfigService.listServices());
|
||||
}
|
||||
|
||||
@PostMapping("/speech-recognition-services")
|
||||
public ApiResponse<SpeechRecognitionServiceDTO> createSpeechRecognitionService(
|
||||
@Valid @RequestBody SpeechRecognitionServiceDTO payload) {
|
||||
return ApiResponse.success(speechRecognitionConfigService.saveCustomService(payload));
|
||||
}
|
||||
|
||||
@DeleteMapping("/speech-recognition-services/{serviceType}")
|
||||
public ApiResponse<Boolean> deleteSpeechRecognitionService(@PathVariable String serviceType) {
|
||||
return ApiResponse.success(speechRecognitionConfigService.deleteCustomService(serviceType));
|
||||
}
|
||||
|
||||
@PutMapping("/speech-recognition-config")
|
||||
public ApiResponse<Boolean> updateSpeechRecognitionConfig(@Valid @RequestBody SpeechRecognitionConfigDTO payload) {
|
||||
return ApiResponse.success(speechRecognitionConfigService.saveConfig(payload));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import jakarta.validation.constraints.Min;
|
|||
public class SpeechRecognitionConfigDTO {
|
||||
|
||||
private Long tenantId;
|
||||
private String serviceType = "CUSTOM";
|
||||
private boolean enabled = true;
|
||||
private String baseUrl;
|
||||
private String transcribePath = "/v1/audio/transcriptions";
|
||||
|
|
@ -48,6 +49,14 @@ public class SpeechRecognitionConfigDTO {
|
|||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
public String getServiceType() {
|
||||
return serviceType;
|
||||
}
|
||||
|
||||
public void setServiceType(String serviceType) {
|
||||
this.serviceType = serviceType;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.unis.crm.dto.speech;
|
||||
|
||||
public class SpeechRecognitionServiceDTO extends SpeechRecognitionConfigDTO {
|
||||
|
||||
private String serviceName;
|
||||
private boolean builtIn = true;
|
||||
private Integer sortOrder;
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public void setServiceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
public boolean isBuiltIn() {
|
||||
return builtIn;
|
||||
}
|
||||
|
||||
public void setBuiltIn(boolean builtIn) {
|
||||
this.builtIn = builtIn;
|
||||
}
|
||||
|
||||
public Integer getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(Integer sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,14 @@ package com.unis.crm.service;
|
|||
import com.unis.crm.common.BusinessException;
|
||||
import com.unis.crm.common.UnauthorizedException;
|
||||
import com.unis.crm.dto.speech.SpeechRecognitionConfigDTO;
|
||||
import com.unis.crm.dto.speech.SpeechRecognitionServiceDTO;
|
||||
import com.unisbase.security.PermissionService;
|
||||
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -20,6 +23,7 @@ public class SpeechRecognitionConfigService {
|
|||
private static final String VIEW_PERM = "speech_recognition_config:view";
|
||||
private static final String UPDATE_PERM = "speech_recognition_config:update";
|
||||
private static final long GLOBAL_TENANT_ID = 0L;
|
||||
private static final String DEFAULT_SERVICE_TYPE = "CUSTOM";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PermissionService permissionService;
|
||||
|
|
@ -38,7 +42,151 @@ public class SpeechRecognitionConfigService {
|
|||
requirePermission(VIEW_PERM, "无权查看语音识别配置");
|
||||
Long resolvedTenantId = resolveTenantId(tenantId);
|
||||
SpeechRecognitionConfigDTO config = findConfig(resolvedTenantId);
|
||||
return config == null ? defaultConfig(resolvedTenantId) : config;
|
||||
return applyServicePreset(config == null ? defaultConfig(resolvedTenantId) : config);
|
||||
}
|
||||
|
||||
public List<SpeechRecognitionServiceDTO> listServices() {
|
||||
requirePermission(VIEW_PERM, "无权查看语音识别配置");
|
||||
return jdbcTemplate.query("""
|
||||
select service_type,
|
||||
service_name,
|
||||
built_in,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
task_status_path,
|
||||
file_field_name,
|
||||
language,
|
||||
response_format,
|
||||
model,
|
||||
prompt,
|
||||
hotwords,
|
||||
timestamp_granularities,
|
||||
enable_speaker_diarization,
|
||||
enable_speaker_identification,
|
||||
enable_text_cleanup,
|
||||
temperature,
|
||||
api_key,
|
||||
api_key_header,
|
||||
connect_timeout_seconds,
|
||||
read_timeout_seconds,
|
||||
task_timeout_seconds,
|
||||
task_poll_interval_millis,
|
||||
sort_order
|
||||
from speech_recognition_service
|
||||
where enabled = true
|
||||
order by sort_order, service_name, service_type
|
||||
""", serviceRowMapper());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SpeechRecognitionServiceDTO saveCustomService(SpeechRecognitionServiceDTO payload) {
|
||||
requirePermission(UPDATE_PERM, "无权修改语音识别配置");
|
||||
SpeechRecognitionServiceDTO normalized = normalizeCustomService(payload);
|
||||
SpeechRecognitionServiceDTO existing = findAnyService(normalized.getServiceType());
|
||||
if (existing != null && existing.isBuiltIn()) {
|
||||
throw new BusinessException("预设语音服务不允许修改");
|
||||
}
|
||||
jdbcTemplate.update("""
|
||||
insert into speech_recognition_service (
|
||||
service_type,
|
||||
service_name,
|
||||
built_in,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
task_status_path,
|
||||
file_field_name,
|
||||
language,
|
||||
response_format,
|
||||
model,
|
||||
prompt,
|
||||
hotwords,
|
||||
timestamp_granularities,
|
||||
enable_speaker_diarization,
|
||||
enable_speaker_identification,
|
||||
enable_text_cleanup,
|
||||
temperature,
|
||||
api_key,
|
||||
api_key_header,
|
||||
connect_timeout_seconds,
|
||||
read_timeout_seconds,
|
||||
task_timeout_seconds,
|
||||
task_poll_interval_millis,
|
||||
sort_order,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
values (?, ?, false, true, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
on conflict (service_type) do update
|
||||
set service_name = excluded.service_name,
|
||||
enabled = excluded.enabled,
|
||||
base_url = excluded.base_url,
|
||||
transcribe_path = excluded.transcribe_path,
|
||||
task_status_path = excluded.task_status_path,
|
||||
file_field_name = excluded.file_field_name,
|
||||
language = excluded.language,
|
||||
response_format = excluded.response_format,
|
||||
model = excluded.model,
|
||||
prompt = excluded.prompt,
|
||||
hotwords = excluded.hotwords,
|
||||
timestamp_granularities = excluded.timestamp_granularities,
|
||||
enable_speaker_diarization = excluded.enable_speaker_diarization,
|
||||
enable_speaker_identification = excluded.enable_speaker_identification,
|
||||
enable_text_cleanup = excluded.enable_text_cleanup,
|
||||
temperature = excluded.temperature,
|
||||
api_key = excluded.api_key,
|
||||
api_key_header = excluded.api_key_header,
|
||||
connect_timeout_seconds = excluded.connect_timeout_seconds,
|
||||
read_timeout_seconds = excluded.read_timeout_seconds,
|
||||
task_timeout_seconds = excluded.task_timeout_seconds,
|
||||
task_poll_interval_millis = excluded.task_poll_interval_millis,
|
||||
sort_order = excluded.sort_order,
|
||||
updated_at = now()
|
||||
""",
|
||||
normalized.getServiceType(),
|
||||
normalized.getServiceName(),
|
||||
normalized.getBaseUrl(),
|
||||
normalized.getTranscribePath(),
|
||||
normalized.getTaskStatusPath(),
|
||||
normalized.getFileFieldName(),
|
||||
normalized.getLanguage(),
|
||||
normalized.getResponseFormat(),
|
||||
normalized.getModel(),
|
||||
normalized.getPrompt(),
|
||||
normalized.getHotwords(),
|
||||
normalized.getTimestampGranularities(),
|
||||
normalized.getEnableSpeakerDiarization(),
|
||||
normalized.getEnableSpeakerIdentification(),
|
||||
normalized.getEnableTextCleanup(),
|
||||
normalized.getTemperature(),
|
||||
normalized.getApiKey(),
|
||||
normalized.getApiKeyHeader(),
|
||||
normalized.getConnectTimeoutSeconds(),
|
||||
normalized.getReadTimeoutSeconds(),
|
||||
normalized.getTaskTimeoutSeconds(),
|
||||
normalized.getTaskPollIntervalMillis(),
|
||||
normalized.getSortOrder());
|
||||
return findAnyService(normalized.getServiceType());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean deleteCustomService(String serviceType) {
|
||||
requirePermission(UPDATE_PERM, "无权修改语音识别配置");
|
||||
String normalizedServiceType = trimToNull(serviceType);
|
||||
if (normalizedServiceType == null || "CUSTOM".equalsIgnoreCase(normalizedServiceType)) {
|
||||
return true;
|
||||
}
|
||||
SpeechRecognitionServiceDTO existing = findAnyService(normalizedServiceType);
|
||||
if (existing == null) {
|
||||
return true;
|
||||
}
|
||||
if (existing.isBuiltIn()) {
|
||||
throw new BusinessException("预设语音服务不允许删除");
|
||||
}
|
||||
jdbcTemplate.update("delete from speech_recognition_service where service_type = ? and built_in = false", normalizedServiceType);
|
||||
jdbcTemplate.update("update speech_recognition_config set service_type = 'CUSTOM', updated_at = now() where service_type = ?", normalizedServiceType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -49,6 +197,7 @@ public class SpeechRecognitionConfigService {
|
|||
jdbcTemplate.update("""
|
||||
insert into speech_recognition_config (
|
||||
tenant_id,
|
||||
service_type,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
|
|
@ -73,9 +222,10 @@ public class SpeechRecognitionConfigService {
|
|||
created_at,
|
||||
updated_at
|
||||
)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
on conflict (tenant_id) do update
|
||||
set enabled = excluded.enabled,
|
||||
set service_type = excluded.service_type,
|
||||
enabled = excluded.enabled,
|
||||
base_url = excluded.base_url,
|
||||
transcribe_path = excluded.transcribe_path,
|
||||
task_status_path = excluded.task_status_path,
|
||||
|
|
@ -99,6 +249,7 @@ public class SpeechRecognitionConfigService {
|
|||
updated_at = now()
|
||||
""",
|
||||
tenantId,
|
||||
normalized.getServiceType(),
|
||||
normalized.isEnabled(),
|
||||
normalized.getBaseUrl(),
|
||||
normalized.getTranscribePath(),
|
||||
|
|
@ -131,17 +282,17 @@ public class SpeechRecognitionConfigService {
|
|||
|
||||
SpeechRecognitionConfigDTO tenantConfig = findConfig(resolvedTenantId);
|
||||
if (tenantConfig != null) {
|
||||
return tenantConfig;
|
||||
return applyServicePreset(tenantConfig);
|
||||
}
|
||||
|
||||
if (resolvedTenantId != GLOBAL_TENANT_ID) {
|
||||
SpeechRecognitionConfigDTO globalConfig = findConfig(GLOBAL_TENANT_ID);
|
||||
if (globalConfig != null) {
|
||||
return globalConfig;
|
||||
return applyServicePreset(globalConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultConfig(resolvedTenantId);
|
||||
return applyServicePreset(defaultConfig(resolvedTenantId));
|
||||
}
|
||||
|
||||
private SpeechRecognitionConfigDTO normalizeConfig(SpeechRecognitionConfigDTO payload, Long tenantId) {
|
||||
|
|
@ -150,6 +301,7 @@ public class SpeechRecognitionConfigService {
|
|||
|
||||
SpeechRecognitionConfigDTO normalized = new SpeechRecognitionConfigDTO();
|
||||
normalized.setTenantId(tenantId);
|
||||
normalized.setServiceType(defaultIfBlank(config.getServiceType(), defaults.getServiceType()));
|
||||
normalized.setEnabled(config.isEnabled());
|
||||
normalized.setBaseUrl(defaultIfBlank(config.getBaseUrl(), defaults.getBaseUrl()));
|
||||
normalized.setTranscribePath(defaultIfBlank(config.getTranscribePath(), defaults.getTranscribePath()));
|
||||
|
|
@ -171,6 +323,7 @@ public class SpeechRecognitionConfigService {
|
|||
normalized.setReadTimeoutSeconds(clamp(config.getReadTimeoutSeconds(), 1, 1800, defaults.getReadTimeoutSeconds()));
|
||||
normalized.setTaskTimeoutSeconds(clamp(config.getTaskTimeoutSeconds(), 1, 1800, defaults.getTaskTimeoutSeconds()));
|
||||
normalized.setTaskPollIntervalMillis(clamp(config.getTaskPollIntervalMillis(), 300, 60000, defaults.getTaskPollIntervalMillis()));
|
||||
applyServicePreset(normalized);
|
||||
|
||||
if (normalized.isEnabled()) {
|
||||
validateBaseUrl(normalized.getBaseUrl());
|
||||
|
|
@ -181,6 +334,7 @@ public class SpeechRecognitionConfigService {
|
|||
private SpeechRecognitionConfigDTO defaultConfig(Long tenantId) {
|
||||
SpeechRecognitionConfigDTO config = new SpeechRecognitionConfigDTO();
|
||||
config.setTenantId(tenantId);
|
||||
config.setServiceType(DEFAULT_SERVICE_TYPE);
|
||||
config.setEnabled(true);
|
||||
config.setBaseUrl(null);
|
||||
config.setTranscribePath("/v1/audio/transcriptions");
|
||||
|
|
@ -203,6 +357,7 @@ public class SpeechRecognitionConfigService {
|
|||
}
|
||||
List<SpeechRecognitionConfigDTO> rows = jdbcTemplate.query("""
|
||||
select tenant_id,
|
||||
service_type,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
|
|
@ -231,10 +386,132 @@ public class SpeechRecognitionConfigService {
|
|||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private SpeechRecognitionServiceDTO findServicePreset(String serviceType) {
|
||||
String normalizedServiceType = trimToNull(serviceType);
|
||||
if (normalizedServiceType == null || "CUSTOM".equalsIgnoreCase(normalizedServiceType)) {
|
||||
return null;
|
||||
}
|
||||
List<SpeechRecognitionServiceDTO> rows = jdbcTemplate.query("""
|
||||
select service_type,
|
||||
service_name,
|
||||
built_in,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
task_status_path,
|
||||
file_field_name,
|
||||
language,
|
||||
response_format,
|
||||
model,
|
||||
prompt,
|
||||
hotwords,
|
||||
timestamp_granularities,
|
||||
enable_speaker_diarization,
|
||||
enable_speaker_identification,
|
||||
enable_text_cleanup,
|
||||
temperature,
|
||||
api_key,
|
||||
api_key_header,
|
||||
connect_timeout_seconds,
|
||||
read_timeout_seconds,
|
||||
task_timeout_seconds,
|
||||
task_poll_interval_millis,
|
||||
sort_order
|
||||
from speech_recognition_service
|
||||
where service_type = ?
|
||||
and enabled = true
|
||||
limit 1
|
||||
""", serviceRowMapper(), normalizedServiceType);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private SpeechRecognitionServiceDTO findAnyService(String serviceType) {
|
||||
String normalizedServiceType = trimToNull(serviceType);
|
||||
if (normalizedServiceType == null || "CUSTOM".equalsIgnoreCase(normalizedServiceType)) {
|
||||
return null;
|
||||
}
|
||||
List<SpeechRecognitionServiceDTO> rows = jdbcTemplate.query("""
|
||||
select service_type,
|
||||
service_name,
|
||||
built_in,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
task_status_path,
|
||||
file_field_name,
|
||||
language,
|
||||
response_format,
|
||||
model,
|
||||
prompt,
|
||||
hotwords,
|
||||
timestamp_granularities,
|
||||
enable_speaker_diarization,
|
||||
enable_speaker_identification,
|
||||
enable_text_cleanup,
|
||||
temperature,
|
||||
api_key,
|
||||
api_key_header,
|
||||
connect_timeout_seconds,
|
||||
read_timeout_seconds,
|
||||
task_timeout_seconds,
|
||||
task_poll_interval_millis,
|
||||
sort_order
|
||||
from speech_recognition_service
|
||||
where service_type = ?
|
||||
limit 1
|
||||
""", serviceRowMapper(), normalizedServiceType);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private SpeechRecognitionConfigDTO applyServicePreset(SpeechRecognitionConfigDTO config) {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
config.setServiceType(defaultIfBlank(config.getServiceType(), DEFAULT_SERVICE_TYPE));
|
||||
SpeechRecognitionServiceDTO preset = findServicePreset(config.getServiceType());
|
||||
if (preset == null) {
|
||||
return config;
|
||||
}
|
||||
|
||||
boolean forcePreset = preset.isBuiltIn();
|
||||
config.setBaseUrl(forcePreset ? preset.getBaseUrl() : defaultIfBlank(config.getBaseUrl(), preset.getBaseUrl()));
|
||||
config.setTranscribePath(forcePreset ? preset.getTranscribePath() : defaultIfBlank(config.getTranscribePath(), preset.getTranscribePath()));
|
||||
config.setTaskStatusPath(forcePreset ? preset.getTaskStatusPath() : defaultIfBlank(config.getTaskStatusPath(), preset.getTaskStatusPath()));
|
||||
config.setFileFieldName("file");
|
||||
config.setLanguage(forcePreset ? preset.getLanguage() : defaultIfBlank(config.getLanguage(), preset.getLanguage()));
|
||||
config.setResponseFormat(forcePreset ? preset.getResponseFormat() : defaultIfBlank(config.getResponseFormat(), preset.getResponseFormat()));
|
||||
config.setModel(forcePreset ? preset.getModel() : defaultIfBlank(config.getModel(), preset.getModel()));
|
||||
config.setPrompt(forcePreset ? preset.getPrompt() : defaultIfBlank(config.getPrompt(), preset.getPrompt()));
|
||||
config.setHotwords(forcePreset ? preset.getHotwords() : defaultIfBlank(config.getHotwords(), preset.getHotwords()));
|
||||
config.setTimestampGranularities(forcePreset ? preset.getTimestampGranularities() : defaultIfBlank(config.getTimestampGranularities(), preset.getTimestampGranularities()));
|
||||
if (forcePreset || config.getEnableSpeakerDiarization() == null) {
|
||||
config.setEnableSpeakerDiarization(preset.getEnableSpeakerDiarization());
|
||||
}
|
||||
if (forcePreset || config.getEnableSpeakerIdentification() == null) {
|
||||
config.setEnableSpeakerIdentification(preset.getEnableSpeakerIdentification());
|
||||
}
|
||||
if (forcePreset || config.getEnableTextCleanup() == null) {
|
||||
config.setEnableTextCleanup(preset.getEnableTextCleanup());
|
||||
}
|
||||
if (forcePreset || config.getTemperature() == null) {
|
||||
config.setTemperature(preset.getTemperature());
|
||||
}
|
||||
config.setApiKey(forcePreset ? preset.getApiKey() : defaultIfBlank(config.getApiKey(), preset.getApiKey()));
|
||||
config.setApiKeyHeader(forcePreset ? preset.getApiKeyHeader() : defaultIfBlank(config.getApiKeyHeader(), preset.getApiKeyHeader()));
|
||||
if (forcePreset) {
|
||||
config.setConnectTimeoutSeconds(preset.getConnectTimeoutSeconds());
|
||||
config.setReadTimeoutSeconds(preset.getReadTimeoutSeconds());
|
||||
config.setTaskTimeoutSeconds(preset.getTaskTimeoutSeconds());
|
||||
config.setTaskPollIntervalMillis(preset.getTaskPollIntervalMillis());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private RowMapper<SpeechRecognitionConfigDTO> configRowMapper() {
|
||||
return (resultSet, rowNum) -> {
|
||||
SpeechRecognitionConfigDTO dto = new SpeechRecognitionConfigDTO();
|
||||
dto.setTenantId(resultSet.getLong("tenant_id"));
|
||||
dto.setServiceType(defaultIfBlank(resultSet.getString("service_type"), DEFAULT_SERVICE_TYPE));
|
||||
dto.setEnabled(resultSet.getBoolean("enabled"));
|
||||
dto.setBaseUrl(trimToNull(resultSet.getString("base_url")));
|
||||
dto.setTranscribePath(trimToNull(resultSet.getString("transcribe_path")));
|
||||
|
|
@ -260,6 +537,92 @@ public class SpeechRecognitionConfigService {
|
|||
};
|
||||
}
|
||||
|
||||
private RowMapper<SpeechRecognitionServiceDTO> serviceRowMapper() {
|
||||
return (resultSet, rowNum) -> {
|
||||
SpeechRecognitionServiceDTO dto = new SpeechRecognitionServiceDTO();
|
||||
dto.setServiceType(defaultIfBlank(resultSet.getString("service_type"), DEFAULT_SERVICE_TYPE));
|
||||
dto.setServiceName(defaultIfBlank(resultSet.getString("service_name"), dto.getServiceType()));
|
||||
dto.setBuiltIn(resultSet.getBoolean("built_in"));
|
||||
dto.setEnabled(resultSet.getBoolean("enabled"));
|
||||
dto.setBaseUrl(trimToNull(resultSet.getString("base_url")));
|
||||
dto.setTranscribePath(defaultIfBlank(resultSet.getString("transcribe_path"), "/v1/audio/transcriptions"));
|
||||
dto.setTaskStatusPath(defaultIfBlank(resultSet.getString("task_status_path"), "/v1/tasks/{task_id}"));
|
||||
dto.setFileFieldName("file");
|
||||
dto.setLanguage(trimToNull(resultSet.getString("language")));
|
||||
dto.setResponseFormat(defaultIfBlank(resultSet.getString("response_format"), "json"));
|
||||
dto.setModel(trimToNull(resultSet.getString("model")));
|
||||
dto.setPrompt(trimToNull(resultSet.getString("prompt")));
|
||||
dto.setHotwords(trimToNull(resultSet.getString("hotwords")));
|
||||
dto.setTimestampGranularities(trimToNull(resultSet.getString("timestamp_granularities")));
|
||||
dto.setEnableSpeakerDiarization((Boolean) resultSet.getObject("enable_speaker_diarization"));
|
||||
dto.setEnableSpeakerIdentification((Boolean) resultSet.getObject("enable_speaker_identification"));
|
||||
dto.setEnableTextCleanup((Boolean) resultSet.getObject("enable_text_cleanup"));
|
||||
dto.setTemperature((Double) resultSet.getObject("temperature"));
|
||||
dto.setApiKey(trimToNull(resultSet.getString("api_key")));
|
||||
dto.setApiKeyHeader(defaultIfBlank(resultSet.getString("api_key_header"), "X-API-Key"));
|
||||
dto.setConnectTimeoutSeconds(resultSet.getLong("connect_timeout_seconds"));
|
||||
dto.setReadTimeoutSeconds(resultSet.getLong("read_timeout_seconds"));
|
||||
dto.setTaskTimeoutSeconds(resultSet.getLong("task_timeout_seconds"));
|
||||
dto.setTaskPollIntervalMillis(resultSet.getLong("task_poll_interval_millis"));
|
||||
dto.setSortOrder(resultSet.getInt("sort_order"));
|
||||
return dto;
|
||||
};
|
||||
}
|
||||
|
||||
private SpeechRecognitionServiceDTO normalizeCustomService(SpeechRecognitionServiceDTO payload) {
|
||||
if (payload == null) {
|
||||
throw new BusinessException("语音服务不能为空");
|
||||
}
|
||||
String serviceName = trimToNull(payload.getServiceName());
|
||||
if (serviceName == null) {
|
||||
throw new BusinessException("请输入语音服务名称");
|
||||
}
|
||||
|
||||
SpeechRecognitionConfigDTO defaults = defaultConfig(GLOBAL_TENANT_ID);
|
||||
SpeechRecognitionServiceDTO normalized = new SpeechRecognitionServiceDTO();
|
||||
normalized.setServiceType(normalizeCustomServiceType(payload.getServiceType()));
|
||||
normalized.setServiceName(serviceName);
|
||||
normalized.setBuiltIn(false);
|
||||
normalized.setEnabled(true);
|
||||
normalized.setBaseUrl(defaultIfBlank(payload.getBaseUrl(), defaults.getBaseUrl()));
|
||||
normalized.setTranscribePath(defaultIfBlank(payload.getTranscribePath(), defaults.getTranscribePath()));
|
||||
normalized.setTaskStatusPath(defaultIfBlank(payload.getTaskStatusPath(), defaults.getTaskStatusPath()));
|
||||
normalized.setFileFieldName("file");
|
||||
normalized.setLanguage(trimToNull(payload.getLanguage()));
|
||||
normalized.setResponseFormat(defaultIfBlank(payload.getResponseFormat(), defaults.getResponseFormat()));
|
||||
normalized.setModel(trimToNull(payload.getModel()));
|
||||
normalized.setPrompt(trimToNull(payload.getPrompt()));
|
||||
normalized.setHotwords(trimToNull(payload.getHotwords()));
|
||||
normalized.setTimestampGranularities(trimToNull(payload.getTimestampGranularities()));
|
||||
normalized.setEnableSpeakerDiarization(payload.getEnableSpeakerDiarization());
|
||||
normalized.setEnableSpeakerIdentification(payload.getEnableSpeakerIdentification());
|
||||
normalized.setEnableTextCleanup(payload.getEnableTextCleanup());
|
||||
normalized.setTemperature(payload.getTemperature());
|
||||
normalized.setApiKey(trimToNull(payload.getApiKey()));
|
||||
normalized.setApiKeyHeader(defaultIfBlank(payload.getApiKeyHeader(), defaults.getApiKeyHeader()));
|
||||
normalized.setConnectTimeoutSeconds(clamp(payload.getConnectTimeoutSeconds(), 1, 120, defaults.getConnectTimeoutSeconds()));
|
||||
normalized.setReadTimeoutSeconds(clamp(payload.getReadTimeoutSeconds(), 1, 1800, defaults.getReadTimeoutSeconds()));
|
||||
normalized.setTaskTimeoutSeconds(clamp(payload.getTaskTimeoutSeconds(), 1, 1800, defaults.getTaskTimeoutSeconds()));
|
||||
normalized.setTaskPollIntervalMillis(clamp(payload.getTaskPollIntervalMillis(), 300, 60000, defaults.getTaskPollIntervalMillis()));
|
||||
normalized.setSortOrder(payload.getSortOrder() == null || payload.getSortOrder() <= 0 ? 100 : payload.getSortOrder());
|
||||
if (StringUtils.hasText(normalized.getBaseUrl())) {
|
||||
validateBaseUrl(normalized.getBaseUrl());
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizeCustomServiceType(String serviceType) {
|
||||
String normalized = trimToNull(serviceType);
|
||||
if (normalized == null || "CUSTOM".equalsIgnoreCase(normalized)) {
|
||||
return "CUSTOM_" + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
|
||||
}
|
||||
normalized = normalized.trim().toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9_-]", "_");
|
||||
if (!normalized.startsWith("CUSTOM_")) {
|
||||
normalized = "CUSTOM_" + normalized;
|
||||
}
|
||||
return normalized.length() > 64 ? normalized.substring(0, 64) : normalized;
|
||||
}
|
||||
|
||||
private void validateBaseUrl(String baseUrl) {
|
||||
if (!StringUtils.hasText(baseUrl)) {
|
||||
throw new BusinessException("语音识别服务地址不能为空");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>UnisBase - 智能会议系统</title>
|
||||
<script type="module" crossorigin src="/assets/index-BBoOWaLk.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CdI-T26o.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
import http from "@/api/http";
|
||||
import type { SpeechRecognitionConfig } from "./types";
|
||||
import type { SpeechRecognitionConfig, SpeechRecognitionService } from "./types";
|
||||
|
||||
export async function getSpeechRecognitionConfig(tenantId?: number) {
|
||||
const resp = await http.get("/sys/api/admin/speech-recognition-config", { params: { tenantId } });
|
||||
return resp.data.data as SpeechRecognitionConfig;
|
||||
}
|
||||
|
||||
export async function getSpeechRecognitionServices() {
|
||||
const resp = await http.get("/sys/api/admin/speech-recognition-services");
|
||||
return resp.data.data as SpeechRecognitionService[];
|
||||
}
|
||||
|
||||
export async function createSpeechRecognitionService(payload: Partial<SpeechRecognitionService> & Pick<SpeechRecognitionService, "serviceName">) {
|
||||
const resp = await http.post("/sys/api/admin/speech-recognition-services", payload);
|
||||
return resp.data.data as SpeechRecognitionService;
|
||||
}
|
||||
|
||||
export async function deleteSpeechRecognitionService(serviceType: string) {
|
||||
const resp = await http.delete(`/sys/api/admin/speech-recognition-services/${encodeURIComponent(serviceType)}`);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function updateSpeechRecognitionConfig(payload: SpeechRecognitionConfig) {
|
||||
const resp = await http.put("/sys/api/admin/speech-recognition-config", payload);
|
||||
return resp.data.data as boolean;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,29 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.speech-recognition-service-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.speech-recognition-service-option > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.speech-recognition-service-divider {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.speech-recognition-service-add {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.speech-recognition-hint {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import {
|
|||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
|
|
@ -18,23 +20,34 @@ import {
|
|||
import {
|
||||
ApiOutlined,
|
||||
AudioOutlined,
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
SecurityScanOutlined
|
||||
SaveOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import { getSpeechRecognitionConfig, updateSpeechRecognitionConfig } from "@/features/speech-recognition/api";
|
||||
import type { SpeechRecognitionConfig } from "@/features/speech-recognition/types";
|
||||
import {
|
||||
createSpeechRecognitionService,
|
||||
deleteSpeechRecognitionService,
|
||||
getSpeechRecognitionConfig,
|
||||
getSpeechRecognitionServices,
|
||||
updateSpeechRecognitionConfig
|
||||
} from "@/features/speech-recognition/api";
|
||||
import type { SpeechRecognitionConfig, SpeechRecognitionService } from "@/features/speech-recognition/types";
|
||||
import "./index.less";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface CustomServiceFormValues {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: SpeechRecognitionConfig = {
|
||||
serviceType: "",
|
||||
enabled: true,
|
||||
baseUrl: "http://10.211.147.11:17003/",
|
||||
baseUrl: "",
|
||||
transcribePath: "/v1/audio/transcriptions",
|
||||
taskStatusPath: "/v1/tasks/{task_id}",
|
||||
fileFieldName: "file",
|
||||
|
|
@ -70,6 +83,7 @@ function normalizePayload(values: SpeechRecognitionConfig, tenantId?: number): S
|
|||
...DEFAULT_CONFIG,
|
||||
...values,
|
||||
tenantId,
|
||||
serviceType: values.serviceType?.trim() || "",
|
||||
baseUrl: values.baseUrl?.trim() || "",
|
||||
transcribePath: values.transcribePath?.trim() || DEFAULT_CONFIG.transcribePath,
|
||||
taskStatusPath: values.taskStatusPath?.trim() || DEFAULT_CONFIG.taskStatusPath,
|
||||
|
|
@ -85,19 +99,94 @@ function normalizePayload(values: SpeechRecognitionConfig, tenantId?: number): S
|
|||
};
|
||||
}
|
||||
|
||||
function applyServicePreset(
|
||||
values: SpeechRecognitionConfig,
|
||||
service?: SpeechRecognitionService
|
||||
): SpeechRecognitionConfig {
|
||||
if (!service) {
|
||||
return { ...values, serviceType: values.serviceType || "" };
|
||||
}
|
||||
|
||||
return {
|
||||
...values,
|
||||
serviceType: service.serviceType,
|
||||
baseUrl: service.baseUrl?.trim() || values.baseUrl || "",
|
||||
transcribePath: service.transcribePath || DEFAULT_CONFIG.transcribePath,
|
||||
taskStatusPath: service.taskStatusPath || DEFAULT_CONFIG.taskStatusPath,
|
||||
fileFieldName: "file",
|
||||
language: service.language ?? values.language ?? "",
|
||||
responseFormat: service.responseFormat || DEFAULT_CONFIG.responseFormat,
|
||||
model: service.model ?? values.model ?? "",
|
||||
prompt: service.prompt ?? values.prompt ?? "",
|
||||
hotwords: service.hotwords ?? values.hotwords ?? "",
|
||||
timestampGranularities: service.timestampGranularities ?? values.timestampGranularities ?? "",
|
||||
enableSpeakerDiarization: service.enableSpeakerDiarization ?? values.enableSpeakerDiarization,
|
||||
enableSpeakerIdentification: service.enableSpeakerIdentification ?? values.enableSpeakerIdentification,
|
||||
enableTextCleanup: service.enableTextCleanup ?? values.enableTextCleanup,
|
||||
temperature: service.temperature ?? values.temperature,
|
||||
apiKey: service.apiKey?.trim() || values.apiKey || "",
|
||||
apiKeyHeader: service.apiKeyHeader?.trim() || DEFAULT_CONFIG.apiKeyHeader,
|
||||
connectTimeoutSeconds: service.connectTimeoutSeconds || DEFAULT_CONFIG.connectTimeoutSeconds,
|
||||
readTimeoutSeconds: service.readTimeoutSeconds || DEFAULT_CONFIG.readTimeoutSeconds,
|
||||
taskTimeoutSeconds: service.taskTimeoutSeconds || DEFAULT_CONFIG.taskTimeoutSeconds,
|
||||
taskPollIntervalMillis: service.taskPollIntervalMillis || DEFAULT_CONFIG.taskPollIntervalMillis
|
||||
};
|
||||
}
|
||||
|
||||
function sortServices(services: SpeechRecognitionService[]) {
|
||||
return [...services].sort((left, right) => {
|
||||
const sortDelta = (left.sortOrder ?? 100) - (right.sortOrder ?? 100);
|
||||
if (sortDelta !== 0) {
|
||||
return sortDelta;
|
||||
}
|
||||
return (left.serviceName || left.serviceType).localeCompare(right.serviceName || right.serviceType, "zh-Hans-CN");
|
||||
});
|
||||
}
|
||||
|
||||
export default function SpeechRecognitionSettings() {
|
||||
const { message } = App.useApp();
|
||||
const { message, modal } = App.useApp();
|
||||
const [form] = Form.useForm<SpeechRecognitionConfig>();
|
||||
const [serviceForm] = Form.useForm<CustomServiceFormValues>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savingService, setSavingService] = useState(false);
|
||||
const [customServiceModalOpen, setCustomServiceModalOpen] = useState(false);
|
||||
const [services, setServices] = useState<SpeechRecognitionService[]>([]);
|
||||
|
||||
const tenantId = useMemo(() => resolveActiveTenantId(), []);
|
||||
const selectedServiceType = Form.useWatch("serviceType", form);
|
||||
const selectedService = useMemo(
|
||||
() => services.find((service) => service.serviceType === selectedServiceType),
|
||||
[selectedServiceType, services]
|
||||
);
|
||||
const serviceParamsDisabled = selectedService ? selectedService.builtIn !== false : false;
|
||||
const serviceOptions = useMemo(() => {
|
||||
return services.map((service) => ({
|
||||
label: service.serviceName || service.serviceType,
|
||||
value: service.serviceType
|
||||
}));
|
||||
}, [services]);
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getSpeechRecognitionConfig(tenantId);
|
||||
form.setFieldsValue({ ...DEFAULT_CONFIG, ...data });
|
||||
const [data, serviceData] = await Promise.all([
|
||||
getSpeechRecognitionConfig(tenantId),
|
||||
getSpeechRecognitionServices()
|
||||
]);
|
||||
const enabledServices = sortServices(
|
||||
(serviceData || [])
|
||||
.filter((service) => service.enabled)
|
||||
.map((service) => ({ ...service, builtIn: service.builtIn !== false }))
|
||||
);
|
||||
setServices(enabledServices);
|
||||
const values = {
|
||||
...DEFAULT_CONFIG,
|
||||
...data,
|
||||
serviceType: data?.serviceType || ""
|
||||
};
|
||||
const service = enabledServices.find((item) => item.serviceType === values.serviceType) || enabledServices[0];
|
||||
form.setFieldsValue(applyServicePreset({ ...values, serviceType: service?.serviceType || "" }, service));
|
||||
} catch (error: any) {
|
||||
message.error(error.message || "语音识别配置加载失败");
|
||||
} finally {
|
||||
|
|
@ -128,8 +217,87 @@ export default function SpeechRecognitionSettings() {
|
|||
}, [form, message, tenantId]);
|
||||
|
||||
const handleResetDefaults = useCallback(() => {
|
||||
form.setFieldsValue({ ...DEFAULT_CONFIG, tenantId });
|
||||
}, [form, tenantId]);
|
||||
const serviceType = form.getFieldValue("serviceType") || services[0]?.serviceType || "";
|
||||
const service = services.find((item) => item.serviceType === serviceType);
|
||||
form.setFieldsValue(applyServicePreset({ ...DEFAULT_CONFIG, tenantId, serviceType }, service));
|
||||
}, [form, services, tenantId]);
|
||||
|
||||
const handleServiceChange = useCallback((serviceType: string) => {
|
||||
const currentValues = {
|
||||
...DEFAULT_CONFIG,
|
||||
...form.getFieldsValue(true),
|
||||
serviceType
|
||||
};
|
||||
const service = services.find((item) => item.serviceType === serviceType);
|
||||
form.setFieldsValue(applyServicePreset(currentValues, service));
|
||||
}, [form, services]);
|
||||
|
||||
const handleOpenCustomServiceModal = useCallback(() => {
|
||||
serviceForm.resetFields();
|
||||
setCustomServiceModalOpen(true);
|
||||
}, [serviceForm]);
|
||||
|
||||
const handleCreateCustomService = useCallback(async () => {
|
||||
try {
|
||||
const { serviceName } = await serviceForm.validateFields();
|
||||
const payload: Partial<SpeechRecognitionService> & Pick<SpeechRecognitionService, "serviceName"> = {
|
||||
serviceType: "",
|
||||
serviceName: serviceName.trim(),
|
||||
builtIn: false,
|
||||
enabled: true
|
||||
};
|
||||
setSavingService(true);
|
||||
const service = await createSpeechRecognitionService(payload);
|
||||
setServices((prev) => sortServices([
|
||||
...prev.filter((item) => item.serviceType !== service.serviceType),
|
||||
service
|
||||
]));
|
||||
form.setFieldsValue(applyServicePreset({
|
||||
...DEFAULT_CONFIG,
|
||||
tenantId,
|
||||
enabled: form.getFieldValue("enabled") ?? DEFAULT_CONFIG.enabled,
|
||||
serviceType: service.serviceType
|
||||
}, service));
|
||||
setCustomServiceModalOpen(false);
|
||||
message.success("自定义语音服务已添加");
|
||||
} catch (error: any) {
|
||||
if (error?.errorFields) {
|
||||
return;
|
||||
}
|
||||
message.error(error.message || "添加自定义语音服务失败");
|
||||
} finally {
|
||||
setSavingService(false);
|
||||
}
|
||||
}, [form, message, serviceForm, tenantId]);
|
||||
|
||||
const handleDeleteCustomService = useCallback((service: SpeechRecognitionService) => {
|
||||
if (service.builtIn !== false) {
|
||||
message.warning("预设语音服务不允许删除");
|
||||
return;
|
||||
}
|
||||
modal.confirm({
|
||||
title: "删除自定义语音服务",
|
||||
content: `确定删除“${service.serviceName || service.serviceType}”?`,
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
await deleteSpeechRecognitionService(service.serviceType);
|
||||
const nextServices = services.filter((item) => item.serviceType !== service.serviceType);
|
||||
setServices(nextServices);
|
||||
if (form.getFieldValue("serviceType") === service.serviceType) {
|
||||
const nextService = nextServices[0];
|
||||
form.setFieldsValue(applyServicePreset({
|
||||
...DEFAULT_CONFIG,
|
||||
tenantId,
|
||||
enabled: form.getFieldValue("enabled") ?? DEFAULT_CONFIG.enabled,
|
||||
serviceType: nextService?.serviceType || ""
|
||||
}, nextService));
|
||||
}
|
||||
message.success("自定义语音服务已删除");
|
||||
}
|
||||
});
|
||||
}, [form, message, modal, services, tenantId]);
|
||||
|
||||
return (
|
||||
<div className="app-page speech-recognition-page">
|
||||
|
|
@ -143,7 +311,7 @@ export default function SpeechRecognitionSettings() {
|
|||
className="speech-recognition-alert"
|
||||
type="info"
|
||||
showIcon
|
||||
message="保存后立即影响当前租户的日报语音转文字功能;上传字段固定为 OpenAI Audio API 兼容的 file。"
|
||||
message="语音服务下拉项来自数据库预设;保存后立即影响当前租户的日报语音转文字功能。"
|
||||
/>
|
||||
|
||||
<Card className="app-page__content-card speech-recognition-card" styles={{ body: { padding: 24 } }}>
|
||||
|
|
@ -182,7 +350,57 @@ export default function SpeechRecognitionSettings() {
|
|||
<span>服务接口</span>
|
||||
</div>
|
||||
<Row gutter={[24, 8]}>
|
||||
<Col xs={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="serviceType" label="语音服务" rules={[{ required: true, message: "请选择语音服务" }]}>
|
||||
<Select
|
||||
placeholder="请选择语音服务"
|
||||
options={serviceOptions}
|
||||
onChange={handleServiceChange}
|
||||
optionRender={(option) => {
|
||||
const service = services.find((item) => item.serviceType === option.value);
|
||||
return (
|
||||
<div className="speech-recognition-service-option">
|
||||
<span>{option.label}</span>
|
||||
{service && service.builtIn === false ? (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
title="删除自定义服务"
|
||||
icon={<DeleteOutlined />}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteCustomService(service);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider className="speech-recognition-service-divider" />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
className="speech-recognition-service-add"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={handleOpenCustomServiceModal}
|
||||
>
|
||||
添加自定义服务
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label="服务 Base URL"
|
||||
|
|
@ -191,35 +409,12 @@ export default function SpeechRecognitionSettings() {
|
|||
{ type: "url", warningOnly: true, message: "建议填写完整 http/https 地址" }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="http://10.211.147.11:17003/" allowClear />
|
||||
<Input placeholder="https://asr.example.com/" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="transcribePath" label="转写接口路径" rules={[{ required: true, message: "请输入转写接口路径" }]}>
|
||||
<Input placeholder="/v1/audio/transcriptions" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="taskStatusPath" label="任务状态路径" rules={[{ required: true, message: "请输入任务状态路径" }]}>
|
||||
<Input placeholder="/v1/tasks/{task_id}" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="language" label="识别语言">
|
||||
<Input placeholder="留空则不传 language 参数" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="responseFormat" label="输出格式" rules={[{ required: true, message: "请选择输出格式" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "json", value: "json" },
|
||||
{ label: "text", value: "text" },
|
||||
{ label: "verbose_json", value: "verbose_json" },
|
||||
{ label: "srt", value: "srt" },
|
||||
{ label: "vtt", value: "vtt" }
|
||||
]}
|
||||
/>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
<Input.Password placeholder="留空则不发送鉴权头" autoComplete="new-password" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -233,104 +428,102 @@ export default function SpeechRecognitionSettings() {
|
|||
<Row gutter={[24, 8]}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="enableSpeakerDiarization" label="说话人分离" valuePropName="checked">
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24}>
|
||||
<Paragraph type="secondary" className="speech-recognition-hint">
|
||||
多人录音建议开启;单人日报录音可以关闭。
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div className="speech-recognition-section">
|
||||
<div className="speech-recognition-section__title">
|
||||
<SecurityScanOutlined />
|
||||
<span>鉴权</span>
|
||||
</div>
|
||||
<Row gutter={[24, 8]}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="apiKeyHeader" label="API Key 请求头" rules={[{ required: true, message: "请输入请求头名称" }]}>
|
||||
<Input placeholder="X-API-Key" allowClear />
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
<Input.Password placeholder="留空则不发送鉴权头" autoComplete="new-password" />
|
||||
<Form.Item name="language" label="识别语言">
|
||||
<Input placeholder="留空则不传 language 参数" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="responseFormat" label="输出格式" rules={[{ required: true, message: "请选择输出格式" }]}>
|
||||
<Select
|
||||
disabled={serviceParamsDisabled}
|
||||
options={[
|
||||
{ label: "json", value: "json" },
|
||||
{ label: "text", value: "text" },
|
||||
{ label: "verbose_json", value: "verbose_json" },
|
||||
{ label: "srt", value: "srt" },
|
||||
{ label: "vtt", value: "vtt" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div className="speech-recognition-section">
|
||||
<div className="speech-recognition-section__title">
|
||||
<ClockCircleOutlined />
|
||||
<span>超时与轮询</span>
|
||||
</div>
|
||||
<Row gutter={[24, 8]}>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="connectTimeoutSeconds" label="连接超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={120} precision={0} className="speech-recognition-number" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="readTimeoutSeconds" label="读取超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={1800} precision={0} className="speech-recognition-number" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="taskTimeoutSeconds" label="任务超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={1800} precision={0} className="speech-recognition-number" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="taskPollIntervalMillis" label="轮询间隔(毫秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={300} max={60000} step={100} precision={0} className="speech-recognition-number" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Paragraph type="secondary" className="speech-recognition-hint">
|
||||
任务状态路径支持 <Text code>{"{task_id}"}</Text> 或 <Text code>{"{taskId}"}</Text> 占位符。
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Collapse ghost className="speech-recognition-advanced">
|
||||
<Panel header="高级参数" key="advanced">
|
||||
<Row gutter={[24, 8]}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="transcribePath" label="转写接口路径" rules={[{ required: true, message: "请输入转写接口路径" }]}>
|
||||
<Input placeholder="/v1/audio/transcriptions" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="taskStatusPath" label="任务状态路径" rules={[{ required: true, message: "请输入任务状态路径" }]}>
|
||||
<Input placeholder="/v1/tasks/{task_id}" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="apiKeyHeader" label="API Key 请求头" rules={[{ required: true, message: "请输入请求头名称" }]}>
|
||||
<Input placeholder="X-API-Key" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="connectTimeoutSeconds" label="连接超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={120} precision={0} className="speech-recognition-number" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="readTimeoutSeconds" label="读取超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={1800} precision={0} className="speech-recognition-number" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="taskTimeoutSeconds" label="任务超时(秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={1800} precision={0} className="speech-recognition-number" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={6}>
|
||||
<Form.Item name="taskPollIntervalMillis" label="轮询间隔(毫秒)" rules={[{ required: true }]}>
|
||||
<InputNumber min={300} max={60000} step={100} precision={0} className="speech-recognition-number" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="model" label="模型">
|
||||
<Input placeholder="留空使用服务端默认模型" allowClear />
|
||||
<Input placeholder="留空使用服务端默认模型" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="enableSpeakerIdentification" label="说话人识别" valuePropName="checked">
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="enableTextCleanup" label="文本清理" valuePropName="checked">
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="hotwords" label="热词">
|
||||
<Input placeholder="热词1 权重1 热词2 权重2" allowClear />
|
||||
<Input placeholder="热词1 权重1 热词2 权重2" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="prompt" label="提示词">
|
||||
<Input placeholder="可作为热词提示的兼容入口" allowClear />
|
||||
<Input placeholder="可作为热词提示的兼容入口" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="timestampGranularities" label="时间戳粒度">
|
||||
<Input placeholder="例如 segment 或 word" allowClear />
|
||||
<Input placeholder="例如 segment 或 word" allowClear disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="temperature" label="Temperature">
|
||||
<InputNumber min={0} max={2} step={0.1} className="speech-recognition-number" />
|
||||
<InputNumber min={0} max={2} step={0.1} className="speech-recognition-number" disabled={serviceParamsDisabled} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -352,8 +545,31 @@ export default function SpeechRecognitionSettings() {
|
|||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Card>
|
||||
</Card>
|
||||
<Modal
|
||||
title="添加自定义语音服务"
|
||||
open={customServiceModalOpen}
|
||||
okText="添加"
|
||||
cancelText="取消"
|
||||
confirmLoading={savingService}
|
||||
destroyOnClose
|
||||
onOk={handleCreateCustomService}
|
||||
onCancel={() => setCustomServiceModalOpen(false)}
|
||||
>
|
||||
<Form form={serviceForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="serviceName"
|
||||
label="服务名称"
|
||||
rules={[
|
||||
{ required: true, message: "请输入服务名称" },
|
||||
{ max: 64, message: "服务名称不能超过 64 个字符" }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:我的转写服务" maxLength={64} allowClear />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export interface SpeechRecognitionConfig {
|
||||
tenantId?: number;
|
||||
serviceType: string;
|
||||
enabled: boolean;
|
||||
baseUrl: string;
|
||||
transcribePath: string;
|
||||
|
|
@ -22,3 +23,9 @@ export interface SpeechRecognitionConfig {
|
|||
taskTimeoutSeconds: number;
|
||||
taskPollIntervalMillis: number;
|
||||
}
|
||||
|
||||
export interface SpeechRecognitionService extends SpeechRecognitionConfig {
|
||||
serviceName: string;
|
||||
builtIn: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,44 @@ begin;
|
|||
|
||||
set search_path to public;
|
||||
|
||||
create table if not exists speech_recognition_service (
|
||||
id bigint generated by default as identity primary key,
|
||||
service_type varchar(64) not null,
|
||||
service_name varchar(128) not null,
|
||||
built_in boolean not null default true,
|
||||
enabled boolean not null default true,
|
||||
base_url varchar(512),
|
||||
transcribe_path varchar(255) not null default '/v1/audio/transcriptions',
|
||||
task_status_path varchar(255) not null default '/v1/tasks/{task_id}',
|
||||
file_field_name varchar(64) not null default 'file',
|
||||
language varchar(32),
|
||||
response_format varchar(32) not null default 'json',
|
||||
model varchar(128),
|
||||
prompt text,
|
||||
hotwords text,
|
||||
timestamp_granularities varchar(128),
|
||||
enable_speaker_diarization boolean,
|
||||
enable_speaker_identification boolean,
|
||||
enable_text_cleanup boolean,
|
||||
temperature double precision,
|
||||
api_key varchar(512),
|
||||
api_key_header varchar(128) not null default 'X-API-Key',
|
||||
connect_timeout_seconds integer not null default 10,
|
||||
read_timeout_seconds integer not null default 300,
|
||||
task_timeout_seconds integer not null default 300,
|
||||
task_poll_interval_millis integer not null default 1500,
|
||||
sort_order integer not null default 100,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint uk_speech_recognition_service_type unique (service_type)
|
||||
);
|
||||
|
||||
alter table if exists speech_recognition_service add column if not exists built_in boolean not null default true;
|
||||
|
||||
create table if not exists speech_recognition_config (
|
||||
id bigint generated by default as identity primary key,
|
||||
tenant_id bigint not null,
|
||||
service_type varchar(64) not null default 'CUSTOM',
|
||||
enabled boolean not null default true,
|
||||
base_url varchar(512),
|
||||
transcribe_path varchar(255) not null default '/v1/audio/transcriptions',
|
||||
|
|
@ -31,6 +66,7 @@ create table if not exists speech_recognition_config (
|
|||
constraint uk_speech_recognition_config_tenant unique (tenant_id)
|
||||
);
|
||||
|
||||
alter table if exists speech_recognition_config add column if not exists service_type varchar(64) not null default 'CUSTOM';
|
||||
alter table if exists speech_recognition_config add column if not exists response_format varchar(32) not null default 'json';
|
||||
alter table if exists speech_recognition_config add column if not exists model varchar(128);
|
||||
alter table if exists speech_recognition_config add column if not exists prompt text;
|
||||
|
|
@ -41,6 +77,88 @@ alter table if exists speech_recognition_config add column if not exists enable_
|
|||
alter table if exists speech_recognition_config add column if not exists enable_text_cleanup boolean;
|
||||
alter table if exists speech_recognition_config add column if not exists temperature double precision;
|
||||
|
||||
insert into speech_recognition_service (
|
||||
service_type,
|
||||
service_name,
|
||||
built_in,
|
||||
enabled,
|
||||
base_url,
|
||||
transcribe_path,
|
||||
task_status_path,
|
||||
file_field_name,
|
||||
language,
|
||||
response_format,
|
||||
api_key,
|
||||
api_key_header,
|
||||
connect_timeout_seconds,
|
||||
read_timeout_seconds,
|
||||
task_timeout_seconds,
|
||||
task_poll_interval_millis,
|
||||
sort_order,
|
||||
created_at,
|
||||
updated_at
|
||||
) values
|
||||
(
|
||||
'ASR_10_100_53_199_8000',
|
||||
'语音转写服务 10.100.53.199:8000',
|
||||
true,
|
||||
true,
|
||||
'http://10.100.53.199:8000/',
|
||||
'/v1/audio/transcriptions',
|
||||
'/v1/tasks/{task_id}',
|
||||
'file',
|
||||
'',
|
||||
'json',
|
||||
null,
|
||||
'X-API-Key',
|
||||
10,
|
||||
300,
|
||||
300,
|
||||
1500,
|
||||
10,
|
||||
now(),
|
||||
now()
|
||||
),
|
||||
(
|
||||
'ASR_10_100_51_199_17003',
|
||||
'语音转写服务 10.100.51.199:17003',
|
||||
true,
|
||||
true,
|
||||
'http://10.100.51.199:17003/',
|
||||
'/v1/audio/transcriptions',
|
||||
'/v1/tasks/{task_id}',
|
||||
'file',
|
||||
'',
|
||||
'json',
|
||||
null,
|
||||
'X-API-Key',
|
||||
10,
|
||||
300,
|
||||
300,
|
||||
1500,
|
||||
20,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
on conflict (service_type) do update
|
||||
set service_name = excluded.service_name,
|
||||
built_in = true,
|
||||
enabled = true,
|
||||
base_url = excluded.base_url,
|
||||
transcribe_path = excluded.transcribe_path,
|
||||
task_status_path = excluded.task_status_path,
|
||||
file_field_name = excluded.file_field_name,
|
||||
language = excluded.language,
|
||||
response_format = excluded.response_format,
|
||||
api_key = excluded.api_key,
|
||||
api_key_header = excluded.api_key_header,
|
||||
connect_timeout_seconds = excluded.connect_timeout_seconds,
|
||||
read_timeout_seconds = excluded.read_timeout_seconds,
|
||||
task_timeout_seconds = excluded.task_timeout_seconds,
|
||||
task_poll_interval_millis = excluded.task_poll_interval_millis,
|
||||
sort_order = excluded.sort_order,
|
||||
updated_at = now();
|
||||
|
||||
do $$
|
||||
declare
|
||||
v_system_parent_id bigint;
|
||||
|
|
|
|||
Loading…
Reference in New Issue