give up on downloader idea. Use file from assets.

This commit is contained in:
Jordan 2025-03-16 07:45:59 -07:00
parent 123933d459
commit e61fb43ee3
4 changed files with 248 additions and 142 deletions

View File

@ -12,3 +12,5 @@
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:
# whisper.rn
-keep class com.rnwhisper.** { *; }

View File

@ -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);

View File

@ -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 (<View><Text>Missing Params!</Text></View>)
return (
<View>
<Text>Missing Params!</Text>
</View>
);
}
/* 2. Get the param */
@ -32,21 +50,26 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
const [messages, setMessages] = useState<Message[]>([]);
const [guestSpeak, setGuestSpeak] = useState<string | undefined>();
const [guestSpeakLoaded, setGuestSpeakLoaded] = useState<boolean>(false);
const [whisperContext, setWhisperContext] = useState<WhisperContext | undefined>();
const [whisperContext, setWhisperContext] = useState<
WhisperContext | undefined
>();
const [cachedTranslator, setCachedTranslator] = useState<
undefined | CachedTranslator
>();
const [languageLabels, setLanguageLabels] = useState<undefined | {
hostNative: {
host: string,
guest: string,
},
guestNative: {
host: string,
guest: string,
}
}>()
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 ? (
<View style={{ flex: 1, flexDirection: "column" }}>
{languageLabels && (<View style={styles.languageLabels}>
<Text style={styles.nativeHostLabel}>{languageLabels.hostNative.host} / {languageLabels.hostNative.guest}</Text>
<Text style={styles.nativeGuestLabel}>{languageLabels.guestNative.host} / {languageLabels.guestNative.guest}</Text>
</View>)
}
{languageLabels && (
<View style={styles.languageLabels}>
<Text style={styles.nativeHostLabel}>
{languageLabels.hostNative.host} / {languageLabels.hostNative.guest}
</Text>
<Text style={styles.nativeGuestLabel}>
{languageLabels.guestNative.host} /{" "}
{languageLabels.guestNative.guest}
</Text>
</View>
)}
<ScrollView
style={{
borderColor: "black",
@ -177,15 +226,9 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
};
const styles = StyleSheet.create({
languageLabels: {
},
nativeHostLabel: {
},
nativeGuestLabel: {
},
})
languageLabels: {},
nativeHostLabel: {},
nativeGuestLabel: {},
});
export default ConversationThread;

View File

@ -36,8 +36,6 @@ const SettingsComponent = () => {
const [whisperModel, setWhisperModel] =
useState<keyof typeof WHISPER_MODELS>("small");
const [whisperFile, setWhisperFile] = useState<WhisperFile | undefined>();
const [whisperFileExists, setWhisperFileExists] = useState<boolean>(false);
const [isWhisperHashValid, setIsWhisperHashValid] = useState<boolean>(false);
const [downloader, setDownloader] = useState<any>(null);
const [bytesDone, setBytesDone] = useState<number | undefined>();
const [bytesRemaining, setBytesRemaining] = useState<number | undefined>();
@ -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 = () => {
))}
</Picker>
<View style={styles.downloadButtonWrapper}>
{((!downloader) && whisperFile) &&
(whisperFile.does_target_exist && (<Pressable onPress={doDelete} style={styles.deleteButton}>
<Text style={styles.buttonText}>DELETE {whisperModel.toUpperCase()}</Text>
</Pressable>))
}
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>DOWNLOAD {whisperModel.toUpperCase()}</Text>
</Pressable>
))
{
downloader && (
<Pressable onPress={doStopDownload} style={styles.pauseDownloadButton}>
<Text style={styles.buttonText}>STOP DOWNLOAD</Text>
</Pressable>
)
}
{bytesDone && bytesRemaining && whisperFile?.does_part_target_exist && (
<View>
{whisperFile &&
(<Text>
Downloading to {whisperFile.targetPath}
</Text>)}
<Text>
{bytesDone} of{" "}
{bytesRemaining} (
{bytesDone / bytesRemaining * 100} %){" "}
{/* The target is completely downloaded */}
{!downloader && whisperFile?.does_target_exist && (
<Pressable onPress={doDelete} style={styles.deleteButton}>
<Text style={styles.buttonText}>
DELETE {whisperModel.toUpperCase()}
</Text>
</View>
</Pressable>
)}
{/* The target "part" is present and is downloading */}
{downloader && whisperFile?.does_part_target_exist && (
<Pressable
onPress={doStopDownload}
style={styles.pauseDownloadButton}
>
<Text style={styles.buttonText}>STOP DOWNLOAD</Text>
</Pressable>
)}
{/* The target "part" is present and we are NOT downloading */}
{!downloader && whisperFile?.does_part_target_exist && (
<>
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>RESUME DOWNLOAD</Text>
</Pressable>
<Pressable onPress={doDelete} style={styles.deleteButton}>
<Text style={styles.buttonText}>
DELETE {whisperModel.toUpperCase()}
</Text>
</Pressable>
</>
)}
{/* Anything else -- usually if the file has not yet been downloaded */}
{!(
downloader ||
whisperFile?.does_target_exist ||
whisperFile?.does_part_target_exist
) && (
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>
DOWNLOAD {whisperModel.toUpperCase()}
</Text>
</Pressable>
)}
{downloader &&
bytesDone &&
bytesRemaining &&
whisperFile?.does_part_target_exist && (
<View>
{whisperFile && (
<Text>Downloading to {whisperFile.targetPath}</Text>
)}
<Text>
{bytesDone} of {bytesRemaining} (
{(bytesDone / bytesRemaining) * 100} %){" "}
</Text>
</View>
)}
</View>
<View>
<Text>Debug Panel</Text>
<Text>downloader is null? {downloader ? "no" : "yes"}</Text>
<Text>
whisperFile.does_target_exist{" "}
{whisperFile?.does_target_exist ? "yes" : "no"}
</Text>
<Text>
whisperFile.does_part_target_exist{" "}
{whisperFile?.does_part_target_exist ? "yes" : "no"}
</Text>
</View>
</View>
) : (
@ -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,