Multi-modal AI cho phép model xử lý cả văn bản lẫn hình ảnh — nhận diện sản phẩm từ ảnh, phân tích hóa đơn, đọc tài liệu scan, mô tả ảnh. GPT-4o và Gemini đều hỗ trợ mạnh.

1. OpenAI Vision — Phân tích ảnh

// src/ai/vision.service.ts
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';
import * as fs from 'fs';

@Injectable()
export class VisionService {
  private openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  // Phân tích ảnh từ URL
  async analyzeImageUrl(imageUrl: string, prompt: string): Promise<string> {
    const response = await this.openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        {
          role: 'user',
          content: [
            {
              type: 'image_url',
              image_url: { url: imageUrl, detail: 'high' }, // 'low' | 'high' | 'auto'
            },
            { type: 'text', text: prompt },
          ],
        },
      ],
      max_tokens: 1000,
    });

    return response.choices[0].message.content ?? '';
  }

  // Phân tích ảnh từ file (base64)
  async analyzeImageFile(filePath: string, prompt: string): Promise<string> {
    const imageBuffer = fs.readFileSync(filePath);
    const base64Image = imageBuffer.toString('base64');
    const mimeType = this.getMimeType(filePath);

    const response = await this.openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        {
          role: 'user',
          content: [
            {
              type: 'image_url',
              image_url: {
                url: `data:${mimeType};base64,${base64Image}`,
              },
            },
            { type: 'text', text: prompt },
          ],
        },
      ],
    });

    return response.choices[0].message.content ?? '';
  }

  // Phân tích buffer (từ upload)
  async analyzeImageBuffer(buffer: Buffer, mimeType: string, prompt: string): Promise<string> {
    const base64 = buffer.toString('base64');

    const response = await this.openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [{
        role: 'user',
        content: [
          {
            type: 'image_url',
            image_url: { url: `data:${mimeType};base64,${base64}` },
          },
          { type: 'text', text: prompt },
        ],
      }],
    });

    return response.choices[0].message.content ?? '';
  }

  // So sánh nhiều ảnh
  async compareImages(imageUrls: string[], prompt: string): Promise<string> {
    const imageContent = imageUrls.map(url => ({
      type: 'image_url' as const,
      image_url: { url },
    }));

    const response = await this.openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [{
        role: 'user',
        content: [
          ...imageContent,
          { type: 'text', text: prompt },
        ],
      }],
    });

    return response.choices[0].message.content ?? '';
  }

  private getMimeType(filePath: string): string {
    const ext = filePath.split('.').pop()?.toLowerCase();
    const map: Record<string, string> = {
      jpg: 'image/jpeg', jpeg: 'image/jpeg',
      png: 'image/png', gif: 'image/gif',
      webp: 'image/webp',
    };
    return map[ext ?? ''] ?? 'image/jpeg';
  }
}

2. Use Cases thực tế

// src/ai/product-vision.service.ts
@Injectable()
export class ProductVisionService {
  constructor(private visionService: VisionService) {}

  // Nhận diện sản phẩm từ ảnh
  async identifyProduct(imageUrl: string): Promise<{
    name: string;
    category: string;
    attributes: Record<string, string>;
    suggestedPrice?: string;
  }> {
    const result = await this.visionService.analyzeImageUrl(imageUrl, `
      Phân tích hình ảnh sản phẩm này. Trả về JSON:
      {
        "name": "Tên sản phẩm",
        "category": "Danh mục",
        "brand": "Thương hiệu (nếu nhận ra)",
        "color": "Màu sắc",
        "condition": "Mới/Cũ/Like new",
        "attributes": { "key": "value" },
        "suggestedTags": ["tag1", "tag2"]
      }
      Chỉ trả về JSON, không giải thích thêm.
    `);

    return JSON.parse(result);
  }

  // Đọc hóa đơn, receipt
  async extractReceipt(imageBuffer: Buffer): Promise<{
    merchant: string;
    date: string;
    items: Array<{ name: string; quantity: number; price: number }>;
    total: number;
    tax?: number;
  }> {
    const result = await this.visionService.analyzeImageBuffer(
      imageBuffer,
      'image/jpeg',
      `Đọc hóa đơn trong ảnh và trả về JSON:
      {
        "merchant": "Tên cửa hàng",
        "date": "YYYY-MM-DD",
        "items": [{"name": "...", "quantity": 1, "price": 0}],
        "total": 0,
        "tax": 0
      }
      Chỉ trả về JSON.`
    );

    return JSON.parse(result);
  }

  // Kiểm tra chất lượng ảnh sản phẩm (cho seller upload)
  async checkProductImageQuality(imageUrl: string): Promise<{
    score: number;       // 1-10
    issues: string[];
    suggestions: string[];
    approved: boolean;
  }> {
    const result = await this.visionService.analyzeImageUrl(imageUrl, `
      Đánh giá chất lượng ảnh sản phẩm này cho e-commerce. Trả về JSON:
      {
        "score": 8,
        "issues": ["Nền không trắng", "Hơi tối"],
        "suggestions": ["Dùng nền trắng", "Tăng độ sáng"],
        "approved": true
      }
      Tiêu chí: nền sạch, đủ sáng, sản phẩm rõ ràng, không blur, không watermark.
      Approved khi score >= 6.
    `);

    return JSON.parse(result);
  }

  // Mô tả ảnh tự động (accessibility)
  async generateAltText(imageUrl: string): Promise<string> {
    return this.visionService.analyzeImageUrl(imageUrl,
      'Viết mô tả ngắn gọn (1-2 câu) cho ảnh này để làm alt text, bằng tiếng Việt.'
    );
  }
}

3. Gemini Vision

npm install @google/generative-ai
import { GoogleGenerativeAI } from '@google/generative-ai';
import * as fs from 'fs';

@Injectable()
export class GeminiVisionService {
  private genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
  private model = this.genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });

  // Phân tích ảnh từ URL (Gemini cần download trước)
  async analyzeFromBuffer(buffer: Buffer, mimeType: string, prompt: string): Promise<string> {
    const result = await this.model.generateContent([
      { inlineData: { data: buffer.toString('base64'), mimeType } },
      prompt,
    ]);

    return result.response.text();
  }

  // Phân tích file local
  async analyzeLocalFile(filePath: string, prompt: string): Promise<string> {
    const buffer = fs.readFileSync(filePath);
    const mimeType = 'image/jpeg';
    return this.analyzeFromBuffer(buffer, mimeType, prompt);
  }

  // Video analysis (Gemini hỗ trợ video)
  async analyzeVideo(videoBuffer: Buffer, prompt: string): Promise<string> {
    const result = await this.model.generateContent([
      { inlineData: { data: videoBuffer.toString('base64'), mimeType: 'video/mp4' } },
      prompt,
    ]);
    return result.response.text();
  }
}

4. Controller — Upload và phân tích ảnh

// src/products/products.controller.ts
@Controller('products')
export class ProductsController {
  constructor(private productVisionService: ProductVisionService) {}

  @Post('analyze-image')
  @UseInterceptors(FileInterceptor('image'))
  async analyzeProductImage(
    @UploadedFile() file: Express.Multer.File,
  ) {
    // 1. Upload lên S3
    const imageUrl = await this.s3Service.upload(file);

    // 2. Phân tích với AI
    const [productInfo, qualityCheck] = await Promise.all([
      this.productVisionService.identifyProduct(imageUrl),
      this.productVisionService.checkProductImageQuality(imageUrl),
    ]);

    return {
      imageUrl,
      productInfo,
      qualityCheck,
    };
  }

  @Post('extract-receipt')
  @UseInterceptors(FileInterceptor('receipt'))
  async extractReceipt(@UploadedFile() file: Express.Multer.File) {
    return this.productVisionService.extractReceipt(file.buffer);
  }
}

5. NextJS — Camera/Upload UI

'use client';
import { useState, useRef } from 'react';

export function ImageAnalyzer() {
  const [result, setResult] = useState<any>(null);
  const [loading, setLoading] = useState(false);
  const fileRef = useRef<HTMLInputElement>(null);

  async function analyzeImage(file: File) {
    setLoading(true);
    const formData = new FormData();
    formData.append('image', file);

    const res = await fetch('/api/products/analyze-image', {
      method: 'POST',
      body: formData,
    });
    const data = await res.json();
    setResult(data);
    setLoading(false);
  }

  return (
    <div>
      <input
        ref={fileRef}
        type="file"
        accept="image/*"
        capture="environment"  // Mobile camera
        onChange={e => e.target.files?.[0] && analyzeImage(e.target.files[0])}
        className="hidden"
      />
      <button onClick={() => fileRef.current?.click()}>
        📷 Chụp ảnh sản phẩm
      </button>

      {loading && <p>Đang phân tích ảnh...</p>}

      {result && (
        <div>
          <h3>{result.productInfo.name}</h3>
          <p>Danh mục: {result.productInfo.category}</p>
          <p>Chất lượng ảnh: {result.qualityCheck.score}/10</p>
          {!result.qualityCheck.approved && (
            <ul className="text-red-500">
              {result.qualityCheck.issues.map((i: string) => <li key={i}>{i}</li>)}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}

6. Kết luận

  • GPT-4o: Tốt nhất cho hiểu nội dung phức tạp, OCR, phân tích chi tiết
  • Gemini Flash: Nhanh và rẻ hơn, hỗ trợ video
  • Base64 vs URL: URL tiện lợi hơn nhưng phải publicly accessible; base64 cho private files
  • detail: 'high': Xử lý ảnh độ phân giải cao hơn (tốn token hơn)
  • JSON output: Luôn yêu cầu model trả về JSON để parse dễ — kết hợp với response_format: { type: 'json_object' }

Multi-modal mở ra nhiều use case mới: tự động tag sản phẩm, kiểm duyệt ảnh, đọc tài liệu scan — giảm manual work đáng kể.