kugelaudio 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Tests for the dictionaries resource.
3
+ *
4
+ * Stubs the global `fetch` to verify the SDK sends the right method,
5
+ * URL, params, and JSON body for each CRUD path, and that snake_case
6
+ * server responses get mapped to camelCase SDK types.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { KugelAudio } from './client';
11
+
12
+ function jsonResponse(body: unknown, status = 200): Response {
13
+ return new Response(JSON.stringify(body), {
14
+ status,
15
+ headers: { 'Content-Type': 'application/json' },
16
+ });
17
+ }
18
+
19
+ function dictRow(overrides: Record<string, unknown> = {}) {
20
+ return {
21
+ id: 1,
22
+ project_id: 42,
23
+ name: 'Brand names',
24
+ description: null,
25
+ language: null,
26
+ is_active: true,
27
+ created_at: '2026-01-01T00:00:00+00:00',
28
+ updated_at: '2026-01-01T00:00:00+00:00',
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ function entryRow(overrides: Record<string, unknown> = {}) {
34
+ return {
35
+ id: 11,
36
+ dictionary_id: 1,
37
+ word: 'Kubernetes',
38
+ replacement: 'koo-ber-net-eez',
39
+ ipa: null,
40
+ case_sensitive: false,
41
+ created_at: '2026-01-01T00:00:00+00:00',
42
+ updated_at: '2026-01-01T00:00:00+00:00',
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ let fetchMock: ReturnType<typeof vi.fn>;
48
+
49
+ beforeEach(() => {
50
+ fetchMock = vi.fn();
51
+ vi.stubGlobal('fetch', fetchMock);
52
+ });
53
+
54
+ afterEach(() => {
55
+ vi.unstubAllGlobals();
56
+ });
57
+
58
+ function makeClient() {
59
+ return new KugelAudio({ apiKey: 'test-key', apiUrl: 'https://api.example.com' });
60
+ }
61
+
62
+ describe('Dictionaries CRUD', () => {
63
+ it('lists dictionaries', async () => {
64
+ fetchMock.mockResolvedValueOnce(
65
+ jsonResponse({ dictionaries: [dictRow(), dictRow({ id: 2, name: 'Other' })] }),
66
+ );
67
+ const client = makeClient();
68
+ const result = await client.dictionaries.list();
69
+ expect(result).toHaveLength(2);
70
+ expect(result[0]).toMatchObject({
71
+ id: 1,
72
+ projectId: 42,
73
+ name: 'Brand names',
74
+ isActive: true,
75
+ });
76
+ const [url, init] = fetchMock.mock.calls[0];
77
+ expect(url).toBe('https://api.example.com/v1/dictionaries');
78
+ expect(init.method).toBe('GET');
79
+ });
80
+
81
+ it('passes project_id as a query param when supplied', async () => {
82
+ fetchMock.mockResolvedValueOnce(jsonResponse({ dictionaries: [] }));
83
+ const client = makeClient();
84
+ await client.dictionaries.list({ projectId: 99 });
85
+ expect(fetchMock.mock.calls[0][0]).toBe(
86
+ 'https://api.example.com/v1/dictionaries?project_id=99',
87
+ );
88
+ });
89
+
90
+ it('creates a dictionary', async () => {
91
+ fetchMock.mockResolvedValueOnce(jsonResponse(dictRow({ id: 7, name: 'Glossary' })));
92
+ const client = makeClient();
93
+ const d = await client.dictionaries.create({
94
+ name: 'Glossary',
95
+ description: 'hi',
96
+ language: 'en',
97
+ });
98
+ expect(d.id).toBe(7);
99
+ const [url, init] = fetchMock.mock.calls[0];
100
+ expect(url).toBe('https://api.example.com/v1/dictionaries');
101
+ expect(init.method).toBe('POST');
102
+ expect(JSON.parse(init.body as string)).toEqual({
103
+ name: 'Glossary',
104
+ description: 'hi',
105
+ language: 'en',
106
+ });
107
+ });
108
+
109
+ it('updates only provided fields', async () => {
110
+ fetchMock.mockResolvedValueOnce(jsonResponse(dictRow({ is_active: false })));
111
+ const client = makeClient();
112
+ await client.dictionaries.update(1, { isActive: false });
113
+ const [url, init] = fetchMock.mock.calls[0];
114
+ expect(url).toBe('https://api.example.com/v1/dictionaries/1');
115
+ expect(init.method).toBe('PATCH');
116
+ expect(JSON.parse(init.body as string)).toEqual({ is_active: false });
117
+ });
118
+
119
+ it('deletes a dictionary', async () => {
120
+ fetchMock.mockResolvedValueOnce(jsonResponse({ deleted: true }));
121
+ const client = makeClient();
122
+ await client.dictionaries.delete(1);
123
+ const [url, init] = fetchMock.mock.calls[0];
124
+ expect(url).toBe('https://api.example.com/v1/dictionaries/1');
125
+ expect(init.method).toBe('DELETE');
126
+ });
127
+ });
128
+
129
+ describe('Dictionary entries CRUD', () => {
130
+ it('lists entries with search + pagination', async () => {
131
+ fetchMock.mockResolvedValueOnce(
132
+ jsonResponse({
133
+ entries: [entryRow()],
134
+ total: 1,
135
+ limit: 25,
136
+ offset: 0,
137
+ }),
138
+ );
139
+ const client = makeClient();
140
+ const res = await client.dictionaries.entries.list(1, {
141
+ search: 'kub',
142
+ limit: 25,
143
+ });
144
+ expect(res.total).toBe(1);
145
+ expect(res.entries[0]).toMatchObject({
146
+ id: 11,
147
+ dictionaryId: 1,
148
+ word: 'Kubernetes',
149
+ caseSensitive: false,
150
+ });
151
+ expect(fetchMock.mock.calls[0][0]).toBe(
152
+ 'https://api.example.com/v1/dictionaries/1/entries?search=kub&limit=25',
153
+ );
154
+ });
155
+
156
+ it('adds a single entry and maps camelCase fields', async () => {
157
+ fetchMock.mockResolvedValueOnce(
158
+ jsonResponse(entryRow({ word: 'Postgres', replacement: 'post-gres' })),
159
+ );
160
+ const client = makeClient();
161
+ const e = await client.dictionaries.entries.add(1, {
162
+ word: 'Postgres',
163
+ replacement: 'post-gres',
164
+ caseSensitive: true,
165
+ });
166
+ expect(e.word).toBe('Postgres');
167
+ const [, init] = fetchMock.mock.calls[0];
168
+ expect(JSON.parse(init.body as string)).toEqual({
169
+ word: 'Postgres',
170
+ replacement: 'post-gres',
171
+ case_sensitive: true,
172
+ });
173
+ });
174
+
175
+ it('updates a single entry', async () => {
176
+ fetchMock.mockResolvedValueOnce(jsonResponse(entryRow({ replacement: 'new' })));
177
+ const client = makeClient();
178
+ await client.dictionaries.entries.update(1, 11, { replacement: 'new' });
179
+ const [url, init] = fetchMock.mock.calls[0];
180
+ expect(url).toBe('https://api.example.com/v1/dictionaries/1/entries/11');
181
+ expect(init.method).toBe('PATCH');
182
+ expect(JSON.parse(init.body as string)).toEqual({ replacement: 'new' });
183
+ });
184
+
185
+ it('deletes a single entry', async () => {
186
+ fetchMock.mockResolvedValueOnce(jsonResponse({ deleted: true }));
187
+ const client = makeClient();
188
+ await client.dictionaries.entries.delete(1, 11);
189
+ expect(fetchMock.mock.calls[0][1].method).toBe('DELETE');
190
+ });
191
+
192
+ it('bulk replaces entries', async () => {
193
+ fetchMock.mockResolvedValueOnce(
194
+ jsonResponse({ upserted: 2, deleted: 3, total: 2 }),
195
+ );
196
+ const client = makeClient();
197
+ const result = await client.dictionaries.entries.replaceAll(1, [
198
+ { word: 'Postgres', replacement: 'post-gres' },
199
+ { word: 'K8s', replacement: 'kubernetes', caseSensitive: true },
200
+ ]);
201
+ expect(result).toEqual({ upserted: 2, deleted: 3, total: 2 });
202
+ const [url, init] = fetchMock.mock.calls[0];
203
+ expect(url).toBe('https://api.example.com/v1/dictionaries/1/entries');
204
+ expect(init.method).toBe('PUT');
205
+ expect(JSON.parse(init.body as string)).toEqual({
206
+ entries: [
207
+ { word: 'Postgres', replacement: 'post-gres' },
208
+ { word: 'K8s', replacement: 'kubernetes', case_sensitive: true },
209
+ ],
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Resources for managing per-project custom word dictionaries.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * const dict = await client.dictionaries.create({ name: 'Brand names' });
7
+ * await client.dictionaries.entries.add(dict.id, {
8
+ * word: 'Postgres',
9
+ * replacement: 'post-gres',
10
+ * });
11
+ *
12
+ * // Sync from an external source — atomic, idempotent
13
+ * await client.dictionaries.entries.replaceAll(dict.id, [
14
+ * { word: 'Postgres', replacement: 'post-gres' },
15
+ * { word: 'Kubernetes', replacement: 'koo-ber-net-eez' },
16
+ * ]);
17
+ * ```
18
+ */
19
+
20
+ import type { KugelAudio } from './client';
21
+ import type {
22
+ BulkReplaceResult,
23
+ CreateDictionaryOptions,
24
+ Dictionary,
25
+ DictionaryEntry,
26
+ DictionaryEntryInput,
27
+ DictionaryEntryListResponse,
28
+ UpdateDictionaryOptions,
29
+ UpdateDictionaryEntryOptions,
30
+ } from './types';
31
+
32
+ function mapDictionary(raw: Record<string, unknown>): Dictionary {
33
+ return {
34
+ id: raw.id as number,
35
+ projectId: raw.project_id as number,
36
+ name: raw.name as string,
37
+ description: (raw.description as string | null) ?? undefined,
38
+ language: (raw.language as string | null) ?? undefined,
39
+ isActive: (raw.is_active as boolean) ?? true,
40
+ createdAt: raw.created_at as string,
41
+ updatedAt: raw.updated_at as string,
42
+ };
43
+ }
44
+
45
+ function mapEntry(raw: Record<string, unknown>): DictionaryEntry {
46
+ return {
47
+ id: raw.id as number,
48
+ dictionaryId: raw.dictionary_id as number,
49
+ word: raw.word as string,
50
+ replacement: raw.replacement as string,
51
+ ipa: (raw.ipa as string | null) ?? undefined,
52
+ caseSensitive: (raw.case_sensitive as boolean) ?? false,
53
+ createdAt: raw.created_at as string,
54
+ updatedAt: raw.updated_at as string,
55
+ };
56
+ }
57
+
58
+ function entryPayload(input: DictionaryEntryInput): Record<string, unknown> {
59
+ const payload: Record<string, unknown> = {
60
+ word: input.word,
61
+ replacement: input.replacement,
62
+ };
63
+ if (input.ipa !== undefined) payload.ipa = input.ipa;
64
+ if (input.caseSensitive !== undefined) {
65
+ payload.case_sensitive = input.caseSensitive;
66
+ }
67
+ return payload;
68
+ }
69
+
70
+ function buildPath(base: string, params: Record<string, unknown>): string {
71
+ const search = new URLSearchParams();
72
+ for (const [key, val] of Object.entries(params)) {
73
+ if (val === undefined || val === null) continue;
74
+ search.set(key, String(val));
75
+ }
76
+ const query = search.toString();
77
+ return query ? `${base}?${query}` : base;
78
+ }
79
+
80
+ /**
81
+ * Resource for entries within a single dictionary.
82
+ * Access via ``client.dictionaries.entries`` and pass ``dictionaryId``
83
+ * to each call.
84
+ */
85
+ export class DictionaryEntriesResource {
86
+ constructor(private client: KugelAudio) {}
87
+
88
+ /** List entries with optional search + pagination. */
89
+ async list(
90
+ dictionaryId: number,
91
+ options?: {
92
+ search?: string;
93
+ limit?: number;
94
+ offset?: number;
95
+ projectId?: number;
96
+ },
97
+ ): Promise<DictionaryEntryListResponse> {
98
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
99
+ search: options?.search,
100
+ limit: options?.limit,
101
+ offset: options?.offset,
102
+ project_id: options?.projectId,
103
+ });
104
+ const raw = await this.client.request<{
105
+ entries: Record<string, unknown>[];
106
+ total: number;
107
+ limit: number;
108
+ offset: number;
109
+ }>('GET', path);
110
+ return {
111
+ entries: raw.entries.map(mapEntry),
112
+ total: raw.total,
113
+ limit: raw.limit,
114
+ offset: raw.offset,
115
+ };
116
+ }
117
+
118
+ /** Add a single entry to a dictionary. */
119
+ async add(
120
+ dictionaryId: number,
121
+ entry: DictionaryEntryInput,
122
+ options?: { projectId?: number },
123
+ ): Promise<DictionaryEntry> {
124
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
125
+ project_id: options?.projectId,
126
+ });
127
+ const raw = await this.client.request<Record<string, unknown>>(
128
+ 'POST',
129
+ path,
130
+ entryPayload(entry),
131
+ );
132
+ return mapEntry(raw);
133
+ }
134
+
135
+ /** Update an existing entry. */
136
+ async update(
137
+ dictionaryId: number,
138
+ entryId: number,
139
+ updates: UpdateDictionaryEntryOptions,
140
+ options?: { projectId?: number },
141
+ ): Promise<DictionaryEntry> {
142
+ const payload: Record<string, unknown> = {};
143
+ if (updates.word !== undefined) payload.word = updates.word;
144
+ if (updates.replacement !== undefined) payload.replacement = updates.replacement;
145
+ if (updates.ipa !== undefined) payload.ipa = updates.ipa;
146
+ if (updates.caseSensitive !== undefined) {
147
+ payload.case_sensitive = updates.caseSensitive;
148
+ }
149
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
150
+ project_id: options?.projectId,
151
+ });
152
+ const raw = await this.client.request<Record<string, unknown>>(
153
+ 'PATCH',
154
+ path,
155
+ payload,
156
+ );
157
+ return mapEntry(raw);
158
+ }
159
+
160
+ /** Delete a single entry. */
161
+ async delete(
162
+ dictionaryId: number,
163
+ entryId: number,
164
+ options?: { projectId?: number },
165
+ ): Promise<void> {
166
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
167
+ project_id: options?.projectId,
168
+ });
169
+ await this.client.request<{ deleted: boolean }>('DELETE', path);
170
+ }
171
+
172
+ /**
173
+ * Replace every entry in the dictionary atomically.
174
+ *
175
+ * Each item must have ``word`` and ``replacement``; existing entries
176
+ * whose ``word`` is not in the supplied list are deleted. Idempotent.
177
+ */
178
+ async replaceAll(
179
+ dictionaryId: number,
180
+ entries: DictionaryEntryInput[],
181
+ options?: { projectId?: number },
182
+ ): Promise<BulkReplaceResult> {
183
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
184
+ project_id: options?.projectId,
185
+ });
186
+ const raw = await this.client.request<BulkReplaceResult>('PUT', path, {
187
+ entries: entries.map(entryPayload),
188
+ });
189
+ return raw;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Resource for managing per-project custom dictionaries.
195
+ */
196
+ export class DictionariesResource {
197
+ /** Per-entry operations within a dictionary. */
198
+ public readonly entries: DictionaryEntriesResource;
199
+
200
+ constructor(private client: KugelAudio) {
201
+ this.entries = new DictionaryEntriesResource(client);
202
+ }
203
+
204
+ /** List every dictionary in the caller's project. */
205
+ async list(options?: { projectId?: number }): Promise<Dictionary[]> {
206
+ const path = buildPath('/v1/dictionaries', {
207
+ project_id: options?.projectId,
208
+ });
209
+ const raw = await this.client.request<{
210
+ dictionaries: Record<string, unknown>[];
211
+ }>('GET', path);
212
+ return raw.dictionaries.map(mapDictionary);
213
+ }
214
+
215
+ /** Create a new dictionary scoped to the caller's project. */
216
+ async create(
217
+ body: CreateDictionaryOptions,
218
+ options?: { projectId?: number },
219
+ ): Promise<Dictionary> {
220
+ const path = buildPath('/v1/dictionaries', {
221
+ project_id: options?.projectId,
222
+ });
223
+ const payload: Record<string, unknown> = { name: body.name };
224
+ if (body.description !== undefined) payload.description = body.description;
225
+ if (body.language !== undefined) payload.language = body.language;
226
+ const raw = await this.client.request<Record<string, unknown>>(
227
+ 'POST',
228
+ path,
229
+ payload,
230
+ );
231
+ return mapDictionary(raw);
232
+ }
233
+
234
+ /** Fetch a single dictionary. */
235
+ async get(
236
+ dictionaryId: number,
237
+ options?: { projectId?: number },
238
+ ): Promise<Dictionary> {
239
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
240
+ project_id: options?.projectId,
241
+ });
242
+ const raw = await this.client.request<Record<string, unknown>>('GET', path);
243
+ return mapDictionary(raw);
244
+ }
245
+
246
+ /** Update name / description / language / isActive. */
247
+ async update(
248
+ dictionaryId: number,
249
+ updates: UpdateDictionaryOptions,
250
+ options?: { projectId?: number },
251
+ ): Promise<Dictionary> {
252
+ const payload: Record<string, unknown> = {};
253
+ if (updates.name !== undefined) payload.name = updates.name;
254
+ if (updates.description !== undefined) payload.description = updates.description;
255
+ if (updates.language !== undefined) payload.language = updates.language;
256
+ if (updates.isActive !== undefined) payload.is_active = updates.isActive;
257
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
258
+ project_id: options?.projectId,
259
+ });
260
+ const raw = await this.client.request<Record<string, unknown>>(
261
+ 'PATCH',
262
+ path,
263
+ payload,
264
+ );
265
+ return mapDictionary(raw);
266
+ }
267
+
268
+ /** Delete a dictionary (cascades to its entries). */
269
+ async delete(
270
+ dictionaryId: number,
271
+ options?: { projectId?: number },
272
+ ): Promise<void> {
273
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
274
+ project_id: options?.projectId,
275
+ });
276
+ await this.client.request<{ deleted: boolean }>('DELETE', path);
277
+ }
278
+ }
package/src/errors.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * All SDK errors inherit from {@link KugelAudioError}. Specific subclasses
5
5
  * map to the server's `error_code` field (see the server-side `ErrorCode`
6
- * enum at `tts/src/serving/deployments/errors.py`) so callers can
6
+ * enum at `engine/src/serving/deployments/errors.py`) so callers can
7
7
  * `instanceof AuthenticationError` without matching on message text.
8
8
  */
9
9
 
@@ -126,6 +126,26 @@ export class ConnectionError extends KugelAudioError {
126
126
  }
127
127
  }
128
128
 
129
+ /**
130
+ * A referenced resource doesn't exist or isn't visible to the caller.
131
+ *
132
+ * Surfaced when the server returns HTTP 404 with `error_code = NOT_FOUND` —
133
+ * e.g. an unknown `voiceId`, a voice that belongs to another org, or a
134
+ * deleted resource. Distinct from {@link ValidationError} (malformed
135
+ * request) so callers can show "not found" UX without parsing messages.
136
+ */
137
+ export class NotFoundError extends KugelAudioError {
138
+ constructor(message?: string, options: KugelAudioErrorOptions = {}) {
139
+ super(message ?? 'Not found.', {
140
+ statusCode: 404,
141
+ errorCode: ErrorCodes.NOT_FOUND,
142
+ ...options,
143
+ });
144
+ this.name = 'NotFoundError';
145
+ Object.setPrototypeOf(this, NotFoundError.prototype);
146
+ }
147
+ }
148
+
129
149
  // ---------------------------------------------------------------------------
130
150
  // Classifiers
131
151
  // ---------------------------------------------------------------------------
@@ -168,6 +188,9 @@ function build(
168
188
  common,
169
189
  );
170
190
  }
191
+ if (errorCode === ErrorCodes.NOT_FOUND || status === 404) {
192
+ return new NotFoundError(message || undefined, common);
193
+ }
171
194
  return new KugelAudioError(message || `HTTP ${status}`, common);
172
195
  }
173
196
 
package/src/index.ts CHANGED
@@ -46,8 +46,14 @@ export { KugelAudio } from './client';
46
46
  export type {
47
47
  AudioChunk,
48
48
  AudioResponse,
49
+ BulkReplaceResult,
49
50
  ContextVoiceSettings,
51
+ CreateDictionaryOptions,
50
52
  CreateVoiceOptions,
53
+ Dictionary,
54
+ DictionaryEntry,
55
+ DictionaryEntryInput,
56
+ DictionaryEntryListResponse,
51
57
  GenerateOptions,
52
58
  GenerationStats,
53
59
  KugelAudioOptions,
@@ -59,6 +65,8 @@ export type {
59
65
  StreamCallbacks,
60
66
  StreamConfig,
61
67
  StreamingSessionCallbacks,
68
+ UpdateDictionaryEntryOptions,
69
+ UpdateDictionaryOptions,
62
70
  UpdateVoiceOptions,
63
71
  Voice,
64
72
  VoiceAge,
@@ -71,6 +79,8 @@ export type {
71
79
  WordTimestamp
72
80
  } from './types';
73
81
 
82
+ export { DictionariesResource, DictionaryEntriesResource } from './dictionaries';
83
+
74
84
  // Errors
75
85
  export {
76
86
  AuthenticationError,
@@ -78,6 +88,7 @@ export {
78
88
  ErrorCodes,
79
89
  InsufficientCreditsError,
80
90
  KugelAudioError,
91
+ NotFoundError,
81
92
  RateLimitError,
82
93
  ValidationError,
83
94
  WsCloseCodes,
package/src/types.ts CHANGED
@@ -62,6 +62,94 @@ export interface VoiceListResponse {
62
62
  */
63
63
  export type VoiceQuality = 'low' | 'mid' | 'high';
64
64
 
65
+ // ─── Dictionaries ────────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * A per-project pronunciation dictionary.
69
+ */
70
+ export interface Dictionary {
71
+ id: number;
72
+ projectId: number;
73
+ name: string;
74
+ description?: string;
75
+ language?: string;
76
+ isActive: boolean;
77
+ createdAt: string;
78
+ updatedAt: string;
79
+ }
80
+
81
+ /**
82
+ * A single word → replacement / IPA mapping within a dictionary.
83
+ */
84
+ export interface DictionaryEntry {
85
+ id: number;
86
+ dictionaryId: number;
87
+ word: string;
88
+ replacement: string;
89
+ ipa?: string;
90
+ caseSensitive: boolean;
91
+ createdAt: string;
92
+ updatedAt: string;
93
+ }
94
+
95
+ /**
96
+ * Paginated response from listing entries.
97
+ */
98
+ export interface DictionaryEntryListResponse {
99
+ entries: DictionaryEntry[];
100
+ total: number;
101
+ limit: number;
102
+ offset: number;
103
+ }
104
+
105
+ /**
106
+ * Counts returned by `entries.replaceAll`.
107
+ */
108
+ export interface BulkReplaceResult {
109
+ upserted: number;
110
+ deleted: number;
111
+ total: number;
112
+ }
113
+
114
+ /**
115
+ * Options for creating a dictionary.
116
+ */
117
+ export interface CreateDictionaryOptions {
118
+ name: string;
119
+ description?: string;
120
+ language?: string;
121
+ }
122
+
123
+ /**
124
+ * Options for updating a dictionary. Only provided fields are changed.
125
+ */
126
+ export interface UpdateDictionaryOptions {
127
+ name?: string;
128
+ description?: string;
129
+ language?: string;
130
+ isActive?: boolean;
131
+ }
132
+
133
+ /**
134
+ * Payload for creating or replacing a single entry.
135
+ */
136
+ export interface DictionaryEntryInput {
137
+ word: string;
138
+ replacement: string;
139
+ ipa?: string;
140
+ caseSensitive?: boolean;
141
+ }
142
+
143
+ /**
144
+ * Options for updating an entry.
145
+ */
146
+ export interface UpdateDictionaryEntryOptions {
147
+ word?: string;
148
+ replacement?: string;
149
+ ipa?: string;
150
+ caseSensitive?: boolean;
151
+ }
152
+
65
153
  /**
66
154
  * Extended voice information returned by voice management endpoints.
67
155
  */