diff --git a/packages/react-native-executorch/src/constants/resourceFetcher.ts b/packages/react-native-executorch/src/constants/resourceFetcher.ts new file mode 100644 index 0000000000..b022bd29bd --- /dev/null +++ b/packages/react-native-executorch/src/constants/resourceFetcher.ts @@ -0,0 +1,134 @@ +import { + LLAMA3_2_3B, + LLAMA3_2_3B_QLORA, + LLAMA3_2_3B_SPINQUANT, + LLAMA3_2_1B, + LLAMA3_2_1B_QLORA, + LLAMA3_2_1B_SPINQUANT, + QWEN3_0_6B, + QWEN3_0_6B_QUANTIZED, + QWEN3_1_7B, + QWEN3_1_7B_QUANTIZED, + QWEN3_4B, + QWEN3_4B_QUANTIZED, + HAMMER2_1_0_5B, + HAMMER2_1_0_5B_QUANTIZED, + HAMMER2_1_1_5B, + HAMMER2_1_1_5B_QUANTIZED, + HAMMER2_1_3B, + HAMMER2_1_3B_QUANTIZED, + SMOLLM2_1_135M, + SMOLLM2_1_135M_QUANTIZED, + SMOLLM2_1_360M, + SMOLLM2_1_360M_QUANTIZED, + SMOLLM2_1_1_7B, + SMOLLM2_1_1_7B_QUANTIZED, + QWEN2_5_0_5B, + QWEN2_5_0_5B_QUANTIZED, + QWEN2_5_1_5B, + QWEN2_5_1_5B_QUANTIZED, + QWEN2_5_3B, + QWEN2_5_3B_QUANTIZED, + PHI_4_MINI_4B, + PHI_4_MINI_4B_QUANTIZED, + EFFICIENTNET_V2_S, + SSDLITE_320_MOBILENET_V3_LARGE, + STYLE_TRANSFER_CANDY, + STYLE_TRANSFER_MOSAIC, + STYLE_TRANSFER_RAIN_PRINCESS, + STYLE_TRANSFER_UDNIE, + WHISPER_TINY_EN, + WHISPER_TINY_EN_QUANTIZED, + WHISPER_BASE_EN, + WHISPER_SMALL_EN, + WHISPER_TINY, + WHISPER_BASE, + WHISPER_SMALL, + DEEPLAB_V3_RESNET50, + CLIP_VIT_BASE_PATCH32_IMAGE, + ALL_MINILM_L6_V2, + ALL_MPNET_BASE_V2, + MULTI_QA_MINILM_L6_COS_V1, + MULTI_QA_MPNET_BASE_DOT_V1, + CLIP_VIT_BASE_PATCH32_TEXT, + BK_SDM_TINY_VPRED_512, + BK_SDM_TINY_VPRED_256, + FSMN_VAD, +} from './modelUrls'; + +export const DOWNLOAD_EVENT_ENDPOINT = + 'https://ai.swmansion.com/telemetry/downloads/api/downloads'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const LIB_VERSION: string = require('../../package.json').version; + +const MODEL_NAME_MAP: Record> = { + 'llama-3.2-3b': LLAMA3_2_3B, + 'llama-3.2-3b-qlora': LLAMA3_2_3B_QLORA, + 'llama-3.2-3b-spinquant': LLAMA3_2_3B_SPINQUANT, + 'llama-3.2-1b': LLAMA3_2_1B, + 'llama-3.2-1b-qlora': LLAMA3_2_1B_QLORA, + 'llama-3.2-1b-spinquant': LLAMA3_2_1B_SPINQUANT, + 'qwen3-0.6b': QWEN3_0_6B, + 'qwen3-0.6b-quantized': QWEN3_0_6B_QUANTIZED, + 'qwen3-1.7b': QWEN3_1_7B, + 'qwen3-1.7b-quantized': QWEN3_1_7B_QUANTIZED, + 'qwen3-4b': QWEN3_4B, + 'qwen3-4b-quantized': QWEN3_4B_QUANTIZED, + 'hammer2.1-0.5b': HAMMER2_1_0_5B, + 'hammer2.1-0.5b-quantized': HAMMER2_1_0_5B_QUANTIZED, + 'hammer2.1-1.5b': HAMMER2_1_1_5B, + 'hammer2.1-1.5b-quantized': HAMMER2_1_1_5B_QUANTIZED, + 'hammer2.1-3b': HAMMER2_1_3B, + 'hammer2.1-3b-quantized': HAMMER2_1_3B_QUANTIZED, + 'smollm2.1-135m': SMOLLM2_1_135M, + 'smollm2.1-135m-quantized': SMOLLM2_1_135M_QUANTIZED, + 'smollm2.1-360m': SMOLLM2_1_360M, + 'smollm2.1-360m-quantized': SMOLLM2_1_360M_QUANTIZED, + 'smollm2.1-1.7b': SMOLLM2_1_1_7B, + 'smollm2.1-1.7b-quantized': SMOLLM2_1_1_7B_QUANTIZED, + 'qwen2.5-0.5b': QWEN2_5_0_5B, + 'qwen2.5-0.5b-quantized': QWEN2_5_0_5B_QUANTIZED, + 'qwen2.5-1.5b': QWEN2_5_1_5B, + 'qwen2.5-1.5b-quantized': QWEN2_5_1_5B_QUANTIZED, + 'qwen2.5-3b': QWEN2_5_3B, + 'qwen2.5-3b-quantized': QWEN2_5_3B_QUANTIZED, + 'phi-4-mini-4b': PHI_4_MINI_4B, + 'phi-4-mini-4b-quantized': PHI_4_MINI_4B_QUANTIZED, + 'efficientnet-v2-s': EFFICIENTNET_V2_S, + 'ssdlite-320-mobilenet-v3-large': SSDLITE_320_MOBILENET_V3_LARGE, + 'style-transfer-candy': STYLE_TRANSFER_CANDY, + 'style-transfer-mosaic': STYLE_TRANSFER_MOSAIC, + 'style-transfer-rain-princess': STYLE_TRANSFER_RAIN_PRINCESS, + 'style-transfer-udnie': STYLE_TRANSFER_UDNIE, + 'whisper-tiny-en': WHISPER_TINY_EN, + 'whisper-tiny-en-quantized': WHISPER_TINY_EN_QUANTIZED, + 'whisper-base-en': WHISPER_BASE_EN, + 'whisper-small-en': WHISPER_SMALL_EN, + 'whisper-tiny': WHISPER_TINY, + 'whisper-base': WHISPER_BASE, + 'whisper-small': WHISPER_SMALL, + 'deeplab-v3-resnet50': DEEPLAB_V3_RESNET50, + 'clip-vit-base-patch32-image': CLIP_VIT_BASE_PATCH32_IMAGE, + 'all-minilm-l6-v2': ALL_MINILM_L6_V2, + 'all-mpnet-base-v2': ALL_MPNET_BASE_V2, + 'multi-qa-minilm-l6-cos-v1': MULTI_QA_MINILM_L6_COS_V1, + 'multi-qa-mpnet-base-dot-v1': MULTI_QA_MPNET_BASE_DOT_V1, + 'clip-vit-base-patch32-text': CLIP_VIT_BASE_PATCH32_TEXT, + 'bk-sdm-tiny-vpred-512': BK_SDM_TINY_VPRED_512, + 'bk-sdm-tiny-vpred-256': BK_SDM_TINY_VPRED_256, + 'fsmn-vad': FSMN_VAD, +}; + +const urlToModelName = new Map(); +for (const [modelName, config] of Object.entries(MODEL_NAME_MAP)) { + for (const value of Object.values(config)) { + if (typeof value === 'string') { + urlToModelName.set(value, modelName); + } + } +} + +export function getModelNameForUrl(url: string): string | undefined { + return urlToModelName.get(url); +} diff --git a/packages/react-native-executorch/src/utils/ResourceFetcher.ts b/packages/react-native-executorch/src/utils/ResourceFetcher.ts index f17e14ae02..b0c4536e39 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcher.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcher.ts @@ -234,6 +234,7 @@ export class ResourceFetcher { to: resource.extendedInfo.fileUri, }); this.downloads.delete(source); + ResourceFetcherUtils.triggerDownloadEvent(resource.extendedInfo.uri); ResourceFetcherUtils.triggerHuggingFaceDownloadCounter( resource.extendedInfo.uri ); @@ -503,6 +504,7 @@ export class ResourceFetcher { to: sourceExtended.fileUri, }); this.downloads.delete(source); + ResourceFetcherUtils.triggerDownloadEvent(uri); ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri); return ResourceFetcherUtils.removeFilePrefix(sourceExtended.fileUri); } diff --git a/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts b/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts index 207adce9ee..727260301b 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcherUtils.ts @@ -1,4 +1,9 @@ import { RNEDirectory } from '../constants/directories'; +import { + DOWNLOAD_EVENT_ENDPOINT, + LIB_VERSION, + getModelNameForUrl, +} from '../constants/resourceFetcher'; import { ResourceSource } from '../types/common'; import { Asset } from 'expo-asset'; import { Logger } from '../common/Logger'; @@ -176,6 +181,50 @@ export namespace ResourceFetcherUtils { return fileInfo.exists; } + function getCountryCode(): string { + try { + const locale = Intl.DateTimeFormat().resolvedOptions().locale; + const regionTag = locale.split('-').pop(); + if (regionTag && regionTag.length === 2) { + return regionTag.toUpperCase(); + } + } catch {} + return 'UNKNOWN'; + } + + function getModelNameFromUri(uri: string): string { + const knownName = getModelNameForUrl(uri); + if (knownName) { + return knownName; + } + const pathname = new URL(uri).pathname; + const filename = pathname.split('/').pop() ?? uri; + return filename.replace(/\.[^.]+$/, ''); + } + + /** + * Sends a download event to the analytics endpoint. + * @param uri - The URI of the downloaded resource. + */ + export function triggerDownloadEvent(uri: string) { + try { + fetch(DOWNLOAD_EVENT_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + modelName: getModelNameFromUri(uri), + countryCode: getCountryCode(), + libVersion: LIB_VERSION, + }), + }); + } catch (e) {} + } + + /** + * Generates a safe filename from a URI by removing the protocol and replacing special characters. + * @param uri - The source URI. + * @returns A sanitized filename string. + */ export function getFilenameFromUri(uri: string) { let cleanUri = uri.replace(/^https?:\/\//, ''); cleanUri = cleanUri.split('#')?.[0] ?? cleanUri;