Renecto commited on
Commit
e732c2f
·
1 Parent(s): 962f426

deploy api_light_hf (2026-03-19 13:12:47)

Browse files
apis/base64img2component.py CHANGED
@@ -1,134 +1,134 @@
1
- from openai import os
2
- from src.clients.llm_client import LLMClient
3
- import json
4
- import pandas as pd
5
- from pydantic import BaseModel
6
- from enum import Enum
7
- import base64
8
- from io import BytesIO
9
- from PIL import Image
10
- from functools import cache
11
- from datetime import datetime
12
- import pytz
13
- from src.utils.tracer import customtracer
14
-
15
- def _ask_raw_hf(messages, model, response_format=None):
16
- """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient."""
17
- from src.clients.llm_client import LLMClient
18
- import json, re
19
-
20
- client = LLMClient()
21
-
22
- # Extract system prompt and user content from messages list
23
- system_prompt = None
24
- user_text = ""
25
- images = []
26
- for msg in messages:
27
- role = msg.get("role", "")
28
- c = msg.get("content", "")
29
- if role == "system":
30
- if isinstance(c, str):
31
- system_prompt = c
32
- elif role == "user":
33
- if isinstance(c, str):
34
- user_text = c
35
- elif isinstance(c, list):
36
- for part in c:
37
- if isinstance(part, dict):
38
- if part.get("type") == "text":
39
- user_text += part.get("text", "")
40
- elif part.get("type") == "image_url":
41
- url = part.get("image_url", {}).get("url", "")
42
- if url.startswith("data:"):
43
- images.append(url.split(",", 1)[1] if "," in url else url)
44
- else:
45
- images.append(url)
46
-
47
- if response_format is not None and hasattr(response_format, "model_json_schema"):
48
- result = client.call(
49
- prompt=user_text,
50
- schema=response_format,
51
- model=model,
52
- system_prompt=system_prompt,
53
- images=images if images else None,
54
- temperature=0,
55
- )
56
- import json
57
- return json.dumps(result.model_dump(), ensure_ascii=False)
58
- else:
59
- return client.call_raw(
60
- prompt=user_text,
61
- model=model,
62
- system_prompt=system_prompt,
63
- images=images if images else None,
64
- )
65
-
66
- class UIoption(str, Enum):
67
- element1 = "バナー/動画"
68
- element2 = "CTA"
69
- element3 = "チE��チE
70
- element4 = "フォーム"
71
-
72
- class Component(BaseModel):
73
- component_large: str
74
- component_middle: str
75
- component_small: list[str]
76
- UIelement: UIoption
77
-
78
- class Components(BaseModel):
79
- components: list[Component]
80
-
81
- def ask_raw(messages):
82
- client = LLMClient()
83
- # HF: beta.parse not available; use _ask_raw_hf instead
84
- response = client.chat.completions.create(
85
- model='meta-llama/Llama-3.3-70B-Instruct',
86
- messages=messages,
87
- top_p=1,
88
- frequency_penalty=0,
89
- presence_penalty=0,
90
- response_format=Components,
91
- temperature=0
92
- )
93
- return response.choices[0].message.content
94
-
95
- @customtracer
96
- def base64img2component(p, image64, openai_key=os.environ.get('OPENAI_KEY')):
97
- """
98
- 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: 今すぐお申し込み
99
- input2 (text): スクショ
100
- input3 (text): default
101
- output1 (json): 頁E��
102
- """
103
- print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), f"base64img2component:", image64[0:30])
104
-
105
- if openai_key == "default":
106
- os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY')
107
- else:
108
- os.environ['OPENAI_API_KEY'] = openai_key
109
-
110
- messages=[
111
- {
112
- "role": "system",
113
- "content": """
114
- 構�E要素名�Eアウト�EチE��サンプル
115
- [
116
- {"component_large":"啁E��/サービスの特徴","component_middle":"アコム", "component_small":[], "UIelement":"チE��チE},
117
- {"component_large":"FAQ/よくある質啁E,"component_middle":"よくある啁E, "component_small":["自宁E��勤務�Eに何か書類が送られてことはありますか�E�E,"家族割などの割引�Eありますか�E�E], "UIelement":"表絁E��"}
118
- ]
119
- """
120
- },
121
- {
122
- "role": "user",
123
- "content": [{"type": "text", "text":p}]
124
- },
125
- ]
126
-
127
- messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url":"data:image/png;base64,"+image64}})
128
- # OpenAI 側の認証エラーなどを�E示皁E��メチE��ージとして上位に伝搬させめE
129
- try:
130
- return ask_raw(messages)
131
- except openai.AuthenticationError as e:
132
- # API キー / 絁E��設定�E問題を含むエラー冁E��をラチE�Eして投げ直ぁE
133
- # 呼び出し�E�E�EE Origin 側など�E�でこ�EメチE��ージをキャチE��してユーザに表示できる
134
  raise RuntimeError(f"[base64img2component] OpenAI AuthenticationError: {e}") from e
 
1
+ from openai import os
2
+ from src.clients.llm_client import LLMClient
3
+ import json
4
+ import pandas as pd
5
+ from pydantic import BaseModel
6
+ from enum import Enum
7
+ import base64
8
+ from io import BytesIO
9
+ from PIL import Image
10
+ from functools import cache
11
+ from datetime import datetime
12
+ import pytz
13
+ from src.utils.tracer import customtracer
14
+
15
+ def _ask_raw_hf(messages, model, response_format=None):
16
+ """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient."""
17
+ from src.clients.llm_client import LLMClient
18
+ import json, re
19
+
20
+ client = LLMClient()
21
+
22
+ # Extract system prompt and user content from messages list
23
+ system_prompt = None
24
+ user_text = ""
25
+ images = []
26
+ for msg in messages:
27
+ role = msg.get("role", "")
28
+ c = msg.get("content", "")
29
+ if role == "system":
30
+ if isinstance(c, str):
31
+ system_prompt = c
32
+ elif role == "user":
33
+ if isinstance(c, str):
34
+ user_text = c
35
+ elif isinstance(c, list):
36
+ for part in c:
37
+ if isinstance(part, dict):
38
+ if part.get("type") == "text":
39
+ user_text += part.get("text", "")
40
+ elif part.get("type") == "image_url":
41
+ url = part.get("image_url", {}).get("url", "")
42
+ if url.startswith("data:"):
43
+ images.append(url.split(",", 1)[1] if "," in url else url)
44
+ else:
45
+ images.append(url)
46
+
47
+ if response_format is not None and hasattr(response_format, "model_json_schema"):
48
+ result = client.call(
49
+ prompt=user_text,
50
+ schema=response_format,
51
+ model=model,
52
+ system_prompt=system_prompt,
53
+ images=images if images else None,
54
+ temperature=0,
55
+ )
56
+ import json
57
+ return json.dumps(result.model_dump(), ensure_ascii=False)
58
+ else:
59
+ return client.call_raw(
60
+ prompt=user_text,
61
+ model=model,
62
+ system_prompt=system_prompt,
63
+ images=images if images else None,
64
+ )
65
+
66
+ class UIoption(str, Enum):
67
+ element1 = "バナー/動画"
68
+ element2 = "CTA"
69
+ element3 = "テキト"
70
+ element4 = "フォーム"
71
+
72
+ class Component(BaseModel):
73
+ component_large: str
74
+ component_middle: str
75
+ component_small: list[str]
76
+ UIelement: UIoption
77
+
78
+ class Components(BaseModel):
79
+ components: list[Component]
80
+
81
+ def ask_raw(messages):
82
+ client = LLMClient()
83
+ # HF: beta.parse not available; use _ask_raw_hf instead
84
+ response = client.chat.completions.create(
85
+ model='meta-llama/Llama-3.3-70B-Instruct',
86
+ messages=messages,
87
+ top_p=1,
88
+ frequency_penalty=0,
89
+ presence_penalty=0,
90
+ response_format=Components,
91
+ temperature=0
92
+ )
93
+ return response.choices[0].message.content
94
+
95
+ @customtracer
96
+ def base64img2component(p, image64, openai_key=os.environ.get('OPENAI_KEY')):
97
+ """
98
+ input1 (text): OCR text extracted from LP screenshot (long string)
99
+ input2 (text): スクショ
100
+ input3 (text): default
101
+ output1 (json): components list
102
+ """
103
+ print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), f"base64img2component:", image64[0:30])
104
+
105
+ if openai_key == "default":
106
+ os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY')
107
+ else:
108
+ os.environ['OPENAI_API_KEY'] = openai_key
109
+
110
+ messages=[
111
+ {
112
+ "role": "system",
113
+ "content": """
114
+ コンポーネント要素アウトプットのサンプル
115
+ [
116
+ {"component_large":"商品/サービスの特徴","component_middle":"アンカー", "component_small":[], "UIelement":"テキト"},
117
+ {"component_large":"FAQ/よくある質問","component_middle":"よくある質問", "component_small":["自宅外出ずに何か書類がくことはありますか?","家族割などの割引ありますか?"], "UIelement":"表組み"}
118
+ ]
119
+ """
120
+ },
121
+ {
122
+ "role": "user",
123
+ "content": [{"type": "text", "text":p}]
124
+ },
125
+ ]
126
+
127
+ messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url":"data:image/png;base64,"+image64}})
128
+ # Propagate OpenAI auth errors explicitly so caller can display message.
129
+ try:
130
+ return ask_raw(messages)
131
+ except openai.AuthenticationError as e:
132
+ # Raise RuntimeError with clear message for API key / auth issues.
133
+ # Caller (BE_Origin side) can catch and display this message to user.
134
  raise RuntimeError(f"[base64img2component] OpenAI AuthenticationError: {e}") from e
apis/baseimg2fvinfo_with_design.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  from src.clients.llm_client import LLMClient
3
- import json
4
  import base64
5
  from io import BytesIO
6
  from PIL import Image
@@ -12,6 +12,7 @@ from enum import Enum
12
 
13
  from src.utils.tracer import customtracer
14
 
 
15
  def _ask_raw_hf(messages, model, response_format=None):
16
  """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient."""
17
  from src.clients.llm_client import LLMClient
@@ -62,119 +63,131 @@ def _ask_raw_hf(messages, model, response_format=None):
62
 
63
 
64
  class Meta(BaseModel):
65
- 会社吁E str
66
- 畁E str
67
- ブランチE str
68
  サービス: str
69
- 啁E��: str
70
  タイトル: str
71
- 訴求テーチE list[str]
 
72
 
73
  class Design(BaseModel):
74
- 重要なフレーズの斁E��色を赤めE��レンジめE��ンクめE��E��などFV上で目立つ色に着色: float
75
- 背景を画像�E主要な配色と変えて目立たせる: float
76
- 四角や丸など図形で囲ぁE��認性を上げめE float
77
- アイコンを使用して視認性を上げめE float
78
- チE��スト�E重要なフレーズの下に水平なアクセント線が引かれてぁE��: float
79
-
 
80
  class sCopy(BaseModel):
81
  text: str
82
  design: Design
83
 
 
84
  class EvsF(str, Enum):
85
- EMOTIONAL = "惁E��E
86
- FUNCTIONAL = "機�E"
 
87
 
88
  class EFitems(BaseModel):
89
  item: str
90
  judge: EvsF
91
 
 
92
  class PvsS(str, Enum):
93
  PROBLEM = "問題提起"
94
  SOLUTION = "課題解決"
95
 
 
96
  class PSitems(BaseModel):
97
  item: str
98
  judge: PvsS
99
 
 
100
  class mCopy(BaseModel):
101
  text: str
102
- appeal_mode : list[EFitems]
103
- forcus_stage : list[PSitems]
104
-
 
105
  class CatchCopy(BaseModel):
106
  main_copy: list[mCopy]
107
  sub_copy: list[sCopy]
108
 
 
109
  class FvInfo(BaseModel):
110
  非LP: bool
111
  メタ: Meta
112
- キャチE��コピ�E: CatchCopy
113
  権威付け: list[str]
114
  ビジュアル: list[str]
115
  CTAボタン: list[str]
116
-
 
117
  def ask_raw(messages, model):
118
  client = LLMClient()
119
-
120
- # パラメータの準備
121
  params = {
122
  "top_p": 1,
123
  "frequency_penalty": 0,
124
  "presence_penalty": 0,
125
  "response_format": FvInfo,
126
  }
127
-
128
- # gpt-5系はtemperatureを渡さなぁE��環墁E��よって0が弾かれるためE��E
129
  model_lower = (model or "").lower()
130
  if not model_lower.startswith("gpt-5"):
131
  params["temperature"] = 0
132
-
133
- response = _ask_raw_hf([{"role":"user","content":p}], model,
 
 
134
  model=model,
135
  messages=messages,
136
- **params
137
  )
138
  return response
139
 
 
140
  @customtracer
141
  def baseimg2fvinfo_with_design(base64img, openai_key=os.environ.get('OPENAI_KEY'), p="", model="meta-llama/Llama-3.3-70B-Instruct"):
142
  """
143
- input1 (text):
144
  input2 (text): default
145
- input3 (text):
146
  input4 (text): gpt-4o
147
  output1 (json): fvinfo
148
  """
149
-
150
- print(f"baseimg2fvinfo_with_design {model} openai_key:",openai_key[-4:])
151
  if openai_key == "default":
152
  os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY')
153
  else:
154
  os.environ['OPENAI_API_KEY'] = openai_key
155
-
156
  messages = [
157
  {
158
- "role": "system",
159
- "content": "あなた�E優れたWEBマ�Eケターで、ランチE��ングペ�Eジの要素を見�Eけることに長けてぁE��す。また�EーケチE��ングの達人なので訴求テーマを言語化するのが上手です、E
160
  },
161
  {
162
- "role": "user",
163
- "content":[
164
- {"type": "text", "text":"""LPのファーストビューの画像を解析します、E
165
- ・何も書かれてぁE��ぁE���E場合�E、空の値を返し、E��LP=Trueとしてください、E
166
- ・CTAボタンが存在する場合、�Eタン冁E�E記載�E容を�E列で教えてぁE��アンカーリンクのあるチE��ストもCTAとしてください、E
167
- ・画像�Eに書かれてぁE��斁E��・コピ�Eを読み取り、LPに掲載されてぁE��頁E��に並べてください。大きい目立つ斁E��で書かれてぁE��冁E��を「main_copy」とぁE��キーで1つ抽出し、情緒�E�Eのどちらに訴えてぁE��かなどを記載、E
168
- ・main_copy以外を「sub_copy」とぁE��キーで、読み取ったテキストをtext、それぞれ�Eサブコピ�E裁E��タイプ�E適用度合いをdesignに0~1のfloatで記述
169
- ・画像�Eに写ってぁE��イメージ(写真めE��ラスチEにつぁE��、どんなも�Eが起用されてぁE��か教えてぁE��E
170
- ・画像�Eに該当�E値がなければ[]のように空の配�Eを回答し、画像になぁE��とは回答しなぁE��ください。特に黒一色めE�E色一色の場合に注意し、E��LP=Trueを返してください、E
171
- ・これら�E抽出惁E��を総合して、メタの吁E��E��を記載してください。訴求要素は、情報かOCRがある限り�E20斁E��で6種類提案してください。情報がなければ空にしてください、E
172
  """ + p}
173
- ]
174
  },
175
  ]
176
 
177
  messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}})
178
  r = ask_raw(messages, model)
179
 
180
- return r
 
1
  import os
2
  from src.clients.llm_client import LLMClient
3
+ import json
4
  import base64
5
  from io import BytesIO
6
  from PIL import Image
 
12
 
13
  from src.utils.tracer import customtracer
14
 
15
+
16
  def _ask_raw_hf(messages, model, response_format=None):
17
  """Compatibility wrapper: routes OpenAI-style messages through HF LLMClient."""
18
  from src.clients.llm_client import LLMClient
 
63
 
64
 
65
  class Meta(BaseModel):
66
+ 会社名: str
67
+ 種: str
68
+ ブランド: str
69
  サービス: str
70
+ 啓発: str
71
  タイトル: str
72
+ 訴求テーマ: list[str]
73
+
74
 
75
  class Design(BaseModel):
76
+ 重要なフレーズの装飾色を赤やオレンジやピンクなどFV上で目立つ色に着色: float
77
+ 背景を画像主要な配色と変えて目立たせる: float
78
+ 四角や丸など図形で囲い視認性を上げる: float
79
+ アイコンを使用して視認性を上げる: float
80
+ テキスト重要なフレーズの下に水平なアクセント線が引かれている: float
81
+
82
+
83
  class sCopy(BaseModel):
84
  text: str
85
  design: Design
86
 
87
+
88
  class EvsF(str, Enum):
89
+ EMOTIONAL = "情緒"
90
+ FUNCTIONAL = "機"
91
+
92
 
93
  class EFitems(BaseModel):
94
  item: str
95
  judge: EvsF
96
 
97
+
98
  class PvsS(str, Enum):
99
  PROBLEM = "問題提起"
100
  SOLUTION = "課題解決"
101
 
102
+
103
  class PSitems(BaseModel):
104
  item: str
105
  judge: PvsS
106
 
107
+
108
  class mCopy(BaseModel):
109
  text: str
110
+ appeal_mode: list[EFitems]
111
+ forcus_stage: list[PSitems]
112
+
113
+
114
  class CatchCopy(BaseModel):
115
  main_copy: list[mCopy]
116
  sub_copy: list[sCopy]
117
 
118
+
119
  class FvInfo(BaseModel):
120
  非LP: bool
121
  メタ: Meta
122
+ キャチコピ: CatchCopy
123
  権威付け: list[str]
124
  ビジュアル: list[str]
125
  CTAボタン: list[str]
126
+
127
+
128
  def ask_raw(messages, model):
129
  client = LLMClient()
130
+
 
131
  params = {
132
  "top_p": 1,
133
  "frequency_penalty": 0,
134
  "presence_penalty": 0,
135
  "response_format": FvInfo,
136
  }
137
+
138
+ # gpt-5 series: skip temperature=0 to avoid rejection in some environments.
139
  model_lower = (model or "").lower()
140
  if not model_lower.startswith("gpt-5"):
141
  params["temperature"] = 0
142
+
143
+ response = _ask_raw_hf(
144
+ [{"role": "user", "content": p}],
145
+ model,
146
  model=model,
147
  messages=messages,
148
+ **params,
149
  )
150
  return response
151
 
152
+
153
  @customtracer
154
  def baseimg2fvinfo_with_design(base64img, openai_key=os.environ.get('OPENAI_KEY'), p="", model="meta-llama/Llama-3.3-70B-Instruct"):
155
  """
156
+ input1 (text):
157
  input2 (text): default
158
+ input3 (text):
159
  input4 (text): gpt-4o
160
  output1 (json): fvinfo
161
  """
162
+
163
+ print(f"baseimg2fvinfo_with_design {model} openai_key:", openai_key[-4:])
164
  if openai_key == "default":
165
  os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_KEY')
166
  else:
167
  os.environ['OPENAI_API_KEY'] = openai_key
168
+
169
  messages = [
170
  {
171
+ "role": "system",
172
+ "content": "あなた優れたWEBマケターで、ランディングペジの要素を見けることに長けています。またーケティングの達人なので訴求テーマを言語化するのが上手です。",
173
  },
174
  {
175
+ "role": "user",
176
+ "content": [
177
+ {"type": "text", "text": """LPのファーストビューの画像を解析します
178
+ ・何も書かれていない画場合、空の値を返し、LP=Trueとしてください
179
+ ・CTAボタンが存在する場合、タン内の記載容を列で教えてくだい。アンカーリンクのあるテキストもCTAとしてください
180
+ ・画像に書かれているテキスト・コピを読み取り、LPに掲載されている順番に並べてください。大きい目立つ文字で書かれている内容を「main_copy」というキーで1つ抽出し、情緒のどちらに訴えているかなどを記載
181
+ ・main_copy以外を「sub_copy」というキーで、読み取ったテキストをtext、それぞれサブコピ装飾タイプ適用度合いをdesignに0~1のfloatで記述
182
+ ・画像に写っているイメージ(写真やイラスト)について、どんなもが起用されているか教えてくだい。
183
+ ・画像に該当値がなければ[]のように空の配を回答し、画像になとは回答しないでください。特に黒一色や白一色の場合に注意し、LP=Trueを返してください
184
+ ・これら抽出情報を総合して、メタの啓発の内容を記載してください。訴求要素は、情報かOCRがある限り20文字で6種類提案してください。情報がなければ空にしてください
185
  """ + p}
186
+ ],
187
  },
188
  ]
189
 
190
  messages[1]["content"].insert(0, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64img}"}})
191
  r = ask_raw(messages, model)
192
 
193
+ return r
apis/url2meta.py CHANGED
@@ -1,34 +1,34 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- import re
4
- import json
5
- from datetime import datetime
6
- import pytz
7
- import psutil
8
- from src.utils.tracer import customtracer
9
-
10
- def get_charset_from_header(headers):
11
- content_type = headers.get('Content-Type')
12
- charset = 'utf-8' # チE��ォルト値
13
- if content_type and 'charset=' in content_type:
14
- charset = content_type.split('charset=')[-1]
15
-
16
- @customtracer
17
- def url2meta(url):
18
- """
19
- input1 (text): https://yahoo.co.jp
20
- output1 (title): title
21
- output2 (description): description
22
- """
23
-
24
- try:
25
- response = requests.get(url, timeout=10)
26
- response.raise_for_status()
27
- response.encoding = get_charset_from_header(response.headers)
28
- soup = BeautifulSoup(response.text, 'html.parser')
29
- title = soup.find('title').text if soup.find('title') else ''
30
- description = next((meta.get('content') for meta in soup.find_all('meta', attrs={"name": "description"})), '')
31
- except requests.RequestException as e:
32
- title = ""
33
- description = ""
34
  return title, description
 
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ import re
4
+ import json
5
+ from datetime import datetime
6
+ import pytz
7
+ import psutil
8
+ from src.utils.tracer import customtracer
9
+
10
+ def get_charset_from_header(headers):
11
+ content_type = headers.get('Content-Type')
12
+ charset = 'utf-8' # fallback charset
13
+ if content_type and 'charset=' in content_type:
14
+ charset = content_type.split('charset=')[-1]
15
+
16
+ @customtracer
17
+ def url2meta(url):
18
+ """
19
+ input1 (text): https://yahoo.co.jp
20
+ output1 (title): title
21
+ output2 (description): description
22
+ """
23
+
24
+ try:
25
+ response = requests.get(url, timeout=10)
26
+ response.raise_for_status()
27
+ response.encoding = get_charset_from_header(response.headers)
28
+ soup = BeautifulSoup(response.text, 'html.parser')
29
+ title = soup.find('title').text if soup.find('title') else ''
30
+ description = next((meta.get('content') for meta in soup.find_all('meta', attrs={"name": "description"})), '')
31
+ except requests.RequestException as e:
32
+ title = ""
33
+ description = ""
34
  return title, description
apis/url2speed.py CHANGED
@@ -1,198 +1,198 @@
1
- import os
2
- import requests
3
- from functools import cache
4
- from decimal import Decimal, ROUND_HALF_UP
5
- import pandas as pd
6
- from PIL import Image
7
- import io
8
- import base64
9
- from datetime import datetime
10
- import pytz
11
- import json
12
- from src.utils.tracer import customtracer
13
-
14
- def remove_distributions(data):
15
- result = {}
16
- for key, value in data.items():
17
- result[key] = {k: v for k, v in value.items() if k != 'distributions'}
18
- return result
19
-
20
- def devide_s(metrics, division=1000, decimal=1):
21
- if isinstance(metrics, dict) and 'percentile' in metrics:
22
- metrics['percentile'] = round(metrics['percentile']/division, decimal)
23
- return metrics
24
-
25
- def get_metric_with_fallback(loading_experience, origin_loading_experience, metric_key, default_error={'percentile': -1}):
26
- """
27
- loadingExperienceからメトリクスを取得し、percentileぁEまた�E無効な場合�E
28
- originLoadingExperienceからフォールバックとして取得すめE
29
-
30
- 違い�E�E
31
- - loadingExperience: 特定�EURL�E�侁E https://povo.jp/�E�に対する実際のユーザーチE�Eタ
32
- ↁEそ�Eペ�Eジに十�EなチE�EタがなぁE��合、percentileぁEになることがあめE
33
- - originLoadingExperience: オリジン全体(侁E https://povo.jp�E��E雁E��データ
34
- ↁEドメイン全体�Eパフォーマンス傾向を示す、より安定したデータ
35
- """
36
- metric = loading_experience.get(metric_key, default_error)
37
-
38
- # percentileが有効な値�E�E0�E��E場合�Eそ�Eまま返す
39
- if metric.get('percentile', -1) > 0:
40
- return metric
41
-
42
- # percentileぁEまた�E無効な場合、originLoadingExperienceから取得を試みめE
43
- origin_metric = origin_loading_experience.get(metric_key, default_error)
44
- if origin_metric.get('percentile', -1) > 0:
45
- # categoryも含めてコピ�E
46
- result = origin_metric.copy()
47
- return result
48
-
49
- # 両方とめEまた�E無効な場合�E-1を返す
50
- return default_error
51
-
52
- def set_category(index):
53
- if index >= 5.8:
54
- return "SLOW"
55
- elif index >= 3.4:
56
- return "AVERAGE"
57
- elif index >= 0:
58
- return "FAST"
59
- else:
60
- return "ERR"
61
-
62
- def set_p_category(index):
63
- if index >= 90:
64
- return "FAST"
65
- elif index >= 50:
66
- return "AVERAGE"
67
- elif index >= 0:
68
- return "SLOW"
69
- else:
70
- return "ERR"
71
-
72
- def extract_metrics(data):
73
- loading_experience = data.get('loadingExperience', {}).get('metrics', {})
74
- origin_loading_experience = data.get('originLoadingExperience', {}).get('metrics', {})
75
- metrics = {}
76
- iferror = {'percentile': -1}
77
-
78
- # 吁E��トリクスを取得し、フォールバックロジチE��を適用
79
- lcp_metric = get_metric_with_fallback(
80
- loading_experience, origin_loading_experience, 'LARGEST_CONTENTFUL_PAINT_MS', iferror
81
- )
82
- metrics['LCP'] = devide_s(lcp_metric.copy()) #MillSec -> Sec
83
-
84
- inp_metric = get_metric_with_fallback(
85
- loading_experience, origin_loading_experience, 'INTERACTION_TO_NEXT_PAINT', iferror
86
- )
87
- metrics['INP'] = inp_metric.copy()
88
-
89
- cls_metric = get_metric_with_fallback(
90
- loading_experience, origin_loading_experience, 'CUMULATIVE_LAYOUT_SHIFT_SCORE', iferror
91
- )
92
- metrics['CLS'] = devide_s(cls_metric.copy(), 100, 2) #パ�EセンチE-> �E�桁少数点
93
-
94
- fcp_metric = get_metric_with_fallback(
95
- loading_experience, origin_loading_experience, 'FIRST_CONTENTFUL_PAINT_MS', iferror
96
- )
97
- metrics['FCP'] = devide_s(fcp_metric.copy()) #MillSec -> Sec
98
-
99
- fid_metric = get_metric_with_fallback(
100
- loading_experience, origin_loading_experience, 'FIRST_INPUT_DELAY_MS', iferror
101
- )
102
- metrics['FID'] = fid_metric.copy()
103
-
104
- ttfb_metric = get_metric_with_fallback(
105
- loading_experience, origin_loading_experience, 'EXPERIMENTAL_TIME_TO_FIRST_BYTE', iferror
106
- )
107
- metrics['TTFB'] = devide_s(ttfb_metric.copy()) #MillSec -> Sec
108
-
109
- r = remove_distributions(metrics)
110
-
111
- # categoryが存在する場合�Eそれを使用、存在しなぁE��合�E独自計箁E
112
- for key in ['LCP', 'INP', 'CLS', 'FCP', 'FID', 'TTFB']:
113
- if key in r:
114
- # 允E��E�Ecategoryを優先的に使用
115
- original_metric = metrics[key]
116
- if 'category' in original_metric:
117
- r[key]['category'] = original_metric['category']
118
- else:
119
- # categoryが存在しなぁE��合�Eみ独自計算(通常は発生しなぁE��E
120
- r[key]['category'] = 'ERR'
121
-
122
- lighthouseResult = data.get("lighthouseResult", {})
123
- try:
124
- # パスに従ってチE�Eタにアクセス
125
- speed_index = lighthouseResult['audits']['metrics']['details']['items'][0]['speedIndex']/1000
126
- ospeed_index= lighthouseResult['audits']['metrics']['details']['items'][0]['observedSpeedIndex']/1000
127
- performance_score = lighthouseResult['categories']['performance']['score'] * 100
128
- except (IndexError, KeyError, TypeError):
129
- # エラーが発生した場合�E-1を返す
130
- speed_index = -1
131
- ospeed_index= -1
132
- performance_score = -1
133
-
134
- r['speedIndex'] = {
135
- 'percentile': speed_index,
136
- 'category': set_category(speed_index)
137
- }
138
- r['observedSpeedIndex'] = {
139
- 'percentile': ospeed_index,
140
- 'category': set_category(ospeed_index)
141
- }
142
- r['performance_score'] = {
143
- 'percentile': performance_score,
144
- 'category': set_p_category(performance_score)
145
- }
146
- return r
147
-
148
- def get_screenshots_from_mobile_response(mobile_response):
149
- header, base64_image = mobile_response['lighthouseResult']['fullPageScreenshot']['screenshot']['data'].split(",", 1)
150
- full_image = Image.open(io.BytesIO(base64.b64decode(base64_image)))
151
- width, height = full_image.size
152
- left,top,right = 0, 0, width
153
- bottom = min(height, 3*width) #first viewの定義�E�E*widthピクセル未満の場合�E画像�E高さまで
154
- image_fv = full_image.crop((left, top, right, bottom))
155
- return full_image,image_fv,base64_image
156
-
157
- def fetch_mobile_response(url):
158
- endpoint = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
159
- try:
160
- response = requests.get(endpoint, params={
161
- "url": url,
162
- "strategy": "mobile",
163
- "key": os.environ.get('PAGESPEED_KEY')
164
- })
165
-
166
- # スチE�Eタスコードが200以外�E場合�EHTTPError発甁E
167
- response.raise_for_status()
168
-
169
- return response.json() # JSONをパースして返す
170
-
171
- except requests.exceptions.RequestException as e:
172
- print(f"Request failed with status {e.response.status_code}: {e.response.text}")
173
- raise e
174
-
175
- except json.JSONDecodeError as e:
176
- raise json.JSONDecodeError(f"JSONチE��ードエラー: {e.msg}, doc: {e.doc}, pos: {e.pos}")
177
-
178
- except Exception as e:
179
- raise RuntimeError(f"予期せぬエラーが発生しました: {e}")
180
-
181
- return None # エラーが発生した場合�ENoneを返す
182
-
183
- @customtracer
184
- def url2speed(url):
185
- """
186
- input1 (text): https://yahoo.co.jp
187
- output1 (json): サイト評価
188
- output2 (text): base64形式�E画僁E
189
- """
190
- print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), "url2speed", url)
191
- try:
192
- mobile_response = fetch_mobile_response(url)
193
- data = extract_metrics(mobile_response)
194
- full_image,image_fv,base64_image = get_screenshots_from_mobile_response(mobile_response)
195
- return data,base64_image
196
- except Exception as e:
197
- print(f"{url} An error occurred: {e}")
198
  raise
 
1
+ import os
2
+ import requests
3
+ from functools import cache
4
+ from decimal import Decimal, ROUND_HALF_UP
5
+ import pandas as pd
6
+ from PIL import Image
7
+ import io
8
+ import base64
9
+ from datetime import datetime
10
+ import pytz
11
+ import json
12
+ from src.utils.tracer import customtracer
13
+
14
+ def remove_distributions(data):
15
+ result = {}
16
+ for key, value in data.items():
17
+ result[key] = {k: v for k, v in value.items() if k != 'distributions'}
18
+ return result
19
+
20
+ def devide_s(metrics, division=1000, decimal=1):
21
+ if isinstance(metrics, dict) and 'percentile' in metrics:
22
+ metrics['percentile'] = round(metrics['percentile']/division, decimal)
23
+ return metrics
24
+
25
+ def get_metric_with_fallback(loading_experience, origin_loading_experience, metric_key, default_error={'percentile': -1}):
26
+ """
27
+ loadingExperience metrics: get percentile, fallback to originLoadingExperience if invalid.
28
+ originLoadingExperienceからフォールバックとして取得すめE
29
+
30
+ Args:
31
+ - loadingExperience: metrics for specific URL (e.g. https://povo.jp/). percentile may be 0 if insufficient data.
32
+ If percentile is 0, fallback to originLoadingExperience.
33
+ - originLoadingExperience: origin-wide data (e.g. https://povo.jp). Shows domain-level performance trend.
34
+ More stable data representing overall domain performance.
35
+ """
36
+ metric = loading_experience.get(metric_key, default_error)
37
+
38
+ # If percentile is valid (> 0), return as-is.
39
+ if metric.get('percentile', -1) > 0:
40
+ return metric
41
+
42
+ # If percentile is invalid, try to get from originLoadingExperience.
43
+ origin_metric = origin_loading_experience.get(metric_key, default_error)
44
+ if origin_metric.get('percentile', -1) > 0:
45
+ # Include category field.
46
+ result = origin_metric.copy()
47
+ return result
48
+
49
+ # Both invalid: return -1.
50
+ return default_error
51
+
52
+ def set_category(index):
53
+ if index >= 5.8:
54
+ return "SLOW"
55
+ elif index >= 3.4:
56
+ return "AVERAGE"
57
+ elif index >= 0:
58
+ return "FAST"
59
+ else:
60
+ return "ERR"
61
+
62
+ def set_p_category(index):
63
+ if index >= 90:
64
+ return "FAST"
65
+ elif index >= 50:
66
+ return "AVERAGE"
67
+ elif index >= 0:
68
+ return "SLOW"
69
+ else:
70
+ return "ERR"
71
+
72
+ def extract_metrics(data):
73
+ loading_experience = data.get('loadingExperience', {}).get('metrics', {})
74
+ origin_loading_experience = data.get('originLoadingExperience', {}).get('metrics', {})
75
+ metrics = {}
76
+ iferror = {'percentile': -1}
77
+
78
+ # Get each metric with fallback logic.
79
+ lcp_metric = get_metric_with_fallback(
80
+ loading_experience, origin_loading_experience, 'LARGEST_CONTENTFUL_PAINT_MS', iferror
81
+ )
82
+ metrics['LCP'] = devide_s(lcp_metric.copy()) #MillSec -> Sec
83
+
84
+ inp_metric = get_metric_with_fallback(
85
+ loading_experience, origin_loading_experience, 'INTERACTION_TO_NEXT_PAINT', iferror
86
+ )
87
+ metrics['INP'] = inp_metric.copy()
88
+
89
+ cls_metric = get_metric_with_fallback(
90
+ loading_experience, origin_loading_experience, 'CUMULATIVE_LAYOUT_SHIFT_SCORE', iferror
91
+ )
92
+ metrics['CLS'] = devide_s(cls_metric.copy(), 100, 2) # scale: raw -> score
93
+
94
+ fcp_metric = get_metric_with_fallback(
95
+ loading_experience, origin_loading_experience, 'FIRST_CONTENTFUL_PAINT_MS', iferror
96
+ )
97
+ metrics['FCP'] = devide_s(fcp_metric.copy()) #MillSec -> Sec
98
+
99
+ fid_metric = get_metric_with_fallback(
100
+ loading_experience, origin_loading_experience, 'FIRST_INPUT_DELAY_MS', iferror
101
+ )
102
+ metrics['FID'] = fid_metric.copy()
103
+
104
+ ttfb_metric = get_metric_with_fallback(
105
+ loading_experience, origin_loading_experience, 'EXPERIMENTAL_TIME_TO_FIRST_BYTE', iferror
106
+ )
107
+ metrics['TTFB'] = devide_s(ttfb_metric.copy()) #MillSec -> Sec
108
+
109
+ r = remove_distributions(metrics)
110
+
111
+ # Restore category field that was removed by remove_distributions.
112
+ for key in ['LCP', 'INP', 'CLS', 'FCP', 'FID', 'TTFB']:
113
+ if key in r:
114
+ # Restore category from original metric.
115
+ original_metric = metrics[key]
116
+ if 'category' in original_metric:
117
+ r[key]['category'] = original_metric['category']
118
+ else:
119
+ # category not found, set ERR.
120
+ r[key]['category'] = 'ERR'
121
+
122
+ lighthouseResult = data.get("lighthouseResult", {})
123
+ try:
124
+ # Fetch speed metrics from PageSpeed API.
125
+ speed_index = lighthouseResult['audits']['metrics']['details']['items'][0]['speedIndex']/1000
126
+ ospeed_index= lighthouseResult['audits']['metrics']['details']['items'][0]['observedSpeedIndex']/1000
127
+ performance_score = lighthouseResult['categories']['performance']['score'] * 100
128
+ except (IndexError, KeyError, TypeError):
129
+ # On error, set -1.
130
+ speed_index = -1
131
+ ospeed_index= -1
132
+ performance_score = -1
133
+
134
+ r['speedIndex'] = {
135
+ 'percentile': speed_index,
136
+ 'category': set_category(speed_index)
137
+ }
138
+ r['observedSpeedIndex'] = {
139
+ 'percentile': ospeed_index,
140
+ 'category': set_category(ospeed_index)
141
+ }
142
+ r['performance_score'] = {
143
+ 'percentile': performance_score,
144
+ 'category': set_p_category(performance_score)
145
+ }
146
+ return r
147
+
148
+ def get_screenshots_from_mobile_response(mobile_response):
149
+ header, base64_image = mobile_response['lighthouseResult']['fullPageScreenshot']['screenshot']['data'].split(",", 1)
150
+ full_image = Image.open(io.BytesIO(base64.b64decode(base64_image)))
151
+ width, height = full_image.size
152
+ left,top,right = 0, 0, width
153
+ bottom = min(height, 3*width) # first view estimate: 3*width px
154
+ image_fv = full_image.crop((left, top, right, bottom))
155
+ return full_image,image_fv,base64_image
156
+
157
+ def fetch_mobile_response(url):
158
+ endpoint = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
159
+ try:
160
+ response = requests.get(endpoint, params={
161
+ "url": url,
162
+ "strategy": "mobile",
163
+ "key": os.environ.get('PAGESPEED_KEY')
164
+ })
165
+
166
+ # Raise HTTPError if status is not 200.
167
+ response.raise_for_status()
168
+
169
+ return response.json() # JSONをパースして返す
170
+
171
+ except requests.exceptions.RequestException as e:
172
+ print(f"Request failed with status {e.response.status_code}: {e.response.text}")
173
+ raise e
174
+
175
+ except json.JSONDecodeError as e:
176
+ raise json.JSONDecodeError(f"JSON parse error: {e.msg}, doc: {e.doc}, pos: {e.pos}")
177
+
178
+ except Exception as e:
179
+ raise RuntimeError(f"予期せぬエラーが発生しました: {e}")
180
+
181
+ return None # return None on unexpected error
182
+
183
+ @customtracer
184
+ def url2speed(url):
185
+ """
186
+ input1 (text): https://yahoo.co.jp
187
+ output1 (json): サイト評価
188
+ output2 (text): base64 encoded image (full page screenshot)
189
+ """
190
+ print(datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"), "url2speed", url)
191
+ try:
192
+ mobile_response = fetch_mobile_response(url)
193
+ data = extract_metrics(mobile_response)
194
+ full_image,image_fv,base64_image = get_screenshots_from_mobile_response(mobile_response)
195
+ return data,base64_image
196
+ except Exception as e:
197
+ print(f"{url} An error occurred: {e}")
198
  raise