Skip to content

Commit a3e90ea

Browse files
committed
update transcript schema
1 parent f1a7e47 commit a3e90ea

File tree

11 files changed

+178
-21
lines changed

11 files changed

+178
-21
lines changed

apps/desktop/src/components/main/body/sessions/floating/listen.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,22 +174,42 @@ function useStartSession(sessionId: string) {
174174

175175
function useAppendTranscript(sessionId: string) {
176176
const store = persisted.UI.useStore(persisted.STORE_ID);
177+
const transcriptIds = persisted.UI.useSliceRowIds(
178+
persisted.INDEXES.transcriptsBySession,
179+
sessionId,
180+
persisted.STORE_ID,
181+
);
177182

178183
const handler = useCallback((res: StreamResponse) => {
179184
if (store && res.type === "Results") {
180-
res.channel.alternatives[0].words.forEach((w) => {
181-
store.setRow("words", id(), {
185+
let transcriptId = transcriptIds?.[0];
186+
187+
if (!transcriptId) {
188+
transcriptId = id();
189+
store.setRow("transcripts", transcriptId, {
182190
session_id: sessionId,
191+
user_id: "",
192+
created_at: new Date().toISOString(),
193+
});
194+
}
195+
196+
const { channel: { alternatives: [{ words }] }, channel_index } = res;
197+
198+
words.forEach((w) => {
199+
const word: persisted.Word = {
200+
transcript_id: transcriptId,
183201
text: w.word,
184202
start_ms: Math.round(w.start * 1000),
185203
end_ms: Math.round(w.end * 1000),
186-
speaker: w.speaker?.toString() ?? undefined,
204+
channel: channel_index[0],
187205
user_id: "",
188206
created_at: new Date().toISOString(),
189-
});
207+
};
208+
209+
store.setRow("words", id(), word);
190210
});
191211
}
192-
}, [store, sessionId]);
212+
}, [store, sessionId, transcriptIds]);
193213

194214
return handler;
195215
}

apps/desktop/src/components/main/body/sessions/note-input/transcript.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import * as persisted from "../../../../../store/tinybase/persisted";
44
export function TranscriptView({ sessionId }: { sessionId: string }) {
55
const store = persisted.UI.useStore(persisted.STORE_ID);
66

7+
const transcriptIds = persisted.UI.useSliceRowIds(
8+
persisted.INDEXES.transcriptsBySession,
9+
sessionId,
10+
persisted.STORE_ID,
11+
);
12+
const transcriptId = transcriptIds?.[0];
13+
714
const QUERY = `${sessionId}_words`;
815
const QUERIES = persisted.UI.useCreateQueries(
916
store,
1017
(store) =>
1118
createQueries(store).setQueryDefinition(QUERY, "words", ({ select, where }) => {
1219
select("text");
13-
where("session_id", sessionId);
20+
if (transcriptId) {
21+
where("transcript_id", transcriptId);
22+
}
1423
}),
15-
[sessionId],
24+
[sessionId, transcriptId],
1625
);
1726

1827
const words = persisted.UI.useResultTable(QUERY, QUERIES);

apps/desktop/src/devtool/seed/shared.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
Tag,
1616
TemplateSection,
1717
TemplateStorage,
18+
Transcript,
1819
Word,
1920
} from "../../store/tinybase/persisted";
2021
import { id } from "../../utils";
@@ -137,8 +138,7 @@ export const generateEnhancedMarkdown = () => {
137138

138139
export const generateTranscript = () => {
139140
const wordCount = faker.number.int({ min: 50, max: 200 });
140-
const words: Array<{ speaker: string; text: string; start_ms: number; end_ms: number }> = [];
141-
const speakers = ["Speaker 1", "Speaker 2"];
141+
const words: Array<Word> = [];
142142

143143
let currentTimeMs = 0;
144144

@@ -147,7 +147,10 @@ export const generateTranscript = () => {
147147
const durationMs = faker.number.int({ min: 200, max: 800 });
148148

149149
words.push({
150-
speaker: faker.helpers.arrayElement(speakers),
150+
user_id: USER_ID,
151+
created_at: faker.date.recent({ days: 30 }).toISOString(),
152+
transcript_id: id(),
153+
channel: 0,
151154
text: word,
152155
start_ms: currentTimeMs,
153156
end_ms: currentTimeMs + durationMs,
@@ -382,6 +385,7 @@ export const generateMockData = (config: MockConfig) => {
382385
const calendars: Record<string, Calendar> = {};
383386
const folders: Record<string, Folder> = {};
384387
const sessions: Record<string, SessionStorage> = {};
388+
const transcripts: Record<string, Transcript> = {};
385389
const words: Record<string, Word> = {};
386390
const events: Record<string, Event> = {};
387391
const mapping_session_participant: Record<string, mappingSessionParticipant> = {};
@@ -494,16 +498,23 @@ export const generateMockData = (config: MockConfig) => {
494498
sessions[session.id] = session.data;
495499
sessionIds.push(session.id);
496500

501+
const transcriptId = id();
502+
transcripts[transcriptId] = {
503+
user_id: USER_ID,
504+
session_id: session.id,
505+
created_at: faker.date.recent({ days: 30 }).toISOString(),
506+
};
507+
497508
const transcript = generateTranscript();
498509
transcript.words.forEach((word) => {
499510
const wordId = id();
500511
words[wordId] = {
501512
user_id: USER_ID,
502-
session_id: session.id,
513+
transcript_id: transcriptId,
503514
text: word.text,
504515
start_ms: word.start_ms,
505516
end_ms: word.end_ms,
506-
speaker: word.speaker || undefined,
517+
channel: word.channel,
507518
created_at: faker.date.recent({ days: 30 }).toISOString(),
508519
};
509520
});
@@ -553,6 +564,7 @@ export const generateMockData = (config: MockConfig) => {
553564
calendars,
554565
folders,
555566
sessions,
567+
transcripts,
556568
words,
557569
events,
558570
mapping_session_participant,

apps/desktop/src/store/tinybase/internal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createMergeableStore, createQueries, type MergeableStore, type TablesSc
33
import { z } from "zod";
44

55
import { createBroadcastChannelSynchronizer } from "tinybase/synchronizers/synchronizer-broadcast-channel/with-schemas";
6+
import { DEFAULT_USER_ID } from "../../utils";
67
import { createLocalPersister } from "./localPersister";
78
import { type InferTinyBaseSchema, jsonObject, type ToStorageType } from "./shared";
89

@@ -98,8 +99,7 @@ export const createStore = () => {
9899
export const useStore = () => {
99100
const store = useCreateMergeableStore(() => createStore());
100101

101-
// TODO
102-
store.setValue("user_id", "4c2c0e44-f674-4c67-87d0-00bcfb78dc8a");
102+
store.setValue("user_id", DEFAULT_USER_ID);
103103

104104
const synchronizer = useCreateSynchronizer(
105105
store,

apps/desktop/src/store/tinybase/persisted.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
TABLE_SESSIONS,
2828
tagSchema as baseTagSchema,
2929
templateSchema as baseTemplateSchema,
30+
transcriptSchema as baseTranscriptSchema,
3031
wordSchema,
3132
} from "@hypr/db";
3233
import { createBroadcastChannelSynchronizer } from "tinybase/synchronizers/synchronizer-broadcast-channel/with-schemas";
@@ -67,6 +68,10 @@ export const sessionSchema = baseSessionSchema.omit({ id: true }).extend({
6768
folder_id: z.preprocess(val => val ?? undefined, z.string().optional()),
6869
});
6970

71+
export const transcriptSchema = baseTranscriptSchema.omit({ id: true }).extend({
72+
created_at: z.string(),
73+
});
74+
7075
export const mappingSessionParticipantSchema = baseMappingSessionParticipantSchema.omit({ id: true }).extend({
7176
created_at: z.string(),
7277
});
@@ -99,6 +104,7 @@ export const chatMessageSchema = baseChatMessageSchema.omit({ id: true }).extend
99104
export const wordSchemaOverride = wordSchema.omit({ id: true }).extend({
100105
created_at: z.string(),
101106
speaker: z.preprocess(val => val ?? undefined, z.string().optional()),
107+
transcript_id: z.string(),
102108
});
103109

104110
export type Human = z.infer<typeof humanSchema>;
@@ -107,6 +113,7 @@ export type Calendar = z.infer<typeof calendarSchema>;
107113
export type Organization = z.infer<typeof organizationSchema>;
108114
export type Folder = z.infer<typeof folderSchema>;
109115
export type Session = z.infer<typeof sessionSchema>;
116+
export type Transcript = z.infer<typeof transcriptSchema>;
110117
export type Word = z.infer<typeof wordSchemaOverride>;
111118
export type mappingSessionParticipant = z.infer<typeof mappingSessionParticipantSchema>;
112119
export type Tag = z.infer<typeof tagSchema>;
@@ -117,6 +124,7 @@ export type ChatGroup = z.infer<typeof chatGroupSchema>;
117124
export type ChatMessage = z.infer<typeof chatMessageSchema>;
118125

119126
export type SessionStorage = ToStorageType<typeof sessionSchema>;
127+
export type TranscriptStorage = ToStorageType<typeof transcriptSchema>;
120128
export type TemplateStorage = ToStorageType<typeof templateSchema>;
121129
export type ChatMessageStorage = ToStorageType<typeof chatMessageSchema>;
122130

@@ -138,14 +146,19 @@ const SCHEMA = {
138146
raw_md: { type: "string" },
139147
enhanced_md: { type: "string" },
140148
} satisfies InferTinyBaseSchema<typeof sessionSchema>,
149+
transcripts: {
150+
user_id: { type: "string" },
151+
created_at: { type: "string" },
152+
session_id: { type: "string" },
153+
} satisfies InferTinyBaseSchema<typeof transcriptSchema>,
141154
words: {
142155
user_id: { type: "string" },
143156
created_at: { type: "string" },
144157
text: { type: "string" },
145-
session_id: { type: "string" },
158+
transcript_id: { type: "string" },
146159
start_ms: { type: "number" },
147160
end_ms: { type: "number" },
148-
speaker: { type: "string" },
161+
channel: { type: "number" },
149162
} satisfies InferTinyBaseSchema<typeof wordSchema>,
150163
humans: {
151164
user_id: { type: "string" },
@@ -318,6 +331,18 @@ export const StoreComponent = () => {
318331
"folders",
319332
"parent_folder_id",
320333
)
334+
.setRelationshipDefinition(
335+
"transcriptToSession",
336+
"transcripts",
337+
"sessions",
338+
"session_id",
339+
)
340+
.setRelationshipDefinition(
341+
"wordToTranscript",
342+
"words",
343+
"transcripts",
344+
"transcript_id",
345+
)
321346
.setRelationshipDefinition(
322347
"sessionParticipantToHuman",
323348
"mapping_session_participant",
@@ -436,6 +461,8 @@ export const StoreComponent = () => {
436461
.setIndexDefinition(INDEXES.sessionsByHuman, "mapping_session_participant", "human_id")
437462
.setIndexDefinition(INDEXES.foldersByParent, "folders", "parent_folder_id", "name")
438463
.setIndexDefinition(INDEXES.sessionsByFolder, "sessions", "folder_id", "created_at")
464+
.setIndexDefinition(INDEXES.transcriptsBySession, "transcripts", "session_id")
465+
.setIndexDefinition(INDEXES.wordsByTranscript, "words", "transcript_id", "start_ms")
439466
.setIndexDefinition(INDEXES.eventsByCalendar, "events", "calendar_id", "started_at")
440467
.setIndexDefinition(
441468
INDEXES.eventsByDate,
@@ -529,6 +556,8 @@ export const INDEXES = {
529556
sessionParticipantsBySession: "sessionParticipantsBySession",
530557
foldersByParent: "foldersByParent",
531558
sessionsByFolder: "sessionsByFolder",
559+
transcriptsBySession: "transcriptsBySession",
560+
wordsByTranscript: "wordsByTranscript",
532561
eventsByCalendar: "eventsByCalendar",
533562
eventsByDate: "eventsByDate",
534563
sessionByDateWithoutEvent: "sessionByDateWithoutEvent",

apps/desktop/src/store/zustand/tabs/test-utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1+
import { id } from "../../../utils";
12
import { type Tab, useTabs } from ".";
23

34
type SessionTab = Extract<Tab, { type: "sessions" }>;
45
type ContactsTab = Extract<Tab, { type: "contacts" }>;
56

6-
const id = () => crypto.randomUUID();
7-
87
type SessionOverrides = Partial<Omit<SessionTab, "type" | "state">> & {
98
state?: Partial<SessionTab["state"]>;
109
};

apps/desktop/src/utils.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/desktop/src/utils/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from "./timeline";
2+
3+
export const id = () => crypto.randomUUID() as string;
4+
5+
// https://www.rfc-editor.org/rfc/rfc4122#section-4.1.7
6+
export const DEFAULT_USER_ID = "00000000-0000-0000-0000-000000000000";
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { buildSegments } from "./transcript";
4+
5+
describe("buildSegments", () => {
6+
const WORD = (
7+
{
8+
text,
9+
channel,
10+
start_ms,
11+
end_ms,
12+
}: {
13+
text: string;
14+
channel: number;
15+
start_ms: number;
16+
end_ms: number;
17+
},
18+
) => ({
19+
user_id: "TODO",
20+
created_at: "TODO",
21+
transcript_id: "TODO",
22+
text,
23+
channel,
24+
start_ms,
25+
end_ms,
26+
});
27+
28+
it.each([
29+
{
30+
description: "returns empty array for empty input",
31+
words: [],
32+
segments: [],
33+
},
34+
{
35+
description: "creates single segment from single word",
36+
words: [
37+
WORD({ text: "hello", channel: 0, start_ms: 0, end_ms: 100 }),
38+
],
39+
segments: [
40+
{ speaker: "Speaker 0", text: "hello" },
41+
],
42+
},
43+
])("$description", ({ words, segments }) => {
44+
const result = buildSegments({
45+
words,
46+
speakerFromChannel: (channel) => `Speaker ${channel}`,
47+
});
48+
49+
expect(result).toEqual(segments);
50+
});
51+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Word } from "../store/tinybase/persisted";
2+
3+
type Segment = {
4+
text: string;
5+
speaker: string;
6+
};
7+
8+
export const buildSegments = (
9+
{
10+
speakerFromChannel,
11+
words,
12+
}: {
13+
speakerFromChannel: (channel: number) => string;
14+
words: Word[];
15+
},
16+
): Segment[] => {
17+
const segments: Segment[] = [];
18+
19+
for (const word of words) {
20+
const speaker = speakerFromChannel(word.channel);
21+
segments.push({ text: word.text, speaker });
22+
}
23+
24+
return segments;
25+
};

0 commit comments

Comments
 (0)