EduTechTeam commited on
Commit
8a1b08d
·
verified ·
1 Parent(s): d0cc0ea

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +356 -0
app.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from openai import OpenAI
3
+ import requests
4
+ from PIL import Image
5
+ from io import BytesIO
6
+ import hashlib
7
+ import json
8
+ import os
9
+ from datetime import datetime, timedelta
10
+ from functools import wraps
11
+ import time
12
+ from typing import Generator
13
+ import asyncio
14
+ from pathlib import Path
15
+
16
+ # 常量定義
17
+ LANGUAGES = ["中文", "英文", "日文", "韓文", "閩南語"]
18
+ STORY_TYPES = ["童話", "科幻", "懸疑", "奇幻", "歷史", "愛情"]
19
+ IMAGE_STYLES = ["寫實", "卡通", "水彩", "素描", "國畫", "書法"]
20
+
21
+ # 自定義錯誤類
22
+ class APIError(Exception):
23
+ """自定義 API 錯誤"""
24
+ pass
25
+
26
+ # 緩存系統
27
+ class StoryCache:
28
+ def __init__(self, cache_dir="story_cache"):
29
+ self.cache_dir = Path(cache_dir)
30
+ self.cache_dir.mkdir(exist_ok=True)
31
+
32
+ def _get_cache_key(self, **kwargs):
33
+ """生成緩存鍵"""
34
+ cache_str = json.dumps(kwargs, sort_keys=True)
35
+ return hashlib.md5(cache_str.encode()).hexdigest()
36
+
37
+ def get(self, **kwargs):
38
+ """獲取緩存的故事"""
39
+ cache_key = self._get_cache_key(**kwargs)
40
+ cache_file = self.cache_dir / f"{cache_key}.json"
41
+
42
+ if cache_file.exists():
43
+ with cache_file.open('r', encoding='utf-8') as f:
44
+ cached_data = json.load(f)
45
+ # 檢查緩存是否過期(24小時)
46
+ if datetime.fromisoformat(cached_data['timestamp']) + timedelta(hours=24) > datetime.now():
47
+ return cached_data['content']
48
+ return None
49
+
50
+ def set(self, content, **kwargs):
51
+ """設置故事緩存"""
52
+ cache_key = self._get_cache_key(**kwargs)
53
+ cache_file = self.cache_dir / f"{cache_key}.json"
54
+
55
+ cache_data = {
56
+ 'content': content,
57
+ 'timestamp': datetime.now().isoformat(),
58
+ 'metadata': kwargs
59
+ }
60
+
61
+ with cache_file.open('w', encoding='utf-8') as f:
62
+ json.dump(cache_data, f, ensure_ascii=False, indent=2)
63
+
64
+ # 初始化緩存
65
+ story_cache = StoryCache()
66
+
67
+ # 錯誤處理裝飾器
68
+ def handle_api_error(func):
69
+ @wraps(func)
70
+ def wrapper(*args, **kwargs):
71
+ try:
72
+ return func(*args, **kwargs)
73
+ except APIError as e:
74
+ return str(e)
75
+ except Exception as e:
76
+ return f"發生錯誤: {str(e)}\n請稍後再試或聯繫支持團隊。"
77
+ return wrapper
78
+
79
+ def validate_api_key(api_key):
80
+ """驗證 API 密鑰"""
81
+ if not api_key or len(api_key.strip()) < 10:
82
+ raise APIError("請提供有效的 API 密鑰")
83
+ return api_key.strip()
84
+
85
+ @handle_api_error
86
+ def generate_story(prompt, story_type, language, api_key):
87
+ """生成故事"""
88
+ api_key = validate_api_key(api_key)
89
+ client = OpenAI(api_key=api_key)
90
+
91
+ system_prompt = f"""你是一位專業的{story_type}故事作家。請遵循以下準則:
92
+ 1. 使用{language}創作
93
+ 2. 故事結構需包含:開頭、發展、高潮、結局
94
+ 3. 根據故事類型添加相應的元素和氛圍
95
+ 4. 確保故事適合所有年齡層
96
+ 5. 字數控制在1000字左右
97
+ """
98
+
99
+ try:
100
+ response = client.chat.completions.create(
101
+ model="gpt-3.5-turbo",
102
+ messages=[
103
+ {"role": "system", "content": system_prompt},
104
+ {"role": "user", "content": f"寫一個關於{prompt}的{story_type}短篇故事"}
105
+ ],
106
+ temperature=0.7,
107
+ max_tokens=2000
108
+ )
109
+ return response.choices[0].message.content
110
+ except Exception as e:
111
+ raise APIError(f"故事生成失敗: {str(e)}")
112
+
113
+ @handle_api_error
114
+ def translate_story(story, target_language, api_key):
115
+ """翻譯故事"""
116
+ client = OpenAI(api_key=api_key)
117
+ try:
118
+ response = client.chat.completions.create(
119
+ model="gpt-3.5-turbo",
120
+ messages=[
121
+ {"role": "system", "content": f"你是一位專業翻譯。請將以下故事翻譯成{target_language},保持原文的風格和感情。"},
122
+ {"role": "user", "content": story}
123
+ ],
124
+ temperature=0.3
125
+ )
126
+ return response.choices[0].message.content
127
+ except Exception as e:
128
+ raise APIError(f"翻譯錯誤: {str(e)}")
129
+
130
+ @handle_api_error
131
+ def generate_image(prompt, style, api_key):
132
+ """生成圖片"""
133
+ client = OpenAI(api_key=api_key)
134
+ try:
135
+ response = client.images.generate(
136
+ prompt=f"以{style}風格為一個關於{prompt}的故事創作插圖。畫面需要精美細緻,構圖完整。",
137
+ size="1024x1024",
138
+ n=1,
139
+ quality="standard"
140
+ )
141
+ image_url = response.data[0].url
142
+ image_response = requests.get(image_url)
143
+ return Image.open(BytesIO(image_response.content))
144
+ except Exception as e:
145
+ raise APIError(f"圖片生成失敗: {str(e)}")
146
+
147
+ def process_with_progress(prompt, story_type, original_language, target_language, image_style, api_key) -> Generator:
148
+ """使用生成器提供進度更新"""
149
+ steps = [
150
+ ("正在構思故事...", 0.2),
151
+ ("正在生成故事內容...", 0.4),
152
+ ("正在生成插圖...", 0.7),
153
+ ("正在翻譯故事...", 0.9),
154
+ ("完成!", 1.0)
155
+ ]
156
+
157
+ original_story = None
158
+ translated_story = None
159
+ image = None
160
+
161
+ try:
162
+ for message, progress in steps:
163
+ yield {"progress": progress, "message": message, "status": "running"}
164
+
165
+ if progress == 0.4:
166
+ # 檢查緩存
167
+ cache_key = {
168
+ 'prompt': prompt,
169
+ 'story_type': story_type,
170
+ 'language': original_language
171
+ }
172
+ original_story = story_cache.get(**cache_key)
173
+ if not original_story:
174
+ original_story = generate_story(prompt, story_type, original_language, api_key)
175
+ story_cache.set(original_story, **cache_key)
176
+
177
+ elif progress == 0.7:
178
+ image = generate_image(prompt, image_style, api_key)
179
+
180
+ elif progress == 0.9 and target_language != original_language:
181
+ translated_story = translate_story(original_story, target_language, api_key)
182
+
183
+ yield {
184
+ "progress": 1.0,
185
+ "message": "生成完成!",
186
+ "status": "complete",
187
+ "results": {
188
+ "original_story": original_story,
189
+ "translated_story": translated_story if target_language != original_language else "",
190
+ "image": image
191
+ }
192
+ }
193
+ except Exception as e:
194
+ yield {
195
+ "progress": 0,
196
+ "message": f"發生錯誤: {str(e)}",
197
+ "status": "error"
198
+ }
199
+
200
+ def save_story(story, filename="story.txt"):
201
+ """保存故事"""
202
+ try:
203
+ with open(filename, 'w', encoding='utf-8') as f:
204
+ f.write(story)
205
+ return f"故事已保存為 {filename}"
206
+ except Exception as e:
207
+ return f"故事保存失敗: {str(e)}"
208
+
209
+ def save_image(image, filename="illustration.png"):
210
+ """保存圖片"""
211
+ try:
212
+ if image is not None:
213
+ image.save(filename)
214
+ return f"插圖已保存為 {filename}"
215
+ return "沒有可保存的插圖"
216
+ except Exception as e:
217
+ return f"插圖保存失敗: {str(e)}"
218
+
219
+ def export_to_pdf(story, translated_story, image, filename="story_book.pdf"):
220
+ """導出為PDF"""
221
+ try:
222
+ from reportlab.pdfgen import canvas
223
+ from reportlab.lib.pagesizes import A4
224
+ from reportlab.pdfbase import pdfmetrics
225
+ from reportlab.pdfbase.ttfonts import TTFont
226
+
227
+ # 註冊中文字體(需要自行提供字體文件)
228
+ try:
229
+ pdfmetrics.registerFont(TTFont('ChineseFont', 'path/to/chinese/font.ttf'))
230
+ except:
231
+ pass # 如果沒有中文字體,使用默認字體
232
+
233
+ c = canvas.Canvas(filename, pagesize=A4)
234
+ width, height = A4
235
+
236
+ # 添加標題
237
+ c.setFont('Helvetica-Bold', 24)
238
+ c.drawCentredString(width/2, height-50, "創意故事")
239
+
240
+ # 添加原文
241
+ c.setFont('ChineseFont' if 'ChineseFont' in pdfmetrics.getRegisteredFontNames() else 'Helvetica', 12)
242
+ text_object = c.beginText()
243
+ text_object.setTextOrigin(50, height-100)
244
+ for line in story.split('\n'):
245
+ text_object.textLine(line)
246
+ c.drawText(text_object)
247
+
248
+ # 添加翻譯
249
+ if translated_story:
250
+ c.showPage() # 新頁面
251
+ text_object = c.beginText()
252
+ text_object.setTextOrigin(50, height-50)
253
+ for line in translated_story.split('\n'):
254
+ text_object.textLine(line)
255
+ c.drawText(text_object)
256
+
257
+ # 添加圖片
258
+ if image:
259
+ c.showPage()
260
+ image_path = "temp_illustration.png"
261
+ image.save(image_path)
262
+ c.drawImage(image_path, 50, height-500, width=400, height=400)
263
+ os.remove(image_path)
264
+
265
+ c.save()
266
+ return f"已導出為 PDF: {filename}"
267
+ except Exception as e:
268
+ return f"PDF 導出失敗: {str(e)}"
269
+
270
+ # Gradio 介面
271
+ with gr.Blocks(css="""
272
+ .container { max-width: 900px; margin: auto; padding: 20px; }
273
+ .output-box { border: 1px solid #ddd; padding: 10px; border-radius: 5px; }
274
+ .btn { background-color: #4CAF50; color: white; padding: 10px 20px; border-radius: 5px; border: none; }
275
+ .btn:hover { background-color: #45a049; }
276
+ """) as demo:
277
+ gr.Markdown("# 🌟 創意故事與插畫生成器 🎨")
278
+
279
+ with gr.Row():
280
+ with gr.Column(scale=2):
281
+ prompt = gr.Textbox(label="✍️ 故事靈感", placeholder="例如:一隻會說話的貓咪")
282
+ with gr.Column(scale=1):
283
+ api_key = gr.Textbox(label="🔑 OpenAI API 密鑰", type="password")
284
+
285
+ with gr.Row():
286
+ with gr.Column():
287
+ story_type = gr.Dropdown(STORY_TYPES, label="📚 故事類型", value="童話")
288
+ original_language = gr.Dropdown(LANGUAGES, label="🗣️ 原始語言", value="中文")
289
+ with gr.Column():
290
+ image_style = gr.Dropdown(IMAGE_STYLES, label="🖌️ 插圖風格", value="水彩")
291
+ target_language = gr.Dropdown(LANGUAGES, label="🌐 翻譯語言", value="英文")
292
+
293
+ # 添加進度顯示
294
+ status = gr.Markdown("準備就緒")
295
+ progress = gr.Progress()
296
+
297
+ generate_btn = gr.Button("🚀 開始創作", elem_classes="btn")
298
+
299
+ with gr.Row():
300
+ with gr.Column():
301
+ original_story = gr.Textbox(label="📖 原始故事", elem_classes="output-box")
302
+ translated_story = gr.Textbox(label="🔄 翻譯後的故事", elem_classes="output-box")
303
+ with gr.Column():
304
+ image_output = gr.Image(label="🖼️ 生成的插圖")
305
+
306
+ with gr.Row():
307
+ save_story_btn = gr.Button("💾 保存故事", elem_classes="btn")
308
+ save_image_btn = gr.Button("💾 保存插圖", elem_classes="btn")
309
+ export_pdf_btn = gr.Button("📑 導出PDF", elem_classes="btn")
310
+
311
+ save_status = gr.Textbox(label="保存狀態")
312
+
313
+ def process_with_ui_updates(prompt, story_type, original_language, target_language, image_style, api_key):
314
+ for update in process_with_progress(prompt, story_type, original_language, target_language, image_style, api_key):
315
+ status.value = update["message"]
316
+ if update["status"] == "complete":
317
+ return (
318
+ update["results"]["original_story"],
319
+ update["results"]["translated_story"],
320
+ update["results"]["image"]
321
+ )
322
+ elif update["status"] == "error":
323
+ return update["message"], "", None
324
+
325
+ # 事件處理
326
+ generate_btn.click(
327
+ fn=process_with_ui_updates,
328
+ inputs=[prompt, story_type, original_language, target_language, image_style, api_key],
329
+ outputs=[original_story, translated_story, image_output]
330
+ )
331
+
332
+ save_story_btn.click(
333
+ fn=save_story,
334
+ inputs=[original_story],
335
+ outputs=[save_status]
336
+ )
337
+
338
+ save_image_btn.click(
339
+ fn=save_image,
340
+ inputs=[image_output],
341
+ outputs=[save_status]
342
+ )
343
+
344
+ export_pdf_btn.click(
345
+ fn=export_to_pdf,
346
+ inputs=[original_story, translated_story, image_output],
347
+ outputs=[save_status]
348
+ )
349
+
350
+ gr.Markdown("""
351
+ ### 📌 使用說明
352
+ 1. 輸入你的創意故事靈感
353
+ 2. 選擇故事類型、語言和插圖風格""")
354
+
355
+ if __name__ == "__main__":
356
+ demo.launch()