Skip to content

feat: add Xiaomi MiMo as a first-class TTS provider#129

Merged
daggerhashimoto merged 6 commits intomasterfrom
feat/xiaomi-mimo-tts-provider
Mar 21, 2026
Merged

feat: add Xiaomi MiMo as a first-class TTS provider#129
daggerhashimoto merged 6 commits intomasterfrom
feat/xiaomi-mimo-tts-provider

Conversation

@daggerhashimoto
Copy link
Owner

@daggerhashimoto daggerhashimoto commented Mar 20, 2026

Summary

Add Xiaomi MiMo as a manual TTS provider in Nerve.

What changed

  • added Xiaomi API key support via MIMO_API_KEY and /api/keys
  • added a Xiaomi MiMo TTS service
  • routed explicit /api/tts requests to provider: 'xiaomi'
  • added Xiaomi model, voice, and style controls in Audio settings
  • extended shared TTS provider plumbing and related tests

Verification

  • npx vitest run server/routes/api-keys.test.ts server/services/xiaomi-tts.test.ts server/routes/tts.test.ts server/lib/tts-config.test.ts src/features/tts/useTTS.test.ts src/features/settings/AudioSettings.component.test.tsx
  • npm run lint -- --quiet
  • npm run build
  • smoke tested real Xiaomi synthesis against the live API

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This pull request adds Xiaomi MiMo TTS as a new text-to-speech provider. The implementation spans backend API key management, a new synthesis service that interfaces with Xiaomi's API, route handlers for configuration endpoints, and frontend UI components for provider selection and voice/style configuration.

Changes

Cohort / File(s) Summary
Configuration & Environment
server/lib/config.ts
Added mimoApiKey field sourced from MIMO_API_KEY environment variable.
TTS Voice Configuration
server/lib/tts-config.ts, server/lib/tts-config.test.ts
Extended TTSVoiceConfig interface with a new xiaomi section containing model, voice, and style fields; added defaults (model: 'mimo-v2-tts', voice: 'mimo_default', style: '') and corresponding test coverage.
Xiaomi TTS Service
server/services/xiaomi-tts.ts, server/services/xiaomi-tts.test.ts
Implemented new synthesizeXiaomi function that validates API key presence, constructs OpenAI-compatible payloads with optional XML-style wrapping, makes authenticated POST requests to Xiaomi's endpoint, and returns base64-decoded audio buffers or error objects. Includes comprehensive test suite covering missing API key, successful synthesis, and malformed payload scenarios.
API Key Management Routes
server/routes/api-keys.ts, server/routes/api-keys.test.ts
Extended GET /api/keys and PUT /api/keys endpoints to expose xiaomiKeySet status and accept mimoApiKey in request body; persists to .env file and updates runtime config. Added test suite validating endpoint behavior.
TTS Provider Route Handler
server/routes/tts.ts, server/routes/tts.test.ts
Added xiaomi as a supported provider in request schema; integrated synthesizeXiaomi call path with Xiaomi-specific cache hashing (xiaomiStyle); extended config schema to accept xiaomi section with model, voice, and style keys. Includes tests for provider selection, content-type validation, and config patching.
Frontend TTS Provider Types
src/features/tts/useTTS.ts, src/features/tts/useTTS.test.ts
Extended TTSProvider union type to include 'xiaomi'; updated migrateTTSProvider to recognize xiaomi as valid provider.
Frontend TTS Configuration Hook
src/features/tts/useTTSConfig.ts
Extended TTSVoiceConfig interface with xiaomi configuration object; added style field to debounced text-field set for 500ms debounce behavior.
Frontend Settings UI
src/features/settings/AudioSettings.tsx, src/features/settings/AudioSettings.component.test.tsx
Added Xiaomi provider button, conditional API key prompt, and provider-specific UI controls (model selector with fallback to config default, voice dropdown with three options, expandable style input). Updated fetch endpoint from /api/transcribe/config to /api/keys for key availability. Includes comprehensive test suite covering provider presence, key prompt rendering, field interactions, and config updates.
Command Palette
src/features/command-palette/commands.ts
Added new tts-xiaomi command under voice category that invokes onTtsProviderChange('xiaomi').
Settings Context
src/contexts/SettingsContext.tsx
Updated TTS provider rotation order from ['openai', 'replicate', 'edge'] to ['openai', 'replicate', 'xiaomi', 'edge'].

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend<br/>(AudioSettings)
    participant TTS_API as TTS Route<br/>Handler
    participant Xiaomi_Svc as Xiaomi Service<br/>(synthesizeXiaomi)
    participant Xiaomi_API as Xiaomi API<br/>api.xiaomimimo.com

    User->>Frontend: Select Xiaomi provider
    Frontend->>Frontend: Update local state
    alt No API Key Set
        Frontend->>Frontend: Show API key prompt
        User->>Frontend: Enter MIMO_API_KEY
        Frontend->>TTS_API: PUT /api/keys {mimoApiKey}
        TTS_API->>TTS_API: Persist to .env & config
    end
    
    User->>Frontend: Send text for synthesis
    Frontend->>TTS_API: POST /api/tts {provider: 'xiaomi', text}
    
    TTS_API->>Xiaomi_Svc: synthesizeXiaomi(text, opts)
    Xiaomi_Svc->>Xiaomi_Svc: Load config.mimoApiKey<br/>Derive model/voice<br/>Wrap style as XML
    Xiaomi_Svc->>Xiaomi_API: POST /v1/chat/completions<br/>with audio.format='wav'
    
    alt Success Path
        Xiaomi_API-->>Xiaomi_Svc: {choices[0].message.audio.data: base64}
        Xiaomi_Svc->>Xiaomi_Svc: Decode base64 to Buffer
        Xiaomi_Svc-->>TTS_API: {ok: true, buf, contentType}
    else Error Path
        Xiaomi_API-->>Xiaomi_Svc: Error response
        Xiaomi_Svc-->>TTS_API: {ok: false, status, message}
    end
    
    TTS_API-->>Frontend: Audio buffer or error
    Frontend-->>User: Play audio or display error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes span multiple architectural layers (backend service, route handlers, API integration, frontend UI and hooks) with heterogeneous logic including external API communication, error handling, config validation, and state management. Key files like xiaomi-tts.ts, AudioSettings.tsx, and tts.ts contain non-trivial logic requiring careful review of API contract, cache behavior, and UI state synchronization.

Possibly related PRs

  • PR #21: Both PRs modify server/lib/config.ts to add environment-backed configuration properties to the exported config object.

Poem

🐰 A whispered wish for voice so sweet,
MiMo's style makes speech complete!
With Xiaomi's might and API key,
New audio paths for all to see—
Five-line hops through Shanghai streams! 🎵

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers the main changes but lacks specific details required by the template sections. Expand the description to include explicit 'What', 'Why', and 'How' sections as specified in the template, select appropriate Type of Change checkboxes, and provide verification details in the Checklist section.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding Xiaomi MiMo as a TTS provider. It accurately summarizes the main changeset across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/xiaomi-mimo-tts-provider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/contexts/SettingsContext.tsx (1)

209-215: ⚠️ Potential issue | 🟠 Major

Reset ttsModel when cycling providers.

toggleTtsProvider updates only provider, while Line 140 changeTtsProvider also clears provider-specific model. This can leave stale models (e.g., OpenAI model while on Xiaomi) and cause synthesis failures.

💡 Suggested fix
 const toggleTtsProvider = useCallback(() => {
-  setTtsProvider(prev => {
-    const order: TTSProvider[] = ['openai', 'replicate', 'xiaomi', 'edge'];
-    const next = order[(order.indexOf(prev) + 1) % order.length]!;
-    localStorage.setItem('oc-tts-provider', next);
-    return next;
-  });
-}, []);
+  const order: TTSProvider[] = ['openai', 'replicate', 'xiaomi', 'edge'];
+  const next = order[(order.indexOf(ttsProvider) + 1) % order.length]!;
+  changeTtsProvider(next);
+}, [ttsProvider, changeTtsProvider]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/SettingsContext.tsx` around lines 209 - 215, toggleTtsProvider
currently only advances the provider and stores 'oc-tts-provider', but leaves
the provider-specific model in state and localStorage causing stale-model synth
failures; update toggleTtsProvider (the useCallback) to also clear ttsModel
state and remove the stored model (same key used by changeTtsProvider, e.g.,
'oc-tts-model')—mirror what changeTtsProvider does by calling the state setter
that clears ttsModel and removing the model from localStorage whenever you set
the new provider.
🧹 Nitpick comments (3)
server/routes/api-keys.test.ts (1)

41-67: Consider adding a test for the unconfigured key state.

The tests verify the happy path (key configured), but don't verify that xiaomiKeySet: false is returned when mimoKey is empty. This would mirror the existing behavior for other providers.

💡 Suggested additional test case
+  it('reports xiaomiKeySet as false when key is not configured', async () => {
+    mockDeps({ mimoKey: '' });
+    const app = await buildApp();
+
+    const res = await app.request('/api/keys');
+    expect(res.status).toBe(200);
+
+    const json = await res.json() as Record<string, unknown>;
+    expect(json.xiaomiKeySet).toBe(false);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/api-keys.test.ts` around lines 41 - 67, Add a test that
verifies xiaomiKeySet is false when no MIMO key is configured: use mockDeps()
with no mimoKey, call buildApp() and GET '/api/keys', assert status 200 and that
the returned JSON has xiaomiKeySet === false; place it alongside the existing
tests (near 'reports xiaomiKeySet from config' and 'writes MIMO_API_KEY from
mimoApiKey input') and reuse mockDeps and buildApp to keep test setup
consistent.
server/services/xiaomi-tts.test.ts (1)

81-100: Consider adding a test for upstream HTTP errors.

The service has error handling for when resp.ok is false (lines 61-65 in xiaomi-tts.ts), but this path isn't covered by the tests.

💡 Suggested test case for HTTP errors
+  it('returns upstream error when Xiaomi API returns non-OK status', async () => {
+    vi.doMock('../lib/config.js', () => ({
+      config: { mimoApiKey: 'sk-mimo' },
+    }));
+
+    vi.doMock('../lib/tts-config.js', () => ({
+      getTTSConfig: () => ({ xiaomi: { model: 'mimo-v2-tts', voice: 'mimo_default', style: '' } }),
+    }));
+
+    vi.mocked(fetch).mockResolvedValue(
+      new Response('Rate limit exceeded', { status: 429 }),
+    );
+
+    const { synthesizeXiaomi } = await import('./xiaomi-tts.js');
+    await expect(synthesizeXiaomi('Hello')).resolves.toMatchObject({
+      ok: false,
+      status: 429,
+      message: 'Rate limit exceeded',
+    });
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/xiaomi-tts.test.ts` around lines 81 - 100, Add a new unit
test in xiaomi-tts.test.ts that covers the branch where the upstream HTTP
response is not ok: mock fetch to return a Response with ok=false (e.g., status
500 and an error body), import synthesizeXiaomi from ./xiaomi-tts.js and assert
the resolved result has ok: false and the expected status/message handling for
non-ok responses (matching the code path that checks resp.ok in
synthesizeXiaomi).
server/services/xiaomi-tts.ts (1)

40-59: Add a timeout to the fetch request to prevent hanging on slow API responses.

The fetch call (lines 40-59) has no timeout, which could cause it to hang indefinitely if the Xiaomi API becomes unresponsive. This risks tying up resources in high-concurrency scenarios.

⏱️ Add AbortSignal timeout
  const resp = await fetch(XIAOMI_TTS_URL, {
+   signal: AbortSignal.timeout(30_000), // 30 second timeout
    method: 'POST',
    headers: {
      Authorization: `Bearer ${config.mimoApiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: effectiveModel,
      messages: [
        {
          role: 'assistant',
          content,
        },
      ],
      audio: {
        format: 'wav',
        voice: effectiveVoice,
      },
    }),
  });

Note: AbortError will be thrown on timeout and must be handled—either catch it explicitly in this function or rely on the caller's error handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/xiaomi-tts.ts` around lines 40 - 59, The fetch to
XIAOMI_TTS_URL that assigns to resp lacks a timeout; wrap the fetch with an
AbortController (create controller, pass controller.signal to fetch) and set a
setTimeout to call controller.abort() after a sensible timeout (e.g.,
configurable ms), then clearTimeout on success; ensure the fetch rejection for
AbortError is either caught here or allowed to propagate so callers can handle
it; update the code around the resp fetch invocation to use controller.signal
and clear the timeout when the response is received.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/contexts/SettingsContext.tsx`:
- Around line 209-215: toggleTtsProvider currently only advances the provider
and stores 'oc-tts-provider', but leaves the provider-specific model in state
and localStorage causing stale-model synth failures; update toggleTtsProvider
(the useCallback) to also clear ttsModel state and remove the stored model (same
key used by changeTtsProvider, e.g., 'oc-tts-model')—mirror what
changeTtsProvider does by calling the state setter that clears ttsModel and
removing the model from localStorage whenever you set the new provider.

---

Nitpick comments:
In `@server/routes/api-keys.test.ts`:
- Around line 41-67: Add a test that verifies xiaomiKeySet is false when no MIMO
key is configured: use mockDeps() with no mimoKey, call buildApp() and GET
'/api/keys', assert status 200 and that the returned JSON has xiaomiKeySet ===
false; place it alongside the existing tests (near 'reports xiaomiKeySet from
config' and 'writes MIMO_API_KEY from mimoApiKey input') and reuse mockDeps and
buildApp to keep test setup consistent.

In `@server/services/xiaomi-tts.test.ts`:
- Around line 81-100: Add a new unit test in xiaomi-tts.test.ts that covers the
branch where the upstream HTTP response is not ok: mock fetch to return a
Response with ok=false (e.g., status 500 and an error body), import
synthesizeXiaomi from ./xiaomi-tts.js and assert the resolved result has ok:
false and the expected status/message handling for non-ok responses (matching
the code path that checks resp.ok in synthesizeXiaomi).

In `@server/services/xiaomi-tts.ts`:
- Around line 40-59: The fetch to XIAOMI_TTS_URL that assigns to resp lacks a
timeout; wrap the fetch with an AbortController (create controller, pass
controller.signal to fetch) and set a setTimeout to call controller.abort()
after a sensible timeout (e.g., configurable ms), then clearTimeout on success;
ensure the fetch rejection for AbortError is either caught here or allowed to
propagate so callers can handle it; update the code around the resp fetch
invocation to use controller.signal and clear the timeout when the response is
received.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a80dfb5-d909-43f5-83b4-9031f0fe9af4

📥 Commits

Reviewing files that changed from the base of the PR and between 705ef59 and 5a851e2.

📒 Files selected for processing (16)
  • server/lib/config.ts
  • server/lib/tts-config.test.ts
  • server/lib/tts-config.ts
  • server/routes/api-keys.test.ts
  • server/routes/api-keys.ts
  • server/routes/tts.test.ts
  • server/routes/tts.ts
  • server/services/xiaomi-tts.test.ts
  • server/services/xiaomi-tts.ts
  • src/contexts/SettingsContext.tsx
  • src/features/command-palette/commands.ts
  • src/features/settings/AudioSettings.component.test.tsx
  • src/features/settings/AudioSettings.tsx
  • src/features/tts/useTTS.test.ts
  • src/features/tts/useTTS.ts
  • src/features/tts/useTTSConfig.ts

@daggerhashimoto daggerhashimoto merged commit 6b545af into master Mar 21, 2026
3 checks passed
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.

1 participant