attempt to use part/copy idea.

This commit is contained in:
Jordan 2025-03-14 06:54:06 -07:00
parent f0a722b3fb
commit 123933d459
7 changed files with 219 additions and 18 deletions

View File

@ -3,5 +3,5 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
<application android:largeHeap="true" android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@ -15,7 +15,7 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:largeHeap="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>

61
app/lib/readstream.ts Normal file
View File

@ -0,0 +1,61 @@
/* eslint-disable unicorn/no-null */
import * as fs from 'expo-file-system';
import { Readable } from 'readable-stream';
class ExpoReadStream extends Readable {
private readonly fileUri: string;
private fileSize: number;
private currentPosition: number;
private readonly chunkSize: number;
constructor(fileUri: string, options: fs.ReadingOptions) {
super();
this.fileUri = fileUri;
this.fileSize = 0; // Initialize file size (could be fetched if necessary)
this.currentPosition = options.position ?? 0;
/**
* Default chunk size in bytes. React Native Expo will OOM at 110MB, so we set this to 1/100 of it to balance speed and memory usage and importantly the feedback for user.
* If this is too large, the progress bar will be stuck when down stream processing this chunk.
*/
this.chunkSize = options.length ?? 1024 * 1024;
void this._init();
}
async _init() {
try {
const fileInfo = await fs.getInfoAsync(this.fileUri, { size: true });
if (fileInfo.exists) {
this.fileSize = fileInfo.size ?? 0;
} else {
this.fileSize = 0;
}
} catch (error) {
this.emit('error', error);
}
}
_read() {
const readingOptions = {
encoding: fs.EncodingType.Base64,
position: this.currentPosition,
length: this.chunkSize,
} satisfies fs.ReadingOptions;
fs.readAsStringAsync(this.fileUri, readingOptions).then(chunk => {
if (chunk.length === 0) {
// End of the stream
this.emit('progress', 1);
this.push(null);
} else {
this.currentPosition = Math.min(this.chunkSize + this.currentPosition, this.fileSize);
this.emit('progress', this.fileSize === 0 ? 0.5 : (this.currentPosition / this.fileSize));
this.push(Buffer.from(chunk, 'base64'));
}
}).catch(error => {
this.emit('error', error);
});
}
}
export function createReadStream(fileUri: string, options: { encoding?: fs.EncodingType; end?: number; highWaterMark?: number; start?: number } = {}): ExpoReadStream {
return new ExpoReadStream(fileUri, options);
}

View File

@ -4,6 +4,7 @@ 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";
export const WHISPER_MODEL_PATH = Paths.join(
FileSystem.documentDirectory || "file:///",
@ -119,6 +120,7 @@ export class WhisperFile {
target_hash: string | undefined;
does_target_exist: boolean = false;
does_part_target_exist: boolean = false;
download_data: FileSystem.DownloadProgressData | undefined;
constructor(
@ -136,6 +138,10 @@ export class WhisperFile {
return Paths.join(WHISPER_MODEL_PATH, this.targetFileName as string);
}
get targetPartPath () {
return this.targetPath + ".part";
}
get targetFile() {
return new File(this.targetPath);
}
@ -144,8 +150,13 @@ export class WhisperFile {
return await FileSystem.getInfoAsync(this.targetPath);
}
async getTargetPartInfo() {
return await FileSystem.getInfoAsync(this.targetPartPath);
}
async updateTargetExistence() {
this.does_target_exist = (await this.getTargetInfo()).exists;
this.does_part_target_exist = (await this.getTargetPartInfo()).exists;
}
public async getTargetSha() {
@ -160,6 +171,7 @@ export class WhisperFile {
});
const data = strToArrBuf(strData);
const digest = await Crypto.digest(
Crypto.CryptoDigestAlgorithm.SHA256,
data
@ -246,8 +258,10 @@ export class WhisperFile {
async createDownloadResumable(
options: {
onData?: DownloadCallback | undefined;
onComplete?: CompletionCallback | undefined;
} = {
onData: undefined,
onComplete: undefined,
}
) {
await this.syncHfMetadata();
@ -263,6 +277,10 @@ 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, {
@ -289,11 +307,11 @@ export class WhisperFile {
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.updateTargetHash();
// } catch (er) {
// console.error("Failed to update target hash: %s", er);
// }
try {
await this.updateTargetExistence();
@ -301,6 +319,16 @@ export class WhisperFile {
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({
from: this.targetPartPath,
to: this.targetPath,
});
await this.updateTargetExistence();
options.onComplete && options.onComplete(this);
}
},
existingData ? existingData : undefined
);
@ -311,6 +339,7 @@ export class WhisperFile {
}
export type DownloadCallback = (arg0: WhisperFile) => any;
export type CompletionCallback = (arg0: WhisperFile) => any;
export const WHISPER_FILES = {
small: new WhisperFile("small"),

View File

@ -110,6 +110,10 @@ const SettingsComponent = () => {
setBytesRemaining(arg0.download_data?.totalBytesExpectedToWrite);
};
const doOnComplete = (arg0: WhisperFile) => {
setWhisperFile(arg0);
}
const doDownload = async () => {
if (!whisperModel) {
throw new Error("Could not start download because whisperModel not set.");
@ -122,8 +126,10 @@ const SettingsComponent = () => {
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);
@ -193,7 +199,7 @@ const SettingsComponent = () => {
<Text style={styles.buttonText}>DELETE {whisperModel.toUpperCase()}</Text>
</Pressable>))
}
<Pressable onPress={doDownload} style={styles.pauseDownloadButton}>
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>DOWNLOAD {whisperModel.toUpperCase()}</Text>
</Pressable>
))
@ -205,7 +211,7 @@ const SettingsComponent = () => {
</Pressable>
)
}
{bytesDone && bytesRemaining && (
{bytesDone && bytesRemaining && whisperFile?.does_part_target_exist && (
<View>
{whisperFile &&
(<Text>

117
package-lock.json generated
View File

@ -50,6 +50,7 @@
"react-native-sqlite-storage": "^6.0.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"readable-stream": "^4.7.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"whisper.rn": "^0.3.9"
@ -66,6 +67,7 @@
"@types/react-native-sqlite-storage": "^6.0.5",
"@types/react-navigation": "^3.0.8",
"@types/react-test-renderer": "^18.3.1",
"@types/readable-stream": "^4.0.18",
"babel-jest": "^29.7.0",
"babel-plugin-module-resolver": "^5.0.2",
"expo": "~52.0.28",
@ -5047,6 +5049,24 @@
"@types/react": "^18"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
"integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"safe-buffer": "~5.1.1"
}
},
"node_modules/@types/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@ -5590,6 +5610,21 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/are-we-there-yet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -6031,6 +6066,20 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
@ -15216,17 +15265,43 @@
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": ">= 6"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readable-stream/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/readline": {
@ -16451,6 +16526,20 @@
"readable-stream": "^3.5.0"
}
},
"node_modules/stream-browserify/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/stream-buffers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
@ -16849,6 +16938,20 @@
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tar/node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",

View File

@ -24,6 +24,7 @@
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native-stack": "^7.2.0",
"expo": "~52.0.28",
"expo-audio": "~0.3.4",
"expo-background-fetch": "~13.0.5",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.6",
@ -56,10 +57,10 @@
"react-native-sqlite-storage": "^6.0.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"readable-stream": "^4.7.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"whisper.rn": "^0.3.9",
"expo-audio": "~0.3.4"
"whisper.rn": "^0.3.9"
},
"jest": {
"preset": "jest-expo",
@ -86,6 +87,7 @@
"@types/react-native-sqlite-storage": "^6.0.5",
"@types/react-navigation": "^3.0.8",
"@types/react-test-renderer": "^18.3.1",
"@types/readable-stream": "^4.0.18",
"babel-jest": "^29.7.0",
"babel-plugin-module-resolver": "^5.0.2",
"expo": "~52.0.28",