emilstabil commited on
Commit
c7c1a20
·
verified ·
1 Parent(s): e6db603

Deploy Ekko v1 Tiny + PnC browser demo

Browse files
Files changed (3) hide show
  1. app.js +58 -12
  2. index.html +1 -1
  3. 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-1"));
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 text = transcriptText();
 
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(state.segments),
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.punctuationApplied = record.punctuationApplied;
272
- state.segments = copySegments(record.punctuationApplied ? record.segments : record.rawSegments);
 
 
 
 
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 || !state.rawSegments.length) return;
393
  ensurePunctuation();
394
  state.pncRequestId += 1;
 
 
 
 
 
 
 
 
 
 
 
395
  pncWorker.postMessage({
396
  type: "punctuate",
397
  requestId: state.pncRequestId,
398
  configUrl: signedAsset("model-config.json"),
399
- segments: state.rawSegments,
 
 
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
- if (message.requestId !== state.pncRequestId) return;
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-1" type="module"></script>
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
- let offset = 0;
 
 
 
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(request.segments);
 
 
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) {