dashboard_upgrade
This commit is contained in:
parent
be60530d9e
commit
a7fd012bd6
@ -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
|
||||
|
||||
@ -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}>'
|
||||
|
||||
@ -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
166
app/routes/voice_clone.py
Normal 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
334
app/static/js/dashboard.js
Normal 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)`;
|
||||
});
|
||||
});
|
||||
374
app/static/js/voice_clone.js
Normal file
374
app/static/js/voice_clone.js
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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 %}
|
||||
|
||||
186
app/templates/voice_clone/index.html
Normal file
186
app/templates/voice_clone/index.html
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user