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