@creativeorange/azure-text-to-speech 2.2.3 → 3.0.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 +496 -0
- package/dist/co-azure-tts.es.js +425 -166
- package/dist/co-azure-tts.umd.js +8 -8
- package/package.json +21 -3
- package/src/SpeechToText.ts +106 -40
- package/src/TextToSpeech.ts +230 -129
- package/src/authentication.ts +228 -0
- package/src/main.ts +7 -0
- package/.eslintrc.js +0 -30
- package/index.html +0 -578
- package/nuxt/plugins/azure-speech-to-text.client.js +0 -27
- package/nuxt/plugins/azure-text-to-speech.client.js +0 -28
- package/tsconfig.json +0 -21
- package/vite.config.ts +0 -26
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export type SpeechAuthorization = {
|
|
2
|
+
token: string;
|
|
3
|
+
region: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type SpeechAuthorizationProvider =
|
|
7
|
+
() => Promise<SpeechAuthorization>;
|
|
8
|
+
|
|
9
|
+
export type SpeechTokenRequestOptions = {
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
credentials?: RequestCredentials;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SpeechAuthenticationOptions = {
|
|
15
|
+
tokenEndpoint?: string;
|
|
16
|
+
getAuthorizationToken?: SpeechAuthorizationProvider;
|
|
17
|
+
tokenRequestOptions?: SpeechTokenRequestOptions;
|
|
18
|
+
tokenLifetimeMs?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SpeechSafeError = {
|
|
22
|
+
message: string;
|
|
23
|
+
code?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TOKEN_LIFETIME_MS = 8 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
export function createSpeechAuthorizationProvider(
|
|
29
|
+
options: SpeechAuthenticationOptions,
|
|
30
|
+
): SpeechAuthorizationProvider {
|
|
31
|
+
const hasTokenEndpoint = typeof options.tokenEndpoint === 'string' &&
|
|
32
|
+
options.tokenEndpoint.trim() !== '';
|
|
33
|
+
const hasProvider = typeof options.getAuthorizationToken === 'function';
|
|
34
|
+
|
|
35
|
+
if (hasTokenEndpoint && hasProvider) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Provide either tokenEndpoint or getAuthorizationToken, not both.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!hasTokenEndpoint && !hasProvider) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'A tokenEndpoint or getAuthorizationToken provider is required.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hasProvider) {
|
|
48
|
+
return options.getAuthorizationToken as SpeechAuthorizationProvider;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tokenEndpoint = (options.tokenEndpoint as string).trim();
|
|
52
|
+
const requestOptions = options.tokenRequestOptions ?? {};
|
|
53
|
+
|
|
54
|
+
return async () => {
|
|
55
|
+
let response: Response;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
response = await fetch(tokenEndpoint, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
credentials: requestOptions.credentials ?? 'same-origin',
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
...(requestOptions.headers ?? {}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
throw createSafeError(
|
|
68
|
+
'Could not reach the speech token endpoint.',
|
|
69
|
+
'TOKEN_ENDPOINT_UNREACHABLE',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw createSafeError(
|
|
75
|
+
`Speech token endpoint returned HTTP ${response.status}.`,
|
|
76
|
+
'TOKEN_ENDPOINT_HTTP_ERROR',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let payload: unknown;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
payload = await response.json();
|
|
84
|
+
} catch {
|
|
85
|
+
throw createSafeError(
|
|
86
|
+
'Speech token endpoint returned invalid JSON.',
|
|
87
|
+
'TOKEN_ENDPOINT_INVALID_JSON',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return validateSpeechAuthorization(payload);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateSpeechAuthorization(
|
|
96
|
+
value: unknown,
|
|
97
|
+
): SpeechAuthorization {
|
|
98
|
+
if (!value || typeof value !== 'object') {
|
|
99
|
+
throw createSafeError(
|
|
100
|
+
'Speech authorization response must be an object.',
|
|
101
|
+
'TOKEN_INVALID_RESPONSE',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const candidate = value as Partial<SpeechAuthorization>;
|
|
106
|
+
const token = typeof candidate.token === 'string' ? candidate.token.trim() : '';
|
|
107
|
+
const region = typeof candidate.region === 'string' ?
|
|
108
|
+
candidate.region.trim() :
|
|
109
|
+
'';
|
|
110
|
+
|
|
111
|
+
if (token === '') {
|
|
112
|
+
throw createSafeError(
|
|
113
|
+
'Speech authorization token is missing or empty.',
|
|
114
|
+
'TOKEN_EMPTY',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (region === '') {
|
|
119
|
+
throw createSafeError(
|
|
120
|
+
'Speech authorization region is missing or empty.',
|
|
121
|
+
'TOKEN_REGION_MISSING',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {token, region};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createSafeError(
|
|
129
|
+
message: string,
|
|
130
|
+
code?: string,
|
|
131
|
+
): Error & SpeechSafeError {
|
|
132
|
+
const error = new Error(message) as Error & SpeechSafeError;
|
|
133
|
+
error.message = message;
|
|
134
|
+
if (code) {
|
|
135
|
+
error.code = code;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function toSafeErrorDetail(error: unknown): SpeechSafeError {
|
|
142
|
+
if (error && typeof error === 'object') {
|
|
143
|
+
const candidate = error as {message?: unknown; code?: unknown};
|
|
144
|
+
const message = typeof candidate.message === 'string' &&
|
|
145
|
+
candidate.message.trim() !== '' ?
|
|
146
|
+
sanitizeErrorText(candidate.message) :
|
|
147
|
+
'An unexpected speech error occurred.';
|
|
148
|
+
const detail: SpeechSafeError = {message};
|
|
149
|
+
|
|
150
|
+
if (typeof candidate.code === 'string' && candidate.code.trim() !== '') {
|
|
151
|
+
detail.code = candidate.code;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return detail;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
message: 'An unexpected speech error occurred.',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sanitizeErrorText(message: string): string {
|
|
163
|
+
return message
|
|
164
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
|
|
165
|
+
.replace(/token["']?\s*[:=]\s*["']?[^"',\s}]+/gi, 'token=[redacted]');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fetches, validates, caches and refreshes temporary Azure Speech tokens.
|
|
170
|
+
* Never logs authorization tokens.
|
|
171
|
+
*/
|
|
172
|
+
export class SpeechAuthorizationManager {
|
|
173
|
+
private authorization?: SpeechAuthorization;
|
|
174
|
+
private authorizationExpiresAt = 0;
|
|
175
|
+
private authorizationRequest?: Promise<SpeechAuthorization>;
|
|
176
|
+
private readonly provider: SpeechAuthorizationProvider;
|
|
177
|
+
private readonly tokenLifetimeMs: number;
|
|
178
|
+
|
|
179
|
+
constructor(options: SpeechAuthenticationOptions) {
|
|
180
|
+
this.provider = createSpeechAuthorizationProvider(options);
|
|
181
|
+
this.tokenLifetimeMs = options.tokenLifetimeMs && options.tokenLifetimeMs > 0 ?
|
|
182
|
+
options.tokenLifetimeMs :
|
|
183
|
+
DEFAULT_TOKEN_LIFETIME_MS;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getAuthorization(): Promise<SpeechAuthorization> {
|
|
187
|
+
if (this.authorization && Date.now() < this.authorizationExpiresAt) {
|
|
188
|
+
return this.authorization;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.authorizationRequest) {
|
|
192
|
+
return this.authorizationRequest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.authorizationRequest = this.fetchAuthorization()
|
|
196
|
+
.then((authorization) => {
|
|
197
|
+
this.authorization = authorization;
|
|
198
|
+
this.authorizationExpiresAt = Date.now() + this.tokenLifetimeMs;
|
|
199
|
+
|
|
200
|
+
return authorization;
|
|
201
|
+
})
|
|
202
|
+
.finally(() => {
|
|
203
|
+
this.authorizationRequest = undefined;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return this.authorizationRequest;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
clearAuthorization(): void {
|
|
210
|
+
this.authorization = undefined;
|
|
211
|
+
this.authorizationExpiresAt = 0;
|
|
212
|
+
this.authorizationRequest = undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async fetchAuthorization(): Promise<SpeechAuthorization> {
|
|
216
|
+
try {
|
|
217
|
+
const authorization = await this.provider();
|
|
218
|
+
|
|
219
|
+
return validateSpeechAuthorization(authorization);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.clearAuthorization();
|
|
222
|
+
throw createSafeError(
|
|
223
|
+
toSafeErrorDetail(error).message,
|
|
224
|
+
(error as SpeechSafeError)?.code,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/main.ts
CHANGED
package/.eslintrc.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
'env': {
|
|
3
|
-
'browser': true,
|
|
4
|
-
'es6': true
|
|
5
|
-
},
|
|
6
|
-
'extends': [
|
|
7
|
-
'eslint:recommended',
|
|
8
|
-
'plugin:@typescript-eslint/eslint-recommended',
|
|
9
|
-
'google'
|
|
10
|
-
],
|
|
11
|
-
'rules': {
|
|
12
|
-
"no-console": "off",
|
|
13
|
-
'max-len': ['error', {'code': 125}],
|
|
14
|
-
'indent': ['error', 4],
|
|
15
|
-
'require-jsdoc': ['error', {
|
|
16
|
-
'require': {
|
|
17
|
-
'FunctionDeclaration': false,
|
|
18
|
-
'MethodDefinition': false,
|
|
19
|
-
'ClassDeclaration': false,
|
|
20
|
-
'ArrowFunctionExpression': false,
|
|
21
|
-
'FunctionExpression': false
|
|
22
|
-
}
|
|
23
|
-
}],
|
|
24
|
-
},
|
|
25
|
-
'parserOptions': {
|
|
26
|
-
'sourceType': 'module',
|
|
27
|
-
},
|
|
28
|
-
'parser': '@typescript-eslint/parser',
|
|
29
|
-
"ignorePatterns": ["**/*.html", "**/*.scss"],
|
|
30
|
-
}
|