slide2manual / app.py
Johnny0619's picture
Create app.py
3edbc8d verified
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()