126 lines
5.9 KiB
Python
126 lines
5.9 KiB
Python
import logging
|
|
import os
|
|
from urllib.parse import urlparse
|
|
from common.db.source import select_run_raw_data, update_raw_info_merge, update_raw_info_logo_url
|
|
from integrations.vision import VisionClient
|
|
from integrations.color_extractor import extract_brand_assets_from_site
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def collect_brand_assets(analysis_run_id: str, info_id: int) -> None:
|
|
"""홈페이지에서 로고 URL + brand hex 색상 추출 → branding raw_info["brandAssets"] 머지.
|
|
- 로고 URL/hex: HTML·CSS 정규식 (color_extractor) — Vision 의존 X, 사이트 전체 컬러 시스템이 더 정확.
|
|
- 로고 정성 묘사(심볼/워드마크/톤): Gemini Vision (GEMINI_API_KEY 없으면 색상만 저장하고 skip).
|
|
"""
|
|
logger.info("[brand_assets] start run=%s info=%s", analysis_run_id, info_id)
|
|
raw = await select_run_raw_data(analysis_run_id)
|
|
mainpage = raw.get("mainpage") or {}
|
|
homepage_url = mainpage.get("sourceUrl") or ""
|
|
branding = mainpage.get("branding") or {}
|
|
|
|
# 0~1. 사이트 1회 fetch 로 logo URL + brand hex 동시 추출 (img/background-image/CSS .logo, Vision 의존 X)
|
|
site = await extract_brand_assets_from_site(homepage_url) if homepage_url else {}
|
|
html_logo_url = site.get("logo_url")
|
|
css_colors = site.get("colors") or {}
|
|
if html_logo_url:
|
|
logger.info("[brand_assets] HTML logo found: %s", html_logo_url)
|
|
if css_colors:
|
|
logger.info("[brand_assets] css colors: %s", css_colors.get("brand_colors"))
|
|
|
|
# 2. 로고/대표 이미지 후보 (logo → og:image → favicon 순)
|
|
logo_url = html_logo_url or branding.get("logoUrl")
|
|
og_image = branding.get("ogImage")
|
|
favicon = branding.get("faviconUrl")
|
|
candidates: list[tuple[str, str]] = []
|
|
if logo_url: candidates.append(("logo", logo_url))
|
|
if og_image: candidates.append(("og", og_image))
|
|
if favicon: candidates.append(("favicon", favicon))
|
|
if homepage_url:
|
|
parsed = urlparse(homepage_url)
|
|
if parsed.scheme and parsed.netloc:
|
|
candidates.append(("favicon", f"{parsed.scheme}://{parsed.netloc}/favicon.ico"))
|
|
|
|
if not candidates and not css_colors:
|
|
logger.info("[brand_assets] skip — no logo/og/favicon candidates and no CSS colors")
|
|
return
|
|
|
|
# 3. Vision 은 로고 정성 묘사만 (hex 는 CSS 추출이 더 정확). 키 없으면 색상만 저장.
|
|
# SVG 는 vision 내부에서 resvg 로 PNG 래스터화 후 Gemini 에 던지므로 분기 불필요.
|
|
result: dict = {}
|
|
used_kind: str | None = None
|
|
api_key = os.getenv("GEMINI_API_KEY")
|
|
if api_key and candidates:
|
|
vc = VisionClient(api_key)
|
|
for kind, cand in candidates:
|
|
result = await vc.analyze_brand_assets(logo_url=cand, homepage_url=homepage_url)
|
|
if result:
|
|
used_kind = kind
|
|
break
|
|
elif not api_key:
|
|
logger.info("[brand_assets] GEMINI_API_KEY not set — 색상만 저장, Vision 묘사 skip")
|
|
|
|
# 4. CSS 에서 추출한 brand_colors/palette 를 Vision 보다 우선 사용
|
|
if css_colors:
|
|
if css_colors.get("brand_colors"): result["brand_colors"] = css_colors["brand_colors"]
|
|
if css_colors.get("color_palette"): result["color_palette"] = css_colors["color_palette"]
|
|
result["color_source"] = "html+css"
|
|
elif result:
|
|
result["color_source"] = "vision"
|
|
|
|
# 5. logo URL 은 JSON 이 아니라 raw_info.logo_url 컬럼에 분리 저장 (raw vs 분석 텍스트 분리).
|
|
# favicon 으로만 매칭된 경우 진짜 로고 아니라 컬럼 저장 X.
|
|
result.pop("logo_images", None)
|
|
column_logo_url = logo_url if used_kind in ("logo", "og") and logo_url else None
|
|
if column_logo_url:
|
|
await update_raw_info_logo_url(info_id, column_logo_url)
|
|
|
|
if result:
|
|
result["logo_source"] = used_kind or "none"
|
|
await update_raw_info_merge(info_id, {"brandAssets": result})
|
|
logger.info("[brand_assets] done logo_url=%s keys=%s",
|
|
bool(column_logo_url), list(result.keys()) if result else None)
|
|
|
|
|
|
async def collect_channel_logos(analysis_run_id: str, info_id: int) -> None:
|
|
"""채널별 프로필 이미지(로고)를 모아 Gemini Vision 으로 설명 + 공식 로고 일치 여부 평가.
|
|
→ branding raw_info["channelLogos"] 머지. GEMINI_API_KEY 없으면 skip.
|
|
brand_assets(공식 로고) · 채널 raw_info(profileImage) 가 채워진 뒤 실행돼야 함."""
|
|
api_key = os.getenv("GEMINI_API_KEY")
|
|
if not api_key:
|
|
logger.info("[channel_logos] skip — GEMINI_API_KEY 없음")
|
|
return
|
|
|
|
raw = await select_run_raw_data(analysis_run_id)
|
|
branding = raw.get("branding") or {}
|
|
official = ((branding.get("brandAssets") or {}).get("logo_images") or {}).get("horizontal")
|
|
|
|
# KR 메인 채널 + EN/TikTok 부가 채널 profileImage 수집 (raw_info dict 키: instagram, instagram_en, ...)
|
|
_label = {
|
|
"instagram": "Instagram",
|
|
"facebook": "Facebook",
|
|
"youtube": "YouTube",
|
|
"instagram_en": "Instagram EN",
|
|
"facebook_en": "Facebook EN",
|
|
"tiktok": "TikTok",
|
|
}
|
|
logos: list[dict] = []
|
|
for key, label in _label.items():
|
|
img = (raw.get(key) or {}).get("profileImage")
|
|
if img:
|
|
logos.append({"channel": label, "url": img})
|
|
|
|
if not logos:
|
|
logger.info("[channel_logos] skip — 채널 프로필 이미지 없음")
|
|
return
|
|
|
|
logger.info("[channel_logos] start run=%s channels=%s official=%s",
|
|
analysis_run_id, [l["channel"] for l in logos], bool(official))
|
|
result = await VisionClient(api_key).describe_channel_logos(official, logos)
|
|
if result:
|
|
# Vision 이 못 본 채널도 url 은 채워둠 (프론트에서 이미지 표시용)
|
|
result["logos"] = logos
|
|
await update_raw_info_merge(info_id, {"channelLogos": result})
|
|
logger.info("[channel_logos] done run=%s keys=%s",
|
|
analysis_run_id, list(result.keys()) if result else None)
|