@stacksjs/ts-cloud 0.1.8 → 0.1.9

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.
Files changed (75) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/package.json +18 -16
  3. package/src/aws/acm.ts +768 -0
  4. package/src/aws/application-autoscaling.ts +845 -0
  5. package/src/aws/bedrock.ts +4074 -0
  6. package/src/aws/client.ts +891 -0
  7. package/src/aws/cloudformation.ts +896 -0
  8. package/src/aws/cloudfront.ts +1531 -0
  9. package/src/aws/cloudwatch-logs.ts +154 -0
  10. package/src/aws/comprehend.ts +839 -0
  11. package/src/aws/connect.ts +1056 -0
  12. package/src/aws/deploy-imap.ts +384 -0
  13. package/src/aws/dynamodb.ts +340 -0
  14. package/src/aws/ec2.ts +1385 -0
  15. package/src/aws/ecr.ts +621 -0
  16. package/src/aws/ecs.ts +615 -0
  17. package/src/aws/elasticache.ts +301 -0
  18. package/src/aws/elbv2.ts +942 -0
  19. package/src/aws/email.ts +928 -0
  20. package/src/aws/eventbridge.ts +248 -0
  21. package/src/aws/iam.ts +1689 -0
  22. package/src/aws/imap-server.ts +2100 -0
  23. package/src/aws/index.ts +213 -0
  24. package/src/aws/kendra.ts +1097 -0
  25. package/src/aws/lambda.ts +786 -0
  26. package/src/aws/opensearch.ts +158 -0
  27. package/src/aws/personalize.ts +977 -0
  28. package/src/aws/polly.ts +559 -0
  29. package/src/aws/rds.ts +888 -0
  30. package/src/aws/rekognition.ts +846 -0
  31. package/src/aws/route53-domains.ts +359 -0
  32. package/src/aws/route53.ts +1046 -0
  33. package/src/aws/s3.ts +2334 -0
  34. package/src/aws/scheduler.ts +571 -0
  35. package/src/aws/secrets-manager.ts +769 -0
  36. package/src/aws/ses.ts +1081 -0
  37. package/src/aws/setup-phone.ts +104 -0
  38. package/src/aws/setup-sms.ts +580 -0
  39. package/src/aws/sms.ts +1735 -0
  40. package/src/aws/smtp-server.ts +531 -0
  41. package/src/aws/sns.ts +758 -0
  42. package/src/aws/sqs.ts +382 -0
  43. package/src/aws/ssm.ts +807 -0
  44. package/src/aws/sts.ts +92 -0
  45. package/src/aws/support.ts +391 -0
  46. package/src/aws/test-imap.ts +86 -0
  47. package/src/aws/textract.ts +780 -0
  48. package/src/aws/transcribe.ts +108 -0
  49. package/src/aws/translate.ts +641 -0
  50. package/src/aws/voice.ts +1379 -0
  51. package/src/config.ts +35 -0
  52. package/src/deploy/index.ts +7 -0
  53. package/src/deploy/static-site-external-dns.ts +945 -0
  54. package/src/deploy/static-site.ts +1175 -0
  55. package/src/dns/cloudflare.ts +548 -0
  56. package/src/dns/godaddy.ts +412 -0
  57. package/src/dns/index.ts +205 -0
  58. package/src/dns/porkbun.ts +362 -0
  59. package/src/dns/route53-adapter.ts +414 -0
  60. package/src/dns/types.ts +119 -0
  61. package/src/dns/validator.ts +369 -0
  62. package/src/generators/index.ts +5 -0
  63. package/src/generators/infrastructure.ts +1660 -0
  64. package/src/index.ts +163 -0
  65. package/src/push/apns.ts +452 -0
  66. package/src/push/fcm.ts +506 -0
  67. package/src/push/index.ts +58 -0
  68. package/src/security/pre-deploy-scanner.ts +655 -0
  69. package/src/ssl/acme-client.ts +478 -0
  70. package/src/ssl/index.ts +7 -0
  71. package/src/ssl/letsencrypt.ts +747 -0
  72. package/src/types.ts +2 -0
  73. package/src/utils/cli.ts +398 -0
  74. package/src/validation/index.ts +5 -0
  75. package/src/validation/template.ts +405 -0
@@ -0,0 +1,559 @@
1
+ /**
2
+ * AWS Polly Client
3
+ * Text-to-Speech service
4
+ * No external SDK dependencies - implements AWS Signature V4 directly
5
+ */
6
+
7
+ import { AWSClient } from './client'
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export type VoiceId =
14
+ | 'Aditi' | 'Amy' | 'Aria' | 'Arlet' | 'Arthur' | 'Astrid'
15
+ | 'Ayanda' | 'Bianca' | 'Brian' | 'Camila' | 'Carla' | 'Carmen'
16
+ | 'Celine' | 'Chantal' | 'Conchita' | 'Cristiano' | 'Daniel' | 'Dora'
17
+ | 'Elin' | 'Emma' | 'Enrique' | 'Ewa' | 'Filiz' | 'Gabrielle'
18
+ | 'Geraint' | 'Giorgio' | 'Gwyneth' | 'Hala' | 'Hannah' | 'Hans'
19
+ | 'Hiujin' | 'Ida' | 'Ines' | 'Ivy' | 'Jacek' | 'Jan'
20
+ | 'Joanna' | 'Joey' | 'Justin' | 'Kajal' | 'Karl' | 'Kazuha'
21
+ | 'Kendra' | 'Kevin' | 'Kimberly' | 'Laura' | 'Lea' | 'Liam'
22
+ | 'Lisa' | 'Liv' | 'Lotte' | 'Lucia' | 'Lupe' | 'Mads'
23
+ | 'Maja' | 'Marlene' | 'Mathieu' | 'Matthew' | 'Maxim' | 'Mia'
24
+ | 'Miguel' | 'Mizuki' | 'Naja' | 'Niamh' | 'Nicole' | 'Ola'
25
+ | 'Olivia' | 'Pedro' | 'Penelope' | 'Raveena' | 'Remi' | 'Ricardo'
26
+ | 'Ruben' | 'Russell' | 'Ruth' | 'Salli' | 'Seoyeon' | 'Sergio'
27
+ | 'Sofie' | 'Stephen' | 'Suvi' | 'Takumi' | 'Tatyana' | 'Thiago'
28
+ | 'Tomoko' | 'Vicki' | 'Vitoria' | 'Zayd' | 'Zeina' | 'Zhiyu'
29
+
30
+ export type LanguageCode =
31
+ | 'arb' | 'ca-ES' | 'cmn-CN' | 'cy-GB' | 'da-DK' | 'de-AT' | 'de-DE'
32
+ | 'en-AU' | 'en-GB' | 'en-GB-WLS' | 'en-IE' | 'en-IN' | 'en-NZ' | 'en-US' | 'en-ZA'
33
+ | 'es-ES' | 'es-MX' | 'es-US' | 'fi-FI' | 'fr-BE' | 'fr-CA' | 'fr-FR'
34
+ | 'hi-IN' | 'is-IS' | 'it-IT' | 'ja-JP' | 'ko-KR' | 'nb-NO' | 'nl-BE' | 'nl-NL'
35
+ | 'pl-PL' | 'pt-BR' | 'pt-PT' | 'ro-RO' | 'ru-RU' | 'sv-SE' | 'tr-TR' | 'yue-CN'
36
+
37
+ export type Engine = 'standard' | 'neural' | 'long-form' | 'generative'
38
+
39
+ export type OutputFormat = 'json' | 'mp3' | 'ogg_vorbis' | 'pcm'
40
+
41
+ export type TextType = 'ssml' | 'text'
42
+
43
+ export type SpeechMarkType = 'sentence' | 'ssml' | 'viseme' | 'word'
44
+
45
+ export interface Voice {
46
+ Gender?: 'Female' | 'Male'
47
+ Id?: VoiceId
48
+ LanguageCode?: LanguageCode
49
+ LanguageName?: string
50
+ Name?: string
51
+ AdditionalLanguageCodes?: LanguageCode[]
52
+ SupportedEngines?: Engine[]
53
+ }
54
+
55
+ export interface SynthesizeSpeechCommandInput {
56
+ Engine?: Engine
57
+ LanguageCode?: LanguageCode
58
+ LexiconNames?: string[]
59
+ OutputFormat: OutputFormat
60
+ SampleRate?: string
61
+ SpeechMarkTypes?: SpeechMarkType[]
62
+ Text: string
63
+ TextType?: TextType
64
+ VoiceId: VoiceId
65
+ }
66
+
67
+ export interface SynthesizeSpeechCommandOutput {
68
+ AudioStream?: Uint8Array
69
+ ContentType?: string
70
+ RequestCharacters?: number
71
+ }
72
+
73
+ export interface DescribeVoicesCommandInput {
74
+ Engine?: Engine
75
+ LanguageCode?: LanguageCode
76
+ IncludeAdditionalLanguageCodes?: boolean
77
+ NextToken?: string
78
+ }
79
+
80
+ export interface DescribeVoicesCommandOutput {
81
+ Voices?: Voice[]
82
+ NextToken?: string
83
+ }
84
+
85
+ export interface StartSpeechSynthesisTaskCommandInput {
86
+ Engine?: Engine
87
+ LanguageCode?: LanguageCode
88
+ LexiconNames?: string[]
89
+ OutputFormat: OutputFormat
90
+ OutputS3BucketName: string
91
+ OutputS3KeyPrefix?: string
92
+ SampleRate?: string
93
+ SnsTopicArn?: string
94
+ SpeechMarkTypes?: SpeechMarkType[]
95
+ Text: string
96
+ TextType?: TextType
97
+ VoiceId: VoiceId
98
+ }
99
+
100
+ export interface SynthesisTask {
101
+ Engine?: Engine
102
+ TaskId?: string
103
+ TaskStatus?: 'scheduled' | 'inProgress' | 'completed' | 'failed'
104
+ TaskStatusReason?: string
105
+ OutputUri?: string
106
+ CreationTime?: string
107
+ RequestCharacters?: number
108
+ SnsTopicArn?: string
109
+ LexiconNames?: string[]
110
+ OutputFormat?: OutputFormat
111
+ SampleRate?: string
112
+ SpeechMarkTypes?: SpeechMarkType[]
113
+ TextType?: TextType
114
+ VoiceId?: VoiceId
115
+ LanguageCode?: LanguageCode
116
+ }
117
+
118
+ export interface StartSpeechSynthesisTaskCommandOutput {
119
+ SynthesisTask?: SynthesisTask
120
+ }
121
+
122
+ export interface GetSpeechSynthesisTaskCommandInput {
123
+ TaskId: string
124
+ }
125
+
126
+ export interface GetSpeechSynthesisTaskCommandOutput {
127
+ SynthesisTask?: SynthesisTask
128
+ }
129
+
130
+ export interface ListSpeechSynthesisTasksCommandInput {
131
+ MaxResults?: number
132
+ NextToken?: string
133
+ Status?: 'scheduled' | 'inProgress' | 'completed' | 'failed'
134
+ }
135
+
136
+ export interface ListSpeechSynthesisTasksCommandOutput {
137
+ NextToken?: string
138
+ SynthesisTasks?: SynthesisTask[]
139
+ }
140
+
141
+ export interface PutLexiconCommandInput {
142
+ Name: string
143
+ Content: string
144
+ }
145
+
146
+ export interface PutLexiconCommandOutput {
147
+ // Empty
148
+ }
149
+
150
+ export interface GetLexiconCommandInput {
151
+ Name: string
152
+ }
153
+
154
+ export interface Lexicon {
155
+ Content?: string
156
+ Name?: string
157
+ }
158
+
159
+ export interface LexiconAttributes {
160
+ Alphabet?: string
161
+ LanguageCode?: LanguageCode
162
+ LastModified?: string
163
+ LexemesCount?: number
164
+ LexiconArn?: string
165
+ Size?: number
166
+ }
167
+
168
+ export interface GetLexiconCommandOutput {
169
+ Lexicon?: Lexicon
170
+ LexiconAttributes?: LexiconAttributes
171
+ }
172
+
173
+ export interface DeleteLexiconCommandInput {
174
+ Name: string
175
+ }
176
+
177
+ export interface DeleteLexiconCommandOutput {
178
+ // Empty
179
+ }
180
+
181
+ export interface ListLexiconsCommandInput {
182
+ NextToken?: string
183
+ }
184
+
185
+ export interface ListLexiconsCommandOutput {
186
+ Lexicons?: Array<{
187
+ Name?: string
188
+ Attributes?: LexiconAttributes
189
+ }>
190
+ NextToken?: string
191
+ }
192
+
193
+ // ============================================================================
194
+ // Polly Client
195
+ // ============================================================================
196
+
197
+ export class PollyClient {
198
+ private client: AWSClient
199
+ private region: string
200
+
201
+ constructor(region: string = 'us-east-1') {
202
+ this.region = region
203
+ this.client = new AWSClient()
204
+ }
205
+
206
+ // -------------------------------------------------------------------------
207
+ // Speech Synthesis
208
+ // -------------------------------------------------------------------------
209
+
210
+ /**
211
+ * Synthesize speech from text (synchronous)
212
+ */
213
+ async synthesizeSpeech(params: SynthesizeSpeechCommandInput): Promise<SynthesizeSpeechCommandOutput> {
214
+ const result = await this.client.request({
215
+ service: 'polly',
216
+ region: this.region,
217
+ method: 'POST',
218
+ path: '/v1/speech',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({
223
+ Engine: params.Engine,
224
+ LanguageCode: params.LanguageCode,
225
+ LexiconNames: params.LexiconNames,
226
+ OutputFormat: params.OutputFormat,
227
+ SampleRate: params.SampleRate,
228
+ SpeechMarkTypes: params.SpeechMarkTypes,
229
+ Text: params.Text,
230
+ TextType: params.TextType,
231
+ VoiceId: params.VoiceId,
232
+ }),
233
+ rawResponse: true,
234
+ returnHeaders: true,
235
+ })
236
+
237
+ return {
238
+ AudioStream: new TextEncoder().encode(result.body),
239
+ ContentType: result.headers?.['content-type'],
240
+ RequestCharacters: result.headers?.['x-amzn-requestcharacters']
241
+ ? Number.parseInt(result.headers['x-amzn-requestcharacters'])
242
+ : undefined,
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Start async speech synthesis task
248
+ */
249
+ async startSpeechSynthesisTask(params: StartSpeechSynthesisTaskCommandInput): Promise<StartSpeechSynthesisTaskCommandOutput> {
250
+ return this.client.request({
251
+ service: 'polly',
252
+ region: this.region,
253
+ method: 'POST',
254
+ path: '/v1/synthesisTasks',
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ },
258
+ body: JSON.stringify(params),
259
+ })
260
+ }
261
+
262
+ /**
263
+ * Get speech synthesis task status
264
+ */
265
+ async getSpeechSynthesisTask(params: GetSpeechSynthesisTaskCommandInput): Promise<GetSpeechSynthesisTaskCommandOutput> {
266
+ return this.client.request({
267
+ service: 'polly',
268
+ region: this.region,
269
+ method: 'GET',
270
+ path: `/v1/synthesisTasks/${encodeURIComponent(params.TaskId)}`,
271
+ })
272
+ }
273
+
274
+ /**
275
+ * List speech synthesis tasks
276
+ */
277
+ async listSpeechSynthesisTasks(params?: ListSpeechSynthesisTasksCommandInput): Promise<ListSpeechSynthesisTasksCommandOutput> {
278
+ const queryParams: Record<string, string> = {}
279
+ if (params?.MaxResults) queryParams.MaxResults = params.MaxResults.toString()
280
+ if (params?.NextToken) queryParams.NextToken = params.NextToken
281
+ if (params?.Status) queryParams.Status = params.Status
282
+
283
+ return this.client.request({
284
+ service: 'polly',
285
+ region: this.region,
286
+ method: 'GET',
287
+ path: '/v1/synthesisTasks',
288
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
289
+ })
290
+ }
291
+
292
+ // -------------------------------------------------------------------------
293
+ // Voices
294
+ // -------------------------------------------------------------------------
295
+
296
+ /**
297
+ * List available voices
298
+ */
299
+ async describeVoices(params?: DescribeVoicesCommandInput): Promise<DescribeVoicesCommandOutput> {
300
+ const queryParams: Record<string, string> = {}
301
+ if (params?.Engine) queryParams.Engine = params.Engine
302
+ if (params?.LanguageCode) queryParams.LanguageCode = params.LanguageCode
303
+ if (params?.IncludeAdditionalLanguageCodes !== undefined) {
304
+ queryParams.IncludeAdditionalLanguageCodes = params.IncludeAdditionalLanguageCodes.toString()
305
+ }
306
+ if (params?.NextToken) queryParams.NextToken = params.NextToken
307
+
308
+ return this.client.request({
309
+ service: 'polly',
310
+ region: this.region,
311
+ method: 'GET',
312
+ path: '/v1/voices',
313
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
314
+ })
315
+ }
316
+
317
+ // -------------------------------------------------------------------------
318
+ // Lexicons
319
+ // -------------------------------------------------------------------------
320
+
321
+ /**
322
+ * Store a pronunciation lexicon
323
+ */
324
+ async putLexicon(params: PutLexiconCommandInput): Promise<PutLexiconCommandOutput> {
325
+ return this.client.request({
326
+ service: 'polly',
327
+ region: this.region,
328
+ method: 'PUT',
329
+ path: `/v1/lexicons/${encodeURIComponent(params.Name)}`,
330
+ headers: {
331
+ 'Content-Type': 'application/pls+xml',
332
+ },
333
+ body: params.Content,
334
+ })
335
+ }
336
+
337
+ /**
338
+ * Get a lexicon
339
+ */
340
+ async getLexicon(params: GetLexiconCommandInput): Promise<GetLexiconCommandOutput> {
341
+ return this.client.request({
342
+ service: 'polly',
343
+ region: this.region,
344
+ method: 'GET',
345
+ path: `/v1/lexicons/${encodeURIComponent(params.Name)}`,
346
+ })
347
+ }
348
+
349
+ /**
350
+ * Delete a lexicon
351
+ */
352
+ async deleteLexicon(params: DeleteLexiconCommandInput): Promise<DeleteLexiconCommandOutput> {
353
+ return this.client.request({
354
+ service: 'polly',
355
+ region: this.region,
356
+ method: 'DELETE',
357
+ path: `/v1/lexicons/${encodeURIComponent(params.Name)}`,
358
+ })
359
+ }
360
+
361
+ /**
362
+ * List lexicons
363
+ */
364
+ async listLexicons(params?: ListLexiconsCommandInput): Promise<ListLexiconsCommandOutput> {
365
+ const queryParams: Record<string, string> = {}
366
+ if (params?.NextToken) queryParams.NextToken = params.NextToken
367
+
368
+ return this.client.request({
369
+ service: 'polly',
370
+ region: this.region,
371
+ method: 'GET',
372
+ path: '/v1/lexicons',
373
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
374
+ })
375
+ }
376
+
377
+ // -------------------------------------------------------------------------
378
+ // Convenience Methods
379
+ // -------------------------------------------------------------------------
380
+
381
+ /**
382
+ * Simple text to speech - returns MP3 audio bytes
383
+ */
384
+ async textToSpeech(
385
+ text: string,
386
+ options?: {
387
+ voiceId?: VoiceId
388
+ engine?: Engine
389
+ languageCode?: LanguageCode
390
+ },
391
+ ): Promise<Uint8Array> {
392
+ const result = await this.synthesizeSpeech({
393
+ Text: text,
394
+ VoiceId: options?.voiceId || 'Joanna',
395
+ Engine: options?.engine || 'neural',
396
+ LanguageCode: options?.languageCode,
397
+ OutputFormat: 'mp3',
398
+ })
399
+ return result.AudioStream || new Uint8Array()
400
+ }
401
+
402
+ /**
403
+ * Text to speech with SSML support
404
+ */
405
+ async ssmlToSpeech(
406
+ ssml: string,
407
+ options?: {
408
+ voiceId?: VoiceId
409
+ engine?: Engine
410
+ languageCode?: LanguageCode
411
+ },
412
+ ): Promise<Uint8Array> {
413
+ const result = await this.synthesizeSpeech({
414
+ Text: ssml,
415
+ TextType: 'ssml',
416
+ VoiceId: options?.voiceId || 'Joanna',
417
+ Engine: options?.engine || 'neural',
418
+ LanguageCode: options?.languageCode,
419
+ OutputFormat: 'mp3',
420
+ })
421
+ return result.AudioStream || new Uint8Array()
422
+ }
423
+
424
+ /**
425
+ * Save speech to S3 (for longer texts)
426
+ */
427
+ async saveToS3(
428
+ text: string,
429
+ bucket: string,
430
+ keyPrefix: string,
431
+ options?: {
432
+ voiceId?: VoiceId
433
+ engine?: Engine
434
+ languageCode?: LanguageCode
435
+ textType?: TextType
436
+ },
437
+ ): Promise<{ taskId: string; outputUri: string }> {
438
+ const result = await this.startSpeechSynthesisTask({
439
+ Text: text,
440
+ TextType: options?.textType || 'text',
441
+ VoiceId: options?.voiceId || 'Joanna',
442
+ Engine: options?.engine || 'neural',
443
+ LanguageCode: options?.languageCode,
444
+ OutputFormat: 'mp3',
445
+ OutputS3BucketName: bucket,
446
+ OutputS3KeyPrefix: keyPrefix,
447
+ })
448
+
449
+ return {
450
+ taskId: result.SynthesisTask?.TaskId || '',
451
+ outputUri: result.SynthesisTask?.OutputUri || '',
452
+ }
453
+ }
454
+
455
+ /**
456
+ * List voices for a specific language
457
+ */
458
+ async listVoicesForLanguage(languageCode: LanguageCode): Promise<Voice[]> {
459
+ const result = await this.describeVoices({ LanguageCode: languageCode })
460
+ return result.Voices || []
461
+ }
462
+
463
+ /**
464
+ * List neural voices
465
+ */
466
+ async listNeuralVoices(): Promise<Voice[]> {
467
+ const result = await this.describeVoices({ Engine: 'neural' })
468
+ return result.Voices || []
469
+ }
470
+
471
+ /**
472
+ * Wait for synthesis task to complete
473
+ */
474
+ async waitForTask(
475
+ taskId: string,
476
+ options?: { maxWaitMs?: number; pollIntervalMs?: number },
477
+ ): Promise<SynthesisTask> {
478
+ const maxWaitMs = options?.maxWaitMs ?? 300000 // 5 minutes
479
+ const pollIntervalMs = options?.pollIntervalMs ?? 5000
480
+ const startTime = Date.now()
481
+
482
+ while (Date.now() - startTime < maxWaitMs) {
483
+ const result = await this.getSpeechSynthesisTask({ TaskId: taskId })
484
+ const task = result.SynthesisTask
485
+
486
+ if (task?.TaskStatus === 'completed') {
487
+ return task
488
+ }
489
+ if (task?.TaskStatus === 'failed') {
490
+ throw new Error(`Polly task ${taskId} failed: ${task.TaskStatusReason}`)
491
+ }
492
+
493
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
494
+ }
495
+
496
+ throw new Error(`Timeout waiting for Polly task ${taskId}`)
497
+ }
498
+ }
499
+
500
+ // ============================================================================
501
+ // Helper Functions
502
+ // ============================================================================
503
+
504
+ /**
505
+ * Quick text to speech
506
+ */
507
+ export async function textToSpeech(
508
+ text: string,
509
+ options?: {
510
+ voiceId?: VoiceId
511
+ engine?: Engine
512
+ region?: string
513
+ },
514
+ ): Promise<Uint8Array> {
515
+ const client = new PollyClient(options?.region || 'us-east-1')
516
+ return client.textToSpeech(text, options)
517
+ }
518
+
519
+ /**
520
+ * List available voices
521
+ */
522
+ export async function listVoices(
523
+ options?: {
524
+ languageCode?: LanguageCode
525
+ engine?: Engine
526
+ region?: string
527
+ },
528
+ ): Promise<Voice[]> {
529
+ const client = new PollyClient(options?.region || 'us-east-1')
530
+ const result = await client.describeVoices({
531
+ LanguageCode: options?.languageCode,
532
+ Engine: options?.engine,
533
+ })
534
+ return result.Voices || []
535
+ }
536
+
537
+ /**
538
+ * Create SSML with speech marks (pauses, emphasis, etc.)
539
+ */
540
+ export function createSSML(text: string, options?: {
541
+ rate?: 'x-slow' | 'slow' | 'medium' | 'fast' | 'x-fast'
542
+ pitch?: 'x-low' | 'low' | 'medium' | 'high' | 'x-high'
543
+ volume?: 'silent' | 'x-soft' | 'soft' | 'medium' | 'loud' | 'x-loud'
544
+ }): string {
545
+ let ssml = '<speak>'
546
+
547
+ if (options?.rate || options?.pitch || options?.volume) {
548
+ ssml += '<prosody'
549
+ if (options.rate) ssml += ` rate="${options.rate}"`
550
+ if (options.pitch) ssml += ` pitch="${options.pitch}"`
551
+ if (options.volume) ssml += ` volume="${options.volume}"`
552
+ ssml += `>${text}</prosody>`
553
+ } else {
554
+ ssml += text
555
+ }
556
+
557
+ ssml += '</speak>'
558
+ return ssml
559
+ }