add whisper download utils. add react-navigator.
This commit is contained in:
@ -1,19 +1,21 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import * as React from 'react';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import SettingsComponent from '@/components/Settings';
|
||||
import { LanguageSelection } from '@/components/LanguageSelection';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import ConversationThread from '@/components/ConversationThread';
|
||||
import Home from '.';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: '#f4511e',
|
||||
},
|
||||
headerTintColor: '#fff',
|
||||
headerTitleStyle: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Stack>
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator initialRouteName='LanguageSelection'>
|
||||
<Stack.Screen name="LanguageSelection" component={Home} />
|
||||
<Stack.Screen name="ConversationThread" component={ConversationThread} />
|
||||
<Stack.Screen name="Settings" component={SettingsComponent} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Cache } from "react-native-cache";
|
||||
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Settings } from "../lib/settings";
|
||||
|
||||
type language_t = string;
|
||||
|
||||
@ -51,6 +52,11 @@ export class LanguageServer {
|
||||
throw new Error(`Can't extract values from data: ${JSON.stringify(data)}`)
|
||||
}
|
||||
}
|
||||
|
||||
static async getDefault() {
|
||||
const settings = await Settings.getDefault();
|
||||
return new LanguageServer(await settings.getLibretranslateBaseUrl());
|
||||
}
|
||||
}
|
||||
|
||||
export class Translator {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { LanguageSelection } from "@/components/LanguageSelection";
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useState } from "react";
|
||||
import { Image, Text, View, StyleSheet, Button, Pressable } from "react-native";
|
||||
import { Translator, language_matrix_entry } from "./i18n/api";
|
||||
import ConversationThread from "@/components/ConversationThread";
|
||||
import { LanguageServer, Translator, language_matrix_entry } from "./i18n/api";
|
||||
import { Conversation } from "./lib/conversation";
|
||||
import { LanguageSelection } from "@/components/LanguageSelection";
|
||||
|
||||
function LogoTitle() {
|
||||
return (
|
||||
@ -16,21 +15,25 @@ function LogoTitle() {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [lang, setLang] = useState<language_matrix_entry | undefined>();
|
||||
const [conversation, setConversation] = useState<Conversation | undefined>();
|
||||
const [setShowSettings, showSettings] = useState<boolean>(false);
|
||||
|
||||
function onLangSelected(lang: language_matrix_entry | undefined) {
|
||||
console.log("Language %s selected", lang?.code);
|
||||
async function onLangSelected(lang: language_matrix_entry) {
|
||||
console.log("Language %s selected", lang.code);
|
||||
setLang(lang);
|
||||
if (!lang?.code) return;
|
||||
setConversation(
|
||||
new Conversation(
|
||||
new Translator("en", lang.code),
|
||||
{ id: "host", language: "en" },
|
||||
{ id: "guest", language: lang.code }
|
||||
)
|
||||
const langServer = await LanguageServer.getDefault();
|
||||
const conversation = new Conversation(
|
||||
new Translator("en", lang.code, langServer),
|
||||
{ id: "host", language: "en" },
|
||||
{ id: "guest", language: lang.code }
|
||||
);
|
||||
navigation.navigate("Conversation", {
|
||||
conversation,
|
||||
});
|
||||
}
|
||||
|
||||
function onGoBack() {
|
||||
@ -39,9 +42,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="settings" />
|
||||
<Stack.Screen name="conversation" />
|
||||
<LanguageSelection onLangSelected={onLangSelected} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -7,12 +7,22 @@ export const MIGRATE_UP = {
|
||||
libretranslate_base_url TEXT,
|
||||
ui_direction INTEGER
|
||||
)`,
|
||||
],
|
||||
2: [
|
||||
`CREATE TABLE IF NOT EXISTS whisper_models (
|
||||
model TEXT PRIMARY KEY,
|
||||
bytes_done INTEGER,
|
||||
bytes_total INTEGER,
|
||||
)`,
|
||||
]
|
||||
}
|
||||
|
||||
export const MIGRATE_DOWN = {
|
||||
1: [
|
||||
`DROP TABLE IF EXISTS settings`
|
||||
],
|
||||
2: [
|
||||
`DROP TABLE IF EXISTS whisper_models`
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { SQLiteDatabase } from "expo-sqlite";
|
||||
import FileSystem from "expo-file-system"
|
||||
import { getDb } from "./db";
|
||||
|
||||
export class Settings {
|
||||
|
||||
@ -6,6 +8,7 @@ export class Settings {
|
||||
"host_language",
|
||||
"libretranslate_base_url",
|
||||
'ui_direction',
|
||||
"wisper_model",
|
||||
]
|
||||
|
||||
constructor(public db: SQLiteDatabase) {
|
||||
@ -53,4 +56,16 @@ LIMIT 1`
|
||||
return await this.getValue("libretranslate_base_url")
|
||||
}
|
||||
|
||||
async setWhisperModel(value : string) {
|
||||
await this.setValue("whisper_model", value);
|
||||
}
|
||||
|
||||
async getWhisperModel() {
|
||||
return await this.getValue("whisper_model");
|
||||
}
|
||||
|
||||
static async getDefault() {
|
||||
return new Settings(await getDb())
|
||||
}
|
||||
|
||||
}
|
190
app/lib/whisper.ts
Normal file
190
app/lib/whisper.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { Platform } from "react-native";
|
||||
import FileSystem from "expo-file-system";
|
||||
import { File, Paths } from 'expo-file-system/next';
|
||||
import { getDb } from "./db";
|
||||
|
||||
export const WHISPER_MODEL_PATH = Paths.join(FileSystem.bundleDirectory || "file:///", "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 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 = (typeof WHISPER_MODEL_TAGS)[number];
|
||||
|
||||
export const WHISPER_MODELS = {
|
||||
small: {
|
||||
source:
|
||||
"https://huggingface.co/openai/whisper-small/blob/main/pytorch_model.bin",
|
||||
target: "small.bin",
|
||||
label: "Small",
|
||||
},
|
||||
medium: {
|
||||
source:
|
||||
"https://huggingface.co/openai/whisper-medium/blob/main/pytorch_model.bin",
|
||||
target: "medium.bin",
|
||||
label: "Medium",
|
||||
},
|
||||
large: {
|
||||
source:
|
||||
"https://huggingface.co/openai/whisper-large/blob/main/pytorch_model.bin",
|
||||
target: "large.bin",
|
||||
label: "Large",
|
||||
},
|
||||
} as {
|
||||
[key: whisper_model_tag_t]: { source: string; target: string; label: string };
|
||||
};
|
||||
|
||||
export function getWhisperTarget(key : whisper_model_tag_t) {
|
||||
const path = Paths.join(WHISPER_MODEL_DIR, WHISPER_MODELS[key].target);
|
||||
return new File(path)
|
||||
}
|
||||
|
||||
export type download_status =
|
||||
| {
|
||||
status: "not_started" | "complete";
|
||||
}
|
||||
| {
|
||||
status: "in_progress";
|
||||
bytes: {
|
||||
total: number;
|
||||
done: number;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getModelFileSize(whisper_model: whisper_model_tag_t) {
|
||||
const target = getWhisperTarget(whisper_model)
|
||||
if (!target.exists) return undefined;
|
||||
return target.size;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param whisper_model The whisper model key to check (e.g. `"small"`)
|
||||
* @returns
|
||||
*/
|
||||
export async function getWhisperDownloadStatus(
|
||||
whisper_model: whisper_model_tag_t
|
||||
): Promise<download_status> {
|
||||
// const files = await FileSystem.readDirectoryAsync("file:///whisper");
|
||||
const result = (await (
|
||||
await getDb()
|
||||
).getFirstSync(
|
||||
`
|
||||
SELECT (bytes_done, total) WHERE model = ?
|
||||
`,
|
||||
[whisper_model]
|
||||
)) as { bytes_done: number; total: number } | undefined;
|
||||
|
||||
if (!result)
|
||||
return {
|
||||
status: "not_started",
|
||||
};
|
||||
|
||||
if (result.bytes_done < result.total)
|
||||
return {
|
||||
status: "in_progress",
|
||||
bytes: {
|
||||
done: result.bytes_done,
|
||||
total: result.total,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
status: "complete",
|
||||
};
|
||||
}
|
||||
|
||||
export function whisperFileExists(whisper_model : whisper_model_tag_t) {
|
||||
const target = getWhisperTarget(whisper_model);
|
||||
return target.exists
|
||||
}
|
||||
|
||||
export async function downloadWhisperModel(
|
||||
whisper_model: whisper_model_tag_t,
|
||||
options: {
|
||||
force_redownload: boolean;
|
||||
} = {
|
||||
force_redownload: false,
|
||||
}
|
||||
) {
|
||||
|
||||
if (!WHISPER_MODEL_DIR.exists) {
|
||||
await FileSystem.makeDirectoryAsync(WHISPER_MODEL_PATH, {
|
||||
intermediates: true,
|
||||
})
|
||||
}
|
||||
|
||||
const whisperTarget = getWhisperTarget(whisper_model);
|
||||
|
||||
// If the target file exists, delete it.
|
||||
if (whisperTarget.exists) {
|
||||
if (options.force_redownload) {
|
||||
whisperTarget.delete()
|
||||
} else {
|
||||
console.warn("Whisper model for %s already exists", whisper_model);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate a new resumable download.
|
||||
const spec = WHISPER_MODELS[whisper_model];
|
||||
const resumable = FileSystem.createDownloadResumable(
|
||||
spec.source,
|
||||
whisperTarget.uri,
|
||||
undefined,
|
||||
// On each data write, update the whisper model download status.
|
||||
// Note that since createDownloadResumable callback only works in the foreground,
|
||||
// a background process will also be updating the file size.
|
||||
async (data) => {
|
||||
const db = await getDb();
|
||||
const args = [
|
||||
whisper_model,
|
||||
data.totalBytesWritten,
|
||||
data.totalBytesExpectedToWrite,
|
||||
];
|
||||
await db.runAsync(
|
||||
`INSERT OR REPLACE INTO whisper_models (model, bytes_done, bytes_remaining) VALUES (?, ?, ?)`,
|
||||
args
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await resumable.downloadAsync();
|
||||
}
|
36
app/service/download.ts
Normal file
36
app/service/download.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Text, View, Button, Platform } from 'react-native';
|
||||
import * as Device from 'expo-device';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
|
||||
export function initNotifications() {
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function sendPushNotification(expoPushToken: string) {
|
||||
const message = {
|
||||
to: expoPushToken,
|
||||
sound: 'default',
|
||||
title: 'Original Title',
|
||||
body: 'And here is the body!',
|
||||
data: { someData: 'goes here' },
|
||||
};
|
||||
|
||||
await fetch('https://exp.host/--/api/v2/push/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-encoding': 'gzip, deflate',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user