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

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"),