Skip to content

Make the voice end-of-turn pause longer and configurable - #301

Open
arqo123 wants to merge 1 commit into
artcc:developfrom
arqo123:fix/configurable-end-of-speech-window
Open

Make the voice end-of-turn pause longer and configurable#301
arqo123 wants to merge 1 commit into
artcc:developfrom
arqo123:fix/configurable-end-of-speech-window

Conversation

@arqo123

@arqo123 arqo123 commented Aug 19, 2026

Copy link
Copy Markdown

Closes #296.

Problem

ConversationMode set the VAD redemption window from a hardcoded, level-derived table: 1300 ms for A1/A2, 1100 ms for B1/B2, 900 ms for C1/C2, 1000 ms without a level. A learner pausing to search for a word regularly exceeds that, so the utterance is closed and sent mid-sentence and the tutor answers a fragment. Nothing in Settings exposes the window, and a level assigned too high makes it shorter still.

Change

Longer automatic values. 1800 ms for A1/A2, 1500 ms for B1/B2, 1200 ms for C1/C2, 1500 ms when the level is unknown. This raises the window for every user, which is the behaviour change in this PR — the scaling-by-level idea is kept, each tier just gets more room.

A user setting. conversation_speech_pause on the user stores the window in milliseconds: 0 means automatic, and 1000 / 2000 / 3000 override it. It is offered in Settings → Conversation next to the existing session settings, validated server-side against that set, and returned by /api/auth/me.

One resolution rule. frontend/src/lib/conversation-vad.ts holds resolveVadRedemptionMs(speechPause, cefrLevel) — the stored value wins when set, otherwise the level decides. The local vadRedemptionMs() in ConversationMode is gone.

Two notes on behaviour:

  • useMicVAD reads redemptionMs when the VAD is created, so a changed setting applies to the next conversation, not to a session already running.
  • A subscription downgrade resets the conversation quota settings but deliberately leaves this one alone — it is a comfort preference, not a quota.

The adaptive option from the issue (keep the turn open after an obviously incomplete utterance) is not implemented; it needs transcript-level signals that are not available at the point the VAD closes a turn.

Migration

0051_conversation_speech_pause adds the column with server_default="0". It is hand-written, since autogenerate needs the Docker stack; it applies with the usual alembic upgrade head.

Tests

  • backend/tests/test_conversation.py — PATCH stores a chosen pause, 0 restores automatic, an unoffered value returns 422, and /api/auth/me exposes the field with its default.
  • frontend/tests/lib/conversation-vad.test.ts — automatic tiers, unknown/missing level, explicit override wins, stored values outside the offered set are ignored, and every tier is longer than the previous window.
  • frontend/tests/components/ConversationSection.test.tsx — the options render, the chosen pause is sent, automatic is sent back as 0, and saving another setting preserves the stored pause.

Backend: 976 passed, coverage 85%. Frontend: 460 passed, lint and tsc --noEmit clean.

Docs

specs/phase-3-conversation.instructions.md, specs/database-models.instructions.md, specs/api-endpoints.instructions.md, specs/architecture-frontend.instructions.md and the conversation-settings line in AGENTS.md are updated. CHANGELOG.md and specs/version.md are left untouched — releases and version bumps look like maintainer territory, and version edits would conflict across parallel PRs.

Not covered here

The roadmap/ENABLE_CONVERSATION_BARGE_IN discrepancy noticed in the issue is untouched — it is a separate question about whether the roadmap or the flag is out of date.

The VAD closed an utterance after 0.9-1.3 s of silence, derived from the
CEFR level and hardcoded in ConversationMode. A learner reaching for a
word routinely pauses longer than that, so the turn was submitted while
they were still speaking and the tutor answered half a sentence. Nothing
in Settings could change it, and a level assigned too high made the
window shorter still.

The automatic values now leave more room — 1800 ms for A1/A2, 1500 ms for
B1/B2, 1200 ms for C1/C2, 1500 ms without a level — and learners who need
more can choose 1, 2 or 3 seconds in Settings under Conversation.

The resolution rule lives in lib/conversation-vad.ts: the stored
conversation_speech_pause wins when set, 0 means automatic. The value is
persisted on the user, validated against the offered set, and read when
the VAD is created, so a change applies to the next session.

The setting is a comfort preference, not a quota, so a subscription
downgrade leaves it untouched.
@artcc

artcc commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution. The implementation preserves the stable-turn behavior, so speaking while Lingu is responding still does not interrupt the tutor.

Before merging, could you please review the speech-pause selector on narrow mobile screens? The four buttons use a single non-wrapping flex row, and longer German, Spanish, or Russian translations can overflow or become clipped at widths of 320–375 px. A two-column grid on mobile and four columns on larger screens would solve it.

There is also a small correction needed in the PR description: apply_subscription_quotas() resets the existing conversation settings when a subscription is activated, not when it is downgraded.

I will handle the remaining documentation and versioning updates separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Voice conversation cuts off utterances during hesitation pauses: end-of-speech window is 0.9–1.3 s, hardcoded and not adjustable

2 participants