# NousResearch/hermes-agent · Desktop STT transcription timeout too short

## Open now

- Nothing open on GitHub.

Multiple issues and fixes address the desktop app's audio transcription timeout being too short (defaulting to ~15s), causing local speech-to-text to fail on longer audio. Proposed solutions include overriding, extending, or making the timeout configurable.

## TL;DR

- 6 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #68056. Verify it addresses your case.
- **Recommended:** read PR #68056 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #39448: \[Feature Request\] Configurable timeout for STT transcription (currently hardcoded 15s in Desktop)
- #46573: Desktop: transcribeAudio missing timeoutMs — local STT fails for audio >15s

## Prior & competing PRs

- **#46579: fix(desktop): override request timeout for transcribeAudio to prevent local STT timeout**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#46530`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#68056: fix(desktop): extend read aloud + transcription timeouts (salvage of #39286)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/src/hermes.test.ts`, `apps/desktop/src/hermes.ts`
  - fix hunks:
  - `apps/desktop/src/hermes.test.ts`

    ```diff
    @@ -1,6 +1,12 @@
     import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
     
     import {
    +  AUDIO_SPEAK_MAX_REQUEST_TIMEOUT_MS,
    +  AUDIO_SPEAK_MIN_REQUEST_TIMEOUT_MS,
    +  AUDIO_TRANSCRIBE_MAX_REQUEST_TIMEOUT_MS,
    +  AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS,
    +  audioSpeakRequestTimeoutMs,
    +  audioTranscribeRequestTimeoutMs,
       getCronJobs,
       getGlobalModelInfo,
       getGlobalModelOptions,
    ```

    ```diff
    @@ -12,7 +18,9 @@ import {
       listAllProfileSessions,
       listSessions,
       listSidebarSessions,
    -  resetSidebarBatchCapability
    +  resetSidebarBatchCapability,
    +  speakText,
    +  transcribeAudio
     } from './hermes'
     import { refreshActiveProfile } from './store/profile'
     
    ```

    ```diff
    @@ -23,7 +31,7 @@ const emptySessionsResponse = {
       total: 0
     }
     
    -describe('Hermes REST session helpers', () => {
    +describe('Hermes REST helpers', () => {
       let api: ReturnType<typeof vi.fn>
     
       beforeEach(() => {
    ```

    ```diff
    @@ -322,6 +330,62 @@ describe('Hermes REST session helpers', () => {
         })
       })
     
    +  it('bounds blocking TTS synthesis timeouts by text length', () => {
    +    expect(audioSpeakRequestTimeoutMs('short message')).toBe(AUDIO_SPEAK_MIN_REQUEST_TIMEOUT_MS)
    +    expect(audioSpeakRequestTimeoutMs('x'.repeat(8_000))).toBe(280_000)
    +    expect(audioSpeakRequestTimeoutMs('x'.repeat(100_000))).toBe(AUDIO_SPEAK_MAX_REQUEST_TIMEOUT_MS)
    +  })
    +
    +  it('uses an extended timeout for blocking TTS synthesis', async () => {
    +    api.mockResolvedValueOnce({
    +      data_url: 'data:audio/mpeg;base64,AA==',
    +      mime_type: 'audio/mpeg',
    +      ok: true,
    +      provider: 'openai'
    +    })
    +
    +    await expect(speakText('Read this aloud')).resolves.toEqual({
    +      data_url: 'data:audio/mpeg;base64,AA==',
    +      mime_type: 'audio/mpeg',
    +      ok: true,
    +      provider: 'openai'
    +    })
    +
    +    expect(api).toHaveBeenCalledWith({
    +      body: { text: 'Read this aloud' },
    +      method: 'POST',
    +      path: '/api/audio/speak',
    +      timeoutMs: AUDIO_SPEAK_MIN_REQUEST_TIMEOUT_MS
    +    })
    +  })
    +
    +  it('bounds blocking transcription timeouts by payload length', () => {
    +    expect(audioTranscribeRequestTimeoutMs('data:audio/webm;base64,AA==')).toBe(AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS)
    +    expect(audioTranscribeRequestTimeoutMs('x'.repeat(3_000_000))).toBe(300_000)
    +    expect(audioTranscribeRequestTimeoutMs('x'.repeat(9_000_000))).toBe(AUDIO_TRANSCRIBE_MAX_REQUEST_TIMEOUT_MS)
    +  })
    +
    +  it('uses an extended timeout for blocking transcription', async () => {
    +    api.mockResolvedValueOnce({
    +      ok: true,
    +      provider: 'openai',
    +      text: 'transcribed text'
    +    })
    +
    +    await expect(transcribeAudio('data:audio/webm;base64,AA==', 'audio/webm')).resolves.toEqual({
    +      ok: true,
    +      provider: 'openai',
    +      text: 'transcribed text'
    +    })
    +
    +    expect(api).toHaveBeenCalledWith({
    +      body: { data_url: 'data:audio/webm;base64,AA==', mime_type: 'audio/webm' },
    +      method: 'POST',
    +      path: '/api/audio/transcribe',
    +      timeoutMs: AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS
    +    })
    +  })
    +
       it('defaults model options to configured providers only', async () => {
         await getGlobalModelOptions()
     
    ```
  - `apps/desktop/src/hermes.ts`

    ```diff
    @@ -84,6 +84,36 @@ const SESSION_LIST_REQUEST_TIMEOUT_MS = 60_000
     // agent-turn ceiling (agent.gateway_timeout = 1800s) so the ack timeout only
     // ever fires when the turn itself would have been abandoned server-side.
     export const PROMPT_SUBMIT_REQUEST_TIMEOUT_MS = 1_800_000
    +export const AUDIO_SPEAK_MIN_REQUEST_TIMEOUT_MS = 180_000
    +export const AUDIO_SPEAK_MAX_REQUEST_TIMEOUT_MS = 600_000
    +const AUDIO_SPEAK_TIMEOUT_MS_PER_CHAR = 35
    +
    +export function audioSpeakRequestTimeoutMs(text: string): number {
    +  const estimated = Math.max(
    +    AUDIO_SPEAK_MIN_REQUEST_TIMEOUT_MS,
    +    Math.ceil(String(text || '').length * AUDIO_SPEAK_TIMEOUT_MS_PER_CHAR)
    +  )
    +
    +  return Math.min(AUDIO_SPEAK_MAX_REQUEST_TIMEOUT_MS, estimated)
    +}
    +
    +export const AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS = 180_000
    +export const AUDIO_TRANSCRIBE_MAX_REQUEST_TIMEOUT_MS = 600_000
    +// The transcribe payload is the base64 audio data URL itself, so its string
    +// length tracks clip size. ~0.1ms/char keeps short clips at the floor while
    +// letting multi-minute recordings scale toward the cap (a base64 char is
    +// ~0.75 bytes, so at 128kbps ≈ 21k chars/s of audio this budgets ~2s of
    +// timeout per 1s of audio before the cap clamps it).
    +const AUDIO_TRANSCRIBE_TIMEOUT_MS_PER_CHAR = 0.1
    +
    +export function audioTranscribeRequestTimeoutMs(dataUrl: string): number {
    +  const estimated = Math.max(
    +    AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS,
    +    Math.ceil(String(dataUrl || '').length * AUDIO_TRANSCRIBE_TIMEOUT_MS_PER_CHAR)
    +  )
    +
    +  return Math.min(AUDIO_TRANSCRIBE_MAX_REQUEST_TIMEOUT_MS, estimated)
    +}
     
     export type {
       ActionResponse,
    ```

    ```diff
    @@ -1346,7 +1376,11 @@ export function transcribeAudio(dataUrl: string, mimeType?: string): Promise<Aud
         body: {
           data_url: dataUrl,
           mime_type: mimeType
    -    }
    +    },
    +    // Transcription blocks until provider STT, file handling, and response
    +    // encoding finish. Remote providers and long clips regularly exceed the
    +    // default 15s Electron backend timeout.
    +    timeoutMs: audioTranscribeRequestTimeoutMs(dataUrl)
       })
     }
     
    ```

    ```diff
    @@ -1354,7 +1388,11 @@ export function speakText(text: string): Promise<AudioSpeakResponse> {
       return window.hermesDesktop.api<AudioSpeakResponse>({
         path: '/api/audio/speak',
         method: 'POST',
    -    body: { text }
    +    body: { text },
    +    // TTS blocks until provider synthesis, file read, and base64 encoding
    +    // finish. Remote providers and large messages regularly exceed the
    +    // default 15s Electron backend timeout.
    +    timeoutMs: audioSpeakRequestTimeoutMs(text)
       })
     }
     
    ```
- **#46740: fix(desktop): add timeoutMs to transcribeAudio for local STT models**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `2bb531b67a`, `#68056`
- **#51862: fix(desktop): extend audio transcription timeout**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#46530`, `#46636`, `#46530`

## Fix status

- Merged to main in PR #68056 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-20`.
- Subsystem: `apps/desktop`
