Skip to content
Open
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
46 changes: 46 additions & 0 deletions __tests__/integration/models/pixel10ImageGenCpuPath.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { hardwareService } from '../../../src/services/hardware';

const originalOS = Platform.OS;

jest.mock('react-native-device-info', () => ({
getModel: jest.fn(() => 'Pixel 10 Pro XL'),
getHardware: jest.fn(async () => 'tensor'),
getTotalMemory: jest.fn(async () => 12 * 1024 * 1024 * 1024),
getUsedMemory: jest.fn(async () => 4 * 1024 * 1024 * 1024),
getSystemName: jest.fn(() => 'Android'),
getSystemVersion: jest.fn(() => '17'),
isEmulator: jest.fn(async () => false),
getDeviceId: jest.fn(() => 'pixel10'),
}));

describe('Pixel 10 image generation CPU path integration', () => {
beforeEach(() => {
Platform.OS = 'android';
(hardwareService as any).cachedDeviceInfo = null;
(hardwareService as any).cachedSoCInfo = null;
(hardwareService as any).cachedImageRecommendation = null;
(hardwareService as any).cachedOpenCLCapability = null;
jest.clearAllMocks();
});

afterEach(() => {
Platform.OS = originalOS;
});

it('detects Pixel 10 and disables OpenCL image acceleration', async () => {
expect(hardwareService.requiresCpuImageBackend()).toBe(true);
const openCl = await hardwareService.getOpenCLCapability();
expect(openCl.supported).toBe(false);
expect(openCl.reason).toBe('pixel_10_cpu_only');
});

it('recommends only MNN models with a Pixel 10-specific banner', async () => {
const rec = await hardwareService.getImageModelRecommendation();
expect(rec.recommendedBackend).toBe('mnn');
expect(rec.compatibleBackends).toEqual(['mnn']);
expect(rec.bannerText).toContain('Pixel 10');
expect(DeviceInfo.getModel).toHaveBeenCalled();
});
});
Comment on lines +18 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Integration test only exercises a single module — duplicates unit coverage instead of validating cross-layer behavior.

Both tests here call only hardwareService methods (requiresCpuImageBackend, getOpenCLCapability, getImageModelRecommendation) with a mocked react-native-device-info. The second test (Lines 39-45) is essentially identical to the "recommends MNN with Pixel 10 CPU notice" case already added in __tests__/unit/services/hardware.test.ts (lines 1035-1050) — same setup, same assertions.

Per the PR stack, the actual CPU-only enforcement happens in ImageGenerationService (disabling OpenCL) and activeModelService (forceCpuImage/cpuOnly) — neither is exercised here. A file named pixel10ImageGenCpuPath.test.ts under __tests__/integration/ implies it should verify the full path (HardwareService detection → ImageGenerationService/activeModelService consuming it), not just re-test HardwareService in isolation.

Consider either:

  • Extending this test to actually invoke ImageGenerationService/activeModelService with the Pixel 10 mock and assert OpenCL is disabled / cpuOnly: true is set during model load, or
  • Removing this file if it doesn't add coverage beyond the unit test.

As per coding guidelines, "Integration tests in __tests__/integration/ should verify how multiple modules work together end-to-end, using mocked native modules but real logic across layers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/integration/models/pixel10ImageGenCpuPath.test.ts` around lines 18
- 46, The current integration test only re-checks hardwareService behavior and
duplicates unit coverage instead of validating the Pixel 10 CPU-only flow across
modules. Update pixel10ImageGenCpuPath.test.ts to exercise the real cross-layer
path by involving ImageGenerationService and/or activeModelService (with the
Pixel 10 mock) and assert that OpenCL is disabled and cpuOnly/forceCpuImage is
applied during model selection/load; if that end-to-end path cannot be covered
here, remove the test file since it does not add integration coverage.

Source: Coding guidelines

25 changes: 23 additions & 2 deletions __tests__/unit/services/hardware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('HardwareService', () => {
(hardwareService as any).cachedDeviceInfo = null;
(hardwareService as any).cachedSoCInfo = null;
(hardwareService as any).cachedImageRecommendation = null;
(hardwareService as any).cachedOpenCLCapability = null;
});

// ========================================================================
Expand Down Expand Up @@ -1013,7 +1014,7 @@ describe('HardwareService', () => {
});
const rec = await hardwareService.getImageModelRecommendation();
expect(rec.recommendedBackend).toBe('mnn');
expect(rec.bannerText).toContain('GPU');
expect(rec.bannerText).toContain('MNN');
expect(rec.bannerText).toContain('888');
expect(rec.compatibleBackends).toEqual(['mnn']);
});
Expand All @@ -1027,6 +1028,26 @@ describe('HardwareService', () => {
});
const rec = await hardwareService.getImageModelRecommendation();
expect(rec.recommendedBackend).toBe('mnn');
expect(rec.bannerText).toContain('MNN');
expect(rec.bannerText).toContain('Tensor');
});

it('recommends MNN with Pixel 10 CPU notice', async () => {
await setupDevice({
totalGB: 12,
platform: 'android',
hardware: 'tensor',
model: 'Pixel 10 Pro XL',
});
const rec = await hardwareService.getImageModelRecommendation();
expect(rec.recommendedBackend).toBe('mnn');
expect(rec.compatibleBackends).toEqual(['mnn']);
expect(rec.bannerText).toContain('Pixel 10');
expect(hardwareService.requiresCpuImageBackend()).toBe(true);
expect(await hardwareService.getOpenCLCapability()).toEqual({
supported: false,
reason: 'pixel_10_cpu_only',
});
});
});

Expand All @@ -1051,7 +1072,7 @@ describe('HardwareService', () => {

const rec = await hardwareService.getImageModelRecommendation();
expect(rec.recommendedBackend).toBe('mnn');
expect(rec.bannerText).toContain('GPU');
expect(rec.bannerText).toContain('MNN');
expect(rec.compatibleBackends).toEqual(['mnn']);
});
});
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/services/huggingFaceModelBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('huggingFaceModelBrowser', () => {
// parseFileName (tested indirectly via fetchAvailableModels)
// -----------------------------------------------------------------------
describe('parseFileName (via fetchAvailableModels)', () => {
it('parses MNN backend zip as a GPU model', async () => {
it('parses MNN backend zip as an MNN model', async () => {
mockFetchResponses(
{ ok: true, body: [treeEntry('AnythingV5.zip', 500, 'file', 2000)] },
{ ok: true, body: [] },
Expand All @@ -67,7 +67,7 @@ describe('huggingFaceModelBrowser', () => {
expect(models[0]).toMatchObject({
id: 'anythingv5_cpu',
name: 'AnythingV5',
displayName: 'Anything V5 (GPU)',
displayName: 'Anything V5 (MNN)',
backend: 'mnn',
fileName: 'AnythingV5.zip',
size: 2000,
Expand Down
2 changes: 1 addition & 1 deletion src/screens/DownloadManagerScreen/useDownloadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function getActiveItemFileName(
function getImageAuthor(backend?: string): string {
if (backend === 'coreml') return 'Core ML';
if (backend === 'qnn') return 'NPU';
if (backend === 'mnn') return 'GPU';
if (backend === 'mnn') return 'MNN';
return 'Image Generation';
}

Expand Down
14 changes: 11 additions & 3 deletions src/screens/ModelsScreen/ImageFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ interface Props {
hasActiveImageFilters: boolean;
clearImageFilters: () => void;
setUserChangedBackendFilter: (v: boolean) => void;
/** When false, hide the NPU filter chip (device has no QNN catalog entries). */
npuAvailable?: boolean;
}

function getBackendLabel(filter: BackendFilter): string {
if (filter === 'mnn') return 'GPU';
if (filter === 'mnn') return 'MNN';
if (filter === 'qnn') return 'NPU';
if (filter === 'coreml') return 'Core ML';
return 'Backend';
Expand All @@ -45,20 +47,21 @@ interface ExpandedSectionProps {
setStyleFilter: (f: string) => void;
setImageFilterExpanded: (d: ImageFilterDimension | ((prev: ImageFilterDimension) => ImageFilterDimension)) => void;
setUserChangedBackendFilter: (v: boolean) => void;
backendOptions: { key: BackendFilter; label: string }[];
}

const FilterExpandedSection: React.FC<ExpandedSectionProps> = ({
imageFilterExpanded, backendFilter, sdVersionFilter, styleFilter,
setBackendFilter, setSdVersionFilter, setStyleFilter,
setImageFilterExpanded, setUserChangedBackendFilter,
setImageFilterExpanded, setUserChangedBackendFilter, backendOptions,
}) => {
const styles = useThemedStyles(createStyles);

if (imageFilterExpanded === 'backend' && Platform.OS !== 'ios') {
return (
<View style={styles.filterExpandedContent}>
<View style={styles.filterChipWrap}>
{BACKEND_OPTIONS.map(option => (
{backendOptions.map(option => (
<TouchableOpacity
key={option.key}
style={[styles.filterChip, backendFilter === option.key && styles.filterChipActive]}
Expand Down Expand Up @@ -118,8 +121,12 @@ export const ImageFilterBar: React.FC<Props> = ({
imageFilterExpanded, setImageFilterExpanded,
hasActiveImageFilters, clearImageFilters,
setUserChangedBackendFilter,
npuAvailable = true,
}) => {
const styles = useThemedStyles(createStyles);
const backendOptions = npuAvailable
? BACKEND_OPTIONS
: BACKEND_OPTIONS.filter(o => o.key !== 'qnn');

const backendLabel = getBackendLabel(backendFilter);
const sdLabel = getSdLabel(sdVersionFilter);
Expand Down Expand Up @@ -175,6 +182,7 @@ export const ImageFilterBar: React.FC<Props> = ({
setStyleFilter={setStyleFilter}
setImageFilterExpanded={setImageFilterExpanded}
setUserChangedBackendFilter={setUserChangedBackendFilter}
backendOptions={backendOptions}
/>
</View>
);
Expand Down
9 changes: 6 additions & 3 deletions src/screens/ModelsScreen/ImageModelsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Props = Pick<ModelsScreenViewModel,
| 'hasActiveImageFilters'
| 'showRecommendedOnly' | 'setShowRecommendedOnly'
| 'showRecHint' | 'setShowRecHint'
| 'imageRec' | 'ramGB' | 'imageRecommendation'
| 'imageRec' | 'ramGB' | 'imageRecommendation' | 'imageNpuAvailable'
| 'handleDownloadImageModel' | 'handleCancelImageDownload' | 'loadHFModels'
| 'clearImageFilters' | 'setUserChangedBackendFilter'
| 'isRecommendedModel'
Expand Down Expand Up @@ -133,11 +133,12 @@ interface ScrollContentProps {
handleDownloadImageModel: Props['handleDownloadImageModel'];
handleCancelImageDownload: Props['handleCancelImageDownload'];
imageSearchQuery: string;
imageNpuAvailable: boolean;
}

const ImageModelsScrollContent: React.FC<ScrollContentProps> = ({
showRecHint, showRecommendedOnly, setShowRecHint,
imageRec, ramGB, imageRecommendation,
imageRec, ramGB, imageRecommendation, imageNpuAvailable,
imageFiltersVisible, backendFilter, setBackendFilter,
styleFilter, setStyleFilter, sdVersionFilter, setSdVersionFilter,
imageFilterExpanded, setImageFilterExpanded,
Expand Down Expand Up @@ -202,6 +203,7 @@ const ImageModelsScrollContent: React.FC<ScrollContentProps> = ({
hasActiveImageFilters={hasActiveImageFilters}
clearImageFilters={clearImageFilters}
setUserChangedBackendFilter={setUserChangedBackendFilter}
npuAvailable={imageNpuAvailable}
/>
)}

Expand Down Expand Up @@ -262,7 +264,7 @@ export const ImageModelsTab: React.FC<Props> = ({
hasActiveImageFilters,
showRecommendedOnly, setShowRecommendedOnly,
showRecHint, setShowRecHint,
imageRec, ramGB, imageRecommendation,
imageRec, ramGB, imageRecommendation, imageNpuAvailable,
handleDownloadImageModel, handleCancelImageDownload, loadHFModels,
clearImageFilters, setUserChangedBackendFilter,
isRecommendedModel,
Expand Down Expand Up @@ -311,6 +313,7 @@ export const ImageModelsTab: React.FC<Props> = ({
imageRec={imageRec}
ramGB={ramGB}
imageRecommendation={imageRecommendation}
imageNpuAvailable={imageNpuAvailable}
imageFiltersVisible={imageFiltersVisible}
backendFilter={backendFilter}
setBackendFilter={setBackendFilter}
Expand Down
2 changes: 1 addition & 1 deletion src/screens/ModelsScreen/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const SD_VERSION_OPTIONS = [

export const BACKEND_OPTIONS: { key: BackendFilter; label: string }[] = [
{ key: 'all', label: 'All' },
{ key: 'mnn', label: 'GPU' },
{ key: 'mnn', label: 'MNN' },
{ key: 'qnn', label: 'NPU' },
];

Expand Down
2 changes: 1 addition & 1 deletion src/screens/ModelsScreen/imageDownloadQnn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function getQnnWarningMessage(
if (!socInfo.hasNPU) {
return 'NPU models require a Qualcomm Snapdragon processor. ' +
'Your device does not have a compatible NPU and this model will not work. ' +
'Consider downloading a CPU model instead.';
'Consider downloading an MNN model instead.';
}
if (!modelInfo.variant || !socInfo.qnnVariant) return null;

Expand Down
8 changes: 8 additions & 0 deletions src/screens/ModelsScreen/useImageModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
const [imageSearchQuery, setImageSearchQuery] = useState('');
const [imageFiltersVisible, setImageFiltersVisible] = useState(false);
const [imageRec, setImageRec] = useState<ImageModelRecommendation | null>(null);
const [imageNpuAvailable, setImageNpuAvailable] = useState(false);
const [userChangedBackendFilter, setUserChangedBackendFilter] = useState(false);
const [showRecommendedOnly, setShowRecommendedOnly] = useState(true);
const [showRecHint, setShowRecHint] = useState(true);
Expand Down Expand Up @@ -66,6 +67,7 @@
})));
} else {
const socInfo = await hardwareService.getSoCInfo();
setImageNpuAvailable(socInfo.hasNPU);
setAvailableHFModels(await fetchAvailableModels(forceRefresh, { skipQnn: !socInfo.hasNPU }));
}
} catch (error: any) {
Expand Down Expand Up @@ -130,6 +132,11 @@
hardwareService.getImageModelRecommendation().then(rec => {
if (cancelled) return;
setImageRec(rec);
hardwareService.getSoCInfo().then(soc => {
if (cancelled) return;
setImageNpuAvailable(soc.hasNPU);
setBackendFilter(prev => (!soc.hasNPU && prev === 'qnn') ? 'mnn' : prev);

Check failure on line 138 in src/screens/ModelsScreen/useImageModels.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest functions more than 4 levels deep.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ9KqUNdUAjAyoCpgvto&open=AZ9KqUNdUAjAyoCpgvto&pullRequest=457
});
Comment on lines +135 to +139

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Unhandled socinfo rejection 🐞 Bug ☼ Reliability

useImageModels() starts a nested hardwareService.getSoCInfo() promise chain without error handling,
so a failure in SoC detection can produce an unhandled promise rejection and skip updating
imageNpuAvailable/backendFilter. This can leave the image models UI in an inconsistent state after a
DeviceInfo/native failure.
Agent Prompt
### Issue description
`useImageModels()` triggers `hardwareService.getSoCInfo()` in a nested `.then(...)` chain without a `.catch(...)` or surrounding `try/catch`. If `getSoCInfo()` rejects (e.g., `DeviceInfo.getHardware()` fails), this becomes an unhandled promise rejection and the state updates inside the `.then` never run.

### Issue Context
`hardwareService.getSoCInfo()` contains awaited native calls that can reject and it does not internally catch/convert those failures.

### Fix Focus Areas
- src/screens/ModelsScreen/useImageModels.ts[130-139]
- src/services/hardware.ts[308-334]

### Suggested fix
- In the mount effect, convert the chain to `async/await` with `try/catch`, or attach `.catch(...)` to the `getSoCInfo()` call.
- On error, set a safe fallback (e.g., `setImageNpuAvailable(false)` and ensure `backendFilter` is not left as `'qnn'`).
- Optionally, consider making `getSoCInfo()` resilient by catching native errors and returning `{ vendor: 'unknown', hasNPU: false }` rather than rejecting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

if (!userChangedBackendFilter && Platform.OS !== 'ios') {
let filter: 'qnn' | 'mnn' | 'all';
if (rec.recommendedBackend === 'qnn') filter = 'qnn';
Expand Down Expand Up @@ -208,6 +215,7 @@
imageFiltersVisible, setImageFiltersVisible,
imageRec, showRecommendedOnly, setShowRecommendedOnly,
showRecHint, setShowRecHint,
imageNpuAvailable,
downloadedImageModels,
hasActiveImageFilters, filteredHFModels, imageRecommendation,
loadHFModels, loadDownloadedImageModels,
Expand Down
1 change: 1 addition & 0 deletions src/screens/ModelsScreen/useModelsScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export function useModelsScreen() {
imageFiltersVisible: image.imageFiltersVisible,
setImageFiltersVisible: image.setImageFiltersVisible,
imageRec: image.imageRec,
imageNpuAvailable: image.imageNpuAvailable,
showRecommendedOnly: image.showRecommendedOnly,
setShowRecommendedOnly: image.setShowRecommendedOnly,
showRecHint: image.showRecHint,
Expand Down
5 changes: 3 additions & 2 deletions src/services/activeModelService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class ActiveModelService {
return {
canLoad: false,
error:
'NPU models require a Qualcomm Snapdragon processor. Your device does not have a compatible NPU. Please use a GPU model instead.',
'NPU models require a Qualcomm Snapdragon processor. Your device does not have a compatible NPU. Please download an MNN model instead.',
};
}
}
Expand Down Expand Up @@ -308,12 +308,13 @@ class ActiveModelService {
}
this.loadingState.image = true;
this.notifyListeners();
const forceCpuImage = hardwareService.requiresCpuImageBackend();
this.imageLoadPromise = doLoadImageModel({
model,
modelId,
imageThreads,
needsThreadReload,
cpuOnly: false,
cpuOnly: forceCpuImage,
preferGpu: hardwareService.preferGpuForImageGen(),
store,
timeoutMs,
Expand Down
25 changes: 23 additions & 2 deletions src/services/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,14 +397,23 @@
rec = {
recommendedBackend: 'mnn',
bannerText:
'GPU models recommended \u2014 your Snapdragon doesn\u2019t support NPU acceleration',
'MNN models recommended \u2014 your Snapdragon does not support NPU acceleration',
compatibleBackends: ['mnn'],
};
} else if (socInfo.vendor === 'tensor') {
rec = {
recommendedBackend: 'mnn',
bannerText:
this.requiresCpuImageBackend()
? 'MNN models recommended for Pixel \u2014 image generation runs on CPU on Pixel 10 until GPU support lands'
: 'MNN models recommended for Pixel and Tensor devices',
compatibleBackends: ['mnn'],
};
} else {
rec = {
recommendedBackend: 'mnn',
bannerText:
'GPU models recommended \u2014 NPU requires Snapdragon 888+',
'MNN models recommended \u2014 NPU acceleration requires Snapdragon 888+',
Comment on lines 397 to +416

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Em dashes in bannertext 📘 Rule violation ✧ Quality

User-facing bannerText strings include Unicode em dashes (), which are disallowed in
user-visible text content. This can lead to noncompliant UI copy and inconsistent typography across
platforms.
Agent Prompt
## Issue description
User-facing `bannerText` strings in `HardwareService.getImageModelRecommendation()` contain Unicode em dashes (`—`, U+2014). The compliance rule requires using ASCII hyphens (`-`) instead in user-visible text.

## Issue Context
These `bannerText` fields are displayed to users as recommendation banners, so they are in-scope for the text-content restriction.

## Fix Focus Areas
- src/services/hardware.ts[397-416]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

compatibleBackends: ['mnn'],
};
}
Expand Down Expand Up @@ -436,6 +445,9 @@
async getOpenCLCapability(): Promise<{ supported: boolean; reason?: string }> {
if (this.cachedOpenCLCapability) return this.cachedOpenCLCapability;
if (Platform.OS !== 'android') return { supported: false, reason: 'not_android' };
if (this.requiresCpuImageBackend()) {
return (this.cachedOpenCLCapability = { supported: false, reason: 'pixel_10_cpu_only' });

Check warning on line 449 in src/services/hardware.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment of "this.cachedOpenCLCapability" from this expression.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ9KqUI-UAjAyoCpgvtn&open=AZ9KqUI-UAjAyoCpgvtn&pullRequest=457
}
try {
const hardware = (await DeviceInfo.getHardware()).toLowerCase();
// Support Qualcomm Adreno (qcom) and ARM Mali GPUs.
Expand All @@ -445,5 +457,14 @@
return (this.cachedOpenCLCapability = { supported: true });
} catch { return (this.cachedOpenCLCapability = { supported: false, reason: 'detection_failed' }); }
}

/**
* Pixel 10 OpenCL/GPU image backends are broken on-device (same class of bug as
* LiteRT GPU on Pixel 10). Force the MNN CPU path for image load + generation.
*/
requiresCpuImageBackend(): boolean {
if (Platform.OS !== 'android') return false;
return DeviceInfo.getModel().toLowerCase().includes('pixel 10');
}
}
export const hardwareService = new HardwareService();
5 changes: 3 additions & 2 deletions src/services/huggingFaceModelBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ function parseFileName(fileName: string, backend: 'mnn' | 'qnn'): Omit<HFImageMo
};
}

// GPU: e.g. "AnythingV5.zip"
// MNN: CPU/GPU via OpenCL — label as MNN so users on Pixel/Tensor are not told
// to pick a separate "CPU model" while the catalog only lists "GPU" and "NPU".
return {
id: `${baseName.toLowerCase()}_cpu`,
name: baseName,
displayName: `${insertSpaces(baseName)} (GPU)`,
displayName: `${insertSpaces(baseName)} (MNN)`,
backend: 'mnn',
fileName,
};
Expand Down
Loading