Demo: Previsão do Tempo em Linguagem Natural (Prompt API)

intermediário

Transforme 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.

Verificando...

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

  1. Página carrega → verifica Prompt API, inicializa sessão com system prompt de apresentador de clima
  2. Card exibe dados mock → primeira cidade (São Paulo) com temperatura, umidade, vento, UV
  3. “Outra cidade” → rotaciona aleatoriamente entre 5 cidades com climas distintos
  4. “Gerar Previsão” → envia JSON ao modelo, recebe descrição humanizada
  5. Texto aparece → parágrafo natural com contexto (dica de guarda-chuva, calor, etc.)
  6. Botão 🔊 → Web Speech API lê o texto em pt-BR com voz do sistema
  7. Durante fala → ícone muda para 🔇, retorna ao 🔊 quando termina

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API indisponívelMensagem + botões desabilitados
Modelo em downloadProgresso visível no status bar
speechSynthesis não suportadoBotão 🔊 funciona como no-op silencioso
Voz pt-BR não disponívelUsa voz padrão do sistema (fallback nativo)
Texto gerado vazioNão exibe resultado, mantém estado anterior
Sessão abortadaSilencioso (ignora AbortError)
Múltiplos cliques rápidos em “Gerar”Botão desabilitado durante processamento
Clique em 🔊 durante falaspeechSynthesis.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; }