Skip to content

Commit 8a26aaf

Browse files
test(image-gen): GPU-path advice rule, advice card, and the 256 size floor
- imageGenAdvice unit: mnn-only; raiseSteps (<20), lowerSize (>256), raiseSize (<256 = garbage, the 128 case), quiet at the 256/20 sweet spot, no false raiseSize at width 0. - ImageGenAdviceCard (RNTL): hidden off-mnn + at sweet spot; shows the right tip per setting, driving the real store + real rule. - imageGenerationFlow: a stale 128 setting is floored to 256 before the native call; a valid 512 passes through unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3ea60c1 commit 8a26aaf

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

__tests__/integration/generation/imageGenerationFlow.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,43 @@ describe('Image Generation Flow Integration', () => {
16181618
// re-run the load forcing past the budget. Before the fix, imageGenerationService
16191619
// stringified the typed OverridableMemoryError, so the override was never offered.
16201620
// ============================================================================
1621+
describe('size floor (never generate at a garbage sub-256 resolution)', () => {
1622+
it('floors a stale 128 setting up to 256 before it reaches the native pipeline', async () => {
1623+
const imageModel = setupImageModelState();
1624+
// Simulate the on-device state: user had dragged size down to 128 (garbage for SD1.5).
1625+
useAppStore.setState({ settings: { ...useAppStore.getState().settings, imageWidth: 128, imageHeight: 128 } });
1626+
mockActiveModelService.getActiveModels.mockReturnValue({
1627+
text: { model: null, isLoaded: false, isLoading: false },
1628+
image: { model: imageModel, isLoaded: true, isLoading: false },
1629+
});
1630+
1631+
await imageGenerationService.generateImage({ prompt: 'a dog' });
1632+
1633+
expect(mockLocalDreamService.generateImage).toHaveBeenCalledWith(
1634+
expect.objectContaining({ width: 256, height: 256 }),
1635+
expect.any(Function),
1636+
expect.any(Function),
1637+
);
1638+
});
1639+
1640+
it('passes a valid 512 through unchanged', async () => {
1641+
const imageModel = setupImageModelState();
1642+
useAppStore.setState({ settings: { ...useAppStore.getState().settings, imageWidth: 512, imageHeight: 512 } });
1643+
mockActiveModelService.getActiveModels.mockReturnValue({
1644+
text: { model: null, isLoaded: false, isLoading: false },
1645+
image: { model: imageModel, isLoaded: true, isLoading: false },
1646+
});
1647+
1648+
await imageGenerationService.generateImage({ prompt: 'a dog' });
1649+
1650+
expect(mockLocalDreamService.generateImage).toHaveBeenCalledWith(
1651+
expect.objectContaining({ width: 512, height: 512 }),
1652+
expect.any(Function),
1653+
expect.any(Function),
1654+
);
1655+
});
1656+
});
1657+
16211658
describe('Load Anyway override (overridable memory gate)', () => {
16221659
beforeEach(() => useModelFailureStore.getState().clear());
16231660

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* ImageGenAdviceCard — GPU-path speed/quality guidance. Renders nothing off the mnn path
3+
* or at good settings; shows the right tips (raise steps / lower size / raise size) when
4+
* the live settings warrant it. Drives the REAL store + REAL advice rule.
5+
*/
6+
import React from 'react';
7+
import { render } from '@testing-library/react-native';
8+
9+
jest.mock('react-native-vector-icons/Feather', () => 'Icon');
10+
11+
import { ImageGenAdviceCard } from '../../../src/components/GenerationSettingsModal/ImageGenAdviceCard';
12+
import { useAppStore } from '../../../src/stores';
13+
14+
const setup = (backend: string | undefined, imageSteps: number, imageWidth: number) => {
15+
useAppStore.setState({
16+
downloadedImageModels: backend
17+
? ([{ id: 'img', name: 'M', modelPath: '/m', backend, downloadedAt: '', size: 1 }] as any)
18+
: ([] as any),
19+
activeImageModelId: backend ? 'img' : null,
20+
settings: { ...useAppStore.getState().settings, imageSteps, imageWidth, imageHeight: imageWidth },
21+
});
22+
};
23+
24+
describe('ImageGenAdviceCard', () => {
25+
it('renders nothing on the NPU (qnn) path', () => {
26+
setup('qnn', 8, 512);
27+
const { queryByTestId } = render(<ImageGenAdviceCard />);
28+
expect(queryByTestId('image-gen-advice')).toBeNull();
29+
});
30+
31+
it('renders nothing at the sweet spot (mnn, 22 steps, 256)', () => {
32+
setup('mnn', 22, 256);
33+
const { queryByTestId } = render(<ImageGenAdviceCard />);
34+
expect(queryByTestId('image-gen-advice')).toBeNull();
35+
});
36+
37+
it('shows the raise-steps tip on the GPU path at low steps', () => {
38+
setup('mnn', 8, 256);
39+
const { getByTestId, queryByTestId } = render(<ImageGenAdviceCard />);
40+
expect(getByTestId('image-gen-advice')).toBeTruthy();
41+
expect(getByTestId('image-gen-advice-steps')).toBeTruthy();
42+
expect(queryByTestId('image-gen-advice-size')).toBeNull();
43+
});
44+
45+
it('shows the lower-size tip when too large (512)', () => {
46+
setup('mnn', 22, 512);
47+
const { getByTestId } = render(<ImageGenAdviceCard />);
48+
expect(getByTestId('image-gen-advice-size')).toBeTruthy();
49+
});
50+
51+
it('shows the raise-size (garbage) tip when below 256 (the 128 case)', () => {
52+
setup('mnn', 22, 128);
53+
const { getByTestId, queryByTestId } = render(<ImageGenAdviceCard />);
54+
expect(getByTestId('image-gen-advice-raise-size')).toBeTruthy();
55+
expect(queryByTestId('image-gen-advice-size')).toBeNull();
56+
});
57+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* getImageGenAdvice — the GPU-path (mnn) speed/quality guidance rule. Only the mnn path
3+
* gets advice (NPU/CoreML are fast + fixed-shape). Encodes the on-device reality:
4+
* - <20 steps looks muddy,
5+
* - >256 is very slow on a mid-tier GPU,
6+
* - <256 is GARBAGE (SD1.5 below training res), not just smaller.
7+
*/
8+
import { getImageGenAdvice, QUALITY_STEP_FLOOR, SWEET_SPOT_SIZE } from '../../../src/utils/imageGenAdvice';
9+
10+
describe('getImageGenAdvice', () => {
11+
it('gives NO advice for the NPU (qnn) path', () => {
12+
expect(getImageGenAdvice({ backend: 'qnn', steps: 8, width: 512 })).toEqual({
13+
show: false, raiseSteps: false, lowerSize: false, raiseSize: false,
14+
});
15+
});
16+
17+
it('gives NO advice for CoreML (iOS ANE)', () => {
18+
expect(getImageGenAdvice({ backend: 'coreml', steps: 4, width: 128 }).show).toBe(false);
19+
});
20+
21+
it('recommends raising steps on the GPU path when below the quality floor', () => {
22+
const a = getImageGenAdvice({ backend: 'mnn', steps: QUALITY_STEP_FLOOR - 1, width: SWEET_SPOT_SIZE });
23+
expect(a.raiseSteps).toBe(true);
24+
expect(a.show).toBe(true);
25+
});
26+
27+
it('does not nag about steps at/above the quality floor', () => {
28+
expect(getImageGenAdvice({ backend: 'mnn', steps: QUALITY_STEP_FLOOR, width: SWEET_SPOT_SIZE }).raiseSteps).toBe(false);
29+
});
30+
31+
it('recommends LOWERING size for speed when above the sweet spot', () => {
32+
const a = getImageGenAdvice({ backend: 'mnn', steps: 22, width: 512 });
33+
expect(a.lowerSize).toBe(true);
34+
expect(a.raiseSize).toBe(false);
35+
expect(a.show).toBe(true);
36+
});
37+
38+
it('recommends RAISING size when below 256 (garbage, not "smaller") — the 128 case', () => {
39+
const a = getImageGenAdvice({ backend: 'mnn', steps: 22, width: 128 });
40+
expect(a.raiseSize).toBe(true);
41+
expect(a.lowerSize).toBe(false);
42+
expect(a.show).toBe(true);
43+
});
44+
45+
it('is quiet at the sweet spot (256, >=20 steps)', () => {
46+
expect(getImageGenAdvice({ backend: 'mnn', steps: 22, width: SWEET_SPOT_SIZE })).toEqual({
47+
show: false, raiseSteps: false, lowerSize: false, raiseSize: false,
48+
});
49+
});
50+
51+
it('a zero/unknown width does not falsely trigger raiseSize', () => {
52+
expect(getImageGenAdvice({ backend: 'mnn', steps: 22, width: 0 }).raiseSize).toBe(false);
53+
});
54+
});

0 commit comments

Comments
 (0)