feat(shorts): AI agent tips category — ideas, script structure, tips voice, category column
This commit is contained in:
@@ -45,3 +45,4 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
.env*.local
|
||||
/services/client-pulse-bot/dist/
|
||||
/tmp/checkcat.mjs
|
||||
|
||||
@@ -133,6 +133,7 @@ model YoutubeIdea {
|
||||
funnelStage String?
|
||||
status String @default("pending")
|
||||
rejectedReason String?
|
||||
category String? @default("brands")
|
||||
sourceUrl String?
|
||||
sourceSummary String?
|
||||
competitionScore String? // "gap" | "moderate" | "crowded"
|
||||
@@ -172,6 +173,8 @@ model YoutubeScript {
|
||||
// Source content (tweet/thread/news) rendered as visual cards
|
||||
sourceUrl String?
|
||||
sourceData Json?
|
||||
// Content category (brands | ai-agent-tips | ...)
|
||||
category String? @default("brands")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const EDGE_TTS_BIN =
|
||||
? "/home/code/.hermes/hermes-agent/venv/bin/edge-tts"
|
||||
: "edge-tts");
|
||||
const VOICE = process.env.TTS_VOICE || "en-US-ChristopherNeural";
|
||||
const VOICE_TIPS = process.env.TTS_VOICE_TIPS || "en-US-GuyNeural"; // tech/explainer
|
||||
const CHROME = process.env.CHROME_BIN || "/usr/bin/google-chrome-stable";
|
||||
const MAX_TOTAL_SEC = 60;
|
||||
const FPS = 30;
|
||||
@@ -111,12 +112,12 @@ function sh(cmd, args, opts = {}) {
|
||||
return r;
|
||||
}
|
||||
|
||||
function tts(text, outBase) {
|
||||
function tts(text, outBase, voice = VOICE) {
|
||||
const out = `${outBase}.mp3`;
|
||||
const srt = `${outBase}.sentences.json`;
|
||||
sh(
|
||||
TTS_PYTHON,
|
||||
[TTS_WORDS_PY, text, VOICE, out, srt],
|
||||
[TTS_WORDS_PY, text, voice, out, srt],
|
||||
{ timeout: 60_000, env: { ...process.env } }
|
||||
);
|
||||
let sentences = [];
|
||||
@@ -166,18 +167,29 @@ function extractStatData(text) {
|
||||
|
||||
/** Build segment text list from a YoutubeScript's structured fields. */
|
||||
function buildSegments(script) {
|
||||
const isTips = script.category === "ai-agent-tips";
|
||||
const segs = [];
|
||||
const push = (text, label, highlight) => {
|
||||
const clean = (text || "").trim();
|
||||
const chunk = clean.length > 180 ? clean.slice(0, 180) + "…" : clean;
|
||||
if (chunk) segs.push({ text: chunk, label, highlight });
|
||||
};
|
||||
push(script.hook, "HOOK", script.hook);
|
||||
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
||||
push(script.storySetup, "THE STORY", null);
|
||||
push(script.conflict, "THE CONFLICT", null);
|
||||
push(script.insight, "THE INSIGHT", null);
|
||||
push(script.cta, "CTA", null);
|
||||
// For AI-agent tips: labels = MISTAKE / THE FIX instead of THE STORY / ...
|
||||
if (isTips) {
|
||||
push(script.hook, "HOOK", script.hook);
|
||||
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
||||
push(script.storySetup, "THE MISTAKE", null);
|
||||
push(script.conflict, "WHY IT FAILS", null);
|
||||
push(script.insight, "THE FIX", null);
|
||||
push(script.cta, "CTA", null);
|
||||
} else {
|
||||
push(script.hook, "HOOK", script.hook);
|
||||
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
||||
push(script.storySetup, "THE STORY", null);
|
||||
push(script.conflict, "THE CONFLICT", null);
|
||||
push(script.insight, "THE INSIGHT", null);
|
||||
push(script.cta, "CTA", null);
|
||||
}
|
||||
if (segs.length === 0) push(script.fullScript || script.title, script.title || "SHORTS", null);
|
||||
return segs;
|
||||
}
|
||||
@@ -194,7 +206,7 @@ async function renderScript(script) {
|
||||
const voiced = [];
|
||||
for (let i = 0; i < baseSegs.length; i++) {
|
||||
const s = baseSegs[i];
|
||||
const t = tts(s.text, path.join(work, `seg${i}`));
|
||||
const t = tts(s.text, path.join(work, `seg${i}`), script.category === "ai-agent-tips" ? VOICE_TIPS : VOICE);
|
||||
voiced.push({ ...s, ...t });
|
||||
log(`[${id}] seg${i}: ${t.durationSec.toFixed(2)}s "${s.text.slice(0, 40)}"`);
|
||||
}
|
||||
|
||||
@@ -45,8 +45,9 @@ function fetchArticle(url: string): string {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { topic, sourceUrl, sourceSummary } = await req.json();
|
||||
const { topic, sourceUrl, sourceSummary, category } = await req.json();
|
||||
if (!topic) return NextResponse.json({ error: "topic required" }, { status: 400 });
|
||||
const isTips = category === "ai-agent-tips";
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY || "";
|
||||
const braveKey = process.env.BRAVE_API_KEY || "";
|
||||
@@ -171,20 +172,25 @@ ${sourceContext || "No sources available — be EXTRA cautious with claims, hedg
|
||||
- Include a "factClaims" array listing every specific factual claim in the script (dates, numbers, events, quotes)
|
||||
- Each claim MUST be traceable to the source material above
|
||||
|
||||
## THE USER'S ANGLE
|
||||
## THE USER'S ANGLE${isTips ? `
|
||||
- Builds and ships AI agents (coding agents, MCP, agent loops) — the tips are for OTHER builders
|
||||
- Voice: direct, first-principles, tells you the mistake you are probably making
|
||||
- Every tip must be concrete and actionable — no fluff
|
||||
- The viewer is technical: they write code and wire tools together
|
||||
- Ground each tip in a real failure/lesson: "I ran this in production and learned X"` : `
|
||||
- Runs a marketing-focused business
|
||||
- Content: founder branding, marketing, distribution > product, culture > features
|
||||
- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable
|
||||
${pastScriptsRef}
|
||||
${feedbackContext}
|
||||
- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable`}
|
||||
${pastScriptsRef}
|
||||
${feedbackContext}
|
||||
|
||||
OUTPUT FORMAT — valid JSON only:
|
||||
{
|
||||
"title": "Brand/Person — Core Lesson (short)",
|
||||
"hook": "Opening 2-4 lines. Scroll-stopping.",
|
||||
"storySetup": "The backstory context. 3-5 sentences.",
|
||||
"conflict": "The tension pivot or controversial statement. 3-5 sentences.",
|
||||
"insight": "The aha moment payoff lesson for founders. 3-5 sentences.",
|
||||
"hook": "Opening 2-4 lines. Scroll-stopping.${isTips ? " Lead with the COMMON MISTAKE or counter-intuitive truth about AI agents." : ""}",
|
||||
"storySetup": "The backstory context. 3-5 sentences.${isTips ? " The situation: why builders get this wrong." : ""}",
|
||||
"conflict": "The tension pivot or controversial statement. 3-5 sentences.${isTips ? " The failure mode / what everyone does that backfires." : ""}",
|
||||
"insight": "The aha moment payoff lesson for founders. 3-5 sentences.${isTips ? " THE concrete fix/pattern — specific, actionable, names the tool/technique." : ""}",
|
||||
"cta": "One clear action. One line.",
|
||||
"caption": "One punchy line without spoilers.",
|
||||
"onScreenText": "What appears on screen during the hook",
|
||||
|
||||
@@ -33,7 +33,11 @@ function braveSearch(query: string, braveKey: string, count = 5): { title: strin
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
export async function POST(req?: Request) {
|
||||
const body = req ? await req.json().catch(() => ({})) : {};
|
||||
const category: string = body.category || "brands";
|
||||
const isTips = category === "ai-agent-tips";
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY || "";
|
||||
const braveKey = process.env.BRAVE_API_KEY || "";
|
||||
if (!apiKey) return NextResponse.json({ error: "No API key" }, { status: 500 });
|
||||
@@ -65,10 +69,19 @@ export async function POST() {
|
||||
try { coveredTopics = fs.readFileSync(SCRIPTS_FILE, "utf-8").match(/### .+/g)?.join(", ") || ""; } catch { /* ok - file may not exist on Vercel */ }
|
||||
|
||||
// -- STEP 1: Ask GPT to generate search queries --
|
||||
const queryGenResult = callOpenAI(apiKey, {
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
|
||||
const querySystem = isTips
|
||||
? `You generate search queries to find practical, actionable AI AGENT tips and best practices for a viral short-form video creator.
|
||||
|
||||
The creator makes videos for builders/developers who ship AI agents (coding agents, MCP servers, tool-using LLMs, agent loops).
|
||||
Topics to cover: agent architecture, tool design, context window management, evals/testing, prompt engineering for agents, MCP, sub-agents, cost control, reliability/failure modes, memory, permissions/safety, orchestration patterns.
|
||||
|
||||
Generate 10 diverse search queries for REAL, concrete tips (engineering blogs, docs, HN discussions, practitioner posts). Prefer "best practices", "lessons learned", "common mistakes", "how to", "patterns".
|
||||
|
||||
DO NOT search for ideas already generated: ${existingTitles}
|
||||
${rejectionContext}
|
||||
|
||||
Output JSON: {"queries":["search query 1","search query 2",...]}`
|
||||
: `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
|
||||
|
||||
The creator covers a WIDE mix: mainstream brands (Red Bull, Nike, Barbie, Tesla, Apple, McDonald's, etc.), tech, pop culture, and business strategy. Most videos are about mainstream brands with a marketing/founder lesson.
|
||||
|
||||
@@ -81,8 +94,15 @@ DO NOT search for stories already covered: ${coveredTopics}
|
||||
DO NOT search for ideas already generated: ${existingTitles}
|
||||
${rejectionContext}
|
||||
|
||||
Output JSON: {"queries":["search query 1","search query 2",...]}` },
|
||||
{ role: "user", content: "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
|
||||
Output JSON: {"queries":["search query 1","search query 2",...]}`;
|
||||
|
||||
const queryGenResult = callOpenAI(apiKey, {
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: querySystem },
|
||||
{ role: "user", content: isTips
|
||||
? "10 diverse search queries for real, actionable AI agent tips and lessons."
|
||||
: "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
|
||||
],
|
||||
temperature: 0.95,
|
||||
response_format: { type: "json_object" }
|
||||
@@ -104,10 +124,23 @@ Output JSON: {"queries":["search query 1","search query 2",...]}` },
|
||||
).join("\n\n");
|
||||
|
||||
// -- STEP 3: Feed real articles to GPT to create ideas --
|
||||
const ideaGenResult = callOpenAI(apiKey, {
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
|
||||
const ideaSystem = isTips
|
||||
? `You create viral short-form video IDEAS about practical AI AGENT tips for builders, based on REAL articles found via web search.
|
||||
|
||||
CRITICAL RULES:
|
||||
- Each idea MUST be one concrete, actionable tip/lesson from the search results below
|
||||
- Each idea MUST include a sourceUrl from the search results
|
||||
- Each idea MUST include a sourceSummary (1-2 sentence summary of the real tip/lesson)
|
||||
- Hook must stop the scroll in 3 seconds — lead with a common MISTAKE or counter-intuitive truth
|
||||
- Examples of strong hooks: "Your AI agent fails because of one missing tool", "Stop giving your agent 40 tools", "Nobody tells you this about MCP servers"
|
||||
- The angle must name a SPECIFIC technique/tool/pattern (MCP, tool schemas, evals, context pruning, sub-agents...) — never vague "use AI better"
|
||||
- Mix funnel stages: 5 TOF (wide builder appeal), 3 MOF (specific workflows), 2 BOF (advanced architecture)
|
||||
- DO NOT repeat these existing ideas: ${existingTitles}
|
||||
- Each hook should have normal punctuation (commas, periods)
|
||||
${rejectionContext}
|
||||
|
||||
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"specific technique/pattern","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real tip"}]}`
|
||||
: `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
|
||||
|
||||
CRITICAL RULES:
|
||||
- Each idea MUST be based on a REAL story from the search results below
|
||||
@@ -124,8 +157,15 @@ CRITICAL RULES:
|
||||
- Always name the SPECIFIC brand/person — never be vague
|
||||
${rejectionContext}
|
||||
|
||||
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}` },
|
||||
{ role: "user", content: `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
|
||||
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}`;
|
||||
|
||||
const ideaGenResult = callOpenAI(apiKey, {
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: ideaSystem },
|
||||
{ role: "user", content: isTips
|
||||
? `Based on these REAL articles found via web search, create 10 AI AGENT TIP video ideas. Each idea MUST be one concrete tip and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.`
|
||||
: `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
|
||||
],
|
||||
temperature: 0.85,
|
||||
response_format: { type: "json_object" }
|
||||
@@ -149,6 +189,7 @@ Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scrol
|
||||
angle: idea.angle || null,
|
||||
hookType: idea.hookType || null,
|
||||
funnelStage: idea.funnelStage || null,
|
||||
category,
|
||||
status: "pending",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,15 +22,17 @@ const LongformTab = dynamic(() => import("@/app/longform/page"), { ssr: false, l
|
||||
const OutlierFeed = dynamic(() => import("@/components/OutlierFeed"), { ssr: false, loading: () => <TabSkeleton /> });
|
||||
|
||||
interface Idea {
|
||||
id: string;
|
||||
title: string;
|
||||
hook: string;
|
||||
angle: string;
|
||||
hook?: string;
|
||||
angle?: string;
|
||||
hookType?: string;
|
||||
funnelStage?: string;
|
||||
rejectedReason?: string;
|
||||
status?: "pending" | "rejected" | "approved";
|
||||
category?: string;
|
||||
sourceUrl?: string;
|
||||
sourceSummary?: string;
|
||||
rejectedReason?: string;
|
||||
status?: "pending" | "rejected" | "approved";
|
||||
}
|
||||
|
||||
interface Script {
|
||||
@@ -82,6 +84,7 @@ export default function YouTubePage() {
|
||||
const [batchGenerating, setBatchGenerating] = useState(false);
|
||||
const [ideaTab, setIdeaTab] = useState<"pending" | "rejected">("pending");
|
||||
const [scriptTab, setScriptTab] = useState<"draft" | "approved" | "rejected">("draft");
|
||||
const [ideaCategory, setIdeaCategory] = useState<"brands" | "ai-agent-tips">("brands");
|
||||
const [rejectModal, setRejectModal] = useState<{ type: "idea" | "script"; id: string; title: string } | null>(null);
|
||||
const rejectRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -139,7 +142,7 @@ export default function YouTubePage() {
|
||||
const res = await fetch("/api/youtube/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary }),
|
||||
body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary, category: idea.category || ideaCategory }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@@ -629,11 +632,24 @@ export default function YouTubePage() {
|
||||
}}
|
||||
>{batchGenerating ? `✨ Generating ${selectedIdeas.size}...` : `Generate ${selectedIdeas.size} Script${selectedIdeas.size > 1 ? "s" : ""}`}</Button>
|
||||
)}
|
||||
<select
|
||||
value={ideaCategory}
|
||||
onChange={(e) => setIdeaCategory(e.target.value as "brands" | "ai-agent-tips")}
|
||||
className="text-xs bg-[var(--surface)] border border-[var(--border)] rounded-lg px-2 py-1.5 text-[var(--muted)]"
|
||||
title="Content category for idea generation"
|
||||
>
|
||||
<option value="brands">Brands & Founders</option>
|
||||
<option value="ai-agent-tips">AI Agent Tips</option>
|
||||
</select>
|
||||
<Button size="sm" variant="ghost" disabled={generatingIdeas}
|
||||
onClick={async () => {
|
||||
setGeneratingIdeas(true);
|
||||
try {
|
||||
const res = await fetch("/api/youtube/ideas/generate", { method: "POST" });
|
||||
const res = await fetch("/api/youtube/ideas/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ category: ideaCategory }),
|
||||
});
|
||||
if (res.ok) fetchIdeas();
|
||||
} catch { /* empty */ }
|
||||
setGeneratingIdeas(false);
|
||||
|
||||
Reference in New Issue
Block a user