matt HOFFNER
add isComplete value to hook for easier access to autosending messages once complete
862ccf9
"use client"; | |
import { useCallback, useMemo, useState } from "react"; | |
import { useWorker } from "./useWorker"; | |
import Constants from "../constants"; | |
interface ProgressItem { | |
file: string; | |
loaded: number; | |
progress: number; | |
total: number; | |
name: string; | |
status: string; | |
} | |
interface TranscriberUpdateData { | |
data: [ | |
string, | |
{ chunks: { text: string; timestamp: [number, number | null] }[] }, | |
]; | |
text: string; | |
} | |
interface TranscriberCompleteData { | |
data: { | |
text: string; | |
chunks: { text: string; timestamp: [number, number | null] }[]; | |
}; | |
} | |
export interface TranscriberData { | |
isComplete?: boolean; | |
isBusy: boolean; | |
text: string; | |
chunks: { text: string; timestamp: [number, number | null] }[]; | |
} | |
export interface Transcriber { | |
onInputChange: () => void; | |
isBusy: boolean; | |
isComplete: boolean; | |
isModelLoading: boolean; | |
progressItems: ProgressItem[]; | |
start: (audioData: AudioBuffer | undefined) => void; | |
output?: TranscriberData; | |
model: string; | |
setModel: (model: string) => void; | |
multilingual: boolean; | |
setMultilingual: (model: boolean) => void; | |
quantized: boolean; | |
setQuantized: (model: boolean) => void; | |
subtask: string; | |
setSubtask: (subtask: string) => void; | |
language?: string; | |
setLanguage: (language: string) => void; | |
} | |
export function useTranscriber(): Transcriber { | |
const [transcript, setTranscript] = useState<TranscriberData | undefined>( | |
undefined, | |
); | |
const [isBusy, setIsBusy] = useState(false); | |
const [isComplete, setIsComplete] = useState(false); | |
const [isModelLoading, setIsModelLoading] = useState(false); | |
const [progressItems, setProgressItems] = useState<ProgressItem[]>([]); | |
const webWorker = useWorker((event) => { | |
const message = event.data; | |
// Update the state with the result | |
switch (message.status) { | |
case "progress": | |
// Model file progress: update one of the progress items. | |
setProgressItems((prev) => | |
prev.map((item) => { | |
if (item.file === message.file) { | |
return { ...item, progress: message.progress }; | |
} | |
return item; | |
}), | |
); | |
break; | |
case "update": | |
// Received partial update | |
// console.log("update", message); | |
// eslint-disable-next-line no-case-declarations | |
const updateMessage = message as TranscriberUpdateData; | |
setTranscript({ | |
isBusy: true, | |
text: updateMessage.data[0], | |
chunks: updateMessage.data[1].chunks, | |
}); | |
break; | |
case "complete": | |
// Received complete transcript | |
// console.log("complete", message); | |
// eslint-disable-next-line no-case-declarations | |
const completeMessage = message as TranscriberCompleteData; | |
setTranscript({ | |
isBusy: false, | |
text: completeMessage.data.text, | |
chunks: completeMessage.data.chunks, | |
}); | |
setIsBusy(false); | |
setIsComplete(true); | |
break; | |
case "initiate": | |
// Model file start load: add a new progress item to the list. | |
setIsModelLoading(true); | |
setProgressItems((prev) => [...prev, message]); | |
break; | |
case "ready": | |
setIsModelLoading(false); | |
break; | |
case "error": | |
setIsBusy(false); | |
alert( | |
`${message.data.message} This is most likely because you are using Safari on an M1/M2 Mac. Please try again from Chrome, Firefox, or Edge.\n\nIf this is not the case, please file a bug report.`, | |
); | |
break; | |
case "done": | |
// Model file loaded: remove the progress item from the list. | |
setProgressItems((prev) => | |
prev.filter((item) => item.file !== message.file), | |
); | |
break; | |
default: | |
// initiate/download/done | |
break; | |
} | |
}); | |
const [model, setModel] = useState<string>(Constants.DEFAULT_MODEL); | |
const [subtask, setSubtask] = useState<string>(Constants.DEFAULT_SUBTASK); | |
const [quantized, setQuantized] = useState<boolean>( | |
Constants.DEFAULT_QUANTIZED, | |
); | |
const [multilingual, setMultilingual] = useState<boolean>( | |
Constants.DEFAULT_MULTILINGUAL, | |
); | |
const [language, setLanguage] = useState<string>( | |
Constants.DEFAULT_LANGUAGE, | |
); | |
const onInputChange = useCallback(() => { | |
setTranscript(undefined); | |
}, []); | |
const postRequest = useCallback( | |
async (audioData: AudioBuffer | undefined) => { | |
if (audioData) { | |
setTranscript(undefined); | |
setIsBusy(true); | |
setIsComplete(false); | |
let audio; | |
if (audioData.numberOfChannels === 2) { | |
const SCALING_FACTOR = Math.sqrt(2); | |
let left = audioData.getChannelData(0); | |
let right = audioData.getChannelData(1); | |
audio = new Float32Array(left.length); | |
for (let i = 0; i < audioData.length; ++i) { | |
audio[i] = SCALING_FACTOR * (left[i] + right[i]) / 2; | |
} | |
} else { | |
// If the audio is not stereo, we can just use the first channel: | |
audio = audioData.getChannelData(0); | |
} | |
webWorker.postMessage({ | |
audio, | |
model, | |
multilingual, | |
quantized, | |
subtask: multilingual ? subtask : null, | |
language: | |
multilingual && language !== "auto" ? language : null, | |
}); | |
} | |
}, | |
[webWorker, model, multilingual, quantized, subtask, language], | |
); | |
const transcriber = useMemo(() => { | |
return { | |
onInputChange, | |
isBusy, | |
isComplete, | |
isModelLoading, | |
progressItems, | |
start: postRequest, | |
output: transcript, | |
model, | |
setModel, | |
multilingual, | |
setMultilingual, | |
quantized, | |
setQuantized, | |
subtask, | |
setSubtask, | |
language, | |
setLanguage, | |
}; | |
}, [ | |
onInputChange, | |
isBusy, | |
isComplete, | |
isModelLoading, | |
progressItems, | |
postRequest, | |
transcript, | |
model, | |
multilingual, | |
quantized, | |
subtask, | |
language, | |
]); | |
return transcriber; | |
} |