Skip to content

Commit fc44837

Browse files
committed
VLM integration via Ollama added
1 parent a5da984 commit fc44837

20 files changed

Lines changed: 722 additions & 4 deletions

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ AWS_S3_BUCKET_NAME=
3737
#SERVER_TIMEOUT=120000
3838
#SERVER_HEADERS_TIMEOUT=60000
3939
#SERVER_KEEP_ALIVE_TIMEOUT=5000
40+
OLLAMA_BASE_URL=http://localhost:11434

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ services:
1616
APP_FRONTEND_URL: ${APP_FRONTEND_URL}
1717
BODY_PARSER_JSON_LIMIT: ${BODY_PARSER_JSON_LIMIT}
1818
ELASTIC_URL: ${ELASTIC_URL}
19+
# VLM: Uncomment to use Ollama running on host machine
20+
OLLAMA_BASE_URL: http://host.docker.internal:11434
1921
ports:
2022
- "${APP_PORT}:3000"
2123
expose:
2224
- "${APP_PORT}"
25+
# VLM: Uncomment to use Ollama running on host machine
26+
extra_hosts:
27+
- host.docker.internal:host-gateway
2328
depends_on:
2429
postgres:
2530
condition: service_healthy
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Please do not edit this file manually
2-
# It should be added in your version-control system (i.e. Git)
3-
provider = "postgresql"
2+
# It should be added in your version-control system (e.g., Git)
3+
provider = "postgresql"

prisma/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ model TestRun {
6969
baselineBranchName String?
7070
ignoreAreas String @default("[]")
7171
tempIgnoreAreas String @default("[]")
72+
vlmDescription String?
7273
baseline Baseline?
7374
build Build @relation(fields: [buildId], references: [id])
7475
project Project? @relation(fields: [projectId], references: [id])
@@ -138,6 +139,7 @@ enum ImageComparison {
138139
pixelmatch
139140
lookSame
140141
odiff
142+
vlm
141143
}
142144

143145
enum Role {

src/_data_/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const generateTestRun = (testRun?: Partial<TestRun>): TestRun => {
9696
baselineBranchName: 'master',
9797
branchName: 'develop',
9898
merge: false,
99+
vlmDescription: null,
99100
...testRun,
100101
};
101102
};

src/compare/compare.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { CompareService } from './compare.service';
33
import { LookSameService } from './libs/looks-same/looks-same.service';
44
import { OdiffService } from './libs/odiff/odiff.service';
55
import { PixelmatchService } from './libs/pixelmatch/pixelmatch.service';
6+
import { VlmService } from './libs/vlm/vlm.service';
7+
import { OllamaController } from './libs/vlm/ollama.controller';
8+
import { OllamaService } from './libs/vlm/ollama.service';
69
import { StaticModule } from '../static/static.module';
710

811
@Module({
9-
providers: [CompareService, PixelmatchService, LookSameService, OdiffService],
12+
controllers: [OllamaController],
13+
providers: [CompareService, PixelmatchService, LookSameService, OdiffService, VlmService, OllamaService],
1014
imports: [StaticModule],
1115
exports: [CompareService],
1216
})

src/compare/compare.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { PrismaService } from '../prisma/prisma.service';
77
import { DiffResult } from '../test-runs/diffResult';
88
import { LookSameService } from './libs/looks-same/looks-same.service';
99
import { OdiffService } from './libs/odiff/odiff.service';
10+
import { VlmService } from './libs/vlm/vlm.service';
1011
import { isHddStaticServiceConfigured } from '../static/utils';
1112

1213
@Injectable()
@@ -17,6 +18,7 @@ export class CompareService {
1718
private readonly pixelmatchService: PixelmatchService,
1819
private readonly lookSameService: LookSameService,
1920
private readonly odiffService: OdiffService,
21+
private readonly vlmService: VlmService,
2022
private readonly prismaService: PrismaService
2123
) {}
2224

@@ -44,6 +46,9 @@ export class CompareService {
4446

4547
return this.odiffService;
4648
}
49+
case ImageComparison.vlm: {
50+
return this.vlmService;
51+
}
4752
default: {
4853
this.logger.warn(`Unknown ImageComparison value: ${imageComparison}. Falling back to pixelmatch.`);
4954
return this.pixelmatchService;

src/compare/libs/image-comparator.interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { ImageCompareInput } from './ImageCompareInput';
33
import { LooksSameConfig } from './looks-same/looks-same.types';
44
import { OdiffConfig } from './odiff/odiff.types';
55
import { PixelmatchConfig } from './pixelmatch/pixelmatch.types';
6+
import { VlmConfig } from './vlm/vlm.types';
67

7-
export type ImageCompareConfig = PixelmatchConfig | LooksSameConfig | OdiffConfig;
8+
export type ImageCompareConfig = PixelmatchConfig | LooksSameConfig | OdiffConfig | VlmConfig;
89

910
export interface ImageComparator {
1011
getDiff(data: ImageCompareInput, config: ImageCompareConfig): Promise<DiffResult>;

src/compare/libs/vlm/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# VLM (Vision Language Model) Image Comparison
2+
3+
AI-powered semantic image comparison using Vision Language Models via Ollama.
4+
5+
## Quick Start
6+
7+
### 1. Install & Start Ollama
8+
9+
```bash
10+
# Install (macOS)
11+
brew install ollama
12+
13+
# Start Ollama
14+
ollama serve
15+
```
16+
17+
### 2. Download a Model
18+
19+
```bash
20+
# Recommended for accuracy
21+
ollama pull llava:7b
22+
23+
# Or for speed (smaller, less accurate)
24+
ollama pull moondream
25+
```
26+
27+
### 3. Configure Backend
28+
29+
Add to `.env`:
30+
```bash
31+
OLLAMA_BASE_URL=http://localhost:11434
32+
```
33+
34+
### 4. Use VLM in Project
35+
36+
Set project's image comparison to `vlm` with config:
37+
```json
38+
{
39+
"model": "llava:7b",
40+
"temperature": 0.1
41+
}
42+
```
43+
44+
Optional custom prompt:
45+
```json
46+
{
47+
"model": "llava:7b",
48+
"prompt": "Focus on button colors and text changes",
49+
"temperature": 0.1
50+
}
51+
```
52+
53+
## Recommended Models
54+
55+
| Model | Size | Speed | Accuracy | Best For |
56+
|-------|------|-------|----------|----------|
57+
| `llava:7b` | 4.7GB | ⚡⚡ | ⭐⭐⭐ | **Recommended** - best balance |
58+
| `llava:13b` | 8GB || ⭐⭐⭐⭐ | Best accuracy |
59+
| `moondream` | 1.7GB | ⚡⚡⚡ | ⭐⭐ | Fast, may hallucinate |
60+
| `minicpm-v` | 5.5GB | ⚡⚡ | ⭐⭐⭐ | Good alternative |
61+
62+
## Configuration
63+
64+
| Option | Type | Default | Description |
65+
|--------|------|---------|-------------|
66+
| `model` | string | `moondream` | Ollama vision model name |
67+
| `prompt` | string | `""` | Custom context prepended to system prompt |
68+
| `temperature` | number | `0.1` | Lower = more consistent results |
69+
70+
## How It Works
71+
72+
1. VLM analyzes both images semantically
73+
2. Returns `YES` (pass) or `NO` (fail) based on meaningful differences
74+
3. Ignores technical differences (anti-aliasing, sub-pixel, minor spacing)
75+
4. Provides description of differences found
76+
77+
## API Endpoints
78+
79+
```bash
80+
# List available models
81+
GET /ollama/models
82+
83+
# Compare two images (for testing)
84+
POST /ollama/compare?model=llava:7b&prompt=<prompt>&temperature=0.1
85+
```
86+
87+
**Example:**
88+
```bash
89+
curl -X POST "http://localhost:3000/ollama/compare?model=llava:7b&prompt=Are%20these%20images%20the%20same&temperature=0.1" \
90+
-F "images=@baseline.png" \
91+
-F "images=@comparison.png"
92+
```
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Controller, Get, Post, Query, HttpException, HttpStatus, UseInterceptors, UploadedFiles } from '@nestjs/common';
2+
import { FilesInterceptor } from '@nestjs/platform-express';
3+
import { ApiTags, ApiConsumes, ApiBody } from '@nestjs/swagger';
4+
import { OllamaService } from './ollama.service';
5+
6+
@ApiTags('Ollama')
7+
@Controller('ollama')
8+
export class OllamaController {
9+
constructor(private readonly ollamaService: OllamaService) {}
10+
11+
@Get('models')
12+
async listModels() {
13+
return { models: await this.ollamaService.listModels() };
14+
}
15+
16+
@Post('compare')
17+
@ApiConsumes('multipart/form-data')
18+
@ApiBody({
19+
schema: {
20+
type: 'object',
21+
required: ['images'],
22+
properties: {
23+
images: {
24+
type: 'array',
25+
items: { type: 'string', format: 'binary' },
26+
description: 'Two images to compare (baseline and comparison)',
27+
},
28+
},
29+
},
30+
})
31+
@UseInterceptors(FilesInterceptor('images', 2))
32+
async compareImages(
33+
@UploadedFiles() files: Express.Multer.File[],
34+
@Query('model') model: string,
35+
@Query('prompt') prompt: string,
36+
@Query('temperature') temperature: string
37+
) {
38+
if (files?.length !== 2) {
39+
throw new HttpException('Two images required', HttpStatus.BAD_REQUEST);
40+
}
41+
42+
return this.ollamaService.generate({
43+
model,
44+
prompt,
45+
format: 'json',
46+
images: files.map((f) => f.buffer.toString('base64')),
47+
options: { temperature: Number(temperature) },
48+
});
49+
}
50+
}

0 commit comments

Comments
 (0)