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á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; }