Youtube-AI-Recomendations / src /exec_summary.py
jesuszambrano13's picture
Upload 5 files
200934e verified
Raw
History Blame Contribute Delete
9.36 kB
"""Resumen ejecutivo orientado a marketing.
Convierte la salida estructurada del modelo + score híbrido + políticas +
composición visual en un texto **claro y accionable** para alguien que no es
data scientist: marketers, gerentes de cuenta, dueños de canal.
Reglas de redacción:
- Frase principal de UNA línea con la acción.
- 3 razones concretas en bullets cortos.
- Métricas clave en formato amigable (porcentajes, no decimales).
- Nada de "score_hibrido 0.6325" — usar "rendimiento esperado: alto (63 sobre 100)".
- Sin jerga técnica innecesaria. Sin referencias a "feature engineering" ni "logistic regression".
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
# Glosario de equivalencias humanas para los niveles del modelo.
LEVEL_HUMAN = {
"muy_alto": "muy alto",
"alto": "alto",
"medio": "medio",
"bajo": "bajo",
}
ACTION_PHRASES = {
"IMPULSAR": {
"headline": "✅ Adelante: el video está listo para pauta.",
"tone": "positivo",
"color": "#22c55e",
},
"AJUSTAR ANTES DE IMPULSAR": {
"headline": "🛠️ Casi listo: con ajustes puntuales rinde mejor.",
"tone": "neutro_positivo",
"color": "#facc15",
},
"MONITOREAR": {
"headline": "🟡 Decisión: no pautar todavía; optimiza y vuelve a evaluar.",
"tone": "neutro",
"color": "#60a5fa",
},
"NO IMPULSAR": {
"headline": "⛔ No conviene pautar este video.",
"tone": "negativo",
"color": "#f87171",
},
"REVISIÓN HUMANA": {
"headline": "🧑‍⚖️ Mejor lo revisa una persona antes de decidir.",
"tone": "advertencia",
"color": "#a78bfa",
},
}
POLICY_HUMAN = {
"bajo": "✅ Sin alertas de política",
"medio": "🟡 Algunas frases requieren cuidado",
"alto": "🔴 Riesgo alto de política",
"revisión humana": "🧑‍⚖️ Múltiples temas sensibles detectados",
}
def _to_percent(value: float) -> str:
try:
return f"{float(value) * 100:.0f}%"
except Exception:
return "—"
def _format_money(usd: float) -> str:
try:
return f"${float(usd):,.0f}"
except Exception:
return "—"
def _format_int(value) -> str:
try:
return f"{int(float(value)):,}"
except Exception:
return "—"
def build_executive_summary(
*,
final_recommendation: Dict[str, Any],
prediction: Dict[str, Any],
features: Dict[str, Any],
operational_metrics: Dict[str, Any],
policy_block: Dict[str, Any],
visual_analysis: Optional[Dict[str, Any]] = None,
script_analysis: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
cpm: float = 5.0,
budget: float = 0.0,
) -> Dict[str, Any]:
"""Construye un resumen ejecutivo en lenguaje claro.
Devuelve un dict con varias piezas:
- ``headline``: una línea con la acción.
- ``score_global_0_100``: score híbrido en escala 0-100.
- ``probabilidad_pct``: probabilidad de aptitud/candidatura publicitaria en %.
- ``policy_status_human``: estado de política en frase clara.
- ``por_que``: lista de 3 razones cortas.
- ``que_hacer_ahora``: lista de pasos accionables.
- ``forecast``: dict con métricas de pauta (impresiones esperadas, vistas, etc).
- ``markdown``: render completo en markdown para pegar en la UI.
"""
action = final_recommendation.get("accion_final", "MONITOREAR")
action_meta = ACTION_PHRASES.get(action, ACTION_PHRASES["MONITOREAR"])
prob = float(final_recommendation.get("probabilidad_rendimiento", 0))
level = final_recommendation.get("prediccion_rendimiento", "medio")
score_hib = float(final_recommendation.get("score_hibrido", 0))
score_0_100 = int(round(score_hib * 100))
prob_pct = int(round(prob * 100))
policy_lvl = policy_block.get("policy_risk_level", "bajo")
policy_status_human = POLICY_HUMAN.get(policy_lvl, policy_lvl)
policy_cats = policy_block.get("policy_risk_categories", []) or []
# Razones positivas y negativas, intercaladas
razones: List[str] = []
# Modelo
razones.append(
f"📊 Probabilidad de aptitud publicitaria: **{prob_pct}%**. Nivel del modelo: **{LEVEL_HUMAN.get(level, level)}**."
)
# Política
if policy_lvl == "bajo":
razones.append("🛡️ El contenido **no activa alertas de políticas** publicitarias.")
elif policy_lvl == "medio":
razones.append(
f"⚠️ Hay **señales moderadas** de política (`{', '.join(policy_cats[:3])}`). "
"Probablemente apto pero con monetización limitada."
)
elif policy_lvl == "alto":
razones.append(
f"🚫 **Categorías sensibles detectadas**: `{', '.join(policy_cats[:3])}`. "
"YouTube probablemente NO monetizaría este video con anuncios estándar."
)
else: # revisión humana
razones.append(
f"🚨 Múltiples temas delicados ({len(policy_cats)} categorías). "
"Revisar con alguien antes de invertir."
)
# Composición visual (si disponible)
if visual_analysis and visual_analysis.get("visual_ok"):
v_score = float(visual_analysis.get("composition_score", 0))
if v_score >= 0.55:
razones.append(f"🎬 **Composición visual sólida** ({int(v_score*100)} sobre 100). Encuadre y balance correctos.")
elif v_score >= 0.35:
razones.append(f"🎬 Composición visual aceptable ({int(v_score*100)} sobre 100) pero con margen para mejorar.")
else:
razones.append(f"🎬 **Composición visual débil** ({int(v_score*100)} sobre 100). Revisa encuadre y foco.")
# Engagement actual
er = float(features.get("engagement_rate", 0))
if er > 0.05:
razones.append(f"❤️ Engagement actual ALTO ({er*100:.1f}% likes+comments / views).")
elif er > 0.02:
razones.append(f"❤️ Engagement actual saludable ({er*100:.1f}%).")
elif er > 0:
razones.append(f"❤️ Engagement actual bajo ({er*100:.1f}%). El video aún no resuena con su audiencia.")
# Ajustes sugeridos → "qué hacer ahora"
que_hacer = list(final_recommendation.get("ajustes_sugeridos", []))[:5]
if visual_analysis and visual_analysis.get("visual_ok"):
# Tomar 2 recomendaciones visuales si hay
for r in visual_analysis.get("visual_recommendations", [])[:2]:
if r not in que_hacer:
que_hacer.append(r)
if not que_hacer:
que_hacer.append("No hay ajustes críticos pendientes. Mantener el contenido como está.")
# Forecast
reach_per_dollar = float(final_recommendation.get("alcance_estimado_por_dolar", 0))
reach_total = float(final_recommendation.get("alcance_estimado_total", 0))
forecast = {
"cpm_usd": float(final_recommendation.get("cpm_estimado", cpm)),
"budget_usd": float(budget),
"impresiones_esperadas": int(reach_total),
"impresiones_por_dolar": int(reach_per_dollar),
"multiplicador_potencial": float(final_recommendation.get("multiplicador_potencial", 1.0)),
"current_views": int(float(features.get("views", 0))),
"current_likes": int(float(features.get("likes", 0))),
"current_comments": int(float(features.get("comments", 0))),
}
headline = action_meta["headline"]
color = action_meta["color"]
# Construir markdown
title_str = (metadata or {}).get("title") or "Sin título"
md_parts: List[str] = []
md_parts.append(f"# {headline}")
md_parts.append(f"**Video analizado:** {title_str}")
md_parts.append("")
md_parts.append(f"| Métrica clave | Valor |")
md_parts.append(f"|---|---|")
md_parts.append(f"| **Probabilidad de aptitud publicitaria** | {prob_pct}% · nivel {LEVEL_HUMAN.get(level, level)} |")
md_parts.append(f"| **Score global** | **{score_0_100} / 100** |")
md_parts.append(f"| **Política publicitaria** | {policy_status_human} |")
if forecast["budget_usd"] > 0:
md_parts.append(
f"| **Con {_format_money(forecast['budget_usd'])} pautados** | ≈ {_format_int(forecast['impresiones_esperadas'])} impresiones estimadas, no vistas garantizadas |"
)
md_parts.append("")
md_parts.append("### Por qué")
for r in razones[:5]:
md_parts.append(f"- {r}")
md_parts.append("")
md_parts.append("### Qué hacer ahora")
for q in que_hacer[:5]:
md_parts.append(f"- {q}")
md_parts.append("")
md_parts.append(
"> *Este sistema es una herramienta de apoyo. La probabilidad indicada mide aptitud/candidatura publicitaria, no ROI. El alcance esperado por dólar se calcula con "
"el CPM ingresado y un multiplicador de potencial; no representa ROI real garantizado.*"
)
return {
"headline": headline,
"action": action,
"color": color,
"score_global_0_100": score_0_100,
"probabilidad_pct": prob_pct,
"nivel_humano": LEVEL_HUMAN.get(level, level),
"policy_status_human": policy_status_human,
"policy_categories": policy_cats,
"por_que": razones[:5],
"que_hacer_ahora": que_hacer[:5],
"forecast": forecast,
"markdown": "\n".join(md_parts),
}