feat: contexto de video, análisis extendido y métricas sociales
- Campo "Contexto del Video" en formulario de análisis (Paso 03) → se pasa a GPT-4o para enriquecer el análisis - 4 nuevos campos de diagnóstico: fortalezas, debilidades, sugerencias_mejora, hashtags_sugeridos (click para copiar) - Vista de detalle: card de métricas sociales (vistas/likes/compartidos con engagement rate calculado) - Muestra contexto original ingresado por el usuario - Migración SQL 07: 5 nuevas columnas en tabla guiones - validador.js: 4 nuevos campos en schema Zod - server.js + api/analizar.js: acepta y guarda contexto_video Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -33,6 +33,7 @@ export default async function handler(req, res) {
|
||||
likes = null,
|
||||
compartidos = null,
|
||||
fecha_publicacion = null,
|
||||
contexto_video = '',
|
||||
} = req.body
|
||||
|
||||
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
||||
@ -56,7 +57,7 @@ export default async function handler(req, res) {
|
||||
|
||||
// ── PASO 3: Analizar con GPT-4o ───────────────────────
|
||||
paso = 'analisis'
|
||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
|
||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion, contexto_video)
|
||||
|
||||
// ── PASO 4: Validar con Zod ───────────────────────────
|
||||
paso = 'validacion'
|
||||
@ -136,6 +137,13 @@ export default async function handler(req, res) {
|
||||
persona_narradora: analisis.persona_narradora,
|
||||
promesa_explicita: analisis.promesa_explicita,
|
||||
nivel_especificidad: analisis.nivel_especificidad,
|
||||
contexto_video: contexto_video || null,
|
||||
|
||||
// Diagnóstico
|
||||
fortalezas: analisis.fortalezas,
|
||||
debilidades: analisis.debilidades,
|
||||
sugerencias_mejora: analisis.sugerencias_mejora,
|
||||
hashtags_sugeridos: analisis.hashtags_sugeridos,
|
||||
|
||||
// Métricas (score_engagement lo calcula el trigger de Supabase)
|
||||
score_virabilidad: analisis.score_virabilidad,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// ============================================================
|
||||
// ANALIZADOR — GPT-4o
|
||||
// Prompt maestro multidisciplinario: Storytelling + Cialdini
|
||||
// + Neuropublicidad + Copywriting → JSON de 45 campos
|
||||
// + Neuropublicidad + Copywriting → JSON de 49 campos
|
||||
// ============================================================
|
||||
import OpenAI from 'openai'
|
||||
|
||||
@ -22,12 +22,17 @@ SOLO devuelve el JSON, sin texto adicional, sin markdown, sin explicaciones.`
|
||||
* @param {string} niche Nicho del video (ej: "fitness", "finanzas")
|
||||
* @param {string} plataforma tiktok | reels | shorts
|
||||
* @param {number} duracion Duración en segundos
|
||||
* @param {string} contextoVideo Contexto adicional opcional sobre el video
|
||||
* @returns {object} JSON con todos los campos de análisis
|
||||
*/
|
||||
export async function analizarTranscript(transcript, niche, plataforma, duracion) {
|
||||
export async function analizarTranscript(transcript, niche, plataforma, duracion, contextoVideo = '') {
|
||||
const bloqueContexto = contextoVideo
|
||||
? `CONTEXTO ADICIONAL DEL VIDEO (úsalo para enriquecer el análisis):\n"""\n${contextoVideo}\n"""\n\n`
|
||||
: ''
|
||||
|
||||
const promptUsuario = `Analiza este video de ${plataforma} de ${duracion} segundos del nicho "${niche}".
|
||||
|
||||
TRANSCRIPCIÓN:
|
||||
${bloqueContexto}TRANSCRIPCIÓN:
|
||||
"""
|
||||
${transcript}
|
||||
"""
|
||||
@ -88,6 +93,11 @@ Devuelve EXACTAMENTE este JSON con los valores que correspondan:
|
||||
"ingredientes_clave": ["<elemento 1 que NO puede faltar si se replica este guion>", "<elemento 2>", "<elemento 3>"],
|
||||
"replicabilidad": "<alta|media|baja>",
|
||||
|
||||
"fortalezas": ["<fortaleza 1 del video: qué hace especialmente bien>", "<fortaleza 2>", "<fortaleza 3>"],
|
||||
"debilidades": ["<debilidad o área de mejora 1>", "<debilidad 2>"],
|
||||
"sugerencias_mejora": ["<sugerencia accionable concreta 1 para mejorar el video>", "<sugerencia 2>", "<sugerencia 3>"],
|
||||
"hashtags_sugeridos": ["<hashtag1>", "<hashtag2>", "<hashtag3>", "<hashtag4>", "<hashtag5>", "<hashtag6>", "<hashtag7>"],
|
||||
|
||||
"score_virabilidad": <número entero del 1 al 100>,
|
||||
"resumen_patron": "<párrafo de 3-4 oraciones describiendo el patrón ganador de este video: qué hace, por qué funciona psicológicamente y cómo se puede replicar>"
|
||||
}`
|
||||
|
||||
@ -81,6 +81,12 @@ export const AnalisisSchema = z.object({
|
||||
ingredientes_clave: z.array(z.string()).min(1).max(7),
|
||||
replicabilidad: ReplicabilidadEnum,
|
||||
|
||||
// Diagnóstico y mejora
|
||||
fortalezas: z.array(z.string()).min(1).max(5),
|
||||
debilidades: z.array(z.string()).min(1).max(5),
|
||||
sugerencias_mejora: z.array(z.string()).min(1).max(5),
|
||||
hashtags_sugeridos: z.array(z.string()).min(1).max(10),
|
||||
|
||||
// Métricas
|
||||
score_virabilidad: z.number().int().min(1).max(100),
|
||||
resumen_patron: z.string().min(10).max(1500),
|
||||
|
||||
@ -111,6 +111,7 @@ app.post('/api/analizar', async (req, res) => {
|
||||
competidor_referente = false,
|
||||
vistas = null, likes = null, compartidos = null,
|
||||
fecha_publicacion = null,
|
||||
contexto_video = '',
|
||||
} = req.body
|
||||
|
||||
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
||||
@ -135,7 +136,7 @@ app.post('/api/analizar', async (req, res) => {
|
||||
|
||||
paso = 'analisis'
|
||||
console.log(`[3/5] Analizando con GPT-4o...`)
|
||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
|
||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion, contexto_video)
|
||||
|
||||
paso = 'validacion'
|
||||
console.log(`[4/5] Validando schema...`)
|
||||
@ -152,6 +153,7 @@ app.post('/api/analizar', async (req, res) => {
|
||||
proyecto_nombre, competidor_referente,
|
||||
url_origen: url, plataforma, duracion_segundos: duracion,
|
||||
vistas, likes, compartidos, fecha_publicacion,
|
||||
contexto_video: contexto_video || null,
|
||||
...analisis,
|
||||
transcript,
|
||||
embedding_vector: `[${vector.join(',')}]`,
|
||||
|
||||
17
database/migrations/07_diagnostico_contexto.sql
Normal file
17
database/migrations/07_diagnostico_contexto.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- ============================================================
|
||||
-- MIGRACIÓN 07 — Contexto, Diagnóstico y Hashtags
|
||||
-- Ejecutar en Supabase SQL Editor después de la migración 06
|
||||
-- ============================================================
|
||||
|
||||
alter table guiones
|
||||
add column if not exists contexto_video text,
|
||||
add column if not exists fortalezas text[],
|
||||
add column if not exists debilidades text[],
|
||||
add column if not exists sugerencias_mejora text[],
|
||||
add column if not exists hashtags_sugeridos text[];
|
||||
|
||||
comment on column guiones.contexto_video is 'Contexto o idea del video ingresado por el usuario antes del análisis';
|
||||
comment on column guiones.fortalezas is 'Lo que el video hace especialmente bien (generado por GPT-4o)';
|
||||
comment on column guiones.debilidades is 'Áreas de mejora detectadas por GPT-4o';
|
||||
comment on column guiones.sugerencias_mejora is 'Sugerencias accionables para mejorar el video';
|
||||
comment on column guiones.hashtags_sugeridos is 'Hashtags sugeridos por GPT-4o para maximizar alcance';
|
||||
@ -99,6 +99,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Paso 3: Contexto del Video -->
|
||||
<section class="space-y-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-8 h-8 rounded-lg bg-tertiary/10 text-tertiary flex items-center justify-center font-black text-sm border border-tertiary/20">03</span>
|
||||
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Contexto del Video <span class="text-outline font-normal text-base">(opcional)</span></h2>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel p-8 rounded-3xl border border-white/5 shadow-2xl space-y-4">
|
||||
<p class="text-xs text-outline/70 leading-relaxed">
|
||||
Describe de qué trata el video, cuál era la intención del creador, o cualquier detalle que la transcripción sola no captura (ej. tono irónico, contexto de tendencia, referencia cultural). GPT-4o usará esto para enriquecer el análisis.
|
||||
</p>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-4 top-4 text-tertiary text-lg">lightbulb</span>
|
||||
<textarea
|
||||
v-model="form.contexto_video"
|
||||
rows="4"
|
||||
placeholder="Ej. Este video es una respuesta a una tendencia viral donde los creadores muestran su rutina matutina. El creador usa humor sarcástico y habla a emprendedores que trabajan desde casa..."
|
||||
class="w-full bg-surface-container-low border border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:ring-2 focus:ring-tertiary/40 focus:border-tertiary/40 text-white placeholder:text-outline/40 transition-all resize-none leading-relaxed"
|
||||
:disabled="analizando"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Columna de Estado del Pipeline -->
|
||||
@ -176,7 +200,8 @@ const form = ref({
|
||||
compartidos: null,
|
||||
cliente_id: null,
|
||||
proyecto_nombre: '',
|
||||
competidor_referente: false
|
||||
competidor_referente: false,
|
||||
contexto_video: ''
|
||||
})
|
||||
|
||||
const pasosVisibles = [
|
||||
|
||||
@ -43,6 +43,34 @@
|
||||
<!-- Columna izquierda -->
|
||||
<div class="xl:col-span-4 flex flex-col gap-6">
|
||||
|
||||
<!-- Métricas Sociales -->
|
||||
<div v-if="guion.vistas || guion.likes || guion.compartidos" class="bg-surface-container p-5 rounded-3xl border border-outline-variant/10 shadow-xl">
|
||||
<h3 class="text-xs font-headline font-bold text-white mb-4 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-outline text-base">bar_chart</span> Métricas del Video
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||
<span class="material-symbols-outlined text-blue-400 text-lg block mb-1">visibility</span>
|
||||
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Vistas</p>
|
||||
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.vistas) }}</p>
|
||||
</div>
|
||||
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||
<span class="material-symbols-outlined text-red-400 text-lg block mb-1" style="font-variation-settings:'FILL' 1;">favorite</span>
|
||||
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Likes</p>
|
||||
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.likes) }}</p>
|
||||
</div>
|
||||
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||
<span class="material-symbols-outlined text-secondary text-lg block mb-1">share</span>
|
||||
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Compartidos</p>
|
||||
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.compartidos) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="guion.score_engagement" class="mt-3 flex items-center justify-between px-3 py-2 rounded-xl bg-surface-container-lowest border border-white/5">
|
||||
<span class="text-[10px] text-outline font-black uppercase tracking-widest">Engagement Rate</span>
|
||||
<span class="text-sm font-black text-secondary">{{ (guion.score_engagement * 1).toFixed(2) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Puntaje -->
|
||||
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-2xl relative overflow-hidden group">
|
||||
<div class="absolute -top-24 -right-24 w-48 h-48 bg-primary/20 blur-3xl rounded-full group-hover:bg-primary/30 transition-colors pointer-events-none"></div>
|
||||
@ -59,15 +87,11 @@
|
||||
<span class="text-xs font-bold text-primary uppercase tracking-widest mt-1">/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 pt-6 border-t border-white/5">
|
||||
<div class="grid grid-cols-2 gap-4 pt-6 border-t border-white/5">
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Cialdini</p>
|
||||
<p class="text-xl font-bold text-white">{{ guion.score_cialdini ?? 0 }}<span class="text-sm text-outline">/7</span></p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Engagement</p>
|
||||
<p class="text-xl font-bold text-secondary">{{ guion.score_engagement ? (guion.score_engagement*1).toFixed(2) + '%' : 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Intensidad</p>
|
||||
<p class="text-xl font-bold text-orange-400">{{ guion.intensidad_emocional || 0 }}<span class="text-sm text-outline">/10</span></p>
|
||||
@ -281,6 +305,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnóstico: Fortalezas / Debilidades / Sugerencias -->
|
||||
<div v-if="guion.fortalezas?.length || guion.debilidades?.length" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-surface-container p-6 rounded-3xl border border-secondary/10 shadow-xl">
|
||||
<h3 class="text-sm font-headline font-bold text-white mb-5 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-secondary text-base" style="font-variation-settings:'FILL' 1;">thumb_up</span> Fortalezas
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(f, i) in guion.fortalezas" :key="i" class="flex items-start gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-secondary mt-2 shrink-0"></span>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed">{{ f }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container p-6 rounded-3xl border border-red-500/10 shadow-xl">
|
||||
<h3 class="text-sm font-headline font-bold text-white mb-5 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-red-400 text-base">build</span> Áreas de Mejora
|
||||
</h3>
|
||||
<div class="space-y-2 mb-5">
|
||||
<div v-for="(d, i) in guion.debilidades" :key="i" class="flex items-start gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-red-400 mt-2 shrink-0"></span>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed">{{ d }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="guion.sugerencias_mejora?.length">
|
||||
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-2">Sugerencias</p>
|
||||
<div class="space-y-1.5">
|
||||
<div v-for="(s, i) in guion.sugerencias_mejora" :key="i" class="flex items-start gap-2">
|
||||
<span class="text-[9px] font-black text-tertiary shrink-0 mt-0.5">{{ i + 1 }}.</span>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed">{{ s }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hashtags sugeridos -->
|
||||
<div v-if="guion.hashtags_sugeridos?.length" class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
|
||||
<h3 class="text-sm font-headline font-bold text-white mb-4 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-outline text-base">tag</span> Hashtags Sugeridos
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="tag in guion.hashtags_sugeridos"
|
||||
:key="tag"
|
||||
class="px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-xs font-bold text-primary cursor-pointer hover:bg-primary/20 transition-colors"
|
||||
@click="copiarTag(tag)"
|
||||
>#{{ tag.replace(/^#/, '') }}</span>
|
||||
</div>
|
||||
<p class="text-[9px] text-outline/50 mt-3 italic">Haz clic en un hashtag para copiarlo</p>
|
||||
</div>
|
||||
|
||||
<!-- Contexto original del usuario -->
|
||||
<div v-if="guion.contexto_video" class="bg-surface-container p-5 rounded-2xl border border-tertiary/10">
|
||||
<p class="text-[10px] text-tertiary font-black uppercase tracking-widest mb-2 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">lightbulb</span> Contexto ingresado
|
||||
</p>
|
||||
<p class="text-sm text-on-surface-variant leading-relaxed italic">{{ guion.contexto_video }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Transcripción -->
|
||||
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
@ -343,6 +426,17 @@ function openUrl(url) {
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function formatNum(n) {
|
||||
if (!n) return '—'
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
function copiarTag(tag) {
|
||||
navigator.clipboard.writeText('#' + tag.replace(/^#/, ''))
|
||||
}
|
||||
|
||||
function plataformaBadge(p) {
|
||||
return {
|
||||
tiktok: 'bg-red-500/20 text-red-400',
|
||||
|
||||
Reference in New Issue
Block a user