encountered weird network error.

This commit is contained in:
Jordan 2025-03-17 06:56:05 -07:00
parent e61fb43ee3
commit 0ba5c4b309
7 changed files with 40 additions and 557 deletions

1
.gitignore vendored
View File

@ -36,3 +36,4 @@ yarn-error.*
*.tsbuildinfo
coverage/**/*
assets/whisper

View File

@ -115,13 +115,13 @@ export class Translator {
console.log(data);
return data.translatedText;
} else {
console.error(data);
console.error("Status %d: %s", res.status, JSON.stringify(data));
}
}
static async getDefault(defaultTarget: string | undefined = undefined) {
const settings = await Settings.getDefault();
const source = await settings.getHostLanguage();
const source = await settings.getHostLanguage() || "en";
return new Translator(
source,
defaultTarget,

View File

@ -1,361 +1,14 @@
import { Platform } from "react-native";
import * as FileSystem from "expo-file-system";
import { File, Paths } from "expo-file-system/next";
import { getDb } from "./db";
import * as Crypto from "expo-crypto";
import { arrbufToStr, strToArrBuf } from "./util";
import { createReadStream } from "./readstream";
import FileSystem from "expo-file-system"
import { pathToFileURLString } from "expo-file-system/src/next/pathUtilities/url";
export const WHISPER_MODEL_PATH = Paths.join(
FileSystem.documentDirectory || "file:///",
"whisper"
);
export const WHISPER_MODEL_PATH = Paths.join("..", "..", "assets", "whisper");
export const WHISPER_MODEL_DIR = new File(WHISPER_MODEL_PATH);
// Thanks to https://medium.com/@fabi.mofar/downloading-and-saving-files-in-react-native-expo-5b3499adda84
export const WHISPER_MODEL_SMALL_PATH = "file://../../assets/whisper/whisper-small.bin";
export async function saveFile(
uri: string,
filename: string,
mimetype: string
) {
if (Platform.OS === "android") {
const permissions =
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
if (permissions.granted) {
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64,
});
await FileSystem.StorageAccessFramework.createFileAsync(
permissions.directoryUri,
filename,
mimetype
)
.then(async (uri) => {
await FileSystem.writeAsStringAsync(uri, base64, {
encoding: FileSystem.EncodingType.Base64,
});
})
.catch((e) => console.log(e));
} else {
shareAsync(uri);
}
} else {
shareAsync(uri);
}
}
function shareAsync(uri: string) {
throw new Error("Function not implemented.");
}
export const WHISPER_MODEL_TAGS = ["small", "medium", "large"];
export type whisper_model_tag_t = "small" | "medium" | "large";
export const WHISPER_MODELS = {
small: {
source:
"https://huggingface.co/openai/whisper-small/blob/resolve/pytorch_model.bin",
target: "small.bin",
label: "Small",
size: 967092419,
},
medium: {
source:
"https://huggingface.co/openai/whisper-medium/resolve/main/pytorch_model.bin",
target: "medium.bin",
label: "Medium",
size: 3055735323,
},
large: {
source:
"https://huggingface.co/openai/whisper-large/resolve/main/pytorch_model.bin",
target: "large.bin",
label: "Large",
size: 6173629930,
},
} as {
[key: whisper_model_tag_t]: {
source: string;
target: string;
label: string;
size: number;
};
};
export type whisper_tag_t = "small" | "medium" | "large";
export type hf_channel_t = "raw" | "resolve";
export const HF_URL_BASE = "https://huggingface.co/openai/whisper-";
export const HF_URL_RAW = "raw";
export const HF_URL_RESOLVE = "resolve";
export const HF_URL_END = "/main/pytorch_model.bin";
export function create_hf_url(tag: whisper_tag_t, channel: hf_channel_t) {
return `${HF_URL_BASE}${tag}/${channel}${HF_URL_END}`;
}
export type hf_metadata_t = {
version: string;
oid: string;
size: string;
};
export type download_status_t = {
doesTargetExist: boolean;
isDownloadComplete: boolean;
hasDownloadStarted: boolean;
progress?: {
current: number;
total: number;
remaining: number;
percentRemaining: number;
};
};
export class WhisperFile {
hf_metadata: hf_metadata_t | undefined;
target_hash: string | undefined;
does_target_exist: boolean = false;
does_part_target_exist: boolean = false;
download_data: FileSystem.DownloadProgressData | undefined;
constructor(
public tag: whisper_model_tag_t,
private targetFileName?: string,
public label?: string,
public size?: number
) {
this.targetFileName = this.targetFileName || `${tag}.bin`;
this.label =
this.label || `${tag[0].toUpperCase()}${tag.substring(1).toLowerCase()}`;
}
get targetPath() {
return Paths.join(WHISPER_MODEL_PATH, this.targetFileName as string);
}
get targetPartPath() {
return this.targetPath + ".part";
}
get targetFile() {
return new File(this.targetPath);
}
get targetPartFile() {
return new File(this.targetPartPath);
}
async getTargetInfo() {
return await FileSystem.getInfoAsync(this.targetPath);
}
async getTargetPartInfo() {
return await FileSystem.getInfoAsync(this.targetPartPath);
}
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() {
await this.updateTargetExistence();
if (!this.does_target_exist) {
console.debug("%s does not exist", this.targetPath);
return undefined;
}
const strData = await FileSystem.readAsStringAsync(this.targetPath, {
encoding: FileSystem.EncodingType.Base64,
});
const data = strToArrBuf(strData);
const digest = await Crypto.digest(
Crypto.CryptoDigestAlgorithm.SHA256,
data
);
return digest;
}
public async updateTargetHash() {
const targetSha = await this.getTargetSha();
if (!targetSha) return;
this.target_hash = arrbufToStr(targetSha);
}
get isHashValid() {
return this.target_hash === this.hf_metadata?.oid;
}
delete(ignoreErrors = true) {
try {
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("Successfully deleted %s and %s", this.targetPartPath, this.targetPath);
}
get modelUrl() {
return create_hf_url(this.tag, "resolve");
}
get metadataUrl() {
return create_hf_url(this.tag, "raw");
}
get percentDone() {
if (!this.download_data) return 0;
return (
(this.download_data.totalBytesWritten /
this.download_data.totalBytesExpectedToWrite) *
100
);
}
get percentLeft() {
if (!this.download_data) return 0;
return 100 - this.percentDone;
}
public async syncHfMetadata() {
try {
const resp = await fetch(this.metadataUrl, {
credentials: "include",
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0",
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Sec-GPC": "1",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "cross-site",
"If-None-Match": '"8fa71cbce85078986b46fb97caec22039e73351a"',
Priority: "u=0, i",
},
method: "GET",
mode: "cors",
});
const text = await resp.text();
this.hf_metadata = Object.fromEntries(
text.split("\n").map((line) => line.split(" "))
) as hf_metadata_t;
} catch (err) {
console.error("Failed to fetch %s: %s", this.metadataUrl, err);
throw err;
}
}
async createDownloadResumable(
options: {
onData?: DownloadCallback | undefined;
onComplete?: CompletionCallback | undefined;
} = {
onData: undefined,
onComplete: undefined,
}
) {
await this.syncHfMetadata();
// If the whisper model dir doesn't exist, create it.
if (!WHISPER_MODEL_DIR.exists) {
FileSystem.makeDirectoryAsync(WHISPER_MODEL_PATH, {
intermediates: true,
});
}
// Check for the existence of the target file
// If it exists, load the existing data.
await this.updateTargetExistence();
try {
// 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.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;
} catch (err) {
console.error("Failed to set downloadData: %s", err);
}
try {
await this.syncHfMetadata();
} catch (err) {
console.error("Failed to update HuggingFace metadata: %s", err);
}
// try {
// await this.updateTargetHash();
// } catch (er) {
// console.error("Failed to update target hash: %s", er);
// }
try {
await this.updateTargetExistence();
} catch (err) {
console.error("Failed to update target existence: %s", err);
}
if (options.onData) await options.onData(this);
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,
});
await this.updateTargetExistence();
options.onComplete && options.onComplete(this);
}
},
// existingData ? existingData : undefined
);
} catch (err) {
console.error("Could not read %s: %s", this.targetPath, err);
}
}
}
export type DownloadCallback = (arg0: WhisperFile) => any;
export type CompletionCallback = (arg0: WhisperFile) => any;
export const WHISPER_FILES = {
small: new WhisperFile("small"),
medium: new WhisperFile("medium"),
large: new WhisperFile("large"),
};
export async function whisperModelExists() {
const file = new File(WHISPER_MODEL_PATH);
return file.exists;
}

View File

@ -15,10 +15,13 @@ import {
LanguageServer,
language_matrix_entry,
} from "@/app/i18n/api";
import { Settings } from "@/app/lib/settings";
import { WHISPER_FILES } from "@/app/lib/whisper";
import {
WHISPER_MODEL_SMALL_PATH,
whisperModelExists,
} from "@/app/lib/whisper";
import { initWhisper, WhisperContext } from "whisper.rn";
import { useAudioRecorder, AudioModule, RecordingPresets } from "expo-audio";
import FileSystem from "expo-file-system";
const lasOptions = {
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
@ -97,22 +100,27 @@ const ConversationThread = ({
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 {
if (!(await whisperModelExists())) {
throw new Error(`${WHISPER_MODEL_SMALL_PATH} does not exist`);
}
} catch (err) {
console.error(
`Could not determine if %s exists: %s`,
WHISPER_MODEL_SMALL_PATH,
err
);
throw err;
}
try {
setWhisperContext(
await initWhisper({
filePath: whisperFile.targetPath,
filePath: WHISPER_MODEL_SMALL_PATH,
})
);
} catch (err) {
console.error(err)
console.error(err);
throw err;
}

View File

@ -36,7 +36,7 @@ export function LanguageSelection(props: {
// Replace with your actual async data fetching logic
setTranslator(await CachedTranslator.getDefault());
const languageServer = await LanguageServer.getDefault();
const languages = await languageServer.fetchLanguages(5000);
const languages = await languageServer.fetchLanguages(10000);
setLanguages(languages);
setLanguagesLoaded(true);
} catch (error) {

View File

@ -1,23 +1,11 @@
import React, { useState, useEffect } from "react";
import { View, Text, TextInput, Pressable, StyleSheet } from "react-native";
import {
WHISPER_FILES,
WhisperFile,
download_status_t,
whisper_tag_t,
} from "@/app/lib/whisper";
import { View, Text, TextInput, StyleSheet } from "react-native";
import { Settings } from "@/app/lib/settings";
import { Picker } from "@react-native-picker/picker";
import {
LanguageServer,
language_matrix,
language_matrix_entry,
} from "@/app/i18n/api";
const WHISPER_MODELS = {
small: new WhisperFile("small"),
medium: new WhisperFile("medium"),
large: new WhisperFile("large"),
};
const LIBRETRANSLATE_BASE_URL = "https://translate.argosopentech.com/translate";
@ -33,15 +21,6 @@ const SettingsComponent = () => {
success: boolean;
error?: string;
} | null>(null);
const [whisperModel, setWhisperModel] =
useState<keyof typeof WHISPER_MODELS>("small");
const [whisperFile, setWhisperFile] = useState<WhisperFile | undefined>();
const [downloader, setDownloader] = useState<any>(null);
const [bytesDone, setBytesDone] = useState<number | undefined>();
const [bytesRemaining, setBytesRemaining] = useState<number | undefined>();
const [statusTimeout, setStatusTimeout] = useState<
NodeJS.Timeout | undefined
>();
useEffect(() => {
(async function () {
@ -50,22 +29,9 @@ const SettingsComponent = () => {
setLibretranslateBaseUrl(
(await settings.getLibretranslateBaseUrl()) || LIBRETRANSLATE_BASE_URL
);
setWhisperModel((await settings.getWhisperModel()) || "small");
setWhisperFile(WHISPER_FILES[whisperModel]);
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]);
});
const getLanguageOptions = async () => {
const languageServer = await LanguageServer.getDefault();
setLanguageOptions(await languageServer.fetchLanguages());
};
const handleHostLanguageChange = async (lang: string) => {
const settings = await Settings.getDefault();
@ -83,71 +49,17 @@ const SettingsComponent = () => {
const checkLangServerConnection = async (baseUrl: string) => {
try {
// Replace with actual connection check logic
setLangServerConn({ success: true });
const testResult = await fetch(baseUrl, {
method: "HEAD",
});
if (testResult.status !== 200) {
setLangServerConn({ success: true, error: testResult.statusText });
}
} catch (error) {
setLangServerConn({ success: false, error: `${error}` });
}
};
const handleWhisperModelChange = async (model: whisper_tag_t) => {
const settings = await Settings.getDefault();
await settings.setWhisperModel(model);
setWhisperModel(model);
const wFile = WHISPER_FILES[whisperModel];
await wFile.syncHfMetadata();
await wFile.updateTargetExistence();
// await wFile.updateTargetHash();
// setIsWhisperHashValid(wFile.isHashValid);
setWhisperFile(wFile);
};
const doSetDownloadStatus = (arg0: WhisperFile) => {
// console.log("Downloading ....");
setBytesDone(arg0.download_data?.totalBytesWritten);
setBytesRemaining(arg0.download_data?.totalBytesExpectedToWrite);
};
const doOnComplete = async (arg0: WhisperFile) => {
console.log("✅ Download complete.");
setDownloader(undefined);
await arg0.updateTargetExistence();
setWhisperFile(arg0);
await whisperFile?.updateTargetExistence();
};
const doDownload = async () => {
if (!whisperModel) {
throw new Error("Could not start download because whisperModel not set.");
}
console.log("Starting download of %s", whisperModel);
if (!whisperFile) throw new Error("No whisper file");
try {
const resumable = await whisperFile.createDownloadResumable({
onData: doSetDownloadStatus,
onComplete: doOnComplete,
});
setDownloader(resumable);
if (!resumable) throw new Error("Could not construct resumable");
await resumable.resumeAsync();
} catch (error) {
console.error("Failed to download whisper model:", error);
}
};
const doStopDownload = async () => {
downloader.cancelAsync();
setDownloader(null);
};
const doDelete = async () => {
const whisperFile = WHISPER_MODELS[whisperModel];
whisperFile.delete();
await whisperFile.updateTargetExistence();
};
return hostLanguage && libretranslateBaseUrl ? (
<View style={styles.container}>
<Text style={styles.label}>Host Language:</Text>
@ -180,98 +92,6 @@ const SettingsComponent = () => {
Error connecting to {libretranslateBaseUrl}: {langServerConn.error}
</Text>
))}
<Picker
selectedValue={whisperModel}
style={{ height: 50, width: "100%" }}
onValueChange={handleWhisperModelChange}
accessibilityHint="whisper models"
>
{Object.entries(WHISPER_MODELS).map(([key, whisperFile]) => (
<Picker.Item
key={whisperFile.tag}
label={whisperFile.label}
value={key}
/>
))}
</Picker>
<View style={styles.downloadButtonWrapper}>
{/* The target is completely downloaded */}
{!downloader && whisperFile?.does_target_exist && (
<Pressable onPress={doDelete} style={styles.deleteButton}>
<Text style={styles.buttonText}>
DELETE {whisperModel.toUpperCase()}
</Text>
</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>
) : (
<View>

View File

@ -9,7 +9,8 @@
"updateSnapshots": "jest -u --coverage=false",
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo prebuild --npm -p android",
"prebuild:android": "expo prebuild --npm -p android",
"android": "expo run:android",
"ios": "expo prebuild --npm -p android --offline",
"web": "expo start --offline --web",
"lint": "expo lint"