@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 +108 -75
- package/dist/PollyServerProvider.d.ts +29 -1
- package/dist/PollyServerProvider.js +87 -87
- 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,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
|
|
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; //
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 |
|
|
120
|
-
| SSML |
|
|
121
|
-
| Pitch Control |
|
|
122
|
-
| Rate Control |
|
|
123
|
-
| Volume Control |
|
|
124
|
-
| Max Text Length | 3000
|
|
125
|
-
| 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) |
|
|
126
165
|
|
|
127
166
|
## Cost
|
|
128
167
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
172
|
+
## Voices
|
|
136
173
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
* @
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
165
|
-
OutputFormat:
|
|
190
|
+
Engine: this.resolveEngine(),
|
|
191
|
+
OutputFormat: outputFormat,
|
|
166
192
|
Text: text,
|
|
167
193
|
TextType: textType,
|
|
168
194
|
VoiceId: voice,
|
|
169
|
-
SampleRate: String(request.sampleRate ||
|
|
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:
|
|
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.
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
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
|
-
|
|
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.
|
|
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, //
|
|
303
|
-
supportsPitch:
|
|
304
|
-
supportsRate: true, //
|
|
305
|
-
supportsVolume: false, //
|
|
306
|
-
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
|
|
307
310
|
maxTextLength: 3000, // AWS Polly limit per request
|
|
308
311
|
},
|
|
309
312
|
// Provider-specific extensions
|
|
310
313
|
extensions: {
|
|
311
|
-
supportsSpeechMarks: true, //
|
|
314
|
+
supportsSpeechMarks: true, // Native WORD speech marks (millisecond precision)
|
|
312
315
|
supportedFormats: ["mp3", "ogg", "pcm"],
|
|
313
|
-
supportsSampleRate: true, //
|
|
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, // <
|
|
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
|
+
"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
|
|
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.0"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
44
|
"typescript": "^5.9.3",
|