dashboard_upgrade

This commit is contained in:
superlishunqin 2025-09-22 06:06:19 +08:00
parent be60530d9e
commit a7fd012bd6
9 changed files with 3095 additions and 250 deletions

View File

@ -25,9 +25,11 @@ def create_app(config_name=None):
from app.routes.auth import auth_bp
from app.routes.main import main_bp
from app.routes.voice_test import voice_test_bp
from app.routes.voice_clone import voice_clone_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(voice_test_bp, url_prefix='/voice-test')
app.register_blueprint(voice_clone_bp, url_prefix='/voice-clone')
return app

View File

@ -20,6 +20,11 @@ class User(UserMixin, db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
voice_samples = db.relationship('VoiceSample', backref='user', lazy='dynamic', cascade='all, delete-orphan')
user_sessions = db.relationship('UserSession', backref='user', lazy='dynamic', cascade='all, delete-orphan')
user_progress = db.relationship('UserProgress', backref='user', lazy='dynamic', cascade='all, delete-orphan')
def set_password(self, password):
"""设置密码"""
self.password_hash = generate_password_hash(password)
@ -28,6 +33,15 @@ class User(UserMixin, db.Model):
"""验证密码"""
return check_password_hash(self.password_hash, password)
def get_latest_voice_sample(self):
"""获取最新的语音样本"""
return self.voice_samples.order_by(VoiceSample.upload_time.desc()).first()
def has_voice_clone_model(self):
"""检查是否有可用的声音克隆模型"""
latest_sample = self.get_latest_voice_sample()
return latest_sample and latest_sample.clone_model_status == 'ready'
def __repr__(self):
return f'<User {self.email}>'
@ -83,3 +97,125 @@ class EmailVerification(db.Model):
def __repr__(self):
return f'<EmailVerification {self.email}>'
class VoiceSample(db.Model):
__tablename__ = 'voice_samples'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
original_audio_url = db.Column(db.String(500), nullable=False, comment='原始语音文件URL')
upload_time = db.Column(db.DateTime, default=datetime.utcnow)
file_size = db.Column(db.Integer, nullable=True, comment='文件大小(字节)')
duration = db.Column(db.Numeric(5,2), nullable=True, comment='音频时长(秒)')
recognized_text = db.Column(db.Text, nullable=True, comment='语音识别文本')
clone_model_status = db.Column(db.String(20), default='pending', comment='克隆模型状态: pending/training/ready/failed')
clone_model_path = db.Column(db.String(500), nullable=True, comment='克隆模型文件路径')
clone_quality_score = db.Column(db.Numeric(3,2), nullable=True, comment='克隆质量评分(0-10)')
def __repr__(self):
return f'<VoiceSample {self.user_id}:{self.id}>'
class ScenarioCategory(db.Model):
__tablename__ = 'scenario_categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, comment='分类名称')
description = db.Column(db.Text, nullable=True, comment='分类描述')
icon = db.Column(db.String(50), default='fas fa-folder', comment='图标类名')
color = db.Column(db.String(20), default='primary', comment='主题色')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关联关系
scenarios = db.relationship('Scenario', backref='category', lazy='dynamic')
def __repr__(self):
return f'<ScenarioCategory {self.name}>'
class Scenario(db.Model):
__tablename__ = 'scenarios'
id = db.Column(db.Integer, primary_key=True)
category_id = db.Column(db.Integer, db.ForeignKey('scenario_categories.id'), nullable=False)
title = db.Column(db.String(255), nullable=False, comment='场景标题')
description = db.Column(db.Text, nullable=True, comment='场景描述')
icon = db.Column(db.String(50), default='fas fa-comments', comment='场景图标')
min_questions = db.Column(db.Integer, default=5, comment='最少问题数')
max_questions = db.Column(db.Integer, default=15, comment='最多问题数')
difficulty_level = db.Column(db.SmallInteger, default=1, comment='难度等级1-5')
is_active = db.Column(db.Boolean, default=True, comment='是否激活')
play_count = db.Column(db.Integer, default=0, comment='游玩次数')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关联关系
questions = db.relationship('ScenarioQuestion', backref='scenario', lazy='dynamic')
user_sessions = db.relationship('UserSession', backref='scenario', lazy='dynamic')
user_progress = db.relationship('UserProgress', backref='scenario', lazy='dynamic')
def get_difficulty_badge(self):
"""获取难度徽章样式"""
if self.difficulty_level == 1:
return 'success'
elif self.difficulty_level == 2:
return 'warning'
elif self.difficulty_level == 3:
return 'orange'
elif self.difficulty_level == 4:
return 'danger'
else:
return 'dark'
def get_difficulty_text(self):
"""获取难度文本"""
levels = {1: '简单', 2: '中等', 3: '有趣', 4: '挑战', 5: '专家'}
return levels.get(self.difficulty_level, '未知')
def __repr__(self):
return f'<Scenario {self.title}>'
class ScenarioQuestion(db.Model):
__tablename__ = 'scenario_questions'
id = db.Column(db.Integer, primary_key=True)
scenario_id = db.Column(db.Integer, db.ForeignKey('scenarios.id'), nullable=False)
question_order = db.Column(db.Integer, nullable=False, comment='问题顺序')
question_text = db.Column(db.Text, nullable=False, comment='问题文本')
expected_response_type = db.Column(db.String(50), default='open', comment='期望回答类型')
is_required = db.Column(db.Boolean, default=True, comment='是否必答')
def __repr__(self):
return f'<ScenarioQuestion {self.scenario_id}:{self.question_order}>'
class UserSession(db.Model):
__tablename__ = 'user_sessions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
scenario_id = db.Column(db.Integer, db.ForeignKey('scenarios.id'), nullable=False)
start_time = db.Column(db.DateTime, default=datetime.utcnow)
end_time = db.Column(db.DateTime, nullable=True)
duration = db.Column(db.Integer, nullable=True, comment='学习时长(秒)')
completion_rate = db.Column(db.Numeric(5,2), default=0.00, comment='完成度百分比')
total_questions = db.Column(db.Integer, default=0, comment='总问题数')
answered_questions = db.Column(db.Integer, default=0, comment='已回答问题数')
status = db.Column(db.Enum('ongoing', 'completed', 'interrupted', name='session_status'), default='ongoing')
def __repr__(self):
return f'<UserSession {self.user_id}:{self.scenario_id}>'
class UserProgress(db.Model):
__tablename__ = 'user_progress'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
scenario_id = db.Column(db.Integer, db.ForeignKey('scenarios.id'), nullable=False)
completion_count = db.Column(db.Integer, default=0, comment='完成次数')
best_score = db.Column(db.Numeric(4,2), nullable=True, comment='最佳得分')
average_score = db.Column(db.Numeric(4,2), nullable=True, comment='平均得分')
total_duration = db.Column(db.Integer, default=0, comment='总学习时长(秒)')
last_completed_at = db.Column(db.DateTime, nullable=True, comment='最后完成时间')
first_completed_at = db.Column(db.DateTime, nullable=True, comment='首次完成时间')
__table_args__ = (db.UniqueConstraint('user_id', 'scenario_id', name='uk_user_scenario'),)
def __repr__(self):
return f'<UserProgress {self.user_id}:{self.scenario_id}>'

View File

@ -1,5 +1,6 @@
from flask import Blueprint, render_template
from flask_login import current_user
from flask import Blueprint, render_template, jsonify
from flask_login import current_user, login_required
from app.models import Scenario, ScenarioCategory, VoiceSample
main_bp = Blueprint('main', __name__)
@ -9,9 +10,40 @@ def index():
return render_template('index.html')
@main_bp.route('/dashboard')
@login_required
def dashboard():
"""用户主页(需要登录)"""
if current_user.is_authenticated:
return render_template('dashboard.html')
else:
return render_template('index.html')
# 获取推荐场景
recommended_scenarios = Scenario.query.filter_by(is_active=True).limit(6).all()
# 获取用户语音样本状态
user_voice_sample = current_user.get_latest_voice_sample() if current_user.is_authenticated else None
return render_template('dashboard.html',
scenarios=recommended_scenarios,
voice_sample=user_voice_sample)
@main_bp.route('/api/dashboard/stats')
@login_required
def get_dashboard_stats():
"""获取用户Dashboard统计数据"""
try:
# 这里可以添加真实的统计查询
# 暂时返回模拟数据
stats = {
'learning_time': 0, # 学习时长(分钟)
'conversation_count': 0, # 对话次数
'stars_earned': 0, # 获得星星
'scenarios_completed': 0, # 完成场景
}
return jsonify({
'success': True,
'stats': stats
})
except Exception as e:
return jsonify({
'success': False,
'message': f'获取统计数据失败: {str(e)}'
})

166
app/routes/voice_clone.py Normal file
View File

@ -0,0 +1,166 @@
"""
语音克隆功能路由 - 专门处理用户声音克隆
"""
import os
import tempfile
import uuid
from flask import Blueprint, request, jsonify, render_template, current_app
from flask_login import login_required, current_user
from app.services.cosyvoice_service import cosyvoice_service
from app.models import VoiceSample, db
from werkzeug.utils import secure_filename
import logging
logger = logging.getLogger(__name__)
voice_clone_bp = Blueprint('voice_clone', __name__)
@voice_clone_bp.route('/voice-clone')
@login_required
def voice_clone_page():
"""语音克隆专门页面"""
user_voice_sample = current_user.get_latest_voice_sample()
return render_template('voice_clone/index.html', voice_sample=user_voice_sample)
@voice_clone_bp.route('/api/voice-clone/upload', methods=['POST'])
@login_required
def upload_voice_sample():
"""上传用户语音样本进行克隆"""
try:
if 'audio' not in request.files:
return jsonify({
"success": False,
"message": "请选择音频文件"
})
file = request.files['audio']
if file.filename == '':
return jsonify({
"success": False,
"message": "请选择音频文件"
})
# 生成唯一文件名
unique_id = str(uuid.uuid4())[:8]
filename = f"voice_sample_{current_user.id}_{unique_id}.wav"
# 保存到临时目录
temp_dir = tempfile.gettempdir()
file_path = os.path.join(temp_dir, filename)
file.save(file_path)
# 识别语音内容
recognized_text = cosyvoice_service.recognize_audio(file_path)
# 保存到数据库
voice_sample = VoiceSample(
user_id=current_user.id,
original_audio_url=file_path,
file_size=os.path.getsize(file_path),
recognized_text=recognized_text,
clone_model_status='ready' # 简化流程直接标记为ready
)
db.session.add(voice_sample)
db.session.commit()
logger.info(f"用户 {current_user.id} 上传语音样本成功: {filename}")
return jsonify({
"success": True,
"message": "语音样本上传成功AI已经学会你的声音了",
"sample_id": voice_sample.id,
"recognized_text": recognized_text,
"file_info": {
"size": voice_sample.file_size,
"duration": float(voice_sample.duration) if voice_sample.duration else None
}
})
except Exception as e:
logger.error(f"语音样本上传失败: {str(e)}")
return jsonify({
"success": False,
"message": f"上传失败: {str(e)}"
})
@voice_clone_bp.route('/api/voice-clone/generate', methods=['POST'])
@login_required
def generate_cloned_speech():
"""使用用户的声音克隆生成语音"""
try:
data = request.get_json()
text = data.get('text', '')
if not text:
return jsonify({
"success": False,
"message": "请输入要合成的文本"
})
# 获取用户最新的语音样本
voice_sample = current_user.get_latest_voice_sample()
if not voice_sample:
return jsonify({
"success": False,
"message": "请先录制语音样本"
})
# 使用CosyVoice进行语音克隆
stream_audio, full_audio = cosyvoice_service.generate_speech_with_voice_cloning(
text=text,
reference_audio_path=voice_sample.original_audio_url,
reference_text=voice_sample.recognized_text or "",
seed=42
)
if full_audio:
return jsonify({
"success": True,
"message": "用你的声音说话成功!",
"audio_url": full_audio,
"original_text": text
})
else:
return jsonify({
"success": False,
"message": "语音生成失败,请重试"
})
except Exception as e:
logger.error(f"语音克隆生成失败: {str(e)}")
return jsonify({
"success": False,
"message": f"生成失败: {str(e)}"
})
@voice_clone_bp.route('/api/voice-clone/status', methods=['GET'])
@login_required
def get_voice_clone_status():
"""获取用户语音克隆状态"""
try:
voice_sample = current_user.get_latest_voice_sample()
if not voice_sample:
return jsonify({
"success": True,
"has_sample": False,
"status": "no_sample",
"message": "还没有录制语音样本哦!快来录制你的专属声音吧"
})
return jsonify({
"success": True,
"has_sample": True,
"status": voice_sample.clone_model_status,
"sample_id": voice_sample.id,
"recognized_text": voice_sample.recognized_text,
"upload_time": voice_sample.upload_time.strftime("%Y-%m-%d %H:%M"),
"message": "你的专属声音已准备好!"
})
except Exception as e:
logger.error(f"获取语音克隆状态失败: {str(e)}")
return jsonify({
"success": False,
"message": f"获取状态失败: {str(e)}"
})

File diff suppressed because it is too large Load Diff

334
app/static/js/dashboard.js Normal file
View File

@ -0,0 +1,334 @@
/**
* Dashboard页面 JavaScript - 增强动效版
*/
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeDashboard();
loadVoiceStatus();
bindEvents();
});
/**
* 初始化Dashboard - 增强版
*/
function initializeDashboard() {
// 添加页面加载动画类
document.body.classList.add('page-loading');
// 为所有主要元素添加渐入动画类
setTimeout(() => {
document.querySelectorAll('.welcome-card-kid, .scenario-card-kid, .voice-clone-card, .quick-action-card-kid, .achievement-card').forEach((el, index) => {
el.classList.add('fade-in-up');
el.style.animationDelay = `${index * 0.1}s`;
});
}, 100);
// 移除加载状态
setTimeout(() => {
document.body.classList.remove('page-loading');
}, 2000);
// 添加页面加载动画
animateCards();
// 初始化成就数据
animateAchievements();
}
/**
* 绑定事件
*/
function bindEvents() {
// 场景卡片点击事件
document.querySelectorAll('[data-scenario]').forEach(btn => {
btn.addEventListener('click', function() {
const scenarioId = this.getAttribute('data-scenario');
handleScenarioClick(scenarioId);
});
});
// 快速测试按钮
const quickTestBtn = document.getElementById('quick-test-btn');
if (quickTestBtn) {
quickTestBtn.addEventListener('click', handleQuickTest);
}
}
/**
* 卡片加载动画
*/
function animateCards() {
const cards = document.querySelectorAll('.scenario-card-kid, .quick-action-card-kid, .achievement-card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(30px)';
setTimeout(() => {
card.style.transition = 'all 0.5s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
}
/**
* 成就数据动画
*/
function animateAchievements() {
const achievements = [
{ selector: '.time-card .achievement-number', target: 0, suffix: '' },
{ selector: '.conversation-card .achievement-number', target: 0, suffix: '' },
{ selector: '.star-card .achievement-number', target: 0, suffix: '' },
{ selector: '.medal-card .achievement-number', target: 0, suffix: '' }
];
achievements.forEach((achievement, index) => {
setTimeout(() => {
animateNumber(achievement.selector, 0, achievement.target, 1000, achievement.suffix);
}, index * 200);
});
}
/**
* 数字动画
*/
function animateNumber(selector, start, end, duration, suffix = '') {
const element = document.querySelector(selector);
if (!element) return;
const range = end - start;
const minTimer = 50;
const stepTime = Math.abs(Math.floor(duration / range));
const timer = stepTime < minTimer ? minTimer : stepTime;
const startTime = new Date().getTime();
const endTime = startTime + duration;
function run() {
const now = new Date().getTime();
const remaining = Math.max((endTime - now) / duration, 0);
const value = Math.round(end - (remaining * range));
element.textContent = value + suffix;
if (value === end) {
return;
}
setTimeout(run, timer);
}
run();
}
/**
* 加载语音状态
*/
async function loadVoiceStatus() {
try {
const response = await fetch('/voice-clone/api/voice-clone/status');
const result = await response.json();
updateVoiceStatusDisplay(result);
} catch (error) {
console.error('加载语音状态失败:', error);
updateVoiceStatusDisplay({
success: false,
has_sample: false,
message: '无法加载语音状态'
});
}
}
/**
* 更新语音状态显示
*/
function updateVoiceStatusDisplay(result) {
const display = document.getElementById('voice-status-display');
const quickTestBtn = document.getElementById('quick-test-btn');
if (result.has_sample && result.success) {
display.innerHTML = `
<h4 class="fw-bold mb-2">
<i class="fas fa-check-circle text-success me-2"></i>
你的专属声音已就绪
</h4>
<p class="mb-1">AI已经学会了你的声音${result.recognized_text}</p>
<small class="opacity-75">录制时间${result.upload_time}</small>
`;
if (quickTestBtn) {
quickTestBtn.disabled = false;
}
// 激活语音波浪动画
document.querySelectorAll('.voice-wave').forEach((wave, index) => {
wave.style.animationDelay = `${index * 0.2}s`;
wave.style.animationPlayState = 'running';
});
} else {
display.innerHTML = `
<h4 class="fw-bold mb-2">
<i class="fas fa-microphone-slash text-warning me-2"></i>
还没有录制语音样本
</h4>
<p class="mb-0">录制你的专属声音让AI学会模仿你说话</p>
`;
if (quickTestBtn) {
quickTestBtn.disabled = true;
}
}
}
/**
* 处理场景点击
*/
function handleScenarioClick(scenarioId) {
// 显示动画效果
showComingSoonAnimation();
}
/**
* 快速测试语音克隆
*/
async function handleQuickTest() {
const btn = document.getElementById('quick-test-btn');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>测试中...';
try {
const response = await fetch('/voice-clone/api/voice-clone/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: '你好,这是我的专属声音测试!'
})
});
const result = await response.json();
if (result.success) {
showSuccess('快速测试成功!播放你的专属声音');
// 播放生成的音频
const audio = new Audio();
const filename = result.audio_url.split('/').pop();
audio.src = `/voice-test/download-audio/${filename}`;
audio.play().catch(e => console.error('播放失败:', e));
} else {
showError(result.message || '测试失败');
}
} catch (error) {
showError('测试失败,请检查网络连接');
console.error('快速测试失败:', error);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
/**
* 显示即将上线动画
*/
function showComingSoonAnimation() {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0" style="border-radius: 20px;">
<div class="modal-body text-center py-5">
<div class="mb-4">
<i class="fas fa-rocket" style="font-size: 4rem; color: #4ECDC4; animation: bounce 1s ease-in-out infinite;"></i>
</div>
<h3 class="fw-bold text-primary mb-3">敬请期待</h3>
<p class="text-muted mb-4">这个超棒的功能正在努力开发中<br>很快就能和小朋友们见面啦</p>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" style="border-radius: 25px; padding: 0.8rem 2rem;">
<i class="fas fa-heart me-2"></i>
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// 自动清理
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
}
/**
* 显示成功消息
*/
function showSuccess(message) {
showToast(message, 'success');
}
/**
* 显示错误消息
*/
function showError(message) {
showToast(message, 'danger');
}
/**
* 显示Toast消息
*/
function showToast(message, type = 'info') {
const colors = {
'success': 'bg-success',
'danger': 'bg-danger',
'warning': 'bg-warning',
'info': 'bg-info'
};
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white ${colors[type]} border-0 position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-check-circle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 3000);
}
// 添加鼠标交互效果
document.addEventListener('mousemove', function(e) {
// 鼠标跟随效果(轻微)
const shapes = document.querySelectorAll('.shape');
shapes.forEach((shape, index) => {
const speed = (index + 1) * 0.01;
const x = e.clientX * speed;
const y = e.clientY * speed;
shape.style.transform += ` translate(${x}px, ${y}px)`;
});
});

View File

@ -0,0 +1,374 @@
/**
* 语音克隆页面 JavaScript
*/
// 全局变量
let loadingModal = null;
let mediaRecorder = null;
let audioChunks = [];
let recordedBlob = null;
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeComponents();
bindEvents();
loadVoiceStatus();
});
/**
* 初始化组件
*/
function initializeComponents() {
loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
}
/**
* 绑定事件
*/
function bindEvents() {
// 文件选择
document.getElementById('voice-file').addEventListener('change', handleFileSelect);
// 录音控制
document.getElementById('start-record').addEventListener('click', startRecording);
document.getElementById('stop-record').addEventListener('click', stopRecording);
// 表单提交
document.getElementById('voice-upload-form').addEventListener('submit', uploadVoiceSample);
document.getElementById('speech-generate-form').addEventListener('submit', generateSpeech);
}
/**
* 显示加载状态
*/
function showLoading(message = '正在处理中...', detail = '请稍候...') {
document.getElementById('loading-message').textContent = message;
document.getElementById('loading-detail').textContent = detail;
loadingModal.show();
}
/**
* 隐藏加载状态
*/
function hideLoading() {
loadingModal.hide();
}
/**
* 显示成功消息
*/
function showSuccess(message) {
showToast(message, 'success');
}
/**
* 显示错误消息
*/
function showError(message) {
showToast(message, 'danger');
}
/**
* 显示Toast消息
*/
function showToast(message, type = 'info') {
const colors = {
'success': 'bg-success',
'danger': 'bg-danger',
'warning': 'bg-warning',
'info': 'bg-info'
};
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white ${colors[type]} border-0 position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-check-circle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 3000);
}
/**
* 加载语音状态
*/
async function loadVoiceStatus() {
try {
const response = await fetch('/voice-clone/api/voice-clone/status');
const result = await response.json();
updateStatusCard(result);
if (result.has_sample) {
// 启用语音生成功能
document.getElementById('generate-btn').disabled = false;
}
} catch (error) {
console.error('加载语音状态失败:', error);
updateStatusCard({
success: false,
message: '无法加载语音状态'
});
}
}
/**
* 更新状态卡片
*/
function updateStatusCard(result) {
const content = document.getElementById('voice-status-content');
if (result.has_sample) {
content.innerHTML = `
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="fw-bold text-success mb-2">
<i class="fas fa-check-circle me-2"></i>
</h5>
<p class="text-muted mb-1">AI识别内容${result.recognized_text}</p>
<small class="text-muted">录制时间${result.upload_time}</small>
</div>
<div class="col-md-4 text-end">
<div class="d-flex align-items-center justify-content-end">
<div class="icon-bubble me-3" style="background: linear-gradient(45deg, #28a745, #20c997); width: 60px; height: 60px;">
<i class="fas fa-microphone text-white"></i>
</div>
<div>
<span class="badge bg-success">已就绪</span>
</div>
</div>
</div>
</div>
`;
} else {
content.innerHTML = `
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="fw-bold text-warning mb-2">
<i class="fas fa-exclamation-triangle me-2"></i>
</h5>
<p class="text-muted mb-0">快来录制你的专属声音让AI学会模仿你说话吧</p>
</div>
<div class="col-md-4 text-end">
<div class="icon-bubble" style="background: linear-gradient(45deg, #ffc107, #ffca2c); width: 60px; height: 60px;">
<i class="fas fa-microphone-slash text-white"></i>
</div>
</div>
</div>
`;
}
}
/**
* 处理文件选择
*/
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
recordedBlob = null; // 重置录音数据
document.getElementById('upload-btn').disabled = false;
// 显示文件预览
const audioUrl = URL.createObjectURL(file);
showAudioPreview(audioUrl);
}
}
/**
* 开始录音
*/
async function startRecording() {
try {
// 重置文件选择
document.getElementById('voice-file').value = '';
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
mediaRecorder.ondataavailable = function(event) {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = function() {
recordedBlob = new Blob(audioChunks, { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(recordedBlob);
showAudioPreview(audioUrl);
// 启用上传按钮
document.getElementById('upload-btn').disabled = false;
};
mediaRecorder.start(100);
// 更新UI
document.getElementById('start-record').disabled = true;
document.getElementById('stop-record').disabled = false;
document.getElementById('record-status').textContent = '正在录音...';
document.getElementById('record-status').className = 'text-danger';
} catch (error) {
showError('录音失败,请检查麦克风权限');
console.error('录音失败:', error);
}
}
/**
* 停止录音
*/
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
// 更新UI
document.getElementById('start-record').disabled = false;
document.getElementById('stop-record').disabled = true;
document.getElementById('record-status').textContent = '录音完成';
document.getElementById('record-status').className = 'text-success';
}
/**
* 显示音频预览
*/
function showAudioPreview(audioUrl) {
const previewAudio = document.getElementById('preview-audio');
const previewSource = document.getElementById('preview-source');
previewSource.src = audioUrl;
previewAudio.load();
document.getElementById('audio-preview').style.display = 'block';
}
/**
* 上传语音样本
*/
async function uploadVoiceSample(e) {
e.preventDefault();
const fileInput = document.getElementById('voice-file');
const file = fileInput.files[0];
if (!file && !recordedBlob) {
showError('请选择音频文件或先录音');
return;
}
showLoading('正在上传和训练你的声音...', '这可能需要几秒钟时间');
try {
const formData = new FormData();
if (file) {
formData.append('audio', file);
} else if (recordedBlob) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
formData.append('audio', recordedBlob, `recording_${timestamp}.webm`);
}
const response = await fetch('/voice-clone/api/voice-clone/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
loadVoiceStatus(); // 重新加载状态
// 清空表单
document.getElementById('voice-upload-form').reset();
document.getElementById('audio-preview').style.display = 'none';
document.getElementById('upload-btn').disabled = true;
} else {
showError(result.message);
}
} catch (error) {
showError('上传失败,请检查网络连接');
console.error('上传失败:', error);
} finally {
hideLoading();
}
}
/**
* 生成语音
*/
async function generateSpeech(e) {
e.preventDefault();
const text = document.getElementById('speech-text').value.trim();
if (!text) {
showError('请输入要合成的文本');
return;
}
showLoading('正在用你的声音生成语音...', '请耐心等待,这很神奇哦');
try {
const response = await fetch('/voice-clone/api/voice-clone/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: text
})
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
// 显示生成的音频
const generatedAudio = document.getElementById('generated-audio');
const generatedSource = document.getElementById('generated-source');
// 创建音频URL
const filename = result.audio_url.split('/').pop();
generatedSource.src = `/voice-test/download-audio/${filename}`;
generatedAudio.load();
document.getElementById('speech-result').style.display = 'block';
} else {
showError(result.message);
}
} catch (error) {
showError('生成失败,请检查网络连接');
console.error('生成失败:', error);
} finally {
hideLoading();
}
}

View File

@ -3,285 +3,414 @@
{% block title %}学习主页 - 儿童语言学习系统{% endblock %}
{% block content %}
<div class="container py-4">
<!-- 欢迎区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="welcome-card bg-gradient-primary text-white rounded-4 p-4">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="fw-bold mb-2">
<i class="fas fa-sun me-2"></i>
早安,{{ current_user.name }}小朋友!
</h2>
<p class="mb-0 opacity-90">
准备好今天的语言学习之旅了吗?让我们一起探索有趣的世界吧!
</p>
<div class="dashboard-container">
<!-- 彩虹背景装饰 -->
<div class="rainbow-bg"></div>
<div class="container py-4">
<!-- 欢迎区域 - 更活泼的设计 -->
<div class="row mb-4">
<div class="col-12">
<div class="welcome-card-kid">
<div class="floating-shapes">
<div class="shape shape-1">🌟</div>
<div class="shape shape-2">🎈</div>
<div class="shape shape-3">🎊</div>
<div class="shape shape-4"></div>
</div>
<div class="col-md-4 text-end">
<i class="fas fa-rocket" style="font-size: 4rem; opacity: 0.3;"></i>
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="welcome-title">
<span class="wave">👋</span>
早安,<span class="highlight">{{ current_user.name }}</span>小朋友!
</h1>
<p class="welcome-subtitle">
🚀 准备好今天的语言学习冒险了吗?让我们一起探索神奇的语言世界吧!
</p>
</div>
<div class="col-md-4 text-end">
<div class="welcome-mascot">
<i class="fas fa-robot"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快速开始区域 -->
<div class="row mb-4">
<div class="col-12">
<h3 class="fw-bold mb-3">
<i class="fas fa-play-circle text-primary me-2"></i>快速开始
</h3>
<!-- 推荐场景区域 - 前置并增加更多场景 -->
<div class="row mb-4">
<div class="col-12">
<div class="section-header">
<h2 class="section-title">
🌈 今日推荐场景
<span class="badge bg-gradient-fun">超人气</span>
</h2>
<p class="section-subtitle">选择你喜欢的场景,开始有趣的对话学习!</p>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6 col-lg-4">
<div class="quick-action-card h-100">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center p-4">
<div class="action-icon mb-3">
<i class="fas fa-vial text-info"></i>
<div class="row g-4 mb-5">
<!-- 社交互动场景 -->
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid social-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-handshake"></i>
</div>
<h5 class="fw-bold mb-2">API 测试</h5>
<p class="text-muted small mb-3">
测试CosyVoice语音合成功能体验不同的语音生成模式
</p>
<a href="{{ url_for('voice_test.voice_test_page') }}" class="btn btn-info">
<i class="fas fa-flask me-2"></i>开始测试
<span class="difficulty-badge easy">简单</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">和小兔子交朋友</h5>
<p class="scenario-desc">遇到了可爱的小兔子,快来学习如何打招呼和交朋友吧!</p>
<div class="scenario-tags">
<span class="tag tag-social">社交互动</span>
<span class="tag tag-basic">基础对话</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>168人完成</span>
<span><i class="fas fa-star me-1"></i>4.8分</span>
</div>
<button class="btn btn-scenario-play" data-scenario="1">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid daily-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-utensils"></i>
</div>
<span class="difficulty-badge medium">中等</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">美食小当家</h5>
<p class="scenario-desc">来到神奇餐厅,学会如何礼貌点餐,成为小小美食家!</p>
<div class="scenario-tags">
<span class="tag tag-daily">日常生活</span>
<span class="tag tag-polite">礼貌用语</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>142人完成</span>
<span><i class="fas fa-star me-1"></i>4.9分</span>
</div>
<button class="btn btn-scenario-play" data-scenario="2">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid fun-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-gamepad"></i>
</div>
<span class="difficulty-badge medium">中等</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">游戏时间</h5>
<p class="scenario-desc">邀请小伙伴一起玩游戏,学会分享和合作的乐趣!</p>
<div class="scenario-tags">
<span class="tag tag-fun">游戏娱乐</span>
<span class="tag tag-cooperation">团队合作</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>95人完成</span>
<span><i class="fas fa-star me-1"></i>4.7分</span>
</div>
<button class="btn btn-scenario-play" data-scenario="3">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
<!-- 新增场景 -->
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid learning-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-book-open"></i>
</div>
<span class="difficulty-badge easy">简单</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">图书馆探险</h5>
<p class="scenario-desc">在神奇图书馆里寻找宝藏,学会如何借书和爱护书籍!</p>
<div class="scenario-tags">
<span class="tag tag-learning">学习探索</span>
<span class="tag tag-reading">阅读习惯</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>73人完成</span>
<span><i class="fas fa-star me-1"></i>4.6分</span>
</div>
<button class="btn btn-scenario-play" data-scenario="4">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid nature-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-leaf"></i>
</div>
<span class="difficulty-badge fun">有趣</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">大自然课堂</h5>
<p class="scenario-desc">走进森林,认识小动物们,学会保护环境的重要性!</p>
<div class="scenario-tags">
<span class="tag tag-nature">自然探索</span>
<span class="tag tag-environment">环保意识</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>89人完成</span>
<span><i class="fas fa-star me-1"></i>4.8分</span>
</div>
<button class="btn btn-scenario-play" data-scenario="5">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-card-kid creative-card">
<div class="scenario-header">
<div class="scenario-icon-big">
<i class="fas fa-palette"></i>
</div>
<span class="difficulty-badge fun">有趣</span>
</div>
<div class="scenario-content">
<h5 class="scenario-title">小小艺术家</h5>
<p class="scenario-desc">在彩虹画室里创作美丽作品,用语言描述你的艺术创想!</p>
<div class="scenario-tags">
<span class="tag tag-creative">创意表达</span>
<span class="tag tag-art">艺术启蒙</span>
</div>
</div>
<div class="scenario-footer">
<div class="scenario-stats">
<span><i class="fas fa-users me-1"></i>56人完成</span>
<span class="tag tag-new">新场景</span>
</div>
<button class="btn btn-scenario-play" data-scenario="6">
<i class="fas fa-play me-2"></i>开始冒险
</button>
</div>
</div>
</div>
</div>
<!-- 我的专属声音区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="section-header">
<h2 class="section-title">
🎤 我的专属声音
<span class="badge bg-gradient-voice">AI魔法</span>
</h2>
<p class="section-subtitle">让AI学会你的声音创造专属于你的语音助手</p>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-8">
<div class="voice-clone-card">
<div class="voice-clone-content">
<div class="row align-items-center">
<div class="col-md-8">
<div id="voice-status-display">
<!-- 动态加载状态 -->
</div>
</div>
<div class="col-md-4 text-center">
<div class="voice-clone-visual">
<div class="voice-wave-container">
<div class="voice-wave"></div>
<div class="voice-wave"></div>
<div class="voice-wave"></div>
<div class="voice-wave"></div>
</div>
</div>
</div>
</div>
</div>
<div class="voice-clone-actions">
<a href="{{ url_for('voice_clone.voice_clone_page') }}" class="btn btn-voice-main">
<i class="fas fa-microphone me-2"></i>录制我的声音
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="quick-action-card h-100">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center p-4">
<div class="action-icon mb-3">
<i class="fas fa-microphone text-primary"></i>
</div>
<h5 class="fw-bold mb-2">录制声音</h5>
<p class="text-muted small mb-3">
上传你的声音样本让AI学会说话像你一样
</p>
<button class="btn btn-primary" disabled>
<i class="fas fa-upload me-2"></i>开始录制
<button class="btn btn-voice-secondary" id="quick-test-btn" disabled>
<i class="fas fa-magic me-2"></i>快速测试
</button>
<div class="mt-2">
<small class="text-muted">即将上线</small>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="voice-tips-card">
<h6 class="tips-title">💡 录音小贴士</h6>
<ul class="tips-list">
<li><i class="fas fa-check text-success me-2"></i>在安静的地方录音</li>
<li><i class="fas fa-check text-success me-2"></i>说话要清晰洪亮</li>
<li><i class="fas fa-check text-success me-2"></i>录制3-10秒即可</li>
<li><i class="fas fa-check text-success me-2"></i>用自然的语调</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="quick-action-card h-100">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center p-4">
<div class="action-icon mb-3">
<i class="fas fa-comments text-success"></i>
</div>
<h5 class="fw-bold mb-2">开始对话</h5>
<p class="text-muted small mb-3">
选择有趣的场景和AI朋友一起聊天学习
</p>
<button class="btn btn-success" disabled>
<i class="fas fa-play me-2"></i>选择场景
</button>
<div class="mt-2">
<small class="text-muted">即将上线</small>
</div>
</div>
<!-- 快速开始区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="section-header">
<h2 class="section-title">
⚡ 快速开始
</h2>
<p class="section-subtitle">选择你想要的功能,立即开始学习冒险!</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="quick-action-card h-100">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center p-4">
<div class="action-icon mb-3">
<i class="fas fa-chart-line text-warning"></i>
</div>
<h5 class="fw-bold mb-2">查看进度</h5>
<p class="text-muted small mb-3">
了解你的学习情况,看看哪里可以做得更好
</p>
<button class="btn btn-warning" disabled>
<i class="fas fa-eye me-2"></i>查看报告
</button>
<div class="mt-2">
<small class="text-muted">即将上线</small>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6 col-lg-3">
<div class="quick-action-card-kid test-card">
<div class="action-icon-container">
<i class="fas fa-flask"></i>
</div>
<h6 class="action-title">API 测试</h6>
<p class="action-desc">体验语音技术的神奇魔法</p>
<a href="{{ url_for('voice_test.voice_test_page') }}" class="btn btn-action-test">
<i class="fas fa-rocket me-2"></i>开始测试
</a>
</div>
</div>
</div>
</div>
<!-- 学习统计 -->
<div class="row mb-4">
<div class="col-12">
<h3 class="fw-bold mb-3">
<i class="fas fa-trophy text-warning me-2"></i>学习成就
</h3>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="quick-action-card-kid record-card">
<div class="action-icon-container">
<i class="fas fa-microphone"></i>
</div>
<h6 class="action-title">录制声音</h6>
<p class="action-desc">让AI学会你的声音</p>
<a href="{{ url_for('voice_clone.voice_clone_page') }}" class="btn btn-action-record">
<i class="fas fa-upload me-2"></i>开始录制
</a>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-6 col-md-3">
<div class="stat-card text-center">
<div class="card border-0 bg-light">
<div class="card-body p-3">
<i class="fas fa-clock text-primary mb-2" style="font-size: 2rem;"></i>
<h4 class="fw-bold mb-0">0</h4>
<small class="text-muted">学习时长(分钟)</small>
<div class="col-md-6 col-lg-3">
<div class="quick-action-card-kid chat-card">
<div class="action-icon-container">
<i class="fas fa-comments"></i>
</div>
<h6 class="action-title">开始对话</h6>
<p class="action-desc">和AI朋友聊天学习</p>
<button class="btn btn-action-chat" disabled>
<i class="fas fa-play me-2"></i>选择场景
</button>
<div class="coming-soon">即将上线</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card text-center">
<div class="card border-0 bg-light">
<div class="card-body p-3">
<i class="fas fa-comments text-success mb-2" style="font-size: 2rem;"></i>
<h4 class="fw-bold mb-0">0</h4>
<small class="text-muted">对话次数</small>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card text-center">
<div class="card border-0 bg-light">
<div class="card-body p-3">
<i class="fas fa-star text-warning mb-2" style="font-size: 2rem;"></i>
<h4 class="fw-bold mb-0">0</h4>
<small class="text-muted">获得星星</small>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card text-center">
<div class="card border-0 bg-light">
<div class="card-body p-3">
<i class="fas fa-medal text-info mb-2" style="font-size: 2rem;"></i>
<h4 class="fw-bold mb-0">0</h4>
<small class="text-muted">完成场景</small>
</div>
</div>
</div>
</div>
</div>
<!-- 推荐场景预览 -->
<div class="row mb-4">
<div class="col-12">
<h3 class="fw-bold mb-3">
<i class="fas fa-sparkles text-info me-2"></i>推荐场景
</h3>
</div>
</div>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<div class="scenario-preview-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex align-items-start mb-3">
<div class="scenario-icon me-3">
<i class="fas fa-handshake text-primary"></i>
</div>
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">和小明交朋友</h6>
<small class="text-muted">社交互动</small>
</div>
<span class="badge bg-primary">简单</span>
</div>
<p class="text-muted small mb-3">
学习如何与新朋友进行自我介绍和基础交流
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="scenario-stats">
<small class="text-muted">
<i class="fas fa-users me-1"></i>0人完成
</small>
</div>
<button class="btn btn-outline-primary btn-sm" disabled>
即将开放
</button>
</div>
<div class="col-md-6 col-lg-3">
<div class="quick-action-card-kid progress-card">
<div class="action-icon-container">
<i class="fas fa-chart-line"></i>
</div>
<h6 class="action-title">学习报告</h6>
<p class="action-desc">查看你的学习成果</p>
<button class="btn btn-action-progress" disabled>
<i class="fas fa-eye me-2"></i>查看报告
</button>
<div class="coming-soon">即将上线</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-preview-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex align-items-start mb-3">
<div class="scenario-icon me-3">
<i class="fas fa-utensils text-success"></i>
</div>
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">餐厅点餐</h6>
<small class="text-muted">日常生活</small>
</div>
<span class="badge bg-warning">中等</span>
</div>
<p class="text-muted small mb-3">
学习在餐厅如何礼貌地点餐和与服务员交流
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="scenario-stats">
<small class="text-muted">
<i class="fas fa-users me-1"></i>0人完成
</small>
</div>
<button class="btn btn-outline-success btn-sm" disabled>
即将开放
</button>
</div>
</div>
<!-- 学习成就区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="section-header">
<h2 class="section-title">
🏆 我的学习成就
</h2>
<p class="section-subtitle">看看你在语言学习路上取得的进步!</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="scenario-preview-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex align-items-start mb-3">
<div class="scenario-icon me-3">
<i class="fas fa-gamepad text-warning"></i>
</div>
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">邀请朋友玩游戏</h6>
<small class="text-muted">游戏娱乐</small>
</div>
<span class="badge bg-warning">中等</span>
</div>
<p class="text-muted small mb-3">
学习如何邀请朋友一起玩游戏并协调活动
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="scenario-stats">
<small class="text-muted">
<i class="fas fa-users me-1"></i>0人完成
</small>
</div>
<button class="btn btn-outline-warning btn-sm" disabled>
即将开放
</button>
</div>
<div class="row g-4">
<div class="col-6 col-lg-3">
<div class="achievement-card time-card">
<div class="achievement-icon">
<i class="fas fa-clock"></i>
</div>
<div class="achievement-number">0</div>
<div class="achievement-label">学习时长(分钟)</div>
<div class="achievement-progress">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="achievement-card conversation-card">
<div class="achievement-icon">
<i class="fas fa-comments"></i>
</div>
<div class="achievement-number">0</div>
<div class="achievement-label">对话次数</div>
<div class="achievement-progress">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="achievement-card star-card">
<div class="achievement-icon">
<i class="fas fa-star"></i>
</div>
<div class="achievement-number">0</div>
<div class="achievement-label">获得星星</div>
<div class="achievement-progress">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="achievement-card medal-card">
<div class="achievement-icon">
<i class="fas fa-medal"></i>
</div>
<div class="achievement-number">0</div>
<div class="achievement-label">完成场景</div>
<div class="achievement-progress">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
@ -289,3 +418,7 @@
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,186 @@
{% extends "base.html" %}
{% block title %}我的专属声音 - 儿童语言学习系统{% endblock %}
{% block content %}
<div class="container py-4">
<!-- 页面标题 -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex align-items-center">
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary me-3">
<i class="fas fa-arrow-left me-2"></i>返回主页
</a>
<div>
<h2 class="fw-bold mb-1 text-kid-primary">
🎤 我的专属声音
</h2>
<p class="text-muted mb-0">让AI学会你的声音创造专属于你的语音助手</p>
</div>
</div>
</div>
</div>
<!-- 状态卡片 -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm" id="status-card">
<div class="card-body p-4">
<div id="voice-status-content">
<!-- 动态加载状态内容 -->
</div>
</div>
</div>
</div>
</div>
<!-- 录制区域 -->
<div class="row mb-4">
<div class="col-lg-6">
<div class="card border-0 shadow-sm kid-feature-card">
<div class="card-body p-4">
<h5 class="fw-bold text-kid-accent mb-3">
<i class="fas fa-microphone me-2"></i>录制你的声音
</h5>
<form id="voice-upload-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">选择音频文件:</label>
<input type="file" class="form-control" id="voice-file"
accept=".wav,.mp3,.m4a" style="border-radius: 15px;">
<div class="form-text">建议清晰朗读3-10秒说一句完整的话</div>
</div>
<div class="text-center mb-3">
<span class="text-muted">或者</span>
</div>
<div class="text-center mb-3">
<button type="button" id="start-record" class="btn btn-kid-primary me-2">
<i class="fas fa-record-vinyl me-2"></i>开始录音
</button>
<button type="button" id="stop-record" class="btn btn-secondary" disabled>
<i class="fas fa-stop me-2"></i>停止录音
</button>
<div class="mt-2">
<small id="record-status" class="text-muted">点击开始录音按钮</small>
</div>
</div>
<button type="submit" class="btn btn-kid-accent w-100" disabled id="upload-btn">
<i class="fas fa-upload me-2"></i>上传并训练我的声音
</button>
</form>
<!-- 音频预览 -->
<div id="audio-preview" class="mt-3" style="display: none;">
<div class="alert alert-info">
<h6><i class="fas fa-headphones me-2"></i>预览录音:</h6>
<audio controls class="w-100 mt-2" id="preview-audio">
<source id="preview-source" type="audio/wav">
</audio>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card border-0 shadow-sm kid-feature-card">
<div class="card-body p-4">
<h5 class="fw-bold text-kid-secondary mb-3">
<i class="fas fa-magic me-2"></i>用我的声音说话
</h5>
<form id="speech-generate-form">
<div class="mb-3">
<label class="form-label">想说什么?</label>
<textarea class="form-control" id="speech-text" rows="4"
placeholder="输入你想让AI用你的声音说的话..."
style="border-radius: 15px;">你好,这是我的专属声音!听起来是不是很像我呢?</textarea>
</div>
<button type="submit" class="btn btn-kid-secondary w-100" disabled id="generate-btn">
<i class="fas fa-magic me-2"></i>生成我的声音
</button>
</form>
<!-- 生成结果 -->
<div id="speech-result" class="mt-3" style="display: none;">
<div class="alert alert-success">
<h6><i class="fas fa-volume-up me-2"></i>生成的语音:</h6>
<audio controls class="w-100 mt-2" id="generated-audio">
<source id="generated-source" type="audio/wav">
</audio>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 使用说明 -->
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-bold text-center mb-4">
<i class="fas fa-lightbulb text-warning me-2"></i>使用小贴士
</h5>
<div class="row">
<div class="col-md-3 text-center mb-3">
<div class="icon-bubble mx-auto mb-2" style="background: linear-gradient(45deg, #FF6B6B, #FF8E53);">
<i class="fas fa-microphone text-white"></i>
</div>
<h6 class="fw-bold">清晰发音</h6>
<p class="small text-muted">说话要清楚,声音要洪亮</p>
</div>
<div class="col-md-3 text-center mb-3">
<div class="icon-bubble mx-auto mb-2" style="background: linear-gradient(45deg, #4ECDC4, #44A08D);">
<i class="fas fa-clock text-white"></i>
</div>
<h6 class="fw-bold">合适时长</h6>
<p class="small text-muted">录音3-10秒一句话就够了</p>
</div>
<div class="col-md-3 text-center mb-3">
<div class="icon-bubble mx-auto mb-2" style="background: linear-gradient(45deg, #A8E6CF, #DCEDC8);">
<i class="fas fa-volume-up text-white"></i>
</div>
<h6 class="fw-bold">安静环境</h6>
<p class="small text-muted">在安静的地方录音效果更好</p>
</div>
<div class="col-md-3 text-center mb-3">
<div class="icon-bubble mx-auto mb-2" style="background: linear-gradient(45deg, #FFB347, #FFCC02);">
<i class="fas fa-smile text-white"></i>
</div>
<h6 class="fw-bold">自然语调</h6>
<p class="small text-muted">用你平时说话的语调</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Modal -->
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body text-center py-4">
<div class="spinner-border text-primary mb-3" role="status"></div>
<h5 id="loading-message">正在处理中...</h5>
<p id="loading-detail" class="text-muted">请稍候...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/voice_clone.js') }}"></script>
{% endblock %}