1044 lines
24 KiB
HTML
1044 lines
24 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>童趣古诗学习乐园</title>
|
||
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
|
|
|
||
|
|
<!-- 引入后端集成脚本 -->
|
||
|
|
<script src="../../js/apiService.js"></script>
|
||
|
|
<script src="../../js/dataManager.js"></script>
|
||
|
|
<script src="../../js/userManager.js"></script>
|
||
|
|
<script src="../../js/accessTracker.js"></script>
|
||
|
|
<script src="../../js/gameTracker.js"></script>
|
||
|
|
<script src="../../js/gameDataLogger.js"></script>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
box-sizing: border-box;
|
||
|
|
font-family: 'Comic Sans MS', 'Marker Felt', '幼圆', sans-serif;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||
|
|
min-height: 100vh;
|
||
|
|
padding: 20px;
|
||
|
|
overflow-x: hidden;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 装饰元素 */
|
||
|
|
.cloud {
|
||
|
|
position: absolute;
|
||
|
|
background: white;
|
||
|
|
border-radius: 50%;
|
||
|
|
opacity: 0.7;
|
||
|
|
z-index: -1;
|
||
|
|
animation: float 15s infinite linear;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cloud:nth-child(1) {
|
||
|
|
width: 120px;
|
||
|
|
height: 40px;
|
||
|
|
top: 10%;
|
||
|
|
left: 5%;
|
||
|
|
animation-duration: 20s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cloud:nth-child(2) {
|
||
|
|
width: 180px;
|
||
|
|
height: 60px;
|
||
|
|
top: 25%;
|
||
|
|
right: 8%;
|
||
|
|
animation-duration: 25s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cloud:nth-child(3) {
|
||
|
|
width: 100px;
|
||
|
|
height: 35px;
|
||
|
|
bottom: 15%;
|
||
|
|
left: 15%;
|
||
|
|
animation-duration: 18s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.star {
|
||
|
|
position: absolute;
|
||
|
|
background: #ffeb3b;
|
||
|
|
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
|
||
|
|
opacity: 0.8;
|
||
|
|
z-index: -1;
|
||
|
|
animation: twinkle 3s infinite alternate;
|
||
|
|
}
|
||
|
|
|
||
|
|
.star:nth-child(4) {
|
||
|
|
width: 25px;
|
||
|
|
height: 25px;
|
||
|
|
top: 15%;
|
||
|
|
right: 20%;
|
||
|
|
animation-delay: 0.5s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.star:nth-child(5) {
|
||
|
|
width: 20px;
|
||
|
|
height: 20px;
|
||
|
|
bottom: 20%;
|
||
|
|
left: 25%;
|
||
|
|
animation-delay: 1s;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes float {
|
||
|
|
0% {
|
||
|
|
transform: translateX(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
50% {
|
||
|
|
transform: translateX(20px);
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
transform: translateX(0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes twinkle {
|
||
|
|
0% {
|
||
|
|
opacity: 0.3;
|
||
|
|
transform: scale(0.9);
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
opacity: 1;
|
||
|
|
transform: scale(1.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.container {
|
||
|
|
max-width: 900px;
|
||
|
|
margin: 0 auto;
|
||
|
|
background-color: rgba(255, 255, 255, 0.9);
|
||
|
|
border-radius: 25px;
|
||
|
|
padding: 30px;
|
||
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
border: 8px solid #ffd6e7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.container::before {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
top: -10px;
|
||
|
|
left: -10px;
|
||
|
|
right: -10px;
|
||
|
|
bottom: -10px;
|
||
|
|
border: 4px dashed #ff9ec0;
|
||
|
|
border-radius: 30px;
|
||
|
|
z-index: -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
header {
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
color: #ff6b9c;
|
||
|
|
font-size: 2.8rem;
|
||
|
|
margin-bottom: 15px;
|
||
|
|
text-shadow: 3px 3px 0 #ffe0eb;
|
||
|
|
position: relative;
|
||
|
|
display: inline-block;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1::after {
|
||
|
|
content: "✏️";
|
||
|
|
position: absolute;
|
||
|
|
right: -40px;
|
||
|
|
top: -10px;
|
||
|
|
font-size: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: #5a7bd3;
|
||
|
|
font-size: 1.3rem;
|
||
|
|
margin-top: -10px;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content-area {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 30px;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-section {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 300px;
|
||
|
|
background: linear-gradient(to bottom right, #e0f7ff, #ffebf3);
|
||
|
|
border-radius: 20px;
|
||
|
|
padding: 25px;
|
||
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
||
|
|
border: 4px solid #b8e8ff;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-section h2,
|
||
|
|
.recognition-section h2 {
|
||
|
|
color: #ff6b9c;
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
font-size: 1.8rem;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-section h2::after,
|
||
|
|
.recognition-section h2::after {
|
||
|
|
content: "";
|
||
|
|
display: block;
|
||
|
|
width: 80px;
|
||
|
|
height: 4px;
|
||
|
|
background: linear-gradient(to right, #ff9ec0, #a8edea);
|
||
|
|
margin: 8px auto 0;
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-card {
|
||
|
|
background-color: white;
|
||
|
|
border-radius: 15px;
|
||
|
|
padding: 25px;
|
||
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 25px;
|
||
|
|
border: 3px dashed #ffd1e0;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-card::before {
|
||
|
|
content: "📜";
|
||
|
|
position: absolute;
|
||
|
|
top: -20px;
|
||
|
|
left: -15px;
|
||
|
|
font-size: 2rem;
|
||
|
|
transform: rotate(-20deg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-title {
|
||
|
|
color: #5a7bd3;
|
||
|
|
font-size: 1.7rem;
|
||
|
|
margin-bottom: 15px;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-author {
|
||
|
|
color: #ff9ec0;
|
||
|
|
font-size: 1.2rem;
|
||
|
|
margin-bottom: 25px;
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-content {
|
||
|
|
font-size: 1.4rem;
|
||
|
|
line-height: 2.2;
|
||
|
|
color: #5c5c5c;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-content span {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
.voice-controls {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 20px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: 16px 30px;
|
||
|
|
font-size: 1.2rem;
|
||
|
|
border: none;
|
||
|
|
border-radius: 50px;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 10px;
|
||
|
|
font-weight: bold;
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn:active {
|
||
|
|
transform: translateY(3px);
|
||
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.play-btn {
|
||
|
|
background: linear-gradient(to right, #ff9ec0, #ff6b9c);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.play-btn:hover {
|
||
|
|
background: linear-gradient(to right, #ff8ab3, #ff5a8f);
|
||
|
|
transform: translateY(-3px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-section {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 300px;
|
||
|
|
background: linear-gradient(to bottom right, #e0f7ff, #ffebf3);
|
||
|
|
border-radius: 20px;
|
||
|
|
padding: 25px;
|
||
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
||
|
|
border: 4px solid #a8edea;
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-box {
|
||
|
|
background-color: white;
|
||
|
|
border-radius: 15px;
|
||
|
|
padding: 20px;
|
||
|
|
min-height: 200px;
|
||
|
|
margin-bottom: 25px;
|
||
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
||
|
|
border: 3px solid #d0f0ff;
|
||
|
|
position: relative;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-box::before {
|
||
|
|
content: "🔊";
|
||
|
|
position: absolute;
|
||
|
|
bottom: -20px;
|
||
|
|
right: -15px;
|
||
|
|
font-size: 2.5rem;
|
||
|
|
transform: rotate(15deg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-result {
|
||
|
|
font-size: 1.3rem;
|
||
|
|
color: #5c5c5c;
|
||
|
|
min-height: 150px;
|
||
|
|
/* display: flex; */
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
text-align: center;
|
||
|
|
line-height: 1.8;
|
||
|
|
padding: 15px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-result.active {
|
||
|
|
color: #ff6b9c;
|
||
|
|
font-weight: bold;
|
||
|
|
font-size: 1.4rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.recognition-controls {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 20px;
|
||
|
|
margin-top: 15px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.record-btn {
|
||
|
|
background: linear-gradient(to right, #5a7bd3, #3a5bb5);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.record-btn:hover {
|
||
|
|
background: linear-gradient(to right, #4a6bc3, #2a4ba5);
|
||
|
|
transform: translateY(-3px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.record-btn.recording {
|
||
|
|
background: linear-gradient(to right, #ff5252, #d32f2f);
|
||
|
|
animation: pulse 1.5s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0% {
|
||
|
|
transform: scale(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
50% {
|
||
|
|
transform: scale(1.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
transform: scale(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.visualizer {
|
||
|
|
height: 60px;
|
||
|
|
background: linear-gradient(to bottom, #e0f7ff, #d0f0ff);
|
||
|
|
border-radius: 10px;
|
||
|
|
margin: 20px 0;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 10px;
|
||
|
|
border: 2px solid #b8e8ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.visualizer-bars {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-end;
|
||
|
|
gap: 4px;
|
||
|
|
height: 100%;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bar {
|
||
|
|
width: 12px;
|
||
|
|
background: linear-gradient(to top, #ff9ec0, #ff6b9c);
|
||
|
|
border-radius: 6px 6px 0 0;
|
||
|
|
transition: height 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.score-display {
|
||
|
|
text-align: center;
|
||
|
|
margin-top: 20px;
|
||
|
|
padding: 15px;
|
||
|
|
background: linear-gradient(to right, #a8edea, #b8e8ff);
|
||
|
|
border-radius: 15px;
|
||
|
|
font-size: 1.4rem;
|
||
|
|
color: #5a7bd3;
|
||
|
|
font-weight: bold;
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.score-display.show {
|
||
|
|
display: block;
|
||
|
|
animation: popIn 0.5s;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes popIn {
|
||
|
|
0% {
|
||
|
|
transform: scale(0.8);
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
transform: scale(1);
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.animals {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-around;
|
||
|
|
margin-top: 30px;
|
||
|
|
padding: 0 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.animal {
|
||
|
|
font-size: 3.5rem;
|
||
|
|
text-align: center;
|
||
|
|
animation: bounce 2s infinite alternate;
|
||
|
|
}
|
||
|
|
|
||
|
|
.animal:nth-child(1) {
|
||
|
|
animation-delay: 0s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.animal:nth-child(2) {
|
||
|
|
animation-delay: 0.5s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.animal:nth-child(3) {
|
||
|
|
animation-delay: 1s;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes bounce {
|
||
|
|
0% {
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
transform: translateY(-20px);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
footer {
|
||
|
|
text-align: center;
|
||
|
|
margin-top: 30px;
|
||
|
|
color: #5a7bd3;
|
||
|
|
font-size: 1rem;
|
||
|
|
padding: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-line {
|
||
|
|
display: block;
|
||
|
|
margin: 5px 0;
|
||
|
|
transition: all 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.highlight {
|
||
|
|
background-color: #ffeb3b;
|
||
|
|
border-radius: 5px;
|
||
|
|
padding: 2px 5px;
|
||
|
|
animation: highlight 1s;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes highlight {
|
||
|
|
0% {
|
||
|
|
background-color: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
50% {
|
||
|
|
background-color: #ffeb3b;
|
||
|
|
}
|
||
|
|
|
||
|
|
100% {
|
||
|
|
background-color: transparent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-hint {
|
||
|
|
font-size: 1.1rem;
|
||
|
|
color: #ff6b9c;
|
||
|
|
margin-top: 15px;
|
||
|
|
text-align: center;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.content-area {
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
font-size: 2.2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.poem-content {
|
||
|
|
font-size: 1.2rem;
|
||
|
|
line-height: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: 14px 25px;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
|
||
|
|
<body>
|
||
|
|
<!-- 装饰元素 -->
|
||
|
|
<div class="cloud"></div>
|
||
|
|
<div class="cloud"></div>
|
||
|
|
<div class="cloud"></div>
|
||
|
|
<div class="star"></div>
|
||
|
|
<div class="star"></div>
|
||
|
|
|
||
|
|
<div class="container">
|
||
|
|
<header>
|
||
|
|
<h1>童趣古诗学习乐园</h1>
|
||
|
|
<p class="subtitle">听古诗 • 读古诗 • 学古诗</p>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="content-area">
|
||
|
|
<section class="poem-section">
|
||
|
|
<h2><i class="fas fa-book-open"></i> 古诗欣赏</h2>
|
||
|
|
|
||
|
|
<div class="poem-card">
|
||
|
|
<div class="poem-title">静夜思</div>
|
||
|
|
<div class="poem-author">【唐】李白</div>
|
||
|
|
<div class="poem-content" id="poemText">
|
||
|
|
<span id="line1">床前明月光,</span>
|
||
|
|
<span id="line2">疑是地上霜。</span>
|
||
|
|
<span id="line3">举头望明月,</span>
|
||
|
|
<span id="line4">低头思故乡。</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="poem-hint">
|
||
|
|
请朗读上面这首诗,系统会识别并评分!
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="voice-controls">
|
||
|
|
<button id="playBtn" class="btn play-btn">
|
||
|
|
<i class="fas fa-volume-up"></i> 播放古诗
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="recognition-section">
|
||
|
|
<h2><i class="fas fa-microphone-alt"></i> 语音识别</h2>
|
||
|
|
|
||
|
|
<div class="recognition-box">
|
||
|
|
<div id="recognitionResult" class="recognition-result">
|
||
|
|
<div class="result-line">请点击下方按钮开始朗读古诗</div>
|
||
|
|
<div style="font-size: 2.5rem; margin-top: 15px;">🎤</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="visualizer">
|
||
|
|
<div class="visualizer-bars" id="visualizer">
|
||
|
|
<!-- 动态生成的音频条 -->
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="recognition-controls">
|
||
|
|
<button id="recordBtn" class="btn record-btn">
|
||
|
|
<i class="fas fa-microphone"></i> 开始朗读
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="scoreDisplay" class="score-display">
|
||
|
|
匹配度: <span id="scoreValue">0</span>%
|
||
|
|
<div id="scoreFeedback" style="font-size: 1.1rem; margin-top: 8px;"></div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="animals">
|
||
|
|
<div class="animal">🐰</div>
|
||
|
|
<div class="animal">🐻</div>
|
||
|
|
<div class="animal">🐱</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<footer>
|
||
|
|
<p>使用浏览器内置语音功能 • 童趣学习 • 快乐成长</p>
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
document.addEventListener('DOMContentLoaded', function () {
|
||
|
|
// 创建音频可视化条
|
||
|
|
const visualizer = document.getElementById('visualizer');
|
||
|
|
for (let i = 0; i < 20; i++) {
|
||
|
|
const bar = document.createElement('div');
|
||
|
|
bar.className = 'bar';
|
||
|
|
bar.style.height = Math.floor(Math.random() * 10 + 5) + 'px';
|
||
|
|
visualizer.appendChild(bar);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取DOM元素
|
||
|
|
const playBtn = document.getElementById('playBtn');
|
||
|
|
const recordBtn = document.getElementById('recordBtn');
|
||
|
|
const recognitionResult = document.getElementById('recognitionResult');
|
||
|
|
const scoreDisplay = document.getElementById('scoreDisplay');
|
||
|
|
const scoreValue = document.getElementById('scoreValue');
|
||
|
|
const scoreFeedback = document.getElementById('scoreFeedback');
|
||
|
|
const bars = document.querySelectorAll('.bar');
|
||
|
|
const poemLines = [
|
||
|
|
"床前明月光,",
|
||
|
|
"疑是地上霜。",
|
||
|
|
"举头望明月,",
|
||
|
|
"低头思故乡。"
|
||
|
|
];
|
||
|
|
|
||
|
|
// 初始化语音合成
|
||
|
|
const speechSynthesis = window.speechSynthesis;
|
||
|
|
let speechUtterance = null;
|
||
|
|
|
||
|
|
// 初始化语音识别
|
||
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||
|
|
let recognition = null;
|
||
|
|
let isRecording = false;
|
||
|
|
let userPoem = [];
|
||
|
|
let currentLine = 0;
|
||
|
|
|
||
|
|
if (SpeechRecognition) {
|
||
|
|
recognition = new SpeechRecognition();
|
||
|
|
recognition.continuous = false;
|
||
|
|
recognition.lang = 'zh-CN';
|
||
|
|
recognition.interimResults = true;
|
||
|
|
|
||
|
|
recognition.onstart = function () {
|
||
|
|
isRecording = true;
|
||
|
|
recordBtn.classList.add('recording');
|
||
|
|
recordBtn.innerHTML = '<i class="fas fa-microphone"></i> 朗读中...';
|
||
|
|
recognitionResult.innerHTML = "<div class='result-line'>正在聆听...</div>";
|
||
|
|
scoreDisplay.classList.remove('show');
|
||
|
|
userPoem = [];
|
||
|
|
currentLine = 0;
|
||
|
|
|
||
|
|
// 开始动画
|
||
|
|
animateBars();
|
||
|
|
};
|
||
|
|
|
||
|
|
recognition.onresult = function (event) {
|
||
|
|
let transcript = '';
|
||
|
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||
|
|
if (event.results[i].isFinal) {
|
||
|
|
transcript += event.results[i][0].transcript;
|
||
|
|
} else {
|
||
|
|
transcript = event.results[i][0].transcript;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 更新显示
|
||
|
|
recognitionResult.innerHTML = '';
|
||
|
|
// poemLines.forEach((line, index) => {
|
||
|
|
// const lineEl = document.createElement('div');
|
||
|
|
// lineEl.className = 'result-line';
|
||
|
|
// lineEl.textContent = line;
|
||
|
|
// if (index < currentLine) {
|
||
|
|
// lineEl.style.color = '#5a7bd3';
|
||
|
|
// lineEl.style.fontWeight = 'bold';
|
||
|
|
// }
|
||
|
|
// recognitionResult.appendChild(lineEl);
|
||
|
|
// });
|
||
|
|
|
||
|
|
// 添加用户朗读文本
|
||
|
|
const userLine = document.createElement('div');
|
||
|
|
userLine.className = 'result-line';
|
||
|
|
userLine.style.color = '#ff6b9c';
|
||
|
|
userLine.style.fontWeight = 'bold';
|
||
|
|
userLine.textContent = transcript;
|
||
|
|
recognitionResult.appendChild(userLine);
|
||
|
|
|
||
|
|
// 保存用户朗读内容
|
||
|
|
if (event.results[event.results.length - 1].isFinal) {
|
||
|
|
userPoem.push(transcript);
|
||
|
|
highlightLine(currentLine);
|
||
|
|
currentLine++;
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("recognition.onresult", event.results, event.resultIndex, transcript, userPoem);
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
recognition.onerror = function (event) {
|
||
|
|
console.error('语音识别错误:', event.error);
|
||
|
|
recognitionResult.innerHTML =
|
||
|
|
"<div class='result-line'>识别错误,请重试</div><div style='font-size:2.5rem;margin-top:15px;'>😢</div>";
|
||
|
|
};
|
||
|
|
|
||
|
|
recognition.onend = function () {
|
||
|
|
isRecording = false;
|
||
|
|
recordBtn.classList.remove('recording');
|
||
|
|
recordBtn.innerHTML = '<i class="fas fa-microphone"></i> 开始朗读';
|
||
|
|
|
||
|
|
// 停止动画
|
||
|
|
stopBarsAnimation();
|
||
|
|
|
||
|
|
if (userPoem.length > 0) {
|
||
|
|
// 计算匹配度
|
||
|
|
calculateMatch(userPoem.join(''));
|
||
|
|
|
||
|
|
// 添加完成提示
|
||
|
|
const completeDiv = document.createElement('div');
|
||
|
|
completeDiv.className = 'result-line';
|
||
|
|
completeDiv.style.color = '#5a7bd3';
|
||
|
|
completeDiv.style.marginTop = '15px';
|
||
|
|
completeDiv.textContent = '朗读完成!';
|
||
|
|
recognitionResult.appendChild(completeDiv);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
} else {
|
||
|
|
recordBtn.disabled = true;
|
||
|
|
recordBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 浏览器不支持';
|
||
|
|
recognitionResult.innerHTML =
|
||
|
|
"<div class='result-line'>您的浏览器不支持语音识别功能</div><div class='result-line'>请使用Chrome或Edge浏览器</div>";
|
||
|
|
}
|
||
|
|
|
||
|
|
// 播放古诗
|
||
|
|
playBtn.addEventListener('click', function () {
|
||
|
|
if (speechSynthesis.speaking) {
|
||
|
|
speechSynthesis.cancel();
|
||
|
|
playBtn.innerHTML = '<i class="fas fa-volume-up"></i> 播放古诗';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const poemText = "静夜思,唐,李白。床前明月光,疑是地上霜。举头望明月,低头思故乡。";
|
||
|
|
speechUtterance = new SpeechSynthesisUtterance(poemText);
|
||
|
|
speechUtterance.lang = 'zh-CN';
|
||
|
|
speechUtterance.rate = 0.9;
|
||
|
|
|
||
|
|
speechUtterance.onstart = function () {
|
||
|
|
playBtn.innerHTML = '<i class="fas fa-stop"></i> 停止播放';
|
||
|
|
|
||
|
|
// 高亮显示每行诗句
|
||
|
|
let lineIndex = 0;
|
||
|
|
const highlightInterval = setInterval(() => {
|
||
|
|
if (lineIndex < poemLines.length) {
|
||
|
|
highlightLine(lineIndex);
|
||
|
|
lineIndex++;
|
||
|
|
} else {
|
||
|
|
clearInterval(highlightInterval);
|
||
|
|
}
|
||
|
|
}, 2000);
|
||
|
|
};
|
||
|
|
|
||
|
|
speechUtterance.onend = function () {
|
||
|
|
playBtn.innerHTML = '<i class="fas fa-volume-up"></i> 播放古诗';
|
||
|
|
};
|
||
|
|
|
||
|
|
speechSynthesis.speak(speechUtterance);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 开始/停止朗读
|
||
|
|
recordBtn.addEventListener('click', function () {
|
||
|
|
iframePostMessage();
|
||
|
|
return
|
||
|
|
|
||
|
|
if (!recognition) return;
|
||
|
|
|
||
|
|
if (isRecording) {
|
||
|
|
recognition.stop();
|
||
|
|
} else {
|
||
|
|
recognition.start();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 高亮显示指定行
|
||
|
|
function highlightLine(lineIndex) {
|
||
|
|
const lineId = `line${lineIndex + 1}`;
|
||
|
|
const lineElement = document.getElementById(lineId);
|
||
|
|
|
||
|
|
if (lineElement) {
|
||
|
|
lineElement.classList.add('highlight');
|
||
|
|
|
||
|
|
// 移除高亮效果
|
||
|
|
setTimeout(() => {
|
||
|
|
lineElement.classList.remove('highlight');
|
||
|
|
}, 2000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 计算匹配度
|
||
|
|
function calculateMatch(text) {
|
||
|
|
const targetText = "床前明月光疑是地上霜举头望明月低头思故乡";
|
||
|
|
const userText = text.replace(/\s+/g, '').replace(/[,。]/g, '');
|
||
|
|
|
||
|
|
if (!userText) return;
|
||
|
|
|
||
|
|
// 使用编辑距离算法计算相似度
|
||
|
|
const m = targetText.length;
|
||
|
|
const n = userText.length;
|
||
|
|
const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0));
|
||
|
|
|
||
|
|
for (let i = 0; i <= m; i++) {
|
||
|
|
dp[i][0] = i;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let j = 0; j <= n; j++) {
|
||
|
|
dp[0][j] = j;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let i = 1; i <= m; i++) {
|
||
|
|
for (let j = 1; j <= n; j++) {
|
||
|
|
if (targetText[i - 1] === userText[j - 1]) {
|
||
|
|
dp[i][j] = dp[i - 1][j - 1];
|
||
|
|
} else {
|
||
|
|
dp[i][j] = Math.min(
|
||
|
|
dp[i - 1][j] + 1, // 删除
|
||
|
|
dp[i][j - 1] + 1, // 插入
|
||
|
|
dp[i - 1][j - 1] + 1 // 替换
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const distance = dp[m][n];
|
||
|
|
const maxLength = Math.max(m, n);
|
||
|
|
const accuracy = Math.round((1 - distance / maxLength) * 100);
|
||
|
|
|
||
|
|
scoreValue.textContent = accuracy;
|
||
|
|
|
||
|
|
// 设置不同分数段的反馈
|
||
|
|
let feedback = '';
|
||
|
|
let color = '';
|
||
|
|
|
||
|
|
if (accuracy >= 90) {
|
||
|
|
feedback = '太棒了!读得非常准确!';
|
||
|
|
color = '#2ecc71';
|
||
|
|
} else if (accuracy >= 70) {
|
||
|
|
feedback = '很好!继续加油!';
|
||
|
|
color = '#f1c40f';
|
||
|
|
} else if (accuracy >= 50) {
|
||
|
|
feedback = '不错!再多练习几次!';
|
||
|
|
color = '#e67e22';
|
||
|
|
} else {
|
||
|
|
feedback = '再试一次吧!你可以的!';
|
||
|
|
color = '#e74c3c';
|
||
|
|
}
|
||
|
|
|
||
|
|
scoreFeedback.textContent = feedback;
|
||
|
|
scoreFeedback.style.color = color;
|
||
|
|
|
||
|
|
if (accuracy > 70) {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #a8ffa8, #76e976)";
|
||
|
|
} else if (accuracy > 40) {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #fff9a8, #ffe976)";
|
||
|
|
} else {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #ffa8a8, #ff7676)";
|
||
|
|
}
|
||
|
|
|
||
|
|
scoreDisplay.classList.add('show');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 音频条动画
|
||
|
|
let animationInterval;
|
||
|
|
|
||
|
|
function animateBars() {
|
||
|
|
animationInterval = setInterval(() => {
|
||
|
|
bars.forEach(bar => {
|
||
|
|
const newHeight = Math.floor(Math.random() * 40 + 10);
|
||
|
|
bar.style.height = newHeight + 'px';
|
||
|
|
});
|
||
|
|
}, 200);
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopBarsAnimation() {
|
||
|
|
clearInterval(animationInterval);
|
||
|
|
bars.forEach(bar => {
|
||
|
|
bar.style.height = '5px';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 添加装饰元素
|
||
|
|
createDecorations();
|
||
|
|
|
||
|
|
function createDecorations() {
|
||
|
|
const container = document.querySelector('.container');
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
const star = document.createElement('div');
|
||
|
|
star.className = 'star';
|
||
|
|
star.style.width = Math.floor(Math.random() * 15 + 10) + 'px';
|
||
|
|
star.style.height = star.style.width;
|
||
|
|
star.style.left = Math.random() * 100 + '%';
|
||
|
|
star.style.top = Math.random() * 100 + '%';
|
||
|
|
container.appendChild(star);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 接收语音返回
|
||
|
|
let userPoem = [];
|
||
|
|
function speechResult(message) {
|
||
|
|
let status = message.status;
|
||
|
|
let data = message.data;
|
||
|
|
|
||
|
|
if (status == "start") {
|
||
|
|
|
||
|
|
} else if (status == "process") {
|
||
|
|
const recognitionResult = document.getElementById('recognitionResult');
|
||
|
|
|
||
|
|
let transcript = data;
|
||
|
|
userPoem = [data];
|
||
|
|
|
||
|
|
// 更新显示
|
||
|
|
recognitionResult.innerHTML = '';
|
||
|
|
// 添加用户朗读文本
|
||
|
|
const userLine = document.createElement('div');
|
||
|
|
userLine.className = 'result-line';
|
||
|
|
userLine.style.color = '#ff6b9c';
|
||
|
|
userLine.style.fontWeight = 'bold';
|
||
|
|
userLine.textContent = transcript;
|
||
|
|
recognitionResult.appendChild(userLine);
|
||
|
|
} else if (status == "end") {
|
||
|
|
calculateMatch(userPoem.join(''));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 计算匹配度
|
||
|
|
function calculateMatch(text) {
|
||
|
|
const scoreDisplay = document.getElementById('scoreDisplay');
|
||
|
|
const scoreValue = document.getElementById('scoreValue');
|
||
|
|
const scoreFeedback = document.getElementById('scoreFeedback');
|
||
|
|
|
||
|
|
const targetText = "床前明月光疑是地上霜举头望明月低头思故乡";
|
||
|
|
const userText = text.replace(/\s+/g, '').replace(/[,。]/g, '');
|
||
|
|
|
||
|
|
if (!userText) return;
|
||
|
|
|
||
|
|
// 使用编辑距离算法计算相似度
|
||
|
|
const m = targetText.length;
|
||
|
|
const n = userText.length;
|
||
|
|
const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0));
|
||
|
|
|
||
|
|
for (let i = 0; i <= m; i++) {
|
||
|
|
dp[i][0] = i;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let j = 0; j <= n; j++) {
|
||
|
|
dp[0][j] = j;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let i = 1; i <= m; i++) {
|
||
|
|
for (let j = 1; j <= n; j++) {
|
||
|
|
if (targetText[i - 1] === userText[j - 1]) {
|
||
|
|
dp[i][j] = dp[i - 1][j - 1];
|
||
|
|
} else {
|
||
|
|
dp[i][j] = Math.min(
|
||
|
|
dp[i - 1][j] + 1, // 删除
|
||
|
|
dp[i][j - 1] + 1, // 插入
|
||
|
|
dp[i - 1][j - 1] + 1 // 替换
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const distance = dp[m][n];
|
||
|
|
const maxLength = Math.max(m, n);
|
||
|
|
const accuracy = Math.round((1 - distance / maxLength) * 100);
|
||
|
|
|
||
|
|
scoreValue.textContent = accuracy;
|
||
|
|
|
||
|
|
// 设置不同分数段的反馈
|
||
|
|
let feedback = '';
|
||
|
|
let color = '';
|
||
|
|
|
||
|
|
if (accuracy >= 90) {
|
||
|
|
feedback = '太棒了!读得非常准确!';
|
||
|
|
color = '#2ecc71';
|
||
|
|
} else if (accuracy >= 70) {
|
||
|
|
feedback = '很好!继续加油!';
|
||
|
|
color = '#f1c40f';
|
||
|
|
} else if (accuracy >= 50) {
|
||
|
|
feedback = '不错!再多练习几次!';
|
||
|
|
color = '#e67e22';
|
||
|
|
} else {
|
||
|
|
feedback = '再试一次吧!你可以的!';
|
||
|
|
color = '#e74c3c';
|
||
|
|
}
|
||
|
|
|
||
|
|
scoreFeedback.textContent = feedback;
|
||
|
|
scoreFeedback.style.color = color;
|
||
|
|
|
||
|
|
if (accuracy > 70) {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #a8ffa8, #76e976)";
|
||
|
|
} else if (accuracy > 40) {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #fff9a8, #ffe976)";
|
||
|
|
} else {
|
||
|
|
scoreDisplay.style.background = "linear-gradient(to right, #ffa8a8, #ff7676)";
|
||
|
|
}
|
||
|
|
|
||
|
|
scoreDisplay.classList.add('show');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 网页间相互通信
|
||
|
|
window.addEventListener('message', function (event) {
|
||
|
|
// 检查消息来源是否可信
|
||
|
|
if (event.origin !== 'http://localhost') return;
|
||
|
|
|
||
|
|
const message = event.data;
|
||
|
|
// console.log('iframe 接收到的数据:', message);
|
||
|
|
|
||
|
|
if (message.type == "语音识别") {
|
||
|
|
speechResult(message);
|
||
|
|
}
|
||
|
|
else if (message.type == "体感识别") {
|
||
|
|
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function iframePostMessage() {
|
||
|
|
const message = { type: "语音识别", status: "request" };
|
||
|
|
window.parent.postMessage(message, 'http://localhost'); // 替换为目标域名
|
||
|
|
}
|
||
|
|
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
|
||
|
|
</html>
|