mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(voice-transcribe): 新增语音转写语言过滤配置功能(支持用户自定义允许的转写语言),优化模型下载的超时处理与进度日志,提升下载稳健性,同步更新相关 UI 样式。
This commit is contained in:
@@ -987,6 +987,11 @@ function ChatPage(_props: ChatPageProps) {
|
||||
})
|
||||
}
|
||||
|
||||
const handleRequireModelDownload = useCallback((sessionId: string, messageId: string) => {
|
||||
setPendingVoiceTranscriptRequest({ sessionId, messageId })
|
||||
setShowVoiceTranscribeDialog(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`chat-page ${isResizing ? 'resizing' : ''}`}>
|
||||
{/* 左侧会话列表 */}
|
||||
@@ -1166,6 +1171,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
showTime={!showDateDivider && showTime}
|
||||
myAvatarUrl={myAvatarUrl}
|
||||
isGroupChat={isGroupChat(currentSession.username)}
|
||||
onRequireModelDownload={handleRequireModelDownload}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -1298,20 +1304,16 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}}
|
||||
onDownloadComplete={async () => {
|
||||
setShowVoiceTranscribeDialog(false)
|
||||
// 下载完成后,继续转写
|
||||
// 下载完成后,触发页面刷新让组件重新尝试转写
|
||||
// 通过更新缓存触发组件重新检查
|
||||
if (pendingVoiceTranscriptRequest) {
|
||||
try {
|
||||
const result = await window.electronAPI.chat.getVoiceTranscript(
|
||||
pendingVoiceTranscriptRequest.sessionId,
|
||||
pendingVoiceTranscriptRequest.messageId
|
||||
)
|
||||
if (result.success) {
|
||||
const cacheKey = `voice-transcript:${pendingVoiceTranscriptRequest.messageId}`
|
||||
voiceTranscriptCache.set(cacheKey, (result.transcript || '').trim())
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ChatPage] 语音转文字失败:', error)
|
||||
}
|
||||
// 清除缓存中的请求标记,让组件可以重新尝试
|
||||
const cacheKey = `voice-transcript:${pendingVoiceTranscriptRequest.messageId}`
|
||||
// 不直接调用转写,而是让组件自己重试
|
||||
// 通过触发一个自定义事件来通知所有 MessageBubble 组件
|
||||
window.dispatchEvent(new CustomEvent('model-downloaded', {
|
||||
detail: { messageId: pendingVoiceTranscriptRequest.messageId }
|
||||
}))
|
||||
}
|
||||
setPendingVoiceTranscriptRequest(null)
|
||||
}}
|
||||
@@ -1330,12 +1332,13 @@ const senderAvatarCache = new Map<string, { avatarUrl?: string; displayName?: st
|
||||
const senderAvatarLoading = new Map<string, Promise<{ avatarUrl?: string; displayName?: string } | null>>()
|
||||
|
||||
// 消息气泡组件
|
||||
function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }: {
|
||||
function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, onRequireModelDownload }: {
|
||||
message: Message;
|
||||
session: ChatSession;
|
||||
showTime?: boolean;
|
||||
myAvatarUrl?: string;
|
||||
isGroupChat?: boolean;
|
||||
onRequireModelDownload?: (sessionId: string, messageId: string) => void;
|
||||
}) {
|
||||
const isSystem = isSystemMessage(message.localType)
|
||||
const isEmoji = message.localType === 47
|
||||
@@ -1682,21 +1685,27 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
const requestVoiceTranscript = useCallback(async () => {
|
||||
if (voiceTranscriptLoading || voiceTranscriptRequestedRef.current) return
|
||||
|
||||
// 检查模型状态
|
||||
const modelStatus = await window.electronAPI.whisper?.getModelStatus()
|
||||
if (!modelStatus?.exists) {
|
||||
// 模型未下载,抛出错误让外层处理
|
||||
const error: any = new Error('MODEL_NOT_DOWNLOADED')
|
||||
error.requiresDownload = true
|
||||
error.sessionId = session.username
|
||||
error.messageId = String(message.localId)
|
||||
throw error
|
||||
// 检查 whisper API 是否可用
|
||||
if (!window.electronAPI?.whisper?.getModelStatus) {
|
||||
console.warn('[ChatPage] whisper API 不可用')
|
||||
setVoiceTranscriptError(true)
|
||||
return
|
||||
}
|
||||
|
||||
voiceTranscriptRequestedRef.current = true
|
||||
setVoiceTranscriptLoading(true)
|
||||
setVoiceTranscriptError(false)
|
||||
try {
|
||||
// 检查模型状态
|
||||
const modelStatus = await window.electronAPI.whisper.getModelStatus()
|
||||
if (!modelStatus?.exists) {
|
||||
const error: any = new Error('MODEL_NOT_DOWNLOADED')
|
||||
error.requiresDownload = true
|
||||
error.sessionId = session.username
|
||||
error.messageId = String(message.localId)
|
||||
throw error
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.chat.getVoiceTranscript(session.username, String(message.localId))
|
||||
if (result.success) {
|
||||
const transcriptText = (result.transcript || '').trim()
|
||||
@@ -1709,8 +1718,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
} catch (error: any) {
|
||||
// 检查是否是模型未下载错误
|
||||
if (error?.requiresDownload) {
|
||||
// 不显示错误状态,等待用户手动点击转文字按钮时会触发下载弹窗
|
||||
voiceTranscriptRequestedRef.current = false
|
||||
// 模型未下载,触发下载弹窗
|
||||
onRequireModelDownload?.(error.sessionId, error.messageId)
|
||||
// 不要重置 voiceTranscriptRequestedRef,避免重复触发
|
||||
setVoiceTranscriptLoading(false)
|
||||
return
|
||||
}
|
||||
setVoiceTranscriptError(true)
|
||||
@@ -1718,7 +1729,27 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
} finally {
|
||||
setVoiceTranscriptLoading(false)
|
||||
}
|
||||
}, [message.localId, session.username, voiceTranscriptCacheKey, voiceTranscriptLoading])
|
||||
}, [message.localId, session.username, voiceTranscriptCacheKey, voiceTranscriptLoading, onRequireModelDownload])
|
||||
|
||||
// 监听模型下载完成事件
|
||||
useEffect(() => {
|
||||
if (!isVoice) return
|
||||
|
||||
const handleModelDownloaded = (event: CustomEvent) => {
|
||||
if (event.detail?.messageId === String(message.localId)) {
|
||||
// 重置状态,允许重新尝试转写
|
||||
voiceTranscriptRequestedRef.current = false
|
||||
setVoiceTranscriptError(false)
|
||||
// 立即尝试转写
|
||||
void requestVoiceTranscript()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('model-downloaded', handleModelDownloaded as EventListener)
|
||||
return () => {
|
||||
window.removeEventListener('model-downloaded', handleModelDownloaded as EventListener)
|
||||
}
|
||||
}, [isVoice, message.localId, requestVoiceTranscript])
|
||||
|
||||
// 根据设置决定是否自动转写
|
||||
const [autoTranscribeEnabled, setAutoTranscribeEnabled] = useState(false)
|
||||
|
||||
@@ -352,6 +352,37 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.language-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.language-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:hover .checkbox-label {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 46px;
|
||||
|
||||
@@ -48,6 +48,7 @@ function SettingsPage() {
|
||||
const [whisperDownloadProgress, setWhisperDownloadProgress] = useState(0)
|
||||
const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null)
|
||||
const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false)
|
||||
const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh'])
|
||||
|
||||
const [isLoading, setIsLoadingState] = useState(false)
|
||||
const [isTesting, setIsTesting] = useState(false)
|
||||
@@ -112,6 +113,7 @@ function SettingsPage() {
|
||||
const savedWhisperModelName = await configService.getWhisperModelName()
|
||||
const savedWhisperModelDir = await configService.getWhisperModelDir()
|
||||
const savedAutoTranscribe = await configService.getAutoTranscribeVoice()
|
||||
const savedTranscribeLanguages = await configService.getTranscribeLanguages()
|
||||
|
||||
if (savedKey) setDecryptKey(savedKey)
|
||||
if (savedPath) setDbPath(savedPath)
|
||||
@@ -123,6 +125,15 @@ function SettingsPage() {
|
||||
if (savedImageAesKey) setImageAesKey(savedImageAesKey)
|
||||
setLogEnabled(savedLogEnabled)
|
||||
setAutoTranscribeVoice(savedAutoTranscribe)
|
||||
setTranscribeLanguages(savedTranscribeLanguages)
|
||||
|
||||
// 如果语言列表为空,保存默认值
|
||||
if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) {
|
||||
const defaultLanguages = ['zh']
|
||||
setTranscribeLanguages(defaultLanguages)
|
||||
await configService.setTranscribeLanguages(defaultLanguages)
|
||||
}
|
||||
|
||||
if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir)
|
||||
} catch (e) {
|
||||
console.error('加载配置失败:', e)
|
||||
@@ -454,6 +465,7 @@ function SettingsPage() {
|
||||
}
|
||||
await configService.setWhisperModelDir(whisperModelDir)
|
||||
await configService.setAutoTranscribeVoice(autoTranscribeVoice)
|
||||
await configService.setTranscribeLanguages(transcribeLanguages)
|
||||
await configService.setOnboardingDone(true)
|
||||
|
||||
showMessage('配置保存成功,正在测试连接...', true)
|
||||
@@ -490,6 +502,7 @@ function SettingsPage() {
|
||||
setCachePath('')
|
||||
setLogEnabled(false)
|
||||
setAutoTranscribeVoice(false)
|
||||
setTranscribeLanguages(['zh'])
|
||||
setWhisperModelDir('')
|
||||
setWhisperModelStatus(null)
|
||||
setWhisperDownloadProgress(0)
|
||||
@@ -757,6 +770,46 @@ function SettingsPage() {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>支持的语言</label>
|
||||
<span className="form-hint">选择需要识别的语言(至少选择一种)</span>
|
||||
<div className="language-checkboxes">
|
||||
{[
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'en', name: '英文' },
|
||||
{ code: 'ja', name: '日文' },
|
||||
{ code: 'ko', name: '韩文' }
|
||||
].map((lang) => (
|
||||
<label key={lang.code} className="language-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={transcribeLanguages.includes(lang.code)}
|
||||
onChange={async (e) => {
|
||||
const checked = e.target.checked
|
||||
let newLanguages: string[]
|
||||
|
||||
if (checked) {
|
||||
// 添加语言
|
||||
newLanguages = [...transcribeLanguages, lang.code]
|
||||
} else {
|
||||
// 移除语言,但至少保留一个
|
||||
if (transcribeLanguages.length <= 1) {
|
||||
showMessage('至少需要选择一种语言', false)
|
||||
return
|
||||
}
|
||||
newLanguages = transcribeLanguages.filter(l => l !== lang.code)
|
||||
}
|
||||
|
||||
setTranscribeLanguages(newLanguages)
|
||||
await configService.setTranscribeLanguages(newLanguages)
|
||||
showMessage(`已${checked ? '添加' : '移除'}${lang.name}`, true)
|
||||
}}
|
||||
/>
|
||||
<span className="checkbox-label">{lang.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group whisper-section">
|
||||
<label>语音识别模型 (SenseVoiceSmall)</label>
|
||||
<span className="form-hint">基于 Sherpa-onnx,支持中文、英文、日文、韩文</span>
|
||||
|
||||
Reference in New Issue
Block a user