Merge the capture and dialogue sweep (#241)

One bug with teeth. capture.windowBytes computed int64(window.Seconds()) *
bytesPerSecond, truncating to whole seconds. A sub-second window came out as
zero bytes, which transcribeFile reads as no window, so it hands the transcriber
the entire recording in one call. Multiplied in float now.

One drifted comment. Peek said expired entries below the top are left alone. The
code deletes the whole stack, which is what Pop and TakeExpired both document.
The corrected comment also names the ordering the silent drop depends on:
TakeExpired must run before Peek on a turn, or the expiry notice is unreachable.

Two default TTL literals became DefaultClarifyTTL and DefaultSessionTTL, beside
the existing DefaultMaxAttempts. PendingQuestion was not gofmt clean.

Push and Pop are unused outside tests and stay. Push documents itself as the
widening V-561 fills in, and the stack tests cover it.

Neither package holds a Russian stem pattern. The only Russian strings are two
markers and two prompts, and none of them routes or becomes a fact.

(V-581)
This commit is contained in:
2026-08-06 03:13:01 +04:00
4 changed files with 28 additions and 13 deletions
+4 -1
View File
@@ -626,6 +626,9 @@ func windowBytes(f audio.Format, window time.Duration) int64 {
if bps <= 0 || f.SampleRate <= 0 || window <= 0 {
return 0
}
per := int64(window.Seconds()) * bytesPerSecond(f)
// Fractional seconds count. Truncating the window to whole seconds turned
// any sub-second window into zero bytes, which the caller reads as "no
// window" and answers by handing the transcriber the entire meeting at once.
per := int64(window.Seconds() * float64(bytesPerSecond(f)))
return per - per%bps
}
+1 -3
View File
@@ -193,9 +193,7 @@ func ChunkText(text string, maxRunes int) []string {
// Oversized sentence: emit what is buffered, then cut this one on
// word boundaries.
flush()
for _, piece := range splitWords(sr, maxRunes) {
out = append(out, piece)
}
out = append(out, splitWords(sr, maxRunes)...)
continue
}
if len(cur)+len(sr) > maxRunes {
+17 -8
View File
@@ -37,8 +37,8 @@ type PendingQuestion struct {
// request rather than alone: "завтра" names a day for an hour said earlier.
WhenText string
Asked time.Time
TTL time.Duration
Attempts int // questions already asked
TTL time.Duration
Attempts int // questions already asked
// MaxAttempts caps Attempts. 0 ⇒ DefaultMaxAttempts.
MaxAttempts int
}
@@ -104,11 +104,14 @@ type ClarifyStore struct {
// reply can carry.
const MaxStackDepth = 2
// DefaultClarifyTTL — how long a parked question stays his answer to give.
// Short, like confirmTTL in voice.go: a clarifying question is a same-breath
// gesture, and a stale one should not eat a later utterance.
const DefaultClarifyTTL = 90 * time.Second
func NewClarifyStore(defaultTTL time.Duration) *ClarifyStore {
if defaultTTL <= 0 {
// Short, like confirmTTL in voice.go: a clarifying question is a
// same-breath gesture, a stale one should not eat a later utterance.
defaultTTL = 90 * time.Second
defaultTTL = DefaultClarifyTTL
}
return &ClarifyStore{
stacks: make(map[string][]*PendingQuestion),
@@ -151,9 +154,15 @@ func (s *ClarifyStore) Push(id string, q *PendingQuestion) *PendingQuestion {
return dropped
}
// Peek returns the live question on top, or nil when there is none. Expired
// entries below it are left alone: TakeExpired is what reports those, and
// dropping one here would be the silent death this store is careful about.
// Peek returns the live question on top, or nil when there is none. An expired
// top takes the whole stack with it, exactly as Pop does: the clock that killed
// it has been running for everything underneath too.
//
// That drop is silent, which is the death this store is otherwise careful
// about, so TakeExpired has to run BEFORE Peek on a turn — it is what counts the
// dropped questions and tells him they are gone. cmd/mavend/voice.go calls
// clarifyExpiredNotice first for that reason, and reordering the two makes the
// notice unreachable rather than wrong.
func (s *ClarifyStore) Peek(id string, now time.Time) *PendingQuestion {
s.mu.RLock()
stack := s.stacks[id]
+6 -1
View File
@@ -87,9 +87,14 @@ type SessionStore struct {
persist SessionPersister // may be nil: memory only (tests, no-store paths)
}
// DefaultSessionTTL — how long a turn stays available to inherit from. Longer
// than DefaultClarifyTTL because this is not a question waiting on an answer:
// it is the last thing said, and a follow-up may land after a real pause.
const DefaultSessionTTL = 2 * time.Minute
func NewSessionStore(defaultTTL time.Duration) *SessionStore {
if defaultTTL <= 0 {
defaultTTL = 2 * time.Minute
defaultTTL = DefaultSessionTTL
}
return &SessionStore{
sessions: make(map[string]*Session),