Skip to content

Commit d488d1b

Browse files
authored
fix(privacy-filter)!: resolve labels from modelName, fix fromCustomModel identity collapse (#1198)
## Description `PrivacyFilterModelSources` carried `labelNames` / `viterbiBiases` on it but `usePrivacyFilter`'s deps only watched `modelName` / `modelSource` / `tokenizerSource`, so two custom loads with the same sources but different labels collapsed to the same identity and the runner kept the old config silently. `fromCustomModel` made it worse by hardcoding `modelName: 'custom'`. Followed @chmjkb's suggestion on #1116: match the `ObjectDetection` / `Segmentation` shape — drop `labelNames` / `viterbiBiases` from `PrivacyFilterModelSources`, resolve them from `modelName` via an internal lookup, and keep custom-label support only on `fromCustomModel` (unreachable from the hook). The hook's existing deps now fully cover the prop surface, so the collapse is gone. ### Introduces a breaking change? - [x] Yes - [ ] No (`labelNames` / `viterbiBiases` are no longer accepted as fields on `PrivacyFilterModelSources` or on the `PRIVACY_FILTER_*` constants. Custom label spaces and Viterbi tuning move to `PrivacyFilterModule.fromCustomModel`.) ### Type of change - [x] Bug fix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [ ] Other (chores, tests, code style improvements etc.) ### Tested on - [ ] iOS - [ ] Android ### Testing instructions 1. Run \`apps/llm\` and open the Privacy Filter screen. 2. Switch between the OpenAI and Nemotron presets — both should load and produce the expected entity spans. 3. Re-trigger the hook with the same preset — should not reload. ### Related issues Closes #1116 ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly - [x] My changes generate no new warnings
1 parent 95febe0 commit d488d1b

6 files changed

Lines changed: 90 additions & 77 deletions

File tree

‎docs/docs/03-hooks/01-natural-language-processing/usePrivacyFilter.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ try {
5353

5454
`usePrivacyFilter` takes [`PrivacyFilterProps`](../../06-api-reference/interfaces/PrivacyFilterProps.md) that consists of:
5555

56-
- `model` of type [`PrivacyFilterModelSources`](../../06-api-reference/interfaces/PrivacyFilterModelSources.md) containing the model source, tokenizer source, and BIOES label list.
56+
- `model` of type [`PrivacyFilterModelSources`](../../06-api-reference/type-aliases/PrivacyFilterModelSources.md) — a built-in preset (`modelName` + `modelSource` + `tokenizerSource`). The label list and Viterbi defaults are resolved from `modelName`; for custom fine-tunes use [`PrivacyFilterModule.fromCustomModel`](../../04-typescript-api/01-natural-language-processing/PrivacyFilterModule.md) directly.
5757
- An optional flag [`preventLoad`](../../06-api-reference/interfaces/PrivacyFilterProps.md#preventload) which prevents auto-loading of the model.
5858

5959
You need more details? Check the following resources:
@@ -78,7 +78,7 @@ Token indices in returned entities are positions in the tokenizer's output (the
7878

7979
### Tuning precision and recall
8080

81-
Both built-in models ship with neutral, validity-only Viterbi decoding by default. If you want to shift the precision/recall tradeoff, pass an optional [`viterbiBiases`](../../06-api-reference/interfaces/PrivacyFilterModelSources.md#viterbibiases) object — six floats matching the operating-point schema in OpenAI's `viterbi_calibration.json`. Negative `backgroundToStart` makes the decoder enter spans more eagerly (higher recall); positive `backgroundStay` keeps it in the background label more often (higher precision).
81+
Built-in presets run with neutral, validity-only Viterbi decoding. To shift the precision/recall tradeoff, load the model directly through [`PrivacyFilterModule.fromCustomModel`](../../04-typescript-api/01-natural-language-processing/PrivacyFilterModule.md) (reusing the preset's `modelSource` / `tokenizerSource` and label list) and pass a [`viterbiBiases`](../../06-api-reference/interfaces/ViterbiBiases.md) object — six floats matching the operating-point schema in OpenAI's `viterbi_calibration.json`. Negative `backgroundToStart` makes the decoder enter spans more eagerly (higher recall); positive `backgroundStay` keeps it in the background label more often (higher precision).
8282

8383
## Example
8484

‎docs/docs/04-typescript-api/01-natural-language-processing/PrivacyFilterModule.md‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,18 @@ All methods of `PrivacyFilterModule` are explained in details here: [`PrivacyFil
2727

2828
## Loading the model
2929

30-
To create a ready-to-use instance, call the static [`fromModelName`](../../06-api-reference/classes/PrivacyFilterModule.md#frommodelname) factory with the following parameters:
30+
To create a ready-to-use instance for a built-in preset, call the static [`fromModelName`](../../06-api-reference/classes/PrivacyFilterModule.md#frommodelname) factory with the following parameters:
3131

3232
- `namedSources` — Object containing:
33-
- `modelName` — Model name identifier.
33+
- `modelName` — Built-in preset identifier (`'privacy-filter-openai'` or `'privacy-filter-nemotron'`). The runner resolves the matching BIOES label list from this value.
3434
- `modelSource` — Location of the `.pte` model binary.
3535
- `tokenizerSource` — Location of the `tokenizer.json` file.
36-
- `labelNames` — BIOES label list. Index 0 must be `"O"`; the rest must follow the model's `id2label` mapping exactly.
37-
- `viterbiBiases` (optional) — Six-field bias struct that shifts the decoder's precision/recall tradeoff. Defaults to neutral (validity-only Viterbi).
3836

3937
- `onDownloadProgress` — Optional callback to track download progress (value between 0 and 1).
4038

4139
The factory returns a promise that resolves to a loaded `PrivacyFilterModule` instance.
4240

43-
For custom-exported models, use [`fromCustomModel`](../../06-api-reference/classes/PrivacyFilterModule.md#fromcustommodel) instead — it takes the same fields as positional arguments and is convenient when you only have the raw resource locations.
41+
For custom-exported models with a non-standard label space, use [`fromCustomModel`](../../06-api-reference/classes/PrivacyFilterModule.md#fromcustommodel) instead. It takes `modelSource`, `tokenizerSource`, and a `labelNames` array (BIOES; index 0 must be `"O"`, the rest must follow the model's `id2label` mapping exactly), plus an optional `options` object with `viterbiBiases` (six-field bias struct that shifts the decoder's precision/recall tradeoff; defaults to neutral validity-only Viterbi) and `onDownloadProgress`.
4442

4543
For more information on loading resources, take a look at [loading models](../../01-fundamentals/02-loading-models.md) page.
4644

‎packages/react-native-executorch/src/constants/modelUrls.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
import { Platform } from 'react-native';
2-
import {
3-
PRIVACY_FILTER_NEMOTRON_LABELS,
4-
PRIVACY_FILTER_OPENAI_LABELS,
5-
} from './privacyFilterLabels';
62
import { URL_PREFIX, PREVIOUS_VERSION_TAG } from './versions';
73

84
// LLMs
@@ -1248,7 +1244,6 @@ export const PRIVACY_FILTER_OPENAI = {
12481244
modelName: 'privacy-filter-openai',
12491245
modelSource: `${URL_PREFIX}-privacy-filter-openai/${PREVIOUS_VERSION_TAG}/xnnpack/privacy_filter_openai_xnnpack_8da4w.pte`,
12501246
tokenizerSource: `${URL_PREFIX}-privacy-filter-openai/${PREVIOUS_VERSION_TAG}/tokenizer.json`,
1251-
labelNames: PRIVACY_FILTER_OPENAI_LABELS,
12521247
} as const;
12531248

12541249
/**
@@ -1261,7 +1256,6 @@ export const PRIVACY_FILTER_NEMOTRON = {
12611256
modelName: 'privacy-filter-nemotron',
12621257
modelSource: `${URL_PREFIX}-privacy-filter-nemotron/${PREVIOUS_VERSION_TAG}/xnnpack/privacy_filter_nemotron_xnnpack_8da4w.pte`,
12631258
tokenizerSource: `${URL_PREFIX}-privacy-filter-nemotron/${PREVIOUS_VERSION_TAG}/tokenizer.json`,
1264-
labelNames: PRIVACY_FILTER_NEMOTRON_LABELS,
12651259
} as const;
12661260

12671261
// Image generation

‎packages/react-native-executorch/src/constants/privacyFilterLabels.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { PrivacyFilterModelName } from '../types/privacyFilter';
2+
13
// BIOES tag scheme: 1 outside ("O") + 4 prefix variants × N entity types.
24
// These arrays must match the model's id2label mapping exactly — the runner
35
// uses index = label id, and labels[0] must be "O".
@@ -270,3 +272,17 @@ export const PRIVACY_FILTER_NEMOTRON_LABELS = [
270272
'E-vehicle_identifier',
271273
'S-vehicle_identifier',
272274
] as const;
275+
276+
/**
277+
* Per-model label list lookup for built-in privacy filter presets. The
278+
* runner resolves the label space from `modelName`; custom fine-tunes
279+
* bypass this map and pass their own list through
280+
* `PrivacyFilterModule.fromCustomModel`.
281+
*/
282+
export const PRIVACY_FILTER_LABELS: Record<
283+
PrivacyFilterModelName,
284+
readonly string[]
285+
> = {
286+
'privacy-filter-openai': PRIVACY_FILTER_OPENAI_LABELS,
287+
'privacy-filter-nemotron': PRIVACY_FILTER_NEMOTRON_LABELS,
288+
};

‎packages/react-native-executorch/src/modules/natural_language_processing/PrivacyFilterModule.ts‎

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
PrivacyFilterModelSources,
55
ViterbiBiases,
66
} from '../../types/privacyFilter';
7+
import { PRIVACY_FILTER_LABELS } from '../../constants/privacyFilterLabels';
78
import { ResourceFetcher } from '../../utils/ResourceFetcher';
89
import { BaseModule } from '../BaseModule';
910
import { RnExecutorchErrorCode } from '../../errors/ErrorCodes';
@@ -41,38 +42,26 @@ export class PrivacyFilterModule extends BaseModule {
4142
}
4243

4344
/**
44-
* Creates a Privacy Filter instance for a built-in or custom-shaped model.
45-
* Pass one of the `PRIVACY_FILTER_*` constants from
46-
* `react-native-executorch/constants` for a known-good config, or
47-
* construct your own {@link PrivacyFilterModelSources} for a custom
48-
* fine-tune.
49-
* @param namedSources - Model + tokenizer resource locations and label list.
45+
* Creates a Privacy Filter instance for a built-in preset. Pass one of
46+
* the `PRIVACY_FILTER_*` constants from `react-native-executorch/constants`
47+
* (or the matching `models.privacy_filter.*` accessor); the runner
48+
* resolves the BIOES label list from `modelName`. For custom fine-tunes
49+
* with a non-standard label space, use {@link fromCustomModel} instead.
50+
* @param namedSources - Built-in model + tokenizer resource locations.
5051
* @param onDownloadProgress - Optional 0..1 download progress callback.
5152
* @returns A Promise resolving to a `PrivacyFilterModule` instance.
5253
*/
53-
static async fromModelName(
54+
static fromModelName(
5455
namedSources: PrivacyFilterModelSources,
5556
onDownloadProgress: (progress: number) => void = () => {}
5657
): Promise<PrivacyFilterModule> {
57-
try {
58-
const [modelResult, tokenizerResult] = await Promise.all([
59-
ResourceFetcher.fetch(onDownloadProgress, namedSources.modelSource),
60-
ResourceFetcher.fetch(undefined, namedSources.tokenizerSource),
61-
]);
62-
const modelPath = modelResult?.[0];
63-
const tokenizerPath = tokenizerResult?.[0];
64-
if (!modelPath || !tokenizerPath) {
65-
throw new RnExecutorchError(RnExecutorchErrorCode.DownloadInterrupted);
66-
}
67-
const labels = Array.from(namedSources.labelNames);
68-
const biases = packViterbiBiases(namedSources.viterbiBiases);
69-
return new PrivacyFilterModule(
70-
await global.loadPrivacyFilter(modelPath, tokenizerPath, labels, biases)
71-
);
72-
} catch (error) {
73-
Logger.error('Load failed:', error);
74-
throw parseUnknownError(error);
75-
}
58+
return PrivacyFilterModule.load(
59+
namedSources.modelSource,
60+
namedSources.tokenizerSource,
61+
PRIVACY_FILTER_LABELS[namedSources.modelName],
62+
undefined,
63+
onDownloadProgress
64+
);
7665
}
7766

7867
/**
@@ -94,18 +83,43 @@ export class PrivacyFilterModule extends BaseModule {
9483
onDownloadProgress?: (progress: number) => void;
9584
} = {}
9685
): Promise<PrivacyFilterModule> {
97-
return PrivacyFilterModule.fromModelName(
98-
{
99-
modelName: 'custom',
100-
modelSource,
101-
tokenizerSource,
102-
labelNames,
103-
viterbiBiases: options.viterbiBiases,
104-
},
86+
return PrivacyFilterModule.load(
87+
modelSource,
88+
tokenizerSource,
89+
labelNames,
90+
options.viterbiBiases,
10591
options.onDownloadProgress ?? (() => {})
10692
);
10793
}
10894

95+
private static async load(
96+
modelSource: ResourceSource,
97+
tokenizerSource: ResourceSource,
98+
labelNames: readonly string[],
99+
viterbiBiases: ViterbiBiases | undefined,
100+
onDownloadProgress: (progress: number) => void
101+
): Promise<PrivacyFilterModule> {
102+
try {
103+
const [modelResult, tokenizerResult] = await Promise.all([
104+
ResourceFetcher.fetch(onDownloadProgress, modelSource),
105+
ResourceFetcher.fetch(undefined, tokenizerSource),
106+
]);
107+
const modelPath = modelResult?.[0];
108+
const tokenizerPath = tokenizerResult?.[0];
109+
if (!modelPath || !tokenizerPath) {
110+
throw new RnExecutorchError(RnExecutorchErrorCode.DownloadInterrupted);
111+
}
112+
const labels = Array.from(labelNames);
113+
const biases = packViterbiBiases(viterbiBiases);
114+
return new PrivacyFilterModule(
115+
await global.loadPrivacyFilter(modelPath, tokenizerPath, labels, biases)
116+
);
117+
} catch (error) {
118+
Logger.error('Load failed:', error);
119+
throw parseUnknownError(error);
120+
}
121+
}
122+
109123
/**
110124
* Executes the model's forward pass to detect PII entity spans within the provided text.
111125
* @param text - The input text to scan for PII.

‎packages/react-native-executorch/src/types/privacyFilter.ts‎

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import { RnExecutorchError } from '../errors/errorUtils';
22
import { ResourceSource } from './common';
33

4-
/**
5-
* Union of all built-in privacy filter model names.
6-
* @category Types
7-
*/
8-
export type PrivacyFilterModelName =
9-
| 'privacy-filter-openai'
10-
| 'privacy-filter-nemotron';
11-
124
/**
135
* Six Viterbi transition biases that match the operating-point schema
146
* from the openai/privacy-filter `viterbi_calibration.json`. Each value
@@ -35,30 +27,29 @@ export interface ViterbiBiases {
3527
}
3628

3729
/**
38-
* Bundle of resources needed to instantiate a privacy filter model. The
39-
* built-in `PRIVACY_FILTER_OPENAI` / `PRIVACY_FILTER_NEMOTRON` constants
40-
* conform to this shape; you can also build one yourself for a custom
41-
* fine-tune as long as the label list matches the model's id2label.
30+
* Per-model config for {@link PrivacyFilterModule.fromModelName}. Each
31+
* built-in `modelName` resolves to its baked-in label list and default
32+
* Viterbi biases; custom fine-tunes go through
33+
* {@link PrivacyFilterModule.fromCustomModel} instead.
4234
* @category Types
4335
*/
44-
export interface PrivacyFilterModelSources {
45-
modelName: PrivacyFilterModelName | (string & {});
46-
modelSource: ResourceSource;
47-
tokenizerSource: ResourceSource;
48-
/**
49-
* BIOES label list. Index 0 must be "O"; index i must equal the model's
50-
* id2label[i]. The runner argmaxes over `labelNames.length` classes per
51-
* token, so the size must match the model head exactly.
52-
*/
53-
labelNames: readonly string[];
54-
/**
55-
* Optional Viterbi calibration. When present, biases are added during
56-
* decoding to shift the precision/recall tradeoff. Defaults to all
57-
* zeros (neutral) — same as the `default` operating point in OpenAI's
58-
* `viterbi_calibration.json`.
59-
*/
60-
viterbiBiases?: ViterbiBiases;
61-
}
36+
export type PrivacyFilterModelSources =
37+
| {
38+
modelName: 'privacy-filter-openai';
39+
modelSource: ResourceSource;
40+
tokenizerSource: ResourceSource;
41+
}
42+
| {
43+
modelName: 'privacy-filter-nemotron';
44+
modelSource: ResourceSource;
45+
tokenizerSource: ResourceSource;
46+
};
47+
48+
/**
49+
* Union of all built-in privacy filter model names.
50+
* @category Types
51+
*/
52+
export type PrivacyFilterModelName = PrivacyFilterModelSources['modelName'];
6253

6354
/**
6455
* A single detected PII entity span.

0 commit comments

Comments
 (0)