package callflow import ( "context" "encoding/binary" "errors" "time" ) // CaptureConfig mirrors the real Cell turn boundary: wait for speech, keep // collecting until bounded duration or end-of-speech silence, then send one // canonical PCM16 turn to the AI adapter. type CaptureConfig struct { FirstSpeechTimeout time.Duration MaxDuration time.Duration EndSilence time.Duration VoiceThreshold int MaxTurns int } func captureTurn(ctx context.Context, session MediaSession, cfg CaptureConfig) ([]byte, error) { if cfg.FirstSpeechTimeout <= 0 { cfg.FirstSpeechTimeout = 5 * time.Second } if cfg.MaxDuration <= 0 { cfg.MaxDuration = cfg.FirstSpeechTimeout } if cfg.EndSilence < 0 { cfg.EndSilence = 0 } startedAt := time.Now() firstDeadline := startedAt.Add(cfg.FirstSpeechTimeout) maxDeadline := startedAt.Add(cfg.MaxDuration) started := cfg.VoiceThreshold <= 0 lastVoice := time.Time{} var frames []byte for { now := time.Now() if !started && !now.Before(firstDeadline) { return nil, errors.New("no speech detected before capture timeout") } if !maxDeadline.After(now) { break } readDeadline := maxDeadline if !started && firstDeadline.Before(readDeadline) { readDeadline = firstDeadline } if started && cfg.EndSilence > 0 && !lastVoice.IsZero() { silenceDeadline := lastVoice.Add(cfg.EndSilence) if silenceDeadline.Before(readDeadline) { readDeadline = silenceDeadline } } readCtx, cancel := context.WithDeadline(ctx, readDeadline) payload, err := session.ReadPayload(readCtx) cancel() if err != nil { if errors.Is(err, context.DeadlineExceeded) { if started && cfg.EndSilence > 0 && !lastVoice.IsZero() && !time.Now().Before(lastVoice.Add(cfg.EndSilence)) { break } continue } if errors.Is(err, context.Canceled) && ctx.Err() == nil { continue } return nil, err } if len(payload) == 0 { continue } if cfg.VoiceThreshold > 0 { if pcm16VoiceLevel(payload) >= cfg.VoiceThreshold { started = true lastVoice = time.Now() } if started { frames = append(frames, payload...) } } else { started = true lastVoice = time.Now() frames = append(frames, payload...) } if started && cfg.EndSilence > 0 && !lastVoice.IsZero() && !time.Now().Before(lastVoice.Add(cfg.EndSilence)) { break } } if len(frames) == 0 { return nil, errors.New("captured audio is empty") } return frames, nil } func pcm16VoiceLevel(pcm []byte) int { if len(pcm) < 2 { return 0 } var sum uint64 count := len(pcm) / 2 for i := 0; i < count; i++ { value := int64(int16(binary.LittleEndian.Uint16(pcm[i*2 : i*2+2]))) if value < 0 { value = -value } sum += uint64(value) } return int(sum / uint64(count)) }