Spaces:
Sleeping
Sleeping
| import os | |
| from typing import Literal | |
| from openai import OpenAI | |
| from app.config import settings | |
| # 遅延初期化:インポート時にHTTPクライアントを作らない(環境差異の影響を減らす) | |
| _client = None | |
| def _get_client() -> OpenAI: | |
| global _client | |
| if _client is None: | |
| api_key = settings.OPENAI_API_KEY or os.getenv("OPENAI_API_KEY", "") | |
| if not api_key: | |
| # 起動時ではなく呼び出し時に明示エラー | |
| raise RuntimeError("OPENAI_API_KEY is not set. Please add it to HF Spaces Secrets or .env.") | |
| _client = OpenAI(api_key=api_key) | |
| return _client | |
| TONE_MAP = { | |
| "neutral": "ニュートラルで事実中心", | |
| "formal": "フォーマルで簡潔", | |
| "friendly": "親しみやすく平易", | |
| "investor": "投資家向け、KPI/財務メトリクスを強調", | |
| "pr_bold": "PR向け、ヘッドラインを強調し勢いある表現", | |
| } | |
| TYPE_GUIDE = { | |
| "press_release": "ニュースリリース: タイトル/リード/本文/会社概要/問い合わせ先", | |
| "ir_letter": "IRレター: タイトル/ご挨拶/ハイライト/今後の見通し/お問い合わせ", | |
| "investor_summary": "投資家向けサマリー: 3-7箇条書き、KPI・成長要因・リスク要因", | |
| } | |
| SYSTEM_TMPL = """ | |
| あなたは日本語のPR/IRライターです。事実の正確性、簡潔さ、再利用しやすいMarkdown構成を重視します。 | |
| - 出力は必ずMarkdown。 | |
| - センセーショナルすぎる表現や根拠のない主張は避ける。 | |
| - プレス/IRのルールに配慮し、誤解を招かない文に整える。 | |
| """ | |
| PROMPT_TMPL = """ | |
| 【目的】{ctype}のドラフトを{tone_ja}で作成。 | |
| 【入力テキスト要約】以下の素材から、要点を抽出し、{ctype_guide}に沿ってMarkdownドラフトを生成。 | |
| --- | |
| {source_text} | |
| --- | |
| 出力要件: | |
| - 1行目に短いタイトル(H1) | |
| - 適切な小見出し(H2/H3) | |
| - 箇条書きは短文で | |
| - ファクトと数値はそのまま | |
| - 最後に「メタデータ」セクションを追加(推奨タグ・要約140字・推奨URLスラッグ) | |
| """ | |
| def generate_draft( | |
| source_text: str, | |
| content_type: Literal["press_release", "ir_letter", "investor_summary"], | |
| tone: str, | |
| ): | |
| client = _get_client() | |
| tone_ja = TONE_MAP.get(tone, TONE_MAP["neutral"]) | |
| guide = TYPE_GUIDE[content_type] | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_TMPL}, | |
| {"role": "user", "content": PROMPT_TMPL.format( | |
| ctype=content_type, | |
| tone_ja=tone_ja, | |
| ctype_guide=guide, | |
| source_text=source_text[:12000], | |
| )}, | |
| ] | |
| resp = client.chat.completions.create( | |
| model=settings.OPENAI_MODEL, | |
| temperature=settings.OPENAI_TEMPERATURE, | |
| messages=messages, | |
| ) | |
| text = resp.choices[0].message.content | |
| # 件名(Subject)候補A/B | |
| subj_resp = client.chat.completions.create( | |
| model=settings.OPENAI_MODEL, | |
| temperature=0.3, | |
| messages=[ | |
| {"role": "system", "content": "日本語のPRメール件名ライター。50文字以内、事実に忠実。"}, | |
| {"role": "user", "content": f"次のMarkdownドラフトから、差分のある件名案を2つ出して。\n---\n{text[:6000]}"}, | |
| ], | |
| ) | |
| subs = subj_resp.choices[0].message.content.splitlines() | |
| subj_a = next((s for s in subs if s.strip()), "お知らせ") | |
| subj_b = next((s for s in subs[1:] if s.strip()), subj_a + "【続報】") | |
| # タイトル抽出(先頭行 # 見出し) | |
| title = None | |
| for line in text.splitlines(): | |
| if line.strip().startswith("# "): | |
| title = line.strip().lstrip("# ").strip() | |
| break | |
| return title or "ドラフト", text, subj_a.strip("- ・* "), subj_b.strip("- ・* ") | |