Spaces:
Sleeping
Sleeping
deploy api_light_hf (2026-03-19 13:12:47)
Browse files- apis/base64img2component.py +133 -133
- apis/baseimg2fvinfo_with_design.py +58 -45
- apis/url2meta.py +33 -33
- apis/url2speed.py +197 -197
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 = "
|
| 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):
|
| 99 |
-
input2 (text): スクショ
|
| 100 |
-
input3 (text): default
|
| 101 |
-
output1 (json):
|
| 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":"
|
| 117 |
-
{"component_large":"FAQ/よくある質
|
| 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
|
| 129 |
-
try:
|
| 130 |
-
return ask_raw(messages)
|
| 131 |
-
except openai.AuthenticationError as e:
|
| 132 |
-
# API
|
| 133 |
-
#
|
| 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 |
-
会社
|
| 66 |
-
業
|
| 67 |
-
ブラン
|
| 68 |
サービス: str
|
| 69 |
-
|
| 70 |
タイトル: str
|
| 71 |
-
訴求テー
|
|
|
|
| 72 |
|
| 73 |
class Design(BaseModel):
|
| 74 |
-
重要なフレーズの
|
| 75 |
-
背景を画像
|
| 76 |
-
四角や丸など図形で囲
|
| 77 |
-
アイコンを使用して視認性を上げ
|
| 78 |
-
|
| 79 |
-
|
|
|
|
| 80 |
class sCopy(BaseModel):
|
| 81 |
text: str
|
| 82 |
design: Design
|
| 83 |
|
|
|
|
| 84 |
class EvsF(str, Enum):
|
| 85 |
-
EMOTIONAL = "
|
| 86 |
-
FUNCTIONAL = "機
|
|
|
|
| 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
|
| 103 |
-
forcus_stage
|
| 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 |
-
キャチ
|
| 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
|
| 129 |
model_lower = (model or "").lower()
|
| 130 |
if not model_lower.startswith("gpt-5"):
|
| 131 |
params["temperature"] = 0
|
| 132 |
-
|
| 133 |
-
response = _ask_raw_hf(
|
|
|
|
|
|
|
| 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 |
-
|
| 159 |
-
|
| 160 |
},
|
| 161 |
{
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
・何も書かれて
|
| 166 |
-
・CTAボタンが存在する場合、
|
| 167 |
-
・画像
|
| 168 |
-
・main_copy以外を「sub_copy」と
|
| 169 |
-
・画像
|
| 170 |
-
・画像
|
| 171 |
-
・これら
|
| 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' #
|
| 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
|
| 28 |
-
originLoadingExperienceからフォールバックとして取得すめE
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
- loadingExperience:
|
| 32 |
-
|
| 33 |
-
- originLoadingExperience:
|
| 34 |
-
|
| 35 |
-
"""
|
| 36 |
-
metric = loading_experience.get(metric_key, default_error)
|
| 37 |
-
|
| 38 |
-
# percentile
|
| 39 |
-
if metric.get('percentile', -1) > 0:
|
| 40 |
-
return metric
|
| 41 |
-
|
| 42 |
-
# percentile
|
| 43 |
-
origin_metric = origin_loading_experience.get(metric_key, default_error)
|
| 44 |
-
if origin_metric.get('percentile', -1) > 0:
|
| 45 |
-
# category
|
| 46 |
-
result = origin_metric.copy()
|
| 47 |
-
return result
|
| 48 |
-
|
| 49 |
-
#
|
| 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 |
-
#
|
| 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)
|
| 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
|
| 112 |
-
for key in ['LCP', 'INP', 'CLS', 'FCP', 'FID', 'TTFB']:
|
| 113 |
-
if key in r:
|
| 114 |
-
#
|
| 115 |
-
original_metric = metrics[key]
|
| 116 |
-
if 'category' in original_metric:
|
| 117 |
-
r[key]['category'] = original_metric['category']
|
| 118 |
-
else:
|
| 119 |
-
# category
|
| 120 |
-
r[key]['category'] = 'ERR'
|
| 121 |
-
|
| 122 |
-
lighthouseResult = data.get("lighthouseResult", {})
|
| 123 |
-
try:
|
| 124 |
-
#
|
| 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 |
-
#
|
| 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
|
| 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 |
-
#
|
| 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
|
| 177 |
-
|
| 178 |
-
except Exception as e:
|
| 179 |
-
raise RuntimeError(f"予期せぬエラーが発生しました: {e}")
|
| 180 |
-
|
| 181 |
-
return None #
|
| 182 |
-
|
| 183 |
-
@customtracer
|
| 184 |
-
def url2speed(url):
|
| 185 |
-
"""
|
| 186 |
-
input1 (text): https://yahoo.co.jp
|
| 187 |
-
output1 (json): サイト評価
|
| 188 |
-
output2 (text): base64
|
| 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
|