2025.10.13故事生成系统(文字生成功能实现)
第3天:文字生成功能实现
📋 第3天概述
在第2天完成核心API客户端的基础上,第3天将重点实现文字生成功能的完整业务逻辑,包括故事生成服务、提示词优化、内容格式化以及用户界面集成。
🎯 第3天目标
主要任务:构建完整的文字生成服务,实现智能故事生成、内容管理和用户交互
核心需求:开发稳定可靠的故事生成系统,支持多种故事类型和风格定制
🏗️ 架构设计
服务层架构
class TextGenerationService:
"""文字生成服务类"""
def __init__(self, aliyun_client):
self.client = aliyun_client
self.story_templates = self._load_story_templates()
# 核心生成方法
def generate_story(self, theme, style, length, **kwargs)
def _build_prompt(self, theme, style, length)
def _post_process_story(self, raw_text)
# 模板管理
def _load_story_templates(self)
def get_available_themes(self)
def get_available_styles(self)
# 内容管理
def save_story(self, story_data)
def load_story_history(self)
def delete_story(self, story_id)
🔧 具体实施步骤
步骤1:创建文字生成服务类
创建 src/services/text_service.py:
import json
import re
import time
from datetime import datetime
from typing import Dict, List, Optional, Any
from pathlib import Path
class TextGenerationService:
"""
文字生成服务类
负责故事生成、提示词构建和内容后处理
"""
def __init__(self, aliyun_client):
self.client = aliyun_client
self.story_templates = self._load_story_templates()
self.story_history = []
self.max_history_size = 100
def _load_story_templates(self) -> Dict[str, Dict[str, Any]]:
"""加载故事生成模板"""
templates = {
"童话故事": {
"prompt_template": "请创作一个关于{theme}的{style}童话故事,故事长度约{length}字。",
"styles": ["温馨", "冒险", "奇幻", "教育", "幽默"],
"length_options": ["短篇(200-300字)", "中篇(400-600字)", "长篇(700-1000字)"]
},
"科幻故事": {
"prompt_template": "请创作一个关于{theme}的{style}科幻故事,故事长度约{length}字。",
"styles": ["硬科幻", "太空歌剧", "赛博朋克", "时间旅行", "外星接触"],
"length_options": ["短篇(300-500字)", "中篇(600-800字)", "长篇(900-1200字)"]
},
"冒险故事": {
"prompt_template": "请创作一个关于{theme}的{style}冒险故事,故事长度约{length}字。",
"styles": ["丛林探险", "寻宝", "解谜", "生存挑战", "英雄旅程"],
"length_options": ["短篇(250-400字)", "中篇(450-700字)", "长篇(750-1000字)"]
},
"教育故事": {
"prompt_template": "请创作一个关于{theme}的{style}教育故事,故事长度约{length}字,包含教育意义。",
"styles": ["道德教育", "科学知识", "生活技能", "环保意识", "历史文化"],
"length_options": ["短篇(200-350字)", "中篇(400-600字)", "长篇(650-900字)"]
}
}
return templates
def get_available_themes(self) -> List[str]:
"""获取可用的故事主题"""
return list(self.story_templates.keys())
def get_available_styles(self, theme: str) -> List[str]:
"""获取指定主题的可用风格"""
if theme in self.story_templates:
return self.story_templates[theme]["styles"]
return []
def get_length_options(self, theme: str) -> List[str]:
"""获取指定主题的长度选项"""
if theme in self.story_templates:
return self.story_templates[theme]["length_options"]
return []
def _build_prompt(self, theme: str, style: str, length: str, custom_prompt: str = "") -> str:
"""构建生成提示词"""
if custom_prompt:
# 使用自定义提示词
base_prompt = custom_prompt
elif theme in self.story_templates:
# 使用模板构建提示词
template = self.story_templates[theme]["prompt_template"]
base_prompt = template.format(theme=theme, style=style, length=length)
else:
# 默认提示词
base_prompt = f"请创作一个关于{theme}的{style}故事,故事长度约{length}字。"
# 添加质量要求
quality_requirements = """
请确保故事具有以下特点:
1. 情节连贯,逻辑合理
2. 语言生动,适合儿童阅读
3. 包含明确的开始、发展和结尾
4. 有教育意义或娱乐价值
5. 避免暴力、恐怖或不适当内容
"""
final_prompt = base_prompt + quality_requirements
return final_prompt.strip()
def _parse_length_option(self, length_option: str) -> int:
"""解析长度选项为具体字数"""
length_mapping = {
"短篇(200-300字)": 300,
"短篇(300-500字)": 500,
"短篇(250-400字)": 400,
"短篇(200-350字)": 350,
"中篇(400-600字)": 600,
"中篇(600-800字)": 800,
"中篇(450-700字)": 700,
"中篇(400-600字)": 600,
"长篇(700-1000字)": 1000,
"长篇(900-1200字)": 1200,
"长篇(750-1000字)": 1000,
"长篇(650-900字)": 900
}
return length_mapping.get(length_option, 500)
def generate_story(self, theme: str, style: str, length: str,
custom_prompt: str = "", model: str = "qwen-max",
temperature: float = 0.7, **kwargs) -> Dict[str, Any]:
"""
生成故事
Args:
theme: 故事主题
style: 故事风格
length: 故事长度
custom_prompt: 自定义提示词
model: 模型名称
temperature: 生成温度
**kwargs: 其他参数
Returns:
dict: 生成结果
"""
# 构建提示词
prompt = self._build_prompt(theme, style, length, custom_prompt)
max_tokens = self._parse_length_option(length)
print(f"📝 开始生成故事...")
print(f"主题: {theme}")
print(f"风格: {style}")
print(f"长度: {length}")
print(f"提示词长度: {len(prompt)} 字符")
try:
# 调用API生成文本
start_time = time.time()
response = self.client.generate_text(
prompt=prompt,
model=model,
temperature=temperature,
max_tokens=max_tokens,
**kwargs
)
generation_time = time.time() - start_time
# 解析响应
raw_text = self.client.parse_text_response(response)
# 后处理
processed_text = self._post_process_story(raw_text)
# 构建结果
result = {
"success": True,
"theme": theme,
"style": style,
"length": length,
"content": processed_text,
"raw_content": raw_text,
"generation_time": round(generation_time, 2),
"word_count": len(processed_text),
"timestamp": datetime.now().isoformat(),
"model_used": model
}
# 保存到历史记录
self._save_to_history(result)
print(f"✅ 故事生成成功!")
print(f"生成时间: {generation_time:.2f}秒")
print(f"故事字数: {len(processed_text)}字")
return result
except Exception as e:
error_result = {
"success": False,
"error": str(e),
"theme": theme,
"style": style,
"length": length,
"timestamp": datetime.now().isoformat()
}
print(f"❌ 故事生成失败: {e}")
return error_result
def _post_process_story(self, raw_text: str) -> str:
"""后处理生成的文本"""
# 清理文本
cleaned_text = raw_text.strip()
# 移除可能的API响应格式标记
patterns_to_remove = [
r'^```(?:json|text)?\\n', # 代码块标记
r'\\n```$', # 结束代码块
r'{\"content\":\"', # JSON格式开头
r'"}$', # JSON格式结尾
]
for pattern in patterns_to_remove:
cleaned_text = re.sub(pattern, '', cleaned_text)
# 处理转义字符
cleaned_text = cleaned_text.replace('\\n', '\\n').replace('\\"', '"')
# 确保文本以合适的标点结束
if cleaned_text and cleaned_text[-1] not in ['。', '!', '?', '.', '!', '?']:
cleaned_text += '。'
return cleaned_text
def _save_to_history(self, story_data: Dict[str, Any]):
"""保存故事到历史记录"""
if story_data["success"]:
# 为故事分配ID
story_id = f"story_{len(self.story_history) + 1}_{int(time.time())}"
story_data["id"] = story_id
# 添加到历史记录
self.story_history.append(story_data)
# 限制历史记录大小
if len(self.story_history) > self.max_history_size:
self.story_history = self.story_history[-self.max_history_size:]
def get_story_history(self, limit: int = 10) -> List[Dict[str, Any]]:
"""获取故事历史记录"""
return self.story_history[-limit:]
def get_story_by_id(self, story_id: str) -> Optional[Dict[str, Any]]:
"""根据ID获取故事"""
for story in self.story_history:
if story.get("id") == story_id:
return story
return None
def delete_story(self, story_id: str) -> bool:
"""删除指定故事"""
for i, story in enumerate(self.story_history):
if story.get("id") == story_id:
self.story_history.pop(i)
return True
return False
def export_story(self, story_id: str, format: str = "txt") -> str:
"""导出故事到指定格式"""
story = self.get_story_by_id(story_id)
if not story:
raise ValueError("故事不存在")
if format == "txt":
return self._export_to_txt(story)
elif format == "json":
return self._export_to_json(story)
elif format == "html":
return self._export_to_html(story)
else:
raise ValueError(f"不支持的格式: {format}")
def _export_to_txt(self, story: Dict[str, Any]) -> str:
"""导出为TXT格式"""
content = f"""故事标题: {story['theme']} - {story['style']}
生成时间: {story['timestamp']}
故事字数: {story['word_count']}
生成模型: {story['model_used']}
{story['content']}
"""
return content
def _export_to_json(self, story: Dict[str, Any]) -> str:
"""导出为JSON格式"""
export_data = {
"id": story["id"],
"theme": story["theme"],
"style": story["style"],
"length": story["length"],
"content": story["content"],
"timestamp": story["timestamp"],
"word_count": story["word_count"],
"model_used": story["model_used"],
"generation_time": story["generation_time"]
}
return json.dumps(export_data, ensure_ascii=False, indent=2)
def _export_to_html(self, story: Dict[str, Any]) -> str:
"""导出为HTML格式"""
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{story['theme']} - {story['style']}</title>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 40px; }}
.header {{ border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }}
.content {{ white-space: pre-wrap; font-size: 16px; }}
.metadata {{ color: #666; font-size: 14px; margin-bottom: 20px; }}
</style>
</head>
<body>
<div class="header">
<h1>{story['theme']} - {story['style']}</h1>
</div>
<div class="metadata">
<p>生成时间: {story['timestamp']}</p>
<p>故事字数: {story['word_count']}</p>
<p>生成模型: {story['model_used']}</p>
<p>生成耗时: {story['generation_time']}秒</p>
</div>
<div class="content">{story['content']}</div>
</body>
</html>
"""
return html_content
步骤2:创建Web路由
创建 src/web/routes/text_routes.py:
from flask import Blueprint, request, jsonify, render_template
from src.services.text_service import TextGenerationService
# 创建蓝图
text_bp = Blueprint('text', __name__)
def init_text_routes(app, aliyun_client):
"""初始化文字生成路由"""
text_service = TextGenerationService(aliyun_client)
@text_bp.route('/api/generate_story', methods=['POST'])
def generate_story():
"""生成故事API"""
try:
data = request.get_json()
# 验证必需参数
required_fields = ['theme', 'style', 'length']
for field in required_fields:
if field not in data:
return jsonify({
'success': False,
'error': f'缺少必需参数: {field}'
}), 400
# 调用服务生成故事
result = text_service.generate_story(
theme=data['theme'],
style=data['style'],
length=data['length'],
custom_prompt=data.get('custom_prompt', ''),
model=data.get('model', 'qwen-max'),
temperature=float(data.get('temperature', 0.7))
)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'error': f'生成故事失败: {str(e)}'
}), 500
@text_bp.route('/api/story_history', methods=['GET'])
def get_story_history():
"""获取故事历史记录"""
try:
limit = int(request.args.get('limit', 10))
history = text_service.get_story_history(limit)
return jsonify({
'success': True,
'history': history
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@text_bp.route('/api/story_templates', methods=['GET'])
def get_story_templates():
"""获取故事模板信息"""
try:
themes = text_service.get_available_themes()
templates = {}
for theme in themes:
templates[theme] = {
'styles': text_service.get_available_styles(theme),
'length_options': text_service.get_length_options(theme)
}
return jsonify({
'success': True,
'templates': templates
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
# 注册蓝图
app.register_blueprint(text_bp, url_prefix='/text')
步骤3:创建前端界面
创建 src/web/templates/text_generation.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI故事生成器</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.story-generator {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.btn-generate {
background: linear-gradient(45deg, #ff6b6b, #feca57);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 18px;
cursor: pointer;
transition: transform 0.3s;
}
.btn-generate:hover {
transform: translateY(-2px);
}
.story-result {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
display: none;
}
.loading {
text-align: center;
padding: 20px;
display: none;
}
.error-message {
color: #e74c3c;
background: #ffeaea;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
display: none;
}
</style>
</head>
<body>
<div class="story-generator">
<h1>🎭 AI故事生成器</h1>
<div class="form-container">
<div class="form-group">
<label for="theme">故事主题:</label>
<select id="theme" class="form-control">
<option value="">请选择主题...</option>
</select>
</div>
<div class="form-group">
<label for="style">故事风格:</label>
<select id="style" class="form-control" disabled>
<option value="">请先选择主题</option>
</select>
</div>
<div class="form-group">
<label for="length">故事长度:</label>
<select id="length" class="form-control" disabled>
<option value="">请先选择主题</option>
</select>
</div>
<div class="form-group">
<label for="custom_prompt">自定义提示词 (可选):</label>
<textarea id="custom_prompt" class="form-control" rows="3"
placeholder="可以在这里输入特定的故事要求..."></textarea>
</div>
<button id="generateBtn" class="btn-generate">✨ 生成故事</button>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>AI正在创作中,请稍候...</p>
</div>
<div id="errorMessage" class="error-message"></div>
<div id="storyResult" class="story-result">
<h3>📖 生成的故事</h3>
<div id="storyContent" class="story-content"></div>
<div class="story-meta">
<p><strong>生成时间:</strong> <span id="generationTime"></span></p>
<p><strong>故事字数:</strong> <span id="wordCount"></span></p>
<div class="action-buttons">
<button id="copyBtn" class="btn-secondary">📋 复制文本</button>
<button id="downloadBtn" class="btn-secondary">💾 下载故事</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/text_generation.js') }}"></script>
</body>
</html>
步骤4:创建JavaScript逻辑
创建 src/web/static/js/text_generation.js:
class StoryGenerator {
constructor() {
this.themeSelect = document.getElementById('theme');
this.styleSelect = document.getElementById('style');
this.lengthSelect = document.getElementById('length');
this.generateBtn = document.getElementById('generateBtn');
this.loadingDiv = document.getElementById('loading');
this.errorDiv = document.getElementById('errorMessage');
this.resultDiv = document.getElementById('storyResult');
this.storyContent = document.getElementById('storyContent');
this.init();
}
async init() {
await this.loadTemplates();
this.setupEventListeners();
}
async loadTemplates() {
try {
const response = await fetch('/text/api/story_templates');
const data = await response.json();
if (data.success) {
this.templates = data.templates;
this.populateThemes();
} else {
this.showError('加载模板失败: ' + data.error);
}
} catch (error) {
this.showError('网络错误: ' + error.message);
}
}
populateThemes() {
this.themeSelect.innerHTML = '<option value="">请选择主题...</option>';
Object.keys(this.templates).forEach(theme => {
const option = document.createElement('option');
option.value = theme;
option.textContent = theme;
this.themeSelect.appendChild(option);
});
}
setupEventListeners() {
this.themeSelect.addEventListener('change', (e) => {
this.onThemeChange(e.target.value);
});
this.generateBtn.addEventListener('click', () => {
this.generateStory();
});
// 复制按钮事件
document.getElementById('copyBtn')?.addEventListener('click', () => {
this.copyToClipboard();
});
// 下载按钮事件
document.getElementById('downloadBtn')?.addEventListener('click', () => {
this.downloadStory();
});
}
onThemeChange(theme) {
if (!theme) {
this.styleSelect.innerHTML = '<option value="">请先选择主题</option>';
this.lengthSelect.innerHTML = '<option value="">请先选择主题</option>';
this.styleSelect.disabled = true;
this.lengthSelect.disabled = true;
return;
}
const template = this.templates[theme];
// 填充风格选项
this.styleSelect.innerHTML = '';
template.styles.forEach(style => {
const option = document.createElement('option');
option.value = style;
option.textContent = style;
this.styleSelect.appendChild(option);
});
this.styleSelect.disabled = false;
// 填充长度选项
this.lengthSelect.innerHTML = '';
template.length_options.forEach(length => {
const option = document.createElement('option');
option.value = length;
option.textContent = length;
this.lengthSelect.appendChild(option);
});
this.lengthSelect.disabled = false;
}
async generateStory() {
const theme = this.themeSelect.value;
const style = this.styleSelect.value;
const length = this.lengthSelect.value;
const customPrompt = document.getElementById('custom_prompt').value;
if (!theme || !style || !length) {
this.showError('请完整填写故事参数');
return;
}
this.showLoading(true);
this.hideError();
this.hideResult();
try {
const response = await fetch('/text/api/generate_story', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
theme: theme,
style: style,
length: length,
custom_prompt: customPrompt
})
});
const data = await response.json();
if (data.success) {
this.displayResult(data);
} else {
this.showError('生成失败: ' + data.error);
}
} catch (error) {
this.showError('网络错误: ' + error.message);
} finally {
this.showLoading(false);
}
}
displayResult(data) {
this.storyContent.textContent = data.content;
document.getElementById('generationTime').textContent = data.generation_time + '秒';
document.getElementById('wordCount').textContent = data.word_count + '字';
this.resultDiv.style.display = 'block';
this.resultDiv.scrollIntoView({ behavior: 'smooth' });
}
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.storyContent.textContent);
alert('故事内容已复制到剪贴板!');
} catch (error) {
alert('复制失败,请手动选择文本复制');
}
}
downloadStory() {
const content = this.storyContent.textContent;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `故事_${new Date().toISOString().split('T')[0]}.txt`;
a.click();
URL.revokeObjectURL(url);
}
showLoading(show) {
this.loadingDiv.style.display = show ? 'block' : 'none';
this.generateBtn.disabled = show;
}
showError(message) {
this.errorDiv.textContent = message;
this.errorDiv.style.display = 'block';
}
hideError() {
this.errorDiv.style.display = 'none';
}
hideResult() {
this.resultDiv.style.display = 'none';
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new StoryGenerator();
});
文字生成服务类实现
import requests
import json
from typing import Dict, Any, Optional
class TextGenerationService:
"""文字生成服务类"""
def __init__(self, api_key=None, base_url=None):
self.api_key = api_key or os.getenv('ALIYUN_API_KEY')
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
# 默认模型配置
self.default_model = "qwen-turbo"
self.max_tokens = 2000
self.temperature = 0.7
def _get_headers(self):
"""获取API请求头"""
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
def generate_text(self, prompt, model, max_tokens, temperature):
"""生成文字"""
response = requests.post(self.base_url, json= {
"prompt": prompt,
"model": model,
"max_tokens": max_tokens,
"temperature": temperature
}, headers=self._get_headers())
result = response.json()
return result
def _get_headers(self):
"""获取API请求头"""
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
def generate_text(self, prompt, model, max_tokens, temperature):
"""生成文字"""
response = requests.post(self.base_url, json= {
"prompt": prompt,
"model": model,
"max_tokens": max_tokens,
"temperature": temperature
}, headers=self._get_headers())
result = response.json()
return result
### Web API路由实现
```python
from flask import Blueprint, request, jsonify
from services.text_service import TextGenerationService
text_bp = Blueprint('text', __name__)
@text_bp.route('/generate', methods=['POST'])
def generate_text():
"""文字生成API端点"""
try:
data = request.get_json()
# 验证输入参数
if not data or 'prompt' not in data:
return jsonify({"error": "缺少prompt参数"}), 400
prompt = data['prompt']
model = data.get('model', 'qwen-turbo')
max_tokens = data.get('max_tokens', 2000)
temperature = data.get('temperature', 0.7)
# 创建服务实例
service = TextGenerationService()
# 生成文字
result = service.generate_text(
prompt=prompt,
model=model,
max_tokens=max_tokens,
temperature=temperature
)
return jsonify({
"success": True,
"result": result,
"model": model
})
except Exception as e:
return jsonify({"error": str(e)}), 500
if name == "main":
unittest.main()

浙公网安备 33010602011771号