Upload 11 files
Browse files- Hugging Face/FROM python:3.dockerfile +16 -0
- Hugging Face/README.md +17 -0
- Hugging Face/app.py +84 -0
- Hugging Face/core.py +143 -0
- Hugging Face/hf.yaml +6 -0
- Hugging Face/llm.py +67 -0
- Hugging Face/models.py +24 -0
- Hugging Face/render.py +29 -0
- Hugging Face/requirements.txt +9 -0
- Hugging Face/templates:report.html.j2 +74 -0
- Hugging Face/templating.py +12 -0
Hugging Face/FROM python:3.dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# OS deps for WeasyPrint (Cairo/Pango) + 日本語フォント
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
+
libcairo2 pango1.0-tools libpango-1.0-0 libgdk-pixbuf2.0-0 libffi-dev \
|
| 6 |
+
fonts-noto fonts-noto-cjk \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
WORKDIR /code
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -U pip wheel && pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
CMD ["python", "app.py"]
|
Hugging Face/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IR/ESG Report Generator (HF Space, LLM+Translation)
|
| 2 |
+
|
| 3 |
+
CSV/YAML をアップロードして IR/ESG レポート(HTML/PDF/DOCX)を生成します。
|
| 4 |
+
LLM 要約+翻訳に **OPENAI_API_KEY2** を使用します。
|
| 5 |
+
|
| 6 |
+
## 入力例
|
| 7 |
+
- company.yaml
|
| 8 |
+
- financials.csv
|
| 9 |
+
- esg_metrics.csv
|
| 10 |
+
|
| 11 |
+
## 設定(必須)
|
| 12 |
+
Hugging Face Space → **Settings → Variables and secrets**:
|
| 13 |
+
- Name: `OPENAI_API_KEY2`
|
| 14 |
+
- Value: `<your openai api key>`
|
| 15 |
+
|
| 16 |
+
## 起動
|
| 17 |
+
このリポジトリを Space にそのままアップロード(Docker Space)。自動ビルド後に起動します。
|
Hugging Face/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import tempfile,datetime, json
|
| 4 |
+
from core import generate_report
|
| 5 |
+
import shutil
|
| 6 |
+
|
| 7 |
+
TITLE = "IR/ESG Report Generator(Hugging Face Space)"
|
| 8 |
+
DESC = ""
|
| 9 |
+
CSV/YAMLをアップロードしてIR/ESGレポート(HTML/PDF/DOCX)を生成します。
|
| 10 |
+
""
|
| 11 |
+
|
| 12 |
+
def run(company_yaml,financials_csv,esg_csv,use_llm,lang):
|
| 13 |
+
if company_yamal is None or financials_csv is None or esg_csv is None:
|
| 14 |
+
return "すべてのファイルをアップロードしてください。"None,None,None,None
|
| 15 |
+
|
| 16 |
+
with tempfile.TemporaryDirectory() as td:
|
| 17 |
+
cpath = Path(td)/"company_yaml";cpath.write_bytes(company_yaml.read())
|
| 18 |
+
fpath = Path(td)/"financials_csv";fpath.write_bytes(financials_csv.read())
|
| 19 |
+
epath = Path(td)/"esg_csv";epath.write_bytes(esg_csv.read())
|
| 20 |
+
|
| 21 |
+
outdir = Path(td)/"out"
|
| 22 |
+
outdir.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
llm=None
|
| 25 |
+
if use_llm:
|
| 26 |
+
try:
|
| 27 |
+
from llm import OpenAILLM
|
| 28 |
+
llm = OpenAILLM()
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return f"LLM初期化エラー: {e}",None,None,None
|
| 31 |
+
|
| 32 |
+
html,pdf,docx,meta_json = generate_report(
|
| 33 |
+
company_yamal=str(cpath),
|
| 34 |
+
financials_csv=str(fpath),
|
| 35 |
+
esg_csv=str(epath),
|
| 36 |
+
templates_dir="templates",
|
| 37 |
+
template_name="report.html.j2",
|
| 38 |
+
out_html=str(outdir/"report.html")
|
| 39 |
+
out_pdf=str(outdir/"report.html")
|
| 40 |
+
lang=lang,
|
| 41 |
+
llm=llm
|
| 42 |
+
)
|
| 43 |
+
repo_tmp=Path("./tmp")
|
| 44 |
+
repo_tmp.mkdir(exit_ok=True)
|
| 45 |
+
ts = detatime.datetime.now().strftime("%Ym%d-%H%M%S")
|
| 46 |
+
html_out=repo_tmp/f"report-{ts}.html"
|
| 47 |
+
pdf_out=repo_tmp/f"report-{ts}.pdf"
|
| 48 |
+
docx_out=repo_tmp/f"report-{ts}.docx"
|
| 49 |
+
meta_out=repo_tmp/f"report-{ts}.json"
|
| 50 |
+
shutil.copy(html,html_out)
|
| 51 |
+
shutil.copy(pdf,pdf_out)
|
| 52 |
+
shutil.copy(docx,docx_out)
|
| 53 |
+
Path(meta_out).write_text(json.dumps(meta_json,ensure_ascii=False,indent=2),encoding="utf-8")
|
| 54 |
+
|
| 55 |
+
return"生成が完了しました。",str(html_out),str(pdf_out),str(docx_out),str(meta_out)
|
| 56 |
+
|
| 57 |
+
with gr.Blocks(title=TITLE) as demo:
|
| 58 |
+
gr.Markdown(f"#{TITLE}\n{DESC}")
|
| 59 |
+
|
| 60 |
+
with gr.Row():
|
| 61 |
+
company_yaml=gr.File(label="company.yaml(会社情報・年度等)",file_types=[".yaml",".yml"])
|
| 62 |
+
financials_csv=gr.File(label="financials.csv(財務KPI)",file_types=[".csv"])
|
| 63 |
+
esg_csv=gr.File(label="esg_metrics.csv(ESG指標)",file_types=[".csv"])
|
| 64 |
+
|
| 65 |
+
with gr.Row():
|
| 66 |
+
use_llm=gr.Chheckbox(label="LLMで要約/翻訳を行う(OPENAI_API_KE2 必須)",value=True)
|
| 67 |
+
lang = gr.Dropdown(choices=["ja","en","zh","ko","de","fr"],value="ja",label="出力言語")
|
| 68 |
+
|
| 69 |
+
run_btn=gr.Button("レポート生成")
|
| 70 |
+
|
| 71 |
+
status =gr.Textbox(label="ステータス",interactive=False)
|
| 72 |
+
html_file=gr.File(label="HTMLダウンロード")
|
| 73 |
+
pdf_file=gr.File(label="PDFダウンロード")
|
| 74 |
+
docx_file=gr.File(label="DOCXダウンロード")
|
| 75 |
+
meta_file=gr.File(label="メタ情報(JSON)")
|
| 76 |
+
|
| 77 |
+
run_btn.click(
|
| 78 |
+
fu=run
|
| 79 |
+
inputs=[company_yamal,finanxials_csv,esg_csv,use_llm,lang],
|
| 80 |
+
outputs=[status,html_file,pdf_file,docx_file,meta_file]
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
if__name__=="__main__":
|
| 84 |
+
demo.launch()
|
Hugging Face/core.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import panda as pd
|
| 2 |
+
import yaml,detatime,hashilb,json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from templating import get_env,rander
|
| 5 |
+
from models import CompanyMeta,ReportSections,RenderPayload
|
| 6 |
+
from render import hetml_to_pdf,html_to_docx
|
| 7 |
+
|
| 8 |
+
DISPLAY_NAME={
|
| 9 |
+
"co2_emission":"Co2排出量",
|
| 10 |
+
"energy_renewable_ratio":"再生可能エネルギー比率",
|
| 11 |
+
"female_management_ratio":"女性管理職比率"
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
def sha256(p:Path)->str:
|
| 15 |
+
h=hashlib.sha256()
|
| 16 |
+
with p.open("rb") as f:
|
| 17 |
+
for chunk in iter(lambda: f.read(8192), b""):
|
| 18 |
+
h.update(chunk)
|
| 19 |
+
return h.hexdigest()
|
| 20 |
+
|
| 21 |
+
def load_company_meta(path:str)->CompanyMeta:
|
| 22 |
+
data=yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
| 23 |
+
return CompanyMeta(**data)
|
| 24 |
+
|
| 25 |
+
def load_financials(path:str)->pd.DataFrame:
|
| 26 |
+
return pd.read_csv(path)
|
| 27 |
+
|
| 28 |
+
def compute_kpi(fin_df:pd.DataFrame,fiscal_year:int):
|
| 29 |
+
latest=fin_df[fin_df["year"]==fiscal_year].sort_values("quarter").tail(1)
|
| 30 |
+
prev=fin_df[fin_df["year"]==fiscal_year-1].sort_values("quarter").tail(1)
|
| 31 |
+
|
| 32 |
+
revenue=float(latest["revenue"].iloc[0])
|
| 33 |
+
prev_revenue=float(prev["revenue"].iloc[0]) if not prev.empty else 0
|
| 34 |
+
ebit=float(latest["ebit"].iloc[0]) if not latest.empty else 0
|
| 35 |
+
net_income=float(latest["net_income"].iloc[0]) if not latest.empty else 0
|
| 36 |
+
equity=float(latest["equity"].iloc[0]) if not latest.empty else 0.0
|
| 37 |
+
ebit_margin=ebit /revenue *100 if revenue else 0.0
|
| 38 |
+
revenue_yoy=((revenue/float(prev["revenue"].iloc[0]))-1)*100 if not prev.empty and float(prev["revenue"].iloc[0]) else 0.0
|
| 39 |
+
|
| 40 |
+
return {
|
| 41 |
+
"revenue": revenue,
|
| 42 |
+
"ebit": ebit,
|
| 43 |
+
"ebit_margin": ebit_margin,
|
| 44 |
+
"net_income": net_income,
|
| 45 |
+
"equity": equity,
|
| 46 |
+
"revenue_yoy": revenue_yoy
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
def esg_table (df:pd.DataFrame,fiscal_year:int):
|
| 50 |
+
dfy=df[df["year"]==fiscal_year].copy
|
| 51 |
+
rows=[]
|
| 52 |
+
for _, r in dfy.iterrows():
|
| 53 |
+
display = DISPLAY_NAME.get(r["metric"], r["metric"])
|
| 54 |
+
rows.append({
|
| 55 |
+
"display": display,
|
| 56 |
+
"value": r["value"],
|
| 57 |
+
"unit": r.get("unit", ""),
|
| 58 |
+
"notes": r.get("notes", ""),
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
return rows
|
| 62 |
+
def build_sections(meta:CompanyMeta,kpi:dict,esg_rows:list,llm=None)->ReportSections:
|
| 63 |
+
if llm:
|
| 64 |
+
ceo_message = llm.generate_ceo_message(meta.kpi,esg_rows)
|
| 65 |
+
risk = llm.generate_risk_opportunities(meta.kpi,esg_rows)
|
| 66 |
+
else:
|
| 67 |
+
ceo_message = f"[{meta.fiscal_year}]期は、売上成長と収益性の両立に注力しました。"
|
| 68 |
+
risk = "主要リスクはマクロ環境と規制動向。機会は生成AI活用と脱炭素需要の拡大です。"
|
| 69 |
+
return ReportSections(ceo_message=ceo_message,risk_opportunity=risk)
|
| 70 |
+
|
| 71 |
+
def_translate_payload(payload:dict,lang:str,llm)->dict:
|
| 72 |
+
"""payload のうち、テキスト項目をtarget langに翻訳。数値は非対象。"""
|
| 73 |
+
if not llm or lang =="ja":
|
| 74 |
+
return payload
|
| 75 |
+
|
| 76 |
+
texts=[]
|
| 77 |
+
|
| 78 |
+
texts.append(payload["section"]["ceo_message"])
|
| 79 |
+
texts.append(payload["section"]["risk_opportunity"])
|
| 80 |
+
|
| 81 |
+
for row in payload["esg_table"]:
|
| 82 |
+
texts.append(row["display"])
|
| 83 |
+
texts.append(row["notes"] or "")
|
| 84 |
+
|
| 85 |
+
texts.append(payload["meta"]["report_title"])
|
| 86 |
+
for topic in payload["meta"].get("material_topics", []):
|
| 87 |
+
texts.append(topic)
|
| 88 |
+
|
| 89 |
+
translated = llm.translate_text(texts, target_lang=lang)
|
| 90 |
+
it = iter(translated)
|
| 91 |
+
|
| 92 |
+
payload["section"]["ceo_message"] = next(it)
|
| 93 |
+
payload["section"]["risk_opportunity"] = next(it)
|
| 94 |
+
|
| 95 |
+
for row in payload["esg_table"]:
|
| 96 |
+
row["display"] = next(it)
|
| 97 |
+
row["notes"] = next(it)
|
| 98 |
+
|
| 99 |
+
payload["meta"]["report_title"] = next(it)
|
| 100 |
+
mt = payload["meta"].get("material_topics", [])
|
| 101 |
+
for i in range(len(mt)):
|
| 102 |
+
mt[i] = next(it)
|
| 103 |
+
|
| 104 |
+
return payload
|
| 105 |
+
|
| 106 |
+
def generate_report(company_yaml,financials_csv,esg_csv,
|
| 107 |
+
template_dir,template_name="report.html.j2",
|
| 108 |
+
out_html="output/report.html",out_pdf="output/report.pdf",
|
| 109 |
+
out_docx="output/report.docx",lang="ja",llm=None):
|
| 110 |
+
Path(Path(out_html).parent).mkdir(parents=True, exist_ok=True)
|
| 111 |
+
meta= load_company_meta(company_yaml)
|
| 112 |
+
fin= load_financials(financials_csv)
|
| 113 |
+
esg= load_esg(esg_csv)
|
| 114 |
+
|
| 115 |
+
kpi = compute_kpi(fin, meta.fiscal_year)
|
| 116 |
+
esg_rows = esg_table(esg, meta.fiscal_year)
|
| 117 |
+
sections = build_sections(meta, kpi, esg_rows, llm=llm)
|
| 118 |
+
|
| 119 |
+
env = get_env(template_dir)
|
| 120 |
+
payload = RenderPayload(
|
| 121 |
+
meta=meta, kpi=kpi, esg_table=esg_rows,kpi=kpi, sections=sections,
|
| 122 |
+
generated_at=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),lang=lang
|
| 123 |
+
).model__dump()
|
| 124 |
+
|
| 125 |
+
payload= _translate_payload_texts(payload, lang=lang, llm=llm)
|
| 126 |
+
|
| 127 |
+
html = render(env, template_name, payload)
|
| 128 |
+
Path(out_html).write_text(html, encoding="utf-8")
|
| 129 |
+
html_to_pdf(html, out_pdf)
|
| 130 |
+
html_to_docx(html, out_docx)
|
| 131 |
+
|
| 132 |
+
meta_json ={
|
| 133 |
+
"inputs":{
|
| 134 |
+
"company_yaml_sha":_sha256(Path(company_yaml)),
|
| 135 |
+
"financials_csv_sha":_sha256(Path(financials_csv)),
|
| 136 |
+
"esg_csv_sha":_sha256(Path(esg_csv)),
|
| 137 |
+
"lang":lang
|
| 138 |
+
},
|
| 139 |
+
"outputs":{"html":out_html,"pdf":out_pdf,"docx":out_docx},
|
| 140 |
+
"template":{"dir":templates_dir,"name":template_name},
|
| 141 |
+
"generated_at":datetime.datetime.now().strftime("timespec=sedconds"),
|
| 142 |
+
}
|
| 143 |
+
return out_html,out_pdf,out_docx,meta_json
|
Hugging Face/hf.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: IR-ESG-Report-Generator
|
| 2 |
+
emoji: 📈
|
| 3 |
+
colorFrom: gray
|
| 4 |
+
colorTo: indigo
|
| 5 |
+
sdk: docker
|
| 6 |
+
pinned: false
|
Hugging Face/llm.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
|
| 5 |
+
class OpenAILLM:
|
| 6 |
+
def __init__(self,model_chat:str = "gpt-4o",model_translate:str = "gpt-4o", model_summarize:str = "gpt-4o"):
|
| 7 |
+
api_key = os.environ.get("OPENAI_API_KEY2")
|
| 8 |
+
if not api_key:
|
| 9 |
+
raise RuntimeError("OPENAI_API_KEY2 is not set in environment variables.")
|
| 10 |
+
self.client = OpenAI(api_key=api_key)
|
| 11 |
+
self.model_chat = model_chat
|
| 12 |
+
self.model_translate = model_translate
|
| 13 |
+
self.model_summarize = model_summarize
|
| 14 |
+
|
| 15 |
+
def generate_ceo_message(self,meta, kpi:Dict[str,float],esg_row:List[Dict[str,Any]])->str:
|
| 16 |
+
prompt = (
|
| 17 |
+
"以下の企業情報・KPI・ESG指標をもとに、日本語で200字程度のCEOメッセージ草案を出力してください。"
|
| 18 |
+
"投資家に伝わる簡潔なトーンで、過度な形容表現は避けてください。\n\n"
|
| 19 |
+
f"企業情報: {meta.model_dump()}\n"
|
| 20 |
+
f"KPI: {kpi}\n"
|
| 21 |
+
f"ESG指標: {esg_row}\n"
|
| 22 |
+
)
|
| 23 |
+
rsp = self.client.chat.completions.create(
|
| 24 |
+
model=self.model_chat,
|
| 25 |
+
messages=[{"role": "user", "content": prompt}],
|
| 26 |
+
temperature=0.3,
|
| 27 |
+
)
|
| 28 |
+
return rsp.choices[0].message.content.strip()
|
| 29 |
+
|
| 30 |
+
def generate_risk_opportunity(self,meta,kpi:Dict[str,float],esg_row:List[Dict[str,Any]])->str:
|
| 31 |
+
prompt = (
|
| 32 |
+
"以下の情報から、日本語で200字程度のリスクと機会の草案を出力してください。"
|
| 33 |
+
"定量/定性のバランスを取り、具体的な観点を1-2点挙げてください。\n\n"
|
| 34 |
+
f"企業情報: {meta.model_dump()}\n"
|
| 35 |
+
f"KPI: {kpi}\n"
|
| 36 |
+
f"ESG: {esg_row}\n"
|
| 37 |
+
)
|
| 38 |
+
rsp = self.client.chat.completions.create(
|
| 39 |
+
model=self.model_chat,
|
| 40 |
+
messages=[{"role": "user", "content": prompt}],
|
| 41 |
+
temperature=0.3,
|
| 42 |
+
)
|
| 43 |
+
return rsp.choices[0].message.content.strip()
|
| 44 |
+
|
| 45 |
+
def translate_text(self, texts:List[str], target_language: str) -> List[str]:
|
| 46 |
+
if not texts:
|
| 47 |
+
return texts
|
| 48 |
+
system=(
|
| 49 |
+
"You are precise financial/ESG translator.Preserve numbers,units,and proper nouns."
|
| 50 |
+
"Keep tone concise,suitable for investor reports."
|
| 51 |
+
)
|
| 52 |
+
SEP = "\n<<<SEP>>>\n"
|
| 53 |
+
joined = SEP.join(texts)
|
| 54 |
+
prompt = f"Translate the following content to {target_language}.Keep layout minimal.\n\n{joined}"
|
| 55 |
+
rsp = self.client.chat.completions.create(
|
| 56 |
+
model=self.model_translate,
|
| 57 |
+
messages=[
|
| 58 |
+
{"role": "user", "content": system},{"role": "user", "content": prompt}
|
| 59 |
+
],
|
| 60 |
+
temperature=0.2,
|
| 61 |
+
)
|
| 62 |
+
out = rsp.choices[0].message.content
|
| 63 |
+
parts = [p.strip() for p in out.split("<<<SEP>>>")]
|
| 64 |
+
if len(parts) != len(texts):
|
| 65 |
+
parts = [out]+texts[1:]
|
| 66 |
+
parts = parts[:len(texts)]
|
| 67 |
+
return parts
|
Hugging Face/models.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Lisr,Optional,Dict,Any
|
| 3 |
+
|
| 4 |
+
class CompnayMeta(BaseModel):
|
| 5 |
+
company_name:str
|
| 6 |
+
fiscal_year:int
|
| 7 |
+
currency:str ="JPY"
|
| 8 |
+
ticker:Optional[str] = None
|
| 9 |
+
report_title:str ="Integrated Report"
|
| 10 |
+
ceo_name:Optional[str] = None
|
| 11 |
+
material_topics:List[str] = []
|
| 12 |
+
targets:Dict[str, Any] = {}
|
| 13 |
+
|
| 14 |
+
class ReportSection(BaseModel):
|
| 15 |
+
ceo_message:str=""
|
| 16 |
+
risk_opportunity:str=""
|
| 17 |
+
|
| 18 |
+
class ReportPayload(BaseModel):
|
| 19 |
+
meta:CompanyMeta
|
| 20 |
+
esg_table:List[Dict[str, Any]]
|
| 21 |
+
kpi:Dict[str, float]
|
| 22 |
+
sections:ReportSection
|
| 23 |
+
generated_at: str
|
| 24 |
+
lang:str="ja"
|
Hugging Face/render.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from weasyprint import HTML
|
| 2 |
+
from docx import Document
|
| 3 |
+
from docx.shared import Pt
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
|
| 6 |
+
def html_to_pdf(html_str:str,out_pdf_path:str):
|
| 7 |
+
"""
|
| 8 |
+
Convert HTML string to PDF file.
|
| 9 |
+
"""
|
| 10 |
+
HTML(string=html_str).write_pdf(out_pdf_path)
|
| 11 |
+
|
| 12 |
+
def html_to_docx(html_str:str,out_docx_path:str):
|
| 13 |
+
doc = Document()
|
| 14 |
+
soup = BeautifulSoup(html_str, 'html.parser')
|
| 15 |
+
for tag in soup.find_all(["h1","h2","h3","p","li"]):
|
| 16 |
+
txt=tag.get_text(strip=True)
|
| 17 |
+
if not txt:
|
| 18 |
+
continue
|
| 19 |
+
if tag.name =="h1":
|
| 20 |
+
p=doc.add_heading(txt,level=0)
|
| 21 |
+
elif tag.name =="h2":
|
| 22 |
+
p = doc.add_heading(txt,level=1)
|
| 23 |
+
elif tag.name =="h3":
|
| 24 |
+
p= doc.add_heading(txt,level=2)
|
| 25 |
+
else:
|
| 26 |
+
p=doc.add_paragraph(txt)
|
| 27 |
+
for run in p.runs:
|
| 28 |
+
run.font.size = Pt(11)
|
| 29 |
+
doc.save(out_docx_path)
|
Hugging Face/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.0
|
| 2 |
+
pandas==2.2.2
|
| 3 |
+
pydantic==2.7.4
|
| 4 |
+
Jinja2==3.1.4
|
| 5 |
+
python-docx==1.1.2
|
| 6 |
+
WeasyPrint==62.3
|
| 7 |
+
beautifulsoup4==4.12.3
|
| 8 |
+
PyYAML==6.0.2
|
| 9 |
+
openai==1.40.2
|
Hugging Face/templates:report.html.j2
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="{{ lang }}">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<title>{{ meta.report_title }} - {{ meta.company_name }}</title>
|
| 6 |
+
<style>
|
| 7 |
+
body { font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial; line-height: 1.6; }
|
| 8 |
+
h1,h2,h3 { margin: 0.6em 0; }
|
| 9 |
+
.kpi { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
| 10 |
+
.card { border: 1px solid #ddd; border-radius: 12px; padding: 12px; }
|
| 11 |
+
.small { color: #666; font-size: 0.9em; }
|
| 12 |
+
table { border-collapse: collapse; width: 100%; }
|
| 13 |
+
th, td { border: 1px solid #eee; padding: 8px; text-align: right; }
|
| 14 |
+
th { background: #fafafa; }
|
| 15 |
+
.left { text-align: left; }
|
| 16 |
+
</style>
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<h1>{{ meta.report_title }}({{ meta.fiscal_year }})</h1>
|
| 20 |
+
<p class="small">{{ meta.company_name }} / Ticker: {{ meta.ticker }} / 通貨: {{ meta.currency }}</p>
|
| 21 |
+
|
| 22 |
+
<h2>CEOメッセージ</h2>
|
| 23 |
+
<p>{{ sections.ceo_message }}</p>
|
| 24 |
+
|
| 25 |
+
<h2>ハイライトKPI</h2>
|
| 26 |
+
<div class="kpi">
|
| 27 |
+
<div class="card">
|
| 28 |
+
<div class="small">売上高</div>
|
| 29 |
+
<div><strong>{{ kpi.revenue | round(0) | int }} {{ meta.currency }}</strong></div>
|
| 30 |
+
<div class="small">前年比: {{ kpi.revenue_yoy | round(1) }}%</div>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="card">
|
| 33 |
+
<div class="small">EBIT</div>
|
| 34 |
+
<div><strong>{{ kpi.ebit | round(0) | int }} {{ meta.currency }}</strong></div>
|
| 35 |
+
<div class="small">マージン: {{ kpi.ebit_margin | round(1) }}%</div>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="card">
|
| 38 |
+
<div class="small">純利益</div>
|
| 39 |
+
<div><strong>{{ kpi.net_income | round(0) | int }} {{ meta.currency }}</strong></div>
|
| 40 |
+
<div class="small">ROE: {{ kpi.roe | round(1) }}%</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<h2>ESGサマリー</h2>
|
| 45 |
+
<table>
|
| 46 |
+
<thead>
|
| 47 |
+
<tr><th class="left">指標</th><th>値</th><th>単位</th><th class="left">備考</th></tr>
|
| 48 |
+
</thead>
|
| 49 |
+
<tbody>
|
| 50 |
+
{% for row in esg_table %}
|
| 51 |
+
<tr>
|
| 52 |
+
<td class="left">{{ row.display }}</td>
|
| 53 |
+
<td>{{ row.value }}</td>
|
| 54 |
+
<td>{{ row.unit }}</td>
|
| 55 |
+
<td class="left">{{ row.notes }}</td>
|
| 56 |
+
</tr>
|
| 57 |
+
{% endfor %}
|
| 58 |
+
</tbody>
|
| 59 |
+
</table>
|
| 60 |
+
|
| 61 |
+
<h2>マテリアリティ & ターゲット</h2>
|
| 62 |
+
<ul>
|
| 63 |
+
{% for topic in meta.material_topics %}
|
| 64 |
+
<li>{{ topic }}</li>
|
| 65 |
+
{% endfor %}
|
| 66 |
+
</ul>
|
| 67 |
+
<p class="small">CO₂削減目標: {{ meta.targets.co2_reduction_pct }}% / 女性管理職比率: {{ meta.targets.female_management_ratio }}% / 再エネ比率: {{ meta.targets.renewable_energy_ratio }}%</p>
|
| 68 |
+
|
| 69 |
+
<h2>リスク & 機会(要約)</h2>
|
| 70 |
+
<p>{{ sections.risk_opportunity }}</p>
|
| 71 |
+
|
| 72 |
+
<footer class="small">Generated on {{ generated_at }}</footer>
|
| 73 |
+
</body>
|
| 74 |
+
</html>
|
Hugging Face/templating.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
| 2 |
+
|
| 3 |
+
def get_env(templates_dir: str):
|
| 4 |
+
env = Environment(
|
| 5 |
+
loader=FileSystemLoader(templates_dir),
|
| 6 |
+
autoescape=select_autoescape(["html", "xml"])
|
| 7 |
+
)
|
| 8 |
+
return env
|
| 9 |
+
|
| 10 |
+
def render(env, template_name: str, context: dict) -> str:
|
| 11 |
+
template = env.get_template(template_name)
|
| 12 |
+
return template.render(**context)
|