diff --git a/app/__init__.py b/app/__init__.py index a40fd9b..eecc5d1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/models/__init__.py b/app/models/__init__.py index 799da17..15a3eca 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -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'' @@ -83,3 +97,125 @@ class EmailVerification(db.Model): def __repr__(self): return f'' + +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'' + +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'' + +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'' + +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'' + +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'' + +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'' diff --git a/app/routes/main.py b/app/routes/main.py index df983ce..37fe03e 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -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)}' + }) diff --git a/app/routes/voice_clone.py b/app/routes/voice_clone.py new file mode 100644 index 0000000..2ba009e --- /dev/null +++ b/app/routes/voice_clone.py @@ -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)}" + }) diff --git a/app/static/css/style.css b/app/static/css/style.css index 4669ecd..150bc38 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -16,6 +16,23 @@ --kid-accent: #FF5722; /* Deep Orange */ --kid-bg: #F0F8FF; /* AliceBlue */ --kid-text: #5D4037; /* Brown */ + + /* === Dashboard鲜艳颜色 - 蓝绿活力版 === */ + --rainbow-red: #FF6B6B; + --rainbow-orange: #FFB347; + --rainbow-yellow: #FFD93D; + --rainbow-green: #6BCF7F; + --rainbow-blue: #4D96FF; + --rainbow-purple: #9B59B6; + --rainbow-pink: #FF69B4; + + /* 场景主题色 */ + --social-color: #FF6B6B; + --daily-color: #4ECDC4; + --fun-color: #FFD93D; + --learning-color: #9B59B6; + --nature-color: #6BCF7F; + --creative-color: #FF69B4; } body { @@ -133,6 +150,746 @@ main { } .kid-auth-card a:hover { color: var(--kid-accent); } +/* === Dashboard容器 - 蓝绿活力版 === */ +.dashboard-container { + min-height: 100vh; + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + background-attachment: fixed; + position: relative; + overflow-x: hidden; +} + +/* 彩虹背景装饰 - 增强版 */ +.rainbow-bg { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 15% 25%, rgba(0, 188, 212, 0.15) 12%, transparent 25%), + radial-gradient(circle at 85% 15%, rgba(76, 175, 80, 0.15) 18%, transparent 30%), + radial-gradient(circle at 45% 75%, rgba(33, 150, 243, 0.12) 22%, transparent 35%), + radial-gradient(circle at 90% 85%, rgba(0, 150, 136, 0.18) 28%, transparent 40%), + radial-gradient(circle at 20% 80%, rgba(102, 187, 106, 0.15) 15%, transparent 28%); + z-index: -1; + animation: backgroundShift 25s ease-in-out infinite, backgroundPulse 8s ease-in-out infinite; +} + +@keyframes backgroundShift { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.8; } +} + +@keyframes backgroundPulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* 欢迎卡片 - 更活泼 */ +.welcome-card-kid { + background: linear-gradient(135deg, var(--rainbow-pink), var(--rainbow-purple)); + border-radius: 25px; + padding: 2rem; + color: white; + position: relative; + overflow: hidden; + box-shadow: 0 15px 40px rgba(0,0,0,0.15); +} + +/* 漂浮装饰形状 - 增强版 */ +.floating-shapes { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +.shape { + position: absolute; + font-size: 1.5rem; + animation: enhancedFloat 8s ease-in-out infinite; +} + +.shape-1 { top: 15%; left: 10%; animation-delay: 0s; } +.shape-2 { top: 20%; right: 15%; animation-delay: 2s; } +.shape-3 { bottom: 25%; left: 20%; animation-delay: 4s; } +.shape-4 { bottom: 15%; right: 10%; animation-delay: 6s; } + +@keyframes enhancedFloat { + 0%, 100% { + transform: translateY(0px) scale(1) rotate(0deg); + opacity: 0.8; + } + 25% { + transform: translateY(-20px) scale(1.1) rotate(5deg); + opacity: 1; + } + 50% { + transform: translateY(-10px) scale(0.9) rotate(-3deg); + opacity: 0.9; + } + 75% { + transform: translateY(-25px) scale(1.05) rotate(8deg); + opacity: 1; + } +} + +.welcome-title { + font-size: 2.5rem; + font-weight: 900; + margin-bottom: 1rem; + text-shadow: 2px 2px 10px rgba(0,0,0,0.3); + position: relative; + overflow: hidden; +} + +.welcome-title::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + animation: titleShine 6s ease-in-out infinite; +} + +@keyframes titleShine { + 0% { left: -100%; } + 20%, 80% { left: 100%; } + 100% { left: 100%; } +} + +.welcome-title .wave { + display: inline-block; + animation: wave 2s ease-in-out infinite; +} + +@keyframes wave { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(20deg); } + 75% { transform: rotate(-10deg); } +} + +.welcome-title .highlight { + background: linear-gradient(45deg, #FFD700, #FFA500); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.welcome-subtitle { + font-size: 1.2rem; + opacity: 0.95; + line-height: 1.6; +} + +.welcome-mascot { + text-align: center; +} + +.welcome-mascot i { + font-size: 5rem; + color: rgba(255,255,255,0.3); + animation: bounce 3s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +/* 区域标题 */ +.section-header { + text-align: center; + margin-bottom: 2rem; +} + +.section-title { + font-size: 2.2rem; + font-weight: 900; + color: white; + margin-bottom: 0.5rem; + text-shadow: 2px 2px 8px rgba(0,0,0,0.3); + position: relative; + overflow: hidden; +} + +.section-title::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + animation: titleShine 6s ease-in-out infinite; +} + +.section-subtitle { + font-size: 1.1rem; + color: rgba(255,255,255,0.9); + margin-bottom: 0; +} + +/* 徽章样式 - 增强版 */ +.bg-gradient-fun { + background: linear-gradient(45deg, var(--rainbow-orange), var(--rainbow-yellow)) !important; + animation: badgeGlow 3s ease-in-out infinite; +} + +.bg-gradient-voice { + background: linear-gradient(45deg, var(--rainbow-blue), var(--rainbow-purple)) !important; + animation: badgeGlow 3s ease-in-out infinite; +} + +@keyframes badgeGlow { + 0%, 100% { box-shadow: 0 0 10px rgba(255,193,7,0.3); } + 50% { box-shadow: 0 0 20px rgba(255,193,7,0.6); } +} + +/* 场景卡片 - 全新设计增强版 */ +.scenario-card-kid { + background: white; + border-radius: 20px; + padding: 0; + overflow: hidden; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + transition: all 0.3s ease; + height: 100%; + animation: cardBreathe 4s ease-in-out infinite; +} + +.scenario-card-kid:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 20px 50px rgba(0,0,0,0.2); +} + +@keyframes cardBreathe { + 0%, 100% { box-shadow: 0 10px 30px rgba(0,0,0,0.1); } + 50% { box-shadow: 0 15px 40px rgba(0,0,0,0.15); } +} + +.scenario-header { + padding: 1.5rem; + position: relative; +} + +.social-card .scenario-header { background: linear-gradient(135deg, var(--social-color), #FF8E53); } +.daily-card .scenario-header { background: linear-gradient(135deg, var(--daily-color), #44A08D); } +.fun-card .scenario-header { background: linear-gradient(135deg, var(--fun-color), #FFCC02); } +.learning-card .scenario-header { background: linear-gradient(135deg, var(--learning-color), #8E44AD); } +.nature-card .scenario-header { background: linear-gradient(135deg, var(--nature-color), #4CAF50); } +.creative-card .scenario-header { background: linear-gradient(135deg, var(--creative-color), #E91E63); } + +.scenario-icon-big { + width: 60px; + height: 60px; + background: rgba(255,255,255,0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; +} + +.scenario-icon-big i { + font-size: 2rem; + color: white; + transition: transform 0.3s ease; +} + +.scenario-card-kid:hover .scenario-icon-big i { + transform: rotate(360deg) scale(1.1); +} + +.difficulty-badge { + position: absolute; + top: 1rem; + right: 1rem; + padding: 0.3rem 0.8rem; + border-radius: 15px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.difficulty-badge.easy { background: #28a745; color: white; } +.difficulty-badge.medium { background: #ffc107; color: #333; } +.difficulty-badge.fun { background: #17a2b8; color: white; } + +.scenario-content { + padding: 1.5rem; +} + +.scenario-title { + font-size: 1.3rem; + font-weight: 800; + color: var(--kid-text); + margin-bottom: 1rem; +} + +.scenario-desc { + color: #666; + font-size: 0.95rem; + line-height: 1.5; + margin-bottom: 1rem; +} + +.scenario-tags { + margin-bottom: 1rem; +} + +.tag { + display: inline-block; + padding: 0.3rem 0.8rem; + border-radius: 15px; + font-size: 0.75rem; + font-weight: 600; + margin: 0.2rem; +} + +.tag-social { background: #FFE5E5; color: var(--social-color); } +.tag-daily { background: #E0F7FA; color: var(--daily-color); } +.tag-fun { background: #FFF8E1; color: #F57F17; } +.tag-learning { background: #F3E5F5; color: var(--learning-color); } +.tag-nature { background: #E8F5E8; color: var(--nature-color); } +.tag-creative { background: #FCE4EC; color: var(--creative-color); } +.tag-basic { background: #F5F5F5; color: #666; } +.tag-polite { background: #E3F2FD; color: #1976D2; } +.tag-cooperation { background: #FFF3E0; color: #F57C00; } +.tag-reading { background: #F1F8E9; color: #689F38; } +.tag-environment { background: #E0F2F1; color: #00695C; } +.tag-art { background: #FAF0E6; color: #8D6E63; } +.tag-new { background: linear-gradient(45deg, #FF6B6B, #4ECDC4); color: white; } + +.scenario-footer { + padding: 1rem 1.5rem; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.scenario-stats { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.scenario-stats span { + font-size: 0.8rem; + color: #666; +} + +.btn-scenario-play { + background: linear-gradient(45deg, #4ECDC4, #44A08D); + border: none; + color: white; + padding: 0.5rem 1rem; + border-radius: 20px; + font-weight: 700; + font-size: 0.9rem; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-scenario-play:hover { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(0,0,0,0.2); + color: white; +} + +.btn-scenario-play::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.6s; +} + +.btn-scenario-play:hover::before { + left: 100%; +} + +/* 语音克隆卡片 - 增强版 */ +.voice-clone-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20px; + padding: 2rem; + color: white; + box-shadow: 0 15px 40px rgba(0,0,0,0.15); +} + +.voice-clone-visual { + text-align: center; +} + +.voice-wave-container { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + height: 60px; + animation: containerPulse 3s ease-in-out infinite; +} + +@keyframes containerPulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.voice-wave { + width: 4px; + height: 20px; + background: white; + border-radius: 2px; + animation: voiceWave 1.5s ease-in-out infinite; +} + +.voice-wave:nth-child(2) { animation-delay: 0.2s; } +.voice-wave:nth-child(3) { animation-delay: 0.4s; } +.voice-wave:nth-child(4) { animation-delay: 0.6s; } + +@keyframes voiceWave { + 0%, 100% { height: 20px; opacity: 0.3; } + 50% { height: 40px; opacity: 1; } +} + +.voice-clone-actions { + display: flex; + gap: 1rem; + margin-top: 1.5rem; +} + +.btn-voice-main { + background: linear-gradient(45deg, #FF6B6B, #4ECDC4); + border: none; + color: white; + padding: 0.8rem 1.5rem; + border-radius: 25px; + font-weight: 700; + transition: all 0.3s ease; + flex: 1; + position: relative; + overflow: hidden; +} + +.btn-voice-main:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0,0,0,0.3); + color: white; +} + +.btn-voice-main::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.6s; +} + +.btn-voice-main:hover::before { + left: 100%; +} + +.btn-voice-secondary { + background: rgba(255,255,255,0.2); + border: 2px solid rgba(255,255,255,0.3); + color: white; + padding: 0.8rem 1.5rem; + border-radius: 25px; + font-weight: 700; + transition: all 0.3s ease; +} + +.btn-voice-secondary:hover:not(:disabled) { + background: rgba(255,255,255,0.3); + color: white; +} + +/* 语音提示卡片 */ +.voice-tips-card { + background: white; + border-radius: 15px; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + height: 100%; +} + +.tips-title { + font-weight: 800; + color: var(--kid-text); + margin-bottom: 1rem; +} + +.tips-list { + list-style: none; + padding: 0; + margin: 0; +} + +.tips-list li { + padding: 0.5rem 0; + font-size: 0.9rem; + color: #666; +} + +/* 快速操作卡片 - 增强版 */ +.quick-action-card-kid { + background: white; + border-radius: 20px; + padding: 2rem; + text-align: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + transition: all 0.3s ease; + height: 100%; + position: relative; + animation: cardBreathe 4s ease-in-out infinite; +} + +.quick-action-card-kid:hover { + transform: translateY(-5px); + box-shadow: 0 20px 50px rgba(0,0,0,0.15); +} + +.action-icon-container { + width: 70px; + height: 70px; + border-radius: 50%; + margin: 0 auto 1.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + color: white; +} + +.action-icon-container i { + transition: transform 0.3s ease; +} + +.quick-action-card-kid:hover .action-icon-container i { + transform: rotate(360deg) scale(1.1); +} + +.test-card .action-icon-container { background: linear-gradient(45deg, #17a2b8, #20c997); } +.record-card .action-icon-container { background: linear-gradient(45deg, #FF6B6B, #FF8E53); } +.chat-card .action-icon-container { background: linear-gradient(45deg, #28a745, #20c997); } +.progress-card .action-icon-container { background: linear-gradient(45deg, #ffc107, #fd7e14); } + +.action-title { + font-size: 1.2rem; + font-weight: 800; + color: var(--kid-text); + margin-bottom: 0.8rem; +} + +.action-desc { + color: #666; + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +.btn-action-test { background: linear-gradient(45deg, #17a2b8, #20c997); } +.btn-action-record { background: linear-gradient(45deg, #FF6B6B, #FF8E53); } +.btn-action-chat { background: linear-gradient(45deg, #28a745, #20c997); } +.btn-action-progress { background: linear-gradient(45deg, #ffc107, #fd7e14); } + +.btn-action-test, .btn-action-record, .btn-action-chat, .btn-action-progress { + border: none; + color: white; + padding: 0.7rem 1.3rem; + border-radius: 20px; + font-weight: 700; + font-size: 0.9rem; + transition: all 0.3s ease; + width: 100%; + position: relative; + overflow: hidden; +} + +.btn-action-test:hover, .btn-action-record:hover { + transform: scale(1.05); + color: white; +} + +.btn-action-test::before, .btn-action-record::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.6s; +} + +.btn-action-test:hover::before, .btn-action-record:hover::before { + left: 100%; +} + +.coming-soon { + position: absolute; + top: 1rem; + right: 1rem; + background: #6c757d; + color: white; + padding: 0.3rem 0.8rem; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 600; +} + +/* 成就卡片 - 增强版 */ +.achievement-card { + background: white; + border-radius: 15px; + padding: 1.5rem; + text-align: center; + box-shadow: 0 8px 25px rgba(0,0,0,0.08); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + animation: cardBreathe 4s ease-in-out infinite; +} + +.achievement-card:hover { + transform: translateY(-3px); + box-shadow: 0 15px 35px rgba(0,0,0,0.12); +} + +.achievement-icon { + width: 50px; + height: 50px; + border-radius: 50%; + margin: 0 auto 1rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + color: white; +} + +.time-card .achievement-icon { background: linear-gradient(45deg, #667eea, #764ba2); } +.conversation-card .achievement-icon { background: linear-gradient(45deg, #4ECDC4, #44A08D); } +.star-card .achievement-icon { background: linear-gradient(45deg, #FFD93D, #FFCC02); } +.medal-card .achievement-icon { background: linear-gradient(45deg, #FF6B6B, #FF8E53); } + +.achievement-number { + font-size: 2rem; + font-weight: 900; + color: var(--kid-text); + margin-bottom: 0.5rem; + animation: numberGlow 4s ease-in-out infinite; +} + +@keyframes numberGlow { + 0%, 100% { text-shadow: 0 0 5px rgba(91, 64, 55, 0.3); } + 50% { text-shadow: 0 0 15px rgba(91, 64, 55, 0.6); } +} + +.achievement-label { + font-size: 0.9rem; + color: #666; + margin-bottom: 1rem; +} + +.achievement-progress { + height: 4px; + background: #f0f0f0; + border-radius: 2px; + overflow: hidden; +} + +.achievement-progress .progress-bar { + height: 100%; + border-radius: 2px; + transition: width 0.5s ease; +} + +.time-card .progress-bar { background: linear-gradient(45deg, #667eea, #764ba2); } +.conversation-card .progress-bar { background: linear-gradient(45deg, #4ECDC4, #44A08D); } +.star-card .progress-bar { background: linear-gradient(45deg, #FFD93D, #FFCC02); } +.medal-card .progress-bar { background: linear-gradient(45deg, #FF6B6B, #FF8E53); } + +/* === 页面加载和动效增强 === */ + +/* 页面元素渐入动画 */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + animation: fadeInUp 0.8s ease forwards; +} + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 加载状态类 */ +.page-loading .fade-in-up:nth-child(1) { animation-delay: 0.1s; } +.page-loading .fade-in-up:nth-child(2) { animation-delay: 0.2s; } +.page-loading .fade-in-up:nth-child(3) { animation-delay: 0.3s; } +.page-loading .fade-in-up:nth-child(4) { animation-delay: 0.4s; } +.page-loading .fade-in-up:nth-child(5) { animation-delay: 0.5s; } +.page-loading .fade-in-up:nth-child(6) { animation-delay: 0.6s; } + +/* 自定义图标动画 */ +.icon-bubble { + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-bubble i { + font-size: 1.5rem; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .welcome-title { + font-size: 2rem; + } + + .section-title { + font-size: 1.8rem; + } + + .scenario-card-kid:hover { + transform: translateY(-5px) scale(1.01); + } + + .voice-clone-actions { + flex-direction: column; + } + + .achievement-number { + font-size: 1.5rem; + } +} + +.text-pink { + color: var(--pink-color) !important; +} + +/* === 首页样式修复 === */ + /* === 修复后的轮播图样式 === */ .hero-carousel-fixed { position: relative; @@ -435,7 +1192,7 @@ main { opacity: 0.9; } -/* 响应式设计 */ +/* 首页响应式设计 */ @media (max-width: 768px) { .hero-carousel-fixed .carousel-item { height: 60vh; @@ -465,6 +1222,731 @@ main { } } -.text-pink { - color: var(--pink-color) !important; +/* === 语音克隆页面专用样式 - 儿童友好版 === */ + +/* 语音克隆页面容器 */ +.voice-clone-page { + background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 50%, #FECFEF 100%); + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +/* 语音克隆页面背景装饰 */ +.voice-clone-page::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 30%, rgba(255, 182, 193, 0.3) 15%, transparent 30%), + radial-gradient(circle at 80% 20%, rgba(135, 206, 250, 0.2) 20%, transparent 35%), + radial-gradient(circle at 40% 80%, rgba(255, 218, 185, 0.25) 18%, transparent 32%), + radial-gradient(circle at 90% 70%, rgba(152, 251, 152, 0.2) 22%, transparent 40%); + z-index: -1; + animation: voicePageFloat 20s ease-in-out infinite; +} + +@keyframes voicePageFloat { + 0%, 100% { transform: translateY(0px) scale(1); opacity: 0.8; } + 50% { transform: translateY(-10px) scale(1.02); opacity: 1; } +} + +/* 页面标题区域增强 */ +.voice-clone-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 25px; + padding: 2rem; + margin-bottom: 2rem; + color: white; + position: relative; + overflow: hidden; + box-shadow: 0 15px 35px rgba(102, 126, 234, 0.3); +} + +.voice-clone-header::before { + content: '🎤✨🎵'; + position: absolute; + top: 15px; + right: 20px; + font-size: 1.5rem; + animation: headerEmoji 3s ease-in-out infinite; +} + +@keyframes headerEmoji { + 0%, 100% { transform: rotate(0deg) scale(1); opacity: 0.7; } + 50% { transform: rotate(10deg) scale(1.1); opacity: 1; } +} + +.text-kid-primary { + background: linear-gradient(45deg, #FFD700, #FFA500); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 2px 2px 4px rgba(0,0,0,0.1); +} + +/* 状态卡片增强 */ +.voice-status-card { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + border-radius: 20px; + border: none !important; + box-shadow: 0 10px 30px rgba(168, 237, 234, 0.4); + transition: all 0.3s ease; + animation: cardFloat 4s ease-in-out infinite; +} + +@keyframes cardFloat { + 0%, 100% { transform: translateY(0px); box-shadow: 0 10px 30px rgba(168, 237, 234, 0.4); } + 50% { transform: translateY(-5px); box-shadow: 0 20px 40px rgba(168, 237, 234, 0.6); } +} + +/* 录制卡片样式 */ +.voice-record-card { + background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); + border-radius: 20px; + border: none !important; + box-shadow: 0 15px 35px rgba(255, 154, 158, 0.4); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.voice-record-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); + animation: recordShine 6s linear infinite; + pointer-events: none; +} + +@keyframes recordShine { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.voice-record-card .card-body { + position: relative; + z-index: 2; +} + +/* 生成卡片样式 */ +.voice-generate-card { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + border-radius: 20px; + border: none !important; + box-shadow: 0 15px 35px rgba(168, 237, 234, 0.4); + transition: all 0.3s ease; +} + +/* 儿童友好按钮样式 */ +.btn-kid-primary { + background: linear-gradient(45deg, #ff6b6b, #ee5a52); + border: none; + border-radius: 25px; + color: white !important; + font-weight: 700; + padding: 12px 25px; + font-size: 1.1rem; + box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-kid-primary:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 30px rgba(255, 107, 107, 0.6); + color: white !important; +} + +.btn-kid-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.6s; +} + +.btn-kid-primary:hover::before { + left: 100%; +} + +.btn-kid-accent { + background: linear-gradient(45deg, #4ecdc4, #44a08d); + border: none; + border-radius: 25px; + color: white !important; + font-weight: 700; + padding: 12px 25px; + font-size: 1.1rem; + box-shadow: 0 8px 20px rgba(78, 205, 196, 0.4); + transition: all 0.3s ease; +} + +.btn-kid-accent:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 30px rgba(78, 205, 196, 0.6); + color: white !important; +} + +.btn-kid-secondary { + background: linear-gradient(45deg, #ffd93d, #ffcc02); + border: none; + border-radius: 25px; + color: #333 !important; + font-weight: 700; + padding: 12px 25px; + font-size: 1.1rem; + box-shadow: 0 8px 20px rgba(255, 217, 61, 0.4); + transition: all 0.3s ease; +} + +.btn-kid-secondary:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 30px rgba(255, 217, 61, 0.6); + color: #333 !important; +} + +/* 录音控制按钮特殊样式 */ +.btn-record-start { + background: linear-gradient(45deg, #ff6b6b, #ee5a52); + border: none; + border-radius: 50%; + width: 80px; + height: 80px; + color: white; + font-size: 1.5rem; + box-shadow: 0 10px 25px rgba(255, 107, 107, 0.5); + transition: all 0.3s ease; + animation: recordPulse 2s ease-in-out infinite; +} + +@keyframes recordPulse { + 0%, 100% { transform: scale(1); box-shadow: 0 10px 25px rgba(255, 107, 107, 0.5); } + 50% { transform: scale(1.1); box-shadow: 0 15px 35px rgba(255, 107, 107, 0.7); } +} + +.btn-record-stop { + background: linear-gradient(45deg, #6c757d, #5a6169); + border: none; + border-radius: 50%; + width: 80px; + height: 80px; + color: white; + font-size: 1.5rem; + box-shadow: 0 10px 25px rgba(108, 117, 125, 0.5); + transition: all 0.3s ease; +} + +/* 音频预览增强 */ +.audio-preview-enhanced { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 15px; + padding: 1.5rem; + color: white; + margin-top: 1rem; + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; +} + +.audio-preview-enhanced::before { + content: '🎵'; + position: absolute; + top: 15px; + right: 15px; + font-size: 1.5rem; + animation: musicNote 2s ease-in-out infinite; +} + +@keyframes musicNote { + 0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.7; } + 50% { transform: translateY(-5px) rotate(10deg); opacity: 1; } +} + +.audio-preview-enhanced audio { + width: 100%; + margin-top: 1rem; +} + +/* 表单控件增强 */ +.voice-clone-page .form-control { + border: 3px solid #e3f2fd; + border-radius: 15px; + padding: 12px 20px; + font-size: 1.1rem; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.9); +} + +.voice-clone-page .form-control:focus { + border-color: #4ecdc4; + box-shadow: 0 0 0 0.2rem rgba(78, 205, 196, 0.25); + background: white; + transform: scale(1.02); +} + +.voice-clone-page .form-label { + font-weight: 700; + color: #667eea; + font-size: 1.1rem; + margin-bottom: 8px; +} + +/* 提示卡片增强 */ +.tips-card-enhanced { + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + border-radius: 20px; + border: none !important; + box-shadow: 0 10px 25px rgba(255, 236, 210, 0.5); + transition: all 0.3s ease; +} + +.tips-card-enhanced:hover { + transform: translateY(-5px); + box-shadow: 0 15px 35px rgba(255, 236, 210, 0.7); +} + +.tips-card-enhanced .icon-bubble { + background: linear-gradient(45deg, #ff9a9e, #fecfef); + color: white; + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 10px; + box-shadow: 0 5px 15px rgba(255, 154, 158, 0.4); +} + +/* 成功/错误提示增强 */ +.alert-success-kid { + background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); + border: 2px solid #b8dacd; + border-radius: 15px; + color: #155724; + padding: 1rem 1.5rem; + box-shadow: 0 5px 15px rgba(212, 237, 218, 0.5); +} + +.alert-info-kid { + background: linear-gradient(135deg, #cce7ff 0%, #b3d9ff 100%); + border: 2px solid #9ecfff; + border-radius: 15px; + color: #004085; + padding: 1rem 1.5rem; + box-shadow: 0 5px 15px rgba(204, 231, 255, 0.5); +} + +/* 波形动画增强 */ +.voice-wave-visual { + display: flex; + justify-content: center; + align-items: center; + gap: 6px; + height: 80px; + margin: 1rem 0; +} + +.wave-bar { + width: 6px; + height: 30px; + background: linear-gradient(45deg, #ff6b6b, #4ecdc4); + border-radius: 3px; + animation: waveAnimation 1.5s ease-in-out infinite; +} + +.wave-bar:nth-child(1) { animation-delay: 0s; } +.wave-bar:nth-child(2) { animation-delay: 0.1s; } +.wave-bar:nth-child(3) { animation-delay: 0.2s; } +.wave-bar:nth-child(4) { animation-delay: 0.3s; } +.wave-bar:nth-child(5) { animation-delay: 0.4s; } + +@keyframes waveAnimation { + 0%, 100% { + height: 30px; + opacity: 0.5; + transform: scaleY(1); + } + 50% { + height: 60px; + opacity: 1; + transform: scaleY(1.5); + } +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .voice-clone-header { + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .btn-record-start, .btn-record-stop { + width: 60px; + height: 60px; + font-size: 1.2rem; + } + + .voice-wave-visual { + height: 60px; + } + + .wave-bar { + width: 4px; + height: 20px; + } + + .wave-bar:nth-child(odd) { + height: 25px; + } +} + +/* 加载动画 */ +.voice-loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255,255,255,.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* === 语音克隆页面专用样式 - 儿童友好版 === */ + +/* 语音克隆页面背景 */ +.voice-clone-page { + min-height: 100vh; + background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 50%, #90CAF9 100%); + background-attachment: fixed; + position: relative; + overflow-x: hidden; +} + +/* 语音克隆页面背景装饰 */ +.voice-clone-page::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 30%, rgba(33, 150, 243, 0.1) 15%, transparent 25%), + radial-gradient(circle at 80% 20%, rgba(100, 181, 246, 0.12) 18%, transparent 30%), + radial-gradient(circle at 40% 80%, rgba(144, 202, 249, 0.1) 20%, transparent 35%), + radial-gradient(circle at 90% 70%, rgba(187, 222, 251, 0.15) 22%, transparent 40%); + z-index: -1; + animation: voicePageFloat 20s ease-in-out infinite; +} + +@keyframes voicePageFloat { + 0%, 100% { transform: translateY(0px) scale(1); opacity: 0.8; } + 50% { transform: translateY(-10px) scale(1.02); opacity: 1; } +} + +/* 增强语音克隆卡片样式 */ +.kid-feature-card { + background: linear-gradient(145deg, #ffffff 0%, #f8fbff 100%); + border-radius: 25px; + padding: 2rem; + box-shadow: 0 15px 35px rgba(33, 150, 243, 0.15); + border: 3px solid rgba(100, 181, 246, 0.2); + transition: all 0.4s ease; + position: relative; + overflow: hidden; +} + +.kid-feature-card::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: linear-gradient(45deg, #FF6B6B, #4ECDC4, #45B7D1, #96CEB4, #FFEAA7); + border-radius: 25px; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; +} + +.kid-feature-card:hover::before { + opacity: 0.3; +} + +.kid-feature-card:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 25px 50px rgba(33, 150, 243, 0.25); +} + +/* 语音克隆标题样式 */ +.text-kid-primary { + background: linear-gradient(45deg, #2196F3, #03DAC6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: titleGlow 3s ease-in-out infinite; +} + +@keyframes titleGlow { + 0%, 100% { filter: drop-shadow(0 0 5px rgba(33, 150, 243, 0.3)); } + 50% { filter: drop-shadow(0 0 15px rgba(33, 150, 243, 0.6)); } +} + +/* 增强按钮样式 */ +.btn-kid-primary { + background: linear-gradient(45deg, #FF6B6B, #4ECDC4); + border: none; + border-radius: 25px; + color: white; + font-weight: 700; + padding: 12px 24px; + box-shadow: 0 8px 20px rgba(255, 107, 107, 0.3); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-kid-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.6s; +} + +.btn-kid-primary:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 30px rgba(255, 107, 107, 0.4); + color: white; +} + +.btn-kid-primary:hover::before { + left: 100%; +} + +.btn-kid-accent { + background: linear-gradient(45deg, #FFD93D, #FF8A65); + border: none; + border-radius: 25px; + color: white; + font-weight: 700; + padding: 12px 24px; + box-shadow: 0 8px 20px rgba(255, 217, 61, 0.3); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-kid-accent::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.6s; +} + +.btn-kid-accent:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 30px rgba(255, 217, 61, 0.4); + color: white; +} + +.btn-kid-accent:hover::before { + left: 100%; +} + +/* 表单元素增强 */ +.form-control { + border: 3px solid #E1F5FE; + border-radius: 15px; + padding: 12px 16px; + transition: all 0.3s ease; + background: linear-gradient(145deg, #ffffff 0%, #f8fbff 100%); +} + +.form-control:focus { + border-color: #4ECDC4; + box-shadow: 0 0 0 0.2rem rgba(78, 205, 196, 0.25); + transform: scale(1.02); +} + +/* 状态卡片增强 */ +#status-card { + background: linear-gradient(145deg, #ffffff 0%, #f0f8ff 100%); + border-radius: 20px; + border: 2px solid rgba(33, 150, 243, 0.1); + box-shadow: 0 10px 30px rgba(33, 150, 243, 0.1); + animation: statusCardPulse 4s ease-in-out infinite; +} + +@keyframes statusCardPulse { + 0%, 100% { box-shadow: 0 10px 30px rgba(33, 150, 243, 0.1); } + 50% { box-shadow: 0 15px 40px rgba(33, 150, 243, 0.2); } +} + +/* 录音状态增强 */ +#record-status { + font-weight: 600; + animation: statusBlink 2s ease-in-out infinite; +} + +@keyframes statusBlink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* 音频预览增强 */ +.alert-info { + background: linear-gradient(145deg, #E3F2FD 0%, #BBDEFB 100%); + border: 2px solid #90CAF9; + border-radius: 20px; + color: #1565C0; + animation: previewGlow 3s ease-in-out infinite; +} + +@keyframes previewGlow { + 0%, 100% { box-shadow: 0 5px 15px rgba(33, 150, 243, 0.2); } + 50% { box-shadow: 0 10px 25px rgba(33, 150, 243, 0.3); } +} + +/* 音频控件美化 */ +audio { + border-radius: 15px; + box-shadow: 0 5px 15px rgba(33, 150, 243, 0.2); + background: linear-gradient(145deg, #f8fbff 0%, #ffffff 100%); +} + +/* 提示卡片增强 */ +.voice-tips-card { + background: linear-gradient(145deg, #E8F5E8 0%, #C8E6C9 100%); + border: 2px solid #A5D6A7; + border-radius: 20px; + box-shadow: 0 10px 25px rgba(76, 175, 80, 0.15); + animation: tipsCardFloat 6s ease-in-out infinite; +} + +@keyframes tipsCardFloat { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-5px); } +} + +.tips-title { + color: #2E7D32; + text-shadow: 1px 1px 3px rgba(46, 125, 50, 0.2); +} + +.tips-list li { + color: #388E3C; + transition: transform 0.2s ease; +} + +.tips-list li:hover { + transform: translateX(5px); +} + +/* 加载动画增强 */ +.spinner-border { + animation: rainbowSpin 1.5s linear infinite; +} + +@keyframes rainbowSpin { + 0% { border-top-color: #FF6B6B; transform: rotate(0deg); } + 25% { border-top-color: #4ECDC4; } + 50% { border-top-color: #45B7D1; } + 75% { border-top-color: #96CEB4; } + 100% { border-top-color: #FF6B6B; transform: rotate(360deg); } +} + +/* 图标增强 */ +.fas { + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); + transition: all 0.3s ease; +} + +.btn:hover .fas { + transform: scale(1.1); +} + +/* 语音波浪增强(如果有的话) */ +.voice-wave-container { + background: rgba(78, 205, 196, 0.1); + border-radius: 15px; + padding: 10px; + animation: waveContainerPulse 2s ease-in-out infinite; +} + +@keyframes waveContainerPulse { + 0%, 100% { background: rgba(78, 205, 196, 0.1); } + 50% { background: rgba(78, 205, 196, 0.2); } +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .kid-feature-card { + padding: 1.5rem; + } + + .btn-kid-primary, .btn-kid-accent { + padding: 10px 20px; + font-size: 0.95rem; + } + + .text-kid-primary { + font-size: 1.8rem; + } +} + +/* 成功/错误状态增强 */ +.text-success { + color: #4CAF50 !important; + text-shadow: 1px 1px 3px rgba(76, 175, 80, 0.3); +} + +.text-danger { + color: #F44336 !important; + text-shadow: 1px 1px 3px rgba(244, 67, 54, 0.3); +} + +.text-warning { + color: #FF9800 !important; + text-shadow: 1px 1px 3px rgba(255, 152, 0, 0.3); +} + +/* 容器间距优化 */ +.container { + position: relative; + z-index: 1; +} + +/* 链接样式增强 */ +a.btn { + text-decoration: none !important; +} + +a.btn:hover { + text-decoration: none !important; } diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js new file mode 100644 index 0000000..5d9bfb9 --- /dev/null +++ b/app/static/js/dashboard.js @@ -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 = ` +

+ + 你的专属声音已就绪! +

+

AI已经学会了你的声音:「${result.recognized_text}」

+ 录制时间:${result.upload_time} + `; + + 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 = ` +

+ + 还没有录制语音样本 +

+

录制你的专属声音,让AI学会模仿你说话!

+ `; + + 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 = '测试中...'; + + 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 = ` + + `; + + 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 = ` +
+
+ ${message} +
+ +
+ `; + + 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)`; + }); +}); diff --git a/app/static/js/voice_clone.js b/app/static/js/voice_clone.js new file mode 100644 index 0000000..73295cb --- /dev/null +++ b/app/static/js/voice_clone.js @@ -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 = ` +
+
+ ${message} +
+ +
+ `; + + 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 = ` +
+
+
+ 专属声音已准备好! +
+

AI识别内容:「${result.recognized_text}」

+ 录制时间:${result.upload_time} +
+
+
+
+ +
+
+ 已就绪 +
+
+
+
+ `; + } else { + content.innerHTML = ` +
+
+
+ 还没有录制语音样本 +
+

快来录制你的专属声音,让AI学会模仿你说话吧!

+
+
+
+ +
+
+
+ `; + } +} + +/** + * 处理文件选择 + */ +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(); + } +} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 17b405f..6a00c47 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -3,285 +3,414 @@ {% block title %}学习主页 - 儿童语言学习系统{% endblock %} {% block content %} -
- -
-
-
-
-
-

- - 早安,{{ current_user.name }}小朋友! -

-

- 准备好今天的语言学习之旅了吗?让我们一起探索有趣的世界吧! -

+
+ +
+ +
+ +
+
+
+
+
🌟
+
🎈
+
🎊
+
-
- +
+
+

+ 👋 + 早安,{{ current_user.name }}小朋友! +

+

+ 🚀 准备好今天的语言学习冒险了吗?让我们一起探索神奇的语言世界吧! +

+
+
+
+ +
+
-
- -
-
-

- 快速开始 -

+ +
+
+
+

+ 🌈 今日推荐场景 + 超人气 +

+

选择你喜欢的场景,开始有趣的对话学习!

+
+
-
-
-
-
-
-
-
- + + + +
+
+
+

+ 🎤 我的专属声音 + AI魔法 +

+

让AI学会你的声音,创造专属于你的语音助手!

+
+
+
+ +
- -
-
-
-
-
- -
-
录制声音
-

- 上传你的声音样本,让AI学会说话像你一样 -

- -
- 即将上线 -
+ +
+
+
💡 录音小贴士
+
    +
  • 在安静的地方录音
  • +
  • 说话要清晰洪亮
  • +
  • 录制3-10秒即可
  • +
  • 用自然的语调
  • +
+
+
-
-
-
-
-
- -
-
开始对话
-

- 选择有趣的场景,和AI朋友一起聊天学习 -

- -
- 即将上线 -
-
+ +
+
+
+

+ ⚡ 快速开始 +

+

选择你想要的功能,立即开始学习冒险!

-
-
-
-
-
- -
-
查看进度
-

- 了解你的学习情况,看看哪里可以做得更好 -

- -
- 即将上线 -
+
+
+
+
+
+
API 测试
+

体验语音技术的神奇魔法

+ + 开始测试 +
-
-
- -
-
-

- 学习成就 -

-
-
+
+
+
+ +
+
录制声音
+

让AI学会你的声音

+ + 开始录制 + +
+
-
-
-
-
-
- -

0

- 学习时长(分钟) +
+
+
+
+
开始对话
+

和AI朋友聊天学习

+ +
即将上线
-
- -
-
-
-
- -

0

- 对话次数 -
-
-
-
- -
-
-
-
- -

0

- 获得星星 -
-
-
-
- -
-
-
-
- -

0

- 完成场景 -
-
-
-
-
- -
-
-

- 推荐场景 -

-
-
- -
-
-
-
-
-
-
- -
-
-
和小明交朋友
- 社交互动 -
- 简单 -
-

- 学习如何与新朋友进行自我介绍和基础交流 -

-
-
- - 0人完成 - -
- -
+
+
+
+
+
学习报告
+

查看你的学习成果

+ +
即将上线
-
-
-
-
-
-
- -
-
-
餐厅点餐
- 日常生活 -
- 中等 -
-

- 学习在餐厅如何礼貌地点餐和与服务员交流 -

-
-
- - 0人完成 - -
- -
-
+ +
+
+
+

+ 🏆 我的学习成就 +

+

看看你在语言学习路上取得的进步!

-
-
-
-
-
-
- -
-
-
邀请朋友玩游戏
- 游戏娱乐 -
- 中等 -
-

- 学习如何邀请朋友一起玩游戏并协调活动 -

-
-
- - 0人完成 - -
- -
+
+
+
+
+ +
+
0
+
学习时长(分钟)
+
+
+
+
+
+ +
+
+
+ +
+
0
+
对话次数
+
+
+
+
+
+ +
+
+
+ +
+
0
+
获得星星
+
+
+
+
+
+ +
+
+
+ +
+
0
+
完成场景
+
+
@@ -289,3 +418,7 @@
{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/voice_clone/index.html b/app/templates/voice_clone/index.html new file mode 100644 index 0000000..c35f28f --- /dev/null +++ b/app/templates/voice_clone/index.html @@ -0,0 +1,186 @@ +{% extends "base.html" %} + +{% block title %}我的专属声音 - 儿童语言学习系统{% endblock %} + +{% block content %} +
+ +
+
+
+ + 返回主页 + +
+

+ 🎤 我的专属声音 +

+

让AI学会你的声音,创造专属于你的语音助手

+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+ 录制你的声音 +
+ +
+
+ + +
建议:清晰朗读3-10秒,说一句完整的话
+
+ +
+ 或者 +
+ +
+ + +
+ 点击开始录音按钮 +
+
+ + +
+ + + +
+
+
+ +
+
+
+
+ 用我的声音说话 +
+ +
+
+ + +
+ + +
+ + + +
+
+
+
+ + +
+
+
+
+
+ 使用小贴士 +
+ +
+
+
+ +
+
清晰发音
+

说话要清楚,声音要洪亮

+
+ +
+
+ +
+
合适时长
+

录音3-10秒,一句话就够了

+
+ +
+
+ +
+
安静环境
+

在安静的地方录音效果更好

+
+ +
+
+ +
+
自然语调
+

用你平时说话的语调

+
+
+
+
+
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %}