Compare commits
2 Commits
dca3987e18
...
123933d459
Author | SHA1 | Date | |
---|---|---|---|
|
123933d459 | ||
|
f0a722b3fb |
@ -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>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
@ -13,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"/>
|
||||
|
5
app.json
5
app.json
@ -57,10 +57,11 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-audio"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
app/lib/readstream.ts
Normal file
61
app/lib/readstream.ts
Normal 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);
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
import { TextDecoder } from "util";
|
||||
|
||||
export async function arrbufToStr(arrayBuffer : ArrayBuffer) {
|
||||
export function arrbufToStr(arrayBuffer : ArrayBuffer) {
|
||||
return new TextDecoder().decode(new Uint8Array(arrayBuffer));
|
||||
}
|
||||
|
||||
export function strToArrBuf(input : string) : Uint8Array<ArrayBufferLike> {
|
||||
return new TextEncoder().encode(input)
|
||||
}
|
@ -3,7 +3,8 @@ 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 } from "./util";
|
||||
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() {
|
||||
@ -154,16 +165,25 @@ export class WhisperFile {
|
||||
console.debug("%s does not exist", this.targetPath);
|
||||
return undefined;
|
||||
}
|
||||
return await Crypto.digest(
|
||||
|
||||
const strData = await FileSystem.readAsStringAsync(this.targetPath, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
const data = strToArrBuf(strData);
|
||||
|
||||
|
||||
const digest = await Crypto.digest(
|
||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||
this.targetFile.bytes()
|
||||
data
|
||||
);
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
public async updateTargetHash() {
|
||||
const targetSha = await this.getTargetSha();
|
||||
if (!targetSha) return;
|
||||
this.target_hash = await arrbufToStr(targetSha);
|
||||
this.target_hash = arrbufToStr(targetSha);
|
||||
}
|
||||
|
||||
get isHashValid() {
|
||||
@ -238,9 +258,11 @@ export class WhisperFile {
|
||||
async createDownloadResumable(
|
||||
options: {
|
||||
onData?: DownloadCallback | undefined;
|
||||
onComplete?: CompletionCallback | undefined;
|
||||
} = {
|
||||
onData: undefined,
|
||||
}
|
||||
onData: undefined,
|
||||
onComplete: undefined,
|
||||
}
|
||||
) {
|
||||
await this.syncHfMetadata();
|
||||
|
||||
@ -254,28 +276,70 @@ export class WhisperFile {
|
||||
// Check for the existence of the target file
|
||||
// If it exists, load the existing data.
|
||||
await this.updateTargetExistence();
|
||||
const existingData = this.does_target_exist
|
||||
? this.targetFile.text()
|
||||
: undefined;
|
||||
|
||||
// Create the resumable.
|
||||
return FileSystem.createDownloadResumable(
|
||||
this.modelUrl,
|
||||
this.targetPath,
|
||||
{},
|
||||
async (data: FileSystem.DownloadProgressData) => {
|
||||
this.download_data = data;
|
||||
await this.syncHfMetadata();
|
||||
await this.updateTargetHash();
|
||||
await this.updateTargetExistence();
|
||||
if (options.onData) await options.onData(this);
|
||||
},
|
||||
existingData ? existingData : undefined
|
||||
);
|
||||
if (this.does_part_target_exist) {
|
||||
options.onComplete && options.onComplete(this)
|
||||
}
|
||||
|
||||
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.targetPath,
|
||||
{},
|
||||
async (data: FileSystem.DownloadProgressData) => {
|
||||
// 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 === 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
|
||||
);
|
||||
} 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"),
|
||||
|
@ -1,9 +1,14 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||
import { Alert, ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||
import { useNavigation, Route } from "@react-navigation/native";
|
||||
import { Conversation, Message } from "@/app/lib/conversation";
|
||||
import MessageBubble from "@/components/ui/MessageBubble";
|
||||
import { CachedTranslator, LanguageServer, language_matrix_entry } from "@/app/i18n/api";
|
||||
import { Settings } from "@/app/lib/settings";
|
||||
import { WHISPER_FILES } from "@/app/lib/whisper";
|
||||
import { initWhisper, WhisperContext } from 'whisper.rn'
|
||||
import { useAudioRecorder, AudioModule, RecordingPresets } from 'expo-audio';
|
||||
|
||||
|
||||
const lasOptions = {
|
||||
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
|
||||
@ -14,9 +19,9 @@ const lasOptions = {
|
||||
};
|
||||
// LiveAudioStream.init(lasOptions as any);
|
||||
|
||||
const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversation : Conversation}>}) => {
|
||||
const ConversationThread = ({ route }: { route?: Route<"Conversation", { conversation: Conversation }> }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
|
||||
if (!route) {
|
||||
return (<View><Text>Missing Params!</Text></View>)
|
||||
}
|
||||
@ -27,6 +32,7 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [guestSpeak, setGuestSpeak] = useState<string | undefined>();
|
||||
const [guestSpeakLoaded, setGuestSpeakLoaded] = useState<boolean>(false);
|
||||
const [whisperContext, setWhisperContext] = useState<WhisperContext | undefined>();
|
||||
const [cachedTranslator, setCachedTranslator] = useState<
|
||||
undefined | CachedTranslator
|
||||
>();
|
||||
@ -42,17 +48,50 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
|
||||
}
|
||||
}>()
|
||||
|
||||
// recorder settings
|
||||
const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
|
||||
|
||||
const record = async () => {
|
||||
await audioRecorder.prepareToRecordAsync();
|
||||
audioRecorder.record();
|
||||
};
|
||||
|
||||
const stopRecording = async () => {
|
||||
// The recording will be available on `audioRecorder.uri`.
|
||||
await audioRecorder.stop();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
(async function () {
|
||||
|
||||
const languageServer = await LanguageServer.getDefault();
|
||||
const languages = await languageServer.fetchLanguages();
|
||||
const cc = new CachedTranslator(
|
||||
"en",
|
||||
conversation.guest.language,
|
||||
languageServer,
|
||||
)
|
||||
setCachedTranslator(cc);
|
||||
try {
|
||||
const languages = await languageServer.fetchLanguages(5000);
|
||||
const cc = new CachedTranslator(
|
||||
"en",
|
||||
conversation.guest.language,
|
||||
languageServer,
|
||||
)
|
||||
console.log("Set cached translator from %s", languageServer.baseUrl)
|
||||
setCachedTranslator(cc);
|
||||
} catch (err) {
|
||||
console.error("Could not set translator from %s: %s", languageServer.baseUrl, err)
|
||||
}
|
||||
|
||||
const settings = await Settings.getDefault();
|
||||
const whisperFile = WHISPER_FILES[await settings.getWhisperModel() || "en"];
|
||||
setWhisperContext(await initWhisper({
|
||||
filePath: whisperFile.targetPath,
|
||||
}));
|
||||
|
||||
// recorder settings
|
||||
|
||||
(async () => {
|
||||
const status = await AudioModule.requestRecordingPermissionsAsync();
|
||||
if (!status.granted) {
|
||||
Alert.alert('Permission to access microphone was denied');
|
||||
}
|
||||
})();
|
||||
setGuestSpeak(await cc.translate("Speak"));
|
||||
const hostLang1 = languages[conversation.host.language].name;
|
||||
const guestLang1 = languages[conversation.host.language].name;
|
||||
@ -69,18 +108,22 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
|
||||
}
|
||||
})
|
||||
})();
|
||||
|
||||
|
||||
const updateMessages = (c: Conversation) => {
|
||||
setMessages([...c]);
|
||||
};
|
||||
|
||||
conversation.onAddMessage = updateMessages;
|
||||
conversation.onTranslationDone = updateMessages;
|
||||
if (!conversation) {
|
||||
console.warn("Conversation is null or undefined.")
|
||||
}
|
||||
|
||||
return () => {
|
||||
conversation.onAddMessage = undefined;
|
||||
conversation.onTranslationDone = undefined;
|
||||
};
|
||||
conversation.on("add_message", updateMessages);
|
||||
conversation.on("translation_done", updateMessages);
|
||||
|
||||
// return () => {
|
||||
// conversation.on("add_message", undefined);
|
||||
// conversation.on("translation_done", undefined);
|
||||
// };
|
||||
}, [conversation, guestSpeak]);
|
||||
|
||||
const renderMessages = () =>
|
||||
@ -91,8 +134,8 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
|
||||
return cachedTranslator ? (
|
||||
<View style={{ flex: 1, flexDirection: "column" }}>
|
||||
{languageLabels && (<View style={styles.languageLabels}>
|
||||
<Text style={styles.nativeHostLabel}>{ languageLabels.hostNative.host } / { languageLabels.hostNative.guest }</Text>
|
||||
<Text style={styles.nativeGuestLabel}>{ languageLabels.guestNative.host } / { languageLabels.guestNative.guest }</Text>
|
||||
<Text style={styles.nativeHostLabel}>{languageLabels.hostNative.host} / {languageLabels.hostNative.guest}</Text>
|
||||
<Text style={styles.nativeGuestLabel}>{languageLabels.guestNative.host} / {languageLabels.guestNative.guest}</Text>
|
||||
</View>)
|
||||
}
|
||||
<ScrollView
|
||||
|
@ -18,7 +18,7 @@ export function LanguageSelection(props: {
|
||||
}) {
|
||||
const [languages, setLanguages] = useState<language_matrix | undefined>();
|
||||
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
|
||||
const [translator, setTranslator] = useState<Translator|undefined>();
|
||||
const [translator, setTranslator] = useState<Translator | undefined>();
|
||||
|
||||
const nav = useNavigation();
|
||||
|
||||
@ -56,7 +56,7 @@ export function LanguageSelection(props: {
|
||||
{(languages && languagesLoaded) ? Object.entries(languages).filter((l) => (LANG_FLAGS as any)[l[0]] !== undefined).map(
|
||||
([lang, lang_entry]) => {
|
||||
return (
|
||||
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} translator={translator} />
|
||||
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} translator={translator} />
|
||||
);
|
||||
}
|
||||
) : <Text>Waiting...</Text>
|
||||
|
@ -54,9 +54,12 @@ const SettingsComponent = () => {
|
||||
);
|
||||
setWhisperModel((await settings.getWhisperModel()) || "small");
|
||||
setWhisperFile(WHISPER_FILES[whisperModel]);
|
||||
await whisperFile?.syncHfMetadata();
|
||||
await whisperFile?.updateTargetExistence();
|
||||
await whisperFile?.updateTargetHash();
|
||||
if (whisperFile) {
|
||||
await whisperFile.syncHfMetadata();
|
||||
await whisperFile.updateTargetExistence();
|
||||
await whisperFile.updateTargetHash();
|
||||
setWhisperFileExists(whisperFile.does_target_exist)
|
||||
}
|
||||
})();
|
||||
}, [whisperFile]);
|
||||
|
||||
@ -94,8 +97,8 @@ const SettingsComponent = () => {
|
||||
const wFile = WHISPER_FILES[whisperModel];
|
||||
await wFile.syncHfMetadata();
|
||||
await wFile.updateTargetExistence();
|
||||
await wFile.updateTargetHash();
|
||||
setIsWhisperHashValid(wFile.isHashValid);
|
||||
// await wFile.updateTargetHash();
|
||||
// setIsWhisperHashValid(wFile.isHashValid);
|
||||
setWhisperFile(wFile);
|
||||
setWhisperFileExists(wFile.does_target_exist);
|
||||
};
|
||||
@ -107,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.");
|
||||
@ -119,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);
|
||||
@ -184,19 +193,30 @@ const SettingsComponent = () => {
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
<View>
|
||||
{/* <Text>whisper file: { whisperFile?.tag }</Text> */}
|
||||
{whisperFile &&
|
||||
( whisperFileExists && (<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text>DELETE {whisperModel.toUpperCase()}</Text>
|
||||
</Pressable>))
|
||||
}
|
||||
<Pressable onPress={doDownload} style={styles.pauseDownloadButton}>
|
||||
<Text>DOWNLOAD {whisperModel.toUpperCase()}</Text>
|
||||
<View style={styles.downloadButtonWrapper}>
|
||||
{((!downloader) && whisperFile) &&
|
||||
(whisperFile.does_target_exist && (<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.buttonText}>DELETE {whisperModel.toUpperCase()}</Text>
|
||||
</Pressable>))
|
||||
}
|
||||
<Pressable onPress={doDownload} style={styles.downloadButton}>
|
||||
<Text style={styles.buttonText}>DOWNLOAD {whisperModel.toUpperCase()}</Text>
|
||||
</Pressable>
|
||||
))
|
||||
|
||||
{
|
||||
downloader && (
|
||||
<Pressable onPress={doStopDownload} style={styles.pauseDownloadButton}>
|
||||
<Text style={styles.buttonText}>STOP DOWNLOAD</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
{bytesDone && bytesRemaining && (
|
||||
)
|
||||
}
|
||||
{bytesDone && bytesRemaining && whisperFile?.does_part_target_exist && (
|
||||
<View>
|
||||
{whisperFile &&
|
||||
(<Text>
|
||||
Downloading to {whisperFile.targetPath}
|
||||
</Text>)}
|
||||
<Text>
|
||||
{bytesDone} of{" "}
|
||||
{bytesRemaining} (
|
||||
|
@ -92,7 +92,7 @@ const ISpeakButton = (props: ISpeakButtonProps) => {
|
||||
}, []);
|
||||
|
||||
const countries =
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
DEFAULT_FLAGS[props.language.code] || chooseCountry(props.language.code);
|
||||
|
||||
return title ? (
|
||||
@ -107,10 +107,7 @@ const ISpeakButton = (props: ISpeakButtonProps) => {
|
||||
{countries &&
|
||||
countries.map((c) => {
|
||||
return (
|
||||
<View>
|
||||
<Text>{c}</Text>
|
||||
<CountryFlag isoCode={c} size={25} key={c} />
|
||||
</View>
|
||||
<CountryFlag isoCode={c} size={25} key={c} />
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
129
package-lock.json
generated
129
package-lock.json
generated
@ -17,6 +17,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",
|
||||
@ -49,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"
|
||||
@ -65,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",
|
||||
@ -5046,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",
|
||||
@ -5589,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",
|
||||
@ -6030,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",
|
||||
@ -8162,6 +8212,17 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-audio": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-audio/-/expo-audio-0.3.5.tgz",
|
||||
"integrity": "sha512-gzpDH3vZI1FDL1Q8pXryACtNIW+idZ/zIZ8WqdTRzJuzxucazrG2gLXUS2ngcXQBn09Jyz4RUnU10Tu2N7/Hgg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-background-fetch": {
|
||||
"version": "13.0.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-13.0.5.tgz",
|
||||
@ -15204,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": {
|
||||
@ -16439,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",
|
||||
@ -16837,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",
|
||||
|
@ -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,6 +57,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"
|
||||
@ -85,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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user