diff --git a/src/catalog/catalog.test.ts b/src/catalog/catalog.test.ts index 6074438..d728b15 100644 --- a/src/catalog/catalog.test.ts +++ b/src/catalog/catalog.test.ts @@ -86,18 +86,19 @@ const MARKS_DIR = resolve(import.meta.dir, '../../public/marks'); describe('registry: ids, slugs, refs', () => { it('pins the collection counts (bump intentionally when adding entries)', () => { - // 12 providers / 24 models: the 11 vendors + 23 TTS models, plus the Human + // 12 providers / 25 models: the 11 vendors + 24 TTS models, plus the Human // baseline (provider `human`, model `human`). Two ElevenLabs entries are - // retired (2026-07-23), so the arena carries 22 of the 24. + // retired (2026-07-23) and Sonic 3.6 is unlisted pending its clips, so the + // arena carries 22 of the 25. expect(BASE_PROVIDER_ENTRIES.length).toBe(12); - expect(BASE_MODEL_ENTRIES.length).toBe(24); + expect(BASE_MODEL_ENTRIES.length).toBe(25); expect(arenaProviderEntries().length).toBe(12); expect(arenaModelEntries().length).toBe(22); - // The committed registry carries no unlisted entries today; embargoed - // ones live in the private overlay until their providers announce them. - expect(BASE_MODEL_ENTRIES.filter((m) => m.status === 'unlisted')).toEqual( - [], - ); + // Sonic 3.6 is committed but unlisted until its clips are generated; + // flipping status to 'active' is the one-line re-list. + expect( + BASE_MODEL_ENTRIES.filter((m) => m.status === 'unlisted').map((m) => m.id), + ).toEqual(['cartesia-sonic-36']); }); it('constrains the overlay to unlisted models with resolvable providers', () => { @@ -167,6 +168,7 @@ describe('registry: ids, slugs, refs', () => { 'neuphonic-neu-hq', 'speechify-simba-3-2', 'fish-s21-pro', + 'cartesia-sonic-36', ]); const seedIds = SEED_STANDINGS.models.map((row) => row.id); expect(new Set(seedIds).size).toBe(seedIds.length); @@ -363,7 +365,7 @@ const EXPECTED_MODELS = [ }, { id: 'cartesia-sonic-35', - arenaId: 'cartesia:sonic-3.5', + arenaId: 'cartesia:sonic-3.5-2026-05-04', providerId: 'cartesia', name: 'Sonic 3.5', }, @@ -545,9 +547,9 @@ describe('server/catalog derivation equality', () => { '2f77e73d0fe4bcfe4769ed7f4f44329c', ], [ - 'variant:voice-nelliot:cartesia:sonic-3.5', + 'variant:voice-nelliot:cartesia:sonic-3.5-2026-05-04', 'clip-20', - '7d3e067846de9940a7f4e3e0bde1cf68', + '76f803cefea4c90953672d0c763d8fc2', ], ]; for (const [variantId, promptId, hash] of goldens) { diff --git a/src/catalog/models.ts b/src/catalog/models.ts index 62d3502..4503fe5 100644 --- a/src/catalog/models.ts +++ b/src/catalog/models.ts @@ -531,7 +531,7 @@ export const MODEL_ENTRIES: ModelEntry[] = [ { question: "Is Sonic still Cartesia's current model?", answer: - 'No. Three newer generations have superseded it, with Sonic 3.5 as the current flagship. Sonic remains in the arena as the baseline of the family.', + 'No. Four newer generations have superseded it, with Sonic 3.6 as the current flagship. Sonic remains in the arena as the baseline of the family.', }, ], }, @@ -665,8 +665,8 @@ export const MODEL_ENTRIES: ModelEntry[] = [ slug: 'cartesia-sonic-3-5', providerId: 'cartesia', name: 'Sonic 3.5', - apiModelId: 'sonic-3.5', - arenaApiId: 'sonic-3.5', + apiModelId: 'sonic-3.5-2026-05-04', + arenaApiId: 'sonic-3.5-2026-05-04', status: 'active', releaseDate: { value: '2026-05-04', @@ -684,16 +684,16 @@ export const MODEL_ENTRIES: ModelEntry[] = [ fallbackClip: clip( 'voice-emma', 'cartesia', - 'sonic-3.5', + 'sonic-3.5-2026-05-04', 'clip-20', - '5e21eefdad513597d684cb9ec3b0815d', + '3e36a1883267dd73b9e11f42b78ede03', ), }, copy: [ { heading: 'Background', paragraphs: [ - "Sonic 3.5 is Cartesia's current flagship, released in May 2026. Cartesia positions it as its most natural and fastest model, with sub 90 ms latency and native support for 42 languages. It is tuned for production agent transcripts: it reads order numbers, emails, and confirmation codes correctly without preprocessing, and it resolves heteronyms like read and bow from the surrounding words.", + "Sonic 3.5 is the May 2026 Sonic snapshot, and was Cartesia's flagship until Sonic 3.6 shipped in August 2026. Cartesia positioned it as its most natural and fastest model at release, with sub 90 ms latency and native support for 42 languages. It is tuned for production agent transcripts: it reads order numbers, emails, and confirmation codes correctly without preprocessing, and it resolves heteronyms like read and bow from the surrounding words.", ], sourceUrls: [ 'https://docs.cartesia.ai/build-with-cartesia/tts-models/latest', @@ -719,6 +719,62 @@ export const MODEL_ENTRIES: ModelEntry[] = [ ], }, + { + id: 'cartesia-sonic-36', + slug: 'cartesia-sonic-3-6', + providerId: 'cartesia', + name: 'Sonic 3.6', + apiModelId: 'sonic-3.6-2026-08-27', + arenaApiId: 'sonic-3.6-2026-08-27', + status: 'unlisted', + releaseDate: { + value: '2026-08-27', + sourceUrl: + 'https://docs.cartesia.ai/build-with-cartesia/tts-models/latest', + asOf: '2026-08-26', + note: 'Snapshot release.', + }, + stats: { + // Measured only: needs a bench run against sonic-3.6-2026-08-27. + latencyMs: null, + languages: { + value: 44, + sourceUrl: + 'https://docs.cartesia.ai/build-with-cartesia/tts-models/preview', + asOf: '2026-08-26', + }, + }, + voiceProfile: 27, + copy: [ + { + heading: 'Background', + paragraphs: [ + "Sonic 3.6 is Cartesia's current flagship model. Cartesia positions it as another step change in naturalness with sub 90 ms latency and native support for 44 languages. The model adapts intonation, pacing, and emotiveness to the conversation like a human would.", + ], + sourceUrls: [ + 'https://docs.cartesia.ai/build-with-cartesia/tts-models/latest', + ], + }, + { + heading: 'At a glance', + paragraphs: [ + 'Alphanumerics and heteronyms without preprocessing, 44 languages, and a published sub 90 ms latency claim. It has not been through our 50 trial streaming benchmark yet.', + ], + sourceUrls: [ + 'https://docs.cartesia.ai/build-with-cartesia/tts-models/latest', + ], + }, + ], + faq: [ + HOW_TESTED('Sonic 3.6'), + { + question: 'How fast is Sonic 3.6?', + answer: + 'Cartesia publishes sub 90 ms latency. We have not yet measured it in our 50 trial streaming benchmark.', + }, + ], + }, + /* --------------------------------- xAI ---------------------------------- */ { id: 'xai-xai-tts', diff --git a/src/pipeline/transports/cartesia.ts b/src/pipeline/transports/cartesia.ts index a061a04..f402e61 100644 --- a/src/pipeline/transports/cartesia.ts +++ b/src/pipeline/transports/cartesia.ts @@ -13,12 +13,21 @@ import { BENCH_TEXT, postFormForJson, postJsonForBytes, wsTtfbTrial } from './ht import type { ProviderTransport, TtfbPlan } from './types'; const API = 'https://api.cartesia.ai'; -const VERSION = '2024-11-13'; -const WS_VERSION = '2025-04-16'; -/** The arena Clara clone — the voice the original 50-trial bench used. */ + +/** Cartesia's current published API version, sent on every call. */ +const VERSION = '2026-08-14'; + +/** The arena Clara clone, the voice the original 50-trial bench used. */ const BENCH_VOICE = 'a5d537b0-4a5f-464d-ac12-d143fe1a0a36'; -const apiKey = (): string => requireEnv('CARTESIA_API_KEY'); +/** The four licensed source voices are English (see pipeline/voices.ts). */ +const VOICE_LANGUAGE = 'en'; + +/** Bearer is the documented scheme; x-api-key is the pre-2026 form. */ +const headers = (): Record => ({ + Authorization: `Bearer ${requireEnv('CARTESIA_API_KEY')}`, + 'Cartesia-Version': VERSION, +}); export const cartesia: ProviderTransport = { providerId: 'cartesia', @@ -27,15 +36,13 @@ export const cartesia: ProviderTransport = { synthesize: async ({ vendorModelId, providerVoiceId, text }) => ({ bytes: await postJsonForBytes( `${API}/tts/bytes`, - { - 'x-api-key': apiKey(), - 'cartesia-version': VERSION, - accept: 'audio/mpeg', - }, + { ...headers(), accept: 'audio/mpeg' }, { model_id: vendorModelId, transcript: text, voice: { mode: 'id', id: providerVoiceId }, + // Cartesia recommends setting the language explicitly where possible. + language: VOICE_LANGUAGE, output_format: { container: 'mp3', sample_rate: 44100, bit_rate: 128000 }, }, 'cartesia', @@ -51,11 +58,11 @@ export const cartesia: ProviderTransport = { basename(sampleFiles[0]), ); form.append('name', displayName); - form.append('language', 'en'); + form.append('language', VOICE_LANGUAGE); form.append('mode', 'similarity'); const result = await postFormForJson<{ id: string }>( `${API}/voices/clone`, - { 'x-api-key': apiKey(), 'cartesia-version': VERSION }, + headers(), form, 'cartesia voices/clone', ); @@ -67,10 +74,7 @@ export const cartesia: ProviderTransport = { trial: () => wsTtfbTrial({ url: 'wss://api.cartesia.ai/tts/websocket', - headers: { - Authorization: `Bearer ${apiKey()}`, - 'Cartesia-Version': WS_VERSION, - }, + headers: headers(), framesFor: () => [ JSON.stringify({ model_id: vendorModelId, @@ -83,14 +87,12 @@ export const cartesia: ProviderTransport = { }, context_id: `bench-${Date.now()}`, continue: false, - language: 'en', + language: VOICE_LANGUAGE, }), ], jsonHasAudio: (payload) => Boolean(payload.data), jsonError: (payload) => - payload.type === 'error' || payload.error - ? JSON.stringify(payload) - : null, + payload.type === 'error' || payload.error ? JSON.stringify(payload) : null, }), }), };