File size: 10,927 Bytes
3edbc8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import re
import time
import json
import tempfile
import gradio as gr
from pptx import Presentation
from pptx.util import Pt, Inches, Emu
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.oxml.xmlchemy import OxmlElement
import google.generativeai as genai
from google.cloud import texttospeech
from google.cloud import storage
from google.oauth2 import service_account

# ===================================================================
# 1. 認証・環境設定
# ===================================================================

# 環境変数から設定を読み込む
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
GCP_JSON_CONTENT = os.environ.get("GCP_JSON")
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME")

# Geminiのセットアップ
if GEMINI_API_KEY:
    genai.configure(api_key=GEMINI_API_KEY)
    gemini_model = genai.GenerativeModel('gemini-1.5-pro') # 安定版推奨
else:
    print("Warning: GEMINI_API_KEY is not set.")

# GCP認証情報のセットアップ(JSON文字列を一時ファイルに書き出して使用)
def setup_gcp_credentials():
    if not GCP_JSON_CONTENT:
        return None
    try:
        # 一時ファイルを作成
        with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
            f.write(GCP_JSON_CONTENT)
            return f.name
    except Exception as e:
        print(f"Error setting up GCP credentials: {e}")
        return None

GCP_CREDENTIALS_PATH = setup_gcp_credentials()
if GCP_CREDENTIALS_PATH:
    os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = GCP_CREDENTIALS_PATH

# フォント等の設定(元のコードから継承)
FONT_SETTINGS = {
    'FONT_FAMILY': '游ゴシック', # クラウド上では代替フォントになる可能性があります
    'CONTENT_PAGE_MAIN_TITLE_SIZE': Pt(20),
    'CONTENT_PAGE_SUB_TITLE_SIZE': Pt(12),
    'CONTENT_SECTION_TITLE_SIZE': Pt(14),
    'CONTENT_SUBSECTION_TITLE_SIZE': Pt(12),
    'CONTENT_BODY_SIZE': Pt(12),
    'EMPHASIS_BOLD': True,
    'BODY_LINE_SPACING': 1.2,
    'CPL_FULLWIDTH': 37,
}

# プロンプト(元のコードと同じ)
GEMINI_PROMPT = """
あなたは優秀なテクニカルライターです。
以下のプレゼンテーションの情報を元に、読みやすい研修用マニュアルを作成してください。
(中略:元のプロンプトと同じ内容と仮定して簡略化しますが、実際は元の長いプロンプトを使用してください)
出力はマークダウン形式でお願いします。
# タイトル
## 1.章
### 01 | 大見出し
#### セクション
##### ■ 小タイトル
本文
"""
# ※スペース節約のためプロンプト全文は元のコードのものを使用してください。ここには簡易版を置いています。
# 実際の実装時は、元のコードの GEMINI_PROMPT と GEMINI_PROMPT_VIDEO をここに貼り付けてください。

# ===================================================================
# 2. ロジック関数群 (Tkinter依存を排除)
# ===================================================================

def extract_texts_from_pptx(pptx_path):
    presentation = Presentation(pptx_path)
    slide_texts, notes_texts = [], []
    for i, slide in enumerate(presentation.slides):
        slide_text_parts = [shape.text_frame.text.strip() for shape in slide.shapes if hasattr(shape, "text_frame") and shape.text_frame and shape.text_frame.text]
        if slide_text_parts:
            slide_texts.append(f"--- スライド {i+1} ---\n" + "\n".join(slide_text_parts))
        if slide.has_notes_slide and slide.notes_slide.notes_text_frame and slide.notes_slide.notes_text_frame.text:
            notes_texts.append(f"--- スライド {i+1} ノート ---\n{slide.notes_slide.notes_text_frame.text.strip()}")
    return "\n\n".join(slide_texts), "\n\n".join(notes_texts)

def pptx_to_markdown(file_obj):
    if not GEMINI_API_KEY:
        return "エラー: GEMINI_API_KEYが設定されていません。"
    
    pptx_path = file_obj.name
    slide_text, notes_text = extract_texts_from_pptx(pptx_path)
    
    # 実際はここで元の長い GEMINI_PROMPT を使用する
    full_prompt = f"""
    あなたは優秀なテクニカルライターです。以下の情報を元にマークダウン形式のマニュアルを作成してください。
    
    --- 元データ ---
    ■■■ スライド内容 ■■■
    {slide_text}
    ■■■ 発表者ノート ■■■
    {notes_text}
    """
    
    response = gemini_model.generate_content(full_prompt)
    
    # 出力ファイル作成
    output_filename = os.path.splitext(os.path.basename(pptx_path))[0] + ".txt"
    output_path = os.path.join(tempfile.gettempdir(), output_filename)
    
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(response.text)
        
    return response.text, output_path

def markdown_to_pptx(md_file, template_file):
    if md_file is None or template_file is None:
        return None
        
    # Markdown読み込み
    with open(md_file.name, 'r', encoding='utf-8') as f:
        md_text = f.read()
    
    # ここに元の parse_markdown_manual 関数の中身が必要
    # 簡略化のため、元のロジックを呼び出す形にします
    # (※元のコードの関数定義はクラス外にあるため、そのまま利用可能です)
    manual_data = parse_markdown_manual_logic(md_text) # 下部で定義
    
    # PPTX生成
    template_path = template_file.name
    output_pptx_path = os.path.join(tempfile.gettempdir(), "generated_manual.pptx")
    
    # ここに元の create_presentation_from_manual のロジックを統合
    # 簡略化のため、ロジックを実行するラッパー関数とします
    try:
        generate_pptx_logic(manual_data, template_path, output_pptx_path)
        return output_pptx_path
    except Exception as e:
        return f"エラーが発生しました: {str(e)}"

# 元のコードからロジック部分だけ抽出・適合させたヘルパー関数
def parse_markdown_manual_logic(text):
    # 元の parse_markdown_manual とほぼ同じロジック
    lines = text.split('\n')
    manual_data = {'title': '', 'chapters': []}
    current_chapter = None
    # ... (元のコードのパースロジックをここに移植してください) ...
    # 簡略化のためダミーデータを返します。実際は元のパーサーを使ってください
    return manual_data 

def generate_pptx_logic(manual_data, template_path, output_path):
    # 元の create_presentation_from_manual のロジック
    prs = Presentation(template_path)
    # ... (スライド生成ロジック) ...
    prs.save(output_path)

# TTS処理 (GCP)
def text_to_speech_gcs(md_file):
    if not GCP_CREDENTIALS_PATH or not GCS_BUCKET_NAME:
        return None, "エラー: GCP設定 (JSON/BUCKET) が不足しています。"

    with open(md_file.name, 'r', encoding='utf-8') as f:
        md_text = f.read()
    
    # SSML生成 (元の build_ssml_from_markdown_with_pauses を使用)
    ssml = build_ssml_logic(md_text) 
    
    try:
        client = texttospeech.TextToSpeechLongAudioSynthesizeClient()
        storage_client = storage.Client()
        
        output_filename = f"tts_{int(time.time())}.wav"
        gcs_uri = f"gs://{GCS_BUCKET_NAME}/{output_filename}"
        
        synthesis_input = texttospeech.SynthesisInput(ssml=ssml)
        voice = texttospeech.VoiceSelectionParams(language_code="ja-JP", name="ja-JP-Neural2-B")
        audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.LINEAR16, speaking_rate=1.2)
        
        parent = f"projects/{storage_client.project}/locations/us-central1"
        request = texttospeech.SynthesizeLongAudioRequest(
            parent=parent, input=synthesis_input, audio_config=audio_config, voice=voice, output_gcs_uri=gcs_uri
        )
        
        operation = client.synthesize_long_audio(request=request)
        response = operation.result(timeout=600)
        
        # ダウンロード
        bucket = storage_client.bucket(GCS_BUCKET_NAME)
        blob = bucket.blob(output_filename)
        local_path = os.path.join(tempfile.gettempdir(), output_filename)
        blob.download_to_filename(local_path)
        
        # GCS上のゴミ掃除
        blob.delete()
        
        return local_path, "完了"
        
    except Exception as e:
        return None, f"TTSエラー: {e}"

def build_ssml_logic(text):
    # 元の build_ssml_from_markdown_with_pauses 関数の簡易版
    escaped = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    return f"<speak>{escaped}</speak>"


# ===================================================================
# 3. Gradio UI
# ===================================================================

with gr.Blocks(title="マニュアル自動生成ツール (Cloud版)") as demo:
    gr.Markdown("# 育成マニュアル自動生成ツール")
    
    with gr.Tab("1. PPTX/動画 → Markdown"):
        gr.Markdown("PPTXまたはMP4ファイルをアップロードして、Geminiでテキスト化します。")
        input_file = gr.File(label="入力ファイル (PPTX/MP4)")
        btn_gen_md = gr.Button("Markdown生成")
        output_md_text = gr.Textbox(label="生成テキストプレビュー", lines=10)
        output_md_file = gr.File(label="ダウンロード用Markdown")
        
        btn_gen_md.click(pptx_to_markdown, inputs=[input_file], outputs=[output_md_text, output_md_file])

    with gr.Tab("2. Markdown → PPTX"):
        gr.Markdown("編集したMarkdownとテンプレートPPTXからマニュアルを作成します。")
        input_md = gr.File(label="Markdownファイル (.txt)")
        input_tmpl = gr.File(label="テンプレートPPTX")
        btn_gen_pptx = gr.Button("PPTX生成")
        output_pptx = gr.File(label="生成されたPPTX")
        
        # ※注意: 完全な動作には元のparse/createロジックの完全な移植が必要です
        # btn_gen_pptx.click(markdown_to_pptx, inputs=[input_md, input_tmpl], outputs=[output_pptx])
        gr.Markdown("※この機能はコード内の `parse_markdown_manual` 等のロジック関数を完全に移植する必要があります。")

    with gr.Tab("3. Markdown → 音声 (TTS)"):
        gr.Markdown("Markdownの内容をGoogle Cloud TTSで音声化します。(要GCP設定)")
        input_md_audio = gr.File(label="Markdownファイル")
        btn_gen_audio = gr.Button("音声生成")
        output_audio = gr.Audio(label="生成音声")
        output_msg = gr.Textbox(label="ステータス")
        
        btn_gen_audio.click(text_to_speech_gcs, inputs=[input_md_audio], outputs=[output_audio, output_msg])

if __name__ == "__main__":
    demo.launch()