Vamos aprender juntos como integrar o gemini 2.0 na nossa aplicação angular e criar nossa própria pokéagenda baseado em LLM. Todos os links e passo a passo também se encontram neste GitHub NgPokedexLLM.

Criando App Inicial

Instalando Dependências do projeto

Utilizando no terminal o angular cli, vamos criar uma nova aplicação com o comando ng new ng-gemini, selecionar as opções (estilo: scss, ssr: não) e adicionar a lib do angular material com o comando ng add @angular/material, neste caso quando perguntado sobre o tema, utilizaremos a opção "custom":

npm i -g @angular/cli
ng version
ng new ng-pokedex-llm
// Which stylesheet format would you like to use? scss
// Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No

cd ng-gemini

Também vamos instalar outras dependências, incluindo a principal para este exercício, sobre o uso do gemini:

npm i @google/generative-ai@^0.16.0

Integrando Gemini com Angular

Apenas texto

Vamos começar integrando o Gemini para responder a perguntas sobre Pokémon usando apenas texto. Nesse caso como não demos muito contexto, ele pode dar uma resposta bem aberta.

src/app/app.component.ts

import { Component, signal } from '@angular/core';
import { GoogleGenerativeAI } from '@google/generative-ai';

const API_KEY = '<sua_api_key_aqui>';

@Component({
  selector: 'app-root',
  imports: [],
  template: `
    <h1>Integrando Angular com Gemini</h1>
    <h3>{{ prompt }}</h3>
    <p>{{ text() }}</p>
  `,
})
export class AppComponent {
  private genAI = new GoogleGenerativeAI(API_KEY);
  private model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
  
  prompt = 'Quem é esse pokemon?';
  text = signal('');

  constructor() {
    this.generateText();
  }

  async generateText() {   
    const result = await this.model.generateContent(this.prompt);
    this.text.set(result.response.text());
  }
}

Multimodal (texto + imagem)

Agora, vamos salvar a imagem de um Pokémon da nossa escolha na pasta /public com o nome de "pokemon.png" (deixei uma imagem de um Pokémon chamado "Golduck"), e solicitar ao gemini um conteúdo baseado em um array composto de um prompt (texto) e de uma imagem (convertida em texto/base64). Também vamos organizar a funcionalidade de chamar o Gemini e de converter imagem para base64 em dois novos serviços (GeminiService e ImageService):

src/app/media.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MediaService {
  async imageToBase64(imagePath: string): Promise<string> {
    try {
      const response = await fetch(imagePath);
      const blob = await response.blob();

      return new Promise<string>((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = () => {
          const imageDataUrl = reader.result as string;
          const base64Image = imageDataUrl.split(',')[1];
          resolve(base64Image);
        }
        reader.onerror = reject;
        reader.readAsDataURL(blob);
      });
    } catch (error) {
      console.error('Erro ao converter imagem para Base64:', error);
      return '';
    }
  }
}

src/app/gemini.service.ts

import { Injectable } from '@angular/core';
import { GoogleGenerativeAI, Part } from '@google/generative-ai';

const API_KEY = '<sua_api_key_aqui>';

@Injectable({
  providedIn: 'root'
})
export class GeminiService {
  private genAI = new GoogleGenerativeAI(API_KEY);
  private model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });

  async generateContent(request: string | Array<string | Part>): Promise<string> {
    const data = await this.model.generateContent(request);
    return data.response.text();
  }
}

src/public/pokemon.png Link da imagem

src/app/app.component.ts

import { Component, inject, signal } from '@angular/core';
import { MediaService } from './media.service';
import { GeminiService } from './gemini.service';

@Component({
  selector: 'app-root',
  imports: [],
  template: `
    <h1>Integrando Angular com Gemini</h1>
    <h3>{{ prompt }}</h3>
    <p>{{ text() }}</p>
  `,
})
export class AppComponent {
  private geminiService = inject(GeminiService);
  private mediaService = inject(MediaService);
  
  prompt = 'Quem é esse pokemon?';
  text = signal('');

  constructor() {
    this.generateText();
  }

  async generateText() {   
    const base64 = await this.mediaService.imageToBase64('/pokemon.png');
    const imagePart = { inlineData: { data: base64, mimeType: 'image/png', }};
    const result = await this.geminiService.generateContent([this.prompt, imagePart]);
    this.text.set(result);
  }
}

Criação do Layout principal

Vamos agora criar uma Pokédex que utiliza a câmera do dispositivo para identificar Pokémon em tempo real. A cada 500ms (5 segundos) iremos capturar um snapshot da camera, e enviar com um prompt solicitando o retorno em um formato específico. Caso o retorno seja positivo, que ele identifique um pokemon com uma precisão adequada, iremos abrir um alert com o nome do Pokémon.

src/app/media.service.ts

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MediaService {
  private video!: HTMLVideoElement;
  private canvas!: HTMLCanvasElement;
  private base64Image = signal<string>('');
  base64 = this.base64Image.asReadonly();

  startCamera(video: HTMLVideoElement, canvas: HTMLCanvasElement): void {
    // verificar se a câmera está disponível ('user' ou 'environment')
    navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } })
      .then(stream => {
        this.video = video;
        this.video.srcObject = stream;
        this.canvas = canvas;
        this.startCapture();
      })
      .catch(error => {
        console.error('Erro ao acessar a câmera: ', error);
      })
  }

  startCapture(): void {
    setInterval(() => {
      this.captureAndSend();
    }, 5000); // intervalo entre capturas em milisegundos
  }

  private captureAndSend(): void {
    // capturar a imagem da câmera utilizando o canvas
    const context = this.canvas.getContext('2d')!;
    const { width, height } = this.video.getBoundingClientRect();
    this.canvas.width = width;
    this.canvas.height = height;
    context.drawImage(this.video, 0, 0, width, height);
    const imageDataUrl = this.canvas.toDataURL('image/jpeg');

    // atualizar a imagem como base64 (e propagar para o componente)
    this.base64Image.set(imageDataUrl.split(',')[1]);
  }

  async imageToBase64(imagePath: string): Promise<string> {
    try {
      const response = await fetch(imagePath);
      const blob = await response.blob();

      return new Promise<string>((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = () => {
          const imageDataUrl = reader.result as string;
          const base64Image = imageDataUrl.split(',')[1];
          resolve(base64Image);
        }
        reader.onerror = reject;
        reader.readAsDataURL(blob);
      });
    } catch (error) {
      console.error('Erro ao converter imagem para Base64:', error);
      return '';
    }
  }
}

src/app/app.component.ts

import { Component, effect, ElementRef, inject, viewChild } from '@angular/core';
import { Part } from '@google/generative-ai';
import { MediaService } from './media.service';
import { GeminiService } from './gemini.service';

type PokemonData = {
  name: string;
  number: number;
  probability: number;
}

@Component({
  selector: 'app-root',
  imports: [],
  template: `
    <video #video autoplay playsinline></video>
    <canvas #canvas></canvas>
  `,
})
export class AppComponent {
  private geminiService = inject(GeminiService);
  private mediaService = inject(MediaService);

  videoElement = viewChild<ElementRef<HTMLVideoElement>>('video');
  canvasElement = viewChild<ElementRef<HTMLCanvasElement>>('canvas');
  base64 = this.mediaService.base64;
 
  viewChildRef = effect(() => {
    const video = this.videoElement()!.nativeElement;
    const canvas = this.canvasElement()!.nativeElement;
    this.mediaService.startCamera(video, canvas);
  });

  captureRef = effect(() => {
    this.detectPokemon(this.base64());
  })

  private async detectPokemon(base64: string) {
    if (!base64) return;
    const prompt = `
      Identifique o Pokémon na imagem, usando como base a Pokédex oficial.
      Retorne um JSON no seguinte formato:
      {"name": "Nome do Pokémon", "number": Numero do pokemon, "probability": 0.99}
      A resposta deve ser APENAS o JSON, sem texto adicional ou formatação.
      Se a imagem não contiver um Pokémon reconhecido, retorne {"nome": "Desconhecido", "numero": 0, "probabilidade": 0.0}.
      Priorize a precisão e evite suposições.
    `;
    const imagePart: Part = {
      inlineData: { 
        data: base64,
        mimeType: 'image/jpeg',
      }
    }
    const responseText = await this.geminiService.generateContent([prompt, imagePart]);
    
    // converter o texto string JSON em um objeto JavaScript
    const cleanedText = responseText.replace(/`json\s*|\s*`/g, '');
    const pokemonResponse = JSON.parse(cleanedText) as PokemonData;
    
    // mostrar nome do pokemon encontrado se probabilidade for suficiente
    if (pokemonResponse.number && pokemonResponse.probability > .9) {
      alert(pokemonResponse.name);
    }
  }
}

Conforme também explicado em vídeo, tudo bem deixarmos a constante "API_KEY" no serviço para testarmos localmente, mas antes de fazer o deploy no github pages, e para conseguir testar a aplicação no celular, modifique o serviço do gemini para solicitar a API_KEY através do localstorage, porque deste modo não precisamos subir um dado sensível ao versionar o código no github, ou constar a chave no código frontend em produção. Para criamos essa chave, precisamos acessar a página do AI Studio API keys. Esta chave para utilização dos modelos mencionados nos exemplos acima (gemini-2.0-flash) é gratuita com restrições de uso (RPM - requisições por minuto).

src/app/app.component.ts

import { Injectable } from '@angular/core';
import { GenerativeModel, GoogleGenerativeAI, Part } from '@google/generative-ai';

@Injectable({
  providedIn: 'root'
})
export class GeminiService {
  private genAI!: GoogleGenerativeAI;
  private model!: GenerativeModel;
  
  constructor() {
    let apiKey = localStorage.getItem('API_KEY') || '';
    while (!apiKey) {
      apiKey = prompt("Digite sua API_KEY") || '';
    }
    localStorage.setItem('API_KEY', apiKey);
    this.genAI = new GoogleGenerativeAI(apiKey);
    this.model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
  }

  async generateContent(request: string | Array<string | Part>): Promise<string> {
    const data = await this.model.generateContent(request);
    return data.response.text();
  }
}

Deixei também o link de demonstração no about do repositório para testarem! Também temos a página de documentação oficial sobre a biblioteca e seu uso para web no site Google AI for Developers. Espero que tenham gostado deste conteúdo! Também sigam e compartilhem a página Angularizando no LinkedIn!

Sugestões de assuntos? Envie aqui
Copyright © 2024 Angularizando