feat(shorts): AI agent tips category — ideas, script structure, tips voice, category column

This commit is contained in:
asepharyana
2026-09-22 22:16:02 +07:00
parent 85ce16d6d7
commit 897c33eeac
6 changed files with 116 additions and 37 deletions
+1
View File
@@ -45,3 +45,4 @@ yarn-error.log*
next-env.d.ts
.env*.local
/services/client-pulse-bot/dist/
/tmp/checkcat.mjs
+3
View File
@@ -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
+15 -3
View File
@@ -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 });
};
// 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)}"`);
}
+15 -9
View File
@@ -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",
+54 -13
View File
@@ -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 -6
View File
@@ -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);