Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/helpers/speakerMerge.js
Original file line number Diff line number Diff line change
@@ -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" });
Expand All @@ -16,26 +19,31 @@ 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")}`;
}
return `${mins}:${String(secs).padStart(2, "0")}`;
}

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;
Expand Down Expand Up @@ -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;

Expand Down
48 changes: 47 additions & 1 deletion test/helpers/speakerMerge.test.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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, "");
});
Loading