forked from rhulha/StreamingKokoroJS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
289 lines (248 loc) · 10.1 KB
/
Copy pathmain.js
File metadata and controls
289 lines (248 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import { updateProgress } from "./updateProgress.js";
import { AudioPlayer } from "./AudioPlayer.js";
import { AudioDiskSaver } from "./AudioDiskSaver.js";
import { ButtonHandler } from "./ButtonHandler.js";
import { BackgroundQueueManager } from "./BackgroundQueueManager.js";
// --- Helper function to remap the slider value ---
function getRealSpeed(sliderValue) {
// Linearly maps slider range [0.5, 2.0] to real speed range [0.75, 1.5]
return 0.5 * sliderValue + 0.5;
}
// Register service worker for HTTPS (GitHub Pages)
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
navigator.serviceWorker.register("./service-worker.js").then((registration) => {
console.log("Service Worker registered:", registration);
// Register background sync if supported
if ('sync' in registration) {
console.log('Background Sync API supported');
} else {
console.log('Background Sync not supported, using foreground processing');
}
}).catch(err => {
console.warn("Service Worker registration failed:", err);
});
} else if (window.location.hostname === "localhost") {
// For localhost testing
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./service-worker.js").then(() => {
console.log("Service Worker registered.");
});
}
}
// Use the final fix worker with voice validation + memory management
let tts_worker = new Worker(new URL("./worker_final_memory_fix.js", import.meta.url), { type: "module" });
let audioPlayer = new AudioPlayer(tts_worker);
let audioDiskSaver = new AudioDiskSaver();
let buttonHandler = new ButtonHandler(tts_worker, audioPlayer, audioDiskSaver, getRealSpeed);
let queueManager = new BackgroundQueueManager();
// Track current queue job
let currentQueueJobId = null;
let currentQueueMode = null;
let audioChunksForQueue = [];
let currentJobEstimation = null; // Store the chunk estimation for current job
function populateVoiceSelector(voices) {
const voiceSelector = document.getElementById("voiceSelector");
while (voiceSelector.options.length > 0) {
voiceSelector.remove(0);
}
const voiceGroups = {};
let heartVoice = null;
for (const [id, voice] of Object.entries(voices)) {
if (id === "af_heart") {
heartVoice = { id, name: voice.name, language: voice.language };
continue;
}
const category = id.split('_')[0];
const groupKey = `${category} - ${voice.gender}`;
if (!voiceGroups[groupKey]) {
voiceGroups[groupKey] = [];
}
voiceGroups[groupKey].push({ id, name: voice.name, language: voice.language });
}
const sortedGroups = Object.keys(voiceGroups).sort();
for (const groupKey of sortedGroups) {
const [category, gender] = groupKey.split(' - ');
const optgroup = document.createElement('optgroup');
optgroup.label = `${gender} Voices (${category.toUpperCase()})`;
voiceGroups[groupKey].sort((a, b) => a.name.localeCompare(b.name));
if (category === "af" && gender === "Female" && heartVoice) {
const option = document.createElement('option');
option.value = heartVoice.id;
option.textContent = `${heartVoice.name} (${heartVoice.language})`;
option.selected = true;
optgroup.appendChild(option);
}
for (const voice of voiceGroups[groupKey]) {
const option = document.createElement('option');
option.value = voice.id;
option.textContent = `${voice.name} (${voice.language})`;
if (!heartVoice && voiceSelector.options.length === 0) {
option.selected = true;
}
optgroup.appendChild(option);
}
voiceSelector.appendChild(optgroup);
}
voiceSelector.disabled = false;
}
// --- EVENT LISTENERS ---
tts_worker.addEventListener("message", (e) => {
const data = e.data;
if (data.status === "loading_model_ready") {
console.log("Model loaded successfully with device:", data.device);
populateVoiceSelector(data.voices);
} else if (data.status === "loading_model_progress") {
console.log("Model loading progress:", data.progress);
} else if (data.status === "stream_audio_data") {
if (currentQueueJobId) {
// Queue job - collect all audio
audioChunksForQueue.push(data.audio);
// Calculate progress for queue jobs
if (currentJobEstimation) {
const chunkNum = Math.min(audioChunksForQueue.length, currentJobEstimation);
const fallbackEstimation = Math.max(5, Math.ceil(chunkNum * 1.2));
const percent = Math.min((chunkNum / fallbackEstimation) * 100, 98);
updateProgress(percent, `Processing queue job ${currentQueueJobId}: ${chunkNum}/${fallbackEstimation} chunks (${Math.round(percent)}%)`);
}
tts_worker.postMessage({ type: "buffer_processed" });
} else {
// Manual job - use existing logic
if (buttonHandler.getMode() === "disk") {
const percent = audioDiskSaver.addAudioChunk(data.audio);
updateProgress(percent, "Processing audio for saving...");
buttonHandler.updateDiskButtonToStop();
tts_worker.postMessage({ type: "buffer_processed" });
} else if (buttonHandler.getMode() === "stream") {
buttonHandler.updateStreamButtonToStop();
audioPlayer.queueAudio(data.audio);
}
}
} else if (data.status === "complete") {
if (currentQueueJobId) {
// Queue job complete
console.log(`Queue job ${currentQueueJobId} complete with ${audioChunksForQueue.length} audio chunks`);
if (currentQueueMode === "stream") {
// Stream all collected audio
console.log(`Starting playback of ${audioChunksForQueue.length} audio chunks...`);
audioChunksForQueue.forEach((audio, index) => {
setTimeout(() => {
audioPlayer.queueAudio(audio);
}, index * 100);
});
} else if (currentQueueMode === "disk") {
// Save all collected audio
console.log(`Saving ${audioChunksForQueue.length} audio chunks...`);
audioChunksForQueue.forEach(audio => {
audioDiskSaver.addAudioChunk(audio);
});
audioDiskSaver.finalizeCurrentFile();
}
// Clear current job tracking
currentQueueJobId = null;
currentQueueMode = null;
audioChunksForQueue = [];
currentJobEstimation = null;
queueManager.markCurrentJobComplete();
updateProgress(100, "Job completed successfully!");
buttonHandler.resetButtons();
} else {
// Manual job complete
console.log("Manual job completed");
updateProgress(100, "Job completed successfully!");
buttonHandler.resetButtons();
}
} else if (data.status === "error") {
console.error("Worker error:", data.message);
if (currentQueueJobId) {
queueManager.markCurrentJobError(data.message);
currentQueueJobId = null;
currentQueueMode = null;
audioChunksForQueue = [];
currentJobEstimation = null;
}
updateProgress(0, `Error: ${data.message}`);
buttonHandler.resetButtons();
} else if (data.status === "chunk_progress") {
// Update chunk progress for queue jobs
if (currentQueueJobId) {
console.log(`Chunk progress: ${data.sentencesInChunk} sentences in this chunk`);
}
}
});
// --- QUEUE PROCESSING ---
queueManager.addEventListener("jobstarted", async (event) => {
if (!queueManager.hasActiveJob()) return;
const { jobId, text, voice, speed, mode } = event.detail;
currentQueueJobId = jobId;
currentQueueMode = mode;
audioChunksForQueue = [];
currentJobEstimation = null;
try {
// Split text into chunks and process sequentially
const { chunks, estimatedChunks } = await splitTextForQueue(text);
currentJobEstimation = estimatedChunks;
let currentIndex = 0;
const processNextChunk = async () => {
if (currentIndex >= chunks.length) return;
const totalChunks = chunks.length;
const nextChunk = chunks[currentIndex];
console.log(`Processing chunk ${currentIndex + 1}/${totalChunks}: "${nextChunk.substring(0, 50)}..."`);
// Send next chunk to worker
tts_worker.postMessage({
type: "generate",
text: nextChunk,
voice: voice,
speed: speed
});
currentIndex++;
};
// Process first chunk immediately
await processNextChunk();
// Listen for completion to process next chunk
const handleChunkComplete = async (e) => {
if (e.data.status === "complete") {
// Remove this listener
tts_worker.removeEventListener("message", handleChunkComplete);
// Process next chunk if available
if (currentIndex < chunks.length) {
await processNextChunk();
// Add listener back for next chunk
tts_worker.addEventListener("message", handleChunkComplete);
}
}
};
tts_worker.addEventListener("message", handleChunkComplete);
updateProgress(0, `Processing queue job ${jobId}: 0/${estimatedChunks} chunks...`);
} catch (error) {
console.error("Error processing queue job:", error);
queueManager.markCurrentJobError(error.message);
currentQueueJobId = null;
}
});
// --- QUEUE JOB TEXT SPLITTING ---
async function splitTextForQueue(text) {
const { splitTextSmart } = await import("./semantic-split.js");
try {
// Use 250 characters per chunk for reliability
const chunks = splitTextSmart(text, 250);
const estimatedChunks = chunks.length;
console.log(`Split text into ${estimatedChunks} chunks for queue processing`);
return { chunks, estimatedChunks };
} catch (error) {
console.error("Error splitting text into chunks:", error);
// Fallback: send entire text as single chunk
return { chunks: [text], estimatedChunks: 1 };
}
}
// --- EXPORT BUTTON FUNCTIONALITY ---
window.addEventListener("jobstarted", (event) => {
const { jobId, text, voice, speed, mode } = event.detail;
currentQueueJobId = jobId;
currentQueueMode = mode;
audioChunksForQueue = [];
currentJobEstimation = null;
console.log("Starting job:", { jobId, text: text.substring(0, 100), voice, speed, mode });
});
// Initialize queue manager
queueManager.initialize();
console.log("TTS Generator loaded with final memory fixes");