Building an AI-Powered Alt Text Generator with Angular, Firebase AI Logic, and Gemini 3

Published: (December 11, 2025 at 09:46 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Overview

In this project I upgraded from Gemini 2.5 to Gemini 3 (Pro Preview) in Vertex AI to build an intelligent Image Alt‑Text Generator. The app not only creates descriptive alt text, but also generates hashtags, recommendations, and a surprising or obscure fact using the built‑in Google Search tool. The reasoning (“thinking”) process is exposed so users can see how the model arrives at its answers.

Tech Stack

ComponentTechnology
Front‑endAngular v21
Back‑endFirebase AI Logic (Vertex AI)
ModelGemini 3.0 Pro Preview
Cloud servicesFirebase App Check, Firebase Remote Config
FeaturesJSON‑structured output, Thinking mode, Google Search grounding

Structured Output

The model returns a strictly formatted JSON object, which makes it straightforward to render the data in the UI.

Thinking Mode

Gemini 3.0 enables “thinking” by default. Token usage for thoughts, inputs, and outputs can be tracked to understand the cost of the model’s reasoning.

Tool Calling

The application uses the built‑in Google Search tool to retrieve grounding chunks, suggestions, and web‑search queries. These are included in the grounding metadata returned by the model.


Remote Config Setup

// src/app/firebase/remote-config.ts
import { initializeApp, FirebaseApp } from 'firebase/app';
import { getRemoteConfig, RemoteConfig } from 'firebase/remote-config';

function createRemoteConfig(firebaseApp: FirebaseApp): RemoteConfig {
  const remoteConfig = getRemoteConfig(firebaseApp);

  // Default values for Remote Config parameters.
  remoteConfig.defaultConfig = {
    geminiModelName: 'gemini-3-pro-preview',
    vertexAILocation: 'global',
    includeThoughts: true,
    thinkingBudget: 512,
  };

  return remoteConfig;
}

Configuration Parameters

ParameterDescriptionDefault Value
geminiModelNameGemini model identifiergemini-3-pro-preview
vertexAILocationVertex AI location (e.g., global)global
includeThoughtsWhether the model generates thinking processestrue
thinkingBudgetToken budget for the thinking phase512

If the app cannot fetch remote values, it falls back to these defaults.


Firebase Bootstrap

// src/app/firebase/bootstrap.ts
import { inject } from '@angular/core';
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { fetchAndActivate } from 'firebase/remote-config';
import { ConfigService } from './config.service';
import { firebaseConfig } from '../environments/environment';
import { createRemoteConfig } from './remote-config';

export async function bootstrapFirebase(): Promise {
  try {
    const configService = inject(ConfigService);
    const firebaseApp = initializeApp(firebaseConfig.app);

    // Protect the AI endpoint from abuse.
    initializeAppCheck(firebaseApp, {
      provider: new ReCaptchaEnterpriseProvider(firebaseConfig.recaptchaEnterpriseSiteKey),
      isTokenAutoRefreshEnabled: true,
    });

    const remoteConfig = createRemoteConfig(firebaseApp);
    await fetchAndActivate(remoteConfig);

    configService.loadConfig(firebaseApp, remoteConfig);
  } catch (err) {
    console.error('Remote Config fetch failed', err);
    throw err;
  }
}

Application Initialization

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAppInitializer } from '@angular/core';
import { AppComponent } from './app/app.component';
import { ApplicationConfig } from '@angular/core';
import { bootstrapFirebase } from './app/firebase/bootstrap';

const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => bootstrapFirebase()),
  ],
};

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

Image Analysis Schema

// src/app/models/image-analysis.schema.ts
import { Schema } from '@angular/fire/ai';

export const ImageAnalysisSchema = Schema.object({
  properties: {
    tags: Schema.array({ items: Schema.string() }),
    alternativeText: Schema.string(),
    recommendations: Schema.array({
      items: Schema.object({
        properties: {
          id: Schema.integer(),
          text: Schema.string(),
          reason: Schema.string(),
        },
      }),
    }),
    fact: Schema.string(),
  },
});

Generative AI Model Setup

// src/app/firebase/ai-model.ts
import { inject } from '@angular/core';
import { getAI, getGenerativeModel, VertexAIBackend } from '@angular/fire/ai';
import { RemoteConfig, getValue } from 'firebase/remote-config';
import { FirebaseApp } from 'firebase/app';
import { ImageAnalysisSchema } from '../models/image-analysis.schema';
import { AI_MODEL } from '../tokens';

function getGenerativeAIModel(firebaseApp: FirebaseApp, remoteConfig: RemoteConfig) {
  const model = getValue(remoteConfig, 'geminiModelName').asString();
  const vertexAILocation = getValue(remoteConfig, 'vertexAILocation').asString();
  const includeThoughts = getValue(remoteConfig, 'includeThoughts').asBoolean();
  const thinkingBudget = getValue(remoteConfig, 'thinkingBudget').asNumber();

  const ai = getAI(firebaseApp, { backend: new VertexAIBackend(vertexAILocation) });

  return getGenerativeModel(ai, {
    model,
    generationConfig: {
      responseMimeType: 'application/json',
      responseSchema: ImageAnalysisSchema,
      thinkingConfig: {
        includeThoughts,
        thinkingBudget,
      },
    },
    tools: [{ googleSearch: {} }],
  });
}

Provider for Dependency Injection

// src/app/firebase/ai-provider.ts
import { makeEnvironmentProviders, inject } from '@angular/core';
import { ConfigService } from './config.service';
import { AI_MODEL } from '../tokens';
import { getGenerativeAIModel } from './ai-model';

export function provideFirebase() {
  return makeEnvironmentProviders([
    {
      provide: AI_MODEL,
      useFactory: () => {
        const configService = inject(ConfigService);

        if (!configService.remoteConfig) {
          throw new Error('Remote config does not exist.');
        }
        if (!configService.firebaseApp) {
          throw new Error('Firebase App does not exist');
        }

        return getGenerativeAIModel(configService.firebaseApp, configService.remoteConfig);
      },
    },
  ]);
}

Add the provider to the application configuration:

// src/main.ts (excerpt)
import { provideFirebase } from './app/firebase/ai-provider';

const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => bootstrapFirebase()),
    provideFirebase(),
  ],
};

Angular Service for Alt‑Text Generation

// src/app/services/firebase.service.ts
import { Injectable, inject } from '@angular/core';
import { AI_MODEL } from '../tokens';
import { fileToGenerativePart } from '@angular/fire/ai';
import { ImageAnalysisResponse } from '../models/image-analysis.model';

@Injectable({ providedIn: 'root' })
export class FirebaseService {
  private aiModel = inject(AI_MODEL);

  async generateAltText(image: File): Promise {
    const imagePart = await fileToGenerativePart(image);

    const prompt = `
You are asked to perform four tasks:
1. Generate an alternative text for the image (max 125 characters).
2. Generate at least 3 tags describing the image.
3. Based on the alternative text and tags, suggest ways to make the image more interesting.
4. Search for a surprising or obscure fact that interconnects the tags. If no direct link exists, find a conceptual link.
`;

    const result = await this.aiModel.generateContent({
      contents: [{ role: 'user', parts: [{ text: prompt }, imagePart] }],
    });

    if (result?.response) {
      const response = result.response;
      const thought = response.thoughtSummary?.() ?? '';
      const rawText = response.text?.() ?? '';

      // Strip possible markdown fences.
      const cleaned = rawText.replace(/```json\s*|```/g, '').trim();

      const parsed = JSON.parse(cleaned) as ImageAnalysisResponse;
      const tokenUsage = this.getTokenUsage(response.usageMetadata);

      return {
        ...parsed,
        thought,
        tokenUsage,
      };
    }

    throw new Error('No response from Gemini model');
  }

  private getTokenUsage(usage: any) {
    // Implementation omitted for brevity – extracts input/output token counts.
    return usage;
  }
}

The service:

  1. Converts the uploaded image to a format accepted by Vertex AI.
  2. Sends a multi‑step prompt that instructs Gemini 3 to produce alt text, tags, recommendations, and a fact.
  3. Parses the JSON response, extracts the “thought” summary, and returns token‑usage metadata.
Back to Blog

Related posts

Read more »