add readme. fix downloader operation.
This commit is contained in:
parent
4549442bd8
commit
d00e6d62ff
226
README.md
226
README.md
@ -1,195 +1,81 @@
|
||||
# react-native-linear-gradient
|
||||
# Translation Terrace
|
||||
|
||||
A `<LinearGradient>` element for React Native
|
||||
Translation Terrace is an Expo/React-Native application designed for a translation kiosk. It leverages an offline Whisper speech-to-text model and interfaces with a LibreTranslate server to provide seamless translation services.
|
||||
|
||||
[![ci][1]][2]
|
||||
[![npm version][3]][4]
|
||||
[![npm downloads][5]][4]
|
||||
## Project Structure
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/8ff2a78b-f0b1-463a-aa5b-555df2e71360" width="300"> <img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/9c738be3-6fba-43d5-9c9f-1db1c10fd377" width="300">
|
||||
</p>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage and Examples](#examples)
|
||||
- [Props](#props)
|
||||
- [Example App](#an-example-app)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Other Platforms](#other-platforms)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add react-native-linear-gradient
|
||||
```
|
||||
translation-terraces/
|
||||
├── expo-file-system/
|
||||
│ └── next.js
|
||||
├── @react-native-async-storage/
|
||||
│ └── async-storage.ts
|
||||
├── .ollama/
|
||||
│ ├── ExampleComponent.tsx
|
||||
│ └── ExampleTest.spec.tsx
|
||||
├── package.json
|
||||
├── README.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Or, using npm: `npm install react-native-linear-gradient`
|
||||
## Technologies Used
|
||||
|
||||
## Examples
|
||||
- **React-Native**: Core framework for building the application.
|
||||
- **Whisper Speech-to-Text Model**: Offline model for converting spoken language to text.
|
||||
- **LibreTranslate Server**: Interface for translation services.
|
||||
|
||||
[react-native-login](https://github.com/brentvatne/react-native-login) is a
|
||||
legacy component which showcases the use of `<LinearGradient>`.
|
||||
## Installation Instructions
|
||||
|
||||
### Simple
|
||||
1. **Install Packages**: Run `npm install` to install all necessary dependencies as listed in `package.json`.
|
||||
2. **Prebuild Android**: Since this project cannot be run using Expo Go, prebuild the Android app by running `npm run android`.
|
||||
3. **Run Unit Tests**: Execute Jest unit tests with `npm run test`.
|
||||
|
||||
The following code will produce something like this:
|
||||
## Usage Examples
|
||||
|
||||

|
||||
### Example 1: Initializing Whisper Model
|
||||
|
||||
```javascript
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { initializeWhisper } from './path/to/whisper';
|
||||
|
||||
// Within your render function
|
||||
<LinearGradient colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
|
||||
<Text style={styles.buttonText}>
|
||||
Sign in with Facebook
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
const initModel = async () => {
|
||||
try {
|
||||
await initializeWhisper();
|
||||
console.log('Whisper model initialized successfully.');
|
||||
} catch (error) {
|
||||
console.error('Error initializing Whisper model:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Later on in your styles..
|
||||
var styles = StyleSheet.create({
|
||||
linearGradient: {
|
||||
flex: 1,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
borderRadius: 5
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 18,
|
||||
fontFamily: 'Gill Sans',
|
||||
textAlign: 'center',
|
||||
margin: 10,
|
||||
color: '#ffffff',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
initModel();
|
||||
```
|
||||
|
||||
### Horizontal gradient
|
||||
|
||||
Using the styles from above, set `start` and `end` like this to make the gradient go from left to right, instead of from top to bottom:
|
||||
### Example 2: Translating Text
|
||||
|
||||
```javascript
|
||||
<LinearGradient start={{x: 0, y: 0}} end={{x: 1, y: 0}} colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
|
||||
<Text style={styles.buttonText}>
|
||||
Sign in with Facebook
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
import { translateText } from './path/to/libreTranslate';
|
||||
|
||||
const translate = async () => {
|
||||
try {
|
||||
const translatedText = await translateText('Hello, world!', 'en', 'es');
|
||||
console.log('Translated text:', translatedText);
|
||||
} catch (error) {
|
||||
console.error('Error translating text:', error);
|
||||
}
|
||||
};
|
||||
|
||||
translate();
|
||||
```
|
||||
|
||||
### Text gradient (iOS)
|
||||
## Contributing Guidelines
|
||||
|
||||
On iOS you can use the `MaskedViewIOS` to display text with a gradient. The trick here is to render the text twice; once for the mask, and once to let the gradient have the correct size (hence the `opacity: 0`):
|
||||
1. **Create Feature Branch**: Use the naming convention `<year>-<work week>_<username>_<feature_description>` for your feature branch.
|
||||
2. **Initiate Pull Request (PR)**: Open a PR and provide a detailed description of the changes made.
|
||||
3. **Discuss Features**: Suggest new features or improvements in GitTea issues.
|
||||
|
||||
```jsx
|
||||
<MaskedViewIOS maskElement={<Text style={styles.text} />}>
|
||||
<LinearGradient colors={['#f00', '#0f0']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}>
|
||||
<Text style={[styles.text, { opacity: 0 }]} />
|
||||
</LinearGradient>
|
||||
</MaskedViewIOS>
|
||||
```
|
||||
## License Information
|
||||
|
||||
### Animated Gradient
|
||||
This project is licensed under the MIT License. For more details, please refer to the [LICENSE](LICENSE) file.
|
||||
|
||||
Check out the [example app](https://github.com/react-native-linear-gradient/react-native-linear-gradient/tree/HEAD/example/) (`git clone` this project, cd into it, npm install, open in Xcode and run) to see how this is done:
|
||||
---
|
||||
|
||||

|
||||
|
||||
*This gif was created using [licecap](http://www.cockos.com/licecap/) - a great piece of free OSS*
|
||||
|
||||
### Transparent Gradient
|
||||
|
||||
The use of `transparent` color will most likely not lead to the expected result. `transparent` is actually a transparent black color (`rgba(0, 0, 0, 0)`). If you need a gradient in which the color is "fading", you need to have the same color with changing alpha channel. Example:
|
||||
|
||||
```jsx
|
||||
// RGBA
|
||||
|
||||
<LinearGradient colors={['rgba(255, 255, 255, 0)', 'rgba(255, 255, 255, 1)']} {...otherGradientProps} />
|
||||
|
||||
// Hex
|
||||
|
||||
<LinearGradient colors={['#FFFFFF00', '#FFFFFF']} {...otherGradientProps} />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
In addition to regular `View` props, you can also provide additional props to customize your gradient look:
|
||||
|
||||
#### colors
|
||||
|
||||
An array of at least two color values that represent gradient colors. Example: `['red', 'blue']` sets gradient from red to blue.
|
||||
|
||||
#### start
|
||||
|
||||
An optional object of the following type: `{ x: number, y: number }`. Coordinates declare the position that the gradient starts at, as a fraction of the overall size of the gradient, starting from the top left corner. Example: `{ x: 0.1, y: 0.1 }` means that the gradient will start 10% from the top and 10% from the left.
|
||||
|
||||
#### end
|
||||
|
||||
Same as start, but for the end of the gradient.
|
||||
|
||||
#### locations
|
||||
|
||||
An optional array of numbers defining the location of each gradient color stop, mapping to the color with the same index in `colors` prop. Example: `[0.1, 0.75, 1]` means that first color will take 0% - 10%, second color will take 10% - 75% and finally third color will occupy 75% - 100%.
|
||||
|
||||
```javascript
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 0.25}} end={{x: 0.5, y: 1.0}}
|
||||
locations={[0,0.5,0.6]}
|
||||
colors={['#4c669f', '#3b5998', '#192f6a']}
|
||||
style={styles.linearGradient}>
|
||||
<Text style={styles.buttonText}>
|
||||
Sign in with Facebook
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### useAngle / angle / angleCenter
|
||||
|
||||
You may want to achieve an angled gradient effect, similar to those in image editors like Photoshop.
|
||||
One issue is that you have to calculate the angle based on the view's size, which only happens asynchronously and will cause unwanted flickr.
|
||||
|
||||
In order to do that correctly you can set `useAngle={true} angle={45} angleCenter={{x:0.5,y:0.5}}`, to achieve a gradient with a 45 degrees angle, with its center positioned in the view's exact center.
|
||||
|
||||
`useAngle` is used to turn on/off angle based calculation (as opposed to `start`/`end`).
|
||||
`angle` is the angle in degrees.
|
||||
`angleCenter` is the center point of the angle (will control the weight and stretch of the gradient like it does in photoshop.
|
||||
|
||||
## An example app
|
||||
|
||||
You can see this component in action in [brentvatne/react-native-login](https://github.com/brentvatne/react-native-login/blob/HEAD/App/Screens/LoginScreen.js#L58-L62).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### iOS build fails: library not found, "BVLinearGradient" was not found in the UIManager
|
||||
|
||||
1. Ensure to run `pod install` before running the app on iOS
|
||||
2. Ensure you use `ios/**.xcworkspace` file instead of `ios./**.xcodeproj`
|
||||
|
||||
### Other
|
||||
|
||||
Clearing build caches and reinstalling dependencies sometimes solve some issues. Try next steps:
|
||||
|
||||
1. Reinstalling `node_modules` with `rm -rf node_modules && yarn`
|
||||
2. Clearing Android Gradle cache with `(cd android && ./gradlew clean)`
|
||||
3. Reinstalling iOS CocoaPods with `(cd ios && rm -rf ./ios/Pods/**) && npx pod-install`
|
||||
4. Clearing Xcode Build cache (open Xcode and go to Product -> Clean Build Folder)
|
||||
|
||||
For other troubleshooting issues, go to [React Native Troubleshooting](https://reactnative.dev/docs/troubleshooting.html)
|
||||
|
||||
## Other platforms
|
||||
|
||||
- Web: [react-native-web-community/react-native-web-linear-gradient](https://github.com/react-native-web-community/react-native-web-linear-gradient)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
[1]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/workflows/ci/badge.svg
|
||||
[2]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/actions
|
||||
[3]: https://img.shields.io/npm/v/react-native-linear-gradient.svg
|
||||
[4]: https://www.npmjs.com/package/react-native-linear-gradient
|
||||
[5]: https://img.shields.io/npm/dm/react-native-linear-gradient.svg
|
||||
Feel free to explore the codebase and contribute to Translation Terrace! If you have any questions or need further assistance, feel free to reach out.
|
@ -17,7 +17,7 @@
|
||||
<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"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="landscape">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
@ -3,7 +3,7 @@ import * as 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_PATH = Paths.join(FileSystem.documentDirectory || "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
|
||||
@ -51,19 +51,19 @@ 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",
|
||||
"https://huggingface.co/openai/whisper-small/blob/resolve/pytorch_model.bin",
|
||||
target: "small.bin",
|
||||
label: "Small",
|
||||
},
|
||||
medium: {
|
||||
source:
|
||||
"https://huggingface.co/openai/whisper-medium/blob/main/pytorch_model.bin",
|
||||
"https://huggingface.co/openai/whisper-medium/resolve/main/pytorch_model.bin",
|
||||
target: "medium.bin",
|
||||
label: "Medium",
|
||||
},
|
||||
large: {
|
||||
source:
|
||||
"https://huggingface.co/openai/whisper-large/blob/main/pytorch_model.bin",
|
||||
"https://huggingface.co/openai/whisper-large/resolve/main/pytorch_model.bin",
|
||||
target: "large.bin",
|
||||
label: "Large",
|
||||
},
|
||||
@ -167,12 +167,9 @@ export async function initiateWhisperDownload(
|
||||
|
||||
console.debug("Starting download of %s", whisper_model);
|
||||
|
||||
if (!WHISPER_MODEL_DIR.exists) {
|
||||
await FileSystem.makeDirectoryAsync(WHISPER_MODEL_PATH, {
|
||||
intermediates: true,
|
||||
});
|
||||
console.debug("Created %s", WHISPER_MODEL_DIR);
|
||||
}
|
||||
|
||||
const whisperTarget = getWhisperTarget(whisper_model);
|
||||
|
||||
@ -197,7 +194,14 @@ export async function initiateWhisperDownload(
|
||||
const resumable = FileSystem.createDownloadResumable(
|
||||
spec.source,
|
||||
whisperTarget.uri,
|
||||
{},
|
||||
{
|
||||
md5: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Accept': 'application/octet-stream',
|
||||
},
|
||||
sessionType: FileSystem.FileSystemSessionType.BACKGROUND
|
||||
},
|
||||
// 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.
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
getWhisperTarget,
|
||||
whisper_model_tag_t,
|
||||
} from "@/app/lib/whisper";
|
||||
import { Paths } from "expo-file-system/next";
|
||||
import { File, Paths } from "expo-file-system/next";
|
||||
|
||||
type Language = {
|
||||
code: string;
|
||||
@ -30,12 +30,12 @@ type LanguageMatrix = {
|
||||
|
||||
type connection_test_t =
|
||||
| {
|
||||
success: true;
|
||||
}
|
||||
success: true;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
const SettingsComponent: React.FC = () => {
|
||||
const [hostLanguage, setHostLanguage] = useState<string | null>(null);
|
||||
@ -58,6 +58,7 @@ const SettingsComponent: React.FC = () => {
|
||||
FileSystem.DownloadProgressData | undefined
|
||||
>();
|
||||
const [downloader, setDownloader] = useState<DownloadResumable | undefined>();
|
||||
const [whisperModelTarget, setWhisperModelTarget] = useState<File | undefined>()
|
||||
|
||||
const fillHostLanguageOptions = async () => {
|
||||
const settings = await Settings.getDefault();
|
||||
@ -96,10 +97,11 @@ const SettingsComponent: React.FC = () => {
|
||||
setLibretranslateBaseUrl(libretranslateUrl);
|
||||
console.log("libretranslate url = %s", libretranslateUrl);
|
||||
try {
|
||||
const wModel = await settings.getWhisperModel();
|
||||
setWhisperModel(wModel || "small");
|
||||
const wModel = await settings.getWhisperModel() || "small";
|
||||
setWhisperModel(wModel);
|
||||
setWhisperModelTarget(new File(WHISPER_MODELS[wModel].target))
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
console.warn("Could not set whisper model: %s", err);
|
||||
}
|
||||
|
||||
// setWhisperModel(wModel);
|
||||
@ -112,7 +114,9 @@ const SettingsComponent: React.FC = () => {
|
||||
if (!whisperModel) return null;
|
||||
const dlStatus = await getWhisperDownloadStatus(whisperModel);
|
||||
setDownloadStatus(dlStatus);
|
||||
}, 200);
|
||||
setWhisperModelTarget(new File(WHISPER_MODELS[whisperModel].target))
|
||||
console.log("Setting whisper model target to %s", whisperModelTarget);
|
||||
}, 1000);
|
||||
|
||||
setInterval(async () => {
|
||||
if (!libretranslateBaseUrl) return;
|
||||
@ -150,6 +154,16 @@ const SettingsComponent: React.FC = () => {
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const fileExists = async (file: File) => {
|
||||
const info = await FileSystem.getInfoAsync(file.uri);
|
||||
return info.exists;
|
||||
}
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!whisperModelTarget) return;
|
||||
whisperModelTarget.delete();
|
||||
}
|
||||
|
||||
const doReadownload = async () => {
|
||||
if (!whisperModel) return;
|
||||
await initiateWhisperDownload(whisperModel, {
|
||||
@ -164,6 +178,7 @@ const SettingsComponent: React.FC = () => {
|
||||
setDownloader(
|
||||
await initiateWhisperDownload(whisperModel, {
|
||||
onDownload: setWhisperDownloadProgress,
|
||||
force_redownload: true,
|
||||
})
|
||||
);
|
||||
await downloader?.downloadAsync();
|
||||
@ -195,6 +210,18 @@ const SettingsComponent: React.FC = () => {
|
||||
await fillHostLanguageOptions();
|
||||
};
|
||||
|
||||
const handleWhisperModelChange = async (value: string) => {
|
||||
const settings = await Settings.getDefault();
|
||||
setWhisperModel(value);
|
||||
await settings.setWhisperModel(value);
|
||||
setWhisperModelTarget(getWhisperTarget(value));
|
||||
}
|
||||
|
||||
const doStopDownload = async () => {
|
||||
if (!downloader) return;
|
||||
await downloader.pauseAsync()
|
||||
}
|
||||
|
||||
return isLoaded ? (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>Host Language:</Text>
|
||||
@ -226,9 +253,9 @@ const SettingsComponent: React.FC = () => {
|
||||
</Text>
|
||||
))}
|
||||
<Picker
|
||||
selectedValue={whisperModel || ""}
|
||||
selectedValue={whisperModel || "small"}
|
||||
style={{ height: 50, width: "100%" }}
|
||||
onValueChange={setWhisperModel}
|
||||
onValueChange={handleWhisperModelChange}
|
||||
accessibilityHint="language"
|
||||
>
|
||||
{Object.entries(WHISPER_MODELS).map(([key, { label }]) => (
|
||||
@ -239,13 +266,28 @@ const SettingsComponent: React.FC = () => {
|
||||
{ /* If there's a downloader, that means we're in the middle of a download */}
|
||||
{downloader && whisperDownloadProgress && (
|
||||
<Text>
|
||||
{whisperDownloadProgress.totalBytesWritten} of {whisperDownloadProgress.totalBytesExpectedToWrite}
|
||||
{whisperDownloadProgress.totalBytesWritten} bytes of {whisperDownloadProgress.totalBytesExpectedToWrite} bytes
|
||||
|
||||
({Math.round((whisperDownloadProgress.totalBytesWritten / whisperDownloadProgress.totalBytesExpectedToWrite) * 100)} %)
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
<Pressable onPress={doDownload} style={styles.button}>
|
||||
<Text style={styles.buttonText}>Download</Text>
|
||||
</Pressable>
|
||||
<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>)
|
||||
}
|
||||
{whisperModelTarget && fileExists(whisperModelTarget) &&
|
||||
(<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.buttonText}>Delete</Text>
|
||||
</Pressable>)
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
@ -257,14 +299,26 @@ const SettingsComponent: React.FC = () => {
|
||||
|
||||
// Create styles for the component
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
backgroundColor: "blue",
|
||||
downloadButtonWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
display: "flex",
|
||||
flexShrink: 1,
|
||||
padding: 20,
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
downloadButton: {
|
||||
backgroundColor: "blue",
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: "darkred",
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
},
|
||||
pauseDownloadButton: {
|
||||
backgroundColor: "#444444",
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
},
|
||||
buttonText: {
|
||||
color: "white",
|
||||
|
Loading…
x
Reference in New Issue
Block a user