@pie-players/tts-server-polly 0.3.75 → 1.0.0-beta.1
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 +108 -76
- package/dist/PollyServerProvider.d.ts +22 -1
- package/dist/PollyServerProvider.js +83 -85
- package/package.json +4 -5
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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', //
|
|
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); //
|
|
59
|
-
console.log('
|
|
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
|
-
//
|
|
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,56 +107,74 @@ const result = await provider.synthesize({
|
|
|
94
107
|
```typescript
|
|
95
108
|
interface PollyProviderConfig {
|
|
96
109
|
region: string; // AWS region (required)
|
|
97
|
-
credentials?: { // AWS
|
|
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'; //
|
|
103
|
-
defaultVoice?: string; //
|
|
104
|
-
enableLogging?: boolean; //
|
|
115
|
+
engine?: 'neural' | 'standard'; // fixed per instance (default: 'neural')
|
|
116
|
+
defaultVoice?: string; // default: 'Joanna'
|
|
117
|
+
enableLogging?: boolean; // log SSML detection (default: false)
|
|
105
118
|
}
|
|
106
119
|
```
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
115
152
|
|
|
116
153
|
## Capabilities
|
|
117
154
|
|
|
118
155
|
| Feature | Support |
|
|
119
156
|
|---------|---------|
|
|
120
|
-
| Speech Marks |
|
|
121
|
-
| SSML |
|
|
122
|
-
| Pitch Control |
|
|
123
|
-
| Rate Control |
|
|
124
|
-
| Volume Control |
|
|
125
|
-
| Max Text Length | 3000
|
|
126
|
-
| Audio
|
|
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) |
|
|
127
165
|
|
|
128
166
|
## Cost
|
|
129
167
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
## 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/>
|
|
135
171
|
|
|
136
|
-
|
|
172
|
+
## Voices
|
|
137
173
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- **German:** Marlene, Hans
|
|
143
|
-
- **Italian:** Carla, Giorgio
|
|
144
|
-
- **Portuguese:** Vitoria, Ricardo
|
|
145
|
-
|
|
146
|
-
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`.
|
|
147
178
|
|
|
148
179
|
## Error Handling
|
|
149
180
|
|
|
@@ -161,25 +192,26 @@ try {
|
|
|
161
192
|
}
|
|
162
193
|
```
|
|
163
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
|
+
|
|
164
210
|
## AWS IAM Permissions
|
|
165
211
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
"Version": "2012-10-17",
|
|
171
|
-
"Statement": [
|
|
172
|
-
{
|
|
173
|
-
"Effect": "Allow",
|
|
174
|
-
"Action": [
|
|
175
|
-
"polly:SynthesizeSpeech",
|
|
176
|
-
"polly:DescribeVoices"
|
|
177
|
-
],
|
|
178
|
-
"Resource": "*"
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
}
|
|
182
|
-
```
|
|
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.
|
|
183
215
|
|
|
184
216
|
## License
|
|
185
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
|
-
* @
|
|
38
|
+
* @see https://aws.amazon.com/polly/pricing/
|
|
38
39
|
*/
|
|
39
40
|
engine?: "neural" | "standard";
|
|
40
41
|
/**
|
|
@@ -52,6 +53,18 @@ export interface PollyProviderConfig extends TTSServerConfig {
|
|
|
52
53
|
*/
|
|
53
54
|
enableLogging?: boolean;
|
|
54
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
|
+
*/
|
|
55
68
|
export declare class PollyServerProvider extends BaseTTSProvider {
|
|
56
69
|
readonly providerId = "aws-polly";
|
|
57
70
|
readonly providerName = "AWS Polly";
|
|
@@ -61,6 +74,7 @@ export declare class PollyServerProvider extends BaseTTSProvider {
|
|
|
61
74
|
private defaultVoice;
|
|
62
75
|
private enableLogging;
|
|
63
76
|
private resolveOutputFormat;
|
|
77
|
+
private resolveEngine;
|
|
64
78
|
private resolveSpeechMarkTypes;
|
|
65
79
|
/**
|
|
66
80
|
* Initialize the AWS Polly provider.
|
|
@@ -72,10 +86,17 @@ export declare class PollyServerProvider extends BaseTTSProvider {
|
|
|
72
86
|
* @performance Completes in ~10-50ms
|
|
73
87
|
*/
|
|
74
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;
|
|
75
91
|
/**
|
|
76
92
|
* Synthesize speech with AWS Polly
|
|
77
93
|
*/
|
|
78
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;
|
|
79
100
|
/**
|
|
80
101
|
* Synthesize audio stream
|
|
81
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";
|
|
@@ -66,26 +86,15 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
66
86
|
return "mp3";
|
|
67
87
|
}
|
|
68
88
|
}
|
|
89
|
+
resolveEngine() {
|
|
90
|
+
return this.engine === "neural" ? Engine.NEURAL : Engine.STANDARD;
|
|
91
|
+
}
|
|
69
92
|
resolveSpeechMarkTypes(request) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (!Array.isArray(configured) || configured.length === 0) {
|
|
73
|
-
return ["word"];
|
|
74
|
-
}
|
|
75
|
-
const resolved = configured
|
|
76
|
-
.map((entry) => {
|
|
77
|
-
if (entry === "word")
|
|
78
|
-
return "word";
|
|
79
|
-
if (entry === "sentence")
|
|
80
|
-
return "sentence";
|
|
81
|
-
if (entry === "ssml")
|
|
82
|
-
return "ssml";
|
|
83
|
-
return null;
|
|
84
|
-
})
|
|
85
|
-
.filter((entry) => entry !== null);
|
|
86
|
-
if (resolved.length === 0)
|
|
93
|
+
const configured = request.providerOptions?.speechMarkTypes;
|
|
94
|
+
if (!Array.isArray(configured))
|
|
87
95
|
return ["word"];
|
|
88
|
-
|
|
96
|
+
const resolved = configured.filter((entry) => entry === "word" || entry === "sentence" || entry === "ssml");
|
|
97
|
+
return resolved.length > 0 ? resolved : ["word"];
|
|
89
98
|
}
|
|
90
99
|
/**
|
|
91
100
|
* Initialize the AWS Polly provider.
|
|
@@ -105,18 +114,20 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
105
114
|
this.defaultVoice = config.defaultVoice || "Joanna";
|
|
106
115
|
this.enableLogging = config.enableLogging || false;
|
|
107
116
|
try {
|
|
108
|
-
|
|
109
|
-
this.client = new PollyClient({
|
|
110
|
-
region: config.region,
|
|
111
|
-
credentials: config.credentials,
|
|
112
|
-
});
|
|
117
|
+
this.client = this.createClient(config);
|
|
113
118
|
this.initialized = true;
|
|
114
|
-
// NOTE: We do NOT call getVoices() here - that's an explicit secondary operation
|
|
115
119
|
}
|
|
116
120
|
catch (error) {
|
|
117
121
|
throw new TTSError(TTSErrorCode.INITIALIZATION_ERROR, `Failed to initialize AWS Polly: ${error instanceof Error ? error.message : String(error)}`, { error }, this.providerId);
|
|
118
122
|
}
|
|
119
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
|
+
}
|
|
120
131
|
/**
|
|
121
132
|
* Synthesize speech with AWS Polly
|
|
122
133
|
*/
|
|
@@ -127,7 +138,6 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
127
138
|
const voice = await this.resolveRequestVoice(request, this.defaultVoice);
|
|
128
139
|
const startTime = Date.now();
|
|
129
140
|
try {
|
|
130
|
-
// Make parallel requests for audio and speech marks
|
|
131
141
|
const [audioResponse, speechMarksResponse] = await Promise.all([
|
|
132
142
|
this.synthesizeAudio(request, voice),
|
|
133
143
|
request.includeSpeechMarks !== false
|
|
@@ -153,6 +163,19 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
153
163
|
throw this.mapPollyErrorToTTSError(error, { request });
|
|
154
164
|
}
|
|
155
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
|
+
}
|
|
156
179
|
/**
|
|
157
180
|
* Synthesize audio stream
|
|
158
181
|
*/
|
|
@@ -162,32 +185,21 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
162
185
|
}
|
|
163
186
|
const { text, isSsml } = this.applyProsody(request.text, request, POLLY_SSML_TAGS);
|
|
164
187
|
const textType = isSsml ? "ssml" : "text";
|
|
188
|
+
const outputFormat = this.resolveOutputFormat(request);
|
|
165
189
|
const command = new SynthesizeSpeechCommand({
|
|
166
|
-
Engine: this.
|
|
167
|
-
OutputFormat:
|
|
190
|
+
Engine: this.resolveEngine(),
|
|
191
|
+
OutputFormat: outputFormat,
|
|
168
192
|
Text: text,
|
|
169
193
|
TextType: textType,
|
|
170
194
|
VoiceId: voice,
|
|
171
|
-
SampleRate: String(request.sampleRate ||
|
|
195
|
+
SampleRate: String(request.sampleRate || DEFAULT_SAMPLE_RATE_HERTZ[outputFormat]),
|
|
172
196
|
});
|
|
173
197
|
const response = await this.client.send(command);
|
|
174
198
|
if (!response.AudioStream) {
|
|
175
199
|
throw new Error("No audio stream received from AWS Polly");
|
|
176
200
|
}
|
|
177
|
-
// Convert stream to buffer
|
|
178
|
-
const chunks = [];
|
|
179
|
-
const stream = response.AudioStream;
|
|
180
|
-
if (Symbol.asyncIterator in stream) {
|
|
181
|
-
for await (const chunk of stream) {
|
|
182
|
-
chunks.push(chunk);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else if (stream instanceof Uint8Array) {
|
|
186
|
-
chunks.push(stream);
|
|
187
|
-
}
|
|
188
|
-
const audioBuffer = Buffer.concat(chunks);
|
|
189
201
|
return {
|
|
190
|
-
audio:
|
|
202
|
+
audio: await readResponseBody(response.AudioStream),
|
|
191
203
|
contentType: response.ContentType || "audio/mpeg",
|
|
192
204
|
};
|
|
193
205
|
}
|
|
@@ -198,7 +210,7 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
198
210
|
const { text, isSsml } = this.applyProsody(request.text, request, POLLY_SSML_TAGS);
|
|
199
211
|
const textType = isSsml ? "ssml" : "text";
|
|
200
212
|
const command = new SynthesizeSpeechCommand({
|
|
201
|
-
Engine: this.
|
|
213
|
+
Engine: this.resolveEngine(),
|
|
202
214
|
OutputFormat: "json",
|
|
203
215
|
Text: text,
|
|
204
216
|
TextType: textType,
|
|
@@ -209,21 +221,9 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
209
221
|
if (!response.AudioStream) {
|
|
210
222
|
return [];
|
|
211
223
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
if (Symbol.asyncIterator in stream) {
|
|
216
|
-
for await (const chunk of stream) {
|
|
217
|
-
chunks.push(chunk);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
else if (stream instanceof Uint8Array) {
|
|
221
|
-
chunks.push(stream);
|
|
222
|
-
}
|
|
223
|
-
const marksText = Buffer.concat(chunks).toString("utf-8");
|
|
224
|
-
// Parse NDJSON (newline-delimited JSON)
|
|
225
|
-
// Each line is a separate JSON object
|
|
226
|
-
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
|
|
227
227
|
.trim()
|
|
228
228
|
.split("\n")
|
|
229
229
|
.filter((line) => line.trim())
|
|
@@ -242,7 +242,9 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
242
242
|
value: mark.value,
|
|
243
243
|
};
|
|
244
244
|
});
|
|
245
|
-
|
|
245
|
+
// Polly's offsets count UTF-8 bytes of the text it was sent, prosody
|
|
246
|
+
// envelope included.
|
|
247
|
+
return anchorSpeechMarks(reported, request.text);
|
|
246
248
|
}
|
|
247
249
|
/**
|
|
248
250
|
* Get available voices from AWS Polly
|
|
@@ -251,7 +253,7 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
251
253
|
this.ensureInitialized();
|
|
252
254
|
try {
|
|
253
255
|
const input = {
|
|
254
|
-
Engine: this.
|
|
256
|
+
Engine: this.resolveEngine(),
|
|
255
257
|
};
|
|
256
258
|
if (options?.language) {
|
|
257
259
|
input.LanguageCode = options.language;
|
|
@@ -278,7 +280,6 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
278
280
|
additionalLanguageCodes: voice.AdditionalLanguageCodes,
|
|
279
281
|
},
|
|
280
282
|
})).filter((voice) => {
|
|
281
|
-
// Apply filters
|
|
282
283
|
if (options?.gender && voice.gender !== options.gender) {
|
|
283
284
|
return false;
|
|
284
285
|
}
|
|
@@ -301,27 +302,24 @@ export class PollyServerProvider extends BaseTTSProvider {
|
|
|
301
302
|
return {
|
|
302
303
|
// W3C Standard features
|
|
303
304
|
standard: {
|
|
304
|
-
supportsSSML: true, //
|
|
305
|
-
supportsPitch:
|
|
306
|
-
supportsRate: true, //
|
|
307
|
-
supportsVolume: false, //
|
|
308
|
-
supportsMultipleVoices: true, //
|
|
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
|
|
309
310
|
maxTextLength: 3000, // AWS Polly limit per request
|
|
310
311
|
},
|
|
311
312
|
// Provider-specific extensions
|
|
312
313
|
extensions: {
|
|
313
|
-
supportsSpeechMarks: true, //
|
|
314
|
+
supportsSpeechMarks: true, // Native WORD speech marks (millisecond precision)
|
|
314
315
|
supportedFormats: ["mp3", "ogg", "pcm"],
|
|
315
|
-
supportsSampleRate: true, //
|
|
316
|
+
supportsSampleRate: true, // Configurable sample rate
|
|
316
317
|
// AWS Polly-specific features
|
|
317
318
|
providerSpecific: {
|
|
318
319
|
engines: ["neural", "standard"], // Engine selection
|
|
319
320
|
supportedSpeechMarkTypes: ["word", "sentence", "ssml"],
|
|
320
321
|
supportsLexicons: false, // Not yet implemented
|
|
321
|
-
awsSSMLExtensions: true, // <
|
|
322
|
-
neuralVoicesCount: 30, // ~30 neural voices available
|
|
323
|
-
standardVoicesCount: 30, // ~30 standard voices available
|
|
324
|
-
languagesCount: 25, // 25+ languages supported
|
|
322
|
+
awsSSMLExtensions: true, // <amazon:effect>, <amazon:domain>, <amazon:auto-breaths>
|
|
325
323
|
},
|
|
326
324
|
},
|
|
327
325
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/tts-server-polly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
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
|
|
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.
|
|
42
|
-
"@pie-players/tts-server-core": "0.
|
|
40
|
+
"@aws-sdk/client-polly": "^3.1147.0",
|
|
41
|
+
"@pie-players/tts-server-core": "1.0.0-beta.1"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
44
|
"typescript": "^5.9.3",
|