fix: 更换 PostgreSQL 镜像源 #108
@@ -158,7 +158,8 @@ func (m *MiMoService) Recognize(ctx context.Context, audio []byte, opts Options)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(mResp.Choices) == 0 {
|
if len(mResp.Choices) == 0 {
|
||||||
return "", fmt.Errorf("stt: mimo returned empty choices")
|
// MiMo 返回空结果,视为无法识别(非错误),返回空文本
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
text := strings.TrimSpace(mResp.Choices[0].Message.Content)
|
text := strings.TrimSpace(mResp.Choices[0].Message.Content)
|
||||||
|
|||||||
@@ -103,9 +103,12 @@ func TestMiMoService_Recognize_EmptyChoices(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
wav := makeValidWAV([]byte{0x00, 0x00})
|
wav := makeValidWAV([]byte{0x00, 0x00})
|
||||||
_, err := s.Recognize(context.Background(), wav, Options{})
|
text, err := s.Recognize(context.Background(), wav, Options{})
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatal("expected error for empty choices")
|
t.Fatalf("unexpected error for empty choices: %v", err)
|
||||||
|
}
|
||||||
|
if text != "" {
|
||||||
|
t.Errorf("expected empty string for empty choices, got %q", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,24 +133,48 @@ func (p *Pipeline) ProcessQuery(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 语音模式:执行 STT 语音识别
|
// 语音模式:执行 STT 语音识别
|
||||||
log.Infow("开始语音识别", "request_id", req.RequestID)
|
log.Infow("开始语音识别", "request_id", req.RequestID, "audio_bytes", len(audio))
|
||||||
sttResult, err := p.sttService.Recognize(ctx, audio, stt.Options{
|
sttResult, err := p.sttService.Recognize(ctx, audio, stt.Options{
|
||||||
Encoding: "pcm_s16le",
|
Encoding: "pcm_s16le",
|
||||||
SampleRate: 16000,
|
SampleRate: 16000,
|
||||||
Language: sess.Config.Language,
|
Language: sess.Config.Language,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorw("语音识别失败", "error", err)
|
log.Errorw("语音识别失败", "error", err, "audio_bytes", len(audio))
|
||||||
sender.SendError(models.WsError{
|
sender.SendError(models.WsError{
|
||||||
Type: "error",
|
Type: "error",
|
||||||
RequestID: req.RequestID,
|
RequestID: req.RequestID,
|
||||||
Code: "STT_ERROR",
|
Code: "STT_ERROR",
|
||||||
Message: "语音识别失败",
|
Message: "语音识别失败: " + err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userText = sttResult
|
userText = sttResult
|
||||||
|
|
||||||
|
// STT 返回空文本:未识别到语音,发送结果后直接返回(不调 LLM)
|
||||||
|
if strings.TrimSpace(userText) == "" {
|
||||||
|
log.Infow("语音识别结果为空", "request_id", req.RequestID)
|
||||||
|
userText = "(未识别到语音)"
|
||||||
|
if err := sender.SendSTTResult(models.WsSTTResult{
|
||||||
|
Type: "stt_result",
|
||||||
|
RequestID: req.RequestID,
|
||||||
|
Text: userText,
|
||||||
|
IsFinal: true,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorw("发送 STT 结果失败", "error", err)
|
||||||
|
}
|
||||||
|
// 发送空的 llm_done 以结束本轮处理
|
||||||
|
latency := time.Since(startTime).Milliseconds()
|
||||||
|
_ = sender.SendLLMDone(models.WsLLMDone{
|
||||||
|
Type: "llm_done",
|
||||||
|
RequestID: req.RequestID,
|
||||||
|
FullText: "",
|
||||||
|
Model: p.model,
|
||||||
|
LatencyMs: latency,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 发送 STT 结果
|
// 发送 STT 结果
|
||||||
if err := sender.SendSTTResult(models.WsSTTResult{
|
if err := sender.SendSTTResult(models.WsSTTResult{
|
||||||
Type: "stt_result",
|
Type: "stt_result",
|
||||||
|
|||||||
@@ -590,7 +590,6 @@ body {
|
|||||||
animation: pulse 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
animation: pulse 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-indicator--loading { color: var(--color-warning); }
|
|
||||||
.video-indicator--audio { left: auto; right: 16px; color: var(--color-primary); }
|
.video-indicator--audio { left: auto; right: 16px; color: var(--color-primary); }
|
||||||
.video-indicator--error { color: var(--color-error); animation: none; }
|
.video-indicator--error { color: var(--color-error); animation: none; }
|
||||||
|
|
||||||
@@ -651,26 +650,6 @@ body {
|
|||||||
animation: pulse 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
animation: pulse 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(10, 10, 15, 0.4);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
-webkit-backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-overlay__spinner {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border: 2.5px solid rgba(255, 255, 255, 0.15);
|
|
||||||
border-top-color: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 视频控制栏 ---- */
|
/* ---- 视频控制栏 ---- */
|
||||||
|
|
||||||
.video-controls {
|
.video-controls {
|
||||||
@@ -998,6 +977,52 @@ body {
|
|||||||
border: 1px solid rgba(251, 191, 36, 0.15);
|
border: 1px solid rgba(251, 191, 36, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-message--info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(59, 130, 246, 0.06);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Typing Indicator(跳动点) ---- */
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-muted);
|
||||||
|
animation: typing-bounce 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator__dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator__dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-bounce {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Drawer (Config Panel) ---- */
|
/* ---- Drawer (Config Panel) ---- */
|
||||||
|
|
||||||
.drawer-overlay {
|
.drawer-overlay {
|
||||||
|
|||||||
@@ -306,17 +306,9 @@ function AppContent() {
|
|||||||
{isAudioPlaying && config.ttsEnabled && (
|
{isAudioPlaying && config.ttsEnabled && (
|
||||||
<div className="video-indicator video-indicator--audio">{tr("video.playing")}</div>
|
<div className="video-indicator video-indicator--audio">{tr("video.playing")}</div>
|
||||||
)}
|
)}
|
||||||
{isConnected && !isVADReady && !vadError && (
|
|
||||||
<div className="video-indicator video-indicator--loading">{tr("video.initVad")}</div>
|
|
||||||
)}
|
|
||||||
{vadError && (
|
{vadError && (
|
||||||
<div className="video-indicator video-indicator--error">⚠️ {vadError}</div>
|
<div className="video-indicator video-indicator--error">⚠️ {vadError}</div>
|
||||||
)}
|
)}
|
||||||
{isProcessing && (
|
|
||||||
<div className="video-overlay">
|
|
||||||
<div className="video-overlay__spinner" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isConnected && !stream && (
|
{!isConnected && !stream && (
|
||||||
<div className="video-placeholder">
|
<div className="video-placeholder">
|
||||||
<svg className="video-placeholder__icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
|
<svg className="video-placeholder__icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
@@ -473,6 +465,9 @@ function AppContent() {
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
currentReply={currentReply}
|
currentReply={currentReply}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
isVADReady={isVADReady}
|
||||||
|
vadError={vadError}
|
||||||
isMicOn={isMicOn}
|
isMicOn={isMicOn}
|
||||||
isSpeaking={isSpeaking}
|
isSpeaking={isSpeaking}
|
||||||
onSendText={sendTextMessage}
|
onSendText={sendTextMessage}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ interface ChatPanelProps {
|
|||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
currentReply?: string;
|
currentReply?: string;
|
||||||
connectionStatus: ConnectionStatus;
|
connectionStatus: ConnectionStatus;
|
||||||
|
isProcessing?: boolean;
|
||||||
|
isVADReady?: boolean;
|
||||||
|
vadError?: string;
|
||||||
isMicOn?: boolean;
|
isMicOn?: boolean;
|
||||||
isSpeaking?: boolean;
|
isSpeaking?: boolean;
|
||||||
onSendText?: (text: string) => void;
|
onSendText?: (text: string) => void;
|
||||||
@@ -34,6 +37,9 @@ export function ChatPanel({
|
|||||||
messages,
|
messages,
|
||||||
currentReply,
|
currentReply,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
|
isProcessing,
|
||||||
|
isVADReady,
|
||||||
|
vadError,
|
||||||
isMicOn,
|
isMicOn,
|
||||||
isSpeaking,
|
isSpeaking,
|
||||||
onSendText,
|
onSendText,
|
||||||
@@ -144,6 +150,44 @@ export function ChatPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 连接中状态 */}
|
||||||
|
{connectionStatus === "connecting" && (
|
||||||
|
<div className="system-message system-message--info">
|
||||||
|
<span className="typing-indicator">
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
</span>
|
||||||
|
<span>{t("chat.connecting")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* VAD 初始化中 */}
|
||||||
|
{isConnected && isVADReady === false && !vadError && (
|
||||||
|
<div className="system-message system-message--info">
|
||||||
|
<span className="typing-indicator">
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
</span>
|
||||||
|
<span>{t("chat.vadInit")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI 处理中(尚未开始流式输出) */}
|
||||||
|
{isProcessing && !currentReply && (
|
||||||
|
<div className="chat-message chat-message--assistant">
|
||||||
|
<div className="chat-message__role">AI</div>
|
||||||
|
<div className="chat-message__content">
|
||||||
|
<span className="typing-indicator">
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
<span className="typing-indicator__dot" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function getOffscreen(): HTMLCanvasElement {
|
|||||||
export function sampleFrame(video: HTMLVideoElement): Uint8ClampedArray | null {
|
export function sampleFrame(video: HTMLVideoElement): Uint8ClampedArray | null {
|
||||||
if (video.readyState < 2) return null;
|
if (video.readyState < 2) return null;
|
||||||
const canvas = getOffscreen();
|
const canvas = getOffscreen();
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
if (!ctx) return null;
|
if (!ctx) return null;
|
||||||
ctx.drawImage(video, 0, 0, DETECT_WIDTH, DETECT_HEIGHT);
|
ctx.drawImage(video, 0, 0, DETECT_WIDTH, DETECT_HEIGHT);
|
||||||
return ctx.getImageData(0, 0, DETECT_WIDTH, DETECT_HEIGHT).data;
|
return ctx.getImageData(0, 0, DETECT_WIDTH, DETECT_HEIGHT).data;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { loadConfig, saveConfig } from "../lib/storage";
|
|||||||
import { useI18n } from "../lib/i18n";
|
import { useI18n } from "../lib/i18n";
|
||||||
import { useCamera } from "../components/CameraManager";
|
import { useCamera } from "../components/CameraManager";
|
||||||
import { useMicrophone } from "../components/MicManager";
|
import { useMicrophone } from "../components/MicManager";
|
||||||
import { useVAD, sampleFrame, compareFrames } from "../components/EdgeProcessor";
|
import { useVAD, sampleFrame } from "../components/EdgeProcessor";
|
||||||
import { useWebSocketManager } from "../components/WebSocketManager";
|
import { useWebSocketManager } from "../components/WebSocketManager";
|
||||||
import { useObservationMode } from "./useObservationMode";
|
import { useObservationMode } from "./useObservationMode";
|
||||||
import type { ChatMessage, SessionConfig, ServerMessage, LLMDoneMessage } from "../types";
|
import type { ChatMessage, SessionConfig, ServerMessage, LLMDoneMessage } from "../types";
|
||||||
@@ -194,24 +194,13 @@ export function useVisionSession(accessToken?: string | null) {
|
|||||||
ttsPlayerRef.current?.stop();
|
ttsPlayerRef.current?.stop();
|
||||||
setIsAudioPlaying(false);
|
setIsAudioPlaying(false);
|
||||||
|
|
||||||
|
// 尝试捕获图像帧(允许为 null,纯语音场景无需画面)
|
||||||
const frame = captureFrame();
|
const frame = captureFrame();
|
||||||
if (!frame) {
|
|
||||||
console.warn("[Session] 无法捕获图像帧");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关键帧检测:与上一帧对比,相似度过高则跳过
|
// 更新帧参考(供观察模式使用,但不以此阻断语音请求)
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (video) {
|
if (video) {
|
||||||
const currentSample = sampleFrame(video);
|
const currentSample = sampleFrame(video);
|
||||||
if (currentSample && prevFrameRef.current) {
|
|
||||||
const { similarity } = compareFrames(prevFrameRef.current, currentSample);
|
|
||||||
if (similarity > 0.9) {
|
|
||||||
console.log(`[Session] 画面无变化 (similarity=${similarity.toFixed(2)}),跳过`);
|
|
||||||
prevFrameRef.current = currentSample;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentSample) {
|
if (currentSample) {
|
||||||
prevFrameRef.current = currentSample;
|
prevFrameRef.current = currentSample;
|
||||||
}
|
}
|
||||||
@@ -221,7 +210,7 @@ export function useVisionSession(accessToken?: string | null) {
|
|||||||
send({
|
send({
|
||||||
type: "query",
|
type: "query",
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
image: dataUrlToBase64(frame),
|
image: frame ? dataUrlToBase64(frame) : "",
|
||||||
audio: encodeAudioToBase64(audio),
|
audio: encodeAudioToBase64(audio),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -304,6 +293,20 @@ export function useVisionSession(accessToken?: string | null) {
|
|||||||
case "error":
|
case "error":
|
||||||
console.error("[Session] 服务端错误:", msg.code, msg.message);
|
console.error("[Session] 服务端错误:", msg.code, msg.message);
|
||||||
showToast(getErrorMessage(msg.code, t), "error");
|
showToast(getErrorMessage(msg.code, t), "error");
|
||||||
|
// STT 失败时:将"语音识别中..."占位消息替换为失败提示
|
||||||
|
if (msg.code === "STT_ERROR") {
|
||||||
|
setMessages((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
const lastUserIdx = updated.findLastIndex((m) => m.role === "user");
|
||||||
|
if (lastUserIdx >= 0 && updated[lastUserIdx].content === t("session.recognizing")) {
|
||||||
|
updated[lastUserIdx] = {
|
||||||
|
...updated[lastUserIdx],
|
||||||
|
content: t("session.sttFailed"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const enUS: TranslationMap = {
|
|||||||
"chat.title": "Chat",
|
"chat.title": "Chat",
|
||||||
"chat.mode.observation": "Observing",
|
"chat.mode.observation": "Observing",
|
||||||
"chat.reconnecting": "Connection lost, reconnecting...",
|
"chat.reconnecting": "Connection lost, reconnecting...",
|
||||||
|
"chat.connecting": "Connecting to server...",
|
||||||
|
"chat.vadInit": "Initializing voice detection...",
|
||||||
"chat.empty.prompt": "Type below to start chatting",
|
"chat.empty.prompt": "Type below to start chatting",
|
||||||
"chat.empty.hint": "Type to chat with AI, or click the button on the left to start video",
|
"chat.empty.hint": "Type to chat with AI, or click the button on the left to start video",
|
||||||
"chat.welcome.prompt": "Type below to start chatting",
|
"chat.welcome.prompt": "Type below to start chatting",
|
||||||
@@ -65,6 +67,7 @@ export const enUS: TranslationMap = {
|
|||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ Scene change detected",
|
"session.changeDetected": "👁️ Scene change detected",
|
||||||
"session.recognizing": "(Recognizing speech...)",
|
"session.recognizing": "(Recognizing speech...)",
|
||||||
|
"session.sttFailed": "(Speech recognition failed, please try again)",
|
||||||
"session.noSpeech": "(No speech detected)",
|
"session.noSpeech": "(No speech detected)",
|
||||||
"session.interrupted": "(Interrupted)",
|
"session.interrupted": "(Interrupted)",
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const jaJP: TranslationMap = {
|
|||||||
"chat.title": "チャット",
|
"chat.title": "チャット",
|
||||||
"chat.mode.observation": "観察モード",
|
"chat.mode.observation": "観察モード",
|
||||||
"chat.reconnecting": "接続が切断されました。再接続中...",
|
"chat.reconnecting": "接続が切断されました。再接続中...",
|
||||||
|
"chat.connecting": "サーバーに接続中...",
|
||||||
|
"chat.vadInit": "音声検出を初期化中...",
|
||||||
"chat.empty.prompt": "下にテキストを入力して対話を開始",
|
"chat.empty.prompt": "下にテキストを入力して対話を開始",
|
||||||
"chat.empty.hint": "テキストでAIと対話、または左のボタンでビデオ通話を開始",
|
"chat.empty.hint": "テキストでAIと対話、または左のボタンでビデオ通話を開始",
|
||||||
"chat.welcome.prompt": "下にテキストを入力して対話を開始",
|
"chat.welcome.prompt": "下にテキストを入力して対話を開始",
|
||||||
@@ -65,6 +67,7 @@ export const jaJP: TranslationMap = {
|
|||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ シーン変化を検出",
|
"session.changeDetected": "👁️ シーン変化を検出",
|
||||||
"session.recognizing": "(音声認識中...)",
|
"session.recognizing": "(音声認識中...)",
|
||||||
|
"session.sttFailed": "(音声認識に失敗しました。もう一度お試しください)",
|
||||||
"session.noSpeech": "(音声が検出されませんでした)",
|
"session.noSpeech": "(音声が検出されませんでした)",
|
||||||
"session.interrupted": "(中断済み)",
|
"session.interrupted": "(中断済み)",
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const zhCN: TranslationMap = {
|
|||||||
"chat.title": "对话",
|
"chat.title": "对话",
|
||||||
"chat.mode.observation": "观察模式",
|
"chat.mode.observation": "观察模式",
|
||||||
"chat.reconnecting": "连接已断开,正在重连...",
|
"chat.reconnecting": "连接已断开,正在重连...",
|
||||||
|
"chat.connecting": "正在连接服务...",
|
||||||
|
"chat.vadInit": "正在初始化语音检测...",
|
||||||
"chat.empty.prompt": "在下方输入文字开始对话",
|
"chat.empty.prompt": "在下方输入文字开始对话",
|
||||||
"chat.empty.hint": "输入文字即可与 AI 交互,也可点击左侧按钮开启视频",
|
"chat.empty.hint": "输入文字即可与 AI 交互,也可点击左侧按钮开启视频",
|
||||||
"chat.welcome.prompt": "在下方输入文字开始对话",
|
"chat.welcome.prompt": "在下方输入文字开始对话",
|
||||||
@@ -65,6 +67,7 @@ export const zhCN: TranslationMap = {
|
|||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ 画面变化检测",
|
"session.changeDetected": "👁️ 画面变化检测",
|
||||||
"session.recognizing": "(语音识别中...)",
|
"session.recognizing": "(语音识别中...)",
|
||||||
|
"session.sttFailed": "(语音识别失败,请重试)",
|
||||||
"session.noSpeech": "(未识别到语音)",
|
"session.noSpeech": "(未识别到语音)",
|
||||||
"session.interrupted": "(已打断)",
|
"session.interrupted": "(已打断)",
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user