Integra estrategias LinkedIn en calendarios (backlog publicado vs pendiente), unifica roles/cookies de vista agencia y permite deshacer cambios de brandbook dejando constancia en el historial. Co-authored-by: Cursor <cursoragent@cursor.com>
633 lines
19 KiB
Python
633 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Remedia brandbooks: elimina kits inventados y aplica solo señales
|
||
verificadas desde webs oficiales (+ notas de procedencia).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import random
|
||
import time
|
||
import urllib.request
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
def env(k: str) -> str:
|
||
for line in (ROOT / ".env.local").read_text().splitlines():
|
||
if line.startswith(k + "="):
|
||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||
return ""
|
||
|
||
|
||
URL = env("NEXT_PUBLIC_SUPABASE_URL")
|
||
KEY = env("SUPABASE_SERVICE_ROLE_KEY")
|
||
HR = {"apikey": KEY, "Authorization": f"Bearer {KEY}"}
|
||
HW = {**HR, "Content-Type": "application/json", "Prefer": "return=minimal"}
|
||
|
||
|
||
def get(path: str) -> Any:
|
||
req = urllib.request.Request(f"{URL}/rest/v1/{path}", headers=HR)
|
||
return json.loads(urllib.request.urlopen(req).read())
|
||
|
||
|
||
def patch(path: str, body: dict) -> None:
|
||
req = urllib.request.Request(
|
||
f"{URL}/rest/v1/{path}",
|
||
data=json.dumps(body).encode(),
|
||
headers=HW,
|
||
method="PATCH",
|
||
)
|
||
urllib.request.urlopen(req)
|
||
|
||
|
||
def nid(p: str) -> str:
|
||
return f"{p}_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
|
||
|
||
|
||
def color(name: str, hex_: str, role: str, usage: str) -> dict:
|
||
return {
|
||
"id": nid("col"),
|
||
"name": name,
|
||
"hex": hex_.upper() if hex_.startswith("#") else hex_,
|
||
"role": role,
|
||
"usage": usage,
|
||
}
|
||
|
||
|
||
def font(family: str, role: str, source: str, notes: str) -> dict:
|
||
return {
|
||
"id": nid("font"),
|
||
"family": family,
|
||
"role": role,
|
||
"weights": "",
|
||
"source": source,
|
||
"url_or_notes": notes,
|
||
}
|
||
|
||
|
||
# Solo colores con evidencia fuerte en HTML/CSS de la web oficial.
|
||
# Si no hay evidencia clara → colors = [] (mejor vacío que inventado).
|
||
VERIFIED_VISUAL: dict[str, dict[str, Any]] = {
|
||
"4ventures-hub": {
|
||
"colors": [
|
||
color(
|
||
"Naranja marca (web)",
|
||
"#E05200",
|
||
"primary",
|
||
"Frecuencia alta en CSS de https://4ventures.es/",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"DM Sans",
|
||
"headline",
|
||
"google",
|
||
"font-family detectado en HTML de 4ventures.es",
|
||
),
|
||
font(
|
||
"Cormorant Garamond",
|
||
"display",
|
||
"google",
|
||
"font-family detectado en HTML de 4ventures.es",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://4ventures.es/",
|
||
},
|
||
"agua-miia": {
|
||
"colors": [
|
||
color(
|
||
"Azul primario",
|
||
"#0085F2",
|
||
"primary",
|
||
"CSS var marca en https://aguamiia.es/",
|
||
),
|
||
color(
|
||
"Azul corporativo",
|
||
"#2071CF",
|
||
"secondary",
|
||
"CSS var / uso frecuente en aguamiia.es",
|
||
),
|
||
color(
|
||
"Azul oscuro",
|
||
"#0F4F9E",
|
||
"neutral",
|
||
"CSS var en aguamiia.es",
|
||
),
|
||
color(
|
||
"Verde acento",
|
||
"#2E9B76",
|
||
"accent",
|
||
"CSS var en aguamiia.es",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Urbanist",
|
||
"headline",
|
||
"google",
|
||
"font-family dominante en HTML de aguamiia.es",
|
||
),
|
||
font(
|
||
"Nunito",
|
||
"body",
|
||
"google",
|
||
"font-family secundario en HTML de aguamiia.es",
|
||
),
|
||
],
|
||
"source": "CSS variables + font-family https://aguamiia.es/",
|
||
},
|
||
"bps": {
|
||
# BPS ya tenía kit; reforzamos con theme-color oficial
|
||
"colors": [
|
||
color(
|
||
"BPS Blue",
|
||
"#0051AA",
|
||
"primary",
|
||
"theme-color meta + uso en https://blueparrotschool.com/",
|
||
),
|
||
color(
|
||
"Accent amarillo",
|
||
"#FDC526",
|
||
"accent",
|
||
"Hex frecuente en CSS de blueparrotschool.com",
|
||
),
|
||
color(
|
||
"Ink",
|
||
"#201E22",
|
||
"neutral",
|
||
"Hex frecuente en CSS de blueparrotschool.com",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Figtree",
|
||
"headline",
|
||
"google",
|
||
"Referenciado en HTML blueparrotschool.com",
|
||
),
|
||
font(
|
||
"Jost",
|
||
"body",
|
||
"google",
|
||
"Referenciado en HTML blueparrotschool.com",
|
||
),
|
||
],
|
||
"source": "theme-color + CSS https://blueparrotschool.com/",
|
||
"merge_colors_if_existing": True,
|
||
},
|
||
"brava-dermoestetica": {
|
||
"colors": [
|
||
color(
|
||
"Rosa/brandy clínica",
|
||
"#7A3F4D",
|
||
"primary",
|
||
"Hex dominante (69×) en https://www.bravadermoestetica.com/",
|
||
),
|
||
color(
|
||
"Rosa oscuro",
|
||
"#61323D",
|
||
"secondary",
|
||
"Hex frecuente en bravadermoestetica.com",
|
||
),
|
||
color(
|
||
"Fondo cálido",
|
||
"#F8F5F2",
|
||
"neutral",
|
||
"Hex de fondo en bravadermoestetica.com",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Playfair Display",
|
||
"headline",
|
||
"google",
|
||
"font-family en HTML bravadermoestetica.com",
|
||
),
|
||
font(
|
||
"Inter",
|
||
"body",
|
||
"google",
|
||
"font-family en HTML bravadermoestetica.com",
|
||
),
|
||
font(
|
||
"Myriad Pro",
|
||
"other",
|
||
"adobe",
|
||
"font-family referenciado en HTML (licencia Adobe/sistema)",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://www.bravadermoestetica.com/",
|
||
},
|
||
"delgado-abogados": {
|
||
"colors": [
|
||
color(
|
||
"Azul enlace/marca",
|
||
"#1863DC",
|
||
"primary",
|
||
"Hex frecuente en https://abogadosdelgado.es/",
|
||
),
|
||
color(
|
||
"Ink",
|
||
"#212121",
|
||
"neutral",
|
||
"Hex frecuente en abogadosdelgado.es",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Inter",
|
||
"body",
|
||
"google",
|
||
"WP preset font-family Inter en abogadosdelgado.es",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://abogadosdelgado.es/ (parcial; ampliar con brand kit PDF)",
|
||
},
|
||
"dtatf": {
|
||
"colors": [
|
||
color(
|
||
"Amarillo De Tú a Tú",
|
||
"#F6A800",
|
||
"primary",
|
||
"Hex en CSS/HTML https://detuatuformacion.es/",
|
||
),
|
||
color(
|
||
"Azul oscuro De Tú a Tú",
|
||
"#273645",
|
||
"secondary",
|
||
"Hex en CSS/HTML detuatuformacion.es (+ #253645)",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Poppins-DeTUaTU",
|
||
"headline",
|
||
"file",
|
||
"Familia custom referenciada en detuatuformacion.es (archivo/fuente propia)",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://detuatuformacion.es/",
|
||
},
|
||
"ecp": {
|
||
"colors": [
|
||
color(
|
||
"Amarillo ECP",
|
||
"#F7A801",
|
||
"primary",
|
||
"Hex dominante en https://escueladecuidadoresprofesionales.com/",
|
||
),
|
||
color(
|
||
"Azul",
|
||
"#1863DC",
|
||
"secondary",
|
||
"Hex frecuente en web ECP",
|
||
),
|
||
color(
|
||
"Ink",
|
||
"#212121",
|
||
"neutral",
|
||
"Hex frecuente en web ECP",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Raleway",
|
||
"headline",
|
||
"google",
|
||
"font-family dominante en web ECP",
|
||
),
|
||
font(
|
||
"Open Sans",
|
||
"body",
|
||
"google",
|
||
"font-family secundario en web ECP",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://escueladecuidadoresprofesionales.com/",
|
||
},
|
||
"equinsa-parking": {
|
||
"colors": [
|
||
color(
|
||
"Navy Equinsa",
|
||
"#000028",
|
||
"primary",
|
||
"Hex en https://equinsaparking.com/",
|
||
),
|
||
color(
|
||
"Azul",
|
||
"#003388",
|
||
"secondary",
|
||
"Hex en equinsaparking.com",
|
||
),
|
||
color(
|
||
"Azul enlace",
|
||
"#046BD2",
|
||
"accent",
|
||
"CSS var / hex en equinsaparking.com",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Roboto",
|
||
"body",
|
||
"google",
|
||
"font-family dominante en equinsaparking.com",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://equinsaparking.com/",
|
||
},
|
||
"fbm-abogados": {
|
||
"colors": [
|
||
color(
|
||
"Navy FBM",
|
||
"#04243C",
|
||
"primary",
|
||
"Hex dominante (75×) en https://fbmasociados.com/",
|
||
),
|
||
],
|
||
"fonts_kit": [], # tahoma genérico — no afirmar kit de marca
|
||
"source": "HTML/CSS https://fbmasociados.com/ (color). Tipografía: pendiente brand kit.",
|
||
},
|
||
"halal-safety": {
|
||
"colors": [], # scrape débil / ruido WP
|
||
"fonts_kit": [
|
||
font(
|
||
"Manrope",
|
||
"body",
|
||
"google",
|
||
"Referenciado en HTML halalsafety.com",
|
||
),
|
||
],
|
||
"source": "Tipografía HTML https://www.halalsafety.com/. Colores: insuficientes en scrape — vacíos a propósito.",
|
||
},
|
||
"la-freseria": {
|
||
"colors": [], # el scrape pilló azules de template Framer, NO son marca fresa
|
||
"fonts_kit": [
|
||
font(
|
||
"La Fresería",
|
||
"display",
|
||
"file",
|
||
"Familia custom 'La Fresería' referenciada en lafreseria.es (archivo propio)",
|
||
),
|
||
],
|
||
"source": "Fuentes custom en https://lafreseria.es/. Colores NO aplicados: el HTML mezcla tokens Framer (#046bd2) no confiables como marca.",
|
||
},
|
||
"noberplast": {
|
||
"colors": [
|
||
color(
|
||
"Azul Noberplast",
|
||
"#0072AB",
|
||
"primary",
|
||
"Hex en https://noberplast.com/",
|
||
),
|
||
color(
|
||
"Verde",
|
||
"#06A161",
|
||
"secondary",
|
||
"Hex en noberplast.com",
|
||
),
|
||
color(
|
||
"Rojo acento",
|
||
"#DB062C",
|
||
"accent",
|
||
"Hex en noberplast.com",
|
||
),
|
||
],
|
||
"fonts_kit": [],
|
||
"source": "Hex en https://noberplast.com/. Tipografía: no detectada con claridad.",
|
||
},
|
||
"tsp": {
|
||
"colors": [
|
||
color(
|
||
"Navy Scholar",
|
||
"#161B2F",
|
||
"primary",
|
||
"Hex frecuente en https://talentoscholarprogram.com/",
|
||
),
|
||
color(
|
||
"Azul claro",
|
||
"#A4CAEF",
|
||
"secondary",
|
||
"Hex frecuente en talentoscholarprogram.com",
|
||
),
|
||
color(
|
||
"Rojo acento",
|
||
"#A40406",
|
||
"accent",
|
||
"Hex en talentoscholarprogram.com",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Poppins",
|
||
"body",
|
||
"google",
|
||
"font-family dominante en talentoscholarprogram.com",
|
||
),
|
||
font(
|
||
"Fraunces",
|
||
"display",
|
||
"google",
|
||
"font-family en talentoscholarprogram.com",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://talentoscholarprogram.com/",
|
||
},
|
||
"tyd": {
|
||
"colors": [
|
||
color(
|
||
"Rojo TYD",
|
||
"#E92428",
|
||
"primary",
|
||
"Hex dominante en https://talentoydeporte.com/",
|
||
),
|
||
color(
|
||
"Azul TYD",
|
||
"#004885",
|
||
"secondary",
|
||
"Hex en talentoydeporte.com",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"Montserrat",
|
||
"headline",
|
||
"google",
|
||
"font-family en talentoydeporte.com",
|
||
),
|
||
font(
|
||
"Oswald",
|
||
"display",
|
||
"google",
|
||
"font-family referenciado en talentoydeporte.com",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://talentoydeporte.com/",
|
||
},
|
||
"virginia-molet": {
|
||
"colors": [
|
||
color(
|
||
"Azul Gravitae",
|
||
"#004271",
|
||
"primary",
|
||
"Hex frecuente en https://gravitaeacademy.com/",
|
||
),
|
||
color(
|
||
"Azul enlace",
|
||
"#1863DC",
|
||
"secondary",
|
||
"Hex frecuente en gravitaeacademy.com (posible token WP; contrastar con kit)",
|
||
),
|
||
],
|
||
"fonts_kit": [
|
||
font(
|
||
"quincy-cf",
|
||
"display",
|
||
"adobe",
|
||
"font-family 'quincy-cf' en gravitaeacademy.com",
|
||
),
|
||
font(
|
||
"Inter",
|
||
"body",
|
||
"google",
|
||
"font-family Inter en gravitaeacademy.com",
|
||
),
|
||
],
|
||
"source": "HTML/CSS https://gravitaeacademy.com/ (revisar azul #1863DC vs kit oficial)",
|
||
},
|
||
"wine-advisors": {
|
||
"colors": [
|
||
color(
|
||
"Rojo WineAdvisor",
|
||
"#CC0100",
|
||
"primary",
|
||
"Hex detectado en https://www.wineadvisor.mx/",
|
||
),
|
||
],
|
||
"fonts_kit": [],
|
||
"source": "Hex limitado en https://www.wineadvisor.mx/. Tipografía: no detectada.",
|
||
},
|
||
}
|
||
|
||
PROVENANCE_HEADER = (
|
||
"[PROCEDENCIA 2026-10-15] "
|
||
"Webs/handles/LinkedIn: contrastados con sitio oficial + Meta business_discovery + páginas LinkedIn. "
|
||
"Colores/fuentes: solo si aparecen en HTML/CSS de la web (ver visual_notes). "
|
||
"Textos de posicionamiento/tono/ICP: síntesis de brand manager a partir del copy público de la web "
|
||
"(no son citas literales del cliente ni de un PDF de marca). "
|
||
"Si un campo no tiene evidencia → vacío. "
|
||
)
|
||
|
||
|
||
def parse(c: Any) -> dict:
|
||
if isinstance(c, str):
|
||
return json.loads(c)
|
||
return dict(c or {})
|
||
|
||
|
||
def main() -> None:
|
||
tenants = get(
|
||
"tenants?agency_followed=neq.false&select=id,slug,name&order=slug"
|
||
)
|
||
for t in tenants:
|
||
slug = t["slug"]
|
||
docs = get(
|
||
f"documents?tenant_id=eq.{t['id']}&type=eq.brandbook&select=id,content&order=updated_at.desc&limit=1"
|
||
)
|
||
if not docs:
|
||
print("skip", slug)
|
||
continue
|
||
book = parse(docs[0]["content"])
|
||
vis = VERIFIED_VISUAL.get(slug)
|
||
|
||
if vis is None:
|
||
# no verified visual → clear invented kits
|
||
book["colors"] = []
|
||
book["fonts_kit"] = []
|
||
book["colors_primary"] = []
|
||
book["colors_secondary"] = []
|
||
book["fonts"] = []
|
||
note = "Sin señales visuales verificables en scrape; kit vaciado."
|
||
else:
|
||
book["colors"] = vis.get("colors") or []
|
||
book["fonts_kit"] = vis.get("fonts_kit") or []
|
||
book["colors_primary"] = [
|
||
c["hex"] for c in book["colors"] if c.get("role") == "primary"
|
||
]
|
||
book["colors_secondary"] = [
|
||
c["hex"]
|
||
for c in book["colors"]
|
||
if c.get("role") in ("secondary", "accent")
|
||
]
|
||
book["fonts"] = [f["family"] for f in book["fonts_kit"]]
|
||
if book["fonts_kit"]:
|
||
heads = [f for f in book["fonts_kit"] if f["role"] in ("headline", "display")]
|
||
bodies = [f for f in book["fonts_kit"] if f["role"] == "body"]
|
||
if heads:
|
||
book["typography_headline"] = heads[0]["family"]
|
||
if bodies:
|
||
book["typography_body"] = bodies[0]["family"]
|
||
note = vis.get("source") or ""
|
||
|
||
# Visual notes: honest provenance
|
||
book["visual_notes"] = (
|
||
f"PROCEDENCIA VISUAL: {note}. "
|
||
"Los hex/fuentes listados salen del HTML/CSS público. "
|
||
"Sustituir por brand kit oficial (PDF/AI) cuando esté en Drive/Zona de marca. "
|
||
"NO usar paletas inventadas por IA."
|
||
)
|
||
|
||
# Provenance in freeform
|
||
prev = (book.get("freeform_notes") or "").strip()
|
||
# remove older provenance blocks to avoid duplication
|
||
lines = [
|
||
ln
|
||
for ln in prev.split("\n")
|
||
if not ln.startswith("[PROCEDENCIA")
|
||
and "PROCEDENCIA VISUAL" not in ln
|
||
]
|
||
book["freeform_notes"] = (
|
||
PROVENANCE_HEADER
|
||
+ (f"Visual: {note}. " if note else "")
|
||
+ ("\n" + "\n".join(lines) if lines else "")
|
||
).strip()
|
||
|
||
# Clear weak invented claims that often aren't on-site
|
||
# Keep website/handles/products — those were verified in packs
|
||
# Soften proof_points that cite unverified stats
|
||
if slug == "la-freseria":
|
||
# don't keep invented franchise € figures as hard fact without date stamp
|
||
pps = book.get("proof_points") or []
|
||
book["proof_points"] = [
|
||
p
|
||
for p in pps
|
||
if "65.000" not in p and "EBITDA" not in p
|
||
] + [
|
||
"Claim web/franquicia: 'las fresas más famosas del mundo' (lafreseria.es)",
|
||
"IG oficial @lafreseriaoficial",
|
||
"LinkedIn company/la-freseria",
|
||
]
|
||
|
||
if slug == "equinsa-parking":
|
||
# keep qualitative; avoid conflicting installation counts
|
||
book["proof_points"] = [
|
||
"Web: especializada en sistemas de gestión de aparcamientos (equinsaparking.com)",
|
||
"LinkedIn company/equinsa-parking",
|
||
"Sede comunicada: C. Primavera 16, Torrejón de Ardoz (web)",
|
||
"Contacto web: sat@equinsa.es / +34 618 18 89 24",
|
||
]
|
||
|
||
patch(f"documents?id=eq.{docs[0]['id']}", {"content": book})
|
||
n_col = len(book.get("colors") or [])
|
||
n_font = len(book.get("fonts_kit") or [])
|
||
print(f"[ok] {slug:22} colors={n_col} fonts={n_font} · {note[:80]}")
|
||
|
||
print("DONE — invented kits removed; verified web signals applied")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|