Conjunto de Fala
Versão em série do Falar as Palavras: o aprendiz responde múltiplos prompts em sequência usando reconhecimento de voz. Ideal para prática oral extensiva.
Experimente
Leia cada termo em voz alta quando o microfone aparecer:
1 / 4Você pode ouvir a pronúncia de cada termo, mas este navegador não reconhece fala — os microfones estão desativados. Para praticar com correção automática, use o Chrome ou outro navegador baseado em Chromium.
Código-fonte
Dados de exemplo
Termos pra ler em voz alta, um por vez.
| 1 |
<?php
|
| 2 |
$termos = ['maçã', 'banana', 'laranja'];
|
| 3 |
$total = count($termos);
|
| 4 |
?>
|
Estrutura da atividade
Contador no topo e os termos empilhados, todos menos o primeiro nascem ocultos.
| 1 |
<div class="questao" data-tipo="conjunto-fala" data-total="<?= $total ?>">
|
| 2 |
|
| 3 |
<div class="questao-cabecalho">
|
| 4 |
<p>Diga cada termo em voz alta quando o microfone aparecer:</p>
|
| 5 |
<span><span class="questao-atual">1</span> / <?= $total ?></span>
|
| 6 |
</div>
|
| 7 |
|
| 8 |
<?php foreach ($termos as $qi => $termo): ?>
|
| 9 |
<div class="questao-item<?= $qi > 0 ? ' oculto' : '' ?>" data-qi="<?= $qi ?>">
|
| 10 |
<p class="questao-termo"><?= htmlspecialchars($termo) ?></p>
|
| 11 |
<button type="button" class="questao-mic" data-resposta="<?= htmlspecialchars($termo) ?>">
|
| 12 |
Falar
|
| 13 |
</button>
|
| 14 |
<p class="questao-feedback" hidden></p>
|
| 15 |
</div>
|
| 16 |
<?php endforeach; ?>
|
Botão único de avançar
Fora do laço de termos — sempre a última coisa do card, some de novo a cada clique.
| 1 |
<button type="button" class="questao-proximo" hidden>Próxima →</button>
|
| 2 |
|
| 3 |
<div class="questao-resultado" hidden>
|
| 4 |
<p class="questao-placar"><span class="questao-acertos">0</span> / <?= $total ?> termos reconhecidos</p>
|
| 5 |
<button type="button" class="questao-reiniciar">Recomeçar atividade</button>
|
| 6 |
</div>
|
| 7 |
|
| 8 |
</div>
|
Card e cabeçalho
Contêiner da atividade e a linha com instrução + contador "1 / N".
| 1 |
.questao[data-tipo="conjunto-fala"] {
|
| 2 |
max-width: 420px;
|
| 3 |
padding: 24px;
|
| 4 |
border: 1px solid #e2e8f0;
|
| 5 |
border-radius: 16px;
|
| 6 |
background: #fff;
|
| 7 |
font-family: sans-serif;
|
| 8 |
}
|
| 9 |
|
| 10 |
.questao-cabecalho {
|
| 11 |
display: flex;
|
| 12 |
align-items: center;
|
| 13 |
justify-content: space-between;
|
| 14 |
margin-bottom: 20px;
|
| 15 |
font-size: 14px;
|
| 16 |
color: #64748b;
|
| 17 |
}
|
| 18 |
|
| 19 |
.questao-cabecalho span:last-child {
|
| 20 |
padding: 4px 10px;
|
| 21 |
border-radius: 999px;
|
| 22 |
background: #f1f5f9;
|
| 23 |
font-size: 12px;
|
| 24 |
flex-shrink: 0;
|
| 25 |
}
|
Termo ativo
.oculto tira o termo da tela — só um fica visível por vez.
| 1 |
.questao-item.oculto {
|
| 2 |
display: none;
|
| 3 |
}
|
| 4 |
|
| 5 |
.questao-termo {
|
| 6 |
margin: 0 0 16px;
|
| 7 |
font-weight: 600;
|
| 8 |
font-size: 18px;
|
| 9 |
color: #0f172a;
|
| 10 |
}
|
Botão do microfone
Cor por classe (.ouvindo/.acerto/.erro) — o clique já é a tentativa e a correção.
| 1 |
.questao-mic {
|
| 2 |
padding: 12px 24px;
|
| 3 |
border: 2px solid #6366f1;
|
| 4 |
border-radius: 999px;
|
| 5 |
background: rgba(99, 102, 241, .1);
|
| 6 |
color: #4338ca;
|
| 7 |
font: inherit;
|
| 8 |
font-weight: 600;
|
| 9 |
cursor: pointer;
|
| 10 |
}
|
| 11 |
|
| 12 |
.questao-mic:disabled { cursor: not-allowed; }
|
| 13 |
.questao-mic.ouvindo { border-color: #dc2626; background: rgba(220, 38, 38, .1); color: #b91c1c; }
|
| 14 |
.questao-mic.acerto { border-color: #16a34a; background: rgba(22, 163, 74, .1); color: #15803d; }
|
| 15 |
.questao-mic.erro { border-color: #dc2626; background: rgba(220, 38, 38, .1); color: #b91c1c; }
|
Feedback e navegação
Mensagem de acerto/erro por termo, e os botões de avançar/reiniciar.
| 1 |
.questao-feedback {
|
| 2 |
margin-top: 16px;
|
| 3 |
padding: 10px 14px;
|
| 4 |
border-radius: 10px;
|
| 5 |
font-size: 14px;
|
| 6 |
}
|
| 7 |
|
| 8 |
.questao-feedback.acerto { background: #dcfce7; color: #14532d; }
|
| 9 |
.questao-feedback.erro { background: #fee2e2; color: #7f1d1d; }
|
| 10 |
|
| 11 |
.questao-proximo,
|
| 12 |
.questao-reiniciar {
|
| 13 |
width: 100%;
|
| 14 |
margin-top: 16px;
|
| 15 |
padding: 12px 16px;
|
| 16 |
border: none;
|
| 17 |
border-radius: 10px;
|
| 18 |
background: #4f46e5;
|
| 19 |
color: #fff;
|
| 20 |
font: inherit;
|
| 21 |
font-weight: 600;
|
| 22 |
cursor: pointer;
|
| 23 |
}
|
| 24 |
|
| 25 |
.questao-reiniciar { background: #e5e7eb; color: #1f2937; margin-top: 12px; }
|
Resultado final
Placar centralizado, escondido até o último termo ser reconhecido.
| 1 |
.questao-resultado {
|
| 2 |
padding: 16px;
|
| 3 |
border-radius: 10px;
|
| 4 |
background: #eef2ff;
|
| 5 |
text-align: center;
|
| 6 |
}
|
| 7 |
|
| 8 |
.questao-placar {
|
| 9 |
margin: 0;
|
| 10 |
font-size: 22px;
|
| 11 |
font-weight: 700;
|
| 12 |
color: #4338ca;
|
| 13 |
}
|
Encontrar os elementos da atividade
Contador, placar e o botão único de avançar — compartilhados por todos os termos.
| 1 |
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
document.querySelectorAll('.questao[data-tipo="conjunto-fala"]').forEach(function (questao) {
|
| 3 |
var total = parseInt(questao.dataset.total, 10);
|
| 4 |
var acertos = 0;
|
| 5 |
var atualEl = questao.querySelector('.questao-atual');
|
| 6 |
var proximo = questao.querySelector('.questao-proximo');
|
| 7 |
var resultado = questao.querySelector('.questao-resultado');
|
| 8 |
var acertosEl = questao.querySelector('.questao-acertos');
|
Verificar suporte do navegador
Só o Chrome e outros baseados em Chromium implementam o reconhecimento de voz.
| 1 |
var Reconhecimento = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 2 |
if (!Reconhecimento) {
|
| 3 |
questao.querySelectorAll('.questao-mic').forEach(function (mic) { mic.disabled = true; });
|
| 4 |
return;
|
| 5 |
}
|
Comparar a transcrição com o termo esperado
Normaliza maiúsculas, acentos e pontuação — a API costuma devolver "Maçã." em vez de "maçã".
| 1 |
function normalizar(texto) {
|
| 2 |
return texto
|
| 3 |
.toLowerCase()
|
| 4 |
.normalize('NFD').replace(/\p{Diacritic}/gu, '')
|
| 5 |
.replace(/[^\p{L}\p{N}\s]/gu, '')
|
| 6 |
.trim();
|
| 7 |
}
|
Revelar o botão de avançar
Rotula pro termo atual ("Próxima →" ou "Ver resultado" no último).
| 1 |
function revelarProximo(qi) {
|
| 2 |
proximo.textContent = qi < total - 1 ? 'Próxima →' : 'Ver resultado';
|
| 3 |
proximo.dataset.qi = qi;
|
| 4 |
proximo.hidden = false;
|
| 5 |
}
|
Reconhecer cada termo
Um microfone por item — clicar já é a tentativa e a correção, no mesmo gesto.
| 1 |
questao.querySelectorAll('.questao-item').forEach(function (item) {
|
| 2 |
var qi = parseInt(item.dataset.qi, 10);
|
| 3 |
var mic = item.querySelector('.questao-mic');
|
| 4 |
var fb = item.querySelector('.questao-feedback');
|
| 5 |
var esperado = mic.dataset.resposta;
|
| 6 |
|
| 7 |
mic.addEventListener('click', function () {
|
| 8 |
var recognition = new Reconhecimento();
|
| 9 |
recognition.lang = 'pt-BR';
|
| 10 |
|
| 11 |
recognition.onstart = function () { mic.classList.add('ouvindo'); };
|
| 12 |
|
| 13 |
recognition.onresult = function (evento) {
|
| 14 |
var transcricao = evento.results[0][0].transcript;
|
| 15 |
var correta = normalizar(transcricao) === normalizar(esperado);
|
| 16 |
|
| 17 |
mic.classList.remove('ouvindo');
|
| 18 |
mic.classList.add(correta ? 'acerto' : 'erro');
|
| 19 |
mic.disabled = true;
|
| 20 |
if (correta) acertos++;
|
| 21 |
|
| 22 |
fb.className = 'questao-feedback ' + (correta ? 'acerto' : 'erro');
|
| 23 |
fb.textContent = correta
|
| 24 |
? 'Reconhecido! Você disse: "' + transcricao + '".'
|
| 25 |
: 'Não reconheci. Você disse: "' + transcricao + '".';
|
| 26 |
fb.hidden = false;
|
| 27 |
|
| 28 |
revelarProximo(qi);
|
| 29 |
};
|
| 30 |
|
| 31 |
recognition.onerror = function () { mic.classList.remove('ouvindo'); };
|
| 32 |
|
| 33 |
recognition.start();
|
| 34 |
});
|
| 35 |
});
|
Avançar pro próximo termo ou pro resultado
Um único botão pra atividade inteira — o clique já o esconde de novo.
| 1 |
proximo.addEventListener('click', function () {
|
| 2 |
var qi = parseInt(proximo.dataset.qi, 10);
|
| 3 |
proximo.hidden = true;
|
| 4 |
questao.querySelector('.questao-item[data-qi="' + qi + '"]').classList.add('oculto');
|
| 5 |
|
| 6 |
if (qi === total - 1) {
|
| 7 |
resultado.hidden = false;
|
| 8 |
acertosEl.textContent = acertos;
|
| 9 |
} else {
|
| 10 |
questao.querySelector('.questao-item[data-qi="' + (qi + 1) + '"]').classList.remove('oculto');
|
| 11 |
atualEl.textContent = qi + 2;
|
| 12 |
}
|
| 13 |
});
|
Reiniciar
Zera o placar, volta pro primeiro termo e libera todos os microfones.
| 1 |
questao.querySelector('.questao-reiniciar').addEventListener('click', function () {
|
| 2 |
acertos = 0;
|
| 3 |
atualEl.textContent = 1;
|
| 4 |
resultado.hidden = true;
|
| 5 |
|
| 6 |
questao.querySelectorAll('.questao-item').forEach(function (item, i) {
|
| 7 |
item.classList.toggle('oculto', i !== 0);
|
| 8 |
});
|
| 9 |
questao.querySelectorAll('.questao-mic').forEach(function (mic) {
|
| 10 |
mic.disabled = false;
|
| 11 |
mic.classList.remove('acerto', 'erro');
|
| 12 |
});
|
| 13 |
questao.querySelectorAll('.questao-feedback').forEach(function (fb) { fb.hidden = true; });
|
| 14 |
});
|