Skip to content

feat(mcp): clone_voice tool — clone a new voice from reference audio (#1194) - #1195

Merged
debpalash merged 2 commits into
debpalash:mainfrom
paoloantinori:feat/mcp-clone-voice
Jul 20, 2026
Merged

feat(mcp): clone_voice tool — clone a new voice from reference audio (#1194)#1195
debpalash merged 2 commits into
debpalash:mainfrom
paoloantinori:feat/mcp-clone-voice

Conversation

@paoloantinori

@paoloantinori paoloantinori commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Closes #1194.

What

Adds a clone_voice MCP tool so AI agents can create a voice profile from a reference audio sample — not just use existing ones. The new tool:

  • Takes name + ref_audio_base64 (base64-encoded WAV/MP3/FLAC, 5-30s clean speech) + optional ref_text/instruct/language.
  • Decodes the base64, POSTs as a multipart ref_audio file to the existing POST /profiles endpoint (kind=clone).
  • Returns {profile_id, name, kind} so the agent can immediately use the voice with generate_speech.

Consistent with the existing transcribe tool's base64-audio pattern (validated decode + 200 MB cap + application/octet-stream content-type).

Review

Self-reviewed against the transcribe tool (same base64-audio transport): caught and fixed three robustness gaps — missing base64 decode error handling, missing size cap, and wrong content-type (hardcoded audio/wav instead of application/octet-stream). The full bot review (CodeRabbit/Greptile) will run on push.

Files

  • backend/mcp_server.py — one new @mcp.tool() (~50 lines, follows the generate_speech pattern).
  • tests/test_mcp_mount.py — added clone_voice to the asserted tool surface.
  • CHANGELOG.md### Added entry.

Summary

Adds a clone_voice MCP tool that lets agents create voice profiles from base64-encoded reference audio (WAV/MP3/FLAC).

  • Exposes clone_voice(name, ref_audio_base64, ref_text="", instruct="", language="Auto").
  • Decodes and validates the base64 audio input, enforcing a 200 MB size cap; rejects empty/invalid reference audio.
  • Uploads the decoded audio to POST /profiles as a multipart ref_audio file with kind=clone and content type application/octet-stream, along with optional ref_text, instruct, and language.
  • Returns the created profile information ({profile_id, name, kind}) as a JSON string for immediate use with generate_speech.
  • Updates MCP tool registration test expectations, and documents the new tool in docs/mcp.md; also adds a CHANGELOG.md entry.
flowchart LR
  A[AI agent] --> B[clone_voice MCP tool]
  B --> C[Decode + validate ref_audio_base64 (<=200MB)]
  C --> D[Multipart upload to POST /profiles (kind=clone)]
  D --> E[Return profile_id]
  E --> F[generate_speech]
Loading

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b001bb70-b75c-4e82-bdc9-a3461d7aa21e

📥 Commits

Reviewing files that changed from the base of the PR and between 9b49a3b and 80597a0.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • backend/mcp_server.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • backend/mcp_server.py

📝 Walkthrough

Walkthrough

Adds a clone_voice MCP tool that validates base64 reference audio, creates a clone profile through POST /profiles, returns profile metadata, updates the advertised tool set, and documents the addition.

Changes

Voice cloning via MCP

Layer / File(s) Summary
Implement clone_voice profile creation
backend/mcp_server.py
The tool decodes and validates reference audio, enforces a 200 MB limit, submits multipart data to /profiles, and returns profile_id, name, and kind.
Advertise and document the tool
tests/test_mcp_mount.py, docs/mcp.md, CHANGELOG.md
The expected MCP tool set, MCP documentation, and changelog now describe clone_voice.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant clone_voice
  participant ProfilesAPI
  MCPClient->>clone_voice: Send base64 reference audio and metadata
  clone_voice->>clone_voice: Decode and validate audio
  clone_voice->>ProfilesAPI: POST /profiles as multipart form data
  ProfilesAPI-->>clone_voice: Return created profile
  clone_voice-->>MCPClient: Return profile_id, name, and kind
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is useful but does not follow the required template headings or checklist sections. Rewrite it into Summary, Changes, Type, Testing, and Checklist sections, and fill in the required checkboxes.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional-commit scope and accurately names the new clone_voice MCP tool, with issue #1194 referenced.
Linked Issues check ✅ Passed The PR implements clone_voice, base64 audio transport, POST /profiles with kind=clone, and profile_id return as required by #1194.
Out of Scope Changes check ✅ Passed Only the requested MCP tool, tests, docs, and changelog updates are present; no unrelated changes are apparent.
Cross-Platform Default Parity ✅ Passed clone_voice is pure base64→multipart HTTP with no OS branches; MCP mount is the same on macOS/Windows/Linux and only opt-out is OMNIVOICE_MCP_DISABLE.
I18n Completeness (21 Locales) ✅ Passed No frontend files changed in the diff; only backend/docs/tests were touched, so there are no new t('...') keys or hardcoded UI strings to audit.
Local-First Guarantee ✅ Passed clone_voice only posts base64 audio to local /profiles via default localhost API; touched files add no telemetry, accounts, API keys, or new cloud deps.
Backward Compatibility ✅ Passed Only backend/mcp_server.py:261-305 adds a wrapper over existing POST /profiles; no migration files or model-weight paths changed, so omnivoice_data and installed engines stay compatible.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a clone_voice MCP tool that accepts a base64-encoded audio sample, decodes it, and POSTs it as multipart form data to the existing POST /profiles endpoint, returning the new profile_id for immediate use with generate_speech.

  • Base64 validation and a 200 MB pre-decode size cap mirror the transcribe tool's guard pattern; the empty-check after decoding is a nice addition.
  • ref_text and instruct are unconditionally included in the form body even when the caller omits them (empty-string defaults), inconsistent with how every other tool in this file handles optional fields — generate_speech and transcribe both omit optional fields when falsy.
  • Test, docs, and changelog are correctly updated to reflect the new tool surface.

Confidence Score: 4/5

Safe to merge after fixing the empty-field serialisation; all other concerns are already tracked in open threads.

The new tool unconditionally sends ref_text="" and instruct="" to /profiles even when the caller never provides them, diverging from the guarded pattern every other tool in this file uses. If the backend validates these fields as non-empty when present, every clone call without optional args will produce an unhandled error. Fixing that one spot makes this straightforwardly safe.

backend/mcp_server.py — specifically the multipart form data construction at lines 296–304.

Important Files Changed

Filename Overview
backend/mcp_server.py Adds the clone_voice MCP tool (~50 lines) following the base64-audio pattern of transcribe. Empty optional fields (ref_text, instruct) are unconditionally serialised into the POST body, inconsistent with the guarded field inclusion in every other tool in this file.
tests/test_mcp_mount.py Updates the tool-surface assertion to include clone_voice; minimal and correct change.
CHANGELOG.md Adds a ### Added changelog entry for clone_voice with correct attribution and issue reference.
docs/mcp.md Updates the MCP tool table and intro sentence to include clone_voice; no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent as AI Agent
    participant MCP as clone_voice (MCP tool)
    participant BE as POST /profiles

    Agent->>MCP: name, ref_audio_base64, [ref_text, instruct, language]
    MCP->>MCP: "len(base64) > 200 MB? → error"
    MCP->>MCP: "base64.b64decode(validate=True) → raw"
    MCP->>MCP: raw empty? → error
    MCP->>BE: multipart (ref_audio + form data)
    BE-->>MCP: "{id, name, kind}"
    MCP-->>Agent: "JSON {profile_id, name, kind}"
    Agent->>Agent: "generate_speech(profile_id=...)"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Agent as AI Agent
    participant MCP as clone_voice (MCP tool)
    participant BE as POST /profiles

    Agent->>MCP: name, ref_audio_base64, [ref_text, instruct, language]
    MCP->>MCP: "len(base64) > 200 MB? → error"
    MCP->>MCP: "base64.b64decode(validate=True) → raw"
    MCP->>MCP: raw empty? → error
    MCP->>BE: multipart (ref_audio + form data)
    BE-->>MCP: "{id, name, kind}"
    MCP-->>Agent: "JSON {profile_id, name, kind}"
    Agent->>Agent: "generate_speech(profile_id=...)"
Loading

Reviews (3): Last reviewed commit: "docs(mcp): refresh module docstring tool..." | Re-trigger Greptile

Comment thread backend/mcp_server.py Outdated
Comment on lines +289 to +306
r = await _api_post_form(
"/profiles",
data={
"name": name,
"kind": "clone",
"ref_text": ref_text,
"instruct": instruct,
"language": language,
},
files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},
)
profile = r.json()
import json as _json
return _json.dumps({
"profile_id": profile.get("id", "unknown"),
"name": profile.get("name", name),
"kind": profile.get("kind", "clone"),
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 HTTP errors and JSON parse failures propagate unhandled

_api_post_form calls r.raise_for_status() (line 89), so any 4xx/5xx from /profiles — duplicate name, audio too short, backend validation failure — throws httpx.HTTPStatusError that is completely uncaught here. Voice cloning commonly fails at the quality-gate stage, so this is a very live failure mode. The calling agent gets a generic framework exception string instead of the structured {"error": "..."} JSON it expects. r.json() on line 300 is also unguarded; a non-JSON body (e.g. a proxy error page) throws JSONDecodeError.

Fix in Claude Code

Comment thread backend/mcp_server.py
"instruct": instruct,
"language": language,
},
files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The multipart filename is hardcoded as ref_audio.wav even when the caller sends MP3 or FLAC. Some backends (and the underlying libsndfile/ffmpeg dispatch) use the filename extension as a format hint before falling back to magic-byte detection. If the backend relies on the extension for its initial format sniff, a FLAC sent as ref_audio.wav can produce a silent decode error. Using a neutral name like ref_audio.bin (paired with application/octet-stream) makes the intent explicit.

Suggested change
files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},
files={"ref_audio": ("ref_audio.bin", raw, "application/octet-stream")},

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/mcp_server.py`:
- Around line 282-285: Update the ref_audio_base64 decoding logic in the
surrounding handler to remove an optional data-URI prefix before decoding, then
catch only the specific base64 decoding exception raised by base64.b64decode
instead of broad Exception. Preserve the existing invalid-input JSON error
response for decoding failures.
- Line 298: Update the multipart file construction in the relevant backend
request flow to detect the input audio format from raw magic bytes and choose
the matching filename extension instead of always using ref_audio.wav. Preserve
the existing upload content and field name, and support the MP3, FLAC, and WAV
formats documented by the surrounding API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c530bd26-e83a-49f8-8c16-5833f1d27ce9

📥 Commits

Reviewing files that changed from the base of the PR and between c191c6e and 4f40ad9.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • backend/mcp_server.py
  • tests/test_mcp_mount.py

Comment thread backend/mcp_server.py
Comment on lines +282 to +285
try:
raw = base64.b64decode(ref_audio_base64, validate=True)
except Exception:
return '{"error":"ref_audio_base64 is not valid base64"}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Narrow the blind exception and strip data URIs.

Catching the blind Exception from base64 decoding masks other errors and is flagged by static analysis. Additionally, LLM agents frequently prepend data URIs (e.g., data:audio/wav;base64,...) when interacting with file-upload tools; stripping this prefix before decoding prevents a round-trip failure.

🛠️ Proposed fix
+        import binascii
+        
+        if ref_audio_base64.startswith("data:"):
+            ref_audio_base64 = ref_audio_base64.split(",", 1)[-1]
+
         try:
             raw = base64.b64decode(ref_audio_base64, validate=True)
-        except Exception:
+        except (binascii.Error, ValueError):
             return '{"error":"ref_audio_base64 is not valid base64"}'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
raw = base64.b64decode(ref_audio_base64, validate=True)
except Exception:
return '{"error":"ref_audio_base64 is not valid base64"}'
import binascii
if ref_audio_base64.startswith("data:"):
ref_audio_base64 = ref_audio_base64.split(",", 1)[-1]
try:
raw = base64.b64decode(ref_audio_base64, validate=True)
except (binascii.Error, ValueError):
return '{"error":"ref_audio_base64 is not valid base64"}'
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 284-284: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/mcp_server.py` around lines 282 - 285, Update the ref_audio_base64
decoding logic in the surrounding handler to remove an optional data-URI prefix
before decoding, then catch only the specific base64 decoding exception raised
by base64.b64decode instead of broad Exception. Preserve the existing
invalid-input JSON error response for decoding failures.

Source: Linters/SAST tools

Comment thread backend/mcp_server.py
"instruct": instruct,
"language": language,
},
files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Sniff magic bytes to ensure correct file extensions for non-WAV inputs.

Mismatched container extensions (e.g., saving an MP3 bitstream as .wav) can break strict HTML5 <audio> playback on Safari when the UI fetches the profile reference, or cause failures in downstream ffmpeg pipelines that rely on extension hints. Since the docstring explicitly invites agents to send MP3 or FLAC, sniff the magic bytes to provide the correct file extension to the backend form.

🎵 Proposed fix
+        ext = ".wav"
+        if raw.startswith(b"fLaC"):
+            ext = ".flac"
+        elif raw.startswith(b"ID3") or raw.startswith(b"\xff\xfb") or raw.startswith(b"\xff\xf3"):
+            ext = ".mp3"
+        elif raw.startswith(b"OggS"):
+            ext = ".ogg"
+        elif raw[4:8] == b"ftyp":
+            ext = ".m4a"
+
-            files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},
+            files={"ref_audio": (f"ref_audio{ext}", raw, "application/octet-stream")},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
files={"ref_audio": ("ref_audio.wav", raw, "application/octet-stream")},
ext = ".wav"
if raw.startswith(b"fLaC"):
ext = ".flac"
elif raw.startswith(b"ID3") or raw.startswith(b"\xff\xfb") or raw.startswith(b"\xff\xf3"):
ext = ".mp3"
elif raw.startswith(b"OggS"):
ext = ".ogg"
elif raw[4:8] == b"ftyp":
ext = ".m4a"
files={"ref_audio": (f"ref_audio{ext}", raw, "application/octet-stream")},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/mcp_server.py` at line 298, Update the multipart file construction in
the relevant backend request flow to detect the input audio format from raw
magic bytes and choose the matching filename extension instead of always using
ref_audio.wav. Preserve the existing upload content and field name, and support
the MP3, FLAC, and WAV formats documented by the surrounding API.

…ebpalash#1194)

AI agents driving OmniVoice via MCP could use and list voices but couldn't
create one. Add a clone_voice MCP tool that takes a base64-encoded reference
audio sample (consistent with transcribe's audio_base64 pattern), decodes it,
and POSTs it as a multipart ref_audio to POST /profiles (kind=clone). Returns
the new profile_id so the agent can immediately use it with generate_speech.

Update test_mcp_mount.py to include clone_voice in the asserted tool surface.
CHANGELOG entry.
@paoloantinori
paoloantinori force-pushed the feat/mcp-clone-voice branch from 4f40ad9 to 9b49a3b Compare July 19, 2026 20:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
docs/mcp.md (1)

14-14: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Document the consent boundary for voice cloning.

The row makes cloning immediately actionable but does not tell users to obtain the speaker’s explicit permission. Add a concise warning that clone_voice must only be used with consent-verified reference audio.

Based on learnings, prefer a consent-verified voice profile for any agent that speaks as a person.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mcp.md` at line 14, Update the clone_voice entry in the MCP
documentation to add a concise warning that cloning requires the speaker’s
explicit consent and consent-verified reference audio. Also state the preference
for consent-verified voice profiles when an agent speaks as a person.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/mcp.md`:
- Line 14: Update the clone_voice entry in the MCP documentation to add a
concise warning that cloning requires the speaker’s explicit consent and
consent-verified reference audio. Also state the preference for consent-verified
voice profiles when an agent speaks as a person.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 19a5056f-c838-4d53-8e77-d3b3b5d01bf9

📥 Commits

Reviewing files that changed from the base of the PR and between 4f40ad9 and 9b49a3b.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • backend/mcp_server.py
  • docs/mcp.md
  • tests/test_mcp_mount.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_mcp_mount.py
  • CHANGELOG.md

…changelog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@debpalash
debpalash merged commit 983ef70 into debpalash:main Jul 20, 2026
16 checks passed
pull Bot pushed a commit to Malumbo21/OmniVoice-Studio that referenced this pull request Jul 20, 2026
… + reviewer configs

Harvested and verified every CodeRabbit/Greptile finding from PRs debpalash#1175,
debpalash#1189, debpalash#1192, debpalash#1195: 16 real ones fixed (fallback ASR preflight bypass,
VRAM release on stream exit, typed 409 parity, uv env independence,
path-privacy in errors, MCP clone_voice hardening, CaptureWidget WS
guard, test hygiene), 4 refuted with evidence, rest documented as
deliberate design or deferred.

Deterministic CI replaces hand-enforcement: tests/test_changelog_style.py
(quiet one-liner format) and tests/test_locale_parity.py (21-locale
key/placeholder lockstep with a ratchet baseline) — the latter surfaced
and fixes 151 already-broken locale strings. CodeRabbit/Greptile carry
the house rules via .coderabbit.yaml + greptile.json; CLAUDE.md gains
the harvest-before-merge and never-accept-as-is rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

[Feature] Clone a new voice via MCP — expose POST /profiles as a clone_voice tool

2 participants