From e61fb43ee344081c3e29718d8b1595b363f56e98 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sun, 16 Mar 2025 07:45:59 -0700 Subject: [PATCH] give up on downloader idea. Use file from assets. --- android/app/proguard-rules.pro | 2 + app/lib/whisper.ts | 59 ++++++---- components/ConversationThread.tsx | 185 ++++++++++++++++++------------ components/Settings.tsx | 144 +++++++++++++++-------- 4 files changed, 248 insertions(+), 142 deletions(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 551eb41..ce4dc4d 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -12,3 +12,5 @@ -keep class com.facebook.react.turbomodule.** { *; } # Add any project specific keep options here: +# whisper.rn +-keep class com.rnwhisper.** { *; } \ No newline at end of file diff --git a/app/lib/whisper.ts b/app/lib/whisper.ts index baeb7cc..9ad4c6a 100644 --- a/app/lib/whisper.ts +++ b/app/lib/whisper.ts @@ -138,7 +138,7 @@ export class WhisperFile { return Paths.join(WHISPER_MODEL_PATH, this.targetFileName as string); } - get targetPartPath () { + get targetPartPath() { return this.targetPath + ".part"; } @@ -146,6 +146,10 @@ export class WhisperFile { return new File(this.targetPath); } + get targetPartFile() { + return new File(this.targetPartPath); + } + async getTargetInfo() { return await FileSystem.getInfoAsync(this.targetPath); } @@ -156,7 +160,9 @@ export class WhisperFile { async updateTargetExistence() { this.does_target_exist = (await this.getTargetInfo()).exists; + console.log("Determining if %s exists: %s", this.targetPath, this.does_target_exist) this.does_part_target_exist = (await this.getTargetPartInfo()).exists; + console.log("Determining if %s exists: %s", this.targetPartPath, this.does_part_target_exist) } public async getTargetSha() { @@ -171,7 +177,6 @@ export class WhisperFile { }); const data = strToArrBuf(strData); - const digest = await Crypto.digest( Crypto.CryptoDigestAlgorithm.SHA256, data @@ -192,14 +197,15 @@ export class WhisperFile { delete(ignoreErrors = true) { try { - this.targetFile.delete(); + this.does_target_exist && this.targetFile.delete(); + this.does_part_target_exist && this.targetPartFile.delete(); } catch (err) { console.error(err); if (!ignoreErrors) { throw err; } } - console.debug("Created %s", WHISPER_MODEL_DIR); + console.debug("Successfully deleted %s and %s", this.targetPartPath, this.targetPath); } get modelUrl() { @@ -260,9 +266,9 @@ export class WhisperFile { onData?: DownloadCallback | undefined; onComplete?: CompletionCallback | undefined; } = { - onData: undefined, - onComplete: undefined, - } + onData: undefined, + onComplete: undefined, + } ) { await this.syncHfMetadata(); @@ -277,23 +283,26 @@ export class WhisperFile { // If it exists, load the existing data. await this.updateTargetExistence(); - if (this.does_part_target_exist) { - options.onComplete && options.onComplete(this) - } - try { - const existingData = this.does_target_exist - ? await FileSystem.readAsStringAsync(this.targetPath, { - encoding: FileSystem.EncodingType.Base64, - }) - : undefined; + // const existingData = this.does_target_exist + // ? await FileSystem.readAsStringAsync(this.targetPath, { + // encoding: FileSystem.EncodingType.Base64, + // }) + // : undefined; // Create the resumable. return FileSystem.createDownloadResumable( this.modelUrl, - this.targetPath, + this.targetPartPath, {}, async (data: FileSystem.DownloadProgressData) => { + console.log( + "Downloading %s: %d of %d", + this.targetPartPath, + data.totalBytesExpectedToWrite, + data.totalBytesWritten + ); + // console.debug("yes, I'm still downloading"); try { this.download_data = data; @@ -304,7 +313,7 @@ export class WhisperFile { try { await this.syncHfMetadata(); } catch (err) { - console.error("Failed to update HuggingFace metadata: %s", err) + console.error("Failed to update HuggingFace metadata: %s", err); } // try { @@ -316,13 +325,17 @@ export class WhisperFile { try { await this.updateTargetExistence(); } catch (err) { - console.error("Failed to update target existence: %s", err) + console.error("Failed to update target existence: %s", err); } if (options.onData) await options.onData(this); - if (data.totalBytesExpectedToWrite === 0) { - console.debug("Finalizing; copying from %s -> %s", this.targetPartPath, this.targetPath); - await FileSystem.copyAsync({ + if (data.totalBytesExpectedToWrite === data.totalBytesWritten) { + console.debug( + "Finalizing; copying from %s -> %s", + this.targetPartPath, + this.targetPath + ); + await FileSystem.moveAsync({ from: this.targetPartPath, to: this.targetPath, }); @@ -330,7 +343,7 @@ export class WhisperFile { options.onComplete && options.onComplete(this); } }, - existingData ? existingData : undefined + // existingData ? existingData : undefined ); } catch (err) { console.error("Could not read %s: %s", this.targetPath, err); diff --git a/components/ConversationThread.tsx b/components/ConversationThread.tsx index b864e63..832d7a4 100644 --- a/components/ConversationThread.tsx +++ b/components/ConversationThread.tsx @@ -1,14 +1,24 @@ import React, { useState, useEffect } from "react"; -import { Alert, ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native"; +import { + Alert, + ScrollView, + StyleSheet, + Text, + TouchableHighlight, + View, +} from "react-native"; import { useNavigation, Route } from "@react-navigation/native"; import { Conversation, Message } from "@/app/lib/conversation"; import MessageBubble from "@/components/ui/MessageBubble"; -import { CachedTranslator, LanguageServer, language_matrix_entry } from "@/app/i18n/api"; +import { + CachedTranslator, + LanguageServer, + language_matrix_entry, +} from "@/app/i18n/api"; import { Settings } from "@/app/lib/settings"; import { WHISPER_FILES } from "@/app/lib/whisper"; -import { initWhisper, WhisperContext } from 'whisper.rn' -import { useAudioRecorder, AudioModule, RecordingPresets } from 'expo-audio'; - +import { initWhisper, WhisperContext } from "whisper.rn"; +import { useAudioRecorder, AudioModule, RecordingPresets } from "expo-audio"; const lasOptions = { sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition @@ -19,11 +29,19 @@ const lasOptions = { }; // LiveAudioStream.init(lasOptions as any); -const ConversationThread = ({ route }: { route?: Route<"Conversation", { conversation: Conversation }> }) => { +const ConversationThread = ({ + route, +}: { + route?: Route<"Conversation", { conversation: Conversation }>; +}) => { const navigation = useNavigation(); if (!route) { - return (Missing Params!) + return ( + + Missing Params! + + ); } /* 2. Get the param */ @@ -32,21 +50,26 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers const [messages, setMessages] = useState([]); const [guestSpeak, setGuestSpeak] = useState(); const [guestSpeakLoaded, setGuestSpeakLoaded] = useState(false); - const [whisperContext, setWhisperContext] = useState(); + const [whisperContext, setWhisperContext] = useState< + WhisperContext | undefined + >(); const [cachedTranslator, setCachedTranslator] = useState< undefined | CachedTranslator >(); - const [languageLabels, setLanguageLabels] = useState() + const [languageLabels, setLanguageLabels] = useState< + | undefined + | { + hostNative: { + host: string; + guest: string; + }; + guestNative: { + host: string; + guest: string; + }; + } + >(); // recorder settings const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); @@ -63,50 +86,70 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers useEffect(() => { (async function () { - const languageServer = await LanguageServer.getDefault(); try { const languages = await languageServer.fetchLanguages(5000); - const cc = new CachedTranslator( + const cachedTranslator = new CachedTranslator( "en", conversation.guest.language, - languageServer, - ) - console.log("Set cached translator from %s", languageServer.baseUrl) - setCachedTranslator(cc); + languageServer + ); + console.log("Set cached translator from %s", languageServer.baseUrl); + setCachedTranslator(cachedTranslator); + + const settings = await Settings.getDefault(); + const whisperFileLabel = (await settings.getWhisperModel()) || "small"; + const whisperFile = WHISPER_FILES[whisperFileLabel]; + + if (!whisperFile) { + throw new Error(`Could not find the whisper file with the label ${whisperFileLabel}`); + } + + try { + setWhisperContext( + await initWhisper({ + filePath: whisperFile.targetPath, + }) + ); + } catch (err) { + console.error(err) + throw err; + } + + // recorder settings + + (async () => { + const status = await AudioModule.requestRecordingPermissionsAsync(); + if (!status.granted) { + Alert.alert("Permission to access microphone was denied"); + } + })(); + setGuestSpeak(await cachedTranslator.translate("Speak")); + const hostLang1 = languages[conversation.host.language].name; + const guestLang1 = languages[conversation.host.language].name; + const hostLang2 = await cachedTranslator.translate( + languages[conversation.host.language].name + ); + const guestLang2 = await cachedTranslator.translate( + languages[conversation.host.language].name + ); + setLanguageLabels({ + hostNative: { + host: hostLang1, + guest: guestLang1, + }, + guestNative: { + host: hostLang2, + guest: guestLang2, + }, + }); } catch (err) { - console.error("Could not set translator from %s: %s", languageServer.baseUrl, err) + console.error( + "Could not set translator from %s: %s", + languageServer.baseUrl, + err + ); } - - const settings = await Settings.getDefault(); - const whisperFile = WHISPER_FILES[await settings.getWhisperModel() || "en"]; - setWhisperContext(await initWhisper({ - filePath: whisperFile.targetPath, - })); - - // recorder settings - - (async () => { - const status = await AudioModule.requestRecordingPermissionsAsync(); - if (!status.granted) { - Alert.alert('Permission to access microphone was denied'); - } - })(); - setGuestSpeak(await cc.translate("Speak")); - const hostLang1 = languages[conversation.host.language].name; - const guestLang1 = languages[conversation.host.language].name; - const hostLang2 = await cc.translate(languages[conversation.host.language].name); - const guestLang2 = await cc.translate(languages[conversation.host.language].name); - setLanguageLabels({ - hostNative: { - host: hostLang1, - guest: guestLang1, - }, - guestNative: { - host: hostLang2, - guest: guestLang2, - } - }) })(); const updateMessages = (c: Conversation) => { @@ -114,7 +157,7 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers }; if (!conversation) { - console.warn("Conversation is null or undefined.") + console.warn("Conversation is null or undefined."); } conversation.on("add_message", updateMessages); @@ -133,11 +176,17 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers return cachedTranslator ? ( - {languageLabels && ( - {languageLabels.hostNative.host} / {languageLabels.hostNative.guest} - {languageLabels.guestNative.host} / {languageLabels.guestNative.guest} - ) - } + {languageLabels && ( + + + {languageLabels.hostNative.host} / {languageLabels.hostNative.guest} + + + {languageLabels.guestNative.host} /{" "} + {languageLabels.guestNative.guest} + + + )} { const [whisperModel, setWhisperModel] = useState("small"); const [whisperFile, setWhisperFile] = useState(); - const [whisperFileExists, setWhisperFileExists] = useState(false); - const [isWhisperHashValid, setIsWhisperHashValid] = useState(false); const [downloader, setDownloader] = useState(null); const [bytesDone, setBytesDone] = useState(); const [bytesRemaining, setBytesRemaining] = useState(); @@ -54,12 +52,13 @@ const SettingsComponent = () => { ); setWhisperModel((await settings.getWhisperModel()) || "small"); setWhisperFile(WHISPER_FILES[whisperModel]); - if (whisperFile) { - await whisperFile.syncHfMetadata(); - await whisperFile.updateTargetExistence(); - await whisperFile.updateTargetHash(); - setWhisperFileExists(whisperFile.does_target_exist) + if (!whisperFile) { + throw new Error("Invalid Whisper file!"); } + await whisperFile.syncHfMetadata(); + await whisperFile.updateTargetExistence(); + await whisperFile.updateTargetHash(); + console.log("Does %s exist? part=%s, target=%s", whisperFile.label, whisperFile.does_part_target_exist, whisperFile.does_target_exist) })(); }, [whisperFile]); @@ -100,19 +99,21 @@ const SettingsComponent = () => { // await wFile.updateTargetHash(); // setIsWhisperHashValid(wFile.isHashValid); setWhisperFile(wFile); - setWhisperFileExists(wFile.does_target_exist); }; const doSetDownloadStatus = (arg0: WhisperFile) => { - console.log("Downloading ....") - setIsWhisperHashValid(arg0.isHashValid); + // console.log("Downloading ...."); setBytesDone(arg0.download_data?.totalBytesWritten); setBytesRemaining(arg0.download_data?.totalBytesExpectedToWrite); }; - const doOnComplete = (arg0: WhisperFile) => { + const doOnComplete = async (arg0: WhisperFile) => { + console.log("✅ Download complete."); + setDownloader(undefined); + await arg0.updateTargetExistence(); setWhisperFile(arg0); - } + await whisperFile?.updateTargetExistence(); + }; const doDownload = async () => { if (!whisperModel) { @@ -144,7 +145,7 @@ const SettingsComponent = () => { const doDelete = async () => { const whisperFile = WHISPER_MODELS[whisperModel]; whisperFile.delete(); - setStatusTimeout(undefined); + await whisperFile.updateTargetExistence(); }; return hostLanguage && libretranslateBaseUrl ? ( @@ -194,36 +195,82 @@ const SettingsComponent = () => { ))} - {((!downloader) && whisperFile) && - (whisperFile.does_target_exist && ( - DELETE {whisperModel.toUpperCase()} - )) - } - - DOWNLOAD {whisperModel.toUpperCase()} - - )) - - { - downloader && ( - - STOP DOWNLOAD - - ) - } - {bytesDone && bytesRemaining && whisperFile?.does_part_target_exist && ( - - {whisperFile && - ( - Downloading to {whisperFile.targetPath} - )} - - {bytesDone} of{" "} - {bytesRemaining} ( - {bytesDone / bytesRemaining * 100} %){" "} + {/* The target is completely downloaded */} + {!downloader && whisperFile?.does_target_exist && ( + + + DELETE {whisperModel.toUpperCase()} - + )} + + {/* The target "part" is present and is downloading */} + + {downloader && whisperFile?.does_part_target_exist && ( + + STOP DOWNLOAD + + )} + + {/* The target "part" is present and we are NOT downloading */} + + {!downloader && whisperFile?.does_part_target_exist && ( + <> + + RESUME DOWNLOAD + + + + DELETE {whisperModel.toUpperCase()} + + + + )} + + {/* Anything else -- usually if the file has not yet been downloaded */} + + {!( + downloader || + whisperFile?.does_target_exist || + whisperFile?.does_part_target_exist + ) && ( + + + DOWNLOAD {whisperModel.toUpperCase()} + + + )} + + {downloader && + bytesDone && + bytesRemaining && + whisperFile?.does_part_target_exist && ( + + {whisperFile && ( + Downloading to {whisperFile.targetPath} + )} + + {bytesDone} of {bytesRemaining} ( + {(bytesDone / bytesRemaining) * 100} %){" "} + + + )} + + + + Debug Panel + downloader is null? {downloader ? "no" : "yes"} + + whisperFile.does_target_exist{" "} + {whisperFile?.does_target_exist ? "yes" : "no"} + + + whisperFile.does_part_target_exist{" "} + {whisperFile?.does_part_target_exist ? "yes" : "no"} + ) : ( @@ -239,11 +286,12 @@ const styles = StyleSheet.create({ flexDirection: "row", }, downloadButton: { - backgroundColor: "darkblue", + backgroundColor: "#236b9f", padding: 20, margin: 10, - flex: 3, + flex: 1, flexDirection: "column", + alignItems: "center", }, deleteButton: { backgroundColor: "darkred", @@ -261,11 +309,11 @@ const styles = StyleSheet.create({ }, buttonText: { color: "#fff", - flex: 1, - fontSize: 16, - alignSelf: "center", - textAlign: "center", - textAlignVertical: "top", + // flex: 1, + // fontSize: 16, + // alignSelf: "center", + // textAlign: "center", + // textAlignVertical: "top", }, container: { flex: 1,