Demo: Gerador de Alt Text com IA Multimodal no Navegador
avançadoGere textos alternativos para imagens automaticamente usando input multimodal da Prompt API. Melhore a acessibilidade do seu site com IA on-device no Chrome.
Visão Geral
Demo que gera textos alternativos (alt text) pra imagens usando input multimodal da Prompt API. O usuário faz upload via drag & drop ou file picker, e o modelo descreve a imagem pra uso em acessibilidade. É o tipo de coisa que todo dev web deveria ter na manga — e agora dá pra fazer sem pagar API nenhuma.
Pra quem: Desenvolvedores web interessados em acessibilidade e input multimodal.
Técnica principal: Input multimodal { type: 'image', value: Blob } com expectedInputs configurado pra imagem.
Wireframe
┌─────────────────────────────────────────────────────────┐
│ 🖼️ Gerador de Alt Text │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ 📁 📷 │ Arraste uma imagem aqui │ │
│ │ │ │ ou clique para selecionar │ │
│ │ └──────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Contexto (opcional): │
│ ┌────────────────────────────────────────────────┐ │
│ │ ex: "foto de produto para e-commerce" │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ [ 🏷️ Gerar Alt Text ] │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 🏷️ Alt Text Gerado: │ │
│ │ │ │
│ │ "Pessoa sorrindo segurando uma caneca de │ │
│ │ cerâmica artesanal azul em uma oficina │ │
│ │ de olaria iluminada pela luz natural" │ │
│ │ [📋 Copiar] │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="gerador-alt-text">
<h2>🖼️ Gerador de Alt Text</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<div id="drop-zone" class="drop-zone">
<div id="drop-placeholder" class="drop-placeholder">
<span class="drop-icon">📁</span>
<p>Arraste uma imagem aqui ou clique para selecionar</p>
<p class="drop-hint">PNG, JPG, WebP — máx. 5MB</p>
</div>
<img id="preview-image" class="preview" hidden />
<input type="file" id="file-input" accept="image/png,image/jpeg,image/webp" hidden />
</div>
<label for="context-input">Contexto (opcional):</label>
<input
id="context-input"
type="text"
placeholder="ex: foto de produto para e-commerce, imagem de blog técnico..."
/>
<div class="actions">
<button id="btn-generate" disabled>🏷️ Gerar Alt Text</button>
</div>
<div id="result" class="result-box" hidden>
<h3>🏷️ Alt Text Gerado:</h3>
<blockquote id="alt-output"></blockquote>
<div class="result-actions">
<button id="btn-copy">📋 Copiar</button>
<button id="btn-regenerate">🔄 Gerar Outra Versão</button>
</div>
</div>
</section>
Código JavaScript
class AltTextGenerator {
constructor() {
this.session = null;
this.imageBlob = null;
this.dropZone = document.getElementById("drop-zone");
this.fileInput = document.getElementById("file-input");
this.previewImg = document.getElementById("preview-image");
this.placeholder = document.getElementById("drop-placeholder");
this.contextInput = document.getElementById("context-input");
this.btnGenerate = document.getElementById("btn-generate");
this.btnCopy = document.getElementById("btn-copy");
this.btnRegenerate = document.getElementById("btn-regenerate");
this.resultEl = document.getElementById("result");
this.outputEl = document.getElementById("alt-output");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
this.init();
}
async init() {
if (!("LanguageModel" in window)) {
this.showStatus("❌ Prompt API não disponível. Use Chrome 148+.", "error");
return;
}
const availability = await LanguageModel.availability({
expectedInputs: [{ type: "image" }]
});
if (availability === "unavailable") {
this.showStatus("❌ Modelo com suporte a imagem não disponível.", "error");
return;
}
try {
this.session = await LanguageModel.create({
expectedInputs: [
{ type: "text", languages: ["pt", "en"] },
{ type: "image" }
],
expectedOutputs: [{ type: "text", languages: ["pt"] }],
initialPrompts: [{
role: "system",
content: `Você é um especialista em acessibilidade web. Gere textos alternativos (alt text) concisos e descritivos para imagens. O alt text deve:
- Ter entre 10 e 150 caracteres
- Descrever o conteúdo visual objetivamente
- Não começar com "imagem de" ou "foto de"
- Ser útil para leitores de tela
- Estar em português brasileiro`
}]
});
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro: ${err.message}`, "error");
return;
}
this.setupDropZone();
this.btnGenerate.addEventListener("click", () => this.generate());
this.btnCopy.addEventListener("click", () => this.copy());
this.btnRegenerate.addEventListener("click", () => this.generate());
}
setupDropZone() {
// Click to upload
this.dropZone.addEventListener("click", () => this.fileInput.click());
this.fileInput.addEventListener("change", (e) => {
if (e.target.files[0]) this.handleFile(e.target.files[0]);
});
// Drag & drop
this.dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
this.dropZone.classList.add("drag-over");
});
this.dropZone.addEventListener("dragleave", () => {
this.dropZone.classList.remove("drag-over");
});
this.dropZone.addEventListener("drop", (e) => {
e.preventDefault();
this.dropZone.classList.remove("drag-over");
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) {
this.handleFile(file);
}
});
}
handleFile(file) {
if (file.size > 5 * 1024 * 1024) {
this.showStatus("⚠️ Imagem muito grande (máx. 5MB).", "error");
return;
}
this.imageBlob = file;
// Preview
const url = URL.createObjectURL(file);
this.previewImg.src = url;
this.previewImg.hidden = false;
this.placeholder.hidden = true;
this.btnGenerate.disabled = false;
this.resultEl.hidden = true;
}
async generate() {
if (!this.imageBlob) return;
this.btnGenerate.disabled = true;
this.btnGenerate.textContent = "⏳ Analisando imagem...";
const context = this.contextInput.value.trim();
const textPrompt = context
? `Gere um alt text para esta imagem. Contexto: ${context}`
: `Gere um alt text descritivo para esta imagem.`;
try {
const result = await this.session.prompt([{
role: "user",
content: [
{ type: "text", value: textPrompt },
{ type: "image", value: this.imageBlob }
]
}]);
this.outputEl.textContent = result.replace(/^["']|["']$/g, "");
this.resultEl.hidden = false;
} catch (err) {
this.showStatus(`❌ Erro: ${err.message}`, "error");
} finally {
this.btnGenerate.disabled = false;
this.btnGenerate.textContent = "🏷️ Gerar Alt Text";
}
}
copy() {
navigator.clipboard.writeText(this.outputEl.textContent).then(() => {
this.btnCopy.textContent = "✅ Copiado!";
setTimeout(() => { this.btnCopy.textContent = "📋 Copiar"; }, 2000);
});
}
showStatus(msg, type) {
this.statusBar.hidden = false;
this.statusBar.className = `status status-${type}`;
this.statusMessage.textContent = msg;
}
hideStatus() {
this.statusBar.hidden = true;
}
}
document.addEventListener("DOMContentLoaded", () => new AltTextGenerator());
Fluxo UX
- Página carrega → verifica suporte multimodal (
expectedInputs: [{ type: "image" }]) - Usuário arrasta imagem ou clica na zona de upload
- Preview aparece → botão “Gerar” habilita
- (Opcional) Usuário adiciona contexto no campo de texto
- Clica “Gerar” → botão mostra “Analisando imagem…”
- Alt text aparece em blockquote com opções de copiar e regenerar
- Copiar → texto vai para clipboard
- Regenerar → novo alt text sem reupload
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Arquivo não é imagem | accept no input limita; drop valida file.type |
| Imagem > 5MB | Rejeita com aviso antes de processar |
| Modelo sem suporte multimodal | availability() com expectedInputs: [{ type: "image" }] detecta |
| Alt text começa com “imagem de” | System prompt instrui contra; resultado exibido como está |
| Imagem muito pequena/vazia | Modelo pode retornar descrição genérica |
| URL.createObjectURL leak | Apenas um preview por vez (antigo GC’d) |
| Rede offline | Funciona — modelo é local |
CSS Essencial
.drop-zone {
border: 2px dashed #d1d5db;
border-radius: 0.75rem;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.drop-zone.drag-over { border-color: #2563eb; background: #eff6ff; }
.preview {
max-width: 100%;
max-height: 300px;
border-radius: 0.5rem;
object-fit: contain;
}
.drop-icon { font-size: 3rem; }
.drop-hint { color: #9ca3af; font-size: 0.875rem; }
blockquote {
border-left: 4px solid #2563eb;
padding-left: 1rem;
font-style: italic;
font-size: 1.1rem;
}
Notas de Implementação
- O
prompt()aceita array de content parts quando multimodal —[{ type: "text", value: "..." }, { type: "image", value: Blob }]. - O
Blobdo arquivo pode ser passado diretamente (File herda de Blob). - O
availability()deve receberexpectedInputs: [{ type: "image" }]para verificar se o dispositivo suporta input visual. - Imagens muito grandes podem ser redimensionadas no canvas antes de enviar para reduzir processamento.