Demo: Transcrição de Áudio com IA Local (Prompt API)

avançado

Transcreva arquivos de áudio MP3/WAV direto no navegador usando Gemini Nano multimodal. Upload ou gravação ao vivo com a Prompt API — sem servidor, sem custo.

Verificando...

Visão Geral

Demo que transcreve áudio para texto usando a Prompt API com input multimodal. O usuário faz upload de um arquivo (MP3/WAV) ou grava direto pelo microfone — o Gemini Nano processa o áudio localmente e retorna a transcrição. Zero backend, zero custo por request.

Pra quem: Desenvolvedores explorando a capacidade multimodal da Prompt API — input de áudio direto no modelo on-device.

Técnica principal: LanguageModel.create() com input multimodal, passando audio blob como { type: "audio" } no prompt. Requer a flag chrome://flags/#prompt-api-for-gemini-nano-multimodal-input habilitada.


Wireframe

┌─────────────────────────────────────────────────────┐
│  🎤 Transcrição de Áudio                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  [📁 Upload Arquivo]   [🎙️ Gravar Áudio]     │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  ▁▃▅▇▅▃▁▃▅▇█▇▅▃▁  ← waveform do áudio      │  │
│  │  arquivo.mp3 — 00:32 — 245 KB                 │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  [ ✨ Transcrever ]                                  │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  "Olá, este é um teste de transcrição de      │  │
│  │  áudio usando inteligência artificial local    │  │
│  │  no navegador..."                             │  │
│  │                                    [📋 Copiar]│  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ⚠️ Requer Chrome 138+ com flag multimodal ativa    │
└─────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="transcricao-audio">
  <h2>🎤 Transcrição de Áudio</h2>

  <div id="status-bar" class="status" hidden>
    <span id="status-message"></span>
  </div>

  <div class="input-controls">
    <label class="upload-btn" for="audio-file">
      📁 Upload Arquivo
      <input type="file" id="audio-file" accept="audio/mp3,audio/wav,audio/webm,.mp3,.wav,.webm" hidden>
    </label>
    <button id="btn-record" class="record-btn">🎙️ Gravar Áudio</button>
  </div>

  <div id="audio-preview" class="audio-preview" hidden>
    <canvas id="waveform" width="500" height="60"></canvas>
    <p id="audio-info"></p>
    <audio id="audio-player" controls></audio>
  </div>

  <div class="actions">
    <button id="btn-transcribe" disabled>✨ Transcrever</button>
  </div>

  <div id="result" class="result-box" hidden>
    <p id="transcription-text"></p>
    <button id="btn-copy" class="copy-btn">📋 Copiar</button>
  </div>
</section>

Código JavaScript

class AudioTranscriber {
  constructor() {
    this.session = null;
    this.audioBlob = null;
    this.mediaRecorder = null;
    this.isRecording = false;

    this.fileInput = document.getElementById("audio-file");
    this.btnRecord = document.getElementById("btn-record");
    this.btnTranscribe = document.getElementById("btn-transcribe");
    this.btnCopy = document.getElementById("btn-copy");
    this.audioPreview = document.getElementById("audio-preview");
    this.audioPlayer = document.getElementById("audio-player");
    this.audioInfo = document.getElementById("audio-info");
    this.resultEl = document.getElementById("result");
    this.transcriptionText = document.getElementById("transcription-text");
    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 138+ com flags habilitadas.", "error");
      return;
    }

    const availability = await LanguageModel.availability();

    if (availability === "unavailable") {
      this.showStatus("❌ Modelo indisponível. Verifique chrome://flags/#prompt-api-for-gemini-nano-multimodal-input", "error");
      return;
    }

    if (availability === "downloading") {
      this.showStatus("⏳ Baixando modelo multimodal... Aguarde.", "loading");
    }

    try {
      this.session = await LanguageModel.create({
        expectedInputs: [{ type: "audio" }],
        initialPrompts: [{
          role: "system",
          content: "Você é um transcritor de áudio. Transcreva fielmente o conteúdo falado no áudio fornecido. Retorne apenas o texto transcrito, sem comentários adicionais."
        }],
        monitor(m) {
          m.addEventListener("downloadprogress", (e) => {
            document.getElementById("status-message").textContent =
              `⏳ Baixando modelo... ${Math.round(e.loaded * 100)}%`;
          });
        }
      });

      this.hideStatus();
    } catch (err) {
      this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
      return;
    }

    // Event listeners
    this.fileInput.addEventListener("change", (e) => this.handleFileUpload(e));
    this.btnRecord.addEventListener("click", () => this.toggleRecording());
    this.btnTranscribe.addEventListener("click", () => this.transcribe());
    this.btnCopy.addEventListener("click", () => this.copyResult());
  }

  handleFileUpload(e) {
    const file = e.target.files[0];
    if (!file) return;

    // Validar tamanho (máx 10MB)
    if (file.size > 10 * 1024 * 1024) {
      this.showStatus("❌ Arquivo muito grande. Máximo: 10 MB.", "error");
      return;
    }

    this.audioBlob = file;
    this.showAudioPreview(file, file.name);
  }

  async toggleRecording() {
    if (this.isRecording) {
      this.stopRecording();
      return;
    }

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const chunks = [];

      this.mediaRecorder = new MediaRecorder(stream, {
        mimeType: MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4"
      });

      this.mediaRecorder.ondataavailable = (e) => chunks.push(e.data);

      this.mediaRecorder.onstop = () => {
        stream.getTracks().forEach((t) => t.stop());
        this.audioBlob = new Blob(chunks, { type: this.mediaRecorder.mimeType });
        this.showAudioPreview(this.audioBlob, "Gravação");
      };

      this.mediaRecorder.start();
      this.isRecording = true;
      this.btnRecord.textContent = "⏹️ Parar Gravação";
      this.btnRecord.classList.add("recording");
    } catch (err) {
      this.showStatus("❌ Sem acesso ao microfone.", "error");
    }
  }

  stopRecording() {
    this.mediaRecorder.stop();
    this.isRecording = false;
    this.btnRecord.textContent = "🎙️ Gravar Áudio";
    this.btnRecord.classList.remove("recording");
  }

  showAudioPreview(blob, name) {
    const url = URL.createObjectURL(blob);
    this.audioPlayer.src = url;
    const sizeMB = (blob.size / 1024 / 1024).toFixed(2);
    this.audioInfo.textContent = `${name} — ${sizeMB} MB`;
    this.audioPreview.hidden = false;
    this.btnTranscribe.disabled = false;
  }

  async transcribe() {
    if (!this.audioBlob || !this.session) return;

    this.btnTranscribe.disabled = true;
    this.btnTranscribe.textContent = "⏳ Transcrevendo...";
    this.resultEl.hidden = true;

    try {
      const result = await this.session.prompt([
        {
          role: "user",
          content: [
            { type: "audio", data: this.audioBlob },
            { type: "text", data: "Transcreva o áudio acima em português." }
          ]
        }
      ]);

      this.transcriptionText.textContent = result;
      this.resultEl.hidden = false;
    } catch (err) {
      if (err.name === "AbortError") return;
      this.showStatus(`❌ Erro na transcrição: ${err.message}`, "error");
    } finally {
      this.btnTranscribe.disabled = false;
      this.btnTranscribe.textContent = "✨ Transcrever";
    }
  }

  async copyResult() {
    await navigator.clipboard.writeText(this.transcriptionText.textContent);
    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 AudioTranscriber());

Fluxo UX

  1. Página carrega → verifica suporte da Prompt API + flag multimodal
  2. Modelo pronto → controles de upload e gravação habilitados
  3. Upload de arquivo → valida tipo/tamanho, mostra preview com player de áudio
  4. Gravação ao vivo → botão muda para “Parar”, indicador visual de gravação ativa
  5. Clica “Transcrever” → botão desabilita, feedback de processamento
  6. Resultado chega → texto transcrito aparece com botão de copiar
  7. Erro → mensagem contextual no status bar

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API ausenteMensagem + link para requisitos do Chrome
Flag multimodal desativadaOrienta ativar chrome://flags/#prompt-api-for-gemini-nano-multimodal-input
Arquivo > 10 MBRejeita com mensagem de limite
Formato não suportadoaccept no input limita; fallback com mensagem
Microfone negadoCatch no getUserMedia com mensagem amigável
Áudio muito longo (> 5 min)Modelo pode truncar; avisar limite recomendado
Áudio sem fala (silêncio/música)Modelo retorna texto vazio ou irrelevante — UI trata gracefully
Sessão abortadaAbortError silenciado
Modelo em downloadProgress bar com percentual
Idioma diferente do esperadoSystem prompt orienta PT-BR, mas aceita outros

CSS Essencial

.input-controls {
  display: flex;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.upload-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem;
  background: #f3f4f6;
  border: 2px dashed #d1d5db;
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.2s;
}

.upload-btn:hover {
  border-color: #6366f1;
}

.record-btn {
  padding: 0.75rem 1.25rem;
  border-radius: 8px;
  border: none;
  background: #1f2937;
  color: white;
  cursor: pointer;
}

.record-btn.recording {
  background: #dc2626;
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}

.audio-preview {
  background: #f9fafb;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1.5rem;
}

.result-box {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  border-radius: 8px;
  padding: 1.5rem;
  position: relative;
}

.copy-btn {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  background: none;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  padding: 0.4rem 0.75rem;
  cursor: pointer;
  font-size: 0.85rem;
}

.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }