Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ func record(capture audio.CaptureDevice, keyup <-chan struct{}, sess transcriber
var voiceDetected bool
var lastVoiceTime time.Time
done := make(chan struct{})
var closeOnce sync.Once
closeDone := func() { closeOnce.Do(func() { close(done) }) }

capture.SetCallback(func(data []byte, frameCount uint32) {
bufMu.Lock()
Expand Down Expand Up @@ -569,6 +571,7 @@ func record(capture audio.CaptureDevice, keyup <-chan struct{}, sess transcriber
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var lastTranscriptWarn time.Time
for {
select {
case <-done:
Expand All @@ -584,7 +587,15 @@ func record(capture audio.CaptureDevice, keyup <-chan struct{}, sess transcriber
checkSilenceDuring(&bufMu, &lastVoiceTime)
}
if streamEnabled {
checkTranscriptSilence(lastTranscript)
checkTranscriptSilence(lastTranscript, &lastTranscriptWarn)
if shouldAutoClose(lastTranscript, recordStart) {
log.Info("silence_auto_close")
tuiSend(SilenceAutoCloseMsg{})
go beep.PlayEnd()
time.Sleep(recordTail)
closeDone()
return
}
}
}
}
Expand All @@ -598,7 +609,7 @@ func record(capture audio.CaptureDevice, keyup <-chan struct{}, sess transcriber
if streamEnabled {
time.Sleep(recordTail)
}
close(done)
closeDone()
}()
<-done

Expand Down Expand Up @@ -628,6 +639,7 @@ func checkNoVoice(mu *sync.Mutex, elapsed float64, peakLevel *float64, beeped *b
}

const silenceTimeout = 8 * time.Second
const silenceAutoCloseTimeout = 30 * time.Second

func checkSilenceDuring(mu *sync.Mutex, lastVoiceTime *time.Time) {
mu.Lock()
Expand All @@ -643,17 +655,31 @@ func checkSilenceDuring(mu *sync.Mutex, lastVoiceTime *time.Time) {
}
}

func checkTranscriptSilence(lastTranscript *atomic.Int64) {
func shouldAutoClose(lastTranscript *atomic.Int64, recordStart time.Time) bool {
ref := recordStart
if ts := lastTranscript.Load(); ts != 0 {
ref = time.Unix(0, ts)
}
return time.Since(ref) > silenceAutoCloseTimeout
}

func checkTranscriptSilence(lastTranscript *atomic.Int64, lastWarn *time.Time) {
ts := lastTranscript.Load()
if ts == 0 {
return // no transcript received yet
return
}
if time.Since(time.Unix(0, ts)) > silenceTimeout {
lastTranscript.Store(time.Now().UnixNano())
log.Info("transcript_silence_warning")
tuiSend(TranscriptSilenceMsg{})
beep.PlayError()
timeSinceTranscript := time.Since(time.Unix(0, ts))
if timeSinceTranscript <= silenceTimeout {
*lastWarn = time.Time{}
return
}
if !lastWarn.IsZero() && time.Since(*lastWarn) <= silenceTimeout {
return
}
*lastWarn = time.Now()
log.Info("transcript_silence_warning")
tuiSend(TranscriptSilenceMsg{})
beep.PlayError()
}

func updatePercentileStats() {
Expand Down
3 changes: 2 additions & 1 deletion tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type RateLimitMsg struct{ Text string } // Rate limit info
type RequestDeviceSelectionMsg struct{} // Request to change microphone
type NoVoiceWarningMsg struct{} // No voice detected during recording
type TranscriptSilenceMsg struct{} // No transcript updates from backend
type SilenceAutoCloseMsg struct{} // Recording auto-closed due to prolonged silence
type HybridHelpMsg struct{ Enabled bool } // Whether hybrid tap+hold is enabled
type UpdateAvailableMsg struct{ Version string } // New version available
type tickMsg time.Time
Expand Down Expand Up @@ -194,7 +195,7 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.liveText = ""
m.clampViewIdx()

case RecordingStopMsg:
case RecordingStopMsg, SilenceAutoCloseMsg:
m.state = tuiStateIdle
m.audioLevel = 0
m.clampViewIdx()
Expand Down