Toowired commited on
Commit
2e9fe73
·
verified ·
1 Parent(s): f97aa0a

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +566 -19
  3. prompts.txt +7 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Tts Reader
3
- emoji: 👀
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: tts-reader
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,566 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Google TTS Document Reader (BYOK)</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .dropzone {
11
+ border: 2px dashed #9CA3AF;
12
+ transition: all 0.3s ease;
13
+ }
14
+ .dropzone.active {
15
+ border-color: #3B82F6;
16
+ background-color: #EFF6FF;
17
+ }
18
+ .document-content {
19
+ max-height: 60vh;
20
+ overflow-y: auto;
21
+ }
22
+ .highlight {
23
+ background-color: #FEF08A;
24
+ transition: background-color 0.2s;
25
+ border-radius: 3px;
26
+ padding: 0 2px;
27
+ }
28
+ .voice-card {
29
+ transition: all 0.2s ease;
30
+ border: 1px solid #E5E7EB;
31
+ }
32
+ .voice-card:hover {
33
+ border-color: #3B82F6;
34
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
35
+ }
36
+ .voice-card.selected {
37
+ border-color: #3B82F6;
38
+ background-color: #EFF6FF;
39
+ }
40
+ .progress-chunk {
41
+ height: 6px;
42
+ background-color: #3B82F6;
43
+ border-radius: 3px;
44
+ margin-right: 2px;
45
+ }
46
+ .loading-spinner {
47
+ border: 3px solid rgba(255, 255, 255, 0.3);
48
+ border-radius: 50%;
49
+ border-top: 3px solid #3B82F6;
50
+ width: 20px;
51
+ height: 20px;
52
+ animation: spin 1s linear infinite;
53
+ }
54
+ @keyframes spin {
55
+ 0% { transform: rotate(0deg); }
56
+ 100% { transform: rotate(360deg); }
57
+ }
58
+ .api-key-input {
59
+ transition: all 0.3s ease;
60
+ }
61
+ .api-key-input:focus {
62
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
63
+ }
64
+ </style>
65
+ </head>
66
+ <body class="bg-gray-50 min-h-screen">
67
+ <div class="container mx-auto px-4 py-8">
68
+ <header class="text-center mb-8">
69
+ <h1 class="text-4xl font-bold text-blue-600 mb-2">Google TTS Document Reader</h1>
70
+ <p class="text-gray-600">Natural HD English voices with BYOK (Bring Your Own Key)</p>
71
+ </header>
72
+
73
+ <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden">
74
+ <!-- API Key Section -->
75
+ <div class="bg-blue-50 p-6 border-b border-blue-100">
76
+ <div class="flex flex-col md:flex-row items-center gap-4">
77
+ <div class="flex-1">
78
+ <label for="apiKey" class="block text-sm font-medium text-blue-800 mb-1">Google Cloud API Key</label>
79
+ <input type="password" id="apiKey" placeholder="Enter your Google Cloud API key"
80
+ class="api-key-input w-full px-4 py-2 border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
81
+ </div>
82
+ <button id="saveKeyBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-md transition whitespace-nowrap">
83
+ <i class="fas fa-save mr-2"></i> Save Key
84
+ </button>
85
+ </div>
86
+ <p class="text-xs text-blue-600 mt-2">
87
+ <i class="fas fa-info-circle mr-1"></i> Your API key is stored locally and never sent to our servers.
88
+ </p>
89
+ </div>
90
+
91
+ <!-- Input Section -->
92
+ <div class="p-6">
93
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
94
+ <button id="uploadBtn" class="bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
95
+ <i class="fas fa-upload mr-2"></i> Upload File
96
+ </button>
97
+ <button id="pasteBtn" class="bg-green-500 hover:bg-green-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
98
+ <i class="fas fa-paste mr-2"></i> Paste Text
99
+ </button>
100
+ <div id="dropzone" class="dropzone bg-gray-50 py-3 px-4 rounded-lg flex items-center justify-center cursor-pointer">
101
+ <div class="text-center">
102
+ <i class="fas fa-file-import text-2xl text-gray-400 mb-1"></i>
103
+ <p class="text-gray-500">Drag & Drop File</p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <input type="file" id="fileInput" class="hidden" accept=".txt,.pdf,.doc,.docx,.odt,.rtf">
109
+ </div>
110
+
111
+ <!-- Document Display -->
112
+ <div class="border-t border-gray-200 p-6">
113
+ <div class="flex justify-between items-center mb-4">
114
+ <h2 class="text-xl font-semibold text-gray-800">Document Content</h2>
115
+ <div class="text-sm text-gray-500" id="charCount">0 characters</div>
116
+ </div>
117
+
118
+ <div id="documentContent" class="document-content bg-gray-50 p-4 rounded-lg border border-gray-200 min-h-32">
119
+ <p class="text-gray-500 italic">Your document content will appear here...</p>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Voice Selection -->
124
+ <div class="border-t border-gray-200 p-6 bg-gray-50">
125
+ <div class="flex justify-between items-center mb-4">
126
+ <h2 class="text-xl font-semibold text-gray-800">Select Google TTS Voice</h2>
127
+ <div class="flex items-center gap-2">
128
+ <div class="relative">
129
+ <select id="languageSelect" class="bg-white border border-gray-300 rounded-md py-1 px-3 pr-8 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
130
+ <option value="en-US">English (US)</option>
131
+ <option value="en-GB">English (UK)</option>
132
+ <option value="en-AU">English (Australia)</option>
133
+ </select>
134
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
135
+ <i class="fas fa-chevron-down text-xs"></i>
136
+ </div>
137
+ </div>
138
+ <button id="refreshVoicesBtn" class="text-blue-500 hover:text-blue-700 text-sm flex items-center">
139
+ <i class="fas fa-sync-alt mr-1"></i> Refresh
140
+ </button>
141
+ </div>
142
+ </div>
143
+ <div id="voiceGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
144
+ <!-- Voices will be loaded dynamically -->
145
+ <div class="text-center py-8">
146
+ <div class="loading-spinner mx-auto"></div>
147
+ <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Voice Controls -->
153
+ <div class="bg-gray-100 p-6">
154
+ <div class="controls flex flex-wrap justify-between items-center gap-4 mb-4">
155
+ <div class="voice-options flex flex-wrap items-center gap-4">
156
+ <div>
157
+ <label for="rateSelect" class="block text-sm font-medium text-gray-700 mb-1">Speed</label>
158
+ <select id="rateSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
159
+ <option value="0.5">0.5x</option>
160
+ <option value="0.8">0.8x</option>
161
+ <option value="1" selected>1x</option>
162
+ <option value="1.2">1.2x</option>
163
+ <option value="1.5">1.5x</option>
164
+ <option value="2">2x</option>
165
+ </select>
166
+ </div>
167
+
168
+ <div>
169
+ <label for="pitchSelect" class="block text-sm font-medium text-gray-700 mb-1">Pitch</label>
170
+ <select id="pitchSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
171
+ <option value="-20">Low</option>
172
+ <option value="0" selected>Normal</option>
173
+ <option value="20">High</option>
174
+ </select>
175
+ </div>
176
+
177
+ <div>
178
+ <label for="modelSelect" class="block text-sm font-medium text-gray-700 mb-1">Model</label>
179
+ <select id="modelSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
180
+ <option value="studio">Studio (Highest Quality)</option>
181
+ <option value="standard" selected>Standard</option>
182
+ <option value="wavenet">WaveNet</option>
183
+ </select>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="playback-controls flex items-center gap-2">
188
+ <button id="playBtn" class="bg-blue-600 hover:bg-blue-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition">
189
+ <i class="fas fa-play"></i>
190
+ </button>
191
+ <button id="pauseBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition">
192
+ <i class="fas fa-pause"></i>
193
+ </button>
194
+ <button id="stopBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition">
195
+ <i class="fas fa-stop"></i>
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="progress-container">
201
+ <div class="flex justify-between text-sm text-gray-600 mb-1">
202
+ <span id="currentTime">0:00</span>
203
+ <span id="totalTime">0:00</span>
204
+ </div>
205
+ <div id="progressContainer" class="w-full flex">
206
+ <!-- Progress chunks will be added dynamically -->
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <div class="mt-8 text-center text-gray-500 text-sm">
213
+ <p>Supports documents up to 1,000,000 characters. Requires a valid Google Cloud API key with Text-to-Speech API enabled.</p>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- PDF.js library for PDF support -->
218
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.min.js"></script>
219
+
220
+ <script>
221
+ document.addEventListener('DOMContentLoaded', () => {
222
+ // DOM Elements
223
+ const apiKeyInput = document.getElementById('apiKey');
224
+ const saveKeyBtn = document.getElementById('saveKeyBtn');
225
+ const uploadBtn = document.getElementById('uploadBtn');
226
+ const pasteBtn = document.getElementById('pasteBtn');
227
+ const dropzone = document.getElementById('dropzone');
228
+ const fileInput = document.getElementById('fileInput');
229
+ const documentContent = document.getElementById('documentContent');
230
+ const charCount = document.getElementById('charCount');
231
+ const voiceGrid = document.getElementById('voiceGrid');
232
+ const refreshVoicesBtn = document.getElementById('refreshVoicesBtn');
233
+ const languageSelect = document.getElementById('languageSelect');
234
+ const rateSelect = document.getElementById('rateSelect');
235
+ const pitchSelect = document.getElementById('pitchSelect');
236
+ const modelSelect = document.getElementById('modelSelect');
237
+ const playBtn = document.getElementById('playBtn');
238
+ const pauseBtn = document.getElementById('pauseBtn');
239
+ const stopBtn = document.getElementById('stopBtn');
240
+ const currentTime = document.getElementById('currentTime');
241
+ const totalTime = document.getElementById('totalTime');
242
+ const progressContainer = document.getElementById('progressContainer');
243
+
244
+ // Audio and state variables
245
+ let audioContext;
246
+ let audioBuffer;
247
+ let audioSource;
248
+ let currentText = '';
249
+ let isPlaying = false;
250
+ let selectedVoice = null;
251
+ let startTime = 0;
252
+ let estimatedDuration = 0;
253
+ let currentChunkIndex = 0;
254
+ const MAX_CHUNK_SIZE = 5000; // Characters per chunk (Google TTS limit)
255
+ let totalChunks = 0;
256
+ let progressChunks = [];
257
+ let audioQueue = [];
258
+ let isPaused = false;
259
+ let apiKey = localStorage.getItem('googleTTSApiKey') || '';
260
+
261
+ // Initialize
262
+ function init() {
263
+ // Load saved API key
264
+ if (apiKey) {
265
+ apiKeyInput.value = apiKey;
266
+ loadVoices();
267
+ }
268
+
269
+ // Set PDF.js worker path
270
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
271
+
272
+ // Initialize AudioContext
273
+ try {
274
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
275
+ } catch (e) {
276
+ console.error('Web Audio API not supported', e);
277
+ alert('Your browser does not support the Web Audio API. Please use Chrome or Edge.');
278
+ }
279
+ }
280
+
281
+ // API Key Management
282
+ saveKeyBtn.addEventListener('click', () => {
283
+ const key = apiKeyInput.value.trim();
284
+ if (key) {
285
+ apiKey = key;
286
+ localStorage.setItem('googleTTSApiKey', key);
287
+ loadVoices();
288
+ showToast('API key saved successfully', 'success');
289
+ } else {
290
+ showToast('Please enter a valid API key', 'error');
291
+ }
292
+ });
293
+
294
+ // Load available voices from Google TTS
295
+ async function loadVoices() {
296
+ if (!apiKey) {
297
+ voiceGrid.innerHTML = `
298
+ <div class="col-span-full text-center py-8">
299
+ <i class="fas fa-key text-gray-400 text-2xl"></i>
300
+ <p class="text-gray-500 mt-2">Please enter your Google Cloud API key</p>
301
+ <p class="text-gray-400 text-sm mt-1">The key must have Text-to-Speech API enabled</p>
302
+ </div>
303
+ `;
304
+ return;
305
+ }
306
+
307
+ voiceGrid.innerHTML = `
308
+ <div class="col-span-full text-center py-8">
309
+ <div class="loading-spinner mx-auto"></div>
310
+ <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
311
+ </div>
312
+ `;
313
+
314
+ try {
315
+ const languageCode = languageSelect.value;
316
+ const response = await fetch(`https://texttospeech.googleapis.com/v1/voices?key=${apiKey}&languageCode=${languageCode}`);
317
+
318
+ if (!response.ok) {
319
+ throw new Error(`API error: ${response.status}`);
320
+ }
321
+
322
+ const data = await response.json();
323
+ renderVoiceOptions(data.voices);
324
+ } catch (error) {
325
+ console.error('Error loading voices:', error);
326
+ voiceGrid.innerHTML = `
327
+ <div class="col-span-full text-center py-8">
328
+ <i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
329
+ <p class="text-gray-500 mt-2">Failed to load voices</p>
330
+ <p class="text-red-500 text-sm mt-1">${error.message}</p>
331
+ <p class="text-gray-400 text-xs mt-2">Check your API key and network connection</p>
332
+ </div>
333
+ `;
334
+ }
335
+ }
336
+
337
+ function renderVoiceOptions(voices) {
338
+ voiceGrid.innerHTML = '';
339
+
340
+ if (!voices || voices.length === 0) {
341
+ voiceGrid.innerHTML = `
342
+ <div class="col-span-full text-center py-8">
343
+ <i class="fas fa-microphone-slash text-gray-400 text-2xl"></i>
344
+ <p class="text-gray-500 mt-2">No voices available for selected language</p>
345
+ </div>
346
+ `;
347
+ return;
348
+ }
349
+
350
+ // Filter for natural HD voices (WaveNet and Studio)
351
+ const naturalVoices = voices.filter(voice =>
352
+ voice.name.includes('Wavenet') ||
353
+ voice.name.includes('Studio') ||
354
+ voice.name.includes('Neural2')
355
+ );
356
+
357
+ // Sort by name
358
+ naturalVoices.sort((a, b) => a.name.localeCompare(b.name));
359
+
360
+ // Create voice cards
361
+ naturalVoices.forEach(voice => {
362
+ const voiceCard = document.createElement('div');
363
+ voiceCard.className = 'voice-card bg-white p-4 rounded-lg cursor-pointer relative';
364
+ voiceCard.dataset.voiceName = voice.name;
365
+
366
+ // Determine voice gender icon
367
+ let genderIcon = 'fa-user';
368
+ let genderColor = 'text-gray-500';
369
+ if (voice.ssmlGender === 'FEMALE') {
370
+ genderIcon = 'fa-female';
371
+ genderColor = 'text-pink-500';
372
+ } else if (voice.ssmlGender === 'MALE') {
373
+ genderIcon = 'fa-male';
374
+ genderColor = 'text-blue-500';
375
+ }
376
+
377
+ // Determine voice quality badge
378
+ let qualityBadge = '';
379
+ if (voice.name.includes('Studio')) {
380
+ qualityBadge = '<div class="absolute top-2 right-2 bg-purple-500 text-white text-xs px-2 py-1 rounded">Studio</div>';
381
+ } else if (voice.name.includes('Wavenet')) {
382
+ qualityBadge = '<div class="absolute top-2 right-2 bg-green-500 text-white text-xs px-2 py-1 rounded">WaveNet</div>';
383
+ } else if (voice.name.includes('Neural2')) {
384
+ qualityBadge = '<div class="absolute top-2 right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">Neural2</div>';
385
+ }
386
+
387
+ voiceCard.innerHTML = `
388
+ <div class="flex items-start">
389
+ <div class="mr-3 ${genderColor}">
390
+ <i class="fas ${genderIcon} text-xl"></i>
391
+ </div>
392
+ <div class="flex-1">
393
+ <h3 class="font-medium text-gray-800">${voice.name.replace('en-', '').replace('Wavenet', '').replace('Neural2', '').replace('Studio', '').trim()}</h3>
394
+ <p class="text-sm text-gray-600">${voice.languageCodes[0]}</p>
395
+ <div class="mt-2 flex items-center">
396
+ <button class="play-sample text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-2 rounded">
397
+ <i class="fas fa-play mr-1"></i> Sample
398
+ </button>
399
+ </div>
400
+ </div>
401
+ <div class="ml-2 text-blue-500 hidden selected-icon">
402
+ <i class="fas fa-check-circle"></i>
403
+ </div>
404
+ </div>
405
+ ${qualityBadge}
406
+ `;
407
+
408
+ voiceCard.addEventListener('click', () => {
409
+ document.querySelectorAll('.voice-card').forEach(card => {
410
+ card.classList.remove('selected');
411
+ card.querySelector('.selected-icon').classList.add('hidden');
412
+ });
413
+
414
+ voiceCard.classList.add('selected');
415
+ voiceCard.querySelector('.selected-icon').classList.remove('hidden');
416
+ selectedVoice = voice;
417
+ });
418
+
419
+ // Play sample when clicking the play button
420
+ const playSampleBtn = voiceCard.querySelector('.play-sample');
421
+ playSampleBtn.addEventListener('click', (e) => {
422
+ e.stopPropagation();
423
+ playSample(voice, "Hello, this is a sample of my voice. I can read your documents with natural sounding speech.");
424
+ });
425
+
426
+ voiceGrid.appendChild(voiceCard);
427
+ });
428
+
429
+ // Select first voice by default
430
+ const firstCard = voiceGrid.querySelector('.voice-card');
431
+ if (firstCard) firstCard.click();
432
+ }
433
+
434
+ // Play a voice sample
435
+ async function playSample(voice, text) {
436
+ if (!apiKey) {
437
+ showToast('Please enter your API key first', 'error');
438
+ return;
439
+ }
440
+
441
+ if (isPlaying) {
442
+ stopPlayback();
443
+ }
444
+
445
+ try {
446
+ const audioData = await synthesizeSpeech(text, voice, parseFloat(rateSelect.value), parseFloat(pitchSelect.value), modelSelect.value);
447
+ playAudioData(audioData);
448
+ } catch (error) {
449
+ console.error('Error playing sample:', error);
450
+ showToast('Failed to play sample: ' + error.message, 'error');
451
+ }
452
+ }
453
+
454
+ // File Handling
455
+ uploadBtn.addEventListener('click', () => fileInput.click());
456
+
457
+ fileInput.addEventListener('change', (e) => {
458
+ if (e.target.files.length > 0) {
459
+ handleFile(e.target.files[0]);
460
+ }
461
+ });
462
+
463
+ pasteBtn.addEventListener('click', () => {
464
+ documentContent.innerHTML = '<p class="text-gray-500 italic">Press Ctrl+V (Cmd+V on Mac) to paste your text...</p>';
465
+ documentContent.focus();
466
+
467
+ const pasteHandler = (e) => {
468
+ e.preventDefault();
469
+ const text = (e.clipboardData || window.clipboardData).getData('text');
470
+ if (text) {
471
+ setDocumentContent(text);
472
+ document.removeEventListener('paste', pasteHandler);
473
+ }
474
+ };
475
+
476
+ document.addEventListener('paste', pasteHandler);
477
+ });
478
+
479
+ // Drag and Drop
480
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
481
+ dropzone.addEventListener(eventName, preventDefaults, false);
482
+ });
483
+
484
+ function preventDefaults(e) {
485
+ e.preventDefault();
486
+ e.stopPropagation();
487
+ }
488
+
489
+ ['dragenter', 'dragover'].forEach(eventName => {
490
+ dropzone.addEventListener(eventName, highlight, false);
491
+ });
492
+
493
+ ['dragleave', 'drop'].forEach(eventName => {
494
+ dropzone.addEventListener(eventName, unhighlight, false);
495
+ });
496
+
497
+ function highlight() {
498
+ dropzone.classList.add('active');
499
+ }
500
+
501
+ function unhighlight() {
502
+ dropzone.classList.remove('active');
503
+ }
504
+
505
+ dropzone.addEventListener('drop', (e) => {
506
+ const dt = e.dataTransfer;
507
+ const file = dt.files[0];
508
+ if (file) {
509
+ handleFile(file);
510
+ }
511
+ });
512
+
513
+ // File processing
514
+ function handleFile(file) {
515
+ const fileType = file.type;
516
+ const fileName = file.name.toLowerCase();
517
+
518
+ if (fileType === 'text/plain' || fileName.endsWith('.txt')) {
519
+ readTextFile(file);
520
+ } else if (fileType === 'application/pdf' || fileName.endsWith('.pdf')) {
521
+ readPDFFile(file);
522
+ } else if (fileType === 'application/msword' ||
523
+ fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
524
+ fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
525
+ alert('Word document support requires conversion to text. For best results, save as plain text or PDF first.');
526
+ } else {
527
+ alert('Unsupported file type. Please upload a TXT or PDF file.');
528
+ }
529
+ }
530
+
531
+ function readTextFile(file) {
532
+ const reader = new FileReader();
533
+ reader.onload = (e) => {
534
+ setDocumentContent(e.target.result);
535
+ };
536
+ reader.readAsText(file);
537
+ }
538
+
539
+ function readPDFFile(file) {
540
+ // Check if PDF.js is available
541
+ if (typeof pdfjsLib === 'undefined') {
542
+ alert('PDF.js not loaded. PDF support requires PDF.js library.');
543
+ return;
544
+ }
545
+
546
+ // Show loading state
547
+ documentContent.innerHTML = '<div class="text-center py-8"><div class="loading-spinner mx-auto"></div><p class="text-gray-500 mt-2">Processing PDF file...</p></div>';
548
+
549
+ // Load PDF.js
550
+ pdfjsLib.getDocument(URL.createObjectURL(file)).promise.then(pdf => {
551
+ let text = '';
552
+ const numPages = pdf.numPages;
553
+ const pagePromises = [];
554
+
555
+ for (let i = 1; i <= numPages; i++) {
556
+ pagePromises.push(pdf.getPage(i).then(page => {
557
+ return page.getTextContent().then(textContent => {
558
+ return textContent.items.map(item => item.str).join(' ');
559
+ });
560
+ }));
561
+ }
562
+
563
+ Promise.all(pagePromises).then(pagesText => {
564
+ text = pagesText.join('\n\n');
565
+ set
566
+ </html>
prompts.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ VocalizeAI: Premium Text-to-Speech Converter Transform written content into natural-sounding speech with our high-definition text-to-speech engine. Simply upload or paste any document, and our AI will convert it into crystal-clear audio with customizable voices, accents, and speaking styles. Features: • Studio-quality voice synthesis with natural intonation • Multiple language support with regional accents • Adjustable speaking speed, tone, and emphasis • Easy export in multiple audio formats • Batch processing for multiple documents
2
+ i just want a high def quality voiced long form document reader that could take documents and pastes/dragged/'ctrl v' not a product for sale
3
+ ValForge: Full-Stack Val Town Project Generator Create a comprehensive Val Town project from your existing code or specifications. This tool analyzes your input and automatically generates a properly structured project with all necessary components for a complete full-stack application. Features: Project Structure Creation: Organizes your code into Val Town's project structure with proper folders and files Sets up the appropriate directory hierarchy according to Val Town best practices Creates necessary configuration files for your project Val Management: Generates individual vals with appropriate permissions and exports Sets up proper relative imports between vals (using the ./foo/bar syntax) Configures public/private accessibility for each val Component Organization: Splits monolithic code into modular components Creates logical separation between frontend, backend, and data layers Implements proper dependency management between components Full-Stack Architecture: Generates frontend components with responsive design Creates serverless backend functions for business logic Implements API endpoints with proper routing and error handling Database Integration: Sets up Val Town's SQLite or Blob Storage for data persistence Creates appropriate database schemas and queries Implements data access patterns following Val Town best practices Development Workflow: Configures version control for collaborative development Enables easy forking and pull request workflows Supports Val Town's built-in version history Deployment Automation: Streamlines the deployment process with proper environment configuration Sets up scheduled functions if needed Creates proper documentation for your project Simply provide your existing code or project requirements, and ValForge will transform it into a production-ready Val Town project that follows the platform's latest best practices for structure, organization, and scalability.
4
+ use a browser embedded TTS engine with HD natural sounding voies
5
+ try again as those voices are not working
6
+ Use only the Google voices and allow for documents up to a million characters
7
+ only natural HD english voices, using google TTS api with BYOK