Skip to content
Draft
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
452 changes: 7 additions & 445 deletions Cargo.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { type RefObject, useCallback, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";

import type { DegradedError } from "@hypr/plugin-listener";
import type { RuntimeSpeakerHint } from "@hypr/transcript";
import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks";
import { cn } from "@hypr/utils";

Expand Down Expand Up @@ -46,42 +45,12 @@ export function TranscriptContainer({
const partialWordsByChannel = useListener(
(state) => state.partialWordsByChannel,
);
const partialHintsByChannel = useListener(
(state) => state.partialHintsByChannel,
);

const partialWords = useMemo(
() => Object.values(partialWordsByChannel).flat(),
[partialWordsByChannel],
);

const partialHints = useMemo(() => {
const channelIndices = Object.keys(partialWordsByChannel)
.map(Number)
.sort((a, b) => a - b);

const offsetByChannel = new Map<number, number>();
let currentOffset = 0;
for (const channelIndex of channelIndices) {
offsetByChannel.set(channelIndex, currentOffset);
currentOffset += partialWordsByChannel[channelIndex]?.length ?? 0;
}

const reindexedHints: RuntimeSpeakerHint[] = [];
for (const channelIndex of channelIndices) {
const hints = partialHintsByChannel[channelIndex] ?? [];
const offset = offsetByChannel.get(channelIndex) ?? 0;
for (const hint of hints) {
reindexedHints.push({
...hint,
wordIndex: hint.wordIndex + offset,
});
}
}

return reindexedHints;
}, [partialWordsByChannel, partialHintsByChannel]);

const containerRef = useRef<HTMLDivElement>(null);
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(
null,
Expand Down Expand Up @@ -169,11 +138,6 @@ export function TranscriptContainer({
? partialWords
: []
}
partialHints={
index === transcriptIds.length - 1 && currentActive
? partialHints
: []
}
operations={operations}
/>
{index < transcriptIds.length - 1 && <TranscriptSeparator />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { memo, useEffect, useMemo } from "react";

import type {
PartialWord,
RuntimeSpeakerHint,
Segment,
} from "@hypr/transcript";
import type { PartialWord, Segment } from "@hypr/transcript";
import { cn } from "@hypr/utils";

import * as main from "../../../../../../../store/tinybase/store/main";
Expand All @@ -31,7 +27,6 @@ export function RenderTranscript({
editable,
transcriptId,
partialWords,
partialHints,
operations,
}: {
scrollElement: HTMLDivElement | null;
Expand All @@ -40,7 +35,6 @@ export function RenderTranscript({
editable: boolean;
transcriptId: string;
partialWords: PartialWord[];
partialHints: RuntimeSpeakerHint[];
operations?: Operations;
}) {
const finalWords = useFinalWords(transcriptId);
Expand All @@ -54,14 +48,7 @@ export function RenderTranscript({
) as string | undefined;
const numSpeakers = useSessionSpeakers(sessionId);

const allSpeakerHints = useMemo(() => {
const finalWordsCount = finalWords.length;
const adjustedPartialHints = partialHints.map((hint) => ({
...hint,
wordIndex: finalWordsCount + hint.wordIndex,
}));
return [...finalSpeakerHints, ...adjustedPartialHints];
}, [finalWords.length, finalSpeakerHints, partialHints]);
const allSpeakerHints = useMemo(() => finalSpeakerHints, [finalSpeakerHints]);

const segments = useStableSegments(
finalWords,
Expand Down
75 changes: 3 additions & 72 deletions apps/desktop/src/hooks/useRunBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import type { BatchParams } from "@hypr/plugin-listener2";
import { useConfigValue } from "../config/use-config";
import { useListener } from "../contexts/listener";
import * as main from "../store/tinybase/store/main";
import type { SpeakerHintWithId, WordWithId } from "../store/transcript/types";
import {
parseTranscriptHints,
parseTranscriptWords,
updateTranscriptHints,
updateTranscriptWords,
} from "../store/transcript/utils";
import { makePersistCallback } from "../store/transcript/utils";
import type { HandlePersistCallback } from "../store/zustand/listener/transcript";
import { type Tab, useTabs } from "../store/zustand/tabs";
import { id } from "../utils";
Expand Down Expand Up @@ -99,71 +93,8 @@ export const useRunBatch = (sessionId: string) => {
speaker_hints: "[]",
});

const handlePersist: HandlePersistCallback | undefined =
options?.handlePersist;

const persist =
handlePersist ??
((words, hints) => {
if (words.length === 0) {
return;
}

const existingWords = parseTranscriptWords(store, transcriptId);
const existingHints = parseTranscriptHints(store, transcriptId);

const newWords: WordWithId[] = [];
const newWordIds: string[] = [];

words.forEach((word) => {
const wordId = id();

newWords.push({
id: wordId,
text: word.text,
start_ms: word.start_ms,
end_ms: word.end_ms,
channel: word.channel,
});

newWordIds.push(wordId);
});

const newHints: SpeakerHintWithId[] = [];

hints.forEach((hint) => {
if (hint.data.type !== "provider_speaker_index") {
return;
}

const wordId = newWordIds[hint.wordIndex];
const word = words[hint.wordIndex];

if (!wordId || !word) {
return;
}

newHints.push({
id: id(),
word_id: wordId,
type: "provider_speaker_index",
value: JSON.stringify({
provider: hint.data.provider ?? conn.provider,
channel: hint.data.channel ?? word.channel,
speaker_index: hint.data.speaker_index,
}),
});
});

updateTranscriptWords(store, transcriptId, [
...existingWords,
...newWords,
]);
updateTranscriptHints(store, transcriptId, [
...existingHints,
...newHints,
]);
});
const persist: HandlePersistCallback =
options?.handlePersist ?? makePersistCallback(store, transcriptId);

const params: BatchParams = {
session_id: sessionId,
Expand Down
75 changes: 2 additions & 73 deletions apps/desktop/src/hooks/useStartListening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { useConfigValue } from "../config/use-config";
import { useListener } from "../contexts/listener";
import * as main from "../store/tinybase/store/main";
import type { SpeakerHintWithId, WordWithId } from "../store/transcript/types";
import {
parseTranscriptHints,
parseTranscriptWords,
updateTranscriptHints,
updateTranscriptWords,
} from "../store/transcript/utils";
import type { HandlePersistCallback } from "../store/zustand/listener/transcript";
import { makePersistCallback } from "../store/transcript/utils";
import { id } from "../utils";
import { getSessionEventById } from "../utils/session-event";
import { useKeywords } from "./useKeywords";
Expand Down Expand Up @@ -55,70 +48,6 @@ export function useStartListening(sessionId: string) {
stt_model: conn.model,
});

const handlePersist: HandlePersistCallback = (words, hints) => {
if (words.length === 0) {
return;
}

store.transaction(() => {
const existingWords = parseTranscriptWords(store, transcriptId);
const existingHints = parseTranscriptHints(store, transcriptId);

const newWords: WordWithId[] = [];
const newWordIds: string[] = [];

words.forEach((word) => {
const wordId = id();

newWords.push({
id: wordId,
text: word.text,
start_ms: word.start_ms,
end_ms: word.end_ms,
channel: word.channel,
});

newWordIds.push(wordId);
});

const newHints: SpeakerHintWithId[] = [];

if (conn.provider === "deepgram") {
hints.forEach((hint) => {
if (hint.data.type !== "provider_speaker_index") {
return;
}

const wordId = newWordIds[hint.wordIndex];
const word = words[hint.wordIndex];
if (!wordId || !word) {
return;
}

newHints.push({
id: id(),
word_id: wordId,
type: "provider_speaker_index",
value: JSON.stringify({
provider: hint.data.provider ?? conn.provider,
channel: hint.data.channel ?? word.channel,
speaker_index: hint.data.speaker_index,
}),
});
});
}

updateTranscriptWords(store, transcriptId, [
...existingWords,
...newWords,
]);
updateTranscriptHints(store, transcriptId, [
...existingHints,
...newHints,
]);
});
};

start(
{
session_id: sessionId,
Expand All @@ -131,7 +60,7 @@ export function useStartListening(sessionId: string) {
keywords,
},
{
handlePersist,
handlePersist: makePersistCallback(store, transcriptId),
},
);
}, [
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/store/transcript/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { SpeakerHintStorage, WordStorage } from "@hypr/store";

export type WordWithId = WordStorage & { id: string };
export type WordWithId = WordStorage & {
id: string;
state?: "final" | "pending";
};
export type SpeakerHintWithId = SpeakerHintStorage & { id: string };
70 changes: 70 additions & 0 deletions apps/desktop/src/store/transcript/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { TranscriptDelta } from "@hypr/plugin-listener";

import { id } from "../../utils";
import type { HandlePersistCallback } from "../zustand/listener/transcript";
import type { SpeakerHintWithId, WordWithId } from "./types";

interface TranscriptStore {
Expand All @@ -12,6 +16,7 @@ interface TranscriptStore {
cellId: "words" | "speaker_hints",
value: string,
): void;
transaction<T>(fn: () => T): T;
}

export function parseTranscriptWords(
Expand Down Expand Up @@ -66,3 +71,68 @@ export function updateTranscriptHints(
JSON.stringify(hints),
);
}

export function replaceTranscriptWords(
store: TranscriptStore,
transcriptId: string,
replacedIds: Set<string>,
newWords: WordWithId[],
): void {
const existing = parseTranscriptWords(store, transcriptId).filter(
(w) => !replacedIds.has(w.id),
);
const existingHints = parseTranscriptHints(store, transcriptId).filter(
(h) => h.word_id == null || !replacedIds.has(h.word_id),
);
updateTranscriptWords(store, transcriptId, [...existing, ...newWords]);
updateTranscriptHints(store, transcriptId, existingHints);
}

export function makePersistCallback(
store: TranscriptStore,
transcriptId: string,
): HandlePersistCallback {
return (delta: TranscriptDelta) => {
if (delta.new_words.length === 0 && delta.replaced_ids.length === 0) {
return;
}

store.transaction(() => {
const newWords: WordWithId[] = delta.new_words.map((w) => ({
id: w.id,
text: w.text,
start_ms: w.start_ms,
end_ms: w.end_ms,
channel: w.channel,
state: w.state,
}));

const newHints: SpeakerHintWithId[] = delta.hints.map((h) => ({
id: id(),
word_id: h.word_id,
type: "provider_speaker_index" as const,
value: JSON.stringify({ speaker_index: h.speaker_index }),
}));

if (delta.replaced_ids.length > 0) {
replaceTranscriptWords(
store,
transcriptId,
new Set(delta.replaced_ids),
newWords,
);
} else {
const existing = parseTranscriptWords(store, transcriptId);
updateTranscriptWords(store, transcriptId, [...existing, ...newWords]);
}

if (newHints.length > 0) {
const existingHints = parseTranscriptHints(store, transcriptId);
updateTranscriptHints(store, transcriptId, [
...existingHints,
...newHints,
]);
}
});
};
}
Loading