Spaces:
Running
Running
Deploy Ekko v1 Tiny + PnC browser demo
Browse files- app.js +58 -12
- index.html +1 -1
- pnc-worker.js +9 -4
app.js
CHANGED
|
@@ -72,6 +72,8 @@ const state = {
|
|
| 72 |
pncFailed: false,
|
| 73 |
pncInitializationRequested: false,
|
| 74 |
pncRequestId: 0,
|
|
|
|
|
|
|
| 75 |
nextSegmentId: 0,
|
| 76 |
rawSegments: [],
|
| 77 |
segments: [],
|
|
@@ -92,7 +94,7 @@ const state = {
|
|
| 92 |
};
|
| 93 |
|
| 94 |
const worker = new Worker(signedAsset("worker.js?v=20260927-2"));
|
| 95 |
-
const pncWorker = new Worker(signedAsset("pnc-worker.js?v=20261001-
|
| 96 |
|
| 97 |
function setStatus(text, detail = "", kind = "loading") {
|
| 98 |
elements.statusText.textContent = text;
|
|
@@ -156,6 +158,7 @@ function readConversationHistory() {
|
|
| 156 |
rawSegments: copySegments(record.rawSegments || record.segments),
|
| 157 |
segments: copySegments(record.segments),
|
| 158 |
punctuationApplied: Boolean(record.punctuationApplied),
|
|
|
|
| 159 |
}));
|
| 160 |
} catch (error) {
|
| 161 |
console.warn("Kunne ikke læse lokal samtalehistorik", error);
|
|
@@ -245,7 +248,8 @@ function writeConversationHistory() {
|
|
| 245 |
}
|
| 246 |
|
| 247 |
function persistCurrentConversation() {
|
| 248 |
-
const
|
|
|
|
| 249 |
if (!state.currentConversationId || !text) return;
|
| 250 |
const timestamp = new Date().toISOString();
|
| 251 |
const record = {
|
|
@@ -254,8 +258,9 @@ function persistCurrentConversation() {
|
|
| 254 |
updatedAt: timestamp,
|
| 255 |
text,
|
| 256 |
rawSegments: copySegments(state.rawSegments),
|
| 257 |
-
segments: copySegments(
|
| 258 |
punctuationApplied: state.punctuationApplied,
|
|
|
|
| 259 |
};
|
| 260 |
state.history = [record, ...state.history.filter((item) => item.id !== record.id)];
|
| 261 |
writeConversationHistory();
|
|
@@ -265,11 +270,16 @@ function loadConversation(recordId) {
|
|
| 265 |
const record = state.history.find((item) => item.id === recordId);
|
| 266 |
if (!record || state.recording || state.stopping) return;
|
| 267 |
state.pncRequestId += 1;
|
|
|
|
| 268 |
state.currentConversationId = record.id;
|
| 269 |
state.currentConversationCreatedAt = record.createdAt;
|
| 270 |
state.rawSegments = copySegments(record.rawSegments);
|
| 271 |
-
state.
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
state.nextSegmentId = state.segments.length;
|
| 274 |
elements.app.classList.add("session-active");
|
| 275 |
document.body.classList.add("session-active");
|
|
@@ -366,6 +376,8 @@ function renderTranscript({animateNew = false} = {}) {
|
|
| 366 |
|
| 367 |
function clearTranscript() {
|
| 368 |
state.pncRequestId += 1;
|
|
|
|
|
|
|
| 369 |
state.nextSegmentId = 0;
|
| 370 |
state.rawSegments = [];
|
| 371 |
state.segments = [];
|
|
@@ -389,17 +401,55 @@ function ensurePunctuation() {
|
|
| 389 |
}
|
| 390 |
|
| 391 |
function requestPunctuation() {
|
| 392 |
-
if (state.pncFailed ||
|
| 393 |
ensurePunctuation();
|
| 394 |
state.pncRequestId += 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
pncWorker.postMessage({
|
| 396 |
type: "punctuate",
|
| 397 |
requestId: state.pncRequestId,
|
| 398 |
configUrl: signedAsset("model-config.json"),
|
| 399 |
-
|
|
|
|
|
|
|
| 400 |
});
|
| 401 |
}
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
class StreamingLinearResampler {
|
| 404 |
constructor(sourceRate, targetRate) {
|
| 405 |
this.sourceRate = sourceRate;
|
|
@@ -671,11 +721,7 @@ pncWorker.onmessage = (event) => {
|
|
| 671 |
elements.pncStatus.textContent = `Klar · ${formatBytes(message.bytes)}`;
|
| 672 |
requestPunctuation();
|
| 673 |
} else if (message.type === "pnc-result") {
|
| 674 |
-
|
| 675 |
-
state.punctuationApplied = true;
|
| 676 |
-
state.segments = [...message.segments, ...(state.previewSegment ? [state.previewSegment] : [])];
|
| 677 |
-
renderTranscript();
|
| 678 |
-
persistCurrentConversation();
|
| 679 |
} else if (message.type === "pnc-error") {
|
| 680 |
state.pncFailed = true;
|
| 681 |
elements.pncStatus.textContent = "Tegnsætning utilgængelig";
|
|
|
|
| 72 |
pncFailed: false,
|
| 73 |
pncInitializationRequested: false,
|
| 74 |
pncRequestId: 0,
|
| 75 |
+
pncPending: null,
|
| 76 |
+
pncFormattedCount: 0,
|
| 77 |
nextSegmentId: 0,
|
| 78 |
rawSegments: [],
|
| 79 |
segments: [],
|
|
|
|
| 94 |
};
|
| 95 |
|
| 96 |
const worker = new Worker(signedAsset("worker.js?v=20260927-2"));
|
| 97 |
+
const pncWorker = new Worker(signedAsset("pnc-worker.js?v=20261001-2"));
|
| 98 |
|
| 99 |
function setStatus(text, detail = "", kind = "loading") {
|
| 100 |
elements.statusText.textContent = text;
|
|
|
|
| 158 |
rawSegments: copySegments(record.rawSegments || record.segments),
|
| 159 |
segments: copySegments(record.segments),
|
| 160 |
punctuationApplied: Boolean(record.punctuationApplied),
|
| 161 |
+
formattedCount: Number.isInteger(record.formattedCount) ? record.formattedCount : null,
|
| 162 |
}));
|
| 163 |
} catch (error) {
|
| 164 |
console.warn("Kunne ikke læse lokal samtalehistorik", error);
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
function persistCurrentConversation() {
|
| 251 |
+
const finalizedSegments = state.segments.filter((segment) => segment.id !== "live-preview");
|
| 252 |
+
const text = finalizedSegments.map((segment) => segment.text).filter(Boolean).join(" ").trim();
|
| 253 |
if (!state.currentConversationId || !text) return;
|
| 254 |
const timestamp = new Date().toISOString();
|
| 255 |
const record = {
|
|
|
|
| 258 |
updatedAt: timestamp,
|
| 259 |
text,
|
| 260 |
rawSegments: copySegments(state.rawSegments),
|
| 261 |
+
segments: copySegments(finalizedSegments),
|
| 262 |
punctuationApplied: state.punctuationApplied,
|
| 263 |
+
formattedCount: state.pncFormattedCount,
|
| 264 |
};
|
| 265 |
state.history = [record, ...state.history.filter((item) => item.id !== record.id)];
|
| 266 |
writeConversationHistory();
|
|
|
|
| 270 |
const record = state.history.find((item) => item.id === recordId);
|
| 271 |
if (!record || state.recording || state.stopping) return;
|
| 272 |
state.pncRequestId += 1;
|
| 273 |
+
state.pncPending = null;
|
| 274 |
state.currentConversationId = record.id;
|
| 275 |
state.currentConversationCreatedAt = record.createdAt;
|
| 276 |
state.rawSegments = copySegments(record.rawSegments);
|
| 277 |
+
state.pncFormattedCount = record.formattedCount === null
|
| 278 |
+
? (record.punctuationApplied ? state.rawSegments.length : 0)
|
| 279 |
+
: Math.max(0, Math.min(record.formattedCount, state.rawSegments.length));
|
| 280 |
+
state.punctuationApplied = state.pncFormattedCount === state.rawSegments.length;
|
| 281 |
+
state.segments = copySegments(state.pncFormattedCount ? record.segments : record.rawSegments)
|
| 282 |
+
.filter((segment) => segment.id !== "live-preview");
|
| 283 |
state.nextSegmentId = state.segments.length;
|
| 284 |
elements.app.classList.add("session-active");
|
| 285 |
document.body.classList.add("session-active");
|
|
|
|
| 376 |
|
| 377 |
function clearTranscript() {
|
| 378 |
state.pncRequestId += 1;
|
| 379 |
+
state.pncPending = null;
|
| 380 |
+
state.pncFormattedCount = 0;
|
| 381 |
state.nextSegmentId = 0;
|
| 382 |
state.rawSegments = [];
|
| 383 |
state.segments = [];
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
function requestPunctuation() {
|
| 404 |
+
if (state.pncFailed || state.pncPending || state.pncFormattedCount >= state.rawSegments.length) return;
|
| 405 |
ensurePunctuation();
|
| 406 |
state.pncRequestId += 1;
|
| 407 |
+
const start = state.pncFormattedCount;
|
| 408 |
+
const segments = state.rawSegments.slice(start);
|
| 409 |
+
const capitalizeFirst = start > 0 && /[.!?]["”’']*$/u.test(
|
| 410 |
+
state.segments[start - 1]?.text.trim() || "",
|
| 411 |
+
);
|
| 412 |
+
const contextWords = [];
|
| 413 |
+
for (let index = start - 1; index >= 0 && contextWords.length < 12; index -= 1) {
|
| 414 |
+
const words = state.rawSegments[index].text.trim().split(/\s+/).filter(Boolean);
|
| 415 |
+
contextWords.unshift(...words.slice(-Math.max(0, 12 - contextWords.length)));
|
| 416 |
+
}
|
| 417 |
+
state.pncPending = {requestId: state.pncRequestId, start, ids: segments.map((segment) => segment.id)};
|
| 418 |
pncWorker.postMessage({
|
| 419 |
type: "punctuate",
|
| 420 |
requestId: state.pncRequestId,
|
| 421 |
configUrl: signedAsset("model-config.json"),
|
| 422 |
+
contextWords,
|
| 423 |
+
capitalizeFirst,
|
| 424 |
+
segments,
|
| 425 |
});
|
| 426 |
}
|
| 427 |
|
| 428 |
+
function applyPunctuationResult(message) {
|
| 429 |
+
const pending = state.pncPending;
|
| 430 |
+
if (!pending || message.requestId !== pending.requestId) return;
|
| 431 |
+
state.pncPending = null;
|
| 432 |
+
if (!Array.isArray(message.segments)
|
| 433 |
+
|| message.segments.length !== pending.ids.length
|
| 434 |
+
|| message.segments.some((segment, index) => segment.id !== pending.ids[index]
|
| 435 |
+
|| state.rawSegments[pending.start + index]?.id !== segment.id)) {
|
| 436 |
+
state.pncFailed = true;
|
| 437 |
+
elements.pncStatus.textContent = "Tegnsætning utilgængelig";
|
| 438 |
+
console.error("PnC returnerede segmenter i uventet rækkefølge");
|
| 439 |
+
return;
|
| 440 |
+
}
|
| 441 |
+
const formatted = new Map(message.segments.map((segment) => [segment.id, segment]));
|
| 442 |
+
const displayed = new Map(state.segments.map((segment) => [segment.id, segment]));
|
| 443 |
+
state.segments = state.rawSegments.map((segment) =>
|
| 444 |
+
formatted.get(segment.id) || displayed.get(segment.id) || segment);
|
| 445 |
+
if (state.previewSegment) state.segments.push(state.previewSegment);
|
| 446 |
+
state.pncFormattedCount = pending.start + message.segments.length;
|
| 447 |
+
state.punctuationApplied = state.pncFormattedCount === state.rawSegments.length;
|
| 448 |
+
renderTranscript();
|
| 449 |
+
persistCurrentConversation();
|
| 450 |
+
requestPunctuation();
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
class StreamingLinearResampler {
|
| 454 |
constructor(sourceRate, targetRate) {
|
| 455 |
this.sourceRate = sourceRate;
|
|
|
|
| 721 |
elements.pncStatus.textContent = `Klar · ${formatBytes(message.bytes)}`;
|
| 722 |
requestPunctuation();
|
| 723 |
} else if (message.type === "pnc-result") {
|
| 724 |
+
applyPunctuationResult(message);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
} else if (message.type === "pnc-error") {
|
| 726 |
state.pncFailed = true;
|
| 727 |
elements.pncStatus.textContent = "Tegnsætning utilgængelig";
|
index.html
CHANGED
|
@@ -195,6 +195,6 @@
|
|
| 195 |
</form>
|
| 196 |
</dialog>
|
| 197 |
|
| 198 |
-
<script src="app.js?v=20261001-
|
| 199 |
</body>
|
| 200 |
</html>
|
|
|
|
| 195 |
</form>
|
| 196 |
</dialog>
|
| 197 |
|
| 198 |
+
<script src="app.js?v=20261001-2" type="module"></script>
|
| 199 |
</body>
|
| 200 |
</html>
|
pnc-worker.js
CHANGED
|
@@ -285,16 +285,19 @@ async function punctuateWords(words) {
|
|
| 285 |
return output;
|
| 286 |
}
|
| 287 |
|
| 288 |
-
async function punctuateSegments(segments) {
|
| 289 |
const counts = [];
|
| 290 |
-
const sourceWords = [];
|
| 291 |
segments.forEach((segment) => {
|
| 292 |
const words = String(segment.text || "").trim().split(/\s+/).filter(Boolean);
|
| 293 |
counts.push(words.length);
|
| 294 |
sourceWords.push(...words);
|
| 295 |
});
|
| 296 |
const renderedWords = stabilizeRenderedWords(await punctuateWords(sourceWords));
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
| 298 |
return segments.map((segment, segmentIndex) => {
|
| 299 |
const start = offset;
|
| 300 |
const rendered = renderedWords.slice(start, start + counts[segmentIndex]);
|
|
@@ -321,7 +324,9 @@ async function drainRequests() {
|
|
| 321 |
const request = pendingRequest;
|
| 322 |
pendingRequest = null;
|
| 323 |
await initialization;
|
| 324 |
-
const segments = await punctuateSegments(
|
|
|
|
|
|
|
| 325 |
self.postMessage({type: "pnc-result", requestId: request.requestId, segments});
|
| 326 |
}
|
| 327 |
} catch (error) {
|
|
|
|
| 285 |
return output;
|
| 286 |
}
|
| 287 |
|
| 288 |
+
async function punctuateSegments(segments, contextWords = [], capitalizeFirst = false) {
|
| 289 |
const counts = [];
|
| 290 |
+
const sourceWords = [...contextWords];
|
| 291 |
segments.forEach((segment) => {
|
| 292 |
const words = String(segment.text || "").trim().split(/\s+/).filter(Boolean);
|
| 293 |
counts.push(words.length);
|
| 294 |
sourceWords.push(...words);
|
| 295 |
});
|
| 296 |
const renderedWords = stabilizeRenderedWords(await punctuateWords(sourceWords));
|
| 297 |
+
if (capitalizeFirst && renderedWords.length > contextWords.length) {
|
| 298 |
+
renderedWords[contextWords.length] = capitalizeInitial(renderedWords[contextWords.length]);
|
| 299 |
+
}
|
| 300 |
+
let offset = contextWords.length;
|
| 301 |
return segments.map((segment, segmentIndex) => {
|
| 302 |
const start = offset;
|
| 303 |
const rendered = renderedWords.slice(start, start + counts[segmentIndex]);
|
|
|
|
| 324 |
const request = pendingRequest;
|
| 325 |
pendingRequest = null;
|
| 326 |
await initialization;
|
| 327 |
+
const segments = await punctuateSegments(
|
| 328 |
+
request.segments, request.contextWords, request.capitalizeFirst,
|
| 329 |
+
);
|
| 330 |
self.postMessage({type: "pnc-result", requestId: request.requestId, segments});
|
| 331 |
}
|
| 332 |
} catch (error) {
|