Skip to content

Commit 3446645

Browse files
Add session-scoped transcription hints
Build dynamic transcription hints from session participants, event attendees, existing dictionary terms, and metadata keywords with dedupe and caps.
1 parent 6c5b158 commit 3446645

2 files changed

Lines changed: 257 additions & 9 deletions

File tree

apps/desktop/src/stt/useKeywords.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
parseDictionaryTermsText,
77
} from "./keywords";
88
import {
9+
buildKeywords,
910
buildKeywordSourceText,
1011
extractKeywordsFromMarkdown,
1112
getSessionKeywords,
13+
type KeywordStore,
1214
} from "./useKeywords";
1315

1416
describe("extractKeywordsFromMarkdown", () => {
@@ -158,12 +160,121 @@ describe("getSessionKeywords", () => {
158160

159161
expect(
160162
getSessionKeywords({
161-
store,
163+
store: store as unknown as KeywordStore,
162164
sessionId: "session-1",
163165
dictionaryTerms: ["Anarlog"],
164166
}),
165167
).toEqual(expect.arrayContaining(["Anarlog", "Launch"]));
166168
});
169+
170+
it("prioritizes mapped participants and attached event attendees", () => {
171+
const tables = {
172+
sessions: new Map([
173+
[
174+
"session-1",
175+
{
176+
raw_md: "Discuss #Launch and production systems",
177+
title: "Erebor sync",
178+
event_json: JSON.stringify({
179+
tracking_id: "tracking-1",
180+
calendar_id: "calendar-1",
181+
title: "OpenWorld review",
182+
description: "Airborne Brothers follow-up",
183+
location: "Zoom",
184+
}),
185+
},
186+
],
187+
]),
188+
mapping_session_participant: new Map([
189+
[
190+
"mapping-1",
191+
{
192+
session_id: "session-1",
193+
human_id: "human-1",
194+
},
195+
],
196+
[
197+
"mapping-2",
198+
{
199+
session_id: "other-session",
200+
human_id: "human-2",
201+
},
202+
],
203+
]),
204+
humans: new Map([
205+
["human-1", { name: "Alice Kim" }],
206+
["human-2", { name: "Bob Stone" }],
207+
]),
208+
events: new Map([
209+
[
210+
"event-1",
211+
{
212+
tracking_id_event: "tracking-1",
213+
calendar_id: "calendar-1",
214+
participants_json: JSON.stringify([
215+
{ name: "Alice Kim", email: "alice@example.com" },
216+
{ name: "Mina Park", email: "mina@example.com" },
217+
{
218+
name: "John Jeong",
219+
email: "john@example.com",
220+
is_current_user: true,
221+
},
222+
]),
223+
},
224+
],
225+
]),
226+
};
227+
const store = {
228+
getCell: (tableId: keyof typeof tables, rowId: string, cellId: string) =>
229+
(tables[tableId].get(rowId) as Record<string, unknown> | undefined)?.[
230+
cellId
231+
],
232+
forEachRow: (
233+
tableId: "mapping_session_participant" | "events",
234+
callback: (rowId: string, forEachCell: unknown) => void,
235+
) => {
236+
for (const rowId of tables[tableId].keys()) {
237+
callback(rowId, undefined);
238+
}
239+
},
240+
};
241+
242+
const result = getSessionKeywords({
243+
store: store as unknown as KeywordStore,
244+
sessionId: "session-1",
245+
dictionaryTerms: ["Anarlog"],
246+
});
247+
248+
expect(result.slice(0, 3)).toEqual(["Alice Kim", "Mina Park", "Anarlog"]);
249+
expect(result).toEqual(expect.arrayContaining(["Launch"]));
250+
expect(result).not.toContain("Bob Stone");
251+
expect(result).not.toContain("John Jeong");
252+
});
253+
});
254+
255+
describe("buildKeywords", () => {
256+
it("dedupes higher-priority hints and caps the result", () => {
257+
const result = buildKeywords({
258+
rawMd: "",
259+
title: "",
260+
eventJson: "",
261+
sessionParticipantTerms: ["Alice Kim"],
262+
eventParticipantTerms: ["alice kim", "Mina Park"],
263+
dictionaryTerms: Array.from(
264+
{ length: 60 },
265+
(_, index) => `Term ${index}`,
266+
),
267+
});
268+
269+
expect(result).toHaveLength(50);
270+
expect(result.slice(0, 4)).toEqual([
271+
"Alice Kim",
272+
"Mina Park",
273+
"Term 0",
274+
"Term 1",
275+
]);
276+
expect(result).not.toContain("alice kim");
277+
});
167278
});
168279

169280
describe("dictionary term helpers", () => {

apps/desktop/src/stt/useKeywords.ts

Lines changed: 145 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { useConfigValue } from "~/shared/config";
1414
import * as main from "~/store/tinybase/store/main";
1515
import { normalizeKeywordList } from "~/stt/keywords";
1616

17+
const MAX_TRANSCRIPTION_HINTS = 50;
18+
1719
export function useKeywords(sessionId: string) {
1820
const rawMd = main.UI.useCell("sessions", sessionId, "raw_md", main.STORE_ID);
1921
const title = main.UI.useCell("sessions", sessionId, "title", main.STORE_ID);
@@ -35,12 +37,9 @@ export function useKeywords(sessionId: string) {
3537
}, [dictionaryTerms, eventJson, rawMd, title]);
3638
}
3739

38-
type KeywordStore = {
39-
getCell: (
40-
tableId: "sessions",
41-
rowId: string,
42-
cellId: "raw_md" | "title" | "event_json",
43-
) => unknown;
40+
export type KeywordStore = {
41+
getCell: main.Store["getCell"];
42+
forEachRow?: main.Store["forEachRow"];
4443
};
4544

4645
export function getSessionKeywords({
@@ -52,10 +51,30 @@ export function getSessionKeywords({
5251
sessionId: string;
5352
dictionaryTerms: string[];
5453
}) {
54+
return getSessionTranscriptionHints({
55+
store,
56+
sessionId,
57+
dictionaryTerms,
58+
});
59+
}
60+
61+
export function getSessionTranscriptionHints({
62+
store,
63+
sessionId,
64+
dictionaryTerms,
65+
}: {
66+
store: KeywordStore;
67+
sessionId: string;
68+
dictionaryTerms: string[];
69+
}) {
70+
const eventJson = store.getCell("sessions", sessionId, "event_json");
71+
5572
return buildKeywords({
5673
rawMd: store.getCell("sessions", sessionId, "raw_md"),
5774
title: store.getCell("sessions", sessionId, "title"),
58-
eventJson: store.getCell("sessions", sessionId, "event_json"),
75+
eventJson,
76+
sessionParticipantTerms: getSessionParticipantNames(store, sessionId),
77+
eventParticipantTerms: getAttachedEventParticipantNames(store, eventJson),
5978
dictionaryTerms,
6079
});
6180
}
@@ -64,11 +83,15 @@ export function buildKeywords({
6483
rawMd,
6584
title,
6685
eventJson,
86+
sessionParticipantTerms = [],
87+
eventParticipantTerms = [],
6788
dictionaryTerms,
6889
}: {
6990
rawMd: unknown;
7091
title: unknown;
7192
eventJson: unknown;
93+
sessionParticipantTerms?: string[];
94+
eventParticipantTerms?: string[];
7295
dictionaryTerms: string[];
7396
}) {
7497
const sourceText = buildKeywordSourceText({
@@ -81,7 +104,13 @@ export function buildKeywords({
81104
? extractKeywordsFromMarkdown(sourceText)
82105
: { keywords: [], keyphrases: [] };
83106

84-
return normalizeKeywordList([...dictionaryTerms, ...keywords, ...keyphrases]);
107+
return normalizeKeywordList([
108+
...sessionParticipantTerms,
109+
...eventParticipantTerms,
110+
...dictionaryTerms,
111+
...keywords,
112+
...keyphrases,
113+
]).slice(0, MAX_TRANSCRIPTION_HINTS);
85114
}
86115

87116
export function buildKeywordSourceText({
@@ -198,6 +227,114 @@ const eventKeywordFields = (eventJson: unknown): string[] => {
198227
}
199228
};
200229

230+
const getSessionParticipantNames = (
231+
store: KeywordStore,
232+
sessionId: string,
233+
): string[] => {
234+
if (!store.forEachRow) {
235+
return [];
236+
}
237+
238+
const names: string[] = [];
239+
store.forEachRow("mapping_session_participant", (mappingId, _forEachCell) => {
240+
const mappedSessionId = store.getCell(
241+
"mapping_session_participant",
242+
mappingId,
243+
"session_id",
244+
);
245+
if (mappedSessionId !== sessionId) {
246+
return;
247+
}
248+
249+
const humanId = stringValue(
250+
store.getCell("mapping_session_participant", mappingId, "human_id"),
251+
);
252+
if (!humanId) {
253+
return;
254+
}
255+
256+
const name = stringValue(store.getCell("humans", humanId, "name"));
257+
if (name) {
258+
names.push(name);
259+
}
260+
});
261+
262+
return names;
263+
};
264+
265+
const getAttachedEventParticipantNames = (
266+
store: KeywordStore,
267+
eventJson: unknown,
268+
): string[] => {
269+
if (!store.forEachRow) {
270+
return [];
271+
}
272+
273+
const sessionEvent = parseSessionEvent(eventJson);
274+
if (!sessionEvent) {
275+
return [];
276+
}
277+
278+
let participantsJson: unknown;
279+
store.forEachRow("events", (eventId, _forEachCell) => {
280+
if (participantsJson !== undefined) {
281+
return;
282+
}
283+
284+
const trackingId = store.getCell("events", eventId, "tracking_id_event");
285+
const calendarId = store.getCell("events", eventId, "calendar_id");
286+
if (
287+
trackingId === sessionEvent.trackingId &&
288+
calendarId === sessionEvent.calendarId
289+
) {
290+
participantsJson = store.getCell("events", eventId, "participants_json");
291+
}
292+
});
293+
294+
return parseEventParticipantNames(participantsJson);
295+
};
296+
297+
const parseSessionEvent = (
298+
eventJson: unknown,
299+
): { trackingId: string; calendarId: string } | null => {
300+
if (typeof eventJson !== "string" || !eventJson) {
301+
return null;
302+
}
303+
304+
try {
305+
const event = JSON.parse(eventJson);
306+
const trackingId = stringValue(event?.tracking_id);
307+
const calendarId = stringValue(event?.calendar_id);
308+
return trackingId && calendarId ? { trackingId, calendarId } : null;
309+
} catch {
310+
return null;
311+
}
312+
};
313+
314+
const parseEventParticipantNames = (participantsJson: unknown): string[] => {
315+
if (typeof participantsJson !== "string" || !participantsJson) {
316+
return [];
317+
}
318+
319+
try {
320+
const participants = JSON.parse(participantsJson);
321+
if (!Array.isArray(participants)) {
322+
return [];
323+
}
324+
325+
return participants.flatMap((participant) => {
326+
if (participant?.is_current_user === true) {
327+
return [];
328+
}
329+
330+
const name = stringValue(participant?.name);
331+
return name ? [name] : [];
332+
});
333+
} catch {
334+
return [];
335+
}
336+
};
337+
201338
const removeCodeBlocks = (text: string): string =>
202339
text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "");
203340

0 commit comments

Comments
 (0)