@pie-players/tts-server-polly 0.3.74 → 1.0.0-beta.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.
package/README.md CHANGED
@@ -1,23 +1,16 @@
1
1
  # @pie-players/tts-server-polly
2
2
 
3
- AWS Polly provider for server-side text-to-speech with native speech marks support.
4
-
5
- For the cross-package TTS architecture and browser/server flow, see
6
- [TTS Architecture](../../docs/accessibility/tts-architecture.md). This README
7
- focuses on AWS Polly-specific provider configuration and behavior.
8
-
9
- ## Overview
10
-
11
- This package provides a server-side TTS provider that uses AWS Polly to generate high-quality neural speech with millisecond-precise word timing through speech marks.
12
-
13
- ## Features
14
-
15
- - ✅ **Native Speech Marks** - Millisecond-accurate word timing from AWS Polly
16
- - ✅ **Neural Voices** - High-quality neural TTS (default) or standard voices
17
- - ✅ **25+ Languages** - Wide language support
18
- - ✅ **Full SSML** - Supports Speech Synthesis Markup Language
19
- - ✅ **Parallel Requests** - Audio and speech marks fetched simultaneously
20
- - ✅ **60+ Voices** - Multiple voices per language
3
+ `PollyServerProvider`, the AWS Polly provider for server-side text-to-speech
4
+ (TTS), built on `@pie-players/tts-server-core`. It returns audio together with
5
+ Polly's speech marks, the word timings that drive read-aloud highlighting in the
6
+ browser. This README covers the provider's configuration and behavior for
7
+ developers writing a host's TTS routes. The
8
+ [integration guide](./examples/INTEGRATION-GUIDE.md) builds those routes in
9
+ SvelteKit; the [AWS Polly setup guide](../../docs/accessibility/aws-polly-setup-guide.md)
10
+ covers credentials and the IAM policy; [TTS Architecture](../../docs/accessibility/tts-architecture.md)
11
+ covers the browser and server flow across packages.
12
+
13
+ The package runs on Node.js 20 or later.
21
14
 
22
15
  ## Installation
23
16
 
@@ -32,37 +25,48 @@ npm install @pie-players/tts-server-polly
32
25
  ```typescript
33
26
  import { PollyServerProvider } from '@pie-players/tts-server-polly';
34
27
 
28
+ const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env;
29
+
35
30
  const provider = new PollyServerProvider();
36
31
 
37
32
  await provider.initialize({
38
33
  region: 'us-east-1',
39
- credentials: {
40
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
41
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
42
- },
34
+ // Explicit keys only when both are set; otherwise the AWS SDK default
35
+ // credential chain resolves them (IAM role, SSO, shared profile).
36
+ credentials:
37
+ AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY
38
+ ? {
39
+ accessKeyId: AWS_ACCESS_KEY_ID,
40
+ secretAccessKey: AWS_SECRET_ACCESS_KEY,
41
+ sessionToken: AWS_SESSION_TOKEN || undefined,
42
+ }
43
+ : undefined,
43
44
  engine: 'neural', // or 'standard'
44
45
  defaultVoice: 'Joanna',
45
46
  });
46
47
  ```
47
48
 
49
+ `initialize` validates the config and creates the Polly client without calling
50
+ AWS. A missing `region` throws a `TTSError` with `INITIALIZATION_ERROR`.
51
+
48
52
  ### Synthesize Speech
49
53
 
50
54
  ```typescript
51
55
  const result = await provider.synthesize({
52
56
  text: 'Hello world, this is a test of AWS Polly text to speech.',
53
- voice: 'Joanna', // Optional, uses defaultVoice if not specified
57
+ voice: 'Joanna', // optional; see Voice Selection
54
58
  includeSpeechMarks: true,
55
59
  });
56
60
 
57
61
  console.log('Audio:', result.audio); // Buffer
58
- console.log('Speech marks:', result.speechMarks); // Array of word timings
59
- console.log('Duration:', result.metadata.duration, 'seconds');
62
+ console.log('Speech marks:', result.speechMarks); // word timings
63
+ console.log('Synthesis time:', result.metadata.duration, 'seconds');
60
64
  ```
61
65
 
62
66
  ### List Available Voices
63
67
 
64
68
  ```typescript
65
- // Get all neural voices
69
+ // Voices for the configured engine
66
70
  const voices = await provider.getVoices();
67
71
 
68
72
  // Filter by language
@@ -72,6 +76,11 @@ const spanishVoices = await provider.getVoices({ language: 'es-ES' });
72
76
  const femaleVoices = await provider.getVoices({ gender: 'female' });
73
77
  ```
74
78
 
79
+ `getVoices` asks Polly for the configured engine's voices, in the requested
80
+ language when one is given, and filters by gender and quality locally. Each
81
+ voice's `providerMetadata` carries Polly's `supportedEngines` and
82
+ `additionalLanguageCodes`.
83
+
75
84
  ### Speech Marks Example
76
85
 
77
86
  ```typescript
@@ -87,6 +96,10 @@ const result = await provider.synthesize({
87
96
  // ]
88
97
  ```
89
98
 
99
+ `time` is milliseconds into the audio. `start` and `end` are UTF-16 indexes into
100
+ the request text, end exclusive; the provider re-anchors Polly's UTF-8 byte
101
+ offsets to them.
102
+
90
103
  ## Configuration
91
104
 
92
105
  ### PollyProviderConfig
@@ -94,55 +107,74 @@ const result = await provider.synthesize({
94
107
  ```typescript
95
108
  interface PollyProviderConfig {
96
109
  region: string; // AWS region (required)
97
- credentials?: { // AWS credentials (optional if using IAM)
110
+ credentials?: { // omitted: AWS SDK default credential chain
98
111
  accessKeyId: string;
99
112
  secretAccessKey: string;
100
113
  sessionToken?: string;
101
114
  };
102
- engine?: 'neural' | 'standard'; // Voice engine (default: 'neural')
103
- defaultVoice?: string; // Default voice ID (default: 'Joanna')
115
+ engine?: 'neural' | 'standard'; // fixed per instance (default: 'neural')
116
+ defaultVoice?: string; // default: 'Joanna'
117
+ enableLogging?: boolean; // log SSML detection (default: false)
104
118
  }
105
119
  ```
106
120
 
107
- ### Environment Variables
108
-
109
- ```bash
110
- AWS_REGION=us-east-1
111
- AWS_ACCESS_KEY_ID=your_key_id
112
- AWS_SECRET_ACCESS_KEY=your_secret_key
113
- ```
121
+ The provider reads no environment variables; the host passes region and
122
+ credentials in. The [setup guide](../../docs/accessibility/aws-polly-setup-guide.md)
123
+ lists the variables the demo routes read and the IAM role setup for production.
124
+
125
+ ## Synthesis Behavior
126
+
127
+ - **Requests.** Each synthesis sends two SynthesizeSpeech requests in parallel,
128
+ one for the audio and one for the speech marks. `includeSpeechMarks: false`
129
+ skips the second.
130
+ - **Speech-mark types.** `providerOptions.speechMarkTypes` selects among `word`,
131
+ `sentence` and `ssml`. Without it, or when it names none of them, Polly returns
132
+ word marks.
133
+ - **Engine.** The engine is set at `initialize`, and a request cannot change it.
134
+ A host that offers both engines runs one instance per engine.
135
+ - **Output.** `format: 'ogg'` returns Ogg Vorbis, `'pcm'` returns PCM, and
136
+ anything else MP3; `'wav'` fails validation. `sampleRate` defaults to 24000 Hz
137
+ for MP3 and Ogg Vorbis and to 16000 Hz for PCM, which Polly takes only at 8000
138
+ or 16000. `contentType` is the one Polly reports, `audio/mpeg` when it reports
139
+ none.
140
+ - **SSML and prosody.** Text containing SSML tags, Polly's `<amazon:…>` and
141
+ `<aws-…>` extensions included, goes to Polly unchanged. Plain text with a
142
+ `rate` or `pitch` other than 1 is escaped and wrapped in `<speak><prosody>`,
143
+ so the two apply to plain text only. A `rate` above 2 speaks at 2, Polly's
144
+ 200% ceiling. The neural engine drops `pitch`.
145
+
146
+ ### Voice Selection
147
+
148
+ A request's `voice` wins. Without one, the provider picks a voice for the
149
+ request's `language` from the engine's voice listing and falls back to
150
+ `defaultVoice`; the core README's [voice resolution](../tts-server-core/README.md#voice-resolution)
151
+ section has the matching rules.
114
152
 
115
153
  ## Capabilities
116
154
 
117
155
  | Feature | Support |
118
156
  |---------|---------|
119
- | Speech Marks | ✅ Native |
120
- | SSML | ✅ Full |
121
- | Pitch Control | ⚠️ SSML only |
122
- | Rate Control | ✅ SSML |
123
- | Volume Control | ❌ Client-side |
124
- | Max Text Length | 3000 chars |
125
- | Audio Format | MP3 |
157
+ | Speech Marks | Word, sentence and SSML marks |
158
+ | SSML | Polly's supported subset |
159
+ | Pitch Control | Standard engine only; plain text |
160
+ | Rate Control | Plain text, through `<prosody>`; up to 2× |
161
+ | Volume Control | Not supported |
162
+ | Max Text Length | 3000 characters |
163
+ | Audio Formats | MP3, Ogg Vorbis, PCM |
164
+ | Sample Rate | Per request (default 24000 Hz, 16000 Hz for PCM) |
126
165
 
127
166
  ## Cost
128
167
 
129
- - **Standard voices:** $4 per 1M characters
130
- - **Neural voices:** $16 per 1M characters
131
- - **Speech marks:** Included (no extra charge)
132
-
133
- ## Supported Voices
168
+ Polly bills each SynthesizeSpeech request by characters; a synthesis with speech
169
+ marks makes two (audio and marks). Current rates:
170
+ <https://aws.amazon.com/polly/pricing/>
134
171
 
135
- Popular voices include:
172
+ ## Voices
136
173
 
137
- - **English (US):** Joanna, Matthew, Ivy, Kendra, Joey
138
- - **English (UK):** Amy, Brian, Emma
139
- - **Spanish:** Lucia, Conchita, Enrique
140
- - **French:** Celine, Mathieu
141
- - **German:** Marlene, Hans
142
- - **Italian:** Carla, Giorgio
143
- - **Portuguese:** Vitoria, Ricardo
144
-
145
- Use `getVoices()` for complete list.
174
+ Polly's [voice list](https://docs.aws.amazon.com/polly/latest/dg/available-voices.html)
175
+ names each voice and the engines it supports; `getVoices()` returns the voices
176
+ available to the configured engine. A voice the engine does not support fails
177
+ with `INVALID_REQUEST`.
146
178
 
147
179
  ## Error Handling
148
180
 
@@ -160,25 +192,26 @@ try {
160
192
  }
161
193
  ```
162
194
 
195
+ AWS failures from `synthesize` and `getVoices` arrive as a `TTSError`:
196
+
197
+ | AWS failure | `TTSErrorCode` |
198
+ |-------------|----------------|
199
+ | `TextLengthExceededException` | `TEXT_TOO_LONG` |
200
+ | Invalid SSML, sample rate, language, engine, speech-mark, lexicon or S3/SNS input; `ValidationException` | `INVALID_REQUEST` |
201
+ | `ThrottlingException`, `ServiceQuotaExceededException`, HTTP 429 | `RATE_LIMIT_EXCEEDED` |
202
+ | HTTP 401 or 403 | `AUTHENTICATION_ERROR` |
203
+ | Anything else | `PROVIDER_ERROR` |
204
+
205
+ Request validation runs before any AWS call and rejects with a `TTSError`:
206
+ `TEXT_TOO_LONG` for text over 3000 characters, `INVALID_REQUEST` for empty text,
207
+ an unsupported format, or a rate or pitch out of range. A call before
208
+ `initialize` throws a `TTSError` with `INITIALIZATION_ERROR`.
209
+
163
210
  ## AWS IAM Permissions
164
211
 
165
- Required IAM permissions:
166
-
167
- ```json
168
- {
169
- "Version": "2012-10-17",
170
- "Statement": [
171
- {
172
- "Effect": "Allow",
173
- "Action": [
174
- "polly:SynthesizeSpeech",
175
- "polly:DescribeVoices"
176
- ],
177
- "Resource": "*"
178
- }
179
- ]
180
- }
181
- ```
212
+ The provider needs `polly:SynthesizeSpeech` and `polly:DescribeVoices`. The
213
+ [AWS Polly Setup Guide](../../docs/accessibility/aws-polly-setup-guide.md) has the
214
+ policy and the credential setup.
182
215
 
183
216
  ## License
184
217
 
@@ -2,6 +2,7 @@
2
2
  * AWS Polly server-side TTS provider
3
3
  * @module @pie-players/tts-server-polly
4
4
  */
5
+ import { PollyClient } from "@aws-sdk/client-polly";
5
6
  import { BaseTTSProvider, type GetVoicesOptions, type ServerProviderCapabilities, type SynthesizeRequest, type SynthesizeResponse, type TTSServerConfig, type Voice } from "@pie-players/tts-server-core";
6
7
  /**
7
8
  * AWS Polly provider configuration.
@@ -34,7 +35,7 @@ export interface PollyProviderConfig extends TTSServerConfig {
34
35
  *
35
36
  * @extension AWS Polly-specific
36
37
  * @default 'neural'
37
- * @note Neural: $16/1M chars, Standard: $4/1M chars
38
+ * @see https://aws.amazon.com/polly/pricing/
38
39
  */
39
40
  engine?: "neural" | "standard";
40
41
  /**
@@ -45,7 +46,25 @@ export interface PollyProviderConfig extends TTSServerConfig {
45
46
  * @see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
46
47
  */
47
48
  defaultVoice?: string;
49
+ /**
50
+ * Enable detailed logging for debugging
51
+ *
52
+ * @default false
53
+ */
54
+ enableLogging?: boolean;
48
55
  }
56
+ /**
57
+ * AWS Polly Server Provider
58
+ *
59
+ * Provides high-quality neural text-to-speech with precise word-level timing
60
+ * through AWS Polly speech marks.
61
+ *
62
+ * Features:
63
+ * - Native speech marks support (millisecond precision)
64
+ * - Neural and standard voices
65
+ * - Full SSML support
66
+ * - Parallel audio + speech marks requests
67
+ */
49
68
  export declare class PollyServerProvider extends BaseTTSProvider {
50
69
  readonly providerId = "aws-polly";
51
70
  readonly providerName = "AWS Polly";
@@ -53,7 +72,9 @@ export declare class PollyServerProvider extends BaseTTSProvider {
53
72
  private client;
54
73
  private engine;
55
74
  private defaultVoice;
75
+ private enableLogging;
56
76
  private resolveOutputFormat;
77
+ private resolveEngine;
57
78
  private resolveSpeechMarkTypes;
58
79
  /**
59
80
  * Initialize the AWS Polly provider.
@@ -65,10 +86,17 @@ export declare class PollyServerProvider extends BaseTTSProvider {
65
86
  * @performance Completes in ~10-50ms
66
87
  */
67
88
  initialize(config: PollyProviderConfig): Promise<void>;
89
+ /** The AWS client every request goes through; a test subclass supplies a fake. */
90
+ protected createClient(config: PollyProviderConfig): PollyClient;
68
91
  /**
69
92
  * Synthesize speech with AWS Polly
70
93
  */
71
94
  synthesize(request: SynthesizeRequest): Promise<SynthesizeResponse>;
95
+ /**
96
+ * Neural voices take `<prosody>` rate and volume but not pitch. Polly's
97
+ * rate runs 20–200%, so a faster request speaks at 200%.
98
+ */
99
+ protected buildProsodyAttrs(request: SynthesizeRequest): string;
72
100
  /**
73
101
  * Synthesize audio stream
74
102
  */
@@ -3,25 +3,14 @@
3
3
  * @module @pie-players/tts-server-polly
4
4
  */
5
5
  import { DescribeVoicesCommand, Engine, PollyClient, SynthesizeSpeechCommand, } from "@aws-sdk/client-polly";
6
- import { BaseTTSProvider, TTSError, TTSErrorCode, } from "@pie-players/tts-server-core";
7
- /**
8
- * AWS Polly Server Provider
9
- *
10
- * Provides high-quality neural text-to-speech with precise word-level timing
11
- * through AWS Polly speech marks.
12
- *
13
- * Features:
14
- * - Native speech marks support (millisecond precision)
15
- * - Neural and standard voices
16
- * - 25+ languages
17
- * - Full SSML support
18
- * - Parallel audio + speech marks requests
19
- */
6
+ import { anchorSpeechMarks, BaseTTSProvider, TTSError, TTSErrorCode, } from "@pie-players/tts-server-core";
20
7
  /**
21
8
  * Polly's own SSML vocabulary, on top of the standard elements the base provider
22
9
  * knows. `<amazon:effect>` and the `<aws-*>` family are Polly-only.
23
10
  */
24
11
  const POLLY_SSML_TAGS = ["<amazon:", "<aws-"];
12
+ /** Polly's `<prosody rate>` runs 20–200%. */
13
+ const POLLY_MAX_RATE = 2;
25
14
  /**
26
15
  * AWS SDK exception names (`error.name`) that mean the request itself was
27
16
  * invalid or unsupported, as opposed to a transient/provider-side failure.
@@ -45,6 +34,37 @@ const POLLY_RATE_LIMIT_EXCEPTIONS = new Set([
45
34
  "ThrottlingException",
46
35
  "ServiceQuotaExceededException",
47
36
  ]);
37
+ /** Polly takes 8000 and 16000 Hz for pcm, and also 22050 and 24000 for mp3 and ogg_vorbis. */
38
+ const DEFAULT_SAMPLE_RATE_HERTZ = {
39
+ mp3: 24000,
40
+ ogg_vorbis: 24000,
41
+ pcm: 16000,
42
+ };
43
+ /** Collects a Polly response body (`AudioStream`) into one buffer. */
44
+ async function readResponseBody(stream) {
45
+ const chunks = [];
46
+ if (Symbol.asyncIterator in stream) {
47
+ for await (const chunk of stream) {
48
+ chunks.push(chunk);
49
+ }
50
+ }
51
+ else if (stream instanceof Uint8Array) {
52
+ chunks.push(stream);
53
+ }
54
+ return Buffer.concat(chunks);
55
+ }
56
+ /**
57
+ * AWS Polly Server Provider
58
+ *
59
+ * Provides high-quality neural text-to-speech with precise word-level timing
60
+ * through AWS Polly speech marks.
61
+ *
62
+ * Features:
63
+ * - Native speech marks support (millisecond precision)
64
+ * - Neural and standard voices
65
+ * - Full SSML support
66
+ * - Parallel audio + speech marks requests
67
+ */
48
68
  export class PollyServerProvider extends BaseTTSProvider {
49
69
  providerId = "aws-polly";
50
70
  providerName = "AWS Polly";
@@ -52,6 +72,7 @@ export class PollyServerProvider extends BaseTTSProvider {
52
72
  client;
53
73
  engine = "neural";
54
74
  defaultVoice = "Joanna";
75
+ enableLogging = false;
55
76
  resolveOutputFormat(request) {
56
77
  switch (request.format) {
57
78
  case "ogg":
@@ -65,26 +86,15 @@ export class PollyServerProvider extends BaseTTSProvider {
65
86
  return "mp3";
66
87
  }
67
88
  }
89
+ resolveEngine() {
90
+ return this.engine === "neural" ? Engine.NEURAL : Engine.STANDARD;
91
+ }
68
92
  resolveSpeechMarkTypes(request) {
69
- const providerOptions = (request.providerOptions || {});
70
- const configured = providerOptions.speechMarkTypes;
71
- if (!Array.isArray(configured) || configured.length === 0) {
72
- return ["word"];
73
- }
74
- const resolved = configured
75
- .map((entry) => {
76
- if (entry === "word")
77
- return "word";
78
- if (entry === "sentence")
79
- return "sentence";
80
- if (entry === "ssml")
81
- return "ssml";
82
- return null;
83
- })
84
- .filter((entry) => entry !== null);
85
- if (resolved.length === 0)
93
+ const configured = request.providerOptions?.speechMarkTypes;
94
+ if (!Array.isArray(configured))
86
95
  return ["word"];
87
- return resolved;
96
+ const resolved = configured.filter((entry) => entry === "word" || entry === "sentence" || entry === "ssml");
97
+ return resolved.length > 0 ? resolved : ["word"];
88
98
  }
89
99
  /**
90
100
  * Initialize the AWS Polly provider.
@@ -102,19 +112,22 @@ export class PollyServerProvider extends BaseTTSProvider {
102
112
  this.config = config;
103
113
  this.engine = config.engine || "neural";
104
114
  this.defaultVoice = config.defaultVoice || "Joanna";
115
+ this.enableLogging = config.enableLogging || false;
105
116
  try {
106
- // Create Polly client (fast - no API calls)
107
- this.client = new PollyClient({
108
- region: config.region,
109
- credentials: config.credentials,
110
- });
117
+ this.client = this.createClient(config);
111
118
  this.initialized = true;
112
- // NOTE: We do NOT call getVoices() here - that's an explicit secondary operation
113
119
  }
114
120
  catch (error) {
115
121
  throw new TTSError(TTSErrorCode.INITIALIZATION_ERROR, `Failed to initialize AWS Polly: ${error instanceof Error ? error.message : String(error)}`, { error }, this.providerId);
116
122
  }
117
123
  }
124
+ /** The AWS client every request goes through; a test subclass supplies a fake. */
125
+ createClient(config) {
126
+ return new PollyClient({
127
+ region: config.region,
128
+ credentials: config.credentials,
129
+ });
130
+ }
118
131
  /**
119
132
  * Synthesize speech with AWS Polly
120
133
  */
@@ -122,10 +135,9 @@ export class PollyServerProvider extends BaseTTSProvider {
122
135
  this.ensureInitialized();
123
136
  const capabilities = this.getCapabilities();
124
137
  this.validateRequest(request, capabilities);
125
- const voice = request.voice || this.defaultVoice;
138
+ const voice = await this.resolveRequestVoice(request, this.defaultVoice);
126
139
  const startTime = Date.now();
127
140
  try {
128
- // Make parallel requests for audio and speech marks
129
141
  const [audioResponse, speechMarksResponse] = await Promise.all([
130
142
  this.synthesizeAudio(request, voice),
131
143
  request.includeSpeechMarks !== false
@@ -151,41 +163,43 @@ export class PollyServerProvider extends BaseTTSProvider {
151
163
  throw this.mapPollyErrorToTTSError(error, { request });
152
164
  }
153
165
  }
166
+ /**
167
+ * Neural voices take `<prosody>` rate and volume but not pitch. Polly's
168
+ * rate runs 20–200%, so a faster request speaks at 200%.
169
+ */
170
+ buildProsodyAttrs(request) {
171
+ return super.buildProsodyAttrs({
172
+ ...request,
173
+ rate: request.rate === undefined
174
+ ? undefined
175
+ : Math.min(request.rate, POLLY_MAX_RATE),
176
+ pitch: this.engine === "neural" ? undefined : request.pitch,
177
+ });
178
+ }
154
179
  /**
155
180
  * Synthesize audio stream
156
181
  */
157
182
  async synthesizeAudio(request, voice) {
158
- if (this.detectSSML(request.text, POLLY_SSML_TAGS)) {
183
+ if (this.enableLogging && this.detectSSML(request.text, POLLY_SSML_TAGS)) {
159
184
  console.log("[PollyServerProvider] Detected SSML content, using TextType: ssml");
160
185
  }
161
186
  const { text, isSsml } = this.applyProsody(request.text, request, POLLY_SSML_TAGS);
162
187
  const textType = isSsml ? "ssml" : "text";
188
+ const outputFormat = this.resolveOutputFormat(request);
163
189
  const command = new SynthesizeSpeechCommand({
164
- Engine: this.engine === "neural" ? Engine.NEURAL : Engine.STANDARD,
165
- OutputFormat: this.resolveOutputFormat(request),
190
+ Engine: this.resolveEngine(),
191
+ OutputFormat: outputFormat,
166
192
  Text: text,
167
193
  TextType: textType,
168
194
  VoiceId: voice,
169
- SampleRate: String(request.sampleRate || 24000),
195
+ SampleRate: String(request.sampleRate || DEFAULT_SAMPLE_RATE_HERTZ[outputFormat]),
170
196
  });
171
197
  const response = await this.client.send(command);
172
198
  if (!response.AudioStream) {
173
199
  throw new Error("No audio stream received from AWS Polly");
174
200
  }
175
- // Convert stream to buffer
176
- const chunks = [];
177
- const stream = response.AudioStream;
178
- if (Symbol.asyncIterator in stream) {
179
- for await (const chunk of stream) {
180
- chunks.push(chunk);
181
- }
182
- }
183
- else if (stream instanceof Uint8Array) {
184
- chunks.push(stream);
185
- }
186
- const audioBuffer = Buffer.concat(chunks);
187
201
  return {
188
- audio: audioBuffer,
202
+ audio: await readResponseBody(response.AudioStream),
189
203
  contentType: response.ContentType || "audio/mpeg",
190
204
  };
191
205
  }
@@ -196,7 +210,7 @@ export class PollyServerProvider extends BaseTTSProvider {
196
210
  const { text, isSsml } = this.applyProsody(request.text, request, POLLY_SSML_TAGS);
197
211
  const textType = isSsml ? "ssml" : "text";
198
212
  const command = new SynthesizeSpeechCommand({
199
- Engine: this.engine === "neural" ? Engine.NEURAL : Engine.STANDARD,
213
+ Engine: this.resolveEngine(),
200
214
  OutputFormat: "json",
201
215
  Text: text,
202
216
  TextType: textType,
@@ -207,21 +221,9 @@ export class PollyServerProvider extends BaseTTSProvider {
207
221
  if (!response.AudioStream) {
208
222
  return [];
209
223
  }
210
- // Convert stream to text
211
- const chunks = [];
212
- const stream = response.AudioStream;
213
- if (Symbol.asyncIterator in stream) {
214
- for await (const chunk of stream) {
215
- chunks.push(chunk);
216
- }
217
- }
218
- else if (stream instanceof Uint8Array) {
219
- chunks.push(stream);
220
- }
221
- const marksText = Buffer.concat(chunks).toString("utf-8");
222
- // Parse NDJSON (newline-delimited JSON)
223
- // Each line is a separate JSON object
224
- const speechMarks = marksText
224
+ const marksText = (await readResponseBody(response.AudioStream)).toString("utf-8");
225
+ // Polly returns speech marks as NDJSON: one JSON object per line.
226
+ const reported = marksText
225
227
  .trim()
226
228
  .split("\n")
227
229
  .filter((line) => line.trim())
@@ -240,7 +242,9 @@ export class PollyServerProvider extends BaseTTSProvider {
240
242
  value: mark.value,
241
243
  };
242
244
  });
243
- return speechMarks;
245
+ // Polly's offsets count UTF-8 bytes of the text it was sent, prosody
246
+ // envelope included.
247
+ return anchorSpeechMarks(reported, request.text);
244
248
  }
245
249
  /**
246
250
  * Get available voices from AWS Polly
@@ -249,7 +253,7 @@ export class PollyServerProvider extends BaseTTSProvider {
249
253
  this.ensureInitialized();
250
254
  try {
251
255
  const input = {
252
- Engine: this.engine === "neural" ? Engine.NEURAL : Engine.STANDARD,
256
+ Engine: this.resolveEngine(),
253
257
  };
254
258
  if (options?.language) {
255
259
  input.LanguageCode = options.language;
@@ -276,7 +280,6 @@ export class PollyServerProvider extends BaseTTSProvider {
276
280
  additionalLanguageCodes: voice.AdditionalLanguageCodes,
277
281
  },
278
282
  })).filter((voice) => {
279
- // Apply filters
280
283
  if (options?.gender && voice.gender !== options.gender) {
281
284
  return false;
282
285
  }
@@ -299,27 +302,24 @@ export class PollyServerProvider extends BaseTTSProvider {
299
302
  return {
300
303
  // W3C Standard features
301
304
  standard: {
302
- supportsSSML: true, // ✅ Full SSML 1.1 + AWS extensions
303
- supportsPitch: true, // ✅ Via SSML <prosody pitch> (not direct API param)
304
- supportsRate: true, // ✅ Via SSML <prosody rate> (not direct API param)
305
- supportsVolume: false, // ❌ Not supported by Polly API (handle client-side)
306
- supportsMultipleVoices: true, // ✅ 60+ voices across 25+ languages
305
+ supportsSSML: true, // Polly's supported SSML subset
306
+ supportsPitch: this.engine === "standard", // Via SSML <prosody pitch>; neural voices lack it
307
+ supportsRate: true, // Via SSML <prosody rate> (not direct API param)
308
+ supportsVolume: false, // Not supported by Polly API (handle client-side)
309
+ supportsMultipleVoices: true, // see describeVoices for the roster
307
310
  maxTextLength: 3000, // AWS Polly limit per request
308
311
  },
309
312
  // Provider-specific extensions
310
313
  extensions: {
311
- supportsSpeechMarks: true, // ✅ Native WORD speech marks (millisecond precision)
314
+ supportsSpeechMarks: true, // Native WORD speech marks (millisecond precision)
312
315
  supportedFormats: ["mp3", "ogg", "pcm"],
313
- supportsSampleRate: true, // ✅ Configurable sample rate
316
+ supportsSampleRate: true, // Configurable sample rate
314
317
  // AWS Polly-specific features
315
318
  providerSpecific: {
316
319
  engines: ["neural", "standard"], // Engine selection
317
320
  supportedSpeechMarkTypes: ["word", "sentence", "ssml"],
318
321
  supportsLexicons: false, // Not yet implemented
319
- awsSSMLExtensions: true, // <aws-break>, <aws-emphasis>, <aws-w>, etc.
320
- neuralVoicesCount: 30, // ~30 neural voices available
321
- standardVoicesCount: 30, // ~30 standard voices available
322
- languagesCount: 25, // 25+ languages supported
322
+ awsSSMLExtensions: true, // <amazon:effect>, <amazon:domain>, <amazon:auto-breaths>
323
323
  },
324
324
  },
325
325
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/tts-server-polly",
3
- "version": "0.3.74",
3
+ "version": "1.0.0-beta.0",
4
4
  "description": "AWS Polly provider for server-side TTS with speech marks support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,8 +17,7 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "dev": "tsc --watch",
20
- "test": "vitest run --passWithNoTests",
21
- "test:coverage": "vitest --coverage"
20
+ "test": "vitest run"
22
21
  },
23
22
  "keywords": [
24
23
  "tts",
@@ -38,8 +37,8 @@
38
37
  "access": "public"
39
38
  },
40
39
  "dependencies": {
41
- "@aws-sdk/client-polly": "^3.1137.0",
42
- "@pie-players/tts-server-core": "0.3.74"
40
+ "@aws-sdk/client-polly": "^3.1147.0",
41
+ "@pie-players/tts-server-core": "1.0.0-beta.0"
43
42
  },
44
43
  "devDependencies": {
45
44
  "typescript": "^5.9.3",