2025.10.25故事生成系统介绍
大作业:儿童故事管理平台设计与开发
系统架构
整体架构
儿童故事管理平台是一个完整的Web应用,集成了三个实验的核心功能:AI故事生成、AI插图生成和AI语音合成。系统采用前后端分离的架构设计,使用Flask作为后端框架,前端使用HTML、CSS和JavaScript构建用户界面。
组件关系
前端界面 (HTML/JS) → Flask应用 (app.py)
→ 故事管理模块 (story_manager_routes.py/story_manager.py)
→ 故事生成服务 (story_service.py)
→ 插图生成服务 (image_service.py)
→ 语音合成服务 (voice_service.py)
核心代码
1. 应用初始化与配置 (app.py)
import os
from flask import Flask, render_template, send_from_directory
from flask_cors import CORS
from dotenv import load_dotenv
import sys
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 加载环境变量
load_dotenv()
# 创建Flask应用实例
app = Flask(__name__)
# 配置CORS,允许跨域请求
CORS(app, resources={r"/*": {"origins": "*"}})
# 配置上传文件夹
UPLOAD_FOLDER = os.path.join(app.root_path, 'uploads')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# 健康检查路由
@app.route('/health')
def health_check():
return {'status': 'ok'}
# 测试路由
@app.route('/test')
def test():
return {'message': '儿童故事管理平台API正常运行'}
# HTML页面路由
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate')
def generate_page():
return render_template('generate.html')
@app.route('/stories')
def stories_page():
return render_template('stories.html')
# 注册蓝图
from routes.story_routes import bp as story_bp
from routes.image_routes import bp as image_bp
from routes.voice_routes import bp as voice_bp
from routes.story_manager_routes import bp as story_manager_bp
app.register_blueprint(story_bp, url_prefix='/story')
app.register_blueprint(image_bp, url_prefix='/image')
app.register_blueprint(voice_bp, url_prefix='/voice')
app.register_blueprint(story_manager_bp)
# 数据库初始化
from models.db import init_db
with app.app_context():
init_db()
# 静态文件服务配置
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
# 启动应用
if __name__ == '__main__':
# 获取端口配置
port = int(os.environ.get('PORT', 5000))
# 启动Flask应用
app.run(
debug=os.environ.get('DEBUG', 'True').lower() == 'true',
host='0.0.0.0', # 允许外部访问
port=port
)
2. 故事管理路由 (routes/story_manager_routes.py)
from flask import Blueprint, request, jsonify
from services.story_manager import StoryManager
# 创建故事管理蓝图
bp = Blueprint('story_manager', __name__)
# 获取所有故事
@bp.route('/stories', methods=['GET'])
def get_all_stories():
try:
# 从请求参数获取分页信息
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# 调用故事管理服务获取故事列表
stories = StoryManager.get_stories(page=page, per_page=per_page)
# 返回故事列表
return jsonify({
'stories': stories['stories'],
'total': stories['total'],
'page': stories['page'],
'per_page': stories['per_page'],
'pages': stories['pages']
}), 200
except Exception as e:
print(f"❌ 获取故事列表失败: {e}")
return jsonify({'error': f'获取故事列表失败: {str(e)}'}), 500
# 获取单个故事
@bp.route('/stories/<int:story_id>', methods=['GET'])
def get_story(story_id):
try:
# 调用故事管理服务获取单个故事
story = StoryManager.get_story(story_id)
if story:
return jsonify(story), 200
else:
return jsonify({'error': '故事不存在'}), 404
except Exception as e:
print(f"❌ 获取故事失败: {e}")
return jsonify({'error': f'获取故事失败: {str(e)}'}), 500
# 保存新故事
@bp.route('/save_story', methods=['POST'])
def save_story():
try:
# 获取请求数据
data = request.json
# 验证必要的参数
if not data or 'title' not in data or 'content' not in data:
return jsonify({'error': '缺少必要的故事标题或内容参数'}), 400
# 调用故事管理服务创建故事
story_id = StoryManager.create_story(data)
# 返回创建成功的故事ID
return jsonify({
'message': '故事保存成功',
'story_id': story_id
}), 201
except Exception as e:
print(f"❌ 保存故事失败: {e}")
return jsonify({'error': f'保存故事失败: {str(e)}'}), 500
# 更新故事
@bp.route('/stories/<int:story_id>', methods=['PUT'])
def update_story(story_id):
try:
# 获取请求数据
data = request.json
# 调用故事管理服务更新故事
success = StoryManager.update_story(story_id, data)
if success:
return jsonify({'message': '故事更新成功'}), 200
else:
return jsonify({'error': '故事不存在或更新失败'}), 404
except Exception as e:
print(f"❌ 更新故事失败: {e}")
return jsonify({'error': f'更新故事失败: {str(e)}'}), 500
# 删除故事
@bp.route('/stories/<int:story_id>', methods=['DELETE'])
def delete_story(story_id):
try:
# 调用故事管理服务删除故事
success = StoryManager.delete_story(story_id)
if success:
return jsonify({'message': '故事删除成功'}), 200
else:
return jsonify({'error': '故事不存在或删除失败'}), 404
except Exception as e:
print(f"❌ 删除故事失败: {e}")
return jsonify({'error': f'删除故事失败: {str(e)}'}), 500
# 批量删除故事
@bp.route('/stories/batch-delete', methods=['DELETE'])
def batch_delete_stories():
try:
# 获取请求数据
data = request.json
# 验证必要的参数
if not data or 'ids' not in data or not isinstance(data['ids'], list):
return jsonify({'error': '缺少必要的故事ID列表参数'}), 400
# 调用故事管理服务批量删除故事
success_count = StoryManager.batch_delete_story(data['ids'])
return jsonify({
'message': '批量删除完成',
'deleted_count': success_count
}), 200
except Exception as e:
print(f"❌ 批量删除故事失败: {e}")
return jsonify({'error': f'批量删除故事失败: {str(e)}'}), 500
# 切换收藏状态
@bp.route('/stories/<int:story_id>/favorite', methods=['POST'])
def toggle_favorite(story_id):
try:
# 调用故事管理服务切换收藏状态
result = StoryManager.toggle_favorite(story_id)
if result:
return jsonify({
'message': '收藏状态已更新',
'is_favorite': result['is_favorite']
}), 200
else:
return jsonify({'error': '故事不存在'}), 404
except Exception as e:
print(f"❌ 切换收藏状态失败: {e}")
return jsonify({'error': f'切换收藏状态失败: {str(e)}'}), 500
# 获取收藏的故事
@bp.route('/stories/favorites', methods=['GET'])
def get_favorite_stories():
try:
# 调用故事管理服务获取收藏的故事
favorites = StoryManager.get_favorite_stories()
return jsonify({
'stories': favorites
}), 200
except Exception as e:
print(f"❌ 获取收藏故事失败: {e}")
return jsonify({'error': f'获取收藏故事失败: {str(e)}'}), 500
3. 故事管理服务 (services/story_manager.py)
from models.story import Story
from models.db import db
from datetime import datetime
class StoryManager:
"""故事管理服务类"""
@staticmethod
def create_story(story_data):
"""
创建新故事
Args:
story_data: 故事数据字典
Returns:
int: 创建的故事ID
"""
try:
# 创建故事对象
story = Story(
title=story_data.get('title', ''),
summary=story_data.get('summary', ''),
content=story_data.get('content', ''),
age_group=story_data.get('age_group', 'all'),
keywords=story_data.get('keywords', ''),
image_url=story_data.get('image_url', ''),
audio_url=story_data.get('audio_url', ''),
is_favorite=False,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
# 保存到数据库
db.session.add(story)
db.session.commit()
return story.id
except Exception as e:
db.session.rollback()
raise Exception(f"创建故事失败: {str(e)}")
@staticmethod
def get_story(story_id):
"""
获取单个故事
Args:
story_id: 故事ID
Returns:
dict: 故事信息字典
"""
try:
# 查询故事
story = Story.query.get(story_id)
if story:
# 转换为字典返回
return {
'id': story.id,
'title': story.title,
'summary': story.summary,
'content': story.content,
'age_group': story.age_group,
'keywords': story.keywords,
'image_url': story.image_url,
'audio_url': story.audio_url,
'is_favorite': story.is_favorite,
'created_at': story.created_at.isoformat() if story.created_at else None,
'updated_at': story.updated_at.isoformat() if story.updated_at else None
}
return None
except Exception as e:
raise Exception(f"获取故事失败: {str(e)}")
@staticmethod
def get_stories(page=1, per_page=10):
"""
获取故事列表
Args:
page: 页码
per_page: 每页数量
Returns:
dict: 包含故事列表和分页信息的字典
"""
try:
# 分页查询
pagination = Story.query.order_by(Story.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
# 转换为字典列表
stories = [
{
'id': story.id,
'title': story.title,
'summary': story.summary,
'age_group': story.age_group,
'keywords': story.keywords,
'image_url': story.image_url,
'audio_url': story.audio_url,
'is_favorite': story.is_favorite,
'created_at': story.created_at.isoformat() if story.created_at else None
}
for story in pagination.items
]
# 返回分页结果
return {
'stories': stories,
'total': pagination.total,
'page': page,
'per_page': per_page,
'pages': pagination.pages
}
except Exception as e:
raise Exception(f"获取故事列表失败: {str(e)}")
@staticmethod
def update_story(story_id, story_data):
"""
更新故事
Args:
story_id: 故事ID
story_data: 要更新的数据
Returns:
bool: 更新是否成功
"""
try:
# 查询故事
story = Story.query.get(story_id)
if story:
# 更新字段
if 'title' in story_data:
story.title = story_data['title']
if 'summary' in story_data:
story.summary = story_data['summary']
if 'content' in story_data:
story.content = story_data['content']
if 'age_group' in story_data:
story.age_group = story_data['age_group']
if 'keywords' in story_data:
story.keywords = story_data['keywords']
if 'image_url' in story_data:
story.image_url = story_data['image_url']
if 'audio_url' in story_data:
story.audio_url = story_data['audio_url']
# 更新时间
story.updated_at = datetime.utcnow()
# 保存到数据库
db.session.commit()
return True
return False
except Exception as e:
db.session.rollback()
raise Exception(f"更新故事失败: {str(e)}")
@staticmethod
def delete_story(story_id):
"""
删除故事
Args:
story_id: 故事ID
Returns:
bool: 删除是否成功
"""
try:
# 查询故事
story = Story.query.get(story_id)
if story:
# 删除故事
db.session.delete(story)
db.session.commit()
return True
return False
except Exception as e:
db.session.rollback()
raise Exception(f"删除故事失败: {str(e)}")
@staticmethod
def batch_delete_story(story_ids):
"""
批量删除故事
Args:
story_ids: 故事ID列表
Returns:
int: 成功删除的数量
"""
try:
# 批量删除
deleted_count = Story.query.filter(Story.id.in_(story_ids)).delete(synchronize_session=False)
db.session.commit()
return deleted_count
except Exception as e:
db.session.rollback()
raise Exception(f"批量删除故事失败: {str(e)}")
@staticmethod
def toggle_favorite(story_id):
"""
切换收藏状态
Args:
story_id: 故事ID
Returns:
dict: 更新后的收藏状态,或None
"""
try:
# 查询故事
story = Story.query.get(story_id)
if story:
# 切换收藏状态
story.is_favorite = not story.is_favorite
story.updated_at = datetime.utcnow()
# 保存到数据库
db.session.commit()
return {
'is_favorite': story.is_favorite
}
return None
except Exception as e:
db.session.rollback()
raise Exception(f"切换收藏状态失败: {str(e)}")
@staticmethod
def get_favorite_stories():
"""
获取收藏的故事
Returns:
list: 收藏的故事列表
"""
try:
# 查询收藏的故事
favorite_stories = Story.query.filter_by(is_favorite=True).order_by(
Story.updated_at.desc()
).all()
# 转换为字典列表
return [
{
'id': story.id,
'title': story.title,
'summary': story.summary,
'age_group': story.age_group,
'keywords': story.keywords,
'image_url': story.image_url,
'audio_url': story.audio_url,
'is_favorite': story.is_favorite,
'created_at': story.created_at.isoformat() if story.created_at else None,
'updated_at': story.updated_at.isoformat() if story.updated_at else None
}
for story in favorite_stories
]
except Exception as e:
raise Exception(f"获取收藏故事失败: {str(e)}")
4. 完整工作流前端实现 (static/js/generate.js)
// 生成按钮点击事件
async function generateStory() {
// 获取表单数据
const keywords = document.getElementById('keywords').value.trim();
const ageGroup = document.getElementById('ageGroup').value;
// 表单验证
if (!keywords) {
showMessage('请输入故事关键词', 'error');
return;
}
// 显示加载状态
showLoading(true);
showMessage('开始生成故事...', 'info');
try {
// 1. 生成故事文本
const storyData = await generateStoryText(keywords, ageGroup);
if (!storyData) return;
// 2. 生成故事插图
const imageData = await generateIllustration(storyData.title, storyData.content);
if (!imageData) return;
// 3. 生成故事音频
const audioData = await generateVoice(storyData.content, 'child');
if (!audioData) return;
// 4. 保存完整故事
const savedStory = await saveCompleteStory({
title: storyData.title,
summary: storyData.summary,
content: storyData.content,
age_group: ageGroup,
keywords: keywords,
image_url: imageData.url,
audio_url: audioData.url
});
if (savedStory) {
// 显示成功信息
showMessage('故事生成完成!', 'success');
// 显示生成的故事
displayStory({
...savedStory,
image_url: imageData.url,
audio_url: audioData.url
});
}
} catch (error) {
showMessage(`生成过程中出错: ${error.message}`, 'error');
} finally {
showLoading(false);
}
}
// 生成故事文本
async function generateStoryText(keywords, ageGroup) {
try {
// 更新进度
updateProgress(33, '正在生成故事文本...');
// 调用故事生成API
const response = await fetch('/story/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
keywords: keywords,
age_group: ageGroup
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '故事生成失败');
}
const storyData = await response.json();
showMessage('故事文本生成成功!', 'success');
return storyData;
} catch (error) {
showMessage(`故事生成失败: ${error.message}`, 'error');
return null;
}
}
// 生成故事插图
async function generateIllustration(title, content) {
try {
// 更新进度
updateProgress(66, '正在生成故事插图...');
// 构建插图提示词(使用标题和内容摘要)
const prompt = `${title}。${content.substring(0, 100)}...`;
// 调用图像生成API
const response = await fetch('/image/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: prompt,
style: 'cartoon',
size: '1024x1024'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '插图生成失败');
}
const imageData = await response.json();
showMessage('故事插图生成成功!', 'success');
return imageData;
} catch (error) {
showMessage(`插图生成失败: ${error.message}`, 'error');
return null;
}
}
// 生成故事音频
async function generateVoice(content, voiceType) {
try {
// 更新进度
updateProgress(99, '正在生成故事音频...');
// 调用语音合成API
const response = await fetch('/voice/synthesize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: content,
voice_type: voiceType,
speed: 'normal'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '音频生成失败');
}
const audioData = await response.json();
showMessage('故事音频生成成功!', 'success');
return audioData;
} catch (error) {
showMessage(`音频生成失败: ${error.message}`, 'error');
return null;
}
}
// 保存完整故事
async function saveCompleteStory(storyData) {
try {
// 调用保存故事API
const response = await fetch('/save_story', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(storyData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '故事保存失败');
}
const savedData = await response.json();
// 返回完整的故事数据
return {
id: savedData.story_id,
title: storyData.title,
summary: storyData.summary,
content: storyData.content,
age_group: storyData.age_group,
keywords: storyData.keywords
};
} catch (error) {
showMessage(`故事保存失败: ${error.message}`, 'error');
return null;
}
}
// 更新进度条
function updateProgress(percentage, message) {
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
progressBar.style.width = `${percentage}%`;
progressText.textContent = message;
}
// 显示加载状态
function showLoading(show) {
const loadingOverlay = document.getElementById('loadingOverlay');
const progressContainer = document.getElementById('progressContainer');
if (show) {
loadingOverlay.classList.remove('hidden');
progressContainer.classList.remove('hidden');
} else {
loadingOverlay.classList.add('hidden');
progressContainer.classList.add('hidden');
}
}
// 显示消息
function showMessage(message, type = 'info') {
const messageContainer = document.getElementById('messageContainer');
// 清空现有消息
messageContainer.innerHTML = '';
// 创建新消息元素
const messageElement = document.createElement('div');
messageElement.className = `message ${type}`;
messageElement.textContent = message;
// 添加到容器
messageContainer.appendChild(messageElement);
// 自动隐藏(非错误消息)
if (type !== 'error') {
setTimeout(() => {
messageElement.remove();
}, 5000);
}
}
// 显示生成的故事
function displayStory(story) {
const storyDisplay = document.getElementById('storyDisplay');
// 创建故事内容
storyDisplay.innerHTML = `
<div class="story-container">
<h2>${story.title}</h2>
<div class="story-meta">
<span>适合年龄:${getAgeGroupName(story.age_group)}</span>
<span>关键词:${story.keywords}</span>
</div>
<div class="story-content">
<div class="story-image">
<img src="${story.image_url}" alt="${story.title}">
</div>
<div class="story-text">
<p>${story.content}</p>
</div>
</div>
<div class="story-audio">
<h3>🎧 听故事</h3>
<audio controls>
<source src="${story.audio_url}" type="audio/wav">
您的浏览器不支持音频播放。
</audio>
</div>
<div class="story-actions">
<button onclick="window.location.href='/stories'" class="btn btn-primary">
查看所有故事
</button>
<button onclick="window.location.href='/generate'" class="btn btn-secondary">
生成新故事
</button>
</div>
</div>
`;
// 滚动到故事显示区域
storyDisplay.scrollIntoView({ behavior: 'smooth' });
}
// 获取年龄组名称
function getAgeGroupName(ageGroup) {
const ageGroups = {
'3-6': '3-6岁',
'6-9': '6-9岁',
'9-12': '9-12岁',
'all': '所有年龄段'
};
return ageGroups[ageGroup] || ageGroup;
}
// 初始化
function init() {
// 绑定生成按钮事件
document.getElementById('generateBtn').addEventListener('click', generateStory);
// 绑定回车键事件
document.getElementById('keywords').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
generateStory();
}
});
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', init);
完整工作流程
用户故事生成流程
- 用户输入:用户在生成页面输入故事关键词和选择适合的年龄段
- 故事生成:系统调用实验一的故事生成服务,基于关键词创建儿童故事
- 插图生成:系统调用实验二的插图生成服务,为故事生成配套插图
- 语音合成:系统调用实验三的语音合成服务,为故事生成朗读音频
- 数据保存:将完整的故事(文本、插图、音频)保存到数据库
- 结果展示:在页面上展示生成的故事,包括文本、插图和音频播放器
故事管理流程
- 故事列表:用户可以浏览所有生成的故事,支持分页查看
- 故事详情:查看单个故事的完整内容,包括文本、插图和音频
- 故事收藏:标记喜欢的故事为收藏,方便快速访问
- 故事删除:删除不需要的故事,支持单个删除和批量删除
技术栈集成
后端技术栈
- Python 3.12:主要开发语言
- Flask:Web框架,处理HTTP请求和路由
- SQLAlchemy:ORM框架,处理数据库操作
- 阿里云百炼:提供文本生成、图像生成和语音合成API
- 百度AI平台:作为语音合成的备用方案
- pydub:音频处理库,用于音频合并和格式转换
前端技术栈
- HTML5/CSS3:页面结构和样式
- JavaScript (ES6+):交互逻辑和API调用
- Bootstrap:响应式UI框架
- Fetch API:处理HTTP请求
数据库
- SQLite:轻量级数据库,用于存储故事数据
文件存储
- 本地文件系统:存储生成的图像和音频文件
系统特点
完整的工作流
- 集成了三个实验的核心功能,实现端到端的故事生成体验
- 用户只需输入关键词,系统自动完成故事、插图和音频的生成
- 生成的多媒体故事可以保存、管理和再次访问
友好的用户界面
- 直观的生成页面,简洁的操作流程
- 美观的故事展示,支持文本阅读和音频播放
- 方便的故事管理功能,包括收藏和删除
强大的容错机制
- 每个服务都有错误处理和备用方案
- 即使某个服务失败,也能保证基本功能可用
- 详细的错误提示,帮助用户了解问题
灵活的配置选项
- 支持选择不同的年龄段和声音类型
- 可配置不同的图像风格和尺寸
- 支持自定义语速和其他参数
部署说明
-
环境准备:
- Python 3.12
- pip包管理器
- 环境变量配置(阿里云和百度API密钥)
-
安装依赖:
pip install -r requirements.txt -
配置文件:
- 创建.env文件,配置必要的环境变量
-
启动服务:
python app.py -
访问应用:
- 浏览器访问 http://localhost:5000
后续扩展方向
- 用户系统:添加用户注册和登录功能,支持个人故事收藏
- 更多AI模型:集成更多的AI模型,提供多样化的故事风格
- 多语言支持:支持生成和朗读不同语言的故事
- 社交分享:添加故事分享功能,方便用户分享给朋友
- 数据分析:添加用户行为分析,优化故事推荐
- 移动端适配:优化移动端体验,开发移动应用
总结
儿童故事管理平台成功集成了三个实验的核心功能,构建了一个完整的AI驱动的儿童故事生成和管理系统。用户可以通过简单的关键词输入,快速生成包含文本、插图和音频的多媒体故事,并进行管理和收藏。系统采用了模块化的设计,各组件之间松耦合,便于维护和扩展。同时,完善的错误处理和备用方案确保了系统的稳定性和可用性。

浙公网安备 33010602011771号