add knex-expo-sqlite-dialect submodule. migrate to expo (possibly a mistake)

This commit is contained in:
Jordan
2025-03-09 06:57:41 -07:00
parent 5352ae8eb1
commit 918d651638
11 changed files with 408 additions and 1351 deletions

View File

@ -1,9 +1,17 @@
import React, { useState, useEffect } from "react";
import { View, Text, TextInput, Pressable, StyleSheet } from "react-native";
import { WhisperFile, download_status_t, whisper_tag_t } from "@/app/lib/whisper";
import {
WhisperFile,
download_status_t,
whisper_tag_t,
} from "@/app/lib/whisper";
import { Settings } from "@/app/lib/settings";
import { Picker } from "@react-native-picker/picker";
import { LanguageServer, language_matrix, language_matrix_entry } from "@/app/i18n/api";
import {
LanguageServer,
language_matrix,
language_matrix_entry,
} from "@/app/i18n/api";
const WHISPER_MODELS = {
small: new WhisperFile("small"),
medium: new WhisperFile("medium"),
@ -17,7 +25,9 @@ const SettingsComponent = () => {
const [libretranslateBaseUrl, setLibretranslateBaseUrl] = useState<
string | null
>(null);
const [languageOptions, setLanguageOptions] = useState<language_matrix | undefined>();
const [languageOptions, setLanguageOptions] = useState<
language_matrix | undefined
>();
const [langServerConn, setLangServerConn] = useState<{
success: boolean;
error?: string;
@ -25,31 +35,30 @@ const SettingsComponent = () => {
const [whisperModel, setWhisperModel] =
useState<keyof typeof WHISPER_MODELS>("small");
const [downloader, setDownloader] = useState<any>(null);
const [whisperFile, setWhisperFile] = useState<WhisperFile>(null);
const [downloadStatus, setDownloadStatus] = useState<undefined | download_status_t>();
const [downloadStatusChecker, setDownloadStatusChecker] = useState<undefined | any>();
const [whisperFile, setWhisperFile] = useState<WhisperFile | undefined>();
const [downloadStatus, setDownloadStatus] = useState<
undefined | download_status_t
>();
const [statusTimeout, setStatusTimeout] = useState<
NodeJS.Timeout | undefined
>();
useEffect(() => {
loadSettings();
}, []);
useEffect(() => {
checkDownloadStatus(whisperModel);
}, [whisperModel]);
const getLanguageOptions = async () => {
const languageServer = await LanguageServer.getDefault();
setLanguageOptions(await languageServer.fetchLanguages());
}
};
const loadSettings = async () => {
const settings = await Settings.getDefault();
const hostLanguage = await settings.getHostLanguage();
setHostLanguage(hostLanguage);
const libretranslateBaseUrl = await settings.getLibretranslateBaseUrl();
setLibretranslateBaseUrl(libretranslateBaseUrl);
const whisperModel = await settings.getWhisperModel();
setWhisperModel(whisperModel as keyof typeof WHISPER_MODELS);
setHostLanguage((await settings.getHostLanguage()) || "en");
setLibretranslateBaseUrl(
(await settings.getLibretranslateBaseUrl()) || LIBRETRANSLATE_BASE_URL
);
setWhisperModel(await settings.getWhisperModel());
};
const handleHostLanguageChange = async (lang: string) => {
@ -78,11 +87,9 @@ const SettingsComponent = () => {
if (!whisperFile) return;
const status = await whisperFile.getDownloadStatus();
setDownloadStatus(status);
}
};
const handleWhisperModelChange = async (
model: whisper_tag_t,
) => {
const handleWhisperModelChange = async (model: whisper_tag_t) => {
const settings = await Settings.getDefault();
await settings.setWhisperModel(model);
setWhisperModel(model);
@ -90,13 +97,20 @@ const SettingsComponent = () => {
};
const doDownload = async () => {
const resumable = await whisperFile.createDownloadResumable({
onData: (progress) => setWhisperDownloadProgress(progress),
});
if (!whisperModel) {
throw new Error("Could not start download because whisperModel not set.");
}
console.log("Starging download of %s", whisperModel)
const whisperFile = new WhisperFile(whisperModel);
const resumable = await whisperFile.createDownloadResumable();
setDownloader(resumable);
try {
await resumable.downloadAsync();
checkDownloadStatus(whisperModel);
const statusTimeout = setInterval(intervalUpdateDownloadStatus, 200);
setStatusTimeout(statusTimeout);
} catch (error) {
console.error("Failed to download whisper model:", error);
}
@ -110,33 +124,25 @@ const SettingsComponent = () => {
const doDelete = async () => {
const whisperFile = WHISPER_MODELS[whisperModel];
whisperFile.delete();
checkDownloadStatus(whisperModel);
};
const checkDownloadStatus = async (model: keyof typeof WHISPER_MODELS) => {
const whisperFile = WHISPER_MODELS[model];
const status = await whisperFile.getDownloadStatus();
if (
!status.isDownloadComplete &&
(!status.doesTargetExist || !status.hasDownloadStarted)
) {
setDownloader(null);
}
setStatusTimeout(undefined);
};
return hostLanguage && libretranslateBaseUrl ? (
<View style={styles.container}>
<Text style={styles.label}>Host Language:</Text>
{languageOptions && (<Picker
selectedValue={hostLanguage}
style={{ height: 50, width: "100%" }}
onValueChange={handleHostLanguageChange}
accessibilityHint="hostLanguage"
>
{languageOptions && Object.entries(languageOptions).map(([key, value]) => {
return (<Picker.Item label={value.name} value={value.code} />)
})}
</Picker>)}
{
<Picker
selectedValue={hostLanguage}
style={{ height: 50, width: "100%" }}
onValueChange={handleHostLanguageChange}
accessibilityHint="host language"
>
{languageOptions &&
Object.entries(languageOptions).map(([key, value]) => {
return <Picker.Item label={value.name} value={value.code} />;
})}
</Picker>
}
<Text style={styles.label}>LibreTranslate Base URL:</Text>
<TextInput
@ -157,7 +163,7 @@ const SettingsComponent = () => {
selectedValue={whisperModel}
style={{ height: 50, width: "100%" }}
onValueChange={handleWhisperModelChange}
accessibilityHint="language"
accessibilityHint="whisper models"
>
{Object.entries(WHISPER_MODELS).map(([key, whisperFile]) => (
<Picker.Item
@ -168,46 +174,31 @@ const SettingsComponent = () => {
))}
</Picker>
<View>
{downloader && whisperDownloadProgress && (
<Text>
{whisperDownloadProgress.totalBytesWritten} bytes of{" "}
{whisperDownloadProgress.totalBytesExpectedToWrite} bytes (
{Math.round(
(whisperDownloadProgress.totalBytesWritten /
whisperDownloadProgress.totalBytesExpectedToWrite) *
100
)}
%)
</Text>
)}
<View style={styles.downloadButtonWrapper}>
{downloader &&
whisperDownloadProgress &&
whisperDownloadProgress.totalBytesWritten !==
whisperDownloadProgress.totalBytesExpectedToWrite ? (
<Pressable
onPress={doStopDownload}
style={styles.pauseDownloadButton}
>
<Text style={styles.buttonText}>Pause Download</Text>
</Pressable>
) : (
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>DOWNLOAD</Text>
</Pressable>
)}
{whisperModel &&
WHISPER_MODELS[whisperModel] &&
WHISPER_MODELS[whisperModel].doesTargetExist && (
<Pressable
onPress={doDelete}
style={styles.deleteButton}
aria-label="Delete"
>
<Text style={styles.buttonText}>Delete</Text>
{whisperModel &&
(downloadStatus?.isDownloadComplete ? (
downloadStatus?.doesTargetExist ? (
<Pressable onPress={doDelete}>
<Text>DELETE {whisperModel.toUpperCase()}</Text>
</Pressable>
)}
</View>
) : (
<Pressable onPress={doStopDownload}>
<Text>PAUSE</Text>
</Pressable>
)
) : (
<Pressable onPress={doDownload}>
<Text>DOWNLOAD {whisperModel.toUpperCase()}</Text>
</Pressable>
))}
{downloadStatus?.progress && (
<View>
<Text>
{downloadStatus.progress.current} of{" "}
{downloadStatus.progress.total} (
{downloadStatus.progress.percentRemaining} %){" "}
</Text>
</View>
)}
</View>
</View>
) : (

View File

@ -5,10 +5,10 @@ import { language_matrix } from "@/app/i18n/api";
import { Settings } from "@/app/lib/settings";
import { getDb } from "@/app/lib/db";
import { Knex } from "knex";
import { WhisperFile } from "@/app/lib/whisper";
const RENDER_TIME = 1000;
// Mock the WhisperFile class
jest.mock("@/app/lib/whisper", () => {
const originalModule = jest.requireActual("@/app/lib/whisper");
@ -21,7 +21,10 @@ jest.mock("@/app/lib/whisper", () => {
size,
doesTargetExist: jest.fn(),
getDownloadStatus: jest.fn(), // Mock other methods as needed
isDownloadcomplete: () => true,
isDownloadComplete: jest.fn(() => false), // Initially assume download is not complete
createDownloadResumable: jest.fn().mockResolvedValue({
startAsync: jest.fn().mockResolvedValue({}),
}),
})),
};
});
@ -39,32 +42,33 @@ jest.mock("expo-file-system", () => {
});
jest.mock("@/app/i18n/api", () => {
class LanguageServer {
fetchLanguages = () => {
return {
en: {
code: "en",
name: "English",
targets: ["fr", "es"],
},
fr: {
code: "fr",
name: "French",
targets: ["en", "es"],
},
es: {
code: "es",
name: "Spanish",
targets: ["en", "fr"],
},
} as language_matrix;
};
}
class Translator {
translate = jest.fn((text: string, target: string) => {
return "Hola, como estas?";
});
}
const LanguageServer = jest.fn();
const Translator = jest.fn();
// Mock the fetchLanguages method to return a predefined language matrix
LanguageServer.prototype.fetchLanguages = jest.fn(() => ({
en: {
code: "en",
name: "English",
targets: ["fr", "es"],
},
fr: {
code: "fr",
name: "French",
targets: ["en", "es"],
},
es: {
code: "es",
name: "Spanish",
targets: ["en", "fr"],
},
} as language_matrix));
// Mock the translate method
Translator.prototype.translate = jest.fn((text: string, target: string) => {
return "Hola, como estas?";
});
return {
LanguageServer,
Translator,
@ -100,7 +104,6 @@ describe("SettingsComponent", () => {
test("renders correctly with initial settings", async () => {
render(<SettingsComponent />);
jest.advanceTimersByTime(RENDER_TIME);
screen.debug();
// Wait for the component to fetch and display the initial settings
@ -121,7 +124,7 @@ describe("SettingsComponent", () => {
await screen.findByText(/LibreTranslate Base URL:/i);
// Change the host language input value
const picker = screen.getByAccessibilityHint("hostLanguage");
const picker = screen.getByAccessibilityHint("host language");
fireEvent(picker, "onvalueChange", "es");
expect(picker.props.selectedIndex).toStrictEqual(0);
});
@ -147,4 +150,72 @@ describe("SettingsComponent", () => {
screen.getByAccessibilityHint("libretranslate base url")
).toBeTruthy();
});
describe("Download Whisper Model", () => {
it("should trigger download when model is not present", async () => {
const whisperFile = new WhisperFile("small");
(whisperFile.doesTargetExist as jest.Mock).mockResolvedValue(false);
render(<SettingsComponent />);
await screen.findByText(/\s*Download Small\s*/i);
// Assuming there's a button or trigger to start download
act(() => {
fireEvent.press(screen.getByText(/\s*Download Small\s*/i));
})
expect(whisperFile.createDownloadResumable).toHaveBeenCalled();
});
it("should show progress when download is in progress", async () => {
const whisperFile = new WhisperFile("small");
(whisperFile.doesTargetExist as jest.Mock).mockResolvedValue(false);
(whisperFile.getDownloadStatus as jest.Mock).mockResolvedValue({
doesTargetExist: false,
isDownloadComplete: false,
hasDownloadStarted: true,
progress: {
current: 1024,
total: 2048,
remaining: 1024,
percentRemaining: 50,
},
});
render(<SettingsComponent />);
await screen.findByText(/Host Language:/i);
fireEvent.press(screen.getByText(/Download Model/i));
expect(await screen.findByText("50%")).toBeTruthy();
});
it("should indicate download is complete", async () => {
const whisperFile = new WhisperFile("small");
(whisperFile.doesTargetExist as jest.Mock).mockResolvedValue(false);
(whisperFile.getDownloadStatus as jest.Mock)
.mockResolvedValueOnce({
doesTargetExist: false,
isDownloadComplete: false,
hasDownloadStarted: true,
progress: {
current: 1024,
total: 2048,
remaining: 1024,
percentRemaining: 50,
},
})
.mockResolvedValueOnce({
doesTargetExist: true,
isDownloadComplete: true,
hasDownloadStarted: false,
progress: undefined,
});
render(<SettingsComponent />);
await screen.findByText(/Host Language:/i);
fireEvent.press(screen.getByText(/Download Model/i));
expect(await screen.findByText("Download Complete")).toBeTruthy();
});
});
});