From d69e4296dbc766763310a91649ddbd5249b0b45f Mon Sep 17 00:00:00 2001 From: Henry Su Date: Sat, 22 Aug 2026 21:02:45 -0500 Subject: [PATCH] fix(diarization): guard nullish inputs and format non-finite timestamps in speakerMerge (#1779) --- src/helpers/speakerMerge.js | 24 ++++++++++++---- test/helpers/speakerMerge.test.js | 48 ++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/helpers/speakerMerge.js b/src/helpers/speakerMerge.js index 0f43457b0b..db321466a4 100644 --- a/src/helpers/speakerMerge.js +++ b/src/helpers/speakerMerge.js @@ -1,4 +1,7 @@ function splitIntoSentences(text) { + if (typeof text !== "string" || text.trim().length === 0) { + return []; + } if (typeof Intl !== "undefined" && Intl.Segmenter) { try { const segmenter = new Intl.Segmenter(undefined, { granularity: "sentence" }); @@ -16,9 +19,13 @@ function splitIntoSentences(text) { } function formatTimestamp(seconds) { - const hrs = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); + const totalSeconds = + typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 + ? Math.floor(seconds) + : 0; + const hrs = Math.floor(totalSeconds / 3600); + const mins = Math.floor((totalSeconds % 3600) / 60); + const secs = totalSeconds % 60; if (hrs > 0) { return `${hrs}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; } @@ -26,16 +33,17 @@ function formatTimestamp(seconds) { } function mergeSpeakersWithText(segments, text, durationSeconds) { + const safeText = typeof text === "string" ? text : ""; if (!segments || segments.length === 0) { - return [{ speaker: "speaker_0", text, start: 0, end: durationSeconds || 0 }]; + return [{ speaker: "speaker_0", text: safeText, start: 0, end: durationSeconds || 0 }]; } // Segments arrive in stdout order, not sorted — never assume the last one ends latest. const maxSegmentEnd = segments.reduce((max, s) => Math.max(max, s.end || 0), 0); - const sentences = splitIntoSentences(text); + const sentences = splitIntoSentences(safeText); if (sentences.length === 0) { - return [{ speaker: segments[0].speaker, text, start: segments[0].start, end: maxSegmentEnd }]; + return [{ speaker: segments[0].speaker, text: safeText, start: segments[0].start, end: maxSegmentEnd }]; } const totalDuration = durationSeconds || maxSegmentEnd || 1; @@ -87,6 +95,10 @@ function mergeSpeakersWithText(segments, text, durationSeconds) { } function formatSpeakerTranscript(mergedSegments) { + if (!Array.isArray(mergedSegments) || mergedSegments.length === 0) { + return ""; + } + const speakerMap = new Map(); let nextIndex = 1; diff --git a/test/helpers/speakerMerge.test.js b/test/helpers/speakerMerge.test.js index 04ee4ad67b..eee48ca21e 100644 --- a/test/helpers/speakerMerge.test.js +++ b/test/helpers/speakerMerge.test.js @@ -1,6 +1,11 @@ const test = require("node:test"); const assert = require("node:assert/strict"); -const { mergeSpeakersWithText, formatSpeakerTranscript } = require("../../src/helpers/speakerMerge"); +const { + mergeSpeakersWithText, + formatSpeakerTranscript, + splitIntoSentences, + formatTimestamp, +} = require("../../src/helpers/speakerMerge"); test("mergeSpeakersWithText assigns sentences to speakers by time proportion", () => { const segments = [ @@ -171,3 +176,44 @@ test("zero duration with unsorted segments still maps proportionally against the const last = merged[merged.length - 1]; assert.equal(last.speaker, "spk_1", "tail sentences must map to the late segment"); }); + +test("splitIntoSentences returns empty array for nullish, non-string, or whitespace input", () => { + assert.deepEqual(splitIntoSentences(null), []); + assert.deepEqual(splitIntoSentences(undefined), []); + assert.deepEqual(splitIntoSentences(""), []); + assert.deepEqual(splitIntoSentences(" "), []); + assert.deepEqual(splitIntoSentences(123), []); +}); + +test("formatTimestamp handles non-finite, negative, and nullish inputs safely", () => { + assert.equal(formatTimestamp(undefined), "0:00"); + assert.equal(formatTimestamp(null), "0:00"); + assert.equal(formatTimestamp(NaN), "0:00"); + assert.equal(formatTimestamp(Infinity), "0:00"); + assert.equal(formatTimestamp(-Infinity), "0:00"); + assert.equal(formatTimestamp(-5), "0:00"); + assert.equal(formatTimestamp(0), "0:00"); + assert.equal(formatTimestamp(65), "1:05"); + assert.equal(formatTimestamp(3665), "1:01:05"); +}); + +test("formatSpeakerTranscript returns empty string for nullish or invalid inputs", () => { + assert.equal(formatSpeakerTranscript(null), ""); + assert.equal(formatSpeakerTranscript(undefined), ""); + assert.equal(formatSpeakerTranscript("not an array"), ""); +}); + +test("mergeSpeakersWithText handles nullish text and segments safely", () => { + const resultNull = mergeSpeakersWithText(null, null, 10); + assert.equal(resultNull.length, 1); + assert.equal(resultNull[0].speaker, "speaker_0"); + assert.equal(resultNull[0].text, ""); + assert.equal(resultNull[0].start, 0); + assert.equal(resultNull[0].end, 10); + + const segments = [{ start: 0, end: 10, speaker: "speaker_0" }]; + const resultNullText = mergeSpeakersWithText(segments, null, 10); + assert.equal(resultNullText.length, 1); + assert.equal(resultNullText[0].speaker, "speaker_0"); + assert.equal(resultNullText[0].text, ""); +});