第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()