Demo: Gerador de Alt Text com IA Multimodal no Navegador

avançado

Gere textos alternativos para imagens automaticamente usando input multimodal da Prompt API. Melhore a acessibilidade do seu site com IA on-device no Chrome.

Verificando...

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

  1. Página carrega → verifica suporte multimodal (expectedInputs: [{ type: "image" }])
  2. Usuário arrasta imagem ou clica na zona de upload
  3. Preview aparece → botão “Gerar” habilita
  4. (Opcional) Usuário adiciona contexto no campo de texto
  5. Clica “Gerar” → botão mostra “Analisando imagem…”
  6. Alt text aparece em blockquote com opções de copiar e regenerar
  7. Copiar → texto vai para clipboard
  8. Regenerar → novo alt text sem reupload

Edge Cases e Tratamento de Erros

CenárioTratamento
Arquivo não é imagemaccept no input limita; drop valida file.type
Imagem > 5MBRejeita com aviso antes de processar
Modelo sem suporte multimodalavailability() com expectedInputs: [{ type: "image" }] detecta
Alt text começa com “imagem de”System prompt instrui contra; resultado exibido como está
Imagem muito pequena/vaziaModelo pode retornar descrição genérica
URL.createObjectURL leakApenas um preview por vez (antigo GC’d)
Rede offlineFunciona — 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 Blob do arquivo pode ser passado diretamente (File herda de Blob).
  • O availability() deve receber expectedInputs: [{ type: "image" }] para verificar se o dispositivo suporta input visual.
  • Imagens muito grandes podem ser redimensionadas no canvas antes de enviar para reduzir processamento.