Replace small example models, and add example geomodel (#2644)

* Update env.example

* Update model download script

* Delete geomodel.placeholder

* Update README.md

* Update e2e_ios.yml

* Update vision-plugin

* Accept android flavor for downloading models

* Change Android e2e env

* Replace Android e2e model download step

* Update comment

* Move Java setup step

* Revert "Move Java setup step"

This reverts commit d8ca01a176.
This commit is contained in:
Johannes Klein
2025-02-03 12:16:22 +01:00
committed by GitHub
parent 7e19fd58dc
commit c378c8894d
10 changed files with 144 additions and 157 deletions

View File

@@ -74,7 +74,7 @@ jobs:
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
JWT_ANONYMOUS_API_SECRET: ${{ secrets.JWT_ANONYMOUS_API_SECRET }}
GMAPS_API_KEY: ${{ secrets.GMAPS_API_KEY }}
run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\nANDROID_MODEL_FILE_NAME=small_inception_tf1.tflite\nANDROID_TAXONOMY_FILE_NAME=small_export_tax.csv\nIOS_MODEL_FILE_NAME=small_inception_tf1.mlmodel\nIOS_TAXONOMY_FILE_NAME=small_export_tax.json\n' "$JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" "$GMAPS_API_KEY" > .env
run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\nANDROID_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.tflite\nANDROID_TAXONOMY_FILE_NAME=taxonomy.csv\nANDROID_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.tflite\nIOS_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.mlmodel\nIOS_TAXONOMY_FILE_NAME=taxonomy.json\nIOS_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.mlmodel\nCV_MODEL_VERSION=small_2\n' "$JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" "$GMAPS_API_KEY" > .env
- name: Create keystore.properties file
env:
ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
@@ -92,8 +92,8 @@ jobs:
run: mv release.keystore android/app/release.keystore
# Download the example model otherwise an error alert will be shown on app start, requires .env
- name: Download a fake cv model and taxonomy file into the assets folder
run: npm run add-github-actions-test-model
- name: Download the small example cv and geomodel
run: npm run add-example-model -- -f=main
# Macos-latest runner has 3 Java versions pre-installed, if not specified as here, the build step errors with requiring at least Java 11 or higher
# So, this step is needed for the apk build step, but somehow this is breaking emulator setup, so it is placed here

View File

@@ -87,7 +87,7 @@ jobs:
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
JWT_ANONYMOUS_API_SECRET: ${{ secrets.JWT_ANONYMOUS_API_SECRET }}
run: |
printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\nANDROID_MODEL_FILE_NAME=small_inception_tf1.tflite\nANDROID_TAXONOMY_FILE_NAME=small_export_tax.csv\nIOS_MODEL_FILE_NAME=small_inception_tf1.mlmodel\nIOS_TAXONOMY_FILE_NAME=small_export_tax.json\nCV_MODEL_VERSION=2.13\n' \
printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\nANDROID_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.tflite\nANDROID_TAXONOMY_FILE_NAME=taxonomy.csv\nANDROID_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.tflite\nIOS_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.mlmodel\nIOS_TAXONOMY_FILE_NAME=taxonomy.json\nIOS_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.mlmodel\nCV_MODEL_VERSION=small_2\n' \
"$JWT_ANONYMOUS_API_SECRET" \
"$OAUTH_CLIENT_ID" \
"$OAUTH_CLIENT_SECRET" \

View File

@@ -20,14 +20,9 @@ See [CONTRIBUTING](CONTRIBUTING.md) for guidelines on contributing to this proje
1. To run on Android, do this `cp android/example-keystore.properties android/keystore.properties`. Fill in the relevant values. If you are a member of iNat staff, get them from another member of iNat Staff.
1. Add AI Camera model and taxonomy files. The computer vision model and Geomodel files are not part of the code repo, and have to be installed. The app itself will load the model file with the filename specified in a .env file. On Android, the current file names are specified in these env variables `ANDROID_MODEL_FILE_NAME`, `ANDROID_TAXONOMY_FILE_NAME`, and `ANDROID_GEOMODEL_FILE_NAME`. On iOS, the current file names are specified in these env variables `IOS_MODEL_FILE_NAME`, `IOS_TAXONOMY_FILE_NAME`, and `IOS_GEOMODEL_FILE_NAME`. After a fresh clone of the repo and copying the env.example file (see above), you have to add the files by following these steps:
1. Add the example model files by executing `npm run add-example-model`. If that does not work continue with the next step.
1. If the download script fails: The sample model files are available in this [`small_model.zip`](https://github.com/inaturalist/SeekReactNative/releases/tag/v2.9.1-138) file.
1. On Android, these files are named `small_inception_tf1.tflite` and `small_export_tax.csv`. Create a camera folder within Android assets (i.e. `android/app/src/debug/assets/camera`) and place the files there.
1. On iOS, these files are named `small_inception_tf1.mlmodel` and `small_export_tax.json` and should be added to the `ios` folder.
1. On iOS, in the `ios` folder, copy the Geomodel placeholder file `geomodel.placeholder` to `geomodel.mlmodel`. This file is just a placeholder to get the app to build. We'll release a functional Geomodel for development purposes soon.
1. Optional: Add Geomodel file. If you have access to the model file, you can add it by following these steps:
1. Uncomment and set the `ANDROID_GEOMODEL_FILE_NAME` and `IOS_GEOMODEL_FILE_NAME` variables in the `.env` and `.env.staging` files.
1. On Android, the file Geomodel should be placed in the `android/app/src/debug/assets/camera` folder.
1. On iOS, the Geomodel file should be placed in the `ios` folder.
1. If the download script fails: The sample model files are available in the latest release in this [`repository`](https://github.com/inaturalist/model-files).
1. On Android, these files are named `INatVision_Small_2_fact256_8bit.tflite`, `INatGeomodel_Small_2_8bit.tflite` and `taxonomy.csv`. Create a camera folder within Android assets (i.e. `android/app/src/debug/assets/camera`) and place the files there.
1. On iOS, these files are named `smallINatVision_Small_2_fact256_8bit.mlmodel`, `INatGeomodel_Small_2_8bit.mlmodel` and `taxonomy.json` and should be added to the `ios` folder.
### Set up pre-commit hooks

View File

@@ -22,14 +22,13 @@ GMAPS_API_KEY=some-key
# phase in xcode to hard link these files using file names xcode knows about,
# so we don't have to add these files to xcode in addition to declaring them
# here.
ANDROID_MODEL_FILE_NAME=small_inception_tf1.tflite
ANDROID_TAXONOMY_FILE_NAME=small_export_tax.csv
# Android Geomodel is not implemented yet, uncommenting this can cause unforeseen problems
#ANDROID_GEOMODEL_FILE_NAME=small_geomodel_tf1.tflite
IOS_MODEL_FILE_NAME=small_inception_tf1.mlmodel
IOS_TAXONOMY_FILE_NAME=small_export_tax.json
#IOS_GEOMODEL_FILE_NAME=small_geomodel_tf1.mlmodel
CV_MODEL_VERSION=1.0
ANDROID_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.tflite
ANDROID_TAXONOMY_FILE_NAME=taxonomy.csv
ANDROID_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.tflite
IOS_MODEL_FILE_NAME=INatVision_Small_2_fact256_8bit.mlmodel
IOS_TAXONOMY_FILE_NAME=taxonomy.json
IOS_GEOMODEL_FILE_NAME=INatGeomodel_Small_2_8bit.mlmodel
CV_MODEL_VERSION=small_2
# Fastlane
IOS_PROVISIONING_PROFILE_NAME="provisioning profile name"

View File

@@ -1206,7 +1206,7 @@ PODS:
- VisionCamera/React (4.0.5):
- React-Core
- VisionCamera/FrameProcessors
- VisionCameraPluginInatVision (4.2.1):
- VisionCameraPluginInatVision (4.2.2):
- React-Core
- Yoga (1.14.0)
@@ -1616,7 +1616,7 @@ SPEC CHECKSUMS:
RNVectorIcons: 102cd20472bf0d7cd15443d43cd87f9c97228ac3
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: f02de0b1b6b1516b327bd8215237a97e7386db8a
VisionCameraPluginInatVision: f2e065009ed10375293fe343834c6d946e884a8a
VisionCameraPluginInatVision: 69370c8be26b9b7372996d423f4fdb4c94e42ab1
Yoga: c716aea2ee01df6258550c7505fa61b248145ced
PODFILE CHECKSUM: eff4b75123af5d6680139a78c055b44ad37c269b

View File

Binary file not shown.

8
package-lock.json generated
View File

@@ -104,7 +104,7 @@
"sanitize-html": "^2.13.0",
"ts-jest": "^29.1.2",
"uuid": "^11.0.5",
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#dcad11b1532cb8b777eaaf78d6df0a704e8c71d3",
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#fb3c35d3b19c9bdeb4c0de359f3c1b8133b2a8eb",
"zustand": "^4.5.2"
},
"devDependencies": {
@@ -20703,9 +20703,9 @@
}
},
"node_modules/vision-camera-plugin-inatvision": {
"version": "4.2.1",
"resolved": "git+ssh://git@github.com/inaturalist/vision-camera-plugin-inatvision.git#dcad11b1532cb8b777eaaf78d6df0a704e8c71d3",
"integrity": "sha512-PRgGpmWL6/XIYAh6xS50YrHxoUQxSlWWzPdDYHzcYBct9gBESCF0NUeFctWeqCJGjDcmL7RKlCYdizivZyiPmQ==",
"version": "4.2.2",
"resolved": "git+ssh://git@github.com/inaturalist/vision-camera-plugin-inatvision.git#fb3c35d3b19c9bdeb4c0de359f3c1b8133b2a8eb",
"integrity": "sha512-pybIhPdLCgDOVurJmoDpnGxlaip6JLs8gAiHHPgy/NcO3aljJaWJhLqOex8vd4Ofk1G4mKc7Q528/Yg+1qqQZA==",
"license": "MIT",
"dependencies": {
"h3-js": "^4.1.0"

View File

@@ -39,7 +39,6 @@
"e2e": "npm run e2e:build && npm run e2e:test",
"icons": "./scripts/update-icon-font.sh",
"add-example-model": "node scripts/add-example-model.js",
"add-github-actions-test-model": "node scripts/add-github-actions-test-model.js",
"reassure": "reassure"
},
"dependencies": {
@@ -138,7 +137,7 @@
"sanitize-html": "^2.13.0",
"ts-jest": "^29.1.2",
"uuid": "^11.0.5",
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#dcad11b1532cb8b777eaaf78d6df0a704e8c71d3",
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#fb3c35d3b19c9bdeb4c0de359f3c1b8133b2a8eb",
"zustand": "^4.5.2"
},
"devDependencies": {

View File

@@ -2,64 +2,140 @@
const { DownloaderHelper } = require( "node-downloader-helper" );
const fs = require( "fs" ).promises;
const path = require( "path" );
// eslint-disable-next-line import/no-extraneous-dependencies
const Decompress = require( "decompress" );
const yargs = require( "yargs" );
const filename = "small_model.zip";
const modelURL
= "https://github.com/inaturalist/SeekReactNative/releases/download/v2.9.1-138/small_model.zip";
const binariesBaseDir
= "https://github.com/inaturalist/model-files/releases/download/v25.01.15";
const modelPath = path.join( __dirname, "..", "temp", "model" );
const examplePath = path.join( modelPath, "tf1 2" );
const androidModelFile = "small_inception_tf1.tflite";
const androidTaxonomyFile = "small_export_tax.csv";
const iosModelFile = "small_inception_tf1.mlmodel";
const iosTaxonomyFile = "small_export_tax.json";
const androidModelPath = path.join( examplePath, androidModelFile );
const androidTaxonomyPath = path.join( examplePath, androidTaxonomyFile );
const iosModelPath = path.join( examplePath, iosModelFile );
const iosTaxonomyPath = path.join( examplePath, iosTaxonomyFile );
const androidExt = "tflite";
const iosExt = "mlmodel";
const cvModelFilename = "INatVision_Small_2_fact256_8bit";
const geomodelFilename = "INatGeomodel_Small_2_8bit";
const androidDestinationPath
= path.join( __dirname, "..", "android", "app", "src", "debug", "assets", "camera" );
const iosDestinationPath = path.join( __dirname, "..", "ios" );
const androidCV = `${binariesBaseDir}/${cvModelFilename}.${androidExt}`;
const iosCV = `${binariesBaseDir}/${cvModelFilename}.${iosExt}`;
const androidGeo = `${binariesBaseDir}/${geomodelFilename}.${androidExt}`;
const iosGeo = `${binariesBaseDir}/${geomodelFilename}.${iosExt}`;
const taxonomyCSV = `${binariesBaseDir}/taxonomy.csv`;
const taxonomyJSON = `${binariesBaseDir}/taxonomy.json`;
( async () => {
console.log( `Downloading example model from '${modelURL}'...` );
await fs.mkdir( modelPath, { recursive: true } );
const dl = new DownloaderHelper( modelURL, modelPath );
const downloadAndroid = async argv => {
console.log( "argv", argv );
const androidFlavor = argv.androidFlavor || "debug";
console.log( "androidFlavor", androidFlavor );
const androidDestination = path.join(
__dirname,
"..",
"android",
"app",
"src",
androidFlavor,
"assets",
"camera"
);
const androidModel = path.join(
androidDestination,
`${cvModelFilename}.${androidExt}`
);
console.log( "Checking android model files..." );
let exist = true;
try {
await fs.access( androidModel );
} catch ( _ ) {
exist = false;
}
if ( exist ) {
console.log( "Android model exist!" );
return;
}
console.log(
`Android model files missing, downloading from '${binariesBaseDir}'...`
);
await fs.mkdir( androidDestination, { recursive: true } );
const dl = new DownloaderHelper( androidCV, androidDestination );
dl.on( "end", () => console.log( "Download Completed" ) );
dl.on( "error", err => console.log( "Download Failed", err ) );
await dl.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
const dl2 = new DownloaderHelper( androidGeo, androidDestination );
dl2.on( "end", () => console.log( "Download Completed" ) );
dl2.on( "error", err => console.log( "Download Failed", err ) );
await dl2.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
const dl3 = new DownloaderHelper( taxonomyCSV, androidDestination );
dl3.on( "end", () => console.log( "Download Completed" ) );
dl3.on( "error", err => console.log( "Download Failed", err ) );
await dl3.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
console.log( "Unzipping!" );
const zipPath = path.join( modelPath, filename );
await Decompress( zipPath, modelPath )
.then( () => {
console.log( "Done Unzipping!" );
} )
.catch( error => console.log( error ) );
console.log( "Android done!" );
};
console.log( "Copying model files to assets folder..." );
await fs.mkdir( androidDestinationPath, { recursive: true } );
await fs.copyFile( androidModelPath, path.join( androidDestinationPath, androidModelFile ) );
await fs.copyFile(
androidTaxonomyPath,
path.join( androidDestinationPath, androidTaxonomyFile )
const downloadIOS = async () => {
const iosDestination = path.join( __dirname, "..", "ios" );
const iosModel = path.join( iosDestination, `${cvModelFilename}.${iosExt}` );
console.log( "Checking ios model files..." );
let exist = true;
try {
await fs.access( iosModel );
} catch ( _ ) {
exist = false;
}
if ( exist ) {
console.log( "ios model exist!" );
return;
}
console.log(
`iOS Model files missing, downloading from '${binariesBaseDir}'...`
);
await fs.mkdir( iosDestinationPath, { recursive: true } );
await fs.copyFile( iosModelPath, path.join( iosDestinationPath, iosModelFile ) );
await fs.copyFile( iosTaxonomyPath, path.join( iosDestinationPath, iosTaxonomyFile ) );
console.log( "Copying Geomodel placeholder to be model file..." );
await fs.copyFile(
path.join( iosDestinationPath, "geomodel.placeholder" ),
path.join( iosDestinationPath, "geomodel.mlmodel" )
);
await fs.mkdir( iosDestination, { recursive: true } );
console.log( "Delete temp model folder and its contents..." );
await fs.rm( modelPath, { recursive: true } );
const dl = new DownloaderHelper( iosCV, iosDestination );
dl.on( "end", () => console.log( "Download Completed" ) );
dl.on( "error", err => console.log( "Download Failed", err ) );
await dl.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
const dl2 = new DownloaderHelper( iosGeo, iosDestination );
dl2.on( "end", () => console.log( "Download Completed" ) );
dl2.on( "error", err => console.log( "Download Failed", err ) );
await dl2.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
const dl3 = new DownloaderHelper( taxonomyJSON, iosDestination );
dl3.on( "end", () => console.log( "Download Completed" ) );
dl3.on( "error", err => console.log( "Download Failed", err ) );
await dl3.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
console.log( "Done!" );
} )();
console.log( "iOS done!" );
};
// eslint-disable-next-line no-unused-expressions
yargs
.usage( "Usage: $0 [args]" )
.option( "androidFlavor", {
alias: "f",
type: "string",
description: "Android flavor to download model files into"
} )
.command(
"$0",
"Download example model files if not present",
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
async argv => {
await downloadAndroid( argv );
await downloadIOS();
}
)
.help().argv;

View File

@@ -1,82 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { DownloaderHelper } = require( "node-downloader-helper" );
const fs = require( "fs" ).promises;
const path = require( "path" );
// eslint-disable-next-line import/no-extraneous-dependencies
const Decompress = require( "decompress" );
require( "dotenv" ).config();
const filename = "small_model.zip";
const modelURL
= "https://github.com/inaturalist/SeekReactNative/releases/download/v2.9.1-138/small_model.zip";
const modelPath = path.join( __dirname, "..", "temp", "model" );
const androidModelPath = path.join( modelPath, "tf1 2", "small_inception_tf1.tflite" );
const androidTaxonomyPath = path.join( modelPath, "tf1 2", "small_export_tax.csv" );
const iosModelPath = path.join( modelPath, "tf1 2", "small_inception_tf1.mlmodel" );
const iosTaxonomyPath = path.join( modelPath, "tf1 2", "small_export_tax.json" );
const androidDestinationPath = path.join(
__dirname,
"..",
"android",
"app",
"src",
"main",
"assets",
"camera"
);
const iosDestinationPath = path.join( __dirname, "..", "ios" );
( async () => {
console.log( `Downloading example model from '${modelURL}'...` );
await fs.mkdir( modelPath, { recursive: true } );
const dl = new DownloaderHelper( modelURL, modelPath );
dl.on( "end", () => console.log( "Download Completed" ) );
dl.on( "error", err => console.log( "Download Failed", err ) );
await dl.start().catch( err => console.error( err ) );
console.log( "Downloaded!" );
console.log( "Unzipping!" );
const zipPath = path.join( modelPath, filename );
await Decompress( zipPath, modelPath )
.then( () => {
console.log( "Done Unzipping!" );
} )
.catch( error => console.log( error ) );
console.log( "Reading output filenames from .env file..." );
const androidModelFile = process.env.ANDROID_MODEL_FILE_NAME;
const androidTaxonomyFile = process.env.ANDROID_TAXONOMY_FILE_NAME;
const iosModelFile = process.env.IOS_MODEL_FILE_NAME;
const iosTaxonomyFile = process.env.IOS_TAXONOMY_FILE_NAME;
// TODO: donwload an example Geomodel from the internet
// NEEDS: https://github.com/inaturalist/iNaturalistMLWork/issues/146
// const iosGeoModelFile = process.env.IOS_GEOMODEL_FILE_NAME;
console.log( "Copying model files to assets folder..." );
await fs.mkdir( androidDestinationPath, { recursive: true } );
await fs.copyFile(
androidModelPath,
path.join( androidDestinationPath, androidModelFile )
);
await fs.copyFile(
androidTaxonomyPath,
path.join( androidDestinationPath, androidTaxonomyFile )
);
await fs.mkdir( iosDestinationPath, { recursive: true } );
await fs.copyFile(
iosModelPath,
path.join( iosDestinationPath, iosModelFile )
);
await fs.copyFile(
iosTaxonomyPath,
path.join( iosDestinationPath, iosTaxonomyFile )
);
console.log( "Delete temp model folder and its contents..." );
await fs.rm( modelPath, { recursive: true } );
console.log( "Done!" );
} )();