Angular, Firebase AI Logic, 및 Gemini 3를 사용한 AI 기반 Alt 텍스트 생성기 구축
Source: Dev.to
Overview
이 프로젝트에서는 Gemini 2.5에서 Gemini 3 (Pro Preview)로 업그레이드하여 지능형 이미지 Alt‑Text 생성기를 만들었습니다. 이 앱은 설명적인 대체 텍스트를 생성할 뿐만 아니라 해시태그, 권장 사항, 그리고 내장된 Google Search 도구를 활용한 놀라운 혹은 희귀한 사실까지 제공합니다. 모델이 답을 도출하는 과정을 사용자가 확인할 수 있도록 추론(“thinking”) 과정이 공개됩니다.
Tech Stack
| Component | Technology |
|---|---|
| Front‑end | Angular v21 |
| Back‑end | Firebase AI Logic (Vertex AI) |
| Model | Gemini 3.0 Pro Preview |
| Cloud services | Firebase App Check, Firebase Remote Config |
| Features | JSON‑structured output, Thinking mode, Google Search grounding |
Structured Output
모델은 엄격히 형식화된 JSON 객체를 반환하므로 UI에서 데이터를 렌더링하기가 매우 간단합니다.
Thinking Mode
Gemini 3.0은 기본적으로 “thinking”을 지원합니다. 생각, 입력, 출력에 사용된 토큰을 추적하여 모델 추론 비용을 이해할 수 있습니다.
Tool Calling
앱은 내장된 Google Search 도구를 사용해 근거 조각, 제안 및 웹 검색 쿼리를 가져옵니다. 이러한 내용은 모델이 반환하는 근거 메타데이터에 포함됩니다.
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
| Parameter | Description | Default Value |
|---|---|---|
geminiModelName | Gemini 모델 식별자 | gemini-3-pro-preview |
vertexAILocation | Vertex AI 위치 (예: global) | global |
includeThoughts | 모델이 추론 과정을 생성할지 여부 | true |
thinkingBudget | 추론 단계에 할당된 토큰 예산 | 512 |
앱이 원격 값을 가져오지 못하면 이 기본값을 사용합니다.
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);
},
},
]);
}
앱 설정에 프로바이더를 추가합니다:
// 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;
}
}
서비스는 다음을 수행합니다:
- 업로드된 이미지를 Vertex AI가 받아들일 수 있는 형식으로 변환합니다.
- Gemini 3에게 alt 텍스트, 태그, 권장 사항, 사실을 생성하도록 지시하는 다단계 프롬프트를 전송합니다.
- JSON 응답을 파싱하고 “thought” 요약을 추출한 뒤 토큰 사용 메타데이터와 함께 반환합니다.