@creativeorange/azure-text-to-speech 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/src/main.ts ADDED
@@ -0,0 +1,223 @@
1
+ import { SpeakerAudioDestination, AudioConfig, OutputFormat, Recognizer, SpeechConfig, SpeechRecognizer, SpeechSynthesizer } from 'microsoft-cognitiveservices-speech-sdk';
2
+
3
+ export default class TextToSpeech {
4
+
5
+ key: string;
6
+ region: string;
7
+ voice: string;
8
+
9
+ textToRead: string = '';
10
+
11
+ wordBoundryList: any[] = [];
12
+
13
+ clickedNode: any;
14
+ highlightDiv: any;
15
+
16
+ speechConfig: any;
17
+ audioConfig: any;
18
+ player: any;
19
+ synthesizer: any;
20
+
21
+ previousWordBoundary: any;
22
+
23
+
24
+ constructor(key: string, region: string, voice: string = 'male') {
25
+ this.key = key;
26
+ this.region = region;
27
+ this.voice = voice;
28
+ }
29
+
30
+ async start() {
31
+ setInterval(() => {
32
+ if (this.player !== undefined && this.highlightDiv) {
33
+ const currentTime = this.player.currentTime;
34
+ let wordBoundary;
35
+ for (const e of this.wordBoundryList) {
36
+ if (currentTime * 1000 > e.audioOffset / 10000) {
37
+ wordBoundary = e;
38
+ } else {
39
+ break;
40
+ }
41
+ }
42
+ if (~['.', ',', '!', '?'].indexOf(wordBoundary.text)) {
43
+ wordBoundary = this.previousWordBoundary ?? undefined;
44
+ }
45
+
46
+ if (wordBoundary !== undefined) {
47
+ this.previousWordBoundary = wordBoundary;
48
+ this.highlightDiv.innerHTML = this.textToRead.substr(0, wordBoundary.textOffset) +
49
+ "<span class='co-tts-highlight'>" + wordBoundary.text + "</span>" +
50
+ this.textToRead.substr(wordBoundary.textOffset + wordBoundary.wordLength);
51
+ } else {
52
+ this.highlightDiv.innerHTML = this.textToRead;
53
+ }
54
+ }
55
+ }, 50);
56
+
57
+ await this.registerBindings(document);
58
+ }
59
+
60
+ async synthesis() {
61
+
62
+ }
63
+
64
+ async registerBindings(node: any) {
65
+ let nodes = node.childNodes;
66
+ for (let i = 0; i < nodes.length; i++) {
67
+ if (!nodes[i]) {
68
+ continue;
69
+ }
70
+
71
+ let currentNode = nodes[i];
72
+
73
+ if (currentNode.attributes) {
74
+ if (currentNode.attributes.getNamedItem('co-tts.id')) {
75
+ await this.handleIdModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.id'));
76
+ } else if (currentNode.attributes.getNamedItem('co-tts.ajax')) {
77
+ await this.handleAjaxModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.ajax'));
78
+ } else if (currentNode.attributes.getNamedItem('co-tts')) {
79
+ await this.handleDefault(currentNode, currentNode.attributes.getNamedItem('co-tts'));
80
+ } else if (currentNode.attributes.getNamedItem('co-tts.stop')) {
81
+ await this.handleStopModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.stop'));
82
+ } else if (currentNode.attributes.getNamedItem('co-tts.resume')) {
83
+ await this.handleResumeModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.resume'));
84
+ } else if (currentNode.attributes.getNamedItem('co-tts.pause')) {
85
+ await this.handlePauseModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.pause'));
86
+ }
87
+ }
88
+
89
+ if (currentNode.childNodes.length > 0) {
90
+ await this.registerBindings(currentNode);
91
+ }
92
+ }
93
+ }
94
+
95
+ async handleIdModifier(node: any, attr: Attr) {
96
+ node.addEventListener('click', async (_: any) => {
97
+ this.stopPlayer();
98
+ this.clickedNode = node;
99
+ let referenceDiv = document.getElementById(attr.value);
100
+
101
+ if (!referenceDiv) {
102
+ return;
103
+ }
104
+
105
+ if (referenceDiv.hasAttribute('co-tts.text') && referenceDiv.getAttribute('co-tts.text') !== '') {
106
+ this.textToRead = referenceDiv.getAttribute('co-tts.text') ?? '';
107
+ } else {
108
+ this.textToRead = referenceDiv.innerText;
109
+ }
110
+
111
+ if (referenceDiv.hasAttribute('co-tts.highlight')) {
112
+ this.highlightDiv = referenceDiv;
113
+ }
114
+
115
+ this.startSynthesizer(node, attr);
116
+ });
117
+ }
118
+
119
+ async handleAjaxModifier(node: any, attr: Attr) {
120
+ node.addEventListener('click', async (_: any) => {
121
+ this.stopPlayer();
122
+ this.clickedNode = node;
123
+ let response = await fetch(attr.value, {
124
+ method: `GET`,
125
+ });
126
+
127
+ this.textToRead = await response.text();
128
+
129
+ this.startSynthesizer(node, attr);
130
+ })
131
+ }
132
+
133
+ async handleDefault(node: any, attr: Attr) {
134
+ node.addEventListener('click', async (_: any) => {
135
+ this.stopPlayer();
136
+ this.clickedNode = node;
137
+ if (node.hasAttribute('co-tts.highlight')) {
138
+ this.highlightDiv = node;
139
+ }
140
+ if (attr.value === "") {
141
+ this.textToRead = node.innerText;
142
+ } else {
143
+ this.textToRead = attr.value;
144
+ }
145
+
146
+ this.startSynthesizer(node, attr);
147
+ });
148
+ }
149
+
150
+ async handleStopModifier(node: any, attr: Attr) {
151
+ node.addEventListener('click', async (_: any) => {
152
+ await this.stopPlayer();
153
+ });
154
+ }
155
+
156
+ async handlePauseModifier(node: any, attr: Attr) {
157
+ node.addEventListener('click', async (_: any) => {
158
+ await this.player.pause();
159
+ });
160
+ }
161
+
162
+ async handleResumeModifier(node: any, attr: Attr) {
163
+ node.addEventListener('click', async (_: any) => {
164
+ await this.player.resume();
165
+ });
166
+ }
167
+
168
+ async stopPlayer() {
169
+ if (this.highlightDiv !== undefined) {
170
+ this.highlightDiv.innerHTML = this.textToRead;
171
+ }
172
+
173
+ this.textToRead = '';
174
+ this.wordBoundryList = [];
175
+ if (this.player !== undefined) {
176
+ this.player.pause();
177
+ }
178
+ this.player = undefined;
179
+ this.highlightDiv = undefined;
180
+ }
181
+
182
+ async startSynthesizer (node: any, attr: Attr) {
183
+ this.speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
184
+
185
+ if (this.voice === 'female') {
186
+ this.speechConfig.speechSynthesisVoiceName = 'Microsoft Server Speech Text to Speech Voice (nl-NL, ColetteNeural)';
187
+ } else {
188
+ this.speechConfig.speechSynthesisVoiceName = 'Microsoft Server Speech Text to Speech Voice (nl-NL, MaartenNeural)';
189
+ }
190
+ this.speechConfig.speechSynthesisOutputFormat = 8;
191
+
192
+ this.player = new SpeakerAudioDestination();
193
+
194
+ this.audioConfig = AudioConfig.fromSpeakerOutput(this.player);
195
+ this.synthesizer = new SpeechSynthesizer(this.speechConfig, this.audioConfig);
196
+
197
+ this.synthesizer.wordBoundary = (s: any, e: any) => {
198
+ this.wordBoundryList.push(e);
199
+ };
200
+
201
+ this.player.onAudioEnd = () => {
202
+ this.stopPlayer();
203
+
204
+ if (this.clickedNode.hasAttribute('co-tts.next')) {
205
+ let nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
206
+ if (nextNode) {
207
+ nextNode.dispatchEvent(new Event('click'));
208
+ }
209
+ }
210
+ };
211
+
212
+ this.synthesizer.speakTextAsync(this.textToRead,
213
+ () => {
214
+ this.synthesizer.close();
215
+ this.synthesizer = undefined;
216
+ },
217
+ () => {
218
+ this.synthesizer.close();
219
+ this.synthesizer = undefined;
220
+ });
221
+ }
222
+
223
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ESNext", "DOM"],
7
+ "moduleResolution": "Node",
8
+ "strict": true,
9
+ "sourceMap": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "noEmit": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noImplicitReturns": true,
17
+ "skipLibCheck": true
18
+ },
19
+ "include": ["src"]
20
+ }
21
+
package/vite.config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vite';
2
+ import path from 'path';
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ build: {
7
+ lib: {
8
+ entry: './src/main.ts',
9
+ name: 'CreativeOrangeAzureTextToSpeech',
10
+ fileName: 'co-azure-tts',
11
+ },
12
+ outDir: 'dist',
13
+ rollupOptions: {
14
+ input: "./src/main.ts",
15
+ output: {
16
+ dir: "./dist",
17
+ },
18
+ }
19
+ }
20
+ })