Demo: Previsão do Tempo em Linguagem Natural (Prompt API)
intermediárioTransforme dados meteorológicos JSON em descrições humanizadas em português usando IA on-device. Integra Prompt API com Web Speech API para leitura em voz alta.
Visão Geral
Demo que recebe dados meteorológicos estruturados (JSON) e usa a Prompt API para gerar uma descrição natural e contextualizada em português — tipo um âncora de jornal descrevendo o tempo. Combina com Web Speech API pra ler a previsão em voz alta.
Pra quem: Desenvolvedores que querem transformar dados brutos em texto fluido usando IA local.
Técnica principal: Passar JSON estruturado como contexto no prompt e usar session.prompt() para gerar texto livre humanizado. A Web Speech API (speechSynthesis) adiciona a camada de acessibilidade com leitura em voz alta.
Wireframe
┌─────────────────────────────────────────────────────┐
│ 🌤️ Previsão do Tempo — Linguagem Natural │
├─────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 📍 São Paulo, SP │ │
│ │ 🌡️ 24°C | Parcialmente Nublado │ │
│ │ 💧 68% | 🌬️ 12 km/h │ │
│ │ 📅 Domingo, 10:30 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [ 🔄 Outra cidade ] [ 🎙️ Gerar Previsão ] │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ "Manhã de domingo com céu parcialmente │ │
│ │ nublado em São Paulo. Temperatura │ │
│ │ agradável de 24 graus, umidade em 68%..." │ │
│ │ [ 🔊 ] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="previsao-tempo">
<h2>🌤️ Previsão do Tempo — Linguagem Natural</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<div id="weather-card" class="weather-card">
<div class="weather-header">
<span id="city-name">📍 São Paulo, SP</span>
<span id="weather-date">📅 —</span>
</div>
<div class="weather-data">
<span id="temperature" class="temp-display">—°C</span>
<span id="condition">—</span>
</div>
<div class="weather-details">
<span id="humidity">💧 —%</span>
<span id="wind">🌬️ — km/h</span>
<span id="uv">☀️ UV —</span>
</div>
</div>
<div class="actions">
<button id="btn-random-city">🔄 Outra cidade</button>
<button id="btn-generate" disabled>🎙️ Gerar Previsão</button>
</div>
<div id="result" class="result-box" hidden>
<p id="forecast-text"></p>
<button id="btn-speak" class="btn-speak" title="Ouvir previsão">🔊</button>
</div>
</section>
Código JavaScript
// Dados mock — simula resposta de API meteorológica
const MOCK_WEATHER = [
{
city: "São Paulo, SP",
temp: 24,
condition: "Parcialmente Nublado",
humidity: 68,
wind: 12,
uv: 4,
feelsLike: 26,
precipitation: 10
},
{
city: "Rio de Janeiro, RJ",
temp: 31,
condition: "Ensolarado",
humidity: 55,
wind: 8,
uv: 9,
feelsLike: 34,
precipitation: 0
},
{
city: "Curitiba, PR",
temp: 14,
condition: "Chuva Leve",
humidity: 89,
wind: 22,
uv: 1,
feelsLike: 11,
precipitation: 75
},
{
city: "Salvador, BA",
temp: 28,
condition: "Nublado com Pancadas",
humidity: 82,
wind: 15,
uv: 6,
feelsLike: 30,
precipitation: 60
},
{
city: "Porto Alegre, RS",
temp: 9,
condition: "Nevoeiro",
humidity: 95,
wind: 5,
uv: 0,
feelsLike: 7,
precipitation: 20
}
];
class WeatherNarrator {
constructor() {
this.session = null;
this.currentWeather = null;
this.btnGenerate = document.getElementById("btn-generate");
this.btnRandom = document.getElementById("btn-random-city");
this.btnSpeak = document.getElementById("btn-speak");
this.resultEl = document.getElementById("result");
this.forecastText = document.getElementById("forecast-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 148+.", "error");
return;
}
const availability = await LanguageModel.availability();
if (availability === "unavailable") {
this.showStatus("❌ Modelo não disponível neste dispositivo.", "error");
return;
}
if (availability === "downloading") {
this.showStatus("⏳ Baixando modelo... Aguarde.", "loading");
}
try {
this.session = await LanguageModel.create({
expectedInputs: [{ type: "text", languages: ["pt"] }],
expectedOutputs: [{ type: "text", languages: ["pt"] }],
initialPrompts: [{
role: "system",
content: `Você é um apresentador de previsão do tempo brasileiro. Receba dados meteorológicos em JSON e gere uma descrição natural em português (2-3 frases). Seja informativo, conciso e contextualizado — mencione se o tempo está bom pra sair, se precisa de guarda-chuva, etc. Nunca repita os números brutos sem contexto.`
}],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
document.getElementById("status-message").textContent =
`⏳ Baixando modelo... ${Math.round(e.loaded * 100)}%`;
});
}
});
this.btnGenerate.disabled = false;
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
return;
}
// Carregar primeira cidade
this.loadWeather(0);
// Eventos
this.btnGenerate.addEventListener("click", () => this.generate());
this.btnRandom.addEventListener("click", () => this.randomCity());
this.btnSpeak.addEventListener("click", () => this.speak());
}
loadWeather(index) {
this.currentWeather = MOCK_WEATHER[index];
const w = this.currentWeather;
const now = new Date();
document.getElementById("city-name").textContent = `📍 ${w.city}`;
document.getElementById("temperature").textContent = `${w.temp}°C`;
document.getElementById("condition").textContent = w.condition;
document.getElementById("humidity").textContent = `💧 ${w.humidity}%`;
document.getElementById("wind").textContent = `🌬️ ${w.wind} km/h`;
document.getElementById("uv").textContent = `☀️ UV ${w.uv}`;
document.getElementById("weather-date").textContent =
`📅 ${now.toLocaleDateString("pt-BR", { weekday: "long" })}, ${now.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}`;
this.resultEl.hidden = true;
}
randomCity() {
const idx = Math.floor(Math.random() * MOCK_WEATHER.length);
this.loadWeather(idx);
}
async generate() {
if (!this.currentWeather) return;
this.btnGenerate.disabled = true;
this.btnGenerate.textContent = "⏳ Gerando...";
this.resultEl.hidden = true;
const weatherJSON = JSON.stringify(this.currentWeather, null, 2);
try {
const text = await this.session.prompt(
`Dados meteorológicos atuais:\n\n${weatherJSON}\n\nGere uma previsão natural em português (2-3 frases).`
);
this.forecastText.textContent = text.trim();
this.resultEl.hidden = false;
} catch (err) {
if (err.name === "AbortError") return;
this.showStatus(`❌ Erro: ${err.message}`, "error");
} finally {
this.btnGenerate.disabled = false;
this.btnGenerate.textContent = "🎙️ Gerar Previsão";
}
}
speak() {
const text = this.forecastText.textContent;
if (!text) return;
// Cancelar fala anterior
speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "pt-BR";
utterance.rate = 0.95;
utterance.pitch = 1;
// Feedback visual durante fala
this.btnSpeak.textContent = "🔇";
utterance.onend = () => { this.btnSpeak.textContent = "🔊"; };
utterance.onerror = () => { this.btnSpeak.textContent = "🔊"; };
speechSynthesis.speak(utterance);
}
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 WeatherNarrator());
Fluxo UX
- Página carrega → verifica Prompt API, inicializa sessão com system prompt de apresentador de clima
- Card exibe dados mock → primeira cidade (São Paulo) com temperatura, umidade, vento, UV
- “Outra cidade” → rotaciona aleatoriamente entre 5 cidades com climas distintos
- “Gerar Previsão” → envia JSON ao modelo, recebe descrição humanizada
- Texto aparece → parágrafo natural com contexto (dica de guarda-chuva, calor, etc.)
- Botão 🔊 → Web Speech API lê o texto em pt-BR com voz do sistema
- Durante fala → ícone muda para 🔇, retorna ao 🔊 quando termina
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Prompt API indisponível | Mensagem + botões desabilitados |
| Modelo em download | Progresso visível no status bar |
| speechSynthesis não suportado | Botão 🔊 funciona como no-op silencioso |
| Voz pt-BR não disponível | Usa voz padrão do sistema (fallback nativo) |
| Texto gerado vazio | Não exibe resultado, mantém estado anterior |
| Sessão abortada | Silencioso (ignora AbortError) |
| Múltiplos cliques rápidos em “Gerar” | Botão desabilitado durante processamento |
| Clique em 🔊 durante fala | speechSynthesis.cancel() antes de iniciar nova |
CSS Essencial
.weather-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 1rem;
padding: 1.5rem;
margin-bottom: 1rem;
}
.weather-header {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
opacity: 0.9;
margin-bottom: 1rem;
}
.temp-display {
font-size: 2.5rem;
font-weight: 700;
}
.weather-data {
display: flex;
align-items: baseline;
gap: 1rem;
margin-bottom: 1rem;
}
.weather-details {
display: flex;
gap: 1.5rem;
font-size: 0.875rem;
opacity: 0.85;
}
.result-box {
position: relative;
background: #f8fafc;
border-left: 4px solid #667eea;
border-radius: 0.5rem;
padding: 1.25rem;
margin-top: 1rem;
line-height: 1.6;
}
.btn-speak {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.btn-speak:hover {
opacity: 1;
}
.actions {
display: flex;
gap: 0.75rem;
margin: 1rem 0;
}
.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }