diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5df44e342192816a02e103294e6c83824a7766ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +.env +.venv/ +*.egg-info/ +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1efe011dafb2113f4c257d5d0435b34cabd8ec6d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Dockerfile — api_light_hf (FastAPI + Hugging Face Inference API) +FROM python:3.10-slim + +# Non-root user (compatible with HF Spaces default) +RUN useradd -m -u 1000 user + +USER user +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH \ + PYTHONPATH=$HOME/app \ + PYTHONUNBUFFERED=1 + +WORKDIR $HOME/app + +COPY --chown=user . $HOME/app + +RUN python -m pip install --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# PORT is overridable via docker run -e PORT=8080 +ENV PORT=7860 + +EXPOSE $PORT + +CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port ${PORT}"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b6bc37dedea71a271becaddf56f727de55cf7cda --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ +# api_light_hf + +`api_light_dev` の全エンドポイントを **Hugging Face Inference API** に乗せ換えた FastAPI サーバーです。 +Gradio / GCP / Vertex AI への依存を排除し、`HF_TOKEN` 一本で動きます。 + +--- + +## アーキテクチャ概要 + +``` +api_light_hf/ +├── app.py # FastAPI エントリーポイント。apis/ を動的ロード +├── apis/ # 各 API 関数(1ファイル = 1関数) +├── src/ +│ ├── clients/ +│ │ └── llm_client.py # HF Inference API 統合クライアント +│ ├── config/ +│ │ └── models.yaml # モデルエイリアス・タスク定義 +│ └── utils/ +│ └── tracer.py # ロギング/OpenTelemetry トレーサー +├── requirements.txt +└── Dockerfile +``` + +### 主な変更点(api_light_dev との差分) + +| 項目 | api_light_dev | api_light_hf | +|---|---|---| +| サーバー | Gradio | FastAPI (`POST /{api_name}`) | +| LLM バックエンド | OpenAI / Vertex AI / LiteLLM | HF Inference API (`huggingface_hub`) | +| OCR | Google Vision API | VLM (Qwen2.5-VL) | +| Inpaint | Vertex AI Imagen | HF `stable-diffusion-inpainting` | +| Image Generation | Vertex AI Imagen / DALL-E | HF FLUX.1-dev | +| HTML 生成 (baseimg2html) | Vertex AI Gemini 2.5 Pro | HF Qwen2.5-VL-72B | +| 認証 | GCP サービスアカウント JSON | `HF_TOKEN` 環境変数のみ | + +--- + +## セットアップ + +### 1. 環境変数 + +| 変数 | 必須 | 説明 | +|---|---|---| +| `HF_TOKEN` | ✅ | Hugging Face の API トークン([取得](https://huggingface.co/settings/tokens)) | +| `PORT` | – | 待ち受けポート(デフォルト `7860`) | +| `HF_MODEL` | – | デフォルトモデルの上書き(例: `meta-llama/Llama-3.3-70B-Instruct`) | +| `GCP_SERVICE_KEY_FOR_TRACE` | – | OpenTelemetry / Cloud Trace を有効化する場合のサービスアカウント JSON | + +### 2. ローカル起動 + +```bash +# 依存インストール +pip install -r requirements.txt + +# 起動 +HF_TOKEN=hf_xxx uvicorn app:app --host 0.0.0.0 --port 7860 --reload +``` + +### 3. Docker + +```bash +# ビルド +docker build -t api_light_hf . + +# 実行 +docker run -p 7860:7860 -e HF_TOKEN=hf_xxx api_light_hf +``` + +### 4. Hugging Face Spaces へのデプロイ + +専用スクリプト `deploy_api_light_hf.py`(リポジトリルート `DD/` 直下)を使います。 + +#### 前提 + +| 条件 | 内容 | +|---|---| +| `HF_TOKEN` | 書き込み権限のある HF API トークン([取得](https://huggingface.co/settings/tokens)) | +| Space の作成 | HF Spaces で **Docker** SDK の Space を先に作成しておく | +| `git` | ローカルに git がインストールされていること | + +#### 基本コマンド + +```bash +# 環境変数に HF_TOKEN を設定 +export HF_TOKEN=hf_xxxxxxxxxxxx + +# デプロイ(初回・更新共通) +python deploy_api_light_hf.py --org DLPO --space api_light_hf + +# コミットメッセージを指定 +python deploy_api_light_hf.py --org DLPO --space api_light_hf -m "feat: add new api" + +# push せずに手順を確認する(ドライラン) +python deploy_api_light_hf.py --org DLPO --space api_light_hf --dry-run + +# Space の現在の状態を確認 +python deploy_api_light_hf.py status --org DLPO --space api_light_hf +``` + +#### 引数一覧 + +| 引数 | デフォルト | 説明 | +|---|---|---| +| `cmd` | `deploy` | `deploy` または `status` | +| `--org` | `DLPO` または env `HF_ORG` | HF 組織名 / ユーザー名 | +| `--space` | `api_light_hf` または env `HF_SPACE` | Space 名 | +| `--branch` | `main` | 対象ブランチ | +| `-m / --message` | タイムスタンプ付き自動生成 | コミットメッセージ | +| `--token` | env `HF_TOKEN` | HF API トークン | +| `--local-dir` | `<スクリプトと同階層>/api_light_hf` | ローカルのソースディレクトリ | +| `--dry-run` | false | push せず手順だけ表示 | + +#### 失敗時のチェックポイント + +- `[error] HF_TOKEN is not set` → `export HF_TOKEN=hf_xxx` を実行 +- `Push failed` → トークンに `write` 権限があるか、Space が存在するか確認 +- `Local directory not found` → `--local-dir` で正しいパスを指定 +- `git is not available` → git をインストールして PATH を通す + +#### Space の secrets 設定 + +デプロイ後、Space の **Settings > Repository secrets** に以下を登録してください。 + +| Secret 名 | 値 | +|---|---| +| `HF_TOKEN` | Inference API 呼び出し用トークン(アプリ内部で使用) | + +--- + +## API の使い方 + +サーバー起動後、`POST /{api_name}` にリクエストします。 + +### エンドポイント一覧の確認 + +``` +GET /endpoints +``` + +### リクエスト形式 + +```bash +curl -X POST http://localhost:7860/text2theme \ + -H "Content-Type: application/json" \ + -d '{"text": "健康志向の若者向けスポーツドリンク"}' +``` + +### ヘルスチェック + +``` +GET /health → {"status": "ok"} +GET / → {status, uptime_seconds, active_requests, endpoints} +``` + +--- + +## モデル設定 + +`src/config/models.yaml` でモデルのエイリアス・タスク・能力を管理します。 + +```yaml +default_model: meta-llama/Llama-3.3-70B-Instruct + +aliases: + gpt-4o: meta-llama/Llama-3.3-70B-Instruct + gemini-flash: Qwen/Qwen2.5-72B-Instruct + vision: Qwen/Qwen2.5-VL-72B-Instruct + ... + +models: + meta-llama/Llama-3.3-70B-Instruct: + task: text-generation + supports_json: true + supports_images: false + Qwen/Qwen2.5-VL-72B-Instruct: + task: image-text-to-text + supports_json: true + supports_images: true + ... +``` + +--- + +## api_light_dev からの切り替え手順 + +1. `HF_TOKEN` を発行・設定する +2. `docker build` または `pip install` でセットアップ +3. 旧サービス(Gradio)と並行稼働で動作確認 +4. 呼び出し元のベース URL を `https:///...` から `http://:/...` に変更 + - エンドポイント名(`text2theme`, `baseimg2score` など)はそのまま維持 + - リクエストボディは `{"key": "value"}` 形式(旧 Gradio の JSON Body と互換) +5. 旧サービスを停止 + +### 非互換点 + +| 項目 | 旧(api_light_dev) | 新(api_light_hf) | +|---|---|---| +| `gcp_key` 引数 | GCP 認証に使用 | 受け取るが無視(ログ警告なし) | +| `openai_key` / `google_api_key` | LLM 認証に使用 | 受け取るが無視 | +| OCR 精度 | Google Vision API | VLM ベース(精度はモデルに依存) | +| Inpaint モデル | Vertex Imagen | SD-Inpainting(品質差あり) | + +--- + +## 新しい API の追加 + +`apis/` に Python ファイルを追加するだけで自動登録されます。 + +```python +# apis/my_new_api.py +from src.clients.llm_client import LLMClient +from pydantic import BaseModel + +class MyOutput(BaseModel): + result: str + +def my_new_api(input_text: str) -> dict: + """ + input1 (text): 入力テキスト + output1 (json): {"result": "..."} + """ + client = LLMClient() + output = client.call(prompt=input_text, schema=MyOutput) + return output.model_dump() +``` + +サーバーを再起動すると `POST /my_new_api` が自動的に有効になります。 diff --git a/apis/__init__.py b/apis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apis/ab2samediff.py b/apis/ab2samediff.py new file mode 100644 index 0000000000000000000000000000000000000000..57981339fade726d19ebd8f50522b4cbd4b26038 --- /dev/null +++ b/apis/ab2samediff.py @@ -0,0 +1,63 @@ +""" +ab2samediff: 2つの画像を比較して類似点と相違点を返す。 +HF版: VLM (Llama-3.2-Vision) を使用。Vertex AI は使用しない。 +""" + +import base64 +import json +from io import BytesIO +from typing import Optional + +from src.utils.tracer import customtracer + + +def _pil_to_b64(image) -> str: + """PIL Image を base64 文字列に変換。""" + fmt = getattr(image, "format", None) or "PNG" + buf = BytesIO() + image.save(buf, format=fmt) + return base64.b64encode(buf.getvalue()).decode("utf-8") + + +@customtracer +def ab2samediff( + lp1, + lp2, + p: str = "よりコンバージョンが高まるWEBページを作るための観点を比較します。", + m: str = "meta-llama/Llama-3.2-11B-Vision-Instruct", +) -> tuple: + """ + input1 (image): 比較画像1 + input2 (image): 比較画像2 + input3 (text): よりコンバージョンが高まるWEBページを作るための観点を比較します。 + input4 (text): meta-llama/Llama-3.2-11B-Vision-Instruct + output1 (text): 類似点 + output2 (text): 相違点 + + NOTE: HF版は VLM ベース。Vertex AI は使用しない。 + """ + from src.clients.llm_client import LLMClient + from pydantic import BaseModel + + class Comparison(BaseModel): + same: str + diff: str + + b1 = _pil_to_b64(lp1) + b2 = _pil_to_b64(lp2) + + prompt = ( + p + "\n\n" + "1. 2つの画像の類似点を説明してください。\n" + "2. 2つの画像の相違点を説明してください。\n" + ) + + client = LLMClient() + result = client.call( + prompt=prompt, + schema=Comparison, + model=m, + images=[b1, b2], + temperature=0, + ) + return result.same, result.diff diff --git a/apis/background.py b/apis/background.py new file mode 100644 index 0000000000000000000000000000000000000000..f78df3027060544a5a17f6202ec4476b3e049df6 --- /dev/null +++ b/apis/background.py @@ -0,0 +1,123 @@ +from openai import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, Field +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from typing import List, Optional +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer +from src.models.common import model + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json, re + + client = LLMClient() + + # Extract system prompt and user content from messages list + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + import json + return json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + +class Estimations(BaseModel): + name: str + prob: float + reason: str + button_prompt: str + change_candidates: Optional[List[str]] = Field(default_factory=list, description="変更すべきUI要素のリスチE) + +class EstimateCategory(BaseModel): + title: str + estimations: list[Estimations] + +class EstimateBackground(BaseModel): + estimated_bg:list[EstimateCategory] + +def get_openai_request(messages, format): + client = LLMClient() + # HF: beta.parse not available; use _ask_raw_hf instead + response = client.chat.completions.create( + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0 + ) + return response.choices[0].message.content + +@customtracer +def background(p, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): 親子でのスマE料節紁E親子でのお得感 チEEタの余剰利用 通話とネットEコストパフォーマンス スマEチEュー支援 家族向けE安E機E 豊富な端末ラインアチEE + input2 (text): default + output1 (json): 頁E + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """WEBPAGEのOCR惁Eを提供します。このLandingPageにつぁE持Eされた頁Eの制作背景を推定してください。頁Eごとに、指定数の候補と確玁E0~1の間で回答して、E +吁E景EEameEにつぁE、change_candidatesフィールドに「その背景を実現するために変更すべきUI要素」Eリストを斁EE配Eで返してください、E +※禁止ワード:「未来」「革命」「夢」に類する想像E幁E庁EてしまぁEードがあれば具体的で納得度の高い言葉に置き換えて +""", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + return get_openai_request(messages, EstimateBackground) \ No newline at end of file diff --git a/apis/base64img2component.py b/apis/base64img2component.py new file mode 100644 index 0000000000000000000000000000000000000000..3f8bdb8004009285b6b8938a757c786f177d53db --- /dev/null +++ b/apis/base64img2component.py @@ -0,0 +1,134 @@ +from openai import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json, re + + client = LLMClient() + + # Extract system prompt and user content from messages list + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + import json + return json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + +class UIoption(str, Enum): + element1 = "バナー/動画" + element2 = "CTA" + element3 = "チEスチE + element4 = "フォーム" + +class Component(BaseModel): + component_large: str + component_middle: str + component_small: list[str] + UIelement: UIoption + +class Components(BaseModel): + components: list[Component] + +def ask_raw(messages): + client = LLMClient() + # HF: beta.parse not available; use _ask_raw_hf instead + response = client.chat.completions.create( + model='meta-llama/Llama-3.3-70B-Instruct', + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=Components, + temperature=0 + ) + return response.choices[0].message.content + +@customtracer +def base64img2component(p, image64, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): 13: ※金融犯罪にご注愁E手口はこちら、E38: ▼ご利用条件はこちら、E77: ピンチE時E、E133: アコム一抁E409: WEB完結カードを作らぁE415: ご契紁EE翌日から最大30日間利0冁E421: 借りられめE0刁E 644: 今すぐお申し込み 722: 実質年玁E3.0%~18.0%ご融賁EE1丁EE~800丁EE 760: 以前ご利用があっぁE761: ご増額をご希望のお客さまはこちめE784: お客さまはこちめE819: *お申し込み時間めE査によりご希望に沿えなぁE合がござぁEす、E868: お借E可能かすぐに刁EめE秒スピEド診断 977: 侁E22 1055: ご年叁E税込) 1067: 侁E250 1146: 他社お借E顁E1249: 診断開姁E1323: ※クレジチEカードでのショチEング、E行でのお借E(銀行カードローン、住宁Eーン、E動車ローンなど)を除ぁE、キャチEングめEードローンのお借E状況をごE力ください、E1498: 借りるなめE1558: アコム一抁E1710: 20刁E借りられめE1835: アコムなら最短20刁Eお借Eが可能!※すぐにおが忁EとぁE時E、本ペEジの申込ボタンから早速お申し込みくだ 1960: ※お申し込み時間めE査によりご希望に添えなぁE合がござぁEす、E2045: カードを作らずWEB完絁E2165: お申し込み〜お借EまでWEBだけで完結できます。ご希望ぁEだければカードレスでご契紁Eただけます、E2354: |30日間利ぁE冁E2356: 契紁EE翌日から 2470: はじめてご利用のお客さまは、契紁EE翌日から最大30日間利ぁE冁E 2663: たっぁEスチEチE!(最短20刁E 2757: 申し込みから借りるまでの流れ※お申し込み時間めE査によりご希望に添えなぁE合がござぁEす、E2937: お申し込み・1忁E書類提出(審査)お申し込みぁEだぁE後、忁E書類を提EしてぁEだき審査に進みます、E3131: 2ご契紁EEお借E 3194: 審査結果の冁Eにご同意いただけましたら、契紁E続きは完亁Eなります。契紁EE、すぐにお借EぁEだけます。ご希望ぁEだければカードレスでご契紁Eただけます、E3335: 忁E書類とは? 3405: 本人確認書顁E免許証など) 3455: (該当する方のみ)+収E証明書 3488: ※「当社のご利用において50丁EEを趁Eるご契紁E行うお客さま」と「他社を含めたお借E総額が100丁EEを趁Eるお客「さま」につぁEは、収入証明書も忁Eで 3633: アコムの 3664: よくある質啁E3777: 申し込み編 3892: Q勤務Eに在籍確認E電話がかかってきま 3961: 原則、実施しません。※原則、E話での在籍確認Eせずに書面めE申告E容での確認を実施します。もし実施が忁Eとなる場合でも、お客さまの同意を得ずに実施することはありませんので、ご安忁Eださい、E4135: Q契紁Eると、忁Eカードが自宁E郵送さ 4159: れるんですか? 4205: ぁEえ。カードレスでご契紁E続きぁEだくことも可能です、E4296: 自宁E勤務Eに何か書類が送られてくる 4320: ことはありますか? 4366: 原則、E付しません、E郵送契紁E選択された場合や、書面の郵送受け取りを選 4418: んだ場合等を除ぁE 5914: は、ご返済シミュレーションをご利用ぁE5943: ださい、E5992: ペEジ上部に戻る▲ 7671: ご増額をご希望のお客さまはこちめE7671: 以前ご利用があったお客さまはこちめE8033: 今すぐお申し込み + input2 (text): スクショ + input3 (text): default + output1 (json): 頁E + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), f"base64img2component:", image64[0:30]) + + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """ +■構E要素名EアウトEチEサンプル +[ +{"component_large":"啁E/サービスの特徴","component_middle":"アコム", "component_small":[], "UIelement":"チEスチE}, +{"component_large":"FAQ/よくある質啁E,"component_middle":"よくあるご質啁E, "component_small":["自宁E勤務Eに何か書類が送られてくることはありますかEE,"家族割などの割引EありますかEE], "UIelement":"表絁E"} +] +""" + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url":"data:image/png;base64,"+image64}}) + # OpenAI 側の認証エラーなどをE示皁EメチEージとして上位に伝搬させめE + try: + return ask_raw(messages) + except openai.AuthenticationError as e: + # API キー / 絁E設定E問題を含むエラー冁EをラチEEして投げ直ぁE + # 呼び出しEEEE Origin 側などEでこEメチEージをキャチEしてユーザに表示できる + raise RuntimeError(f"[base64img2component] OpenAI AuthenticationError: {e}") from e \ No newline at end of file diff --git a/apis/base64img2score.py b/apis/base64img2score.py new file mode 100644 index 0000000000000000000000000000000000000000..ed0c6a160539d16a7981bf26ca1bbfe12ed58b7c --- /dev/null +++ b/apis/base64img2score.py @@ -0,0 +1,195 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer + +# 追加 +import logging + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +# logger 設定(褁E�� import 時に重褁E��定されなぁE��ぁE��ェチE���E�E +logger = logging.getLogger("base64img2score") +if not logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(handler) +logger.setLevel(logging.INFO) + + +class Answer(BaseModel): + citation: str + suggestion: str + score: int +raw_schema = Answer.schema() +raw_schema["additionalProperties"] = False + +def ask_raw(messages, model): + client = LLMClient() + # OpenAI 呼び出ぁE + response = client.chat.completions.create( + model=model, + messages=messages, + response_format={ + "type": "json_schema", + "json_schema": { + "name": "AnswerSchema", + "strict": True, + "schema": raw_schema + } + } + ) + # ト�Eクン使用量を取得(呼び出し�Eでまとめてログするため、ここでは print しなぁE��E + usage = response.usage + content_str = response + # 以前�E動作と同じくパースした JSON を返すが、呼び出し�Eで usage も使ぁE��ぁE�Eでタプルで返す + return json.loads(content_str), usage + + +@customtracer +def base64img2score(p, image64=None, model="meta-llama/Llama-3.3-70B-Instruct", openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): 13: ※金融犯罪にご注愁E手口はこちら、E38: ▼ご利用条件はこちら、E77: ピンチ�E時�E、E133: アコム一抁E409: WEB完結カードを作らぁE415: ご契紁E�E翌日から最大30日間��利0冁E421: 借りられめE0刁E�� 644: 今すぐお申し込み 722: 実質年玁E3.0%~18.0%ご融賁E��E1丁E�E~800丁E�E 760: 以前ご利用があっぁE761: ご増額をご希望のお客さまはこちめE784: お客さまはこちめE819: *お申し込み時間めE��査によりご希望に沿えなぁE��合がござぁE��す、E868: お借�E可能かすぐに刁E��めE秒スピ�Eド診断 977: 侁E22 1055: ご年叁E税込) 1067: 侁E250 1146: 他社お借�E顁E1249: 診断開姁E1323: ※クレジチE��カードでのショチE��ング、E��行でのお借�E(銀行カードローン、住宁E��ーン、�E動車ローンなど)を除ぁE��、キャチE��ングめE��ードローンのお借�E状況をご�E力ください、E1498: 借りるなめE1558: アコム一抁E1710: 20刁E��借りられめE1835: アコムなら最短20刁E��お借�Eが可能!※すぐにお��が忁E��とぁE��時�E、本ペ�Eジの申込ボタンから早速お申し込みくだ 1960: ※お申し込み時間めE��査によりご希望に添えなぁE��合がござぁE��す、E2045: カードを作らずWEB完絁E2165: お申し込み〜お借�EまでWEBだけで完結できます。ご希望ぁE��だければカードレスでご契紁E��ただけます、E2354: |30日間��利ぁE冁E2356: 契紁E�E翌日から 2470: はじめてご利用のお客さまは、契紁E�E翌日から最大30日間��利ぁE冁E 2663: たっぁEスチE��チE!(最短20刁E 2757: 申し込みから借りるまでの流れ※お申し込み時間めE��査によりご希望に添えなぁE��合がござぁE��す、E2937: お申し込み・1忁E��書類提出(審査)お申し込みぁE��だぁE��後、忁E��書類を提�EしてぁE��だき審査に進みます、E3131: 2ご契紁E�Eお借�E 3194: 審査結果の冁E��にご同意いただけましたら、契紁E��続きは完亁E��なります。契紁E���E、すぐにお借�EぁE��だけます。ご希望ぁE��だければカードレスでご契紁E��ただけます、E3335: 忁E��書類とは? 3405: 本人確認書顁E免許証など) 3455: (該当する方のみ)+収�E証明書 3488: ※「当社のご利用において50丁E�Eを趁E��るご契紁E��行うお客さま」と「他社を含めたお借�E総額が100丁E�Eを趁E��るお客「さま」につぁE��は、収入証明書も忁E��で 3633: アコムの 3664: よくある質啁E3777: 申し込み編 3892: Q勤務�Eに在籍確認�E電話がかかってきま 3961: 原則、実施しません。※原則、E��話での在籍確認�Eせずに書面めE��申告�E容での確認を実施します。もし実施が忁E��となる場合でも、お客さまの同意を得ずに実施することはありませんので、ご安忁E��ださい、E4135: Q契紁E��ると、忁E��カードが自宁E��郵送さ 4159: れるんですか? 4205: ぁE��え。カードレスでご契紁E��続きぁE��だくことも可能です、E4296: 自宁E��勤務�Eに何か書類が送られてくる 4320: ことはありますか? 4366: 原則、E��付しません、E郵送契紁E��選択された場合や、書面の郵送受け取りを選 4418: んだ場合等を除ぁE 5914: は、ご返済シミュレーションをご利用ぁE5943: ださい、E5992: ペ�Eジ上部に戻る▲ 7671: ご増額をご希望のお客さまはこちめE7671: 以前ご利用があったお客さまはこちめE8033: 今すぐお申し込み + input2 (text): + input3 (text): gpt-4o + input4 (text): default + output1 (json): 頁E�� + """ + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """与えられた情報と質問に対して、採点基準を参�Eして以下を回答します、E +citation:当該箁E��の引用 +suggestion:満点でなぁE��合�E満点になるよぁE��具体的な持E��。満点の場合�E優れた点を�E体的な叙述 +""" + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + img_flag= "none" + if image64: + messages[1]["content"].insert(0, { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{image64}"} + }) + img_flag = image64[-4:] + + # --- 前�E琁E��マリ�E�E行!E--- + summary_parts = [] + for i, msg in enumerate(messages): + content = msg.get("content") + is_str = isinstance(content, str) + if is_str: + length = len(content or "") + kind = "str" + elif isinstance(content, list): + length = len(content) + kind = "list" + else: + # unknown type: show repr length where possible + try: + length = len(content) + except Exception: + length = 0 + kind = type(content).__name__ + has_image = False + if isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "image_url": + has_image = True + break + emoji = "🖼�E�E if has_image else " E + summary_parts.append(f"[{i}:{msg.get('role')}] {kind}(len={length}) {emoji}") + pre_summary = " | ".join(summary_parts) + + # 実行とログを一行で出す(エラー含む�E�E + dt = datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%m/%d %H:%M") + + try: + # ask_raw は (result, usage) を返すよう変更 + result, usage = ask_raw(messages, model) + + # messages めEJSON にした長さ(可読のため ensure_ascii=False�E�E + msg_len = len(json.dumps(messages, ensure_ascii=False)) + + combined = ( + f"[base64img2score] (img:{img_flag}) {pre_summary} ↁE" + f"({dt}) token:{usage.total_tokens}, len:{msg_len},mdl:{model} " + f"snt:{usage.prompt_tokens}, Received:{usage.completion_tokens}" + ) + # 一行で出力!Eogging はスレチE��セーフ!E + logger.info(combined) + + return result + + except Exception as e: + # エラー時も一行で記録して再送�E�E�詳細なトレースは再送�E先で扱ってください�E�E + err_msg = f"[base64img2score] {pre_summary} ↁE({dt}) ERROR: {type(e).__name__}: {str(e)}" + logger.error(err_msg) + # 忁E��ならここで詳しい traceback めElogger.debug などで出せますが、E + # 要望どおり「実行前後を一行で出す」ことを優先して多行�E力�E抑制してぁE��す、E + raise + diff --git a/apis/baseimg2baseimg.py b/apis/baseimg2baseimg.py new file mode 100644 index 0000000000000000000000000000000000000000..46fe5d1e00e81d52318f7c3ecf01d4499588f333 --- /dev/null +++ b/apis/baseimg2baseimg.py @@ -0,0 +1,56 @@ +""" +baseimg2baseimg: 画像編集(画像 + プロンプトから新画像生成)。 +HF版: HF Inference API の image-to-image を使用。 +NOTE: 元の実装は OpenAI gpt-image-1 を使用。HF版は SD inpainting ベース。 +""" + +import base64 +import os +from io import BytesIO +from typing import Optional + +from PIL import Image + +from src.utils.tracer import customtracer + + +@customtracer +def baseimg2baseimg( + base64img: str, + p: str, + q: str = "low", + openai_key: str = "default", + model_name: Optional[str] = None, +) -> str: + """ + input1 (text): base64エンコードされた画像 + input2 (text): 変更内容のプロンプト + input3 (text): low + input4 (text): default + output1 (text): 生成画像(base64) + + NOTE: HF版は runwayml/stable-diffusion-inpainting を使用。 + """ + from huggingface_hub import InferenceClient + + hf_token = os.environ.get("HF_TOKEN") + if not hf_token: + raise ValueError("HF_TOKEN is required for baseimg2baseimg.") + + model = model_name or "runwayml/stable-diffusion-inpainting" + + if "," in base64img: + base64img = base64img.split(",", 1)[1] + image_bytes = base64.b64decode(base64img) + image = Image.open(BytesIO(image_bytes)).convert("RGB") + + client = InferenceClient(token=hf_token) + result = client.image_to_image( + image=image, + prompt=p, + model=model, + ) + + buf = BytesIO() + result.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") diff --git a/apis/baseimg2cninfo.py b/apis/baseimg2cninfo.py new file mode 100644 index 0000000000000000000000000000000000000000..348d88c0ae7eb2f0ae1c19833ca7dea9e6d226bf --- /dev/null +++ b/apis/baseimg2cninfo.py @@ -0,0 +1,114 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel +from enum import Enum +from io import BytesIO +from PIL import Image +from functools import cache +from datetime import datetime +import pytz + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class UIoption(str, Enum): + element1 = "バナー/動画" + element2 = "CTA" + element3 = "チE��スチE + element4 = "フォーム" + +class Component(BaseModel): + component_large: str + component_middle: str + component_small: list[str] + UIelement: UIoption + +class Components(BaseModel): + theme: list[str] + components: list[Component] + +def ask_raw(messages, m): + return _ask_raw_hf(messages, m) + +def baseimg2cninfo(p, base64img, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): 13: ※金融犯罪にご注愁E手口はこちら、E38: ▼ご利用条件はこちら、E77: ピンチ�E時�E、E133: アコム一抁E409: WEB完結カードを作らぁE415: ご契紁E�E翌日から最大30日間��利0冁E421: 借りられめE0刁E�� 644: 今すぐお申し込み 722: 実質年玁E3.0%~18.0%ご融賁E��E1丁E�E~800丁E�E 760: 以前ご利用があっぁE761: ご増額をご希望のお客さまはこちめE784: お客さまはこちめE819: *お申し込み時間めE��査によりご希望に沿えなぁE��合がござぁE��す、E868: お借�E可能かすぐに刁E��めE秒スピ�Eド診断 977: 侁E22 1055: ご年叁E税込) 1067: 侁E250 1146: 他社お借�E顁E1249: 診断開姁E1323: ※クレジチE��カードでのショチE��ング、E��行でのお借�E(銀行カードローン、住宁E��ーン、�E動車ローンなど)を除ぁE��、キャチE��ングめE��ードローンのお借�E状況をご�E力ください、E1498: 借りるなめE1558: アコム一抁E1710: 20刁E��借りられめE1835: アコムなら最短20刁E��お借�Eが可能!※すぐにお��が忁E��とぁE��時�E、本ペ�Eジの申込ボタンから早速お申し込みくだ 1960: ※お申し込み時間めE��査によりご希望に添えなぁE��合がござぁE��す、E2045: カードを作らずWEB完絁E2165: お申し込み〜お借�EまでWEBだけで完結できます。ご希望ぁE��だければカードレスでご契紁E��ただけます、E2354: |30日間��利ぁE冁E2356: 契紁E�E翌日から 2470: はじめてご利用のお客さまは、契紁E�E翌日から最大30日間��利ぁE冁E 2663: たっぁEスチE��チE!(最短20刁E 2757: 申し込みから借りるまでの流れ※お申し込み時間めE��査によりご希望に添えなぁE��合がござぁE��す、E2937: お申し込み・1忁E��書類提出(審査)お申し込みぁE��だぁE��後、忁E��書類を提�EしてぁE��だき審査に進みます、E3131: 2ご契紁E�Eお借�E 3194: 審査結果の冁E��にご同意いただけましたら、契紁E��続きは完亁E��なります。契紁E���E、すぐにお借�EぁE��だけます。ご希望ぁE��だければカードレスでご契紁E��ただけます、E3335: 忁E��書類とは? 3405: 本人確認書顁E免許証など) 3455: (該当する方のみ)+収�E証明書 3488: ※「当社のご利用において50丁E�Eを趁E��るご契紁E��行うお客さま」と「他社を含めたお借�E総額が100丁E�Eを趁E��るお客「さま」につぁE��は、収入証明書も忁E��で 3633: アコムの 3664: よくある質啁E3777: 申し込み編 3892: Q勤務�Eに在籍確認�E電話がかかってきま 3961: 原則、実施しません。※原則、E��話での在籍確認�Eせずに書面めE��申告�E容での確認を実施します。もし実施が忁E��となる場合でも、お客さまの同意を得ずに実施することはありませんので、ご安忁E��ださい、E4135: Q契紁E��ると、忁E��カードが自宁E��郵送さ 4159: れるんですか? 4205: ぁE��え。カードレスでご契紁E��続きぁE��だくことも可能です、E4296: 自宁E��勤務�Eに何か書類が送られてくる 4320: ことはありますか? 4366: 原則、E��付しません、E郵送契紁E��選択された場合や、書面の郵送受け取りを選 4418: んだ場合等を除ぁE 5914: は、ご返済シミュレーションをご利用ぁE5943: ださい、E5992: ペ�Eジ上部に戻る▲ 7671: ご増額をご希望のお客さまはこちめE7671: 以前ご利用があったお客さまはこちめE8033: 今すぐお申し込み + input2 (text): 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 + input3 (text): default + output1 (json): 頁E�� + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), f"text2component") + + + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """ +■構�E要素名�Eアウト�EチE��サンプル +[ +{"component_large":"サービス吁E,"component_middle":"アコム", "component_small":["カードローン・キャチE��ングなら消費老E��融�Eアコムにご相諁E��ださい"], "UIelement":"チE��スチE}, +{"component_large":"CTAボタン","component_middle":"お申し込みはこちめE, "component_small":["簡十E0秒でお申し込み完亁E], "UIelement":"CTA"}, +{"component_large":"Q&A�E�E,"component_middle":"よくあるご質啁E, "component_small":["自宁E��勤務�Eに何か書類が送られてくることはありますか�E�E,"家族割などの割引�Eありますか�E�E], "UIelement":"表絁E��"} +] +""" + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}) + return ask_raw(messages, 'gpt-4o-2024-11-20') \ No newline at end of file diff --git a/apis/baseimg2cta_detail.py b/apis/baseimg2cta_detail.py new file mode 100644 index 0000000000000000000000000000000000000000..a27940548ddb111e5395d1bef1c217da899c2ab8 --- /dev/null +++ b/apis/baseimg2cta_detail.py @@ -0,0 +1,143 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from enum import Enum + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + +client = LLMClient() + +class CTAStyle(BaseModel): + font_size: str + font_color: str + letter_spacing: str + border_radius: str + box_shadow: str + background_color: str + gradient: str + hover_effect: str + padding: str + +class CTA(BaseModel): + main_copy: str + sub_copy: str + sub_copy_position: str # "inside" or "outside" + html: str + css: CTAStyle + +class CTAlist(BaseModel): + cta_buttons: list[CTA] + +def ask_raw(messages, model): + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=CTAlist, + temperature=0 + ) + return response + +def baseimg2cta_detail(base64img, ocr, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): 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 + input2 (text): OCR + input3 (text): default + output1 (json): cta + """ + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + messages = [ + { + "role": "system", + "content": "あなた�E優れたWEBマ�Eケターで、ランチE��ングペ�Eジの要素を見�Eけることに長けてぁE��す、E + }, + { + "role": "user", + "content":[ + {"type": "text", "text":"""LPのスクロール画像とOCR結果が以下にあります、E +こ�E画像�Eに存在するすべてのCTAボタンにを探し�Eして個数を数えてください。文言が�Eく同じ�Eタンは不要です、E +また、それぞれ�ECTAボタンのチE��インを�E現するcssを含んだHTMLを書き�Eしてください。書き�Eす際には、CTAボタンごとに +・斁E��サイズ +・斁E���E色 +・斁E���E間隔めE��置・改衁E +・構�Eするレクタングルごとの角�E丸ぁE +・影めE�Eタン背景の色(gradientのあるなぁE +・サブコピ�Eが�Eタン外かボタン冁E�� +・ボタン冁E�E矢印等�E絵斁E��(�EめE���!E +を別、E��認識して丁寧に再現してください。OCRの惁E��で斁E���Eを精確に修正しつつも、主に画像情報からボタンを抽出することに注力してください、E +ボタンの色めE��ザインは基本皁E��一種類になる�Eずなので、特別な琁E��がなぁE��り�E褁E��の色合いのボタンを混ぜなぁE��ください + +例!E +

サブコピ�E1

+ + +# OCR結果 +"""+ocr} + ] + }, + ] + + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}) + r = ask_raw(messages, "meta-llama/Llama-3.3-70B-Instruct") + + return r \ No newline at end of file diff --git a/apis/baseimg2ecinfo_rect.py b/apis/baseimg2ecinfo_rect.py new file mode 100644 index 0000000000000000000000000000000000000000..f0af5d23ed9ad3adaceba613ea0a5dc18af4a757 --- /dev/null +++ b/apis/baseimg2ecinfo_rect.py @@ -0,0 +1,190 @@ +import os +from enum import Enum +from pydantic import BaseModel + +from src.utils.tracer import customtracer +from src.clients.llm_client import get_llm_client + +class Category(str, Enum): + ビジネス = "ビジネスEEaaS・法人支援EE + ヘルスケア = "ヘルスケアE美容・健康EE + ヒューマンリソース = "ヒューマンリソースE求人・紹介!E + コマEス = "コマEスE趣味・食品・衣類!E + ファイナンス = "ファイナンスE融E保険・不動産EE + インフラ = "インフラE電気E通信・ガス・住屁EE + ライフイベンチE= "ライフイベント(教育・結婚E相諁EE + +class CategoryMiddle(str, Enum): + # ビジネス + ITソフトウェア = "IT・ソフトウェア" + マEケ支援コンサル = "マEケ支援・コンサル" + オフィス機器用品E= "オフィス・機器用品E + + # ヘルスケア + 健康食品器具 = "健康食品・器具" + 美容医療クリニック = "美容・医療クリニック" + 美容コスメ = "美容コスメ" + フィチEネスジム = "フィチEネスジム" + + # ヒューマンリソース + 求人惁E = "求人惁E" + 人材紹仁E= "人材紹仁E + 人材派遣 = "人材派遣" + + # コマEス + 動画アニメゲーム = "動画・アニメ・ゲーム" + リユースリサイクル = "リユース・リサイクル" + 旁EEチEレジャー = "旁EEホテル・レジャー" + 趣味交隁E= "趣味・交隁E + 新聞雑誌メチEア = "新聞E雑誌E惁EメチEア" + 自動車レンタカー用品E= "自動車Eレンタカー・用品E + 飲料食品生活用品E= "飲料食品・生活用品E + 家電パソコン = "家電・パソコン" + ファチEョン = "ファチEョン" + + # ファイナンス + 不動産 = "不動産" + 保険 = "保険" + ローン = "ローン" + クレカ電子決渁E= "クレカ・電子決渁E + 証券FX先物 = "証券・FX・先物" + 銀衁E= "銀衁E + + # インフラ + ネット通信サービス = "ネットE通信サービス" + 電気ガス = "電気Eガス" + 住宁E備リフォーム = "住宁E備Eリフォーム" + + # ライフイベンチE + 士業相諁E= "士業・相諁E + 学習スクール = "学習Eスクール" + 結婚E会い = "結婚E出会い" + 葬儀墓地 = "葬儀・墓地" + 引越し介護 = "引越し・介護" + +class Meta(BaseModel): + 会社吁E str + 業畁E Category + 中刁EE CategoryMiddle + サービス: str + 啁E: str + タイトル: str + 訴求テーチE list[str] + +class cood(BaseModel): + x: int + y: int + +class str_with_rect(BaseModel): + text: str + html: str + rect: list[cood] + +class pageInfo(BaseModel): + # ペEジ共送E + メタ: Meta + ロゴ: list[str_with_rect] + グローバル検索バE: list[str_with_rect] + ハンバEガーメニューアイコン: list[str_with_rect] + カートアイコン: list[str_with_rect] + ユーザーメニュー: list[str_with_rect] + + # ナビゲーション + ブレチEクラム: list[str_with_rect] + ペEジネEション: list[str_with_rect] + タブE替: list[str_with_rect] + + # トップEージ + メインビジュアル: list[str_with_rect] + プロモーションバナー: list[str_with_rect] + カチEリカーチE list[str_with_rect] + + # 啁E一覧ペEジ + 啁E一覧: list[str_with_rect] + フィルタ: list[str_with_rect] + ソーチE list[str_with_rect] + ペEジャー: list[str_with_rect] + クイチEビューアイコン: list[str_with_rect] + + # 啁E詳細ペEジ + 啁E吁E list[str_with_rect] + 価格: list[str_with_rect] + ブランチE list[str_with_rect] + サムネイル: list[str_with_rect] + 画像ギャラリー: list[str_with_rect] + カラースウォチE: list[str_with_rect] + サイズセレクタ: list[str_with_rect] + 在庫スチEEタス: list[str_with_rect] + 配送情報: list[str_with_rect] + ボタン_カート追加: list[str_with_rect] + ボタン_今すぐ購入: list[str_with_rect] + レビューサマリー: list[str_with_rect] + レビューボタン: list[str_with_rect] + QnAリンク: list[str_with_rect] + バッジタグ: list[str_with_rect] + 関連啁Eカルーセル: list[str_with_rect] + + # カートEージ + カート商品リスチE list[str_with_rect] + 数量セレクタ: list[str_with_rect] + 削除アイコン: list[str_with_rect] + クーポン入劁E list[str_with_rect] + 注斁E計サマリー: list[str_with_rect] + チェチEアウトEタン: list[str_with_rect] + + # 共通下部 + フッターリンク: list[str_with_rect] + SNSアイコン: list[str_with_rect] + カスタマEサポEトリンク: list[str_with_rect] + + +@customtracer +def baseimg2ecinfo_rect( + base64img: str, + openai_key: str | None, + google_api_key: str | None, + p: str = "", + model: str = "Qwen/Qwen2.5-VL-72B-Instruct", +): + """ + input1 (text): /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUSEhIVFRUVFxUVFxUVFRcVFRUVFRUWFhUVFRYYHSggGBomHRUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGhAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAQsAvQMBEQACEQEDEQH/xAAcAAABBQEBAQAAAAAAAAAAAAACAAEDBAUGBwj/xABPEAABAwEEBAcKCggFBAMAAAABAAIDEQQSITEFBkFREyJhcXOR0gcUMlOBkpOxsrMzQlJicnShwdHwFiMkNENjgsIXNaPD02SitOEVg+L/xAAbAQACAwEBAQAAAAAAAAAAAAAAAQIDBAUGB//EADoRAAIBAgEIBwgDAAIDAQEAAAABAgMRBBIhMUFRcZHRBRMVYYGhwRQiMkJScrHhM5LwI2IkNPGiQ//aAAwDAQACEQMRAD8A3bp5CF6TMeLs2J0ICFJg6cURNmu/nFScblSqZJYjeHYgqtqxojJSV0PJhjmNqFnCXu5wTK00DXAHdknZrSJyjJWiwopHVoUmlqHGcr2ZKGbyo3LMnaEIm7kspjyIkgFFEstYcORYdx3YiiNDB51YggZuPkUpMqhHYyw0KDLkSNKTJJ2LETlW0XRZfhVMjTEstVZch0gHQMYoEyKR1Bj9mKmkQk7LOUXzNORVqi0Z3OL0FV6sRSznS4xkY4fnYt2aRxfepvuL8zmm7RjDVgcSTJWpc8fFeBTihZll3ee1nbUb5dXkxvG91fS1rff3A0GXBR/6v/IpWn9XkiP/AA6MjzfMrh7mn4OPzpv+RSyZP5vJFWVCD/j/AP1LmXI56j4OP/U7arcJr5vJGmNWm1fI83zIpAMxFGfS9tSSn9XkiuXVaVT83zB4cHAxx9ctR5eETyJrRLyRHraTVnDzfMnZaPmR03/rft46g6c/q8kXKrTXy+b5kgl+ZH/qdtRyZ/V5Inl0/p83zJmvPyI/9TtqLjLb+C1Sh9PmyOaYj4kZ9J204wk/m/BXOpCPyebI4bSTmyMek7alKnL6vJEIVoPTG3i+ZI7O9cjr/wDb21FKejK8kWPq75WT5vmOLUfkMrupJ28UdXL6vwProfT+QxO7xbKc0nqvpZEvq/BJVY/R5stRE/Jj6pO2qmpbS+Lh9P5Lcd44Uj6n9tVNS2l6cXqLMZdkOD6n9tQae0ti46kHx/mea/tqNntJXWwFzpB8jzXdtNJ7SLklqIXySO8Ex4cjx/epqNtJBzv8KK8lpfW7RjjgSAHAgGtCRf5D1KSh3lcp7Y3IbRQmhaBg04DaRXeVZTbz5ymtGObNpK1zdUc6vvtM2TsMgXX5ODuQFac6OdaMtdw7XJwZYCOLwbcd3HkVdNZTlv8ARF1eXVqCejJ9WGXYVPXs/wDSkRvYGSpGVebFNZiMrtZkQRWgA0+zJScblcKlnYth4Vdi9SViCQGtQprRYqkne6CuhwqOpK7TzkrKauiIPc04HyHEKVkytSlB6S1FbGnkP2darcGjRCvF9xLUOUc6J3UiTggcDj+d6jlPUTyE8zHETxgKEcqMqL0jUJrMhgXDwwAjNqBOS+JEodQ45FRtcnezzluMqtl8Syw15FWy5ZyeNrhk6vOoNp6iyKktDLDXV51W0Wpic4BCQNpDFwTsJtGNwDWSulMjjWoaK5B2JBNcQCXUyoDtopRpO9yudaNrBzzC8eUM5fiqynF595VVqLMu4qSwkmocQtCkthklBt5mY4iIANwfSaRXnWm6b0nOyWle3AltxLixhY5wMTajAHw5Mq0qeRVU8zk76/RF9e8lCLV/d9WW9V7Ox83ASguYY3OYTea4FpHFOVcCepV4qcoQyoabl3R9OFSq6dRXVs2lHVxat2VuTHDk4R9PWue8XWel+SOxHo7DR0R83zOM1x0UyOcCMhoLA/jE4m84HE8w610MHXlOLUjjdKYSFOSlDMZ9jtT63XY8tK4c4WuUVpOfTqSvZmlwe7D87lVc1ZBmWiNzXVyxzBoFbFpoyTi4u5savaJ4eXjhwa0Xnmpxrk0HeTXLYDyLNia/VQzaXoN2BwnX1PeTstPI6r9GLJWvBu9I/wDFc72yvt8kdrszC6cnzfM5vTJhZaOBgZQMFZHlziA51C2MVOdMTzjlWrDzqTTcmYsXSo0pKNNW25wakDKqvM2dIZswrnQ7ihxzAqiuTSuwr4Qz2fYopZ7FknmvpIGaQZkPtwCm6UtLKViaehEjJ3A4trygqLitTJqpJPOjQjmCocWbIzRaZMN6rcWWqaCdM3PD70slknOOkB1oBxwpvripKD0EXUWkgmtYzGI31+5SjTK5VlpRn2xzTm0K+CaMlVxelFa0S0cKYcRnsqVON8q+0rrTtk2+lDttzuT1qXVoisRJHMwaYc3AtB3jLHfyFaGkznQqSjuNPSOkmng6tIrE1wpjTjyD7lRRjaUt/ojXi6icKd1pj6sk0BpUi0xcfC8G4g436sz/AKgjE006crIMDXlGvG7zaOOY9G4VcbJPUZRxfdHaf2eUVwMkZp84Bw9h3Wt2BdptHJ6XjempbGYVngaW4PINPBOw7cF0W2tRw4wi1pLURkb4JDt3G+5ReS9JbDLj8LuPJaK0bcLnEgXaZkmgojJtnuN1Mr3bXZ3WiLMIYgzM5uO9x+4ZDmXHrT6ybkelw1JUaahxI9PaY73hc8CrzxY2/KkPgjmGJPICoQp5TsWVaypxcjhdGWd1CXSFznFxeTm5zjVxPOarsJKCSSPNXlVk5NmjFIRgWnnGKi0nrLYScczRK5zHbj6x+CSUkTbhPvKrRiQW0HKVPvKEs9rZghHHuCLyGo09gJbQUa403bkadIrZKtFsjY+6auJ6sFJq+ZEIvJd5MvWa1MPOVTKEjXTrQZcjlON1wdzhVOK1miMnqaYEVobU33Y7tibg/lRGNWN/fecaaauLS0jLPFOMbaRTnfPGzM+0yDnOAor4ox1Joa1nEcXExx+TiqNP5s+tkq7zQzfKiAO5APJ+CtsUXPOn64sJBLJCRhUtYajceNisXaVLY/LmdF9B13plHz5GlpTWqFne5MTyH2dj6ANwBlmFPC5CoQx9OLk7PO+7mWVeh601BKUfdVte19xXi13szDebDKKUIBDHUINcDfBUn0jSatZ+XMjHoSvGSkpR8+R7bDaw9rXtODgHDmcKj1rMkdFyMDX6cNsT5SCeCcySjQCfCuHAkDJ52qcJ9U8tlNWj7RHq1pe08pZrjEDUMlFMsGdtaO0qWx+XMwdg4i98qPnyLo17s58KGWu9oYP9xLtKnqT8uZPsSs9Lj58ju9Qray1MNobG9rGuLWGSlXEeE4UJwGVd9dyjPEqrH3bltHAvDz99ptaLX9UjsOGVGSbMs8p0/wB0SzPtTqtleyKrIywMLXGvHkFXitSKA7gN6VPE04POmFbBVaqVmlx5FH/EGzB94QzY5gtj+6RaO0KdrNPy5mLsasp5SlHz5Eo7o9nGUdo6oj/eo+309j/3iWLomstEl58hx3RrLWphtFRkQ2OvvEvb4akxromre7kvPkM7ui2U5xWg84jH+4n7fDYxPoiq9Ml58iKXX6yfFhtAPNHn6RNdIw1p/wC8SuXQtT5ZJceRCO6BF8mbqj7al2jR2Py5kOxcV9cfPkTO7osGyObqjP8Aeo9oUdj8uZN9D4nVKPnyBPdDg2RSjmbGP9xPtGlsflzF2NiNUo+fINndGgAIEc48kZHlPCVSfSFJvQ/LmSj0RiErKUfPkC3uiw41jmP9MeH/AHp9oUdj8uYl0Pidco+fIdvdDs22KbyCMf3pdo0tSflzGuhq+uUfPkNNr/ZT/Bn6o/8AkTXSNNan5cyMuhar1x8+R1FntolZFK0Ua+GJwvHjUpuB+9acPNVIuS1tmHGU3RnGnK11FEwun4pKuz7TN7r1HhhXmT3Zqae8GyfVI/fWhAlpMpAz3zUPSHCaPsxri2MRnniJj/sXSo54JnGxDyaskX9YIeGss8PjIpGjnLDd+2inOF4tFdOrkyT7z5zaaiq5J3TQ0Hop9qnZAzAvOLqVDGDFzzzDrNBtU4Qc5WRXVqKnFyZ9B6PhZBEyGIXWRtDWjkG87TtJ2krqKCSsjiyquTuzle6brP3vZ+AjdSWcFtQcWRZPfyE+COcnYqMRLJVlpZpwkOsld6EeLhYDqiQAkAJACQA4QhM9f0HoCCdxDwxoBAoGRVNQ84XyMrmyuflXqMRkU1mgn4btie08Vg+srt5VSS8d+1rZ3mjY9U7K4Orjdo6rIo6FpEZoCLwLuMW4EipGdCqJ1UrWhHPtW/uWbXoNVKhKSd6s3bY9WbvefVpI59VLM2IvN2ojD6cHGKFxddvCtQDdDQM6uCcakZTUerjpto/WrTuIzoyjTcutlovp36c+vRvZYi1Qspja+7i5sZ8COl5zLxAF2uw05s1B1UptZEdL1d9i2GHbgpOpPOl82tq5JNqXZWuYLuDn3TxYyaXHnY3DFhFeUFRjXTT9yOjZ3rmSlhnFxXWTzv6u58jI1m1ehs0gY2MEFtauYytdowHN1rThnCtBycY8DFjVVw9RRjOVu9njjMhzBeYPavSeyavg952U/wDTxc+S7mA/it3nlOl0/aL9yNFltcNgWx00c5YiSPDS07j1LzB7s1NPNN2yYH90j2fzrQgitJl3DuPUgkeodyy3HvaSI/w5SR9F7QfaD108Fng1sZw+k3k1E9q/B2gtK2ZBzetPBNKWQxzSxgGjJJGjD4ocQ37KLhVI5MnHYz1VKeXTjLakel9zjQ/e8JneP1swBFc2RZtbznwj/TuXTwlDJjlPS/wcTH4tSnkR0L8nV2vSLY2OkeaNYC5x5B6ytMkopt6DHCTnJRjpZ4fp3SclqnfO8HjHit+QweCwcw+0k7VxKk3OTkz01GmqcFFFC4dx6lAtFcO49SAFcO49SAFcO49SAFcO49SAFdO49SBHRx632hpq2NgI2gPB9pdR9LVWrOMfPmcSPQVGLvGcr+HIdmuNpAc0MYA6l4APoaGorxscUn0rUbTcY5t/Ma6DopOKnKz06ORI3Xi1ht0ABtCLoMgFCCCKXtxPWl2nNu+RG+58ya6Hgo5PWTtvXIdmvVsGVBgBgZMm0oPC2UHUk+kpPTCPB8wXREFoqT4rkE7X62nM5cslciM73KetJdItf/zjwfMk+iovTVnxXIhtGulqk8NrXZ+FfNKmppV29Sj0pOKtGEVx5kJ9C0pu8qknva5HMALmHZec9k1bP7JZht73i2/NXbwP8PieX6Vf/kNdyNHvYnG9TnH4LZl2Ob1Lee54qV5k9waenMrL9Uj99aEAZiAOo7n9ruTSM+WwHysd/wDsro9Gv/kcdq/H/wBON02rUYz2O3Ffo7vvpdnIPMdecXatDibSLy4Vi4kr9xq0NDPK5jvICuXLCZeKd9GZ/ryO/DpJUuj4tP3s8VxvfwTXjY7TvpdTIOB15xOvemr5FmaeK2jpOV2bWeTBx5bu5cjpCtn6pePI9J0Nh3k9fLXmXq/Tici7I0pXZzrmHdO6j/Rygvd/1oK0u0rtonmFnCrq3/1//almDOcRaAwyP4Oty+7g73hXLxuXvnXaV5UAdVoPUPSBnhMtgkMXCx8KHFrBwd8cJm4Hwa5Jhc3dbe5ba3WyU2KztFmNws/WMaG8RoeKOde8IOOI2osJM4nWTV6ewSiG0hoe5gkF114XXOc0Y0zqx2HMkM1NBaL0RJAx9r0hJBMb16JsD3taA9wZRwYQatDTntQGc2NHaraEnlZDDpSd8kjrrGizPFTzmOgwBNTgAExXZymtmiGWO1zWZkvCiIgX6UNS0OLTsqK0NPsyCGizqDYYp9I2WGZgfG97w5prRwEMjhlytB8iEDKetNmZFbbVFG0NYyeVjWitGta8gAeQIAzEAVEEj2TV5o7zsuFf2eHLmXawD/4vE8t0sk8R4IlllLTQ3hyVW9K5x5Np58x5EV5Y9+aWnMrL9Uj99aEAZqANHV2a5aYzsJun+oFo+0ha8FPJrx4cf2c/pWl1mDqLYr8M/wCDv769PY8FYV5FgKWmNJCCIvzOTRvccvJmTyArPiq6oU3PXq3mzAYN4qsqerS3sX+zLvPPCSSSTUkkknMkmpK8s227s+gRiopRirJZkaGgLXBFMH2mz98RgOBivXKkjA3huSGddZdYtDySMjGhcXvYwftDs3uDR9pTFnNbXG0aHsFqdZjooSlrWOLhM5o44Ju0JOynWgFc4nTlps9slhZYLCbO41ZwYkvmV7y0MoXUApiP6khnRSauazSg1da250JtrGgHmbN9yYsx2HdI1Nt9ulgmgkbGBCGStfO+NgeHF2AYCHeERXkCGJM8x1o1NlsDGSSzWeQvfcuwyF7gbrnXnVaMOLSvKEiVznCUDPVdRdXZ7BYp9Kus75LRwR72hpVzWuwMz25450GNwHa6gZFs8sdI55L3OvOeS9zicXOcbznHlJJPlSGdL3Mf82sfSSf+PMmhM6zWHTOhZ7XaIbdY32eRk0rO+rPiXFry3hJGsFS454tegFc5bXDVWCyxx2izW6O0wTOLGUpwoLW1dW7VpptPFoXAUxQNHFJEj1vQZPetloQP2eHHbkV3ej/4fFnkemX/AOV4InLsTfJduIIot1thyL7Tykryh9DNLTmVl+qR++tCAM1ABRyXSHD4pDvK01+5OMslqWzPwIzhlxcXrTXHMekB9cRtxXs1nV0fOHFp2YqosKxw2n9I8PLgeIyrW7j8p/l9QC8xjsT11TN8K0cz3HReC9mo+8velnfdsXh+bmcsR0hIA9A1G0BZrOyPS1vtMQhYS6GFhvySSsOAI+U1wrcFTUAkgAgsTOP1i0u62Wqa1PF0yvvBud1oAYxvKQ1rQTvBSGUYpXNcHMcWuaQ5rmktc0g1BaRiCDtCALUul7U7wrXaXfSnld63IA7XW4C06B0bOeOYZHQOLsTQCRhJryws60xazztsYGQA5hRIZ6Loqz6I0dFHap5hbrS9rZIrNGKMYSKtMoNbpG9+7BhITFnZj/4hW/v3v7hON4PAY8BwVa8Fd+2/nXHkRcLFnugO0dOyK3WN4jktDnCay0xY8Cr5CBgw1oDsfeDh8aowRS7mP+bWPpJP/HlQgZS10/zC2fWZ/eOSGYl0Z7eZAFVBI9O0dLSCy5fu0OZ+aV3ejn/w+J5Dptf+T4IeS045DqW65yVFsy3au2ffJ5w/BYezqPfx/R1u28VsjwfMu6U1fgdwFeEo2zsaKOAwEsx2t5SqqeApScr3zP8A2ovr9L4inGDWTnV3m733lIauWY+N85vZVvZ1Hv4/oo7cxOyPB8x/0Yg/meeD/ajs2j38f0Lt3E/9eD5hN1Vg3y+cOyn2dR2vj+g7cxOyPB8x/wBFYf5vnj72o7Oo7XxDtzE7I8HzEdV4Ngl89vqupdm0e/j+h9uYnZHg+YB1YhGfCecOyjs2j38f0LtzE7I8HzBOrMIOPCDneOyjs6j38f0PtzE/9eD5g/ozBX4/OHDso7Oo9/H9B25iv+vB8xSauQjLhPOHZSfR1Hv4/oa6cxOvJ4fsjGrsRy4Trr6mpdnUu/8A3gT7axGyPD9kbtAxbC/yvaPuS7Ppd/Ea6ZxGxcHzIv8A4GIkGricswSPsS7Ppd/En2xX2Lg+ZN/8BFvf5w/BPs6l38f0V9tYjYuD5hfo9D8p/nDsp9nUu/j+hdtYjYuH7JYtW4T4zzh2U10dR7+P6Iy6bxK+ng+ZKNV4N8nnDsp9m0e/j+iPbmK2R4PmL9F4N8nnDso7No9/H9B25itkeD5gu1YgG1/njso7No9/H9B25idkeD5kEmr8I2v84fgovo6j38f0Tj03iXqjwfMqv0BAMuE84fgo+wUe8tXTGJeqPB8zftjGxtgY0YNs8IFTj4JVuGioRcVqbKMfKVSUZvS4ogLgtJz7Mt38cXdRCmmUtbEX9KSCkOZ/Ut5vhJVVSeee/wBEasTF5FL7fVlMSbP/AGFdcx2CEp/ITuFiYSfOQIJz6mgcaddEDzXGcwjCrTy5+vFCE1YYQ8358qAJBCNtUADJcAwBryYdaB6SrJTPEeVIEQPJJqDTlqa+Sii85YsxDJGXGrnDy1qk1csU7aESRQAcvk/FNRISm2TWazXnNaBi5waK73EAY5bUOyV2OKc5KC0s6Y6j2zxTfSM/FZvbqG3yN/Y+K7uIUepVsH8JvpG/ij2+ht8gfQ+K7uIQ1Ntvim+kb+KPb6O3yF2Nie7iMdTLb4pvpGfij2+jt8g7GxOxcRjqXbfFN9Iz8Ue30NvkHY2J7uJC/Ua3H+G30jfxSeOo7fImuiMStnE5C0xFri05gkHnBoVeZbOLaLmlW1MWB+Ai9Spo/NvZpxb+D7UVQ3kHldT1q8xX7x9vhIE9xq6RYKQ4j4BufSSqFFZ57/RF2Lbyaf2+rImkfKHkK0GFpk15nJ1oFZgOc0ZOqgLMXfVMgPIi40mAJqnIIuJoMTu2IATpjvJ5kAVHS8ijcmokT5uX7VG5YoEbp6ZFGUSVO+kGM1215wkiTzFoP2H7Ap3KcnWXNFfvENBhwsWefhtUKnwS3P8ABdhv5ob0ezaZtL447zASQ5pNA0gMBq+9eIoLoIrsqFwKEIylaX+eo9jiJyhG8f8ALX5HPWXWKR0UkgLqNE4FQz4R8p73a7GraC62lMSeSp3TwkYzUXryduhL3jnU8dKVOU1qytmlv3b7NhtaN0o5zxA+GUPaxrnvdwVMQQHG485ljsAFkq0Eo5akrXzJX9VqubKOIlKSpyi7pK7dvR67AW/SMvC8HDG9xiLHSU4K65j2uo3juBBwqCN2KdOjDIyptK97adK3IVWvU6zJpxbta+jQ75s7RXktNtuPa2GSrn1a8mz3o4yQSKX6OcOMBXDKuWNihh8pNyWjOvezvho2lTqYnJaUHneZ+7mXHTs8Lmvoy2iaMPDS3F7SHUqCx5Y6t0kZtOSy1afVyyb7PNXNtGr1kMq1tOnudjwXSb6TSGmIkf7RXfTzI8jUV5y3sPTs1TFsHAxGg5iqqb+LezRiIWUPtRnB35qrTNYk2poizT0ocIMf4DfezKqlplv9EX4le7T+31ZSMnKr7mTJCbTlQJ3JBTlUiDuyRoTRFkgfyDqTuRsC56VwSKkkx3qDZfGCRC5/L61G5Yl3AcyRIQAOyvlQDui3Z8BhgrEUTzseo/JQLOXtEOHDwj+bF7bVGo/cluf4LcMn10H3o9h1kB4Ev4MS8Gb9wl9HXQaC6wG9jTA4bdi4eF+O17XzXzeujwznrsZ/HlZN7Z7Z/TT45tZhaOsjbQDE9sL2saXSWhjz4cvCPIHFANHOvXa0bVu0LZVqOk8uLab0Ra1Ky2+F9ecwUaarLIkotJZ5J63d7NTz21ZjR1StBm4WV7mmQmNhDa+AxguuxGTi57xyOCoxsOryYJZs78W/TMjRgJuplTk8+ZeCWZ+OdreW9JxukN11kbK0GoLnsxNM6EYZkKqk1FXVSz3MurRc3aVNSXe0YFl0aHzSO7yZSJ7WtY10bA03GPq4gVeeNvplhVbZ1nGml1jzrS7vW14fkwU8OpVZPql7rzJWWpPx/HcddY5HubV8fBmpwvB2G+o8q5k1FP3Xc69Nya95WPnzS5/XSfTf7RXeWhHkp/G97D0ycYegh9RVVL5t7NOJ+T7UUmSkZH7AfWrk7GNxTLGKkVsv6WOEHQN97MqqWmW/0RqxPw0/t9WUWq4yMnYaZYKSKmr6SQPO9O5GyJQ/DZ1JkLEb383UlckolSaWqg2XwhYgL1G5ZYEvSuSsDfRcdiWIfmiaISLXlUykYORcLFzQrq2iHpYvbaoVPgluf4LsOrVYb1+T3S12YSNulz2iubHFjua8MQF5+E8h3svHOeyqQy1a7W52KTtX7PS6I7oNA66SOEaDW7Ic3gnOuJVqxVW927+m7YUex0bWStttr37S0dHx8I2UCj2tLAWkgFvyXAYOAzAOSr62WQ4ann8S3qYZanbOs3hsJ5WXmltSKgircCK7Qd6gnZ3LJK6sV9H2AQ3qOe8vdfc55BcTda3YAMmjYrKtV1LZkrZsxVSoqnfO227tvgW1UXHzlpg/rpfpv9or0OpHkZfG97/IemjjD9Xh9RVNL5t7NOI+T7UZ9VaZrFkuUymxpaVGEHQN97MqqWmW/wBEaMT8NP7fVlQK8xEjUyLJAVIgC56VxpFaaVQbLowIC5RLLAFyRKw15A7BNQJliIKaKZBEpisOHICxe0If2iDpY/bCjP4Huf4LcP8Ayx3r8nvNpkLWOc1t4taSGjAuIFQ0HlyXnopOSTdj2M5OMW0r9xx1s0lpOZjnMhFmjDS4ucePQCpArjWnzRzrqQo4SnJKUsp+X+8Ti1K+PqxbjHIVr59P+8DR1AtD5LKXSPc88I4Vc4uNKNwqVR0lCMa1oq2Y0dEVJTw95Nt3ek6RYDqCQAkAfOOmfh5fpv8AaK9BqR5KXxPewtNZw/V4fUVTS+beacR8n2oz6q0zFoBWIpZq6Uyg6BvvZlVS0y3+iLsT8NP7fVlJqvMbJAUyIznouCiV5ZVFstjErlygW2BLkDSGqkOw4TBkrAmiDZOclIqGQMcIEXtCfvMHSx+2FGfwvc/wW4f+WO9fk9/XnT2RU0v8BN0cnsFW0f5I71+Sqv8AxS3P8GD3OP3Q9I/1NWzpT+fwRzuhf/W8WdSucdYSAEgD5w018PJ9N/tFeg1I8m/ie9h6azh+rw+oqml829mjEaIfajOVpnLoVhmZp6Vyg6BvvZlXS0y3+iNGJ+Gn9vqymFcYmM5yBpEMkii2WRiVyaqJbawzigaQKQxAIBkgCZAnjapIrkx3JiQkAEECZd0H+8wdLH7YUZ/A9z/BbQ/ljvX5PoBedPZFTS/wE3RyewVbR/kjvX5Kq/8AFLc/wYPc4/dD0j/U1bOlP5/BHO6F/wDW8WdSucdYoSaYha57XEi5QF10ltT8UEDMVHWFcsPNpNayh4mmpNPUWbNamyVukmmdWub6wFXKDjpLITjLQfO+mh+uk+m/2iu9qR5V/HLe/wAhaazh+rw+oqml82804jRD7UZ9FcZS9RWGdmlpTKDoG+9mVVLTLf6I0Yn4af2+rKDnK4ypEL3qLZNIgcVEtSsMUACkSFRAEjWpkGyRrU0RbJqKRWCgY4CACTIlvQf7zB0sfthVz+F7n+C+h/JHevyfQK88exKml/gJujk9gq2j/JHevyVV/wCKW5/gwe5x+6HpH+pq2dKfz+COd0L/AOt4s6lc46xyukhLdlJinYwvYWNaYA0AmMuqA6t4vvmvKF0aWRePvJuzv8Xf3bLHKq9Zky92SV1ZLJ7r69N7m7o5zqEOZKNtZTGa12Dg3Hdt3rHVS0prwv6o30XLOmn429GeAaZH66T6b/aK7mpHlpP33vYWmhjD9Xh9RVNL5t7NWJfwfaihRXGS5dAU0UM0NLnCDoG+9lVVLTLf6I1Yhe7T+31ZluKsuZ0iFxUSxIaiBjUQMVEAExiERbJA1SIkrGJorbCITECQkMcNTEEQgEHo6cRzRyOrRj2ONM6NcCadShJXTW8upSyZKWxo9RHdMsni5/NZ21zOz5/UvPkd3tel9L8uYz+6RYyCDFMQRQgtYQQcwRfTWAqJ3Ul58hPpai1ZxflzI7L3QbDGLsdnlY2taNjjaK76Byc8FVm7ykm/HkRh0nh6atCDS7kuZL/iXZPFT+aztqPZ8/qXnyJ9r0vpflzGk7o1jcKOhmIwwLWbDUfH3hNYCondSXnyIy6WoNWcX5cw/wDEmy+Kn81nbR2dU2rz5B2zR2Py5nlGkHh73uGTnOI30JqunayscNyvJsl0y3GHoIfUVRS+bezZiX8H2oohquMlyy4qZSW9MnCDoG+9mVFLTLf6I2Yj4af2+rMtxVhSgQEBcVEDFRILiDUBclDVIhcNrUyLZLRSK7iokO411AXCDUxXGeEhogcFEsRV0hO6MR3KVfI1mOVHVVFepKCjk62lxNeEpQqueXe0YuWbuH75cHmN1LwYZK5spWgrka1qjrZKThLTa/cHUQlBVIfDlKNtd9O4sB5yJFcBkc7taHca7DsKtyn/ALd/vAoyFpSflt883mMyQ/GIyDsnZDwzjsoQkpvX6+I5U4/KnptpWl6CZrnVwu0vU+NkMHY76qWU+7/afMrcI2s73t3eHkPefvZTi7Hf1eo05ksqe1efiGTT2S17PD9jvarCpMtaYbjF0EXqKopL4t7N2Kfwfaig1qusZGwnFAi3pnKDoG+9mVFLTLf6I24j4af2+rM4BXGa49ECGogdxUSC4cbE0iMpEgapELkzGJpEHIItTI3EWoHcQaiwrjlqBXI3hJk0yO6lYncJ9nDhQ9eFQd4qMDypSgpKzCFZwldFdmi2BxdVxJaWGpHgk12BVLDRTcru7VvA0Sx9RxUbJJPKzX08SyLK3HDAkEjYSAAD1AdSt6tGf2iSS7tevPn5j96DAEk0BbjTwTSrcuQY58qOrWj/AFv94i9oabaS038dv+zdxILKMeU3vLh+AUurRF4iXlb88wu9W7vzj2j1o6qJH2if+8OSGcxSsJSLOl2YxdDF6is9FfFvZuxTzQ+1FEMV1jI2QOUS0vaXGEHQN97MqaWmW/0RrxD92n9vqyhdVxkuINQFx7qAuO1iLCbJQ1SsQuG1idiLZMGpldxXUBca6gLhBqYriLUAmRuakTTEGIsDkTXE7FdxXEWDKHDE7CuOGIsFx7qLCuPdTABzUrEkyzpVuMfQxepZ6Kzy3s3Yt5qf2opBiusY8opXVWaWzR0q3CDoG+9mVVLTLf6I04l+7T+31ZQuq4yXCupiuNdQFyVrE7EHIMNTFckYxNIg2FdTI3FRAXEGosFwg1AriLUBcAtRYlcdjECbJriZEcMQAVxABBiBjFiBCDEDBcxAixpNmMfRRepUUdMt7NuLean9qKlxXmIz7iqsamzQ0o34HoG+8lVVLTPf6I04l+5T+31ZSuK6xkuK6iwrhsYnYTkHdTsQuE1idhNktxMiPcQAgxADhqACuoAa6gBXEAOxiBklxAWHuoAMMQOw91IBrqYWFcSAZzUwJ9IsxZ0UXsqmj829mnFaKf2L1Kt1XGUzbqgWtl/SbcIegb7yVU0dM9/ojVin7lL7fVlK6rzHccNRYTZKGKRFsdrEATMYgB7qAHLUAINQAQagLBFqBjXUAK6gAmsQFggEhj3UAE0IAYoAZADhqAFdQBYtzcWdFF7KqpfNvZpxOin9i9SqArTMZl1IGy9pIfA9C33kqpo/FPf6I14r4KX2+rKgYtFjFcNrECJGhABhqBhBqBjhqAHISAcNQA4agY5YgAS1ADgIANoQMe6gLDoAYBADhqAsPQIAZABAJDsWLa3FvRR+yqqXzb2acSs0PtXqVlcZjKuJ2K7l7SI+C6FvvJVRR+Ke/wBEa8V8FL7fVldrFeYwqIGExiASJQ1IkFRACAQAqIAcBAw6IAYtQFhqBADhAD0QMdIBUQA4QMQQAqIFYeiB2EGouPJZYtoxb0cfsqml829mjE6IfavUqlXGVmeRzKRWXLePguhb7yVU0dM9/ojXifgpfb6srtarjISXUhhNCBjoAcIAK6gdh2tQNIJIYJTEJACqgQTUiSCLUrkrDJizBNalcaiPcSuPJGup3FYVEAEkSFQICyJrZm3o4/ZVVL5t7NGI0Qt9KIFaZjNAVhnNC0Rh4jIfFhG1pDpY2kEPkJBDnA7R1rNGeRKV09Ox7EdCpSdWFNxazRtpS1vayPvb+ZD6eLtKfXR7+D5FPss9sf7R5i72/mQ+ni7SOuj38HyD2We2P9o8wuA/mQ+ni7SOuj38HyH7LPbH+0eYuB+fD6eLtI66PfwfIPZZ7Y/2jzHFnHjIfTxdpHXR7+D5B7LPbH+0eYRiHjIfTxdpLro9/B8h+zT2x/tHmMIP5kPp4u0n10e/g+QvZp7Y/wBo8x+9/wCZD6eLtJddHv4PkP2ae2P9o8x+BHjIfTxdpHXR7+D5B7NPbH+0eYuBHjIfTxdpHXR7+D5B7NPbH+0eYuAHjIfTRdpHXR7+D5B7NLbH+0eY4hHjIfTxdpHXR7+D5DWGltj/AGjzC4IeMi9NF2kuuWx8HyH7PLav7R5iEQ8ZD6aLtI65d/B8g9ne2P8AaPMfgx4yH00XaR1sdj4PkPqJbY/2jzG4MeMh9PF2kdbHY+D5C9nltX9o8xGMeMi9PF2kdau/g+Qezy2r+0eY3BDxkPpou0n10e/g+QvZ5bY/2jzDaweMi9PF2lF1Y9/B8iaoS2x/tHmOWN8bD6eLtI62PfwfIboPav7R5itTmkijmuoxgJa4OFQMRUYFFL5n3sMQleCvoilmzkeCtKMxntad6mZgxXegYQJ2IHdjVO1AXYV9FguPf5UrDuECgdxAoAK8iw7iLt5RYG9pW0hbxEAS0mtcqbOdVVJqGdl1Kk6t0nYoy6wxta15a7jNkd8Wv6twa4EA4GpGBxxG3BVSxUEk2tvkaIYCpKTimszS168+z/bs5Zm02xj7hD73FGAbTjNDsDWmRTlXipZNghhJyhl3Vv8ALYaDjTatBieYdpKBpscFIdxFxRYG2CKpkc4ZJSzE86EDyoBMcuSsNsQKAuOgYJCCNikxisM9h6IAMc6CQ14bECH8iBhBvIkOwTWoGkOUgdxUQFgSExAT2YOpe2Go6iPvUZJSLISlC9tYBsDCA2hwaWjfRxBz8mCr6qNi1V53uttyJ+hoiahpGyjTQAXODFBkKNyUXQg/93WLI4qqs2n/AO3/ACX7quuZckdA9wxciwrhNQSQ4CQ0hXUXHkioEBZDEIE0wgEDsMgBXUXFYqOKsM7Ym5IAEoEOwYoGtJI0pEk7BFxolYbbsJibCIb1FEpAtCbEgQUyK0hhIkEEiaBKZF6RFAahbEaxag2JMnEc5JDegAFMhckSLAQgitIQOKCWsNImMUCYKZE//9k= + input2 (text): default + input3 (text): default + input4 (text): ECサイトEコンポEネントを抽出してください + output1 (json): ecinfo + """ + print("baseimg2ecinfo_rect p(len):", len(p), " img(len):", len(base64img)) + + system_prompt = "あなたEECサイトE開発兼チEイナEで、ECの吁EEージの構造と用途を熟知してぁEす、E + user_prompt = """以下E手頁EECサイトEのWEBペEジ画像を解析します、E +・画像EのECサイトE要素を以下E形式で刁Eして登録してください、E +・吁E素のチEストE斁Eサイズ、文字色、文字E間隔めE置・改行を認識してください +・吁E素の枠のレクタングルの線や色めEE丸さ、影めE景色、gradientを認識してください +・吁E素冁Eある斁Eで表現しにくいアイコンを絵斁Eや記号として認識してください、E +・ここ認識した情報めEdiv>{icon}{text}の形式でHTML,CSSを使って記Eしてください、E +・画像Eに該当E値がなければ[]のように空の配Eを回答し、画像Eに存在しなぁEとは回答しなぁEください。特に黒一色めEE色一色の場合に注意し、すべての値が空になるよぁEしてください、E +・画像Eに写ってぁEイメージ(写真めEラスチEにつぁE、どんなもEが起用されてぁEか、Eロンプトで再現できるチEストとして登録し、オブジェクトE位置を囲ってください、E +・値を抽出した後E、その値が含まれるレクタングルの座標を2点方式で教えてください。OCRの斁EEの座標min(xs), min(ys), max(xs), max(ys)があるEで、それを参老Eレクタングルを合体したり、Eタン刁EEバッファを庁EたりしてもいぁEす。あくまで画像から座標を抽出してください、E +・OCRの抽出冁Eがある場合E以下に記載するEで、それも利用して抽出の正確さを高めてください +・これらE抽出惁Eを総合して、メタの吁EEを記載してください。訴求要素は、情報かOCRがある限りE20斁Eで6種類提案してください。情報がなければ空にしてください、E + +""" + p + + # キーの解決EEdefault" は None にEE + resolved_openai_key = os.environ.get("OPENAI_KEY") if openai_key == "default" else openai_key + resolved_google_api_key = os.environ.get("GEMINI_KEY") if google_api_key == "default" else google_api_key + + client = get_llm_client( + openai_key=resolved_openai_key, + google_api_key=resolved_google_api_key, + ) + response = client.call( + prompt=user_prompt, + schema=pageInfo, + system_prompt=system_prompt, + images=[base64img], + model=model, + temperature=0, + ) + + return response diff --git a/apis/baseimg2fvinfo.py b/apis/baseimg2fvinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..4836bd75993620600b4bf34013319c75247451ab --- /dev/null +++ b/apis/baseimg2fvinfo.py @@ -0,0 +1,156 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from enum import Enum + +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class Meta(BaseModel): + 会社吁E str + 業畁E str + ブランチE str + サービス: str + 啁E��: str + タイトル: str + 訴求テーチE list[str] + +class Font(str, Enum): + font1 = "ゴシチE��" + font2 = "明朝" + font3 = "手書ぁE + +class EvsF(str, Enum): + EMOTIONAL = "惁E��E + FUNCTIONAL = "機�E" + +class PvsS(str, Enum): + PROBLEM = "問題提起" + SOLUTION = "課題解決" + +class Copy(BaseModel): + text: str + font: Font + color: str + visual: str + appeal_mode : EvsF + forcus_stage : PvsS + +class CatchCopy(BaseModel): + main_copy: list[Copy] + sub_copy: list[Copy] + +class FvInfo(BaseModel): + 非LP: bool + メタ: Meta + キャチE��コピ�E: CatchCopy + 権威付け: list[str] + ビジュアル: list[str] + CTAボタン: list[str] + +def ask_raw(messages, model): + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=FvInfo, + temperature=0 + ) + return response + +@customtracer +def baseimg2fvinfo(base64img, openai_key=os.environ.get('OPENAI_KEY'), p=""): + """ + input1 (text): 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 + input2 (text): default + input3 (text): + output1 (json): fvinfo + """ + + print("baseimg2fvinfo openai_key:",openai_key[-4:]) + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages = [ + { + "role": "system", + "content": "あなた�E優れたWEBマ�Eケターで、ランチE��ングペ�Eジの要素を見�Eけることに長けてぁE��す。また�EーケチE��ングの達人なので訴求テーマを言語化するのが上手です、E + }, + { + "role": "user", + "content":[ + {"type": "text", "text":"""LPのファーストビューの画像を解析します、E +・何も書かれてぁE��ぁE��像�E場合�E、空の値を返し、E��LP=Trueとしてください、E +・CTAボタンが存在する場合、�Eタン冁E�E記載�E容を�E列で教えて下さぁE��アンカーリンクのあるチE��ストもCTAとしてください、E +・画像�Eに書かれてぁE��斁E��・コピ�Eを読み取り、LPに掲載されてぁE��頁E��に並べてください。大きい目立つ斁E��で書かれてぁE��冁E��を「main_copy」とぁE��キー、それ以外を「sub_copy」とぁE��キーで、読み取ったテキストをtext、それぞれ�E斁E��がどんなフォントで書かれてぁE��のか、「ゴシチE��体」や「�E朝体」などのフォント情報をfontをキーとしてできるだけ正確に付与してください。フォント�E色、テキスト周辺で関連するアイコンめE��ジュアルも抽出してください、E +・画像�Eに写ってぁE��イメージ(写真めE��ラスチEにつぁE��、どんなも�Eが起用されてぁE��か教えて下さぁE��E +・画像�Eに該当�E値がなければ[]のように空の配�Eを回答し、画像になぁE��とは回答しなぁE��ください。特に黒一色めE�E色一色の場合に注意し、E��LP=Trueを返してください、E +・これら�E抽出惁E��を総合して、メタの吁E��E��を記載してください。訴求要素は、情報かOCRがある限り�E20斁E��で6種類提案してください。情報がなければ空にしてください、E +""" + p} + ] + }, + ] + + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}) + r = ask_raw(messages, "meta-llama/Llama-3.3-70B-Instruct") + + return r diff --git a/apis/baseimg2fvinfo_rect.py b/apis/baseimg2fvinfo_rect.py new file mode 100644 index 0000000000000000000000000000000000000000..f8d20319317c0f32cb4fddaeb736e36aaf783833 --- /dev/null +++ b/apis/baseimg2fvinfo_rect.py @@ -0,0 +1,197 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from enum import Enum +client = LLMClient() + +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class Category(str, Enum): + ビジネス = "ビジネス�E�EaaS・法人支援�E�E + ヘルスケア = "ヘルスケア�E�美容・健康�E�E + ヒューマンリソース = "ヒューマンリソース�E�求人・紹介!E + コマ�Eス = "コマ�Eス�E�趣味・食品・衣類!E + ファイナンス = "ファイナンス�E���融�E保険・不動産�E�E + インフラ = "インフラ�E�電気�E通信・ガス・住屁E��E + ライフイベンチE= "ライフイベント(教育・結婚�E相諁E��E + +class CategoryMiddle(str, Enum): + # ビジネス + ITソフトウェア = "IT・ソフトウェア" + マ�Eケ支援コンサル = "マ�Eケ支援・コンサル" + オフィス機器用品E= "オフィス・機器用品E + + # ヘルスケア + 健康食品器具 = "健康食品・器具" + 美容医療クリニック = "美容・医療クリニック" + 美容コスメ = "美容コスメ" + フィチE��ネスジム = "フィチE��ネスジム" + + # ヒューマンリソース + 求人惁E�� = "求人惁E��" + 人材紹仁E= "人材紹仁E + 人材派遣 = "人材派遣" + + # コマ�Eス + 動画アニメゲーム = "動画・アニメ・ゲーム" + リユースリサイクル = "リユース・リサイクル" + 旁E���EチE��レジャー = "旁E���Eホテル・レジャー" + 趣味交隁E= "趣味・交隁E + 新聞雑誌メチE��ア = "新聞�E雑誌�E惁E��メチE��ア" + 自動車レンタカー用品E= "自動車�Eレンタカー・用品E + 飲料食品生活用品E= "飲料食品・生活用品E + 家電パソコン = "家電・パソコン" + ファチE��ョン = "ファチE��ョン" + + # ファイナンス + 不動産 = "不動産" + 保険 = "保険" + ローン = "ローン" + クレカ電子決渁E= "クレカ・電子決渁E + 証券FX先物 = "証券・FX・先物" + 銀衁E= "銀衁E + + # インフラ + ネット通信サービス = "ネット�E通信サービス" + 電気ガス = "電気�Eガス" + 住宁E��備リフォーム = "住宁E��備�Eリフォーム" + + # ライフイベンチE + 士業相諁E= "士業・相諁E + 学習スクール = "学習�Eスクール" + 結婚�E会い = "結婚�E出会い" + 葬儀墓地 = "葬儀・墓地" + 引越し介護 = "引越し・介護" + +class Meta(BaseModel): + 会社吁E str + 業畁E Category + 中刁E��E CategoryMiddle + サービス: str + 啁E��: str + タイトル: str + 訴求テーチE list[str] + +class cood(BaseModel): + x: int + y: int + +class str_with_rect(BaseModel): + text: str + html: str + rect: list[cood] + +class FvInfo(BaseModel): + 非LP: bool + メタ: Meta + メインコピ�E: list[str_with_rect] + サブコピ�E: list[str_with_rect] + 権威付け: list[str_with_rect] + ビジュアル: list[str_with_rect] + CTAボタン: list[str_with_rect] + +def ask_raw(messages, model): + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=FvInfo, + temperature=0 + ) + return response + +@customtracer +def baseimg2fvinfo_rect(base64img, openai_key=os.environ.get('OPENAI_KEY'), p=""): + """ + input1 (text): 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 + input2 (text): default + input3 (text): + output1 (json): fvinfo + """ + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + messages = [ + { + "role": "system", + "content": "あなた�E優れたWEBマ�Eケターで、ランチE��ングペ�Eジの要素を見�Eけることに長けてぁE��す。また�EーケチE��ングの達人なので訴求テーマを言語化するのが上手です、E + }, + { + "role": "user", + "content":[ + {"type": "text", "text":"""LPのファーストビューの画像を解析します、E +何も書かれてぁE��ぁE��像�E場合�E、空の値を返し、E��LP=Trueとして終亁E��てください。何か書かれてぁE��ば以下�E優先頁E��斁E��を抽出してください。タイトルめE��ゴはメタ惁E��に刁E��し、権威付けめE��ピ�Eと区別してください、E +・CTAボタンが存在する場合、�Eタン冁E�E記載�E容で教えて下さぁE��ETAを�E現するHTMLを文字サイズ、文字色、文字�E間隔めE��置・改行、構�EするレクタングルめE���E丸さ、影めE�Eタン背景の色、gradientのあるなし、サブコピ�Eが�Eタン外かボタン冁E��、�Eタン冁E�E矢印等�E絵斁E��に気を付けて生�Eしてください、E +・画像�Eに権威付けのバッジがあれ�E最優先で権威付けに刁E��してください。権威付けのバッジを�E現するHTMLをCTAと同様�E頁E��で気を付けて抽出してください。誤ってロゴを�E類しなぁE��ください、E +・キャチE��コピ�EをLPに掲載されてぁE��頁E��に並べてください。大きい目立つ斁E��で書かれてぁE��冁E��をメインコピ�E、それ以外をサブコピ�Eに刁E��して、色めE��景、フォント�E種類などをHTMLで再現してください。また、Eつの斁E��を作るコピ�Eは刁E��ずにまとめて抽出してください、E +・画像�Eに写ってぁE��イメージ(写真めE��ラスチEにつぁE��、どんなも�Eが起用されてぁE��か、�Eロンプトで再現できるチE��ストとしてビジュアルに列挙してください、E +値を抽出した後�E、その値が含まれるレクタングルの座標を2点方式で教えてください。OCRの斁E���Eの座標min(xs), min(ys), max(xs), max(ys)がある�Eで、それを参老E��レクタングルを合体したり、�Eタン刁E�Eバッファを庁E��たりしてもいぁE��す。あくまで画像から座標を抽出してください、E +画像�Eに該当�E値がなければ[]のように空の配�Eを回答し、画像になぁE��とは回答しなぁE��ください。特に黒一色めE�E色一色の場合に注意し、E��LP=Trueを返してください、E +これら�E抽出惁E��を総合して、メタの吁E��E��を記載してください。訴求要素は、情報かOCRがある限り�E20斁E��で6種類提案してください。情報がなければ空にしてください、E +""" + p} + ] + }, + ] + + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}) + r = ask_raw(messages, "meta-llama/Llama-3.3-70B-Instruct") + + return r \ No newline at end of file diff --git a/apis/baseimg2fvinfo_with_design.py b/apis/baseimg2fvinfo_with_design.py new file mode 100644 index 0000000000000000000000000000000000000000..3632daf8d81ac1145d6175e5b32c225e816be39d --- /dev/null +++ b/apis/baseimg2fvinfo_with_design.py @@ -0,0 +1,180 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from typing import Dict +from enum import Enum + +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class Meta(BaseModel): + 会社吁E str + 業畁E str + ブランチE str + サービス: str + 啁E��: str + タイトル: str + 訴求テーチE list[str] + +class Design(BaseModel): + 重要なフレーズの斁E��色を赤めE��レンジめE��ンクめE��E��などFV上で目立つ色に着色: float + 背景を画像�E主要な配色と変えて目立たせる: float + 四角や丸など図形で囲ぁE��認性を上げめE float + アイコンを使用して視認性を上げめE float + チE��スト�E重要なフレーズの下に水平なアクセント線が引かれてぁE��: float + +class sCopy(BaseModel): + text: str + design: Design + +class EvsF(str, Enum): + EMOTIONAL = "惁E��E + FUNCTIONAL = "機�E" + +class EFitems(BaseModel): + item: str + judge: EvsF + +class PvsS(str, Enum): + PROBLEM = "問題提起" + SOLUTION = "課題解決" + +class PSitems(BaseModel): + item: str + judge: PvsS + +class mCopy(BaseModel): + text: str + appeal_mode : list[EFitems] + forcus_stage : list[PSitems] + +class CatchCopy(BaseModel): + main_copy: list[mCopy] + sub_copy: list[sCopy] + +class FvInfo(BaseModel): + 非LP: bool + メタ: Meta + キャチE��コピ�E: CatchCopy + 権威付け: list[str] + ビジュアル: list[str] + CTAボタン: list[str] + +def ask_raw(messages, model): + client = LLMClient() + + # パラメータの準備 + params = { + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0, + "response_format": FvInfo, + } + + # gpt-5系はtemperatureを渡さなぁE��環墁E��よって0が弾かれるためE��E + model_lower = (model or "").lower() + if not model_lower.startswith("gpt-5"): + params["temperature"] = 0 + + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + **params + ) + return response + +@customtracer +def baseimg2fvinfo_with_design(base64img, openai_key=os.environ.get('OPENAI_KEY'), p="", model="meta-llama/Llama-3.3-70B-Instruct"): + """ + input1 (text): + input2 (text): default + input3 (text): + input4 (text): gpt-4o + output1 (json): fvinfo + """ + + print(f"baseimg2fvinfo_with_design {model} openai_key:",openai_key[-4:]) + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages = [ + { + "role": "system", + "content": "あなた�E優れたWEBマ�Eケターで、ランチE��ングペ�Eジの要素を見�Eけることに長けてぁE��す。また�EーケチE��ングの達人なので訴求テーマを言語化するのが上手です、E + }, + { + "role": "user", + "content":[ + {"type": "text", "text":"""LPのファーストビューの画像を解析します、E +・何も書かれてぁE��ぁE��像�E場合�E、空の値を返し、E��LP=Trueとしてください、E +・CTAボタンが存在する場合、�Eタン冁E�E記載�E容を�E列で教えて下さぁE��アンカーリンクのあるチE��ストもCTAとしてください、E +・画像�Eに書かれてぁE��斁E��・コピ�Eを読み取り、LPに掲載されてぁE��頁E��に並べてください。大きい目立つ斁E��で書かれてぁE��冁E��を「main_copy」とぁE��キーで1つ抽出し、情緒�E機�Eのどちらに訴えてぁE��かなどを記載、E +・main_copy以外を「sub_copy」とぁE��キーで、読み取ったテキストをtext、それぞれ�Eサブコピ�Eの裁E��タイプ�E適用度合いをdesignに0~1のfloatで記述 +・画像�Eに写ってぁE��イメージ(写真めE��ラスチEにつぁE��、どんなも�Eが起用されてぁE��か教えて下さぁE��E +・画像�Eに該当�E値がなければ[]のように空の配�Eを回答し、画像になぁE��とは回答しなぁE��ください。特に黒一色めE�E色一色の場合に注意し、E��LP=Trueを返してください、E +・これら�E抽出惁E��を総合して、メタの吁E��E��を記載してください。訴求要素は、情報かOCRがある限り�E20斁E��で6種類提案してください。情報がなければ空にしてください、E +""" + p} + ] + }, + ] + + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}) + r = ask_raw(messages, model) + + return r \ No newline at end of file diff --git a/apis/baseimg2html.py b/apis/baseimg2html.py new file mode 100644 index 0000000000000000000000000000000000000000..a202674ef84bb78abe46987e86bbf23265c0cc0b --- /dev/null +++ b/apis/baseimg2html.py @@ -0,0 +1,54 @@ +""" +baseimg2html: 画像からHTMLコンポーネントを生成。 +HF版: VLM (Qwen2.5-VL) を使用。Vertex AI は使用しない。 +""" + +import os +from typing import List + +from pydantic import BaseModel + +from src.utils.tracer import customtracer + + +class newHTMLs(BaseModel): + HTMLs: List[str] + + +@customtracer +def baseimg2html(p: str, base64img: str, gcp_key: str = "default") -> dict: + """ + input1 (text): OCRテキストやページの説明 + input2 (text): base64エンコードされた画像 + input3 (text): default + output1 (json): HTMLコンポーネントのリスト({"HTMLs": [...]}) + + NOTE: HF版は VLM ベース。Vertex AI / GCP は使用しない。 + """ + from src.clients.llm_client import LLMClient + + client = LLMClient() + + system_prompt = ( + "あなたはHTMLとCSSの達人です。" + "画像とテキスト情報を基に、適切なHTMLコンポーネントを生成してください。" + "各コンポーネントは完全に機能するHTMLとして生成してください。" + ) + + prompt = ( + system_prompt + "\n\n[テキスト情報]\n" + str(p) + + "\n\n画像のコンポーネントをHTMLリストとして出力してください。" + ) + + # Strip base64 data-URI prefix if present + if base64img and "," in base64img: + base64img = base64img.split(",", 1)[1] + + result = client.call( + prompt=prompt, + schema=newHTMLs, + model="Qwen/Qwen2.5-VL-72B-Instruct", + images=[base64img] if base64img else None, + temperature=0.1, + ) + return result.model_dump() diff --git a/apis/baseimg2ocr.py b/apis/baseimg2ocr.py new file mode 100644 index 0000000000000000000000000000000000000000..594701c16f94487c92849392d9c3db10bb2a1fb3 --- /dev/null +++ b/apis/baseimg2ocr.py @@ -0,0 +1,56 @@ +""" +baseimg2ocr: base64画像をOCRしてテキストを抽出。 +HF版: VLM (Qwen2.5-VL) を使用。Google Vision API は使用しない。 + +NOTE: 元の実装は Google Vision API を使用。精度が異なる場合がある。 +""" + +import json +import os + +from src.utils.tracer import customtracer + + +def _vlm_ocr(base64image: str, model: str = "Qwen/Qwen2.5-VL-7B-Instruct") -> str: + """VLM でテキスト抽出(url2ocr 互換フォーマット)。""" + from src.clients.llm_client import LLMClient + from pydantic import BaseModel + from typing import List + + class OcrEntry(BaseModel): + text: str + y: int + size: int + + class OcrResult(BaseModel): + items: List[OcrEntry] + + client = LLMClient() + result = client.call( + prompt=( + "Extract all visible text from this image. " + "For each text block, estimate its vertical position (y coordinate, 0=top) " + "and approximate font size in pixels. " + "Return results sorted by y position." + ), + schema=OcrResult, + model=model, + images=[base64image], + temperature=0, + ) + return json.dumps( + [{"text": e.text, "y": e.y, "size": e.size, "rect": []} for e in result.items], + ensure_ascii=False, + ) + + +@customtracer +def baseimg2ocr(base64image: str, margin: int = 120) -> str: + """ + input1 (text): base64エンコードされた画像 + input2 (text): 120 + output1 (json): OCR結果 + + NOTE: HF版は VLM ベースOCR。Google Vision API は使用しない。 + """ + return _vlm_ocr(base64image) diff --git a/apis/baseimg2pagetype.py b/apis/baseimg2pagetype.py new file mode 100644 index 0000000000000000000000000000000000000000..279fe2b8067c8d6653bfeceaaf836107490a095d --- /dev/null +++ b/apis/baseimg2pagetype.py @@ -0,0 +1,222 @@ +import os +from src.clients.llm_client import LLMClient +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + + +class PageKind(str, Enum): + """ペ�Eジ種別""" + LP = "LP" # ランチE��ングペ�Eジ�E�単一訴求�E問い合わぁE賁E��請求など�E�E + EC = "EC" # ECサイト(商品�Eサービスを比輁E�E選択できるサイト。購入だけでなく、賁E��請求�E見学予紁E��ども含む。カチE��リ一覧、商品詳細、比輁E���Eなどの構造を持つ�E�E + CORPORATE = "CORPORATE" # コーポレートサイト(企業惁E��・採用・会社案�Eなど�E�E + MEDIA = "MEDIA" # メチE��ア/ブログ�E�記事コンチE��チE��忁E��E + FORM = "FORM" # フォーム/申し込みペ�Eジ�E�問ぁE��わせ・予紁E�E申し込みフォーム中忁E��E + OTHER = "OTHER" # そ�E他(上記に当てはまらなぁE�Eージ�E�E + + +class EcPageType(str, Enum): + """ECペ�Eジ種別�E�ECの場合�Eみ有効�E�E"" + TOP_OR_SPECIAL = "EC_TOP_OR_SPECIAL" # ECのTOPめE��雁E�Eージ + CATEGORY_LIST = "EC_CATEGORY_LIST" # カチE��リ一覧ペ�Eジ + PRODUCT_DETAIL = "EC_PRODUCT_DETAIL" # 啁E��詳細ペ�Eジ + CART_OR_CHECKOUT = "EC_CART_OR_CHECKOUT" # カート�EチェチE��アウト�Eージ + OTHER_EC = "EC_OTHER" # そ�E他�EEC関連ペ�Eジ + NOT_EC = "NOT_EC" # ECではなぁE��吁E + + +class PageScore(BaseModel): + """ペ�Eジ種別のスコア""" + kind: PageKind + score: float = Field(ge=0.0, le=1.0, description="0.0、E.0のスコア") + + +class PageClassification(BaseModel): + """ペ�Eジ刁E��結果""" + scores: List[PageScore] # 吁E�Eージ種別のスコア�E�合計�E1.0になる忁E���EなぁE��E + best_kind: PageKind # 最も高いスコアのペ�Eジ種別 + is_ec: bool # ECサイトかどぁE���E�Eest_kind == EC の場吁ETrue�E�E + is_lp: bool # LPかどぁE���E�Eest_kind == LP の場吁ETrue�E�E + ec_page_type: EcPageType # ECの場合�Eペ�Eジ種別�E�非ECの場合�E NOT_EC�E�E + reason: str # 判定理由�E�日本語で簡潔に�E�E + + +def _build_prompt(p: str = "") -> str: + """プロンプトを構築(最小限の持E���E�E"" + page_kinds = "\n".join([f"- {kind.value}: {kind.name}" for kind in PageKind]) + ec_page_types = "\n".join([f"- {pt.value}: {pt.name}" for pt in EcPageType if pt != EcPageType.NOT_EC]) + + base_instruction = f"""画像から�Eージ種別を判定してください、E +【�Eージ種別候補!EageKind�E�、E +{page_kinds} +【ECペ�Eジ種別候補!EcPageType、ECの場合�Eみ�E�、E +{ec_page_types} +【EC判定�E重要基準、E +以下�E構造皁E��徴がある場合�E、ECとして高スコアを付与してください�E�E +- カチE��リ一覧ペ�Eジ�E�褁E��の啁E��・サービスが一覧表示されてぁE���E�E +- 啁E��・サービスの詳細ペ�EジへのリンクがあめE +- 褁E��の選択肢を比輁E��きる機�EがあめE +- フィルター・ソート機�EがあめE +- 価格・料��などの惁E��が表示されてぁE�� +- 最終的なゴールが「購入」だけでなく「賁E��請求」「見学予紁E��「問ぁE��わせ」でも構いません +- 例:霊園検索サイト、不動産検索サイト、求人サイト、比輁E��イトなど +【LP判定�E基準、E +- 単一の啁E��・サービス・惁E��に焦点を当ててぁE�� +- 明確なCTA�E�問ぁE��わせ・賁E��請求�Eタンなど�E�が1つまた�E少数 +- 比輁E���EめE��チE��リ一覧がなぁE +【判定手頁E��E +1. 各PageKindにつぁE��0.0、E.0でスコアを付与!Ecores�E�E +2. 最も高いスコアのPageKindをbest_kindに設宁E +3. best_kind == "EC" の場吁E + - is_ec = True + - ec_page_type めEEC_TOP_OR_SPECIAL / EC_CATEGORY_LIST / EC_PRODUCT_DETAIL / EC_CART_OR_CHECKOUT / EC_OTHER から選抁E +4. best_kind == "LP" の場吁E + - is_lp = True + - ec_page_type = "NOT_EC" +5. reason に判定根拠を日本語で簡潔に記述 +【補足惁E��、E +{p if p else "(補足惁E��なぁE"} +""" + return base_instruction.strip() + + +def ask_raw(base64img: str, openai_key: str, p: str = "") -> PageClassification: + """OpenAIに問い合わせてペ�Eジ刁E��結果を取征E"" + prompt = _build_prompt(p=p) + + # 毎回新しいクライアントを作�Eし、openai_keyを反映 + client = LLMClient() + + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=[ + { + "role": "system", + "content": "あなた�E優れたWEBアナリストで、Webペ�Eジの種別を正確に判定する専門家です。画像から�Eージの種類を判断し、各候補に0.0、E.0のスコアを付与してください。特に、ECサイト�E判定では、購入機�Eの有無よりも、カチE��リ一覧・啁E��比輁E�E褁E��選択肢などの構造皁E��徴を重視してください。賁E��請求や見学予紁E��ゴールでも、構造皁E��EC皁E��サイト�EECとして判定してください、E + }, + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}}, + {"type": "text", "text": prompt} + ] + }, + ], + response_format=PageClassification, + temperature=0, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + ) + + classification: PageClassification = response + + # best_kindからis_ec, is_lpを設定(念のため�E�E + if classification.best_kind == PageKind.EC: + classification.is_ec = True + if classification.ec_page_type == EcPageType.NOT_EC: + # ECと判定されたのにNOT_ECになってぁE��場合�E、デフォルトでOTHER_ECに設宁E + classification.ec_page_type = EcPageType.OTHER_EC + elif classification.best_kind == PageKind.LP: + classification.is_lp = True + if classification.ec_page_type != EcPageType.NOT_EC: + classification.ec_page_type = EcPageType.NOT_EC + else: + # ECでめEPでもなぁE��吁E + classification.is_ec = False + classification.is_lp = False + if classification.ec_page_type != EcPageType.NOT_EC: + classification.ec_page_type = EcPageType.NOT_EC + + return classification + + +@customtracer +def baseimg2pagetype(base64img, openai_key = "default", p = ""): + """ + input1 (text): base64img - base64エンコードされた画僁E + input2 (text): default + input3 (text): 補足チE��スト(任意!E + output1 (json): ペ�EジタイチE + output2 (text): human_readable_summary + """ + # openai_keyの処琁E + if openai_key == "default" or not openai_key: + openai_key = os.environ.get('OPENAI_KEY', '') + + print(f"baseimg2pagetype openai_key:",openai_key[-4:]) + + classification = ask_raw(base64img=base64img, openai_key=openai_key, p=p) + + result_dict = classification.model_dump() + + # 人間向けサマリ + summary_lines = [ + f"best_kind: {classification.best_kind.value}", + f"is_ec: {classification.is_ec}", + f"is_lp: {classification.is_lp}", + f"ec_page_type: {classification.ec_page_type.value}", + "", + "scores:", + ] + # スコアの高い頁E��ソーチE + sorted_scores = sorted(classification.scores, key=lambda s: s.score, reverse=True) + for score in sorted_scores: + summary_lines.append(f" - {score.kind.value}: {score.score:.3f}") + + summary_lines.extend(["", f"reason: {classification.reason}"]) + summary_text = "\n".join(summary_lines) + + return result_dict, summary_text diff --git a/apis/baseimg2score.py b/apis/baseimg2score.py new file mode 100644 index 0000000000000000000000000000000000000000..68991c515bb2a1eaabdef4e52a3dd46c086f5a1c --- /dev/null +++ b/apis/baseimg2score.py @@ -0,0 +1,92 @@ +import os +import json +import logging +from pydantic import BaseModel +from datetime import datetime +import pytz + +from src.clients.llm_client import LLMClient +from src.utils.tracer import customtracer + +logger = logging.getLogger("baseimg2score") +if not logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(handler) +logger.setLevel(logging.INFO) + + +class Answer(BaseModel): + citation: str + suggestion: str + score: int + + +@customtracer +def baseimg2score( + p, + openai_key, + image64=None, + model="meta-llama/Llama-3.3-70B-Instruct", + gemini_key=None, +): + """ + input1 (text): 13: ※金融犯罪にご注愁E手口はこちら、E + input2 (text): default + input3 (text): + input4 (text): gpt-4o + input5 (text): default + output1 (json): 頁E + """ + selected_model = model if model else "meta-llama/Llama-3.3-70B-Instruct" + + #1. webUIからの呼び出しE場合キーをEれなくても実行可能 + #2. APIからの呼び出しE場合キーをEれなくてはならなぁE + if selected_model and "gemini" in selected_model.lower(): + if gemini_key and gemini_key != "default": + api_key = gemini_key + else: + api_key = os.environ.get('GEMINI_KEY') + client = LLMClient(google_api_key=api_key) + else: + if openai_key and openai_key != "default": + api_key = openai_key + else: + api_key = os.environ.get('OPENAI_KEY') + client = LLMClient(openai_key=api_key) + + system_prompt = """与えられた情報と質問に対して、採点基準を参Eして以下を回答します、E +citation:当該箁Eの引用 +suggestion:満点でなぁE合E満点になるよぁE具体的な持E。満点の場合E優れた点をE体的な叙述 +""" + + images = [image64] if image64 else None + img_flag = image64[-4:] if image64 else "none" + + dt = datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%m/%d %H:%M") + + try: + result = client.call( + prompt=p, + schema=Answer, + model=selected_model, + system_prompt=system_prompt, + images=images, + temperature=0, + ) + + combined = ( + f"[baseimg2score] (img:{img_flag}) " + f"({dt}) mdl:{selected_model}" + ) + logger.info(combined) + + return result.model_dump() + + except Exception as e: + err_msg = ( + f"[baseimg2score] (img:{img_flag}) " + f"({dt}) ERROR: {type(e).__name__}: {str(e)}" + ) + logger.error(err_msg) + raise diff --git a/apis/ecinfo2winningrate.py b/apis/ecinfo2winningrate.py new file mode 100644 index 0000000000000000000000000000000000000000..330281d1d724dac02c8d30ea8d3ef7c97738c34e --- /dev/null +++ b/apis/ecinfo2winningrate.py @@ -0,0 +1,233 @@ +import os +from src.clients.llm_client import LLMClient +import json +from pydantic import BaseModel, conint +from enum import Enum +from typing import Any, Dict, List +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class reason(BaseModel): + choice: str + content_description: str + contribution: int + reason: str + recommend: str + +class win_or_lose(str, Enum): + win = "勝ち" + lose = "負ぁE + +class testpattern_win_or_lose(BaseModel): + testpattern: win_or_lose + possibility: int + reasons: list[reason] + +def extract_text_from_items(items: List[Dict[str, Any]]) -> str: + """リスト�EのアイチE��からtextを抽出して結合""" + if not items: + return "なぁE + texts = [] + for item in items: + if isinstance(item, dict) and "text" in item: + texts.append(item["text"]) + return ", ".join(texts) if texts else "なぁE + +def ecinfo_to_text(ecinfo_data: Dict[str, Any]) -> str: + """ + ecinfoチE�Eタをテキスト形式に変換 + FV版�E形式に類似: 「商品名: xxx, 価格: xxx, カート�Eタン: xxx、E + すべてのコンポ�Eネントを含め、空の場合�E「なし」と明示 + """ + parts = [] + + # メタ惁E�� + meta = ecinfo_data.get("メタ", {}) + if meta: + parts.append(f"会社吁E {meta.get('会社吁E, 'なぁE)}") + parts.append(f"業畁E {meta.get('業畁E, 'なぁE)}") + parts.append(f"中刁E��E {meta.get('中刁E��E, 'なぁE)}") + parts.append(f"サービス: {meta.get('サービス', 'なぁE)}") + parts.append(f"啁E��: {meta.get('啁E��', 'なぁE)}") + parts.append(f"タイトル: {meta.get('タイトル', 'なぁE)}") + if meta.get("訴求テーチE): + themes = ", ".join(meta["訴求テーチE]) if isinstance(meta["訴求テーチE], list) else meta["訴求テーチE] + parts.append(f"訴求テーチE {themes}") + else: + parts.append("訴求テーチE なぁE) + + # ペ�Eジ共送E + parts.append(f"ロゴ: {extract_text_from_items(ecinfo_data.get('ロゴ', []))}") + parts.append(f"グローバル検索バ�E: {extract_text_from_items(ecinfo_data.get('グローバル検索バ�E', []))}") + parts.append(f"ハンバ�Eガーメニューアイコン: {extract_text_from_items(ecinfo_data.get('ハンバ�Eガーメニューアイコン', []))}") + parts.append(f"カートアイコン: {extract_text_from_items(ecinfo_data.get('カートアイコン', []))}") + parts.append(f"ユーザーメニュー: {extract_text_from_items(ecinfo_data.get('ユーザーメニュー', []))}") + + # ナビゲーション + parts.append(f"ブレチE��クラム: {extract_text_from_items(ecinfo_data.get('ブレチE��クラム', []))}") + parts.append(f"ペ�Eジネ�Eション: {extract_text_from_items(ecinfo_data.get('ペ�Eジネ�Eション', []))}") + parts.append(f"タブ�E替: {extract_text_from_items(ecinfo_data.get('タブ�E替', []))}") + + # トップ�Eージ + parts.append(f"メインビジュアル: {extract_text_from_items(ecinfo_data.get('メインビジュアル', []))}") + parts.append(f"プロモーションバナー: {extract_text_from_items(ecinfo_data.get('プロモーションバナー', []))}") + parts.append(f"カチE��リカーチE {extract_text_from_items(ecinfo_data.get('カチE��リカーチE, []))}") + + # 啁E��一覧ペ�Eジ + parts.append(f"啁E��一覧: {extract_text_from_items(ecinfo_data.get('啁E��一覧', []))}") + parts.append(f"フィルタ: {extract_text_from_items(ecinfo_data.get('フィルタ', []))}") + parts.append(f"ソーチE {extract_text_from_items(ecinfo_data.get('ソーチE, []))}") + parts.append(f"ペ�Eジャー: {extract_text_from_items(ecinfo_data.get('ペ�Eジャー', []))}") + parts.append(f"クイチE��ビューアイコン: {extract_text_from_items(ecinfo_data.get('クイチE��ビューアイコン', []))}") + + # 啁E��詳細ペ�Eジ + parts.append(f"啁E��吁E {extract_text_from_items(ecinfo_data.get('啁E��吁E, []))}") + parts.append(f"価格: {extract_text_from_items(ecinfo_data.get('価格', []))}") + parts.append(f"ブランチE {extract_text_from_items(ecinfo_data.get('ブランチE, []))}") + parts.append(f"サムネイル: {extract_text_from_items(ecinfo_data.get('サムネイル', []))}") + parts.append(f"画像ギャラリー: {extract_text_from_items(ecinfo_data.get('画像ギャラリー', []))}") + parts.append(f"カラースウォチE��: {extract_text_from_items(ecinfo_data.get('カラースウォチE��', []))}") + parts.append(f"サイズセレクタ: {extract_text_from_items(ecinfo_data.get('サイズセレクタ', []))}") + parts.append(f"在庫スチE�Eタス: {extract_text_from_items(ecinfo_data.get('在庫スチE�Eタス', []))}") + parts.append(f"配送情報: {extract_text_from_items(ecinfo_data.get('配送情報', []))}") + parts.append(f"ボタン_カート追加: {extract_text_from_items(ecinfo_data.get('ボタン_カート追加', []))}") + parts.append(f"ボタン_今すぐ購入: {extract_text_from_items(ecinfo_data.get('ボタン_今すぐ購入', []))}") + parts.append(f"レビューサマリー: {extract_text_from_items(ecinfo_data.get('レビューサマリー', []))}") + parts.append(f"レビューボタン: {extract_text_from_items(ecinfo_data.get('レビューボタン', []))}") + parts.append(f"QnAリンク: {extract_text_from_items(ecinfo_data.get('QnAリンク', []))}") + parts.append(f"バッジタグ: {extract_text_from_items(ecinfo_data.get('バッジタグ', []))}") + parts.append(f"関連啁E��カルーセル: {extract_text_from_items(ecinfo_data.get('関連啁E��カルーセル', []))}") + + # カート�Eージ + parts.append(f"カート商品リスチE {extract_text_from_items(ecinfo_data.get('カート商品リスチE, []))}") + parts.append(f"数量セレクタ: {extract_text_from_items(ecinfo_data.get('数量セレクタ', []))}") + parts.append(f"削除アイコン: {extract_text_from_items(ecinfo_data.get('削除アイコン', []))}") + parts.append(f"クーポン入劁E {extract_text_from_items(ecinfo_data.get('クーポン入劁E, []))}") + parts.append(f"注斁E��計サマリー: {extract_text_from_items(ecinfo_data.get('注斁E��計サマリー', []))}") + parts.append(f"チェチE��アウト�Eタン: {extract_text_from_items(ecinfo_data.get('チェチE��アウト�Eタン', []))}") + + # 共通下部 + parts.append(f"フッターリンク: {extract_text_from_items(ecinfo_data.get('フッターリンク', []))}") + parts.append(f"SNSアイコン: {extract_text_from_items(ecinfo_data.get('SNSアイコン', []))}") + parts.append(f"カスタマ�Eサポ�Eトリンク: {extract_text_from_items(ecinfo_data.get('カスタマ�Eサポ�Eトリンク', []))}") + + return "、E.join(parts) + "、E if parts else "惁E��なぁE + +def parse_ecinfo_input(ecinfo_input: str) -> str: + """ + ecinfo入力をパ�EスしてチE��スト形式に変換 + JSON形式�E場合�Eパ�EスしてチE��ストに変換、既にチE��スト形式�E場合�Eそ�Eまま返す + """ + # JSON形式かどぁE��を判宁E + ecinfo_input = ecinfo_input.strip() + if ecinfo_input.startswith('{') or ecinfo_input.startswith('['): + try: + ecinfo_data = json.loads(ecinfo_input) + return ecinfo_to_text(ecinfo_data) + except json.JSONDecodeError: + # JSONパ�Eスに失敗した場合�E、テキスト形式として扱ぁE + return ecinfo_input + else: + # 既にチE��スト形弁E + return ecinfo_input + +@customtracer +def ecinfo2winningrate(ecinfo1, ecinfo2): + """ + input1 (text): 会社吁E ベルーナ。業畁E コマ�Eス�E�趣味・食品・衣類)。中刁E��E ファチE��ョン。サービス: オンラインショチE��ング。商杁E 裏起毛パーカー。タイトル: あったか裏起毛パフ袖ゆったりパ�Eカー - ベルーナ【�E式】。訴求テーチE チE��イン変更でタチE�E玁E��加, カラー/サイズ変更でCVR増加。ロゴ: ベルーナ【�E式】。グローバル検索バ�E: 検索。商品名: あったか裏起毛パフ袖ゆったりパ�Eカー。価格: 3,289冁E��税込�E�。カラースウォチE��: カラー選択。サイズセレクタ: サイズ選択。�Eタン_カート追加: カートに追加、E + input2 (text): 会社吁E ベルーナ。業畁E コマ�Eス�E�趣味・食品・衣類)。中刁E��E ファチE��ョン。サービス: オンラインショチE�E。商杁E あったか裏起毛パフ袖ゆったりパ�Eカー。タイトル: あったか裏起毛パフ袖ゆったりパ�Eカー - ベルーナ【�E式】。訴求テーチE フリルのスタンドカラー, 大人可愛いチE��イン。ロゴ: alotta。商品名: あったか裏起毛パフ袖ゆったりパ�Eカー。価格: なし。カラースウォチE��: なし。サイズセレクタ: M L LL 3L。�Eタン_カート追加: カートに入れる、E + output1 (json): チE��トパターンの勝敗予測と琁E�� + """ + print("ecinfo2winningrate") + client = LLMClient()) + + # 入力がJSON形式�E場合�EチE��ストに変換 + ecinfo1_text = parse_ecinfo_input(ecinfo1) + ecinfo2_text = parse_ecinfo_input(ecinfo2) + + p = "以下に2つのECサイト�Eージの冁E��を�E挙します。テストパターンの勝敗を予想し、勝敗�E琁E��めEつ述べてください、En\n#オリジナル\n" + ecinfo1_text + "\n\n#チE��トパターン\n" + ecinfo2_text + + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.1-8B-Instruct", + + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": """WEBペ�EジのECサイト情報を比輁E��て、テストパターンの勝敗を予測し、勝因を記述します。以下�E注意に従い記載をしてください、E +STEP1: possibilityには、その勝敗予測が合ってぁE��確玁E��0~100の間で入れてください。�E容が同一の場合�EpossibilityめEにしてください。勝因に特筁E��べきものがあれ�E、contributionを高く設定してください、E +STEP2: contributionに応じて、reasonの強さを「優れてぁE��、E「すると良ぁE��E「可能性がある」�Eように表現を変えてください、E +STEP3: オリジナルと差刁E��なければ、理由には特に何も書かなぁE��ください、E +STEP4: チE��トパターンに値がなぁE��合�E、contributionめEにして何も提案しなくてよいです、E +STEP5: 斁E���E、だ・である調で統一してください、E"" + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": p + } + ] + } + ], + response_format=testpattern_win_or_lose, + temperature=1.2, + top_p=1, + frequency_penalty=0, + presence_penalty=0 + ) + return response + diff --git a/apis/format2cninfo.py b/apis/format2cninfo.py new file mode 100644 index 0000000000000000000000000000000000000000..75d1b6b2fa93988fff4099a3404496df0d7e80f6 --- /dev/null +++ b/apis/format2cninfo.py @@ -0,0 +1,118 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, Field +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from typing import List, Optional +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer +from src.models.common import model + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class UIoption(str, Enum): + element1 = "バナー/動画" + element2 = "CTA" + element3 = "チE��スチE + element4 = "フォーム" + +class Component(BaseModel): + component_large: str + component_middle: str + component_small: list[str] + UIelement: UIoption + +class CNinfo(BaseModel): + components: list[Component] + +def get_openai_request(messages, format): + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0 + ) + return response + +@customtracer +def format2cninfo(p, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): ■自社: 親子でのスマ�E料��節紁E親子でのお得感 チE�Eタの余剰利用 通話とネット�Eコストパフォーマンス スマ�EチE��ュー支援 家族向け�E安�E機�E 豊富な端末ラインアチE�E ■競合他社: 22歳までのお得なプラン 大好評�Eサービス 親子でお得にスマ�Eを利用 22歳以下限定�Eお得なキャンペ�Eン 学生向け�Eお得さ 青春年齢向けのお得なプラン 低価格で高品質な通信サービス 格安SIMとスマ�Eの利便性 22歳以下限定�E割引キャンペ�Eン スマ�EチE��ュー応援 家族割引との絁E��合わせでの最安値 料��プランの多様性 親子でのお得な割引サービス スマ�EチE��ューのお得さ 特別割弁EチE�Eタ3GB提侁E割引サービスによるコスト削渁E新規契紁E��プラン変更による特典 機種代と基本料�Eダブル割弁E大容量データ エントリー制の特典シスチE�� 24時間ぁE��でもオンラインで手続き可能 家族�E員が割引を受けられるサービス 家族間の無料通話サービス プライムビデオ特典 22歳までの長期利用可能 製品ラインナップ�E允E��E期間限定�Eキャンペ�Eン 人気スマ�Eの割引販売 安�E教育サービス 話題�Eスマ�Eが安く手に入めE詳細なサポ�EトとFAQ シンプルな料��プラン 家族�E員の料��割弁E子育てサポ�Eトサービス 業界トレンド:「◯◯◯◯◯」「◯◯◯◯◯」「◯◯◯◯◯」が吁E��共通する訴求コンチE��チE��ある、E60字程度) + input2 (text): default + output1 (json): 頁E�� + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、忁E��なコンチE��チE�E要素を生成してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + return get_openai_request(messages, CNinfo) \ No newline at end of file diff --git a/apis/format2cninfos.py b/apis/format2cninfos.py new file mode 100644 index 0000000000000000000000000000000000000000..cade7d8a9ecab03397569126de85f123ca2b63bd --- /dev/null +++ b/apis/format2cninfos.py @@ -0,0 +1,161 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, Field +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from typing import List, Optional +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer +from src.models.common import model + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +""" +CN(コンチE��チE用の褁E��バリアント生成API�E�Eormat2cninfo.pyに依存しなぁE��立実裁E��E +""" + +# スキーマ定義�E�Eormat2cninfo.pyから独立!E +class UIoption(str, Enum): + element1 = "バナー/動画" + element2 = "CTA" + element3 = "チE��スチE + element4 = "フォーム" + +class Component(BaseModel): + component_large: str + component_middle: str + component_small: list[str] + UIelement: UIoption + +class CNinfo(BaseModel): + components: list[Component] + + +def get_openai_request(messages, format, n=1): + """ + OpenAI API呼び出し!Eパラメータ対応、常にリストを返す�E�E + + Args: + messages: メチE��ージリスチE + format: レスポンスフォーマッチE + n: 生�Eする候補数�E�デフォルチE 1�E�E + + Returns: + list[str]: 常にリストで返却�E�E=1でも長ぁEのリスト!E + """ + client = LLMClient()) + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0, + n=n + ) + + # 常にリストで返す�E�E=1でも統一�E�E + return [choice.message.content for choice in response.choices] + + +@customtracer +def format2cninfos(p, openai_key=os.environ.get('OPENAI_KEY'), n=1): + """ + input1 (text): prompt text + input2 (text): default + input3 (number): 1 + output1 (json): CNinfo variants list + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__, f"n={n}") + + if openai_key == "default" or not openai_key: + openai_key = os.environ.get('OPENAI_KEY', '') + + if openai_key: + os.environ['OPENAI_API_KEY'] = openai_key + + # n を整数に変換し、篁E��チェチE�� + try: + n = int(n) + if n < 1: + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + elif n > 10: + print(f"Warning: n={n} is too large, capping at 10") + n = 10 + except (TypeError, ValueError): + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、忁E��なコンチE��チE�E要素を生成してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + # get_openai_requestは常にリストを返すので、そのまま使用 + result = get_openai_request(messages, CNinfo, n=n) + + print(f"Generated {len(result)} CN variants") + + # リストをJSON斁E���Eとして返す + return json.dumps(result, ensure_ascii=False) diff --git a/apis/format2ecinfo.py b/apis/format2ecinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..3b2bf3edb9bdb931353537ce1b3d01b64b2b885d --- /dev/null +++ b/apis/format2ecinfo.py @@ -0,0 +1,245 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from enum import Enum +from datetime import datetime +import pytz +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +""" +EC用のバリアント生成API +baseimg2ecinfo_rect.pyのpageInfo構造に準拠 +""" + +class Category(str, Enum): + ビジネス = "ビジネス�E�EaaS・法人支援�E�E + ヘルスケア = "ヘルスケア�E�美容・健康�E�E + ヒューマンリソース = "ヒューマンリソース�E�求人・紹介!E + コマ�Eス = "コマ�Eス�E�趣味・食品・衣類!E + ファイナンス = "ファイナンス�E���融�E保険・不動産�E�E + インフラ = "インフラ�E�電気�E通信・ガス・住屁E��E + ライフイベンチE= "ライフイベント(教育・結婚�E相諁E��E + +class CategoryMiddle(str, Enum): + # ビジネス + ITソフトウェア = "IT・ソフトウェア" + マ�Eケ支援コンサル = "マ�Eケ支援・コンサル" + オフィス機器用品E= "オフィス・機器用品E + + # ヘルスケア + 健康食品器具 = "健康食品・器具" + 美容医療クリニック = "美容・医療クリニック" + 美容コスメ = "美容コスメ" + フィチE��ネスジム = "フィチE��ネスジム" + + # ヒューマンリソース + 求人惁E�� = "求人惁E��" + 人材紹仁E= "人材紹仁E + 人材派遣 = "人材派遣" + + # コマ�Eス + 動画アニメゲーム = "動画・アニメ・ゲーム" + リユースリサイクル = "リユース・リサイクル" + 旁E���EチE��レジャー = "旁E���Eホテル・レジャー" + 趣味交隁E= "趣味・交隁E + 新聞雑誌メチE��ア = "新聞�E雑誌�E惁E��メチE��ア" + 自動車レンタカー用品E= "自動車�Eレンタカー・用品E + 飲料食品生活用品E= "飲料食品・生活用品E + 家電パソコン = "家電・パソコン" + ファチE��ョン = "ファチE��ョン" + + # ファイナンス + 不動産 = "不動産" + 保険 = "保険" + ローン = "ローン" + クレカ電子決渁E= "クレカ・電子決渁E + 証券FX先物 = "証券・FX・先物" + 銀衁E= "銀衁E + + # インフラ + ネット通信サービス = "ネット�E通信サービス" + 電気ガス = "電気�Eガス" + 住宁E��備リフォーム = "住宁E��備�Eリフォーム" + + # ライフイベンチE + 士業相諁E= "士業・相諁E + 学習スクール = "学習�Eスクール" + 結婚�E会い = "結婚�E出会い" + 葬儀墓地 = "葬儀・墓地" + 引越し介護 = "引越し・介護" + +class Meta(BaseModel): + 会社吁E str + 業畁E Category + 中刁E��E CategoryMiddle + サービス: str + 啁E��: str + タイトル: str + 訴求テーチE list[str] + +class cood(BaseModel): + x: int + y: int + +class str_with_rect(BaseModel): + text: str + html: str + rect: list[cood] + +class pageInfo(BaseModel): + # ペ�Eジ共送E + メタ: Meta + ロゴ: list[str_with_rect] + グローバル検索バ�E: list[str_with_rect] + ハンバ�Eガーメニューアイコン: list[str_with_rect] + カートアイコン: list[str_with_rect] + ユーザーメニュー: list[str_with_rect] + + # ナビゲーション + ブレチE��クラム: list[str_with_rect] + ペ�Eジネ�Eション: list[str_with_rect] + タブ�E替: list[str_with_rect] + + # トップ�Eージ + メインビジュアル: list[str_with_rect] + プロモーションバナー: list[str_with_rect] + カチE��リカーチE list[str_with_rect] + + # 啁E��一覧ペ�Eジ + 啁E��一覧: list[str_with_rect] + フィルタ: list[str_with_rect] + ソーチE list[str_with_rect] + ペ�Eジャー: list[str_with_rect] + クイチE��ビューアイコン: list[str_with_rect] + + # 啁E��詳細ペ�Eジ + 啁E��吁E list[str_with_rect] + 価格: list[str_with_rect] + ブランチE list[str_with_rect] + サムネイル: list[str_with_rect] + 画像ギャラリー: list[str_with_rect] + カラースウォチE��: list[str_with_rect] + サイズセレクタ: list[str_with_rect] + 在庫スチE�Eタス: list[str_with_rect] + 配送情報: list[str_with_rect] + ボタン_カート追加: list[str_with_rect] + ボタン_今すぐ購入: list[str_with_rect] + レビューサマリー: list[str_with_rect] + レビューボタン: list[str_with_rect] + QnAリンク: list[str_with_rect] + バッジタグ: list[str_with_rect] + 関連啁E��カルーセル: list[str_with_rect] + + # カート�Eージ + カート商品リスチE list[str_with_rect] + 数量セレクタ: list[str_with_rect] + 削除アイコン: list[str_with_rect] + クーポン入劁E list[str_with_rect] + 注斁E��計サマリー: list[str_with_rect] + チェチE��アウト�Eタン: list[str_with_rect] + + # 共通下部 + フッターリンク: list[str_with_rect] + SNSアイコン: list[str_with_rect] + カスタマ�Eサポ�Eトリンク: list[str_with_rect] + + +def get_openai_request(messages, format, openai_key): + """OpenAI APIを呼び出ぁE"" + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0 + ) + return response + +@customtracer +def format2ecinfo(p, openai_key=os.environ.get('OPENAI_KEY')): + """ + ECサイト用のバリアント生成API + baseimg2ecinfo_rect.pyのpageInfo構造に準拠 + + input1 (text): プロンプト�E�Eormat2fvinfoと同様�E形式!E + input2 (text): default + output1 (json): pageInfo形式�EJSON + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + + if openai_key == "default" or not openai_key: + openai_key = os.environ.get('OPENAI_KEY', '') + + if openai_key: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、ECサイト向け�Eペ�Eジ惁E��を生成してください。baseimg2ecinfo_rect.pyのpageInfo構造に準拠し、ECサイト�E特性�E�商品比輁E��カチE��リ一覧、賁E��請求など�E�を老E�Eして、E��刁E��要素を生成してください。各要素はstr_with_rect形式!Eext, html, rect�E�で記述してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + return get_openai_request(messages, pageInfo, openai_key) diff --git a/apis/format2ecinfos.py b/apis/format2ecinfos.py new file mode 100644 index 0000000000000000000000000000000000000000..b9722450c320af36f2c4403321d8fa580b73f635 --- /dev/null +++ b/apis/format2ecinfos.py @@ -0,0 +1,279 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +import numpy as np +from enum import Enum +from datetime import datetime +import pytz +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +""" +EC用の褁E��バリアント生成API�E�Eormat2ecinfo.pyに依存しなぁE��立実裁E��E +baseimg2ecinfo_rect.pyのpageInfo構造に準拠 +""" + +# スキーマ定義�E�Eormat2ecinfo.pyから独立!E +class Category(str, Enum): + ビジネス = "ビジネス�E�EaaS・法人支援�E�E + ヘルスケア = "ヘルスケア�E�美容・健康�E�E + ヒューマンリソース = "ヒューマンリソース�E�求人・紹介!E + コマ�Eス = "コマ�Eス�E�趣味・食品・衣類!E + ファイナンス = "ファイナンス�E���融�E保険・不動産�E�E + インフラ = "インフラ�E�電気�E通信・ガス・住屁E��E + ライフイベンチE= "ライフイベント(教育・結婚�E相諁E��E + +class CategoryMiddle(str, Enum): + # ビジネス + ITソフトウェア = "IT・ソフトウェア" + マ�Eケ支援コンサル = "マ�Eケ支援・コンサル" + オフィス機器用品E= "オフィス・機器用品E + + # ヘルスケア + 健康食品器具 = "健康食品・器具" + 美容医療クリニック = "美容・医療クリニック" + 美容コスメ = "美容コスメ" + フィチE��ネスジム = "フィチE��ネスジム" + + # ヒューマンリソース + 求人惁E�� = "求人惁E��" + 人材紹仁E= "人材紹仁E + 人材派遣 = "人材派遣" + + # コマ�Eス + 動画アニメゲーム = "動画・アニメ・ゲーム" + リユースリサイクル = "リユース・リサイクル" + 旁E���EチE��レジャー = "旁E���Eホテル・レジャー" + 趣味交隁E= "趣味・交隁E + 新聞雑誌メチE��ア = "新聞�E雑誌�E惁E��メチE��ア" + 自動車レンタカー用品E= "自動車�Eレンタカー・用品E + 飲料食品生活用品E= "飲料食品・生活用品E + 家電パソコン = "家電・パソコン" + ファチE��ョン = "ファチE��ョン" + + # ファイナンス + 不動産 = "不動産" + 保険 = "保険" + ローン = "ローン" + クレカ電子決渁E= "クレカ・電子決渁E + 証券FX先物 = "証券・FX・先物" + 銀衁E= "銀衁E + + # インフラ + ネット通信サービス = "ネット�E通信サービス" + 電気ガス = "電気�Eガス" + 住宁E��備リフォーム = "住宁E��備�Eリフォーム" + + # ライフイベンチE + 士業相諁E= "士業・相諁E + 学習スクール = "学習�Eスクール" + 結婚�E会い = "結婚�E出会い" + 葬儀墓地 = "葬儀・墓地" + 引越し介護 = "引越し・介護" + +class Meta(BaseModel): + 会社吁E str + 業畁E Category + 中刁E��E CategoryMiddle + サービス: str + 啁E��: str + タイトル: str + 構�Eの意図: str + 訴求テーチE list[str] + +class cood(BaseModel): + x: int + y: int + +class str_with_rect(BaseModel): + text: str + html: str + rect: list[cood] + +class pageInfo(BaseModel): + # ペ�Eジ共送E + メタ: Meta + ロゴ: list[str_with_rect] + グローバル検索バ�E: list[str_with_rect] + ハンバ�Eガーメニューアイコン: list[str_with_rect] + カートアイコン: list[str_with_rect] + ユーザーメニュー: list[str_with_rect] + + # ナビゲーション + ブレチE��クラム: list[str_with_rect] + ペ�Eジネ�Eション: list[str_with_rect] + タブ�E替: list[str_with_rect] + + # トップ�Eージ + メインビジュアル: list[str_with_rect] + プロモーションバナー: list[str_with_rect] + カチE��リカーチE list[str_with_rect] + + # 啁E��一覧ペ�Eジ + 啁E��一覧: list[str_with_rect] + フィルタ: list[str_with_rect] + ソーチE list[str_with_rect] + ペ�Eジャー: list[str_with_rect] + クイチE��ビューアイコン: list[str_with_rect] + + # 啁E��詳細ペ�Eジ + 啁E��吁E list[str_with_rect] + 価格: list[str_with_rect] + ブランチE list[str_with_rect] + サムネイル: list[str_with_rect] + 画像ギャラリー: list[str_with_rect] + カラースウォチE��: list[str_with_rect] + サイズセレクタ: list[str_with_rect] + 在庫スチE�Eタス: list[str_with_rect] + 配送情報: list[str_with_rect] + ボタン_カート追加: list[str_with_rect] + ボタン_今すぐ購入: list[str_with_rect] + レビューサマリー: list[str_with_rect] + レビューボタン: list[str_with_rect] + QnAリンク: list[str_with_rect] + バッジタグ: list[str_with_rect] + 関連啁E��カルーセル: list[str_with_rect] + + # カート�Eージ + カート商品リスチE list[str_with_rect] + 数量セレクタ: list[str_with_rect] + 削除アイコン: list[str_with_rect] + クーポン入劁E list[str_with_rect] + 注斁E��計サマリー: list[str_with_rect] + チェチE��アウト�Eタン: list[str_with_rect] + + # 共通下部 + フッターリンク: list[str_with_rect] + SNSアイコン: list[str_with_rect] + カスタマ�Eサポ�Eトリンク: list[str_with_rect] + + +def get_openai_request(messages, format, n=1): + """ + OpenAI API呼び出し!Eパラメータ対応、常にリストを返す�E�E + + Args: + messages: メチE��ージリスチE + format: レスポンスフォーマッチE + n: 生�Eする候補数�E�デフォルチE 1�E�E + + Returns: + list[str]: 常にリストで返却�E�E=1でも長ぁEのリスト!E + """ + client = LLMClient()) + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0, + n=n + ) + + # 常にリストで返す�E�E=1でも統一�E�E + return [choice.message.content for choice in response.choices] + + +@customtracer +def format2ecinfos(p, openai_key=os.environ.get('OPENAI_KEY'), n=1): + """ + input1 (text): プロンプト�E�Eormat2ecinfoと同様�E形式!E + input2 (text): default + input3 (number): 1 + output1 (json): pageInfo形式�EJSONの配�E + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__, f"n={n}") + + if openai_key == "default" or not openai_key: + openai_key = os.environ.get('OPENAI_KEY', '') + + if openai_key: + os.environ['OPENAI_API_KEY'] = openai_key + + # n を整数に変換し、篁E��チェチE�� + try: + n = int(n) + if n < 1: + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + elif n > 10: + print(f"Warning: n={n} is too large, capping at 10") + n = 10 + except (TypeError, ValueError): + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、ECサイト向け�Eペ�Eジ惁E��を生成してください。baseimg2ecinfo_rect.pyのpageInfo構造に準拠し、ECサイト�E特性�E�商品比輁E��カチE��リ一覧、賁E��請求など�E�を老E�Eして、E��刁E��要素を生成してください。各要素はstr_with_rect形式!Eext, html, rect�E�で記述してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + # get_openai_requestは常にリストを返すので、そのまま使用 + result = get_openai_request(messages, pageInfo, n=n) + + print(f"Generated {len(result)} EC variants") + + # リストをJSON斁E���Eとして返す + #return json.dumps(result, ensure_ascii=False) + return result diff --git a/apis/format2fvinfo.py b/apis/format2fvinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..1cb6956c84e37f72d93469ab8f722304232a3312 --- /dev/null +++ b/apis/format2fvinfo.py @@ -0,0 +1,154 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, Field +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from typing import List, Optional +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer +from src.models.common import model + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +""" +class FVinfo(BaseModel): + メインコピ�E: list[str] + サブコピ�E: list[str] + ビジュアル: list[str] + 権威付け: list[str] + CTA: list[str] +""" + +class Meta(BaseModel): + 会社吁E str + 業畁E str + ブランチE str + サービス: str + 啁E��: str + タイトル: str + 訴求テーチE list[str] + +class Font(str, Enum): + font1 = "ゴシチE��" + font2 = "明朝" + font3 = "手書ぁE + +class EvsF(str, Enum): + EMOTIONAL = "惁E��E + FUNCTIONAL = "機�E" + +class PvsS(str, Enum): + PROBLEM = "問題提起" + SOLUTION = "課題解決" + +class Copy(BaseModel): + text: str + font: Font + color: str + visual: str + appeal_mode : EvsF + forcus_stage : PvsS + +class CatchCopy(BaseModel): + main_copy: list[Copy] + sub_copy: list[Copy] + +class FVinfo(BaseModel): + 非LP: bool + メタ: Meta + キャチE��コピ�E: CatchCopy + 権威付け: list[str] + ビジュアル: list[str] + CTAボタン: list[str] + +def get_openai_request(messages, format): + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0 + ) + return response + +@customtracer +def format2fvinfo(p, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): ■自社: 親子でのスマ�E料��節紁E親子でのお得感 チE�Eタの余剰利用 通話とネット�Eコストパフォーマンス スマ�EチE��ュー支援 家族向け�E安�E機�E 豊富な端末ラインアチE�E ■競合他社: 22歳までのお得なプラン 大好評�Eサービス 親子でお得にスマ�Eを利用 22歳以下限定�Eお得なキャンペ�Eン 学生向け�Eお得さ 青春年齢向けのお得なプラン 低価格で高品質な通信サービス 格安SIMとスマ�Eの利便性 22歳以下限定�E割引キャンペ�Eン スマ�EチE��ュー応援 家族割引との絁E��合わせでの最安値 料��プランの多様性 親子でのお得な割引サービス スマ�EチE��ューのお得さ 特別割弁EチE�Eタ3GB提侁E割引サービスによるコスト削渁E新規契紁E��プラン変更による特典 機種代と基本料�Eダブル割弁E大容量データ エントリー制の特典シスチE�� 24時間ぁE��でもオンラインで手続き可能 家族�E員が割引を受けられるサービス 家族間の無料通話サービス プライムビデオ特典 22歳までの長期利用可能 製品ラインナップ�E允E��E期間限定�Eキャンペ�Eン 人気スマ�Eの割引販売 安�E教育サービス 話題�Eスマ�Eが安く手に入めE詳細なサポ�EトとFAQ シンプルな料��プラン 家族�E員の料��割弁E子育てサポ�Eトサービス 業界トレンド:「◯◯◯◯◯」「◯◯◯◯◯」「◯◯◯◯◯」が吁E��共通する訴求コンチE��チE��ある、E60字程度) + input2 (text): default + output1 (json): 頁E�� + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、忁E��なファーストビューの要素を生成してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + return get_openai_request(messages, FVinfo) \ No newline at end of file diff --git a/apis/format2fvinfos.py b/apis/format2fvinfos.py new file mode 100644 index 0000000000000000000000000000000000000000..447a583a48b55771da94bea9c8ef061b99661c01 --- /dev/null +++ b/apis/format2fvinfos.py @@ -0,0 +1,184 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, Field +from enum import Enum +import base64 +from io import BytesIO +from PIL import Image +from typing import List, Optional +from functools import cache +from datetime import datetime +import pytz +from src.utils.tracer import customtracer +from src.models.common import model + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +# スキーマ定義�E�Eormat2fvinfo.pyから独立!E +class Meta(BaseModel): + 会社吁E str + 業畁E str + ブランチE str + サービス: str + 啁E��: str + タイトル: str + 構�Eの意図: str + 訴求テーチE list[str] + +class Font(str, Enum): + font1 = "ゴシチE��" + font2 = "明朝" + font3 = "手書ぁE + +class EvsF(str, Enum): + EMOTIONAL = "惁E��E + FUNCTIONAL = "機�E" + +class PvsS(str, Enum): + PROBLEM = "問題提起" + SOLUTION = "課題解決" + +class Copy(BaseModel): + text: str + font: Font + color: str + visual: str + appeal_mode : EvsF + forcus_stage : PvsS + +class CatchCopy(BaseModel): + main_copy: list[Copy] + sub_copy: list[Copy] + +class FVinfo(BaseModel): + 非LP: bool + メタ: Meta + キャチE��コピ�E: CatchCopy + 権威付け: list[str] + ビジュアル: list[str] + CTAボタン: list[str] + +def get_openai_request(messages, format, n=1): + """ + OpenAI API呼び出し!Eパラメータ対応!E + + Args: + messages: メチE��ージリスチE + format: レスポンスフォーマッチE + n: 生�Eする候補数�E�デフォルチE 1�E�E + + Returns: + list[str]: 常にリストで返却�E�E=1でも長ぁEのリスト!E + """ + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=1.2, + n=n + ) + + # 常にリストで返す�E�E=1でも統一�E�E + return [choice.message.content for choice in response.choices] + +@customtracer +def format2fvinfos(p, openai_key=os.environ.get('OPENAI_KEY'), n=1): + """ + 褁E��バリアントを返すformat2fvinfo�E��E列版�E�E + + input1 (text): prompt text + input2 (text): default + input3 (number): 1 + output1 (json): variants list + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__, f"n={n}") + + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + + # n を整数に変換し、篁E��チェチE�� + try: + n = int(n) + if n < 1: + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + elif n > 10: + print(f"Warning: n={n} is too large, capping at 10") + n = 10 + except (TypeError, ValueError): + print(f"Warning: n={n} is invalid, using n=1") + n = 1 + + messages=[ + { + "role": "system", + "content": """提供したフォーマットデータから、忁E��なファーストビューの要素を生成してください、E"", + }, + { + "role": "user", + "content": [{"type": "text", "text":p}] + }, + ] + + # get_openai_requestは常にリストを返すので、そのまま使用 + result = get_openai_request(messages, FVinfo, n=n) + + print(f"Generated {len(result)} variants") + + # リストをJSON斁E���Eとして返す + return json.dumps(result, ensure_ascii=False) diff --git a/apis/framework.py b/apis/framework.py new file mode 100644 index 0000000000000000000000000000000000000000..c3b677b4034010862864ee4c1d3104cef9958038 --- /dev/null +++ b/apis/framework.py @@ -0,0 +1,70 @@ +import os +from pydantic import BaseModel +from typing import List +from datetime import datetime +import pytz + +from src.clients.llm_client import LLMClient + +class EvaluationQuestion(BaseModel): + question: str + result: int + citation: str + reason: str + suggestion: str + +class EvaluationCategory(BaseModel): + category_name: str # カチEリ吁E questions: List[EvaluationQuestion] # 質問リスチE +class EvaluationModel(BaseModel): + categories: List[EvaluationCategory] # カチEリをリストとして保持 + +def framework( + base64img, + p, + framework_p, + openai_key=os.environ.get('OPENAI_API_KEY'), + gemini_key=None, + model="meta-llama/Llama-3.3-70B-Instruct", +): + """ + input0 (text): /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUSEhIVFRUVFxUVFxUVFRcVFRUVFRUWFhUVFRYYHSggGBomHRUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGhAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAQsAvQMBEQACEQEDEQH/xAAcAAABBQEBAQAAAAAAAAAAAAACAAEDBAUGBwj/xABPEAABAwEEBAcKCggFBAMAAAABAAIDEQQSITEFBkFREyJhcXOR0gcUMlOBkpOxsrMzQlJicnShwdHwFiMkNENjgsIXNaPD02SitOEVg+L/xAAbAQACAwEBAQAAAAAAAAAAAAAAAQIDBAUGB//EADoRAAIBAgEIBwgDAAIDAQEAAAABAgMRBBIhMUFRcZHRBRMVYYGhwRQiMkJScrHhM5LwI2IkNPGiQ//aAAwDAQACEQMRAD8A3bp5CF6TMeLs2J0ICFJg6cURNmu/nFScblSqZJYjeHYgqtqxojJSV0PJhjmNqFnCXu5wTK00DXAHdknZrSJyjJWiwopHVoUmlqHGcr2ZKGbyo3LMnaEIm7kspjyIkgFFEstYcORYdx3YiiNDB51YggZuPkUpMqhHYyw0KDLkSNKTJJ2LETlW0XRZfhVMjTEstVZch0gHQMYoEyKR1Bj9mKmkQk7LOUXzNORVqi0Z3OL0FV6sRSznS4xkY4fnYt2aRxfepvuL8zmm7RjDVgcSTJWpc8fFeBTihZll3ee1nbUb5dXkxvG91fS1rff3A0GXBR/6v/IpWn9XkiP/AA6MjzfMrh7mn4OPzpv+RSyZP5vJFWVCD/j/AP1LmXI56j4OP/U7arcJr5vJGmNWm1fI83zIpAMxFGfS9tSSn9XkiuXVaVT83zB4cHAxx9ctR5eETyJrRLyRHraTVnDzfMnZaPmR03/rft46g6c/q8kXKrTXy+b5kgl+ZH/qdtRyZ/V5Inl0/p83zJmvPyI/9TtqLjLb+C1Sh9PmyOaYj4kZ9J204wk/m/BXOpCPyebI4bSTmyMek7alKnL6vJEIVoPTG3i+ZI7O9cjr/wDb21FKejK8kWPq75WT5vmOLUfkMrupJ28UdXL6vwProfT+QxO7xbKc0nqvpZEvq/BJVY/R5stRE/Jj6pO2qmpbS+Lh9P5Lcd44Uj6n9tVNS2l6cXqLMZdkOD6n9tQae0ti46kHx/mea/tqNntJXWwFzpB8jzXdtNJ7SLklqIXySO8Ex4cjx/epqNtJBzv8KK8lpfW7RjjgSAHAgGtCRf5D1KSh3lcp7Y3IbRQmhaBg04DaRXeVZTbz5ymtGObNpK1zdUc6vvtM2TsMgXX5ODuQFac6OdaMtdw7XJwZYCOLwbcd3HkVdNZTlv8ARF1eXVqCejJ9WGXYVPXs/wDSkRvYGSpGVebFNZiMrtZkQRWgA0+zJScblcKlnYth4Vdi9SViCQGtQprRYqkne6CuhwqOpK7TzkrKauiIPc04HyHEKVkytSlB6S1FbGnkP2darcGjRCvF9xLUOUc6J3UiTggcDj+d6jlPUTyE8zHETxgKEcqMqL0jUJrMhgXDwwAjNqBOS+JEodQ45FRtcnezzluMqtl8Syw15FWy5ZyeNrhk6vOoNp6iyKktDLDXV51W0Wpic4BCQNpDFwTsJtGNwDWSulMjjWoaK5B2JBNcQCXUyoDtopRpO9yudaNrBzzC8eUM5fiqynF595VVqLMu4qSwkmocQtCkthklBt5mY4iIANwfSaRXnWm6b0nOyWle3AltxLixhY5wMTajAHw5Mq0qeRVU8zk76/RF9e8lCLV/d9WW9V7Ox83ASguYY3OYTea4FpHFOVcCepV4qcoQyoabl3R9OFSq6dRXVs2lHVxat2VuTHDk4R9PWue8XWel+SOxHo7DR0R83zOM1x0UyOcCMhoLA/jE4m84HE8w610MHXlOLUjjdKYSFOSlDMZ9jtT63XY8tK4c4WuUVpOfTqSvZmlwe7D87lVc1ZBmWiNzXVyxzBoFbFpoyTi4u5savaJ4eXjhwa0Xnmpxrk0HeTXLYDyLNia/VQzaXoN2BwnX1PeTstPI6r9GLJWvBu9I/wDFc72yvt8kdrszC6cnzfM5vTJhZaOBgZQMFZHlziA51C2MVOdMTzjlWrDzqTTcmYsXSo0pKNNW25wakDKqvM2dIZswrnQ7ihxzAqiuTSuwr4Qz2fYopZ7FknmvpIGaQZkPtwCm6UtLKViaehEjJ3A4trygqLitTJqpJPOjQjmCocWbIzRaZMN6rcWWqaCdM3PD70slknOOkB1oBxwpvripKD0EXUWkgmtYzGI31+5SjTK5VlpRn2xzTm0K+CaMlVxelFa0S0cKYcRnsqVON8q+0rrTtk2+lDttzuT1qXVoisRJHMwaYc3AtB3jLHfyFaGkznQqSjuNPSOkmng6tIrE1wpjTjyD7lRRjaUt/ojXi6icKd1pj6sk0BpUi0xcfC8G4g436sz/AKgjE006crIMDXlGvG7zaOOY9G4VcbJPUZRxfdHaf2eUVwMkZp84Bw9h3Wt2BdptHJ6XjempbGYVngaW4PINPBOw7cF0W2tRw4wi1pLURkb4JDt3G+5ReS9JbDLj8LuPJaK0bcLnEgXaZkmgojJtnuN1Mr3bXZ3WiLMIYgzM5uO9x+4ZDmXHrT6ybkelw1JUaahxI9PaY73hc8CrzxY2/KkPgjmGJPICoQp5TsWVaypxcjhdGWd1CXSFznFxeTm5zjVxPOarsJKCSSPNXlVk5NmjFIRgWnnGKi0nrLYScczRK5zHbj6x+CSUkTbhPvKrRiQW0HKVPvKEs9rZghHHuCLyGo09gJbQUa403bkadIrZKtFsjY+6auJ6sFJq+ZEIvJd5MvWa1MPOVTKEjXTrQZcjlON1wdzhVOK1miMnqaYEVobU33Y7tibg/lRGNWN/fecaaauLS0jLPFOMbaRTnfPGzM+0yDnOAor4ox1Joa1nEcXExx+TiqNP5s+tkq7zQzfKiAO5APJ+CtsUXPOn64sJBLJCRhUtYajceNisXaVLY/LmdF9B13plHz5GlpTWqFne5MTyH2dj6ANwBlmFPC5CoQx9OLk7PO+7mWVeh601BKUfdVte19xXi13szDebDKKUIBDHUINcDfBUn0jSatZ+XMjHoSvGSkpR8+R7bDaw9rXtODgHDmcKj1rMkdFyMDX6cNsT5SCeCcySjQCfCuHAkDJ52qcJ9U8tlNWj7RHq1pe08pZrjEDUMlFMsGdtaO0qWx+XMwdg4i98qPnyLo17s58KGWu9oYP9xLtKnqT8uZPsSs9Lj58ju9Qray1MNobG9rGuLWGSlXEeE4UJwGVd9dyjPEqrH3bltHAvDz99ptaLX9UjsOGVGSbMs8p0/wB0SzPtTqtleyKrIywMLXGvHkFXitSKA7gN6VPE04POmFbBVaqVmlx5FH/EGzB94QzY5gtj+6RaO0KdrNPy5mLsasp5SlHz5Eo7o9nGUdo6oj/eo+309j/3iWLomstEl58hx3RrLWphtFRkQ2OvvEvb4akxromre7kvPkM7ui2U5xWg84jH+4n7fDYxPoiq9Ml58iKXX6yfFhtAPNHn6RNdIw1p/wC8SuXQtT5ZJceRCO6BF8mbqj7al2jR2Py5kOxcV9cfPkTO7osGyObqjP8Aeo9oUdj8uZN9D4nVKPnyBPdDg2RSjmbGP9xPtGlsflzF2NiNUo+fINndGgAIEc48kZHlPCVSfSFJvQ/LmSj0RiErKUfPkC3uiw41jmP9MeH/AHp9oUdj8uYl0Pidco+fIdvdDs22KbyCMf3pdo0tSflzGuhq+uUfPkNNr/ZT/Bn6o/8AkTXSNNan5cyMuhar1x8+R1FntolZFK0Ua+GJwvHjUpuB+9acPNVIuS1tmHGU3RnGnK11FEwun4pKuz7TN7r1HhhXmT3Zqae8GyfVI/fWhAlpMpAz3zUPSHCaPsxri2MRnniJj/sXSo54JnGxDyaskX9YIeGss8PjIpGjnLDd+2inOF4tFdOrkyT7z5zaaiq5J3TQ0Hop9qnZAzAvOLqVDGDFzzzDrNBtU4Qc5WRXVqKnFyZ9B6PhZBEyGIXWRtDWjkG87TtJ2krqKCSsjiyquTuzle6brP3vZ+AjdSWcFtQcWRZPfyE+COcnYqMRLJVlpZpwkOsld6EeLhYDqiQAkAJACQA4QhM9f0HoCCdxDwxoBAoGRVNQ84XyMrmyuflXqMRkU1mgn4btie08Vg+srt5VSS8d+1rZ3mjY9U7K4Orjdo6rIo6FpEZoCLwLuMW4EipGdCqJ1UrWhHPtW/uWbXoNVKhKSd6s3bY9WbvefVpI59VLM2IvN2ojD6cHGKFxddvCtQDdDQM6uCcakZTUerjpto/WrTuIzoyjTcutlovp36c+vRvZYi1Qspja+7i5sZ8COl5zLxAF2uw05s1B1UptZEdL1d9i2GHbgpOpPOl82tq5JNqXZWuYLuDn3TxYyaXHnY3DFhFeUFRjXTT9yOjZ3rmSlhnFxXWTzv6u58jI1m1ehs0gY2MEFtauYytdowHN1rThnCtBycY8DFjVVw9RRjOVu9njjMhzBeYPavSeyavg952U/wDTxc+S7mA/it3nlOl0/aL9yNFltcNgWx00c5YiSPDS07j1LzB7s1NPNN2yYH90j2fzrQgitJl3DuPUgkeodyy3HvaSI/w5SR9F7QfaD108Fng1sZw+k3k1E9q/B2gtK2ZBzetPBNKWQxzSxgGjJJGjD4ocQ37KLhVI5MnHYz1VKeXTjLakel9zjQ/e8JneP1swBFc2RZtbznwj/TuXTwlDJjlPS/wcTH4tSnkR0L8nV2vSLY2OkeaNYC5x5B6ytMkopt6DHCTnJRjpZ4fp3SclqnfO8HjHit+QweCwcw+0k7VxKk3OTkz01GmqcFFFC4dx6lAtFcO49SAFcO49SAFcO49SAFcO49SAFdO49SBHRx632hpq2NgI2gPB9pdR9LVWrOMfPmcSPQVGLvGcr+HIdmuNpAc0MYA6l4APoaGorxscUn0rUbTcY5t/Ma6DopOKnKz06ORI3Xi1ht0ABtCLoMgFCCCKXtxPWl2nNu+RG+58ya6Hgo5PWTtvXIdmvVsGVBgBgZMm0oPC2UHUk+kpPTCPB8wXREFoqT4rkE7X62nM5cslciM73KetJdItf/zjwfMk+iovTVnxXIhtGulqk8NrXZ+FfNKmppV29Sj0pOKtGEVx5kJ9C0pu8qknva5HMALmHZec9k1bP7JZht73i2/NXbwP8PieX6Vf/kNdyNHvYnG9TnH4LZl2Ob1Lee54qV5k9waenMrL9Uj99aEAZiAOo7n9ruTSM+WwHysd/wDsro9Gv/kcdq/H/wBON02rUYz2O3Ffo7vvpdnIPMdecXatDibSLy4Vi4kr9xq0NDPK5jvICuXLCZeKd9GZ/ryO/DpJUuj4tP3s8VxvfwTXjY7TvpdTIOB15xOvemr5FmaeK2jpOV2bWeTBx5bu5cjpCtn6pePI9J0Nh3k9fLXmXq/Tici7I0pXZzrmHdO6j/Rygvd/1oK0u0rtonmFnCrq3/1//almDOcRaAwyP4Oty+7g73hXLxuXvnXaV5UAdVoPUPSBnhMtgkMXCx8KHFrBwd8cJm4Hwa5Jhc3dbe5ba3WyU2KztFmNws/WMaG8RoeKOde8IOOI2osJM4nWTV6ewSiG0hoe5gkF114XXOc0Y0zqx2HMkM1NBaL0RJAx9r0hJBMb16JsD3taA9wZRwYQatDTntQGc2NHaraEnlZDDpSd8kjrrGizPFTzmOgwBNTgAExXZymtmiGWO1zWZkvCiIgX6UNS0OLTsqK0NPsyCGizqDYYp9I2WGZgfG97w5prRwEMjhlytB8iEDKetNmZFbbVFG0NYyeVjWitGta8gAeQIAzEAVEEj2TV5o7zsuFf2eHLmXawD/4vE8t0sk8R4IlllLTQ3hyVW9K5x5Np58x5EV5Y9+aWnMrL9Uj99aEAZqANHV2a5aYzsJun+oFo+0ha8FPJrx4cf2c/pWl1mDqLYr8M/wCDv769PY8FYV5FgKWmNJCCIvzOTRvccvJmTyArPiq6oU3PXq3mzAYN4qsqerS3sX+zLvPPCSSSTUkkknMkmpK8s227s+gRiopRirJZkaGgLXBFMH2mz98RgOBivXKkjA3huSGddZdYtDySMjGhcXvYwftDs3uDR9pTFnNbXG0aHsFqdZjooSlrWOLhM5o44Ju0JOynWgFc4nTlps9slhZYLCbO41ZwYkvmV7y0MoXUApiP6khnRSauazSg1da250JtrGgHmbN9yYsx2HdI1Nt9ulgmgkbGBCGStfO+NgeHF2AYCHeERXkCGJM8x1o1NlsDGSSzWeQvfcuwyF7gbrnXnVaMOLSvKEiVznCUDPVdRdXZ7BYp9Kus75LRwR72hpVzWuwMz25450GNwHa6gZFs8sdI55L3OvOeS9zicXOcbznHlJJPlSGdL3Mf82sfSSf+PMmhM6zWHTOhZ7XaIbdY32eRk0rO+rPiXFry3hJGsFS454tegFc5bXDVWCyxx2izW6O0wTOLGUpwoLW1dW7VpptPFoXAUxQNHFJEj1vQZPetloQP2eHHbkV3ej/4fFnkemX/AOV4InLsTfJduIIot1thyL7Tykryh9DNLTmVl+qR++tCAM1ABRyXSHD4pDvK01+5OMslqWzPwIzhlxcXrTXHMekB9cRtxXs1nV0fOHFp2YqosKxw2n9I8PLgeIyrW7j8p/l9QC8xjsT11TN8K0cz3HReC9mo+8velnfdsXh+bmcsR0hIA9A1G0BZrOyPS1vtMQhYS6GFhvySSsOAI+U1wrcFTUAkgAgsTOP1i0u62Wqa1PF0yvvBud1oAYxvKQ1rQTvBSGUYpXNcHMcWuaQ5rmktc0g1BaRiCDtCALUul7U7wrXaXfSnld63IA7XW4C06B0bOeOYZHQOLsTQCRhJryws60xazztsYGQA5hRIZ6Loqz6I0dFHap5hbrS9rZIrNGKMYSKtMoNbpG9+7BhITFnZj/4hW/v3v7hON4PAY8BwVa8Fd+2/nXHkRcLFnugO0dOyK3WN4jktDnCay0xY8Cr5CBgw1oDsfeDh8aowRS7mP+bWPpJP/HlQgZS10/zC2fWZ/eOSGYl0Z7eZAFVBI9O0dLSCy5fu0OZ+aV3ejn/w+J5Dptf+T4IeS045DqW65yVFsy3au2ffJ5w/BYezqPfx/R1u28VsjwfMu6U1fgdwFeEo2zsaKOAwEsx2t5SqqeApScr3zP8A2ovr9L4inGDWTnV3m733lIauWY+N85vZVvZ1Hv4/oo7cxOyPB8x/0Yg/meeD/ajs2j38f0Lt3E/9eD5hN1Vg3y+cOyn2dR2vj+g7cxOyPB8x/wBFYf5vnj72o7Oo7XxDtzE7I8HzEdV4Ngl89vqupdm0e/j+h9uYnZHg+YB1YhGfCecOyjs2j38f0LtzE7I8HzBOrMIOPCDneOyjs6j38f0PtzE/9eD5g/ozBX4/OHDso7Oo9/H9B25iv+vB8xSauQjLhPOHZSfR1Hv4/oa6cxOvJ4fsjGrsRy4Trr6mpdnUu/8A3gT7axGyPD9kbtAxbC/yvaPuS7Ppd/Ea6ZxGxcHzIv8A4GIkGricswSPsS7Ppd/En2xX2Lg+ZN/8BFvf5w/BPs6l38f0V9tYjYuD5hfo9D8p/nDsp9nUu/j+hdtYjYuH7JYtW4T4zzh2U10dR7+P6Iy6bxK+ng+ZKNV4N8nnDsp9m0e/j+iPbmK2R4PmL9F4N8nnDso7No9/H9B25itkeD5gu1YgG1/njso7No9/H9B25idkeD5kEmr8I2v84fgovo6j38f0Tj03iXqjwfMqv0BAMuE84fgo+wUe8tXTGJeqPB8zftjGxtgY0YNs8IFTj4JVuGioRcVqbKMfKVSUZvS4ogLgtJz7Mt38cXdRCmmUtbEX9KSCkOZ/Ut5vhJVVSeee/wBEasTF5FL7fVlMSbP/AGFdcx2CEp/ITuFiYSfOQIJz6mgcaddEDzXGcwjCrTy5+vFCE1YYQ8358qAJBCNtUADJcAwBryYdaB6SrJTPEeVIEQPJJqDTlqa+Sii85YsxDJGXGrnDy1qk1csU7aESRQAcvk/FNRISm2TWazXnNaBi5waK73EAY5bUOyV2OKc5KC0s6Y6j2zxTfSM/FZvbqG3yN/Y+K7uIUepVsH8JvpG/ij2+ht8gfQ+K7uIQ1Ntvim+kb+KPb6O3yF2Nie7iMdTLb4pvpGfij2+jt8g7GxOxcRjqXbfFN9Iz8Ue30NvkHY2J7uJC/Ua3H+G30jfxSeOo7fImuiMStnE5C0xFri05gkHnBoVeZbOLaLmlW1MWB+Ai9Spo/NvZpxb+D7UVQ3kHldT1q8xX7x9vhIE9xq6RYKQ4j4BufSSqFFZ57/RF2Lbyaf2+rImkfKHkK0GFpk15nJ1oFZgOc0ZOqgLMXfVMgPIi40mAJqnIIuJoMTu2IATpjvJ5kAVHS8ijcmokT5uX7VG5YoEbp6ZFGUSVO+kGM1215wkiTzFoP2H7Ap3KcnWXNFfvENBhwsWefhtUKnwS3P8ABdhv5ob0ezaZtL447zASQ5pNA0gMBq+9eIoLoIrsqFwKEIylaX+eo9jiJyhG8f8ALX5HPWXWKR0UkgLqNE4FQz4R8p73a7GraC62lMSeSp3TwkYzUXryduhL3jnU8dKVOU1qytmlv3b7NhtaN0o5zxA+GUPaxrnvdwVMQQHG485ljsAFkq0Eo5akrXzJX9VqubKOIlKSpyi7pK7dvR67AW/SMvC8HDG9xiLHSU4K65j2uo3juBBwqCN2KdOjDIyptK97adK3IVWvU6zJpxbta+jQ75s7RXktNtuPa2GSrn1a8mz3o4yQSKX6OcOMBXDKuWNihh8pNyWjOvezvho2lTqYnJaUHneZ+7mXHTs8Lmvoy2iaMPDS3F7SHUqCx5Y6t0kZtOSy1afVyyb7PNXNtGr1kMq1tOnudjwXSb6TSGmIkf7RXfTzI8jUV5y3sPTs1TFsHAxGg5iqqb+LezRiIWUPtRnB35qrTNYk2poizT0ocIMf4DfezKqlplv9EX4le7T+31ZSMnKr7mTJCbTlQJ3JBTlUiDuyRoTRFkgfyDqTuRsC56VwSKkkx3qDZfGCRC5/L61G5Yl3AcyRIQAOyvlQDui3Z8BhgrEUTzseo/JQLOXtEOHDwj+bF7bVGo/cluf4LcMn10H3o9h1kB4Ev4MS8Gb9wl9HXQaC6wG9jTA4bdi4eF+O17XzXzeujwznrsZ/HlZN7Z7Z/TT45tZhaOsjbQDE9sL2saXSWhjz4cvCPIHFANHOvXa0bVu0LZVqOk8uLab0Ra1Ky2+F9ecwUaarLIkotJZ5J63d7NTz21ZjR1StBm4WV7mmQmNhDa+AxguuxGTi57xyOCoxsOryYJZs78W/TMjRgJuplTk8+ZeCWZ+OdreW9JxukN11kbK0GoLnsxNM6EYZkKqk1FXVSz3MurRc3aVNSXe0YFl0aHzSO7yZSJ7WtY10bA03GPq4gVeeNvplhVbZ1nGml1jzrS7vW14fkwU8OpVZPql7rzJWWpPx/HcddY5HubV8fBmpwvB2G+o8q5k1FP3Xc69Nya95WPnzS5/XSfTf7RXeWhHkp/G97D0ycYegh9RVVL5t7NOJ+T7UUmSkZH7AfWrk7GNxTLGKkVsv6WOEHQN97MqqWmW/0RqxPw0/t9WUWq4yMnYaZYKSKmr6SQPO9O5GyJQ/DZ1JkLEb383UlckolSaWqg2XwhYgL1G5ZYEvSuSsDfRcdiWIfmiaISLXlUykYORcLFzQrq2iHpYvbaoVPgluf4LsOrVYb1+T3S12YSNulz2iubHFjua8MQF5+E8h3svHOeyqQy1a7W52KTtX7PS6I7oNA66SOEaDW7Ic3gnOuJVqxVW927+m7YUex0bWStttr37S0dHx8I2UCj2tLAWkgFvyXAYOAzAOSr62WQ4ann8S3qYZanbOs3hsJ5WXmltSKgircCK7Qd6gnZ3LJK6sV9H2AQ3qOe8vdfc55BcTda3YAMmjYrKtV1LZkrZsxVSoqnfO227tvgW1UXHzlpg/rpfpv9or0OpHkZfG97/IemjjD9Xh9RVNL5t7NOI+T7UZ9VaZrFkuUymxpaVGEHQN97MqqWmW/wBEaMT8NP7fVlQK8xEjUyLJAVIgC56VxpFaaVQbLowIC5RLLAFyRKw15A7BNQJliIKaKZBEpisOHICxe0If2iDpY/bCjP4Huf4LcP8Ayx3r8nvNpkLWOc1t4taSGjAuIFQ0HlyXnopOSTdj2M5OMW0r9xx1s0lpOZjnMhFmjDS4ucePQCpArjWnzRzrqQo4SnJKUsp+X+8Ti1K+PqxbjHIVr59P+8DR1AtD5LKXSPc88I4Vc4uNKNwqVR0lCMa1oq2Y0dEVJTw95Nt3ek6RYDqCQAkAfOOmfh5fpv8AaK9BqR5KXxPewtNZw/V4fUVTS+beacR8n2oz6q0zFoBWIpZq6Uyg6BvvZlVS0y3+iLsT8NP7fVlJqvMbJAUyIznouCiV5ZVFstjErlygW2BLkDSGqkOw4TBkrAmiDZOclIqGQMcIEXtCfvMHSx+2FGfwvc/wW4f+WO9fk9/XnT2RU0v8BN0cnsFW0f5I71+Sqv8AxS3P8GD3OP3Q9I/1NWzpT+fwRzuhf/W8WdSucdYSAEgD5w018PJ9N/tFeg1I8m/ie9h6azh+rw+oqml829mjEaIfajOVpnLoVhmZp6Vyg6BvvZlXS0y3+iNGJ+Gn9vqymFcYmM5yBpEMkii2WRiVyaqJbawzigaQKQxAIBkgCZAnjapIrkx3JiQkAEECZd0H+8wdLH7YUZ/A9z/BbQ/ljvX5PoBedPZFTS/wE3RyewVbR/kjvX5Kq/8AFLc/wYPc4/dD0j/U1bOlP5/BHO6F/wDW8WdSucdYoSaYha57XEi5QF10ltT8UEDMVHWFcsPNpNayh4mmpNPUWbNamyVukmmdWub6wFXKDjpLITjLQfO+mh+uk+m/2iu9qR5V/HLe/wAhaazh+rw+oqml82804jRD7UZ9FcZS9RWGdmlpTKDoG+9mVVLTLf6I0Yn4af2+rKDnK4ypEL3qLZNIgcVEtSsMUACkSFRAEjWpkGyRrU0RbJqKRWCgY4CACTIlvQf7zB0sfthVz+F7n+C+h/JHevyfQK88exKml/gJujk9gq2j/JHevyVV/wCKW5/gwe5x+6HpH+pq2dKfz+COd0L/AOt4s6lc46xyukhLdlJinYwvYWNaYA0AmMuqA6t4vvmvKF0aWRePvJuzv8Xf3bLHKq9Zky92SV1ZLJ7r69N7m7o5zqEOZKNtZTGa12Dg3Hdt3rHVS0prwv6o30XLOmn429GeAaZH66T6b/aK7mpHlpP33vYWmhjD9Xh9RVNL5t7NWJfwfaihRXGS5dAU0UM0NLnCDoG+9lVVLTLf6I1Yhe7T+31ZluKsuZ0iFxUSxIaiBjUQMVEAExiERbJA1SIkrGJorbCITECQkMcNTEEQgEHo6cRzRyOrRj2ONM6NcCadShJXTW8upSyZKWxo9RHdMsni5/NZ21zOz5/UvPkd3tel9L8uYz+6RYyCDFMQRQgtYQQcwRfTWAqJ3Ul58hPpai1ZxflzI7L3QbDGLsdnlY2taNjjaK76Byc8FVm7ykm/HkRh0nh6atCDS7kuZL/iXZPFT+aztqPZ8/qXnyJ9r0vpflzGk7o1jcKOhmIwwLWbDUfH3hNYCondSXnyIy6WoNWcX5cw/wDEmy+Kn81nbR2dU2rz5B2zR2Py5nlGkHh73uGTnOI30JqunayscNyvJsl0y3GHoIfUVRS+bezZiX8H2oohquMlyy4qZSW9MnCDoG+9mVFLTLf6I2Yj4af2+rMtxVhSgQEBcVEDFRILiDUBclDVIhcNrUyLZLRSK7iokO411AXCDUxXGeEhogcFEsRV0hO6MR3KVfI1mOVHVVFepKCjk62lxNeEpQqueXe0YuWbuH75cHmN1LwYZK5spWgrka1qjrZKThLTa/cHUQlBVIfDlKNtd9O4sB5yJFcBkc7taHca7DsKtyn/ALd/vAoyFpSflt883mMyQ/GIyDsnZDwzjsoQkpvX6+I5U4/KnptpWl6CZrnVwu0vU+NkMHY76qWU+7/afMrcI2s73t3eHkPefvZTi7Hf1eo05ksqe1efiGTT2S17PD9jvarCpMtaYbjF0EXqKopL4t7N2Kfwfaig1qusZGwnFAi3pnKDoG+9mVFLTLf6I24j4af2+rM4BXGa49ECGogdxUSC4cbE0iMpEgapELkzGJpEHIItTI3EWoHcQaiwrjlqBXI3hJk0yO6lYncJ9nDhQ9eFQd4qMDypSgpKzCFZwldFdmi2BxdVxJaWGpHgk12BVLDRTcru7VvA0Sx9RxUbJJPKzX08SyLK3HDAkEjYSAAD1AdSt6tGf2iSS7tevPn5j96DAEk0BbjTwTSrcuQY58qOrWj/AFv94i9oabaS038dv+zdxILKMeU3vLh+AUurRF4iXlb88wu9W7vzj2j1o6qJH2if+8OSGcxSsJSLOl2YxdDF6is9FfFvZuxTzQ+1FEMV1jI2QOUS0vaXGEHQN97MqaWmW/0RrxD92n9vqyhdVxkuINQFx7qAuO1iLCbJQ1SsQuG1idiLZMGpldxXUBca6gLhBqYriLUAmRuakTTEGIsDkTXE7FdxXEWDKHDE7CuOGIsFx7qLCuPdTABzUrEkyzpVuMfQxepZ6Kzy3s3Yt5qf2opBiusY8opXVWaWzR0q3CDoG+9mVVLTLf6I04l+7T+31ZQuq4yXCupiuNdQFyVrE7EHIMNTFckYxNIg2FdTI3FRAXEGosFwg1AriLUBcAtRYlcdjECbJriZEcMQAVxABBiBjFiBCDEDBcxAixpNmMfRRepUUdMt7NuLean9qKlxXmIz7iqsamzQ0o34HoG+8lVVLTPf6I04l+5T+31ZSuK6xkuK6iwrhsYnYTkHdTsQuE1idhNktxMiPcQAgxADhqACuoAa6gBXEAOxiBklxAWHuoAMMQOw91IBrqYWFcSAZzUwJ9IsxZ0UXsqmj829mnFaKf2L1Kt1XGUzbqgWtl/SbcIegb7yVU0dM9/ojVin7lL7fVlK6rzHccNRYTZKGKRFsdrEATMYgB7qAHLUAINQAQagLBFqBjXUAK6gAmsQFggEhj3UAE0IAYoAZADhqAFdQBYtzcWdFF7KqpfNvZpxOin9i9SqArTMZl1IGy9pIfA9C33kqpo/FPf6I14r4KX2+rKgYtFjFcNrECJGhABhqBhBqBjhqAHISAcNQA4agY5YgAS1ADgIANoQMe6gLDoAYBADhqAsPQIAZABAJDsWLa3FvRR+yqqXzb2acSs0PtXqVlcZjKuJ2K7l7SI+C6FvvJVRR+Ke/wBEa8V8FL7fVldrFeYwqIGExiASJQ1IkFRACAQAqIAcBAw6IAYtQFhqBADhAD0QMdIBUQA4QMQQAqIFYeiB2EGouPJZYtoxb0cfsqml829mjE6IfavUqlXGVmeRzKRWXLePguhb7yVU0dM9/ojXifgpfb6srtarjISXUhhNCBjoAcIAK6gdh2tQNIJIYJTEJACqgQTUiSCLUrkrDJizBNalcaiPcSuPJGup3FYVEAEkSFQICyJrZm3o4/ZVVL5t7NGI0Qt9KIFaZjNAVhnNC0Rh4jIfFhG1pDpY2kEPkJBDnA7R1rNGeRKV09Ox7EdCpSdWFNxazRtpS1vayPvb+ZD6eLtKfXR7+D5FPss9sf7R5i72/mQ+ni7SOuj38HyD2We2P9o8wuA/mQ+ni7SOuj38HyH7LPbH+0eYuB+fD6eLtI66PfwfIPZZ7Y/2jzHFnHjIfTxdpHXR7+D5B7LPbH+0eYRiHjIfTxdpLro9/B8h+zT2x/tHmMIP5kPp4u0n10e/g+QvZp7Y/wBo8x+9/wCZD6eLtJddHv4PkP2ae2P9o8x+BHjIfTxdpHXR7+D5B7NPbH+0eYuBHjIfTxdpHXR7+D5B7NPbH+0eYuAHjIfTRdpHXR7+D5B7NLbH+0eY4hHjIfTxdpHXR7+D5DWGltj/AGjzC4IeMi9NF2kuuWx8HyH7PLav7R5iEQ8ZD6aLtI65d/B8g9ne2P8AaPMfgx4yH00XaR1sdj4PkPqJbY/2jzG4MeMh9PF2kdbHY+D5C9nltX9o8xGMeMi9PF2kdau/g+Qezy2r+0eY3BDxkPpou0n10e/g+QvZ5bY/2jzDaweMi9PF2lF1Y9/B8iaoS2x/tHmOWN8bD6eLtI62PfwfIboPav7R5itTmkijmuoxgJa4OFQMRUYFFL5n3sMQleCvoilmzkeCtKMxntad6mZgxXegYQJ2IHdjVO1AXYV9FguPf5UrDuECgdxAoAK8iw7iLt5RYG9pW0hbxEAS0mtcqbOdVVJqGdl1Kk6t0nYoy6wxta15a7jNkd8Wv6twa4EA4GpGBxxG3BVSxUEk2tvkaIYCpKTimszS168+z/bs5Zm02xj7hD73FGAbTjNDsDWmRTlXipZNghhJyhl3Vv8ALYaDjTatBieYdpKBpscFIdxFxRYG2CKpkc4ZJSzE86EDyoBMcuSsNsQKAuOgYJCCNikxisM9h6IAMc6CQ14bECH8iBhBvIkOwTWoGkOUgdxUQFgSExAT2YOpe2Go6iPvUZJSLISlC9tYBsDCA2hwaWjfRxBz8mCr6qNi1V53uttyJ+hoiahpGyjTQAXODFBkKNyUXQg/93WLI4qqs2n/AO3/ACX7quuZckdA9wxciwrhNQSQ4CQ0hXUXHkioEBZDEIE0wgEDsMgBXUXFYqOKsM7Ym5IAEoEOwYoGtJI0pEk7BFxolYbbsJibCIb1FEpAtCbEgQUyK0hhIkEEiaBKZF6RFAahbEaxag2JMnEc5JDegAFMhckSLAQgitIQOKCWsNImMUCYKZE//9k= + input1 (text): ■自社: 親子でのスマE料節紁E親子でのお得感 チEEタの余剰利用 通話とネットEコストパフォーマンス スマEチEュー支援 家族向けE安E機E 豊富な端末ラインアチEE ■競合他社: 22歳までのお得なプラン 大好評Eサービス 親子でお得にスマEを利用 22歳以下限定Eお得なキャンペEン 学生向けEお得さ 青春年齢向けのお得なプラン 低価格で高品質な通信サービス 格安SIMとスマEの利便性 22歳以下限定E割引キャンペEン スマEチEュー応援 家族割引との絁E合わせでの最安値 料プランの多様性 親子でのお得な割引サービス スマEチEューのお得さ 特別割弁EチEEタ3GB提侁E割引サービスによるコスト削渁E新規契紁Eプラン変更による特典 機種代と基本料Eダブル割弁E大容量データ エントリー制の特典シスチE 24時間ぁEでもオンラインで手続き可能 家族E員が割引を受けられるサービス 家族間の無料通話サービス プライムビデオ特典 22歳までの長期利用可能 製品ラインナップE允EE期間限定EキャンペEン 人気スマEの割引販売 安E教育サービス 話題EスマEが安く手に入めE詳細なサポEトとFAQ シンプルな料プラン 家族E員の料割弁E子育てサポEトサービス 業界トレンド:「◯◯◯◯◯」「◯◯◯◯◯」「◯◯◯◯◯」が吁E共通する訴求コンチEチEある、E60字程度) + input2 (text): sometext + input3 (text): default + input4 (text): default + input5 (text): gpt-4o + output1 (json): 頁E + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + selected_model = model if model else "meta-llama/Llama-3.3-70B-Instruct" + + # Handle API key based on model + if selected_model and "gemini" in selected_model.lower(): + if gemini_key and gemini_key != "default": + api_key = gemini_key + else: + api_key = os.environ.get('GEMINI_KEY') + client = LLMClient(google_api_key=api_key) + else: + if openai_key and openai_key != "default": + api_key = openai_key + else: + api_key = os.environ.get('OPENAI_KEY') + client = LLMClient(openai_key=api_key) + + system_prompt = f"""与えられた情報と質問に対して、採点基準を参Eして以下を日本語で回答します、Ecitation:当該箁Eの引用 +suggestion:満点でなぁE合E満点になるよぁE具体的な持E、Ereason:高得点の場合E優れた点をE体的な叙述 +出力E忁E、すべてのカチEリと頁Eを含めてください、E +{framework_p} +""" + + result = client.call( + prompt=p, + schema=EvaluationModel, + model=selected_model, + system_prompt=system_prompt, + images=[base64img], + temperature=0, + ) + + return result.model_dump() \ No newline at end of file diff --git a/apis/fvinfo2winningrate_nolift.py b/apis/fvinfo2winningrate_nolift.py new file mode 100644 index 0000000000000000000000000000000000000000..ff546021227b994ee709efeff27f43a70129901c --- /dev/null +++ b/apis/fvinfo2winningrate_nolift.py @@ -0,0 +1,126 @@ +import os +from src.clients.llm_client import LLMClient +client = LLMClient()) +from pydantic import BaseModel, conint +from enum import Enum +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class reason(BaseModel): + choice: str + content_description: str + contribution: int + reason: str + recommend: str + compliance_score: float + +class win_or_lose(str, Enum): + win = "勝ち" + lose= "負ぁE + +class testpattern_win_or_lose(BaseModel): + testpattern: win_or_lose + possibility: float + reasons: list[reason] + +@customtracer +def fvinfo2winningrate_nolift(img1, img2): + """ + input1 (text): メインコピ�E: 重たぁE�Eトルを持ち上げなくてOK�E�E 天然水ウォーターサーバ�E牁E��でラクラク♪, 1日1人あためE7.5冁E��E 天然水ウォーターサーバ�E売丁ENo.1, ウォーターサーバ�E契紁E��E��増数 No.1、ETAボタン: なし。ビジュアル: ウォーターサーバ�Eの写真, ウォーターサーバ�EのボトルをセチE��してぁE��女性の写真, 驚いてぁE��女性の写真, 牁E��でラクラクと書かれた吹き�Eし�EイラスチE 重たぁE�Eトルを持ち上げなくてOK!と書かれたイラスト。権威付け: 天然水ウォーターサーバ�E売丁ENo.1, ウォーターサーバ�E契紁E��E��増数 No.1、E + input2 (text): メインコピ�E: 選ばれてNo.1「安忁E��水」にこだわる人たちに, 選び抜いた日本の銘水, 使ぁE�Eりで衛生皁E 徹底した水質管琁E ※1�E�天然水宁E�E市場におけめE015年売上げ、総合企画センター大阪調べ、ETAボタン: なし。ビジュアル: 水を飲んでぁE��女性の写真, 水のイラスチE 葉�Eイラスト。権威付け: なぁE + output1 (json): CVRと説昁E + """ + print("fvinfo2winningrate_nolift") + + p = "以下に2つのWEBペ�EジのFVの冁E��を�E挙します。テストパターンの勝敗を予想し予想の精度(possibility)と、勝敗�E琁E��めEつ述べてください、En\n#オリジナル\n" + img1 + "\n\n#チE��トパターン\n" + img2 + + response = _ask_raw_hf([{"role":"user","content":p}], model, + #model="ft:gpt-4o-2024-08-06:dlpo-inc::9yywbtA2", #gp4-oにアチE�EチE�Eト、E0件の金融→�Eて-120%・全て負ぁE + #model="ft:gpt-4o-2024-08-06:dlpo-inc::9zMmgJhD", #80件の金融。引き刁E��除く。�E1件だけ差がつく�E全て負ぁE + #model="ft:gpt-4o-2024-08-06:dlpo-inc::9zNF7KD5", #60件学習、E0件検証。引き刁E��除く。�E3件差がつぁE��・全て負ぁE + #model= "ft:gpt-4o-2024-08-06:dlpo-inc:no-lift:9zNTJu9j", #さらにリフトなぁE + model= "ft:gpt-4o-2024-08-06:dlpo-inc:without-color-ratio:A1RIpaUj", #さらに色割合削除 + + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": """WEBペ�EジのFVの冁E��を比輁E��て、テストパターンの勝敗を予測し、勝因を記述します。以下�E頁E��で常体�E言ぁE�Eり�E日本語で記載してください、E +STEP1: possibilityには、その勝敗予測が合ってぁE��確玁E��0~100の間で入れてください。�E容が同一の場合�EpossibilityめEにしてください、E +STEP2: 勝因に特筁E��べきものがあれ�Epossibilityに3を加えてください +STEP3: possibilityに応じて、reasonの強さを「優れてぁE��、E「すると良ぁE��E「可能性がある」�Eように表現を変えてください、E +STEP4: オリジナルと差刁E��なければ、理由には特に何も書かなぁE��ください、E +STEP5: チE��トパターンに値がなぁE��合�E、contributionめEにして何も提案しなくてよいです、E +STEP6: 最後に斁E��を「だ・である調」に統一し、句点は除去してください +STEP7: complianceに違反するリスクのある表現めE~1で判定します。�E皁E��業として問題�Eある表現ぁEつでも含まれればcompliance_scoreぁEに近くなるよぁE��スコアして、E +""" + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": p + } + ] + } + ], + response_format=testpattern_win_or_lose, + temperature=1.2, + top_p=1, + frequency_penalty=0, + presence_penalty=0 + ) + return response \ No newline at end of file diff --git a/apis/heatimage2score.py b/apis/heatimage2score.py new file mode 100644 index 0000000000000000000000000000000000000000..f6dd729d3b03ba8a17aa8dd4a6e4470830e18423 --- /dev/null +++ b/apis/heatimage2score.py @@ -0,0 +1,60 @@ +from matplotlib.colors import LinearSegmentedColormap +import numpy as np +from matplotlib.colors import to_hex,to_rgb +from PIL import Image, ImageDraw, ImageFont, ImageStat, ImageFilter +from datetime import datetime +import pytz + +cmap = LinearSegmentedColormap.from_list("custom_heatmap", ["black","purple", "blue", "green", "yellow", "orange", "red"], N=100) +colors = cmap(np.linspace(0, 1, 256)) +hex_colors = [to_hex(c) for c in colors] +color_score_cache = {} +for color in colors: + hex_color = to_hex(color) + closest_index = np.argmin([np.linalg.norm(np.array(to_rgb(x)) - color[:3]) for x in hex_colors]) + value = 0 + (100 - 0) * (closest_index / 255) + color_score_cache[hex_color] = value + +def heatimage2score(img_path): + """ + input1 (image_filepath): + output2 (number): 相違点 + """ + # 画像を開いてRGBに変換 + img = Image.open(img_path) + img = img.convert('RGB') + + # 画像をnumpy配Eに変換 + pixels = np.array(img) + + # スコア用の配EをE期化 + scores = np.zeros(pixels.shape[:2], dtype=np.float64) + + # RGBごとにスコアを割り当て + for i in range(pixels.shape[0]): # 行ごとに処琁E + for j in range(pixels.shape[1]): # 列ごとに処琁E + pixel = pixels[i, j] / 255.0 # 正規化 + if np.all(pixel == 1.0): # 白色の場吁E + scores[i, j] = 0 + else: + hex_color = to_hex(pixel) + if hex_color in color_score_cache: + scores[i, j] = color_score_cache[hex_color] + else: + # キャチEュになぁEの場合、スコアを計算してキャチEュに追加 + closest_index = np.argmin([np.linalg.norm(np.array(to_rgb(x)) - pixel) for x in hex_colors]) + value = 0 + (100 - 0) * (closest_index / 255) + color_score_cache[hex_color] = value + scores[i, j] = value + + # スコアから忁Eな値を計箁E + max_temperature = np.max(scores) # 最大スコア + total_temperature = np.sum(scores) # スコアの総合 + average_temperature_per_area = np.mean(scores) # 平坁Eコア + colored_area = scores[scores > 0] # 色付きエリアを抽出 + if colored_area.size > 0: + average_temperature_per_colored_area = np.mean(colored_area) # 色付きエリアの平坁Eコア + else: + average_temperature_per_colored_area = 0 # 色付きエリアがなぁE合E0 + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%m-%d %H:%M:%S"), "heatimage2score", len(color_score_cache.keys())) + return int(average_temperature_per_area) \ No newline at end of file diff --git a/apis/heatmap_text2comment.py b/apis/heatmap_text2comment.py new file mode 100644 index 0000000000000000000000000000000000000000..4c8f0c588f63e6c1eb85bd6f92a3a27e9895cea3 --- /dev/null +++ b/apis/heatmap_text2comment.py @@ -0,0 +1,106 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +from enum import Enum + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +client = LLMClient() + +class Comment(BaseModel): + コメンチE str + 琁E��: str + チE��スチE str + チE��スト�E種顁E str + +def ask_raw(messages, model): + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=Comment, + temperature=0 + ) + return response + +def heatmap_text2comment(p, fv_info1,fv_info2,title1, title2, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text): + input2 (text): + input3 (text): + input4 (text): + input5 (text): + input6 (text): default + output1 (json): コメンチE + """ + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + messages = [ + { + "role": "system", + "content": f"以下�E、�E析を進めてぁE��LPの冁E��です、Enこ�ELPの惁E��を�Eに、LP刁E��の専門家としてコメントしてください、En\n#{title1}\n{fv_info1}\n\n#{title2}\n{fv_info2}" + }, + { + "role": "user", + "content":[ + {"type": "text", "text":p} + ] + }, + ] + return ask_raw(messages, "meta-llama/Llama-3.3-70B-Instruct") \ No newline at end of file diff --git a/apis/html2variants.py b/apis/html2variants.py new file mode 100644 index 0000000000000000000000000000000000000000..e1a806e431ba97f31ad4119cb16ff1d7561f7b7a --- /dev/null +++ b/apis/html2variants.py @@ -0,0 +1,159 @@ +""" +HTMLバリアント生成API +允E�EHTMLと変更点を受け取り、Eつの新しいHTMLを提案すめE +""" + +import os +from src.clients.llm_client import LLMClient +import json +import re +from typing import List +from pydantic import BaseModel, Field +from datetime import datetime +import pytz +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + + +class HtmlVariant(BaseModel): + html : str + description: str + changes: list[str] + +class HtmlVariantsResponse(BaseModel): + variants : list[HtmlVariant] + +def get_openai_request(messages, format, openai_key): + """OpenAI APIを呼び出ぁE"" + client = LLMClient() + response = _ask_raw_hf([{"role":"user","content":p}], model, + model="meta-llama/Llama-3.3-70B-Instruct", + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=format, + temperature=0.7 # バリエーションを�Eすため少し高めに設宁E + ) + return response + + +@customtracer +def html2variants(original_html: str, change_points: str, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (text):

title

+ input2 (text): タイトルに下線を表示 + input3 (text): default + output1 (json): html + """ + print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), __name__) + + if openai_key == "default" or not openai_key: + openai_key = os.environ.get('OPENAI_KEY', '') + + if openai_key: + os.environ['OPENAI_API_KEY'] = openai_key + + # HTMLの構造を簡潔に要紁E��長すぎる場合�E要紁E��E + html_summary = original_html[:5000] # 最初�E5000斁E��を使用 + if len(original_html) > 5000: + html_summary += "\n\n[以下省略...]" + + # プロンプトを構篁E + prompt = f"""以下�E允E�EHTMLと変更点の説明を基に、指定数のHTMLバリアントを生�Eしてください、E + +【�EのHTML、E +{html_summary} + +【変更点の説明、E +{change_points} + +【要件、E +1. 允E�EHTMLの構造とスタイルを可能な限り維持すめE +2. 変更点の説明に基づぁE��、指定数のHTMLバリアントを生�Eする +3. バリアント�E完�EなHTMLドキュメントとして返す�E�E!DOCTYPE html>からまで�E�E +4. 画像パス、CSSパス、JavaScriptパスなどは允E�EHTMLからそ�Eまま維持すめE +5. 変更するのは主にチE��ストコンチE��チE��、その周辺のチE��イン�E�フォントサイズ、色、レイアウト、余白、スタイルなど�E�でぁE +6. 変更点周辺のチE��インを変更することで、より効果的な表現を実現してください +7. ただし、変更点以外�E部刁E��ロゴ、ナビゲーション、フチE��ー、その他�Eセクション�E��EチE��インは維持してください +8. 允E�EHTMLのすべての要素�E�ESS、画像、JavaScript、メタタグなど�E�を完�Eに保持する + +【�E力形式、E +- variants: 持E��数のHtmlVariantオブジェクト�EリスチE +- HtmlVariantには以下を含める: + - html: 完�EなHTMLドキュメンチE + - description: こ�Eバリアント�E説明(変更点の要紁E��E0斁E��程度�E�E + - changes: 具体的な変更点のリスト(各頁E��は30斁E��程度�E�E +""" + + messages = [ + { + "role": "system", + "content": """あなた�EHTMLの専門家です。�EのHTMLと変更点の説明を基に、指定数のHTMLバリアントを生�Eしてください、E +バリアント�E完�EなHTMLドキュメントとして返し、�EのHTMLの構造とスタイルを可能な限り維持しながら、変更点を反映してください、E +変更点周辺のチE��イン�E�フォントサイズ、色、レイアウト、余白、スタイルなど�E�も変更可能です、E"", + }, + { + "role": "user", + "content": prompt + }, + ] + + try: + result = get_openai_request(messages, HtmlVariantsResponse, openai_key) + return result + except Exception as e: + print(f"[html2variants] エラー: {e}") + import traceback + print(traceback.format_exc()) + # エラー時�E空のレスポンスを返す + return HtmlVariantsResponse(variants=[]) + diff --git a/apis/image2color.py b/apis/image2color.py new file mode 100644 index 0000000000000000000000000000000000000000..96540196ffb37554f61c2ecdcf6eea6e143eb231 --- /dev/null +++ b/apis/image2color.py @@ -0,0 +1,67 @@ +from PIL import Image +import numpy as np +import os + +def quantize_color(color, levels=8): + """色を量子化する関数。各色成Eを指定されたレベルに近似します、E"" + return (color // levels) * levels + +def image2color(image, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (image): + input2 (text): + output1 (json): サンプル色 + """ + grid_unit=8 + quant_levels=16 + grid_size=(grid_unit, grid_unit) + # 画像をRGB形式に変換し、numpy配Eに変換 + image = image.convert('RGB') + data = np.array(image) + + rows, cols, _ = data.shape + row_height = rows // grid_size[0] + col_width = cols // grid_size[1] + + all_colors = [] + + # 吁EチEュをE琁E + for i in range(grid_size[0]): + for j in range(grid_size[1]): + # メチEュを抽出 + row_start = i * row_height + row_end = (i + 1) * row_height if i < grid_size[0] - 1 else rows + col_start = j * col_width + col_end = (j + 1) * col_width if j < grid_size[1] - 1 else cols + + mesh = data[row_start:row_end, col_start:col_end] + # 色の量子化を適用 + quantized_mesh = quantize_color(mesh, levels=quant_levels) + # 色の出現回数をカウンチE + mesh_colors, counts = np.unique(quantized_mesh.reshape(-1, 3), axis=0, return_counts=True) + all_colors.append((mesh_colors, counts)) + + # 色チEEタを統吁E + color_dict = {} + for colors, counts in all_colors: + for color, count in zip(colors, counts): + hex_color = f"#{color[0]:02X}{color[1]:02X}{color[2]:02X}" + if hex_color in color_dict: + color_dict[hex_color] += count + else: + color_dict[hex_color] = count + + # ソートしてトップN色を取征E + sorted_colors = sorted(color_dict.items(), key=lambda x: x[1], reverse=True) + total_pixels = sum(color_dict.values()) + top_colors = sorted_colors[:5] + + # 最終的なカラーチEEタを作E + final_colors = {} + color_names = ["base_color", "main_color", "accent_color1", "accent_color2", "accent_color3"] + for i, (hex_color, count) in enumerate(top_colors): + name = color_names[i] if i < len(color_names) else f"additional_color_{i-len(color_names)+1}" + ratio = (count / total_pixels) * 100 + final_colors[name] = {'code': hex_color, 'ratio': f"{ratio:.2f}%"} + + return final_colors \ No newline at end of file diff --git a/apis/image2inpaint.py b/apis/image2inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..cfc7f6a51bcd0e5b78102d1f641ea71f073a85d0 --- /dev/null +++ b/apis/image2inpaint.py @@ -0,0 +1,66 @@ +""" +image2inpaint: 画像の一部を変更(Inpainting)。 +HF Inference API の image-to-image (inpainting) タスクを使用。 + +NOTE: FLUX.1-dev はオープンなインペインティングAPIを提供していない。 + runwayml/stable-diffusion-inpainting を使用。 + 画像・マスクは base64 PNG として受け取る。 +""" + +import base64 +import os +from io import BytesIO +from typing import Optional + +from PIL import Image + +from src.utils.tracer import customtracer + + +@customtracer +def image2inpaint( + base64image: str, + base64mask: str, + p: str, + gcp_key: str = "default", + model_name: Optional[str] = None, +) -> str: + """ + input1 (text): base64エンコードされた元画像 + input2 (text): base64エンコードされたマスク画像(変更したい領域を白、保持したい領域を黒) + input3 (text): 変更内容のプロンプト + input4 (text): default + output1 (text): 生成した画像のbase64文字列 + + NOTE: HF版ではgcp_keyは使用しない。HF_TOKENを使用。 + """ + from huggingface_hub import InferenceClient + + hf_token = os.environ.get("HF_TOKEN") + if not hf_token: + raise ValueError("HF_TOKEN is required for image2inpaint.") + + model = model_name if model_name and "gemini" not in model_name else "runwayml/stable-diffusion-inpainting" + + def _decode_b64_image(b64: str) -> Image.Image: + if "," in b64: + b64 = b64.split(",", 1)[1] + data = base64.b64decode(b64) + return Image.open(BytesIO(data)).convert("RGB") + + image = _decode_b64_image(base64image) + mask = _decode_b64_image(base64mask) + + if image.size != mask.size: + mask = mask.resize(image.size) + + client = InferenceClient(token=hf_token) + result = client.image_to_image( + image=image, + prompt=p, + model=model, + ) + + buf = BytesIO() + result.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") diff --git a/apis/image2inpaint3.py b/apis/image2inpaint3.py new file mode 100644 index 0000000000000000000000000000000000000000..a35507591b67c38f95b496a29ee3ac0513471153 --- /dev/null +++ b/apis/image2inpaint3.py @@ -0,0 +1,62 @@ +""" +image2inpaint3: 画像のインペインティング(バリアント)。 +HF Inference API の image-to-image を使用。 +""" + +import base64 +import os +from io import BytesIO +from typing import Optional + +from PIL import Image + +from src.utils.tracer import customtracer + + +@customtracer +def image2inpaint3( + base64image: str, + base64mask: str, + p: str, + gcp_key: str = "default", + model_name: Optional[str] = None, +) -> str: + """ + input1 (text): base64エンコードされた元画像 + input2 (text): base64エンコードされたマスク画像 + input3 (text): 変更内容のプロンプト + input4 (text): default + output1 (text): 生成した画像のbase64文字列 + + NOTE: HF版ではgcp_keyは使用しない。HF_TOKENを使用。 + """ + from huggingface_hub import InferenceClient + + hf_token = os.environ.get("HF_TOKEN") + if not hf_token: + raise ValueError("HF_TOKEN is required for image2inpaint3.") + + model = model_name if model_name and "gemini" not in model_name else "runwayml/stable-diffusion-inpainting" + + def _decode_b64_image(b64: str) -> Image.Image: + if "," in b64: + b64 = b64.split(",", 1)[1] + data = base64.b64decode(b64) + return Image.open(BytesIO(data)).convert("RGB") + + image = _decode_b64_image(base64image) + mask = _decode_b64_image(base64mask) + + if image.size != mask.size: + mask = mask.resize(image.size) + + client = InferenceClient(token=hf_token) + result = client.image_to_image( + image=image, + prompt=p, + model=model, + ) + + buf = BytesIO() + result.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") diff --git a/apis/image2text.py b/apis/image2text.py new file mode 100644 index 0000000000000000000000000000000000000000..d5f9aa2dda8f05e5d7a7324c641c00285799dec0 --- /dev/null +++ b/apis/image2text.py @@ -0,0 +1,57 @@ +""" +image2text: PIL Image を OCR してテキストを返す。 +HF版: VLM (Qwen2.5-VL) を使用。Google Vision API は使用しない。 +""" + +import base64 +import json +from io import BytesIO + +from src.utils.tracer import customtracer + + +def _vlm_ocr_from_pil(image, model: str = "Qwen/Qwen2.5-VL-7B-Instruct") -> str: + """PIL Image → base64 → VLM OCR。""" + from src.clients.llm_client import LLMClient + from pydantic import BaseModel + from typing import List + + buf = BytesIO() + image.save(buf, format="JPEG") + b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + + class OcrEntry(BaseModel): + text: str + y: int + size: int + + class OcrResult(BaseModel): + items: List[OcrEntry] + + client = LLMClient() + result = client.call( + prompt=( + "Extract all visible text from this image. " + "For each text block, estimate its vertical position (y, 0=top) " + "and approximate font size in pixels. Sort by y." + ), + schema=OcrResult, + model=model, + images=[b64], + temperature=0, + ) + return json.dumps( + [{"text": e.text, "y": e.y, "size": e.size} for e in result.items], + ensure_ascii=False, + ) + + +@customtracer +def image2text(image) -> str: + """ + input1 (image): PIL Image + output1 (json): OCR結果 + + NOTE: HF版は VLM ベースOCR。Google Vision API は使用しない。 + """ + return _vlm_ocr_from_pil(image) diff --git a/apis/image2types.py b/apis/image2types.py new file mode 100644 index 0000000000000000000000000000000000000000..4e47c382af11fb984a5e040a2b0c143ac3dd31c5 --- /dev/null +++ b/apis/image2types.py @@ -0,0 +1,114 @@ +import os +from src.clients.llm_client import LLMClient +import json +import base64 +from io import BytesIO +from PIL import Image +import re +from pydantic import BaseModel +from enum import Enum +from functools import cache +from src.utils.tracer import customtracer + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +client = LLMClient() + +class IntentInfo(BaseModel): + タイチE str + 詳細カチE��リ: str + 評価区刁E str + 評価箁E��: str + 評価琁E��: str + +class IntentInfos(BaseModel): + types: list[IntentInfo] + +def ask_raw(messages, model): + response = _ask_raw_hf([{"role":"user","content":p}], model, + model=model, + messages=messages, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + response_format=IntentInfos, + temperature=0 + ) + return response + +@customtracer +def image2types(image, p, openai_key=os.environ.get('OPENAI_KEY')): + """ + input1 (image): + input2 (text): #前提\n・Google検索品質評価ガイドラインを踏まえる\n・添付した画像�E検索上位に表示されてぁE��\n・画像�E検索クエリは「uq wimax 解紁E��\n・画像�Eに記載されてぁE��惁E��のみ利用し、記載されてぁE��ぁE��報は利用しない\n\n#質問\n・画像�Eの惁E��から検索クエリをgo,buy,know,doに刁E��\n・画像�Eの惁E��から刁E���E根拠となった部刁E��箁E��書きで記載\n・画像�Eの惁E��からユーザーが何を求めて検索したかを150斁E��程度で記載\n・エラーペ�Eジの場合�E、「なし」と記載\n・上記�E画像�Eに記載がある冁E��か改めてチェチE��\n\n#出力形式\n検索クエリの刁E��:\n刁E���E根拠となる情報�E�\nユーザーが求める情報�E�\n + input4 (text): default + output1 (json): 検索意図 + """ + if openai_key == "default": + os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY') + else: + os.environ['OPENAI_API_KEY'] = openai_key + messages = [ + { + "role": "system", + "content": "あなた�E優れたWEBマ�Eケターで、各種マ�EケチE��ングフレームワークを前提として、E��客忁E��に基づぁE��刁E��ができます、E + }, + { + "role": "user", + "content":[ + {"type": "text", "text":p} + ] + }, + ] + buffered = BytesIO() + image.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") + messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_str}"}}) + return ask_raw(messages, "meta-llama/Llama-3.3-70B-Instruct") \ No newline at end of file diff --git a/apis/images2inpaint.py b/apis/images2inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..9163f58484e58aff2b477a2daff8a775dcb149df --- /dev/null +++ b/apis/images2inpaint.py @@ -0,0 +1,65 @@ +""" +images2inpaint: 複数画像のインペインティング。 +HF Inference API の image-to-image を使用。 +""" + +import base64 +import os +from io import BytesIO +from typing import List, Optional + +from PIL import Image + +from src.utils.tracer import customtracer + + +@customtracer +def images2inpaint( + base64images: List[str], + base64mask: str, + p: str, + gcp_key: str = "default", + model_name: Optional[str] = None, +) -> List[str]: + """ + input1 (text): base64エンコードされた元画像リスト(JSON配列) + input2 (text): base64エンコードされたマスク画像 + input3 (text): 変更内容のプロンプト + input4 (text): default + output1 (json): 生成した画像のbase64文字列リスト + + NOTE: HF版ではgcp_keyは使用しない。HF_TOKENを使用。 + """ + import json + from huggingface_hub import InferenceClient + + hf_token = os.environ.get("HF_TOKEN") + if not hf_token: + raise ValueError("HF_TOKEN is required for images2inpaint.") + + model = model_name if model_name and "gemini" not in model_name else "runwayml/stable-diffusion-inpainting" + + # Accept JSON string or list + if isinstance(base64images, str): + base64images = json.loads(base64images) + + def _decode_b64(b64: str) -> Image.Image: + if "," in b64: + b64 = b64.split(",", 1)[1] + return Image.open(BytesIO(base64.b64decode(b64))).convert("RGB") + + mask = _decode_b64(base64mask) + + client = InferenceClient(token=hf_token) + results = [] + for b64 in base64images: + img = _decode_b64(b64) + if img.size != mask.size: + m = mask.resize(img.size) + else: + m = mask + out = client.image_to_image(image=img, prompt=p, model=model) + buf = BytesIO() + out.save(buf, format="PNG") + results.append(base64.b64encode(buf.getvalue()).decode("utf-8")) + return results diff --git a/apis/info2img64.py b/apis/info2img64.py new file mode 100644 index 0000000000000000000000000000000000000000..6d0c787863983441d768bfae6a6008b733aaf620 --- /dev/null +++ b/apis/info2img64.py @@ -0,0 +1,129 @@ +from PIL import Image, ImageDraw, ImageFont +import textwrap +import io +import base64 +import os +import json + +def draw_wrapped_text(draw, text, x, y, font, max_width, line_spacing=4, align='left'): + lines = [] + for paragraph in text.split("\n"): + current_line = "" + for char in paragraph: + trial_line = current_line + char + bbox = draw.textbbox((0, 0), trial_line, font=font) + if bbox[2] - bbox[0] > max_width: + lines.append(current_line) + current_line = char + else: + current_line = trial_line + if current_line: + lines.append(current_line) + + for line in lines: + bbox = draw.textbbox((0, 0), line, font=font) + line_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + if align == 'center': + draw_x = x + (max_width - line_width) // 2 + else: + draw_x = x + + draw.text((draw_x, y), line, font=font, fill=(0, 0, 0)) + y += text_height + line_spacing + + return y + +def draw_component(draw, comp, margin, width, sub_font, small_font, y): + y += 60 + box_top = y + # 中チEスチE + y = draw_wrapped_text(draw, comp.get("component_middle", ""), margin + 10, y, sub_font, width - 2 * (margin + 10)) + # 小テキスチE + if comp.get("UIelement") == "チEスチE: + for item in comp.get("component_small", []): + y = draw_wrapped_text(draw, f"・ {item}", margin + 20, y, small_font, width - 2 * (margin + 20)) + box_bottom = y + 10 + # 外枠 + draw.rectangle([margin, box_top, width - margin, box_bottom], outline="black", width=2) + # ラベル: 左寁EEE孁E + if comp.get("component_large"): + txt = comp["component_large"] + bbox = draw.textbbox((0, 0), txt, font=small_font) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + pad = 12 + # 左側に配置 + x0 = margin + x1 = margin + w + pad + y0 = box_top - h - pad + # ラベル背景 + draw.rectangle([x0, y0, x1, box_top], fill="black") + # 太字効极E stroke_widthを使用 + draw.text((x0 + 3, y0 + 3), txt, font=small_font, fill="white", stroke_width=1, stroke_fill="white") + return box_bottom + +# レイアウト描画と高さ計算を共通化 +def layout_and_draw(draw, fvinfo, cninfo, width, margin, fonts): + y = margin + # メインEサブコピE + for section, font in [("main_copy", fonts['main_bold']), ("sub_copy", fonts['sub_font'])]: + for line in fvinfo["キャチEコピE"].get(section, []): + y = draw_wrapped_text(draw, line["text"], margin, y + (10 if section == "sub_copy" else 0), font, width - 2 * margin) + # ビジュアル + for vis in fvinfo.get("ビジュアル", []): + # 上部マEジン + y += 20 + # プレースホルダー枠の高さ + placeholder_h = 100 + # 枠描画 + draw.rectangle([margin, y, width - margin, y + placeholder_h], outline="gray", width=2) + # プレースホルダーチEスチE + text = f"[画僁E {vis}]" + # チEストを中央寁E + text_w, text_h = draw.textbbox((0,0), text, font=fonts['sub_font'])[2:] + tx = margin + (width - 2*margin - text_w) / 2 + ty = y + (placeholder_h - text_h) / 2 + draw.text((tx, ty), text, font=fonts['sub_font'], fill="gray") + y += placeholder_h + # CTA + for cta in fvinfo.get("CTA", []): + y += 20 + draw.rounded_rectangle([margin, y, width - margin, y + 40], radius=10, fill=(255, 100, 100)) + y = draw_wrapped_text(draw, cta, margin, y + 8, fonts['sub_font'], width - 2 * margin, align='center') + y += 10 + # コンポEネンチE + for comp in cninfo.get("components", []): + y = draw_component(draw, comp, margin, width, fonts['sub_font'], fonts['small_font'], y) + return y + +def info2img64(fvinfo_json_raw, cninfo_json_raw): + """ + input1 (text): + input2 (text): + output1 (text): よりコンバEジョンが高まるWEBペEジを作るための観点を比輁Eます、E + """ + fvinfo = json.loads(fvinfo_json_raw) + cninfo = json.loads(cninfo_json_raw) + # フォント設宁E + base = os.path.dirname(__file__) + path = os.path.join(base, "../NotoSansJP-VariableFont_wght.ttf") + fonts = { + 'main_bold': ImageFont.truetype(path, 24), + 'sub_font': ImageFont.truetype(path, 14), + 'small_font': ImageFont.truetype(path, 12) + } + width, margin = 600, 20 + # 仮描画E高さ計算Eみ + tmp = Image.new("RGB", (width, 10000), "white") + d_tmp = ImageDraw.Draw(tmp) + y_end = layout_and_draw(d_tmp, fvinfo, cninfo, width, margin, fonts) + final_h = y_end + 20 + # 本描画E新規キャンバスに描画 + img = Image.new("RGB", (width, final_h), "white") + draw = ImageDraw.Draw(img) + layout_and_draw(draw, fvinfo, cninfo, width, margin, fonts) + # 返却 + buf = io.BytesIO() + img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode() \ No newline at end of file diff --git a/apis/keyword2urls.py b/apis/keyword2urls.py new file mode 100644 index 0000000000000000000000000000000000000000..4ecf8636b52e38a549598ddae35130e0afb88ea8 --- /dev/null +++ b/apis/keyword2urls.py @@ -0,0 +1,50 @@ +import requests +import json +import os +from outscraper import ApiClient +from urllib.parse import urlparse, parse_qs +from src.utils.tracer import customtracer + +def extract_u_value_if_translate(url): + # URLをパース + parsed_url = urlparse(url) + + # ホストが 'translate.google.com' かどぁEを確誁E + if parsed_url.netloc == 'translate.google.com': + # クエリパラメータを辞書形式で取征E + query_params = parse_qs(parsed_url.query) + + # 'u'の値を取得し、リストE最初E要素を返す + return query_params.get('u', [None])[0] + else: + return url + +@customtracer +def keyword2urls(query): + """ + input1 (text): レム睡眠 薬 + output1 (json): 検索結果 + """ + # 結果を格納するリスチE + #results = [] + # + # Google Custom Search JSON APIを使用して結果を取得(上佁E0件EE + #for start_index in range(1, 51, 10): # 1から始まり、E0まで10ごとにEEPIの仕様により一度に最大10件の結果しか取得できなぁEめEE + # url = f"https://www.googleapis.com/customsearch/v1?key={os.environ.get('CSE_KEY')}&cx={os.environ.get('CSE_ID')}&q={query}&start={start_index}&lr=lang_ja" + # response = requests.get(url) + # search_results = response.json() + # + # # 吁EイチEに対してURL、タイトル、説明を抽出 + # for item in search_results.get("items", []): + # results.append(item) + + api_client = ApiClient(api_key=os.environ.get('OUTSCRAPER_KEY')) + results = api_client.google_search([query], language="ja",region="JA", pages_per_query=1) + + #translate.google.comを置揁E + new_results = [] + for result in results[0]["organic_results"]: + result["link"] = extract_u_value_if_translate(result["link"]) + new_results.append(result) + + return new_results \ No newline at end of file diff --git a/apis/modifyButton.py b/apis/modifyButton.py new file mode 100644 index 0000000000000000000000000000000000000000..b8b311364552051411b9595b85a806287af03a1b --- /dev/null +++ b/apis/modifyButton.py @@ -0,0 +1,105 @@ +import os +from src.clients.llm_client import LLMClient +import json +import pandas as pd +from pydantic import BaseModel, ValidationError + +from functools import cache +from typing import List +from datetime import datetime +import pytz + +def _ask_raw_hf(messages, model, response_format=None): + """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient.""" + from src.clients.llm_client import LLMClient + import json as _json + + client = LLMClient() + system_prompt = None + user_text = "" + images = [] + for msg in messages: + role = msg.get("role", "") + c = msg.get("content", "") + if role == "system": + if isinstance(c, str): + system_prompt = c + elif role == "user": + if isinstance(c, str): + user_text = c + elif isinstance(c, list): + for part in c: + if isinstance(part, dict): + if part.get("type") == "text": + user_text += part.get("text", "") + elif part.get("type") == "image_url": + url = part.get("image_url", {}).get("url", "") + if url.startswith("data:"): + images.append(url.split(",", 1)[1] if "," in url else url) + else: + images.append(url) + + if response_format is not None and hasattr(response_format, "model_json_schema"): + result = client.call( + prompt=user_text, + schema=response_format, + model=model, + system_prompt=system_prompt, + images=images if images else None, + temperature=0, + ) + return _json.dumps(result.model_dump(), ensure_ascii=False) + else: + return client.call_raw( + prompt=user_text, + model=model, + system_prompt=system_prompt, + images=images if images else None, + ) + + +class newButton(BaseModel): + subCopy : str + HTML: str + +class newButtons(BaseModel): + Buttons: list[newButton] + +@cache +def modifyButton(p, openai_key=os.environ.get('OPENAI_KEY')): + """ + input0 (text): ボタンの斁E��を通信業界に変えて。色は若老E��け�EぁE��グラチE�Eションで、サブコピ�Eもバイブスあがる感じに。影をつけて、�Eタンは丸くして.