| | |
| | | indicator overlay.Overlay |
| | | |
| | | // State |
| | | mu sync.Mutex |
| | | isRecording bool |
| | | isFreetalking bool |
| | | hotkeyActive bool |
| | | hotkeyPressTime time.Time |
| | | isCombination bool |
| | | tapStopOnPress bool |
| | | isRecordingHotkey bool |
| | | hideGen atomic.Uint64 |
| | | lastStopTime time.Time |
| | | hotkeyPollSeen bool |
| | | lastHotkeyDown bool |
| | | lastHotkeyBlocked time.Time |
| | | silenceSince time.Time // when continuous silence started (free-talk only) |
| | | freeTalkStart time.Time // when free-talk mode started |
| | | liveCaptionCancel context.CancelFunc |
| | | liveCaptionSeq uint64 |
| | | mu sync.Mutex |
| | | isRecording bool |
| | | isStoppingRecording bool |
| | | isFreetalking bool |
| | | hotkeyActive bool |
| | | hotkeyPressTime time.Time |
| | | isCombination bool |
| | | tapStopOnPress bool |
| | | isRecordingHotkey bool |
| | | hideGen atomic.Uint64 |
| | | lastStopTime time.Time |
| | | hotkeyPollSeen bool |
| | | lastHotkeyDown bool |
| | | lastHotkeyBlocked time.Time |
| | | silenceSince time.Time // when continuous silence started (free-talk only) |
| | | freeTalkStart time.Time // when free-talk mode started |
| | | liveCaptionCancel context.CancelFunc |
| | | liveCaptionSeq uint64 |
| | | releaseTailCaptureNanos atomic.Int64 |
| | | } |
| | | |
| | | func RunApp() error { |
| | |
| | | ) |
| | | } |
| | | |
| | | if a.eng == nil || a.isRecordingHotkey { |
| | | if a.eng == nil || a.isRecordingHotkey || a.isStoppingRecording { |
| | | if isDown && time.Since(a.lastHotkeyBlocked) > time.Second { |
| | | a.lastHotkeyBlocked = time.Now() |
| | | logger.Info("Hotkey ignored: key=%s engineReady=%t recordingHotkey=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.eng != nil, a.isRecordingHotkey) |
| | | logger.Info("Hotkey ignored: key=%s engineReady=%t recordingHotkey=%t stoppingRecording=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.eng != nil, a.isRecordingHotkey, a.isStoppingRecording) |
| | | } |
| | | return |
| | | } |
| | |
| | | } |
| | | |
| | | func (a *App) startRecordingLocked() { |
| | | if a.isRecording { |
| | | if a.isRecording || a.isStoppingRecording { |
| | | return |
| | | } |
| | | a.isRecording = true |
| | |
| | | } |
| | | |
| | | a.stopLiveCaptionLocked() |
| | | hasVoice := a.recorder.HasVoiceActivity() |
| | | a.isStoppingRecording = true |
| | | a.indicator.SetStatus(overlay.StatusProcessing, "识别中") |
| | | go func() { |
| | | samples := a.recorder.StopAndGetSamples() |
| | | samples, hasVoice := a.stopRecorderAfterReleaseTailCapture() |
| | | a.recognizeAndPaste(hasVoice, samples) |
| | | }() |
| | | } |
| | | |
| | | func (a *App) startTapRecordingLocked() { |
| | | if a.isRecording || a.isFreetalking { |
| | | if a.isRecording || a.isStoppingRecording || a.isFreetalking { |
| | | return |
| | | } |
| | | a.isFreetalking = true |
| | |
| | | a.lastStopTime = time.Now() |
| | | |
| | | a.stopLiveCaptionLocked() |
| | | hasVoice := a.recorder.HasVoiceActivity() |
| | | a.isStoppingRecording = true |
| | | logger.Info("Tap recording stopped") |
| | | a.indicator.SetStatus(overlay.StatusProcessing, "识别中") |
| | | go func() { |
| | | samples := a.recorder.StopAndGetSamples() |
| | | samples, hasVoice := a.stopRecorderAfterReleaseTailCapture() |
| | | a.recognizeAndPaste(hasVoice, samples) |
| | | }() |
| | | } |
| | |
| | | logger.Info("Free talk stopped (silence auto-stop)") |
| | | a.indicator.SetStatus(overlay.StatusProcessing, "识别中") |
| | | a.recognizeAndPaste(hasVoice, samples) |
| | | } |
| | | |
| | | func (a *App) waitForReleaseTailCapture() { |
| | | delay := a.releaseTailCaptureDelay() |
| | | if delay <= 0 { |
| | | return |
| | | } |
| | | logger.Info("Release tail capture: waiting %dms before stopping audio", delay.Milliseconds()) |
| | | time.Sleep(delay) |
| | | } |
| | | |
| | | func (a *App) releaseTailCaptureDelay() time.Duration { |
| | | nanos := a.releaseTailCaptureNanos.Load() |
| | | if nanos <= 0 { |
| | | return 0 |
| | | } |
| | | return time.Duration(nanos) |
| | | } |
| | | |
| | | func (a *App) stopRecorderAfterReleaseTailCapture() ([]float32, bool) { |
| | | a.waitForReleaseTailCapture() |
| | | samples := a.recorder.StopAndGetSamples() |
| | | hasVoice := a.recorder.HasVoiceActivity() |
| | | |
| | | a.mu.Lock() |
| | | a.isStoppingRecording = false |
| | | a.mu.Unlock() |
| | | |
| | | return samples, hasVoice |
| | | } |
| | | |
| | | // recognizeAndPaste runs ASR on the recorded samples and pastes the result. |
| | |
| | | } |
| | | |
| | | func (a *App) replaceEngine(eng engine.Engine) { |
| | | a.releaseTailCaptureNanos.Store(int64(releaseTailCaptureDelayForEngine(eng))) |
| | | |
| | | a.engineMu.Lock() |
| | | old := a.eng |
| | | a.eng = eng |
| | |
| | | } |
| | | } |
| | | |
| | | func releaseTailCaptureDelayForEngine(eng engine.Engine) time.Duration { |
| | | tailCaptureEng, ok := eng.(engine.ReleaseTailCaptureEngine) |
| | | if !ok { |
| | | return 0 |
| | | } |
| | | delay := tailCaptureEng.ReleaseTailCaptureDelay() |
| | | if delay < 0 { |
| | | return 0 |
| | | } |
| | | return delay |
| | | } |
| | | |
| | | func hotkeyReadyText(keyName, mode string) string { |
| | | if mode == config.HotkeyModeTap { |
| | | return "点按" + keyName + "说话" |