Skip to content

Commit b7be456

Browse files
committed
refactor(registry)!: function-only lowercase \models\; add multimodal/tts/ocr
Adopts Bartek's feedback on #1148 — the accessor is no longer dual-shaped (value AND function). Each leaf is a pure function: call it (optionally with \`{ quant, backend }\`) to get the resolved config. This eliminates the \`useState\` lazy-init footgun and \`useMemo\`/\`useCallback\` dep hazards, so pickers fall back to plain \`===\` reference equality (drops the \`sameValue\` workaround across four \`ModelPicker.tsx\` files). Renames: - \`MODEL_REGISTRY\` → \`models\` (lowercase top-level) - group keys lowercased: \`LLM\` → \`llm\`, etc. - per Kuba: \`vlm\` → \`multimodal\` (anticipates audio-capable LMs like Gemma 4) Adds: - \`models.text_to_speech\` group: \`kokoro_small\`, \`kokoro_medium\`, plus voices as plain configs under \`voices\` (no quant/backend axis). - \`models.ocr({ language })\` parameterized accessor — covers all ISO language tokens via a runtime map built from the existing \`OCR_<LANGUAGE>\` exports. Example apps (22 files, ~150 substitutions) migrated by script. bare-rn demo swapped from \`llama3_2_1b\` to \`lfm2_5_1_2b_instruct\` per Kuba's note. Docs rewritten with the new syntax + TTS + OCR sections. Relaxes the project's \`camelcase\` rule with \`properties: 'never'\` so the lowercase snake_case keys in \`models\` (which mirror the \`.pte\` filename convention) pass without per-file disables. Variable and function names still require camelCase.
1 parent eff6ade commit b7be456

29 files changed

Lines changed: 405 additions & 398 deletions

File tree

.eslintrc.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ module.exports = {
6363
customWordListFile: path.resolve(__dirname, '.cspell-wordlist.txt'),
6464
},
6565
],
66-
'camelcase': 'error',
66+
// `properties: 'never'` lets the lowercase snake_case keys in `models`
67+
// (e.g. `models.text_to_speech.kokoro_small`, mirroring the underlying
68+
// `.pte` filenames) pass while still requiring camelCase for variable
69+
// and function declarations.
70+
'camelcase': ['error', { properties: 'never' }],
6771
'jsdoc/require-jsdoc': 'off',
6872
'jsdoc/require-param': ['error', { checkDestructured: false }],
6973
'jsdoc/check-param-names': ['error', { checkDestructured: false }],

apps/bare-rn/App.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ import {
1313
TouchableWithoutFeedback,
1414
View,
1515
} from 'react-native';
16-
import {
17-
MODEL_REGISTRY,
18-
initExecutorch,
19-
useLLM,
20-
} from 'react-native-executorch';
16+
import { models, initExecutorch, useLLM } from 'react-native-executorch';
2117
import { BareResourceFetcher } from 'react-native-executorch-bare-resource-fetcher';
2218
import { setConfig } from '@kesha-antonov/react-native-background-downloader';
2319
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
@@ -145,7 +141,7 @@ function App() {
145141
const scrollViewRef = useRef<ScrollView>(null);
146142

147143
const llm = useLLM({
148-
model: MODEL_REGISTRY.LLM.LLAMA3_2_1B,
144+
model: models.llm.lfm2_5_1_2b_instruct(),
149145
});
150146
// Alternatively, to use a custom local model, uncomment below:
151147
// const llm = useLLM({ model: {

apps/computer-vision/app/classification/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Spinner from '../../components/Spinner';
22
import { getImage } from '../../utils';
33
import {
4-
MODEL_REGISTRY,
4+
models,
55
useClassification,
66
ClassificationModelSources,
77
} from 'react-native-executorch';
@@ -10,11 +10,11 @@ import { ModelPicker, ModelOption } from '../../components/ModelPicker';
1010
const MODELS: ModelOption<ClassificationModelSources>[] = [
1111
{
1212
label: 'EfficientNet V2 S Quantized',
13-
value: MODEL_REGISTRY.CLASSIFICATION.EFFICIENTNET_V2_S,
13+
value: models.classification.efficientnet_v2_s(),
1414
},
1515
{
1616
label: 'EfficientNet V2 S',
17-
value: MODEL_REGISTRY.CLASSIFICATION.EFFICIENTNET_V2_S({ quant: false }),
17+
value: models.classification.efficientnet_v2_s({ quant: false }),
1818
},
1919
];
2020
import { View, StyleSheet, Image, Text, ScrollView } from 'react-native';
@@ -28,7 +28,7 @@ import ErrorBanner from '../../components/ErrorBanner';
2828
export default function ClassificationScreen() {
2929
const [selectedModel, setSelectedModel] =
3030
useState<ClassificationModelSources>(
31-
MODEL_REGISTRY.CLASSIFICATION.EFFICIENTNET_V2_S
31+
models.classification.efficientnet_v2_s()
3232
);
3333
const [results, setResults] = useState<{ label: string; score: number }[]>(
3434
[]

apps/computer-vision/app/instance_segmentation/index.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BottomBar } from '../../components/BottomBar';
33
import { getImage } from '../../utils';
44
import { ModelPicker, ModelOption } from '../../components/ModelPicker';
55
import {
6-
MODEL_REGISTRY,
6+
models,
77
useInstanceSegmentation,
88
InstanceSegmentationModelSources,
99
} from 'react-native-executorch';
@@ -24,23 +24,23 @@ import ImageWithMasks, {
2424
import { StatsBar } from '../../components/StatsBar';
2525

2626
const MODELS: ModelOption<InstanceSegmentationModelSources>[] = [
27-
{ label: 'Yolo26N', value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26N_SEG },
28-
{ label: 'Yolo26S', value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26S_SEG },
29-
{ label: 'Yolo26M', value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26M_SEG },
30-
{ label: 'Yolo26L', value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26L_SEG },
31-
{ label: 'Yolo26X', value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26X_SEG },
27+
{ label: 'Yolo26N', value: models.instance_segmentation.yolo26n_seg() },
28+
{ label: 'Yolo26S', value: models.instance_segmentation.yolo26s_seg() },
29+
{ label: 'Yolo26M', value: models.instance_segmentation.yolo26m_seg() },
30+
{ label: 'Yolo26L', value: models.instance_segmentation.yolo26l_seg() },
31+
{ label: 'Yolo26X', value: models.instance_segmentation.yolo26x_seg() },
3232
{
3333
label: 'RF-DeTR Nano',
34-
value: MODEL_REGISTRY.INSTANCE_SEGMENTATION.RF_DETR_NANO_SEG,
34+
value: models.instance_segmentation.rf_detr_nano_seg(),
3535
},
36-
{ label: 'FastSAM-S', value: MODEL_REGISTRY.OBJECT_DETECTION.FASTSAM_S },
37-
{ label: 'FastSAM-X', value: MODEL_REGISTRY.OBJECT_DETECTION.FASTSAM_X },
36+
{ label: 'FastSAM-S', value: models.object_detection.fastsam_s() },
37+
{ label: 'FastSAM-X', value: models.object_detection.fastsam_x() },
3838
];
3939

4040
export default function InstanceSegmentationScreen() {
4141
const [selectedModel, setSelectedModel] =
4242
useState<InstanceSegmentationModelSources>(
43-
MODEL_REGISTRY.INSTANCE_SEGMENTATION.YOLO26N_SEG
43+
models.instance_segmentation.yolo26n_seg()
4444
);
4545
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
4646

apps/computer-vision/app/object_detection/index.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BottomBar } from '../../components/BottomBar';
33
import { ModelPicker, ModelOption } from '../../components/ModelPicker';
44
import { getImage } from '../../utils';
55
import {
6-
MODEL_REGISTRY,
6+
models,
77
Detection,
88
useObjectDetection,
99
ObjectDetectionModelSources,
@@ -18,17 +18,17 @@ import { StatsBar } from '../../components/StatsBar';
1818
const MODELS: ModelOption<ObjectDetectionModelSources>[] = [
1919
{
2020
label: 'RF-DeTR Nano',
21-
value: MODEL_REGISTRY.OBJECT_DETECTION.RF_DETR_NANO,
21+
value: models.object_detection.rf_detr_nano(),
2222
},
2323
{
2424
label: 'SSDLite MobileNet',
25-
value: MODEL_REGISTRY.OBJECT_DETECTION.SSDLITE_320_MOBILENET_V3_LARGE,
25+
value: models.object_detection.ssdlite_320_mobilenet_v3_large(),
2626
},
27-
{ label: 'YOLO26N', value: MODEL_REGISTRY.OBJECT_DETECTION.YOLO26N },
28-
{ label: 'YOLO26S', value: MODEL_REGISTRY.OBJECT_DETECTION.YOLO26S },
29-
{ label: 'YOLO26M', value: MODEL_REGISTRY.OBJECT_DETECTION.YOLO26M },
30-
{ label: 'YOLO26L', value: MODEL_REGISTRY.OBJECT_DETECTION.YOLO26L },
31-
{ label: 'YOLO26X', value: MODEL_REGISTRY.OBJECT_DETECTION.YOLO26X },
27+
{ label: 'YOLO26N', value: models.object_detection.yolo26n() },
28+
{ label: 'YOLO26S', value: models.object_detection.yolo26s() },
29+
{ label: 'YOLO26M', value: models.object_detection.yolo26m() },
30+
{ label: 'YOLO26L', value: models.object_detection.yolo26l() },
31+
{ label: 'YOLO26X', value: models.object_detection.yolo26x() },
3232
];
3333
import ErrorBanner from '../../components/ErrorBanner';
3434

@@ -42,7 +42,7 @@ export default function ObjectDetectionScreen() {
4242
}>();
4343
const [selectedModel, setSelectedModel] =
4444
useState<ObjectDetectionModelSources>(
45-
MODEL_REGISTRY.OBJECT_DETECTION.RF_DETR_NANO
45+
models.object_detection.rf_detr_nano()
4646
);
4747
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
4848

apps/computer-vision/app/segment_anything/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
AlphaType,
2222
} from '@shopify/react-native-skia';
2323
import {
24-
MODEL_REGISTRY,
24+
models,
2525
useInstanceSegmentation,
2626
useImageEmbeddings,
2727
useTextEmbeddings,
@@ -49,16 +49,16 @@ import ColorPalette from '../../colors';
4949
type PromptMode = 'point' | 'box' | 'text';
5050

5151
const MODELS: ModelOption<InstanceSegmentationModelSources>[] = [
52-
{ label: 'FastSAM-S', value: MODEL_REGISTRY.OBJECT_DETECTION.FASTSAM_S },
53-
{ label: 'FastSAM-X', value: MODEL_REGISTRY.OBJECT_DETECTION.FASTSAM_X },
52+
{ label: 'FastSAM-S', value: models.object_detection.fastsam_s() },
53+
{ label: 'FastSAM-X', value: models.object_detection.fastsam_x() },
5454
];
5555

5656
export default function SegmentAnythingScreen() {
5757
const { setGlobalGenerating } = useContext(GeneratingContext);
5858

5959
const [selectedModel, setSelectedModel] =
6060
useState<InstanceSegmentationModelSources>(
61-
MODEL_REGISTRY.OBJECT_DETECTION.FASTSAM_S
61+
models.object_detection.fastsam_s()
6262
);
6363
const [mode, setMode] = useState<PromptMode>('point');
6464
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
@@ -77,10 +77,10 @@ export default function SegmentAnythingScreen() {
7777
useInstanceSegmentation({ model: selectedModel });
7878

7979
const clipImage = useImageEmbeddings({
80-
model: MODEL_REGISTRY.IMAGE_EMBEDDING.CLIP_VIT_BASE_PATCH32_IMAGE,
80+
model: models.image_embedding.clip_vit_base_patch32_image(),
8181
});
8282
const clipText = useTextEmbeddings({
83-
model: MODEL_REGISTRY.TEXT_EMBEDDING.CLIP_VIT_BASE_PATCH32_TEXT,
83+
model: models.text_embedding.clip_vit_base_patch32_text(),
8484
});
8585
const skiaSource = useImage(imageUri || null);
8686

apps/computer-vision/app/semantic_segmentation/index.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BottomBar } from '../../components/BottomBar';
33
import { ModelPicker, ModelOption } from '../../components/ModelPicker';
44
import { getImage } from '../../utils';
55
import {
6-
MODEL_REGISTRY,
6+
models,
77
useSemanticSegmentation,
88
SemanticSegmentationModelSources,
99
} from 'react-native-executorch';
@@ -49,39 +49,39 @@ const numberToColor: number[][] = [
4949
const MODELS: ModelOption<SemanticSegmentationModelSources>[] = [
5050
{
5151
label: 'DeepLab MobileNet',
52-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.DEEPLAB_V3_MOBILENET_V3_LARGE,
52+
value: models.semantic_segmentation.deeplab_v3_mobilenet_v3_large(),
5353
},
5454
{
5555
label: 'DeepLab ResNet50',
56-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.DEEPLAB_V3_RESNET50,
56+
value: models.semantic_segmentation.deeplab_v3_resnet50(),
5757
},
5858
{
5959
label: 'DeepLab ResNet101',
60-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.DEEPLAB_V3_RESNET101,
60+
value: models.semantic_segmentation.deeplab_v3_resnet101(),
6161
},
6262
{
6363
label: 'LRASPP MobileNet',
64-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.LRASPP_MOBILENET_V3_LARGE,
64+
value: models.semantic_segmentation.lraspp_mobilenet_v3_large(),
6565
},
6666
{
6767
label: 'FCN ResNet50',
68-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.FCN_RESNET50,
68+
value: models.semantic_segmentation.fcn_resnet50(),
6969
},
7070
{
7171
label: 'FCN ResNet101',
72-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.FCN_RESNET101,
72+
value: models.semantic_segmentation.fcn_resnet101(),
7373
},
7474
{
7575
label: 'Selfie Segmentation',
76-
value: MODEL_REGISTRY.SEMANTIC_SEGMENTATION.SELFIE_SEGMENTATION,
76+
value: models.semantic_segmentation.selfie_segmentation(),
7777
},
7878
];
7979

8080
export default function SemanticSegmentationScreen() {
8181
const { setGlobalGenerating } = useContext(GeneratingContext);
8282
const [selectedModel, setSelectedModel] =
8383
useState<SemanticSegmentationModelSources>(
84-
MODEL_REGISTRY.SEMANTIC_SEGMENTATION.DEEPLAB_V3_MOBILENET_V3_LARGE
84+
models.semantic_segmentation.deeplab_v3_mobilenet_v3_large()
8585
);
8686

8787
const {

apps/computer-vision/app/style_transfer/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BottomBar } from '../../components/BottomBar';
33
import { ModelPicker, ModelOption } from '../../components/ModelPicker';
44
import { getImage } from '../../utils';
55
import {
6-
MODEL_REGISTRY,
6+
models,
77
useStyleTransfer,
88
StyleTransferModelName,
99
ResourceSource,
@@ -22,19 +22,19 @@ type StyleTransferModelSources = {
2222
};
2323

2424
const MODELS: ModelOption<StyleTransferModelSources>[] = [
25-
{ label: 'Candy', value: MODEL_REGISTRY.STYLE_TRANSFER.CANDY },
26-
{ label: 'Mosaic', value: MODEL_REGISTRY.STYLE_TRANSFER.MOSAIC },
25+
{ label: 'Candy', value: models.style_transfer.candy() },
26+
{ label: 'Mosaic', value: models.style_transfer.mosaic() },
2727
{
2828
label: 'Rain Princess',
29-
value: MODEL_REGISTRY.STYLE_TRANSFER.RAIN_PRINCESS,
29+
value: models.style_transfer.rain_princess(),
3030
},
31-
{ label: 'Udnie', value: MODEL_REGISTRY.STYLE_TRANSFER.UDNIE },
31+
{ label: 'Udnie', value: models.style_transfer.udnie() },
3232
];
3333
import ErrorBanner from '../../components/ErrorBanner';
3434

3535
export default function StyleTransferScreen() {
3636
const [selectedModel, setSelectedModel] = useState<StyleTransferModelSources>(
37-
MODEL_REGISTRY.STYLE_TRANSFER.CANDY
37+
models.style_transfer.candy()
3838
);
3939

4040
const model = useStyleTransfer({ model: selectedModel });

apps/computer-vision/app/text_to_image/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import React, { useContext, useEffect, useState } from 'react';
1414
import { useSafeAreaInsets } from 'react-native-safe-area-context';
1515
import Spinner from '../../components/Spinner';
1616
import {
17-
MODEL_REGISTRY,
17+
models,
1818
useTextToImage,
1919
TextToImageProps,
2020
} from 'react-native-executorch';
@@ -31,11 +31,11 @@ type TextToImageModelSources = TextToImageProps['model'];
3131
const MODELS: ModelOption<TextToImageModelSources>[] = [
3232
{
3333
label: 'BK-SDM 256',
34-
value: MODEL_REGISTRY.IMAGE_GENERATION.BK_SDM_TINY_VPRED_256,
34+
value: models.image_generation.bk_sdm_tiny_vpred_256(),
3535
},
3636
{
3737
label: 'BK-SDM 512',
38-
value: MODEL_REGISTRY.IMAGE_GENERATION.BK_SDM_TINY_VPRED_512,
38+
value: models.image_generation.bk_sdm_tiny_vpred_512(),
3939
},
4040
];
4141

@@ -47,7 +47,7 @@ export default function TextToImageScreen() {
4747

4848
const [input, setInput] = useState('');
4949
const [selectedModel, setSelectedModel] = useState<TextToImageModelSources>(
50-
MODEL_REGISTRY.IMAGE_GENERATION.BK_SDM_TINY_VPRED_256
50+
models.image_generation.bk_sdm_tiny_vpred_256()
5151
);
5252
const [generationTime, setGenerationTime] = useState<number | null>(null);
5353

apps/computer-vision/components/ModelPicker.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@ type Props<T> = {
2424

2525
const DROPDOWN_MAX_HEIGHT = 200;
2626

27-
// MODEL_REGISTRY accessors are functions, so passing them to useState makes
28-
// React auto-invoke them as lazy initializers — state becomes the underlying
29-
// config object, breaking reference equality against the accessor in MODELS.
30-
// Match by modelName when both sides expose one, otherwise fall back to ===.
31-
function sameValue<T>(a: T, b: T): boolean {
32-
const am = (a as { modelName?: unknown })?.modelName;
33-
const bm = (b as { modelName?: unknown })?.modelName;
34-
if (typeof am === 'string' && typeof bm === 'string') return am === bm;
35-
return a === b;
36-
}
37-
3827
export function ModelPicker<T>({
3928
models,
4029
selectedModel,
@@ -47,7 +36,7 @@ export function ModelPicker<T>({
4736
const [expandUp, setExpandUp] = useState(false);
4837
const [dropdownTop, setDropdownTop] = useState(0);
4938
const triggerRef = useRef<React.ComponentRef<typeof TouchableOpacity>>(null);
50-
const selected = models.find((m) => sameValue(m.value, selectedModel));
39+
const selected = models.find((m) => m.value === selectedModel);
5140

5241
useEffect(() => {
5342
if (disabled) setOpen(false);
@@ -123,7 +112,7 @@ export function ModelPicker<T>({
123112
showsVerticalScrollIndicator={true}
124113
>
125114
{models.map((item) => {
126-
const isSelected = sameValue(item.value, selectedModel);
115+
const isSelected = item.value === selectedModel;
127116
return (
128117
<TouchableOpacity
129118
key={item.label}

0 commit comments

Comments
 (0)