mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
- DashScope paraformer-realtime-v2 WebSocket streaming - Language-aware provider routing: Whisper for English, Paraformer for Chinese - PCM WAV recording replaces WebM (provider filename bug + diagnostics) - TalkMode push-to-talk fixes (audio drop, WS connecting race) - Vite dev proxy WebSocket upgrade fix - WebSocket binary buffer 8KB → 8MB (Tomcat default truncated voice clips) - Audio chunk pacing at 100ms (DashScope returned 0 chars otherwise) - Resolved language hint propagation + raw frame logging - V46 seed idempotency fix on UI-toggled STT row - Diagnostic cleanup after debugging session
115 lines
5.3 KiB
Java
115 lines
5.3 KiB
Java
package vip.mate.stt;
|
||
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.stereotype.Service;
|
||
import vip.mate.system.model.SystemSettingsDTO;
|
||
import vip.mate.system.service.SystemSettingService;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* STT 语音识别服务
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class SttService {
|
||
|
||
private final SystemSettingService systemSettingService;
|
||
private final SttProviderRegistry providerRegistry;
|
||
|
||
public Map<String, Object> transcribe(byte[] audioData, String fileName, String contentType, String language) {
|
||
SystemSettingsDTO config = systemSettingService.getAllSettings();
|
||
|
||
if (!Boolean.TRUE.equals(config.getSttEnabled())) {
|
||
return Map.of("success", false, "error", "STT 功能未启用,请在系统设置中开启");
|
||
}
|
||
|
||
// Per-call dispatch trace — without this the only signal that STT
|
||
// is even being attempted is the eventual provider success/failure
|
||
// log, which makes "no audio reached us" indistinguishable from
|
||
// "audio reached us but provider rejected it".
|
||
int bytes = audioData != null ? audioData.length : 0;
|
||
log.info("[STT] dispatch bytes={} fileName={} contentType={} language={} provider={}",
|
||
bytes, fileName, contentType, language,
|
||
config.getSttProvider() != null ? config.getSttProvider() : "auto");
|
||
|
||
SttRequest request = SttRequest.builder()
|
||
.audioData(audioData)
|
||
.fileName(fileName)
|
||
.contentType(contentType)
|
||
.language(language)
|
||
.build();
|
||
|
||
// Provider 选择 + fallback
|
||
SttResult result = transcribeWithFallback(request, config);
|
||
|
||
if (result.isSuccess()) {
|
||
return Map.of("success", true, "text", result.getText(),
|
||
"language", result.getLanguage() != null ? result.getLanguage() : "");
|
||
} else {
|
||
return Map.of("success", false, "error", result.getErrorMessage());
|
||
}
|
||
}
|
||
|
||
private SttResult transcribeWithFallback(SttRequest request, SystemSettingsDTO config) {
|
||
// Language hint resolution order:
|
||
// 1. Caller-supplied request.language (explicit per-call override)
|
||
// 2. UI language from system settings (zh-CN / en-US)
|
||
// 3. null — registry falls back to the language-agnostic order
|
||
// Both registry primary-pick and fallback-candidate ordering use the
|
||
// same hint; without it Paraformer/Whisper would frequently swap
|
||
// priorities mid-fallback for the same conversation.
|
||
//
|
||
// Critical: write the resolved hint BACK into the request, so
|
||
// providers (especially DashScope's run-task language_hints field)
|
||
// see it. Pre-fix, providers got null even though we routed by
|
||
// zh-CN — DashScope's auto-detect would then sit through 2-3s of
|
||
// Chinese audio and emit zero result-generated events.
|
||
String languageHint = request.getLanguage();
|
||
if (languageHint == null || languageHint.isBlank()) {
|
||
languageHint = config.getLanguage();
|
||
if (languageHint != null && !languageHint.isBlank()) {
|
||
request.setLanguage(languageHint);
|
||
}
|
||
}
|
||
SttProvider primary = providerRegistry.resolve(config, languageHint);
|
||
if (primary == null) {
|
||
// Most common cause: no provider has its API key configured. Tell
|
||
// the operator that explicitly so they don't dig through provider
|
||
// logs looking for the real reason.
|
||
log.warn("[STT] no provider available — check DashScope / OpenAI API keys in 模型管理");
|
||
return SttResult.failure("没有可用的 STT Provider,请在模型管理中配置 DashScope 或 OpenAI API Key");
|
||
}
|
||
log.info("[STT] primary provider resolved: {} (language={})", primary.id(), languageHint);
|
||
|
||
SttResult result = primary.transcribe(request, config);
|
||
if (result.isSuccess()) {
|
||
log.info("[STT] success via {} ({} chars)", primary.id(), result.getText() != null ? result.getText().length() : 0);
|
||
return result;
|
||
}
|
||
log.warn("[STT] primary {} failed: {}", primary.id(), result.getErrorMessage());
|
||
|
||
List<String> errors = new ArrayList<>();
|
||
errors.add(primary.id() + ": " + result.getErrorMessage());
|
||
|
||
if (Boolean.TRUE.equals(config.getSttFallbackEnabled())) {
|
||
for (SttProvider fb : providerRegistry.fallbackCandidates(config, primary.id(), languageHint)) {
|
||
log.info("[STT] trying fallback provider: {}", fb.id());
|
||
result = fb.transcribe(request, config);
|
||
if (result.isSuccess()) {
|
||
log.info("[STT] fallback success via {} ({} chars)", fb.id(),
|
||
result.getText() != null ? result.getText().length() : 0);
|
||
return result;
|
||
}
|
||
log.warn("[STT] fallback {} failed: {}", fb.id(), result.getErrorMessage());
|
||
errors.add(fb.id() + ": " + result.getErrorMessage());
|
||
}
|
||
}
|
||
|
||
log.error("[STT] all providers failed — errors: {}", errors);
|
||
return SttResult.failure("所有 STT Provider 均失败\n" + String.join("\n", errors));
|
||
}
|
||
}
|