Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
# The Supported Models page dates each model from the release tags;
# blobs are fetched only for the files it reads.
fetch-depth: 0
filter: blob:none
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/publish_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ jobs:
steps:
- name: Check out
uses: actions/checkout@v6
with:
# The Supported Models page dates each model from the release tags;
# blobs are fetched only for the files it reads.
fetch-depth: 0
filter: blob:none

- name: Generate docs
run: >-
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/build

# Generated files
/docs/01-fundamentals/04-supported-models.md
.docusaurus
.cache-loader

Expand Down
2 changes: 1 addition & 1 deletion docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const config = {
llmsTxt: {
siteTitle: 'React Native ExecuTorch',
siteDescription:
"React Native ExecuTorch brings Meta's ExecuTorch AI framework into the React Native ecosystem, enabling developers to run AI models and LLMs locally, directly on mobile devices. It provides a declarative API for on-device inference, allowing you to use local AI models without relying on cloud infrastructure. Built on the ExecuTorch foundation - part of the PyTorch Edge ecosystem - it extends efficient on-device AI deployment to cross-platform mobile applications in React Native.",
"React Native ExecuTorch is Software Mansion's library for running AI models on-device in React Native, built on Meta's ExecuTorch (PyTorch Edge). One package covers LLMs and VLMs (streaming chat, tool calling, thinking mode, constrained sampling), speech (speech-to-text, text-to-speech, voice activity detection), computer vision, image generation and inpainting, and local RAG (text, image and ColBERT embeddings), fully offline on iOS and Android. Unlike bindings to a single runtime, which leave model export, tokenizers and pre/post-processing to the app, it ships a registry of pre-exported models with typed hooks for each task and per-device benchmarks, and still loads any custom ExecuTorch .pte.",
autoSectionDepth: 3,
autoSectionPosition: 1,
enableDescriptions: true,
Expand Down
5 changes: 3 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build && yarn prettier-api-reference && npm run postbuild",
"start": "yarn generate-models && docusaurus start",
"build": "yarn generate-models && docusaurus build && yarn prettier-api-reference && npm run postbuild",
"generate-models": "node scripts/generate-supported-models.mjs",
"docs:version": "./scripts/build-version-docs.sh",
"postbuild": "node scripts/postbuild.mjs",
"prettier-api-reference": "npx prettier --write 'docs/06-api-reference/**/*.{md,mdx}'",
Expand Down
2 changes: 2 additions & 0 deletions docs/scripts/build-version-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ if [ -z "$VERSION" ]; then
exit 1
fi

# The Supported Models page is generated, so freeze the current registry's.
yarn generate-models
yarn docusaurus docs:version $VERSION

find versioned_docs/version-$VERSION -type f \( -name "*.md" -o -name "*.mdx" \) \
Expand Down
76 changes: 76 additions & 0 deletions docs/scripts/generate-supported-models.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { BACKEND_LABELS, readModelRegistry } from './tasks/model-registry.mjs';
import { readAddedIn } from './tasks/model-history.mjs';

// Writes the Supported Models page from the `models` registry, so the list
// moves with the library instead of being kept by hand. The page is generated
// on every build and is not committed; `yarn docs:version` freezes a copy of it
// into the versioned docs.

const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT_PATH = join(__dirname, '..', 'docs', '01-fundamentals', '04-supported-models.md');

const FRONT_MATTER = `---
title: Supported Models
slug: /fundamentals/supported-models
description: 'Every pre-exported model in the React Native ExecuTorch registry, by task: LLMs and VLMs, speech, computer vision, embeddings and retrieval, with backends and precisions.'
keywords:
[
react native executorch,
supported models,
on-device llm,
whisper,
yolo,
depth anything,
colbert,
xnnpack,
core ml,
mlx,
vulkan,
]
---`;

function cell(text) {
return text.replaceAll('|', '\\|');
}

function render(tasks, addedIn) {
const lines = [
FRONT_MATTER,
'',
'# Supported Models',
'',
'Every model below ships pre-exported in the [`models`](./02-downloading-models.md) registry and downloads on first use. Backends: XNNPACK (CPU, iOS and Android), Core ML and MLX (iOS), Vulkan (Android GPU); `DEFAULT` picks the fastest one the device can run. Any other ExecuTorch `.pte` can be loaded too, see [Exporting Custom Models](../03-core-and-advanced/07-exporting-custom-models.md).',
'',
];
for (const task of tasks) {
lines.push(`## ${task.title}`, '');
lines.push(
`[Guide](../${task.doc}) · [Benchmarks](../04-benchmarks/01-benchmarks.mdx#${task.bench})`,
''
);
if (task.note) lines.push(task.note, '');
lines.push(
`| Model | Description | Backends | Precisions |${addedIn ? ' Added in |' : ''}`,
`| :-- | :-- | :-- | :-- |${addedIn ? ' :-- |' : ''}`
);
for (const model of task.models) {
let description = model.description;
if (model.modalities.length > 0)
description += ` Inputs: text, ${model.modalities.join(', ')}.`;
if (model.variants.length > 0) description += ` Variants: ${model.variants.join(', ')}.`;
lines.push(
`| \`${model.path}\` | ${cell(description)} | ${model.backends.map((b) => BACKEND_LABELS[b]).join(', ')} | ${model.precisions.join(', ')} |${addedIn ? ` ${addedIn.get(model.path) ?? 'next'} |` : ''}`
);
}
lines.push('');
}
return lines.join('\n');
}

const tasks = readModelRegistry();
const addedIn = readAddedIn(tasks.flatMap((task) => task.models.map((model) => model.path)));
writeFileSync(OUT_PATH, render(tasks, addedIn), 'utf8');
console.log(`Generated ${OUT_PATH}`);
2 changes: 2 additions & 0 deletions docs/scripts/postbuild.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { fixLLMsTxtDuplicatedPaths } from './tasks/fix-llms-txt.mjs';
import { addModelsToLLMsTxt } from './tasks/llms-txt-models.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand All @@ -9,6 +10,7 @@ const BUILD_DIR = join(__dirname, '..', 'build');
async function postbuild() {
try {
fixLLMsTxtDuplicatedPaths(BUILD_DIR);
addModelsToLLMsTxt(BUILD_DIR);
console.log('All post-build tasks completed successfully.\n');
} catch (error) {
console.error('\nPost-build script failed:', error);
Expand Down
101 changes: 101 additions & 0 deletions docs/scripts/tasks/llms-txt-models.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import {
BACKEND_LABELS,
BACKEND_PLATFORMS,
readModelRegistry,
readDocSlug,
byPrecision,
} from './model-registry.mjs';
import { readAddedIn } from './model-history.mjs';

export { addModelsToLLMsTxt };

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = createRequire(import.meta.url)(join(__dirname, '..', '..', 'docusaurus.config.js'));
const DOCS_URL = `${config.url}${config.baseUrl}docs`;

// The plugin writes llms.txt from the docs pages alone, so assistants reading
// it learn neither which release is current nor which models ship. This adds
// both, ahead of the page index: a summary at the top of the header quote and
// a list of every registry model.
function addModelsToLLMsTxt(buildDir) {
console.log('Adding the model registry to llms.txt...');
const filePath = join(buildDir, 'llms.txt');
if (!existsSync(filePath)) {
console.warn('File llms.txt not found');
return;
}

const tasks = readModelRegistry();
const lines = readFileSync(filePath, 'utf8').split('\n');
if (lines.includes('## Supported Models')) {
console.info('llms.txt already lists the models');
return;
}

const quote = lines.findIndex((line) => line.startsWith('>'));
const firstSection = lines.findIndex((line) => line.startsWith('## '));
if (quote === -1 || firstSection === -1) {
throw new Error('llms.txt has no header quote or no sections to insert the models before');
}

const addedIn = readAddedIn(tasks.flatMap((task) => task.models.map((model) => model.path)));
lines.splice(firstSection, 0, ...renderModels(tasks, addedIn), '');
lines.splice(quote, 0, ...renderSummary(tasks, addedIn), '>');

writeFileSync(filePath, lines.join('\n'), 'utf8');
console.log('Added the model registry to llms.txt');
}

function renderSummary(tasks, addedIn) {
const lastVersion = config.presets[0][1].docs.lastVersion;
const models = tasks.flatMap((task) => task.models);
const backends = [...new Set(models.flatMap((m) => m.backends))];
const backendText = Object.keys(BACKEND_LABELS)
.filter((b) => backends.includes(b))
.map((b) => `${BACKEND_LABELS[b]} (${BACKEND_PLATFORMS[b]})`)
.join(', ');
const precisions = [...new Set(models.flatMap((m) => m.precisions))].sort(byPrecision);
const today = new Date().toISOString().slice(0, 10);

const latestMinor = `v${lastVersion.replace(/\.x$/, '.')}`;
const added = addedIn
? models.filter((m) => addedIn.get(m.path)?.startsWith(latestMinor)).map((m) => `\`${m.path}\``)
: [];

return [
`> Latest stable release: v${lastVersion} (npm \`react-native-executorch\`); this file was generated ${today}.`,
`> ${models.length} pre-exported models across ${tasks.length} tasks, listed under "Supported Models" below.`,
`> Backends: ${backendText}. Precisions: ${precisions.join(', ')}.`,
...(added.length > 0 ? [`> New in v${lastVersion}: ${added.join(', ')}.`] : []),
];
}

function renderModels(tasks, addedIn) {
const lines = [
'## Supported Models',
'',
`Generated from the library's \`models\` registry. Full table: [Supported Models](${DOCS_URL}${readDocSlug('01-fundamentals/04-supported-models.md')}). Any other ExecuTorch \`.pte\` can be loaded too.`,
'',
];
for (const task of tasks) {
lines.push(
`### ${task.title} ([guide](${DOCS_URL}${readDocSlug(task.doc)}), [benchmarks](${DOCS_URL}/benchmarks#${task.bench}))`,
''
);
if (task.note) lines.push(task.note, '');
for (const model of task.models) {
const parts = [model.description];
if (model.modalities.length > 0) parts.push(`Inputs: text, ${model.modalities.join(', ')}.`);
if (model.variants.length > 0) parts.push(`Variants: ${model.variants.join(', ')}.`);
parts.push(`Backends: ${model.backends.map((b) => BACKEND_LABELS[b]).join(', ')}.`);
if (addedIn) parts.push(`Added in ${addedIn.get(model.path) ?? 'the next release'}.`);
lines.push(`- \`${model.path}\`: ${parts.join(' ')}`);
}
lines.push('');
}
return lines.slice(0, -1);
}
133 changes: 133 additions & 0 deletions docs/scripts/tasks/model-history.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { execFileSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { REGISTRY_REPO_PATH, parseRegistry } from './model-registry.mjs';

export { readAddedIn };

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_DIR = join(__dirname, '..', '..', '..');

// Before v0.10.0 models were flat constants spread over src/constants (and
// src/modelUrls.ts in v0.1). A constant whose name equals the registry entry's
// key matches by itself; these are the ones renamed by the v0.10 registry. Only
// the same model counts: EasyOCR is not PaddleOCR, and BK-SDM is not SDXS.
const LEGACY_NAMES = {
'models.llm.SMOLLM2_135M': ['SMOLLM2_1_135M'],
'models.llm.SMOLLM2_360M': ['SMOLLM2_1_360M'],
'models.llm.SMOLLM2_1_7B': ['SMOLLM2_1_1_7B'],
'models.llm.PHI4_MINI': ['PHI_4_MINI_4B'],
'models.llm.LFM2_5_1_2B': ['LFM2_5_1_2B_INSTRUCT'],
'models.llm.LFM2_5_VL_450M': ['LFM2_5_VL_450M_QUANTIZED'],
'models.llm.LFM2_5_VL_1_6B': ['LFM2_5_VL_1_6B_QUANTIZED'],
'models.llm.BIELIK_V3_1_5B': ['BIELIK_V3_0_1_5B'],
'models.llm.GEMMA4_OMNI_E2B': ['GEMMA4_E2B_MM'],
'models.textEmbeddings.CLIP_VIT_BASE_PATCH32_TEXT': ['CLIP_VIT_BASE_PATCH_32_TEXT_ENCODER'],
'models.textEmbeddings.DISTILUSE_BASE_MULTILINGUAL_CASED_V2': [
'DISTILUSE_BASE_MULTILINGUAL_CASED_V2_8DA4W',
],
'models.textEmbeddings.PARAPHRASE_MULTILINGUAL_MINILM_L12_V2': [
'PARAPHRASE_MULTILINGUAL_MINILM_L12_V2_QUANTIZED',
],
'models.imageEmbeddings.CLIP_VIT_BASE_PATCH32': ['CLIP_VIT_BASE_PATCH32_IMAGE'],
'models.privacyFilter.OPENAI': ['PRIVACY_FILTER_OPENAI'],
'models.privacyFilter.NEMOTRON': ['PRIVACY_FILTER_NEMOTRON'],
'models.speechToText.WHISPER': ['WHISPER_TINY_ENCODER', 'WHISPER_TINY', 'WHISPER_TINY_EN'],
'models.textToSpeech.KOKORO': ['KOKORO_SMALL', 'KOKORO_MEDIUM'],
'models.objectDetection.SSDLITE320_MOBILENET_V3_LARGE': ['SSDLITE_320_MOBILENET_V3_LARGE'],
'models.objectDetection.RFDETR_NANO': ['RF_DETR_NANO'],
'models.objectDetection.YOLO26': ['YOLO26N', 'YOLO26S', 'YOLO26M', 'YOLO26L', 'YOLO26X'],
'models.instanceSegmentation.RFDETR_NANO': ['RF_DETR_NANO_SEG'],
'models.instanceSegmentation.FASTSAM': ['FASTSAM_S', 'FASTSAM_X'],
'models.instanceSegmentation.YOLO26': [
'YOLO26N_SEG',
'YOLO26S_SEG',
'YOLO26M_SEG',
'YOLO26L_SEG',
'YOLO26X_SEG',
],
'models.keypointDetection.YOLO26_POSE': ['YOLO26N_POSE'],
'models.keypointDetection.RFDETR_KEYPOINT': ['RF_DETR_KEYPOINT_PREVIEW'],
'models.styleTransfer.CANDY': ['STYLE_TRANSFER_CANDY'],
'models.styleTransfer.MOSAIC': ['STYLE_TRANSFER_MOSAIC'],
'models.styleTransfer.RAIN_PRINCESS': ['STYLE_TRANSFER_RAIN_PRINCESS'],
'models.styleTransfer.UDNIE': ['STYLE_TRANSFER_UDNIE'],
};

const LEGACY_FILE = /(^|\/)src\/(modelUrls\.ts|constants\/.+\.ts)$/;

function git(...args) {
return execFileSync('git', ['-C', REPO_DIR, ...args], {
encoding: 'utf8',
maxBuffer: 256 * 1024 * 1024,
});
}

/**
* Finds the first release that shipped each model, from the release tags.
* Releases are walked in tag-date order, since patch releases of an older
* line (v0.3.4) went out after newer minors (v0.4.1).
* @param {string[]} paths The registry entries to date, e.g. `models.llm.QWEN3_4B`.
* @returns {Map<string, string> | null} The release tag of each entry that has
* been released, or null outside CI when the checkout has no release tags.
*/
function readAddedIn(paths) {
let tags;
try {
tags = git('for-each-ref', '--sort=creatordate', '--format=%(refname:short)', 'refs/tags/v*')
.split('\n')
.filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag));
} catch {
tags = [];
}
if (tags.length === 0) {
// A shallow CI checkout would otherwise publish the page without the column.
if (process.env.CI) {
throw new Error('No release tags to date the models; check out with `fetch-depth: 0`.');
}
console.warn('No release tags found, leaving out when each model was added');
return null;
}

const blobs = new Map();
const show = (id) => {
if (!blobs.has(id)) blobs.set(id, git('cat-file', 'blob', id));
return blobs.get(id);
};

const addedIn = new Map();
for (const tag of tags) {
const pending = paths.filter((path) => !addedIn.has(path));
if (pending.length === 0) break;

const files = git('ls-tree', '-r', tag)
.split('\n')
.filter(Boolean)
.map((line) => {
const [meta, file] = line.split('\t');
return { id: meta.split(' ')[2], file };
});

const registry = files.find((f) => f.file === REGISTRY_REPO_PATH);
if (registry) {
const shipped = new Set(
[...parseRegistry(show(registry.id)).values()].flat().map((model) => model.path)
);
for (const path of pending) if (shipped.has(path)) addedIn.set(path, tag);
continue;
}

const names = new Set();
for (const f of files) {
if (!LEGACY_FILE.test(f.file) || f.file.includes('/legacy/')) continue;
for (const match of show(f.id).matchAll(/^export const ([A-Z0-9_]+)/gm)) {
names.add(match[1].replace(/_URL$/, ''));
}
}
for (const path of pending) {
const candidates = [path.split('.').pop(), ...(LEGACY_NAMES[path] ?? [])];
if (candidates.some((name) => names.has(name))) addedIn.set(path, tag);
}
}
return addedIn;
}
Loading
Loading