Demo: Transcrição de Áudio com IA Local (Prompt API)
avançadoTranscreva 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.
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
- Página carrega → verifica suporte da Prompt API + flag multimodal
- Modelo pronto → controles de upload e gravação habilitados
- Upload de arquivo → valida tipo/tamanho, mostra preview com player de áudio
- Gravação ao vivo → botão muda para “Parar”, indicador visual de gravação ativa
- Clica “Transcrever” → botão desabilita, feedback de processamento
- Resultado chega → texto transcrito aparece com botão de copiar
- Erro → mensagem contextual no status bar
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Prompt API ausente | Mensagem + link para requisitos do Chrome |
| Flag multimodal desativada | Orienta ativar chrome://flags/#prompt-api-for-gemini-nano-multimodal-input |
| Arquivo > 10 MB | Rejeita com mensagem de limite |
| Formato não suportado | accept no input limita; fallback com mensagem |
| Microfone negado | Catch 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 abortada | AbortError silenciado |
| Modelo em download | Progress bar com percentual |
| Idioma diferente do esperado | System 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; }