Skip to content

Commit d0e0b53

Browse files
author
mad-gamer22
authored
Changes to Web Frontend (#6)
Added a search feature to the coagulator web frontend, as well as a feature to download the synthesized audio. Also, to preview voices now, a preview button has been added instead of clicking on the dropdown, since some people may be on phones.
1 parent 135b317 commit d0e0b53

File tree

1 file changed

+210
-41
lines changed

1 file changed

+210
-41
lines changed

coagulator/coagulator_index.html

Lines changed: 210 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,155 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>STAR Coagulator</title>
77
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
8+
<style>
9+
body {
10+
font-family: Arial, sans-serif;
11+
margin: 0;
12+
padding: 0;
13+
background-color: #f4f4f4;
14+
}
15+
16+
.container {
17+
width: 80%;
18+
max-width: 1000px;
19+
margin: 40px auto;
20+
background-color: #fff;
21+
padding: 20px;
22+
border-radius: 8px;
23+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
24+
}
25+
26+
h1 {
27+
text-align: center;
28+
color: #333;
29+
margin-bottom: 20px;
30+
}
31+
32+
h2 {
33+
color: #555;
34+
margin-top: 30px;
35+
}
36+
37+
p, a {
38+
font-size: 16px;
39+
color: #333;
40+
}
41+
42+
a {
43+
color: #007BFF;
44+
text-decoration: none;
45+
}
46+
47+
a:hover {
48+
text-decoration: underline;
49+
}
50+
51+
form {
52+
display: flex;
53+
flex-direction: column;
54+
margin-top: 20px;
55+
}
56+
57+
label {
58+
margin-bottom: 8px;
59+
font-weight: bold;
60+
}
61+
62+
select, input[type="text"], button {
63+
padding: 10px;
64+
font-size: 16px;
65+
margin-bottom: 20px;
66+
border-radius: 5px;
67+
border: 1px solid #ccc;
68+
outline: none;
69+
}
70+
71+
select:focus, input[type="text"]:focus, button:focus {
72+
border-color: #007BFF;
73+
}
74+
75+
button {
76+
background-color: #007BFF;
77+
color: white;
78+
cursor: pointer;
79+
border: none;
80+
}
81+
82+
button:hover {
83+
background-color: #0056b3;
84+
}
85+
86+
#liveRegion {
87+
margin-top: 20px;
88+
color: red;
89+
font-weight: bold;
90+
}
91+
92+
audio {
93+
display: block;
94+
width: 100%;
95+
margin-top: 20px;
96+
border-radius: 8px;
97+
}
98+
99+
details {
100+
margin-top: 30px;
101+
padding: 15px;
102+
background-color: #f9f9f9;
103+
border-radius: 8px;
104+
}
105+
106+
.search-container {
107+
display: flex;
108+
align-items: center;
109+
margin-bottom: 15px;
110+
}
111+
112+
#voiceSearch {
113+
flex: 1;
114+
}
115+
</style>
8116
</head>
9117
<body>
10-
<h1>STAR coagulator</h1>
11-
<p>Hello {{username}}, welcome to the frontend for this <a href="https://github.com/samtupy/star">STAR</a> coagulator which currently has {{voicecount}} voices online.</p>
12-
<details><summary>API</summary>
13-
<p>This web service contains 2 API endpoints. You can query /voices to retrieve a list of connected voices in json format, or else /synthesize?voice=v&text=t to synthesize some text and retrieve the resulting audio data. Other parameters such as r=rate and p=pitch are accepted by the /synthesize endpoint as well. If you wish for a more complete interface, the standard websocket protocol is recommended in that case.</p>
14-
</details>
15-
<h2>Synthesis from your browser</h2>
16-
<form id="quickspeakForm">
17-
<label for="voiceSelect">Choose a voice:</label>
18-
<select id="voiceSelect">
19-
<option value="">--Select a voice--</option>
20-
</select>
21-
<br>
22-
<label for="textInput">Text to quickspeak:</label>
23-
<input type="text" id="textInput" placeholder="Enter text here">
24-
<br>
25-
<button type="button" id="synthesizeButton">Synthesize</button>
26-
</form>
27-
<div id="liveRegion" aria-live="polite" style="margin-top: 20px; color: red;"></div>
28-
<audio id="audioPlayer" controls style="display: none; margin-top: 20px;"></audio>
118+
<div class="container">
119+
<h1>STAR Coagulator</h1>
120+
<p>Hello {{username}}, welcome to the frontend for this <a href="https://github.com/samtupy/star">STAR</a> coagulator which currently has {{voicecount}} voices online.</p>
121+
122+
<details>
123+
<summary>API</summary>
124+
<p>This web service contains 2 API endpoints. You can query /voices to retrieve a list of connected voices in json format, or else /synthesize?voice=v&text=t to synthesize some text and retrieve the resulting audio data. Other parameters such as r=rate and p=pitch are accepted by the /synthesize endpoint as well. If you wish for a more complete interface, the standard websocket protocol is recommended in that case.</p>
125+
</details>
126+
127+
<h2>Synthesis from your browser</h2>
128+
<form id="quickspeakForm">
129+
<div class="search-container">
130+
<input type="text" id="voiceSearch" placeholder="Search for a voice..." aria-label="Search for a voice">
131+
</div>
132+
133+
<label for="voiceSelect">Choose a voice:</label>
134+
<select id="voiceSelect">
135+
<option value="">--Select a voice--</option>
136+
</select>
137+
138+
<button type="button" id="previewButton">Preview</button>
139+
140+
<label for="textInput">Text to quickspeak:</label>
141+
<input type="text" id="textInput" placeholder="Enter text here">
142+
143+
<button type="submit" id="synthesizeButton">Synthesize</button>
144+
</form>
145+
146+
<button type="button" id="downloadButton" style="display:none;">Download Audio</button>
147+
148+
<div id="liveRegion" aria-live="polite"></div>
149+
<audio id="audioPlayer" controls style="display: none;"></audio>
150+
</div>
29151

30152
<script>
31153
const audioCache = new Map();
32-
do_synthesis = function(textOverride) {
154+
let currentAudioUrl = '';
155+
let availableVoices = [];
156+
function do_synthesis(textOverride) {
33157
const selectedVoice = $('#voiceSelect').val();
34158
const inputText = textOverride || $('#textInput').val();
35159
if (!selectedVoice) {
@@ -41,11 +165,13 @@ <h2>Synthesis from your browser</h2>
41165
return;
42166
}
43167
const audioPlayer = $('#audioPlayer');
44-
const cacheKey = `${selectedVoice}:${inputText}`
168+
const cacheKey = `${selectedVoice}:${inputText}`;
45169
if (audioCache.has(cacheKey)) {
46-
audioPlayer.attr('src', audioCache.get(cacheKey));
170+
currentAudioUrl = audioCache.get(cacheKey);
171+
audioPlayer.attr('src', currentAudioUrl);
47172
audioPlayer.show();
48173
audioPlayer[0].play();
174+
$('#downloadButton').show();
49175
return;
50176
}
51177
$.ajax({
@@ -55,11 +181,12 @@ <h2>Synthesis from your browser</h2>
55181
if (xhr.status === 200) {
56182
const contentType = xhr.getResponseHeader('Content-Type');
57183
const blob = new Blob([data], { type: contentType });
58-
const audioUrl = URL.createObjectURL(blob);
59-
audioCache.set(cacheKey, audioUrl);
60-
audioPlayer.attr('src', audioUrl);
184+
currentAudioUrl = URL.createObjectURL(blob);
185+
audioCache.set(cacheKey, currentAudioUrl);
186+
audioPlayer.attr('src', currentAudioUrl);
61187
audioPlayer.show();
62188
audioPlayer[0].play();
189+
$('#downloadButton').show();
63190
} else {
64191
$('#liveRegion').text('Synthesis failed.');
65192
}
@@ -72,20 +199,37 @@ <h2>Synthesis from your browser</h2>
72199
}
73200
});
74201
}
75-
$(document).ready(function () {
76-
$('#quickspeakForm').on('submit', function (event) {
77-
event.preventDefault();
78-
do_synthesis();
202+
function populateVoiceList(searchQuery) {
203+
searchQuery = (searchQuery || '').toLowerCase()
204+
const voiceSelect = $('#voiceSelect');
205+
voiceSelect.empty().append(new Option('--Select a voice--', ''));
206+
availableVoices.forEach(voice => {
207+
if (searchQuery == '' || voice.toLowerCase().includes(searchQuery)) {
208+
voiceSelect.append(new Option(voice, voice));
209+
}
79210
});
211+
}
212+
function isDesktopPlatform() {
213+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
214+
215+
// Define some common mobile devices
216+
if (/windows phone/i.test(userAgent) ||
217+
/iphone|ipod|ipad/i.test(userAgent) ||
218+
/android/i.test(userAgent) ||
219+
/blackberry/i.test(userAgent)) {
220+
return false; // It's a mobile platform
221+
}
222+
return true; // It's likely a desktop platform
223+
}
224+
document.addEventListener('DOMContentLoaded', function () {
225+
// Load voices initially
80226
$.ajax({
81227
url: '/voices',
82228
method: 'GET',
83229
success: function (data) {
84230
if (data.voices && Array.isArray(data.voices)) {
85-
const voiceSelect = $('#voiceSelect');
86-
data.voices.forEach(voice => {
87-
voiceSelect.append(new Option(voice, voice));
88-
});
231+
availableVoices = data.voices;
232+
populateVoiceList();
89233
} else {
90234
$('#liveRegion').text('Failed to load voices.');
91235
}
@@ -94,18 +238,43 @@ <h2>Synthesis from your browser</h2>
94238
$('#liveRegion').text('Error fetching voices.');
95239
}
96240
});
97-
$('#synthesizeButton').on('click', function () {
241+
$('#quickspeakForm').on('submit', function (event) {
242+
event.preventDefault(); // Important to prevent form submission
98243
do_synthesis();
99244
});
100-
$('#voiceSelect').on('dblclick keypress', function (event) {
101-
if (event.type === 'dblclick' || (event.type === 'keypress' && event.key === 'Enter')) {
102-
const selectedVoice = $('#voiceSelect').val();
103-
if (selectedVoice) {
104-
do_synthesis(`Hello there, my name is ${selectedVoice}`);
105-
}
245+
$('#voiceSearch').on('input', function () {
246+
const searchQuery = $(this).val();
247+
populateVoiceList(searchQuery);
248+
});
249+
250+
$('#textInput').on('keypress', function (event) {
251+
if (event.key === 'Enter') {
252+
event.preventDefault();
253+
$('#synthesizeButton').click();
254+
}
255+
});
256+
$('#previewButton').on('click', function() {
257+
const selectedVoice = $('#voiceSelect').val();
258+
if (selectedVoice) {
259+
do_synthesis(`Hello there, my name is ${selectedVoice}.`);
260+
} else {
261+
$('#liveRegion').text('Please select a voice to preview.');
262+
}
263+
});
264+
$('#voiceSelect').on('dblclick keypress', function(event) {
265+
if (event.type === 'dblclick' && isDesktopPlatform() || event.type === 'keypress' && event.key === 'Enter') {
266+
$('#previewButton').click();
267+
}
268+
});
269+
$('#downloadButton').on('click', function () {
270+
if (currentAudioUrl) {
271+
const link = document.createElement('a');
272+
link.href = currentAudioUrl;
273+
link.download = 'speech'; // Hardcoded filename
274+
link.click();
106275
}
107276
});
108277
});
109278
</script>
110279
</body>
111-
</html>
280+
</html>

0 commit comments

Comments
 (0)