-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
374 lines (322 loc) · 13.7 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
from gpt_researcher import GPTResearcher
import asyncio
import os
from typing import Dict, Any, List, Tuple, Optional, Protocol, Union, cast
from pptx import Presentation
from pptx.util import Inches, Pt, Length
from pptx.enum.text import PP_ALIGN
from pptx.shapes.base import BaseShape
from pptx.shapes.autoshape import Shape
from pptx.text.text import _Run, _Paragraph
from pptx.shapes.placeholder import SlidePlaceholder
from openai import AsyncOpenAI
import aiohttp
from yarl import URL
from pptx.shapes.placeholder import SlidePlaceholder
class TextFrameProtocol(Protocol):
def clear(self) -> None: ...
def add_paragraph(self) -> _Paragraph: ...
@property
def paragraphs(self) -> List[_Paragraph]: ...
class FontProtocol(Protocol):
@property
def name(self) -> str: ...
@name.setter
def name(self, value: str) -> None: ...
@property
def size(self) -> Length: ...
@size.setter
def size(self, value: Length) -> None: ...
class ParagraphProtocol(Protocol):
@property
def text(self) -> str: ...
@text.setter
def text(self, value: str) -> None: ...
@property
def font(self) -> FontProtocol: ...
@property
def level(self) -> int: ...
@level.setter
def level(self, value: int) -> None: ...
class ShapeProtocol(Protocol):
@property
def text_frame(self) -> TextFrameProtocol: ...
@property
def text(self) -> str: ...
class PlaceholderProtocol(Protocol):
@property
def text_frame(self) -> TextFrameProtocol: ...
@property
def text(self) -> str: ...
def safe_get_text_frame(
shape: Optional[Union[ShapeProtocol, Shape, PlaceholderProtocol, SlidePlaceholder]]
) -> Optional[TextFrameProtocol]:
"""安全にtext_frameを取得するヘルパー関数"""
if shape is None:
return None
if not hasattr(shape, 'text_frame'):
return None
return cast(TextFrameProtocol, shape.text_frame)
class PowerPointGenerator:
def __init__(self, api_key: str):
"""PowerPointGeneratorの初期化"""
self.prs = Presentation()
self._setup_slide_layouts()
self.client = AsyncOpenAI(api_key=api_key)
def _setup_slide_layouts(self):
"""スライドのレイアウトを設定"""
self.prs.slide_width = Inches(16)
self.prs.slide_height = Inches(9)
# スライドマスターの設定を調整
for layout in self.prs.slide_layouts:
for placeholder in layout.placeholders:
try:
text_frame = safe_get_text_frame(placeholder)
if text_frame:
for paragraph in text_frame.paragraphs:
p = cast(ParagraphProtocol, paragraph)
p.font.name = 'BIZ UDPゴシック'
except Exception:
continue
async def _generate_and_save_image(self, prompt: str, image_path: str) -> Optional[str]:
"""DALL-E 3を使用して画像を生成して保存"""
try:
response = await self.client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1792x1024",
quality="standard",
n=1,
)
image_url = str(response.data[0].url)
if not image_url:
return None
async with aiohttp.ClientSession() as session:
async with session.get(URL(image_url)) as response:
if response.status == 200:
with open(image_path, 'wb') as f:
f.write(await response.read())
return image_path
else:
print(f"画像のダウンロードに失敗しました: {response.status}")
return None
except Exception as e:
print(f"画像生成エラー: {str(e)}")
return None
def _add_title_slide(self, title: str, output_dir: str):
"""タイトルスライドを追加"""
slide_layout = self.prs.slide_layouts[0]
slide = self.prs.slides.add_slide(slide_layout)
# タイトルの設定
text_frame = safe_get_text_frame(slide.shapes.title)
if text_frame:
try:
text_frame.clear()
p = cast(ParagraphProtocol, text_frame.paragraphs[0])
p.text = title
p.font.name = 'BIZ UDPゴシック'
p.font.size = Pt(32)
except Exception:
print("タイトルの設定に失敗しました")
# 背景画像の設定
image_path = os.path.join(output_dir, "title_image.png")
if os.path.exists(image_path):
try:
left = Inches(0)
top = Inches(0)
width = self.prs.slide_width
height = self.prs.slide_height
picture = slide.shapes.add_picture(image_path, left, top, width, height)
try:
# 画像を最背面に移動
slide.shapes._spTree.insert(0, picture._element)
except Exception:
print("画像の重ね順の設定に失敗しました")
except Exception as e:
print(f"背景画像の設定に失敗しました: {str(e)}")
async def _add_content_slide(self, title: str, content: str, output_dir: str, slide_num: int):
"""コンテンツスライドを追加"""
slide_layout = self.prs.slide_layouts[1]
slide = self.prs.slides.add_slide(slide_layout)
# タイトルの設定
text_frame = safe_get_text_frame(slide.shapes.title)
if text_frame:
try:
text_frame.clear()
p = cast(ParagraphProtocol, text_frame.paragraphs[0])
p.text = title
p.font.name = 'BIZ UDPゴシック'
p.font.size = Pt(28)
except Exception:
print("スライドタイトルの設定に失敗しました")
# 本文の設定
if len(slide.placeholders) > 1:
placeholder = cast(PlaceholderProtocol, slide.placeholders[1])
text_frame = safe_get_text_frame(placeholder)
if text_frame:
try:
text_frame.clear()
for para_text in content.split('\n'):
if para_text.strip():
p = cast(ParagraphProtocol, text_frame.add_paragraph())
p.text = para_text
p.font.name = 'BIZ UDPゴシック'
p.font.size = Pt(16)
if para_text.startswith('•') or para_text.startswith('-'):
p.level = 1
else:
p.level = 0
except Exception:
print("本文の設定に失敗しました")
def _parse_research_content(self, content: str) -> Tuple[str, List[Dict[str, str]]]:
"""研究内容をスライド用に解析"""
slides_data = []
# Marpのフロントマター(---で囲まれた部分)を除去
content_parts = content.split('---\n')
if len(content_parts) > 2:
# フロントマターを除いた部分を使用
content = '---\n'.join(content_parts[2:])
else:
content = content_parts[-1]
# スライドを正しく分割
slides = content.split('\n---\n')
# 最初のスライド(タイトルスライド)の処理
first_slide = slides[0].strip()
lines = first_slide.split('\n')
title = ''
content_lines = []
for line in lines:
if line.startswith('# '):
title = line.replace('# ', '').strip()
elif line.strip():
content_lines.append(line.strip())
if not title:
title = "無題のプレゼンテーション"
if content_lines:
slides_data.append({
'title': 'はじめに',
'content': '\n'.join(content_lines)
})
# 残りのスライドの処理
for slide in slides[1:]:
if not slide.strip():
continue
lines = slide.strip().split('\n')
slide_title = ''
slide_content_lines = []
for line in lines:
if line.startswith('## '):
slide_title = line.replace('## ', '').strip()
elif line.strip():
# 箇条書きを日本語スタイルに調整
line = line.replace('* ', '• ').replace('- ', '• ')
slide_content_lines.append(line.strip())
if slide_title and slide_content_lines:
slides_data.append({
'title': slide_title,
'content': '\n'.join(slide_content_lines)
})
elif slide_content_lines: # タイトルがない場合
slides_data.append({
'title': '続き',
'content': '\n'.join(slide_content_lines)
})
return title, slides_data
async def create_presentation(self, markdown_content: str, output_dir: str) -> str:
"""PowerPointプレゼンテーションを作成"""
os.makedirs(output_dir, exist_ok=True)
try:
title, slides_data = self._parse_research_content(markdown_content)
# タイトルスライド用の画像生成プロンプト
title_image_prompt = f"""
以下のトピックに関するプレゼンテーション表紙の画像を作成:
{title}
スタイル:
- モダンでプロフェッショナルなデザイン
- ビジネスプレゼンテーションに適した抽象的な背景
- 清潔で洗練された印象
- 日本のビジネス文化に適した控えめな配色
"""
await self._generate_and_save_image(
title_image_prompt,
os.path.join(output_dir, "title_image.png")
)
self._add_title_slide(title, output_dir)
for i, slide_data in enumerate(slides_data):
await self._add_content_slide(
slide_data['title'],
slide_data['content'],
output_dir,
i + 1
)
pptx_path = os.path.join(output_dir, "presentation.pptx")
self.prs.save(pptx_path)
# ファイルが正しく保存されたことを確認
if not os.path.exists(pptx_path) or os.path.getsize(pptx_path) == 0:
raise Exception("プレゼンテーションファイルの保存に失敗しました")
return pptx_path
except Exception as e:
print(f"プレゼンテーション作成中にエラーが発生しました: {str(e)}")
raise
async def get_report(query: str, report_type: str) -> str:
researcher = GPTResearcher(
query=query,
report_type=report_type,
report_format="markdown"
)
await researcher.conduct_research()
report = await researcher.write_report()
return report
async def translate_report(report: str, api_key: str) -> str:
"""レポートを日本語に翻訳する関数"""
client = AsyncOpenAI(api_key=api_key)
try:
response = await client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[
{
"role": "system",
"content": """あなたは英語から日本語への翻訳の専門家です。
入力された英語の文章を自然な日本語に翻訳してください。
Marp形式を維持して出力してください。具体的には:
- フロントマター(---で囲まれた設定部分)は保持
- スライド区切り(---)は保持
- 各スライドの見出し(#や##)は保持
- 箇条書きの形式は保持"""
},
{
"role": "user",
"content": report
}
],
temperature=0.3
)
translated_content = response.choices[0].message.content
if translated_content is None:
raise ValueError("翻訳結果が空でした")
print("翻訳が完了しました")
return translated_content
except Exception as e:
print(f"翻訳中にエラーが発生しました: {str(e)}")
raise
async def main():
try:
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY environment variable is not set")
query = input("研究するトピックを入力してください: ")
report_type = "research_report"
print("研究を開始しています...")
report = await get_report(query, report_type)
print("翻訳を実行しています...")
translated_report = await translate_report(report, api_key)
print("プレゼンテーションを作成しています...")
generator = PowerPointGenerator(api_key)
output_dir = "output"
pptx_path = await generator.create_presentation(translated_report, output_dir)
print(f"プレゼンテーションが作成されました: {pptx_path}")
except Exception as e:
print(f"エラーが発生しました: {str(e)}")
raise
if __name__ == "__main__":
asyncio.run(main())