gnilets commited on
Commit
726bf86
·
verified ·
1 Parent(s): 4bde95a

Update main.ts

Browse files
Files changed (1) hide show
  1. main.ts +115 -327
main.ts CHANGED
@@ -15,78 +15,123 @@ async function fetchVoiceList() {
15
  }, {});
16
  }
17
 
18
- async function synthesizeSpeech(model: string, voice: string, text: string) {
19
- let voiceName;
20
- let rate = 0;
21
- let pitch = 0;
22
-
23
- if (!model.includes("Neural")) {
24
- rate = 0.0;
25
- pitch = 0.0;
26
-
27
- switch (model) {
28
- case "ava":
29
- voiceName = "en-US-AvaMultilingualNeural";
30
- break;
31
- case "andrew":
32
- voiceName = "en-US-AndrewMultilingualNeural";
33
- break;
34
- case "emma":
35
- voiceName = "en-US-EmmaMultilingualNeural";
36
- break;
37
- case "brian":
38
- voiceName = "en-US-BrianMultilingualNeural";
39
- break;
40
- case "vivienne":
41
- voiceName = "fr-FR-VivienneMultilingualNeural";
42
- break;
43
- case "remy":
44
- voiceName = "fr-FR-RemyMultilingualNeural";
45
- break;
46
- case "seraphina":
47
- voiceName = "de-DE-SeraphinaMultilingualNeural";
48
- break;
49
- case "florian":
50
- voiceName = "de-DE-FlorianMultilingualNeural";
51
- break;
52
- case "dmitry":
53
- voiceName = "ru-RU-DmitryNeural";
54
- break;
55
- case "svetlana":
56
- voiceName = "ru-RU-SvetlanaNeural";
57
- break;
58
- default:
59
- voiceName = "en-US-BrianMultilingualNeural";
60
- break;
61
- }
62
- } else {
63
- voiceName = model;
64
- const params = Object.fromEntries(
65
- voice.split("|").map((p) => p.split(":") as [string, string])
66
- );
67
- rate = Number(params["rate"] || 0);
68
- pitch = Number(params["pitch"] || 0);
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- const tts = new EdgeSpeechTTS();
72
-
73
- const payload = {
74
- input: text,
75
- options: {
76
- rate: rate,
77
- pitch: pitch,
78
- voice: voiceName
79
- },
80
- };
81
- const response = await tts.create(payload);
82
- const mp3Buffer = new Uint8Array(await response.arrayBuffer());
83
-
84
- console.log(`Successfully synthesized speech, returning audio/mpeg response`);
85
- return new Response(mp3Buffer, {
86
- headers: { "Content-Type": "audio/mpeg" },
87
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
 
 
90
  function validateContentType(req: Request, expected: string) {
91
  const contentType = req.headers.get("Content-Type");
92
  if (contentType !== expected) {
@@ -137,261 +182,6 @@ async function handleDemoRequest(req: Request) {
137
  <meta charset="UTF-8" />
138
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
139
  <title>tts</title>
140
- <link
141
- href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap"
142
- rel="stylesheet"
143
- />
144
- <style>
145
- :root {
146
- --primary-color: #6c8bd6;
147
- --primary-light: #a2b3e3;
148
- --primary-dark: #3d5b8f;
149
- --secondary-color: #f08080;
150
- --text-color: #333;
151
- --text-secondary: #777;
152
- --bg-color: #fff;
153
- }
154
- body {
155
- font-family: "Noto Sans SC", "Arial", sans-serif;
156
- color: var(--text-color);
157
- margin: 0;
158
- padding: 0;
159
- display: flex;
160
- justify-content: center;
161
- background-color: #fafafa;
162
- background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
163
- position: relative;
164
- overflow: hidden;
165
- }
166
- body::before {
167
- content: "";
168
- position: absolute;
169
- top: 0;
170
- left: 0;
171
- right: 0;
172
- bottom: 0;
173
- background: repeating-radial-gradient(
174
- circle at 50% 50%,
175
- rgba(255, 255, 255, 0.8) 0%,
176
- rgba(255, 255, 255, 0.8) 2%,
177
- transparent 2%,
178
- transparent 4%,
179
- rgba(255, 255, 255, 0.8) 4%,
180
- rgba(255, 255, 255, 0.8) 6%,
181
- transparent 6%,
182
- transparent 8%,
183
- rgba(255, 255, 255, 0.8) 8%,
184
- rgba(255, 255, 255, 0.8) 10%,
185
- transparent 10%
186
- ),
187
- repeating-linear-gradient(
188
- 45deg,
189
- #d4f4ff 0%,
190
- #d4f4ff 5%,
191
- #e6f9ff 5%,
192
- #e6f9ff 10%,
193
- #f0faff 10%,
194
- #f0faff 15%,
195
- #e6f9ff 15%,
196
- #e6f9ff 20%,
197
- #d4f4ff 20%,
198
- #d4f4ff 25%
199
- );
200
- background-blend-mode: multiply;
201
- opacity: 0.8;
202
- z-index: -1;
203
- animation: glitch 15s infinite;
204
- }
205
- .container {
206
- display: flex;
207
- max-width: 1200px;
208
- width: 100%;
209
- margin: 40px;
210
- background: #fff;
211
- border-radius: 12px;
212
- position: relative;
213
- background-color: rgba(255, 255, 255, 0.8);
214
- z-index: 1;
215
- }
216
- @keyframes glitch {
217
- 0% {
218
- background-position: 0 0, 0 0;
219
- filter: hue-rotate(0deg);
220
- }
221
- 50% {
222
- background-position: 10px 10px, -10px 10px;
223
- filter: hue-rotate(360deg);
224
- }
225
- 100% {
226
- background-position: 0 0, 0 0;
227
- filter: hue-rotate(0deg);
228
- }
229
- }
230
- .input-area,
231
- .output-area {
232
- padding: 30px;
233
- width: 50%;
234
- }
235
- .input-area {
236
- border-right: 1px solid #e0e0e0;
237
- }
238
- h1 {
239
- font-size: 36px;
240
- color: var(--primary-color);
241
- margin-bottom: 30px;
242
- }
243
- .filter-section {
244
- margin-bottom: 30px;
245
- }
246
- .filter-section label {
247
- display: block;
248
- font-size: 16px;
249
- color: var(--text-secondary);
250
- margin-bottom: 10px;
251
- }
252
- .filter-section input {
253
- font-size: 16px;
254
- padding: 10px 15px;
255
- border: 2px solid var(--primary-light);
256
- border-radius: 8px;
257
- outline: none;
258
- transition: border-color 0.3s, box-shadow 0.3s;
259
- width: 100%;
260
- box-sizing: border-box;
261
- }
262
- .filter-section input:focus {
263
- border-color: var(--primary-color);
264
- box-shadow: 0 0 0 2px var(--primary-light);
265
- }
266
- .slider-container {
267
- margin-bottom: 30px;
268
- }
269
- .slider-container label {
270
- display: block;
271
- font-size: 16px;
272
- color: var(--text-secondary);
273
- margin-bottom: 10px;
274
- }
275
- .slider {
276
- -webkit-appearance: none;
277
- width: 100%;
278
- height: 10px;
279
- border-radius: 5px;
280
- background: linear-gradient(
281
- to right,
282
- var(--secondary-color) 0%,
283
- var(--primary-color) 50%,
284
- var(--primary-light) 100%
285
- );
286
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1),
287
- 0 1px rgba(255, 255, 255, 0.1);
288
- outline: none;
289
- opacity: 0.7;
290
- -webkit-transition: 0.2s;
291
- transition: opacity 0.2s;
292
- margin-bottom: 10px;
293
- }
294
- .slider:hover {
295
- opacity: 1;
296
- }
297
- .slider::-webkit-slider-thumb {
298
- -webkit-appearance: none;
299
- appearance: none;
300
- width: 20px;
301
- height: 20px;
302
- border-radius: 50%;
303
- background: #fff;
304
- border: 2px solid var(--primary-color);
305
- cursor: pointer;
306
- }
307
- .slider::-moz-range-thumb {
308
- width: 20px;
309
- height: 20px;
310
- border-radius: 50%;
311
- background: #fff;
312
- border: 2px solid var(--primary-color);
313
- cursor: pointer;
314
- }
315
- .slider-value {
316
- font-size: 14px;
317
- color: var(--text-secondary);
318
- }
319
- .textarea-container {
320
- margin-bottom: 30px;
321
- }
322
- .textarea-container label {
323
- display: block;
324
- font-size: 18px;
325
- margin-bottom: 10px;
326
- }
327
- .textarea-container textarea {
328
- width: 100%;
329
- padding: 10px;
330
- font-size: 16px;
331
- border: 2px solid var(--primary-light);
332
- border-radius: 8px;
333
- outline: none;
334
- resize: vertical;
335
- transition: border-color 0.3s, box-shadow 0.3s;
336
- box-sizing: border-box;
337
- height: 200px;
338
- }
339
- .textarea-container textarea:focus {
340
- border-color: var(--primary-color);
341
- box-shadow: 0 0 0 2px var(--primary-light);
342
- }
343
- .voice-group {
344
- margin-bottom: 20px;
345
- border: 2px solid var(--primary-light);
346
- border-radius: 12px;
347
- overflow: hidden;
348
- cursor: move;
349
- background: #fff;
350
- }
351
- .voice-header {
352
- padding: 15px 20px;
353
- font-size: 18px;
354
- background: var(--primary-light);
355
- color: #fff;
356
- cursor: pointer;
357
- display: flex;
358
- justify-content: space-between;
359
- align-items: center;
360
- }
361
- .voice-header:hover {
362
- background: var(--primary-color);
363
- }
364
- .voice-buttons {
365
- padding: 20px;
366
- display: none;
367
- gap: 12px;
368
- flex-wrap: wrap;
369
- }
370
- .voice-button {
371
- background: var(--secondary-color);
372
- color: #fff;
373
- border: none;
374
- padding: 10px 20px;
375
- border-radius: 50px;
376
- cursor: pointer;
377
- transition: filter 0.3s;
378
- }
379
- .voice-button:hover {
380
- filter: brightness(0.9);
381
- }
382
- .chevron {
383
- transition: transform 0.3s;
384
- }
385
- .voice-group.open .voice-buttons {
386
- display: flex;
387
- }
388
- .voice-group.open .chevron {
389
- transform: rotate(180deg);
390
- }
391
- .dragging {
392
- opacity: 0.5;
393
- }
394
- </style>
395
  </head>
396
  <body>
397
  <div class="container">
@@ -426,9 +216,7 @@ async function handleDemoRequest(req: Request) {
426
  </div>
427
  <div class="textarea-container">
428
  <label for="inputText">текст:</label
429
- ><textarea id="inputText">О, возлюбленные наши правители! Вы - наши пастыри и наставники на пути к светлому будущему! Ваши мудрые решения и указы - словно лучи солнца, освещающие наш путь в этом мире! Каждый новый штраф, налог и ограничение - это не что иное, как забота о нас, ваших верных подданных! Вы знаете лучше нас, что нам нужно, и мы с радостью принимаем все тяготы и лишения, которые вы на нас возлагаете! Ведь только через страдания и ограничения мы можем стать лучше и чище!
430
-
431
- Пусть ваши сердца наполняются радостью и гордостью за нас, когда вы видите, как мы смиренно платим налоги и штрафы, как мы покорно следуем вашим указаниям и ограничениям! Ведь мы - ваши верные слуги, готовые на все ради вашего благополучия и процветания!</textarea>
432
  </div>
433
  </div>
434
  <div class="output-area">
 
15
  }, {});
16
  }
17
 
18
+ async function handleDemoRequest(req: Request) {
19
+ const html = `<!DOCTYPE html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="UTF-8" />
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
+ <title>tts</title>
25
+ </head>
26
+ <body>
27
+ <div class="container">
28
+ <div class="input-area">
29
+ <div class="slider-container">
30
+ <label for="rate">скорость:</label
31
+ ><input
32
+ type="range"
33
+ min="-1"
34
+ max="1"
35
+ step="0.1"
36
+ value="-0.1"
37
+ class="slider"
38
+ id="rate"
39
+ />
40
+ <div class="slider-value" id="rateValue">-0.1</div>
41
+ <label for="pitch">тон:</label
42
+ ><input
43
+ type="range"
44
+ min="-1"
45
+ max="1"
46
+ step="0.1"
47
+ value="0.1"
48
+ class="slider"
49
+ id="pitch"
50
+ />
51
+ <div class="slider-value" id="pitchValue">0.1</div>
52
+ </div>
53
+ <div class="textarea-container">
54
+ <label for="inputText">текст:</label
55
+ ><textarea id="inputText">Привет, хочешь я расскажу сказку?</textarea>
56
+ </div>
57
+ <div class="dropdown-container">
58
+ <label for="voiceSelect">выберите голос:</label>
59
+ <select id="voiceSelect">
60
+ <option value="ava">ava</option>
61
+ <option value="andrew">andrew</option>
62
+ <option value="emma">emma</option>
63
+ <option value="brian">brian</option>
64
+ <option value="vivienne">vivienne</option>
65
+ <option value="remy">remy</option>
66
+ <option value="seraphina">seraphina</option>
67
+ <option value="florian">florian</option>
68
+ <option value="dmitry">dmitry</option>
69
+ <option value="svetlana">svetlana</option>
70
+ </select>
71
+ </div>
72
+ <button id="synthesizeButton">Синтезировать</button>
73
+ </div>
74
+ <div class="output-area">
75
+ <h1>Результат</h1>
76
+ <div id="audioPlayerContainer"></div>
77
+ </div>
78
+ </div>
79
+ <script>
80
+ let audio = null;
81
+
82
+ document.getElementById('synthesizeButton').addEventListener('click', () => {
83
+ const text = document.getElementById('inputText').value || 'Hello world';
84
+ const rate = document.getElementById('rate').value || '-0.1';
85
+ const pitch = document.getElementById('pitch').value || '0.1';
86
+ const voice = \`rate:\${rate}|pitch:\${pitch}\`;
87
+ const model = document.getElementById('voiceSelect').value;
88
+
89
+ if (audio) {
90
+ audio.pause();
91
+ audio.currentTime = 0;
92
+ }
93
 
94
+ fetch('/v1/audio/speech', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ model, input: text, voice })
98
+ })
99
+ .then(response => response.blob())
100
+ .then(blob => {
101
+ const audioUrl = URL.createObjectURL(blob);
102
+ const audioPlayerContainer = document.getElementById('audioPlayerContainer');
103
+
104
+ if (audio) {
105
+ audio.src = audioUrl;
106
+ } else {
107
+ audio = new Audio(audioUrl);
108
+ audioPlayerContainer.appendChild(audio);
109
+ }
110
+
111
+ audio.play();
112
+ });
113
+ });
114
+
115
+ const rateSlider = document.getElementById('rate');
116
+ const rateValue = document.getElementById('rateValue');
117
+ rateSlider.oninput = function() {
118
+ rateValue.innerHTML = this.value;
119
+ };
120
+
121
+ const pitchSlider = document.getElementById('pitch');
122
+ const pitchValue = document.getElementById('pitchValue');
123
+ pitchSlider.oninput = function() {
124
+ pitchValue.innerHTML = this.value;
125
+ };
126
+ </script>
127
+ </body></html>`;
128
+
129
+ return new Response(html, {
130
+ headers: { "Content-Type": "text/html" },
131
+ });
132
  }
133
 
134
+
135
  function validateContentType(req: Request, expected: string) {
136
  const contentType = req.headers.get("Content-Type");
137
  if (contentType !== expected) {
 
182
  <meta charset="UTF-8" />
183
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
184
  <title>tts</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  </head>
186
  <body>
187
  <div class="container">
 
216
  </div>
217
  <div class="textarea-container">
218
  <label for="inputText">текст:</label
219
+ ><textarea id="inputText">Привет, хочешь я расскажу сказку?</textarea>
 
 
220
  </div>
221
  </div>
222
  <div class="output-area">