This commit is contained in:
Harun CAN
2026-03-23 02:26:08 +03:00
parent 458127ce76
commit 83b0ae61a8
13 changed files with 1193 additions and 367 deletions

View File

@@ -1,6 +1,11 @@
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleGenAI } from '@google/genai';
import { ZodSchema, ZodError } from 'zod';
// ============================================
// Types & Interfaces
// ============================================
export interface GeminiGenerateOptions {
model?: string;
@@ -15,30 +20,72 @@ export interface GeminiChatMessage {
content: string;
}
export interface GeminiJSONOptions<T = any> extends GeminiGenerateOptions {
/** Zod schema for runtime validation of the AI response */
zodSchema?: ZodSchema<T>;
/** Max retry attempts for JSON generation (default: 3) */
maxRetries?: number;
}
/**
* Gemini AI Service
* Error types for Gemini API failures
*/
export enum GeminiErrorType {
RATE_LIMIT = 'RATE_LIMIT',
QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',
SAFETY_BLOCKED = 'SAFETY_BLOCKED',
INVALID_RESPONSE = 'INVALID_RESPONSE',
JSON_PARSE_FAILED = 'JSON_PARSE_FAILED',
TIMEOUT = 'TIMEOUT',
UNAVAILABLE = 'UNAVAILABLE',
UNKNOWN = 'UNKNOWN',
}
/**
* Custom exception for Gemini AI errors with rich context
*/
export class GeminiException extends Error {
constructor(
message: string,
public readonly type: GeminiErrorType,
public readonly originalError?: any,
public readonly retryable: boolean = false,
) {
super(message);
this.name = 'GeminiException';
}
}
// ============================================
// Service
// ============================================
/**
* Gemini AI Service — Enhanced with Retry, JSON Recovery & Validation
*
* Provides AI-powered text generation using Google Gemini API.
* Provides AI-powered text/JSON/image generation using Google Gemini API.
* This service is globally available when ENABLE_GEMINI=true.
*
* Key improvements over v1:
* - responseMimeType: "application/json" for native JSON output
* - Exponential backoff retry (up to 3 attempts)
* - Multi-strategy JSON extraction & recovery
* - Optional Zod schema validation
* - Typed GeminiException with error classification
* - AI usage metrics logging
*
* @example
* ```typescript
* // Simple text generation
* const response = await geminiService.generateText('Write a poem about coding');
*
* // With options
* const response = await geminiService.generateText('Translate to Turkish', {
* temperature: 0.7,
* systemPrompt: 'You are a professional translator',
* });
*
* // Chat conversation
* const messages = [
* { role: 'user', content: 'Hello!' },
* { role: 'model', content: 'Hi there!' },
* { role: 'user', content: 'What is 2+2?' },
* ];
* const response = await geminiService.chat(messages);
* // JSON generation with Zod validation
* import { z } from 'zod';
* const schema = z.object({ title: z.string(), score: z.number() });
* const result = await geminiService.generateJSON(
* 'Analyze this script', '{ title, score }',
* { zodSchema: schema }
* );
* ```
*/
@Injectable()
@@ -87,6 +134,10 @@ export class GeminiService implements OnModuleInit {
return this.isEnabled && this.client !== null;
}
// ============================================
// Text Generation
// ============================================
/**
* Generate text content from a prompt
*
@@ -98,11 +149,10 @@ export class GeminiService implements OnModuleInit {
prompt: string,
options: GeminiGenerateOptions = {},
): Promise<{ text: string; usage?: any }> {
if (!this.isAvailable()) {
throw new Error('Gemini AI is not available. Check your configuration.');
}
this.ensureAvailable();
const model = options.model || this.defaultModel;
const startTime = Date.now();
try {
const contents: any[] = [];
@@ -134,16 +184,27 @@ export class GeminiService implements OnModuleInit {
},
});
const durationMs = Date.now() - startTime;
this.logUsage('generateText', model, response.usageMetadata, durationMs);
return {
text: (response.text || '').trim(),
usage: response.usageMetadata,
};
} catch (error) {
this.logger.error('Gemini generation failed', error);
throw error;
const durationMs = Date.now() - startTime;
this.logger.error(
`Gemini generation failed after ${durationMs}ms`,
error,
);
throw this.classifyError(error);
}
}
// ============================================
// Chat
// ============================================
/**
* Have a multi-turn chat conversation
*
@@ -155,11 +216,10 @@ export class GeminiService implements OnModuleInit {
messages: GeminiChatMessage[],
options: GeminiGenerateOptions = {},
): Promise<{ text: string; usage?: any }> {
if (!this.isAvailable()) {
throw new Error('Gemini AI is not available. Check your configuration.');
}
this.ensureAvailable();
const model = options.model || this.defaultModel;
const startTime = Date.now();
try {
const contents = messages.map((msg) => ({
@@ -190,55 +250,165 @@ export class GeminiService implements OnModuleInit {
},
});
const durationMs = Date.now() - startTime;
this.logUsage('chat', model, response.usageMetadata, durationMs);
return {
text: (response.text || '').trim(),
usage: response.usageMetadata,
};
} catch (error) {
this.logger.error('Gemini chat failed', error);
throw error;
throw this.classifyError(error);
}
}
// ============================================
// JSON Generation (Enhanced)
// ============================================
/**
* Generate structured JSON output
* Generate structured JSON output with retry, recovery, and optional Zod validation.
*
* Strategy:
* 1. First attempt uses `responseMimeType: "application/json"` for native JSON
* 2. If that fails, falls back to prompt-based JSON with multi-strategy extraction
* 3. Up to `maxRetries` attempts with exponential backoff
* 4. Optional Zod schema validation on the parsed result
*
* @param prompt - The prompt describing what JSON to generate
* @param schema - JSON schema description for the expected output
* @param options - Optional configuration for the generation
* @returns Parsed JSON object
* @param schema - JSON schema description for the expected output (human readable)
* @param options - Optional configuration including zodSchema and maxRetries
* @returns Parsed and optionally validated JSON object
*/
async generateJSON<T = any>(
prompt: string,
schema: string,
options: GeminiGenerateOptions = {},
options: GeminiJSONOptions<T> = {},
): Promise<{ data: T; usage?: any }> {
const fullPrompt = `${prompt}
this.ensureAvailable();
const maxRetries = options.maxRetries ?? 3;
const model = options.model || this.defaultModel;
let lastError: Error | null = null;
let lastUsage: any = undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const startTime = Date.now();
try {
// Build the full prompt
const fullPrompt = `${prompt}
Output the result as valid JSON that matches this schema:
${schema}
IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
const response = await this.generateText(fullPrompt, options);
const contents: any[] = [];
try {
// Try to extract JSON from the response
let jsonStr = response.text;
if (options.systemPrompt) {
contents.push({
role: 'user',
parts: [{ text: options.systemPrompt }],
});
contents.push({
role: 'model',
parts: [
{ text: 'Understood. I will follow these instructions.' },
],
});
}
// Remove potential markdown code blocks
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
contents.push({
role: 'user',
parts: [{ text: fullPrompt }],
});
// Configure responseMimeType for native JSON (only when no tools — tools don't support it)
const config: any = {
temperature: options.temperature,
maxOutputTokens: options.maxTokens,
};
if (!options.tools || options.tools.length === 0) {
config.responseMimeType = 'application/json';
}
if (options.tools) {
config.tools = options.tools;
}
const response = await this.client!.models.generateContent({
model,
contents,
config,
});
const durationMs = Date.now() - startTime;
lastUsage = response.usageMetadata;
this.logUsage(
`generateJSON (attempt ${attempt}/${maxRetries})`,
model,
response.usageMetadata,
durationMs,
);
const rawText = (response.text || '').trim();
// Try to extract and parse JSON
const jsonStr = this.extractJSON(rawText);
const data = JSON.parse(jsonStr) as T;
// Validate with Zod schema if provided
if (options.zodSchema) {
const validated = options.zodSchema.parse(data);
return { data: validated as T, usage: lastUsage };
}
return { data, usage: lastUsage };
} catch (error) {
lastError = error as Error;
const isParseError =
error instanceof SyntaxError ||
(error instanceof ZodError) ||
(error instanceof Error &&
error.message.includes('Failed to extract JSON'));
const isRetryable = isParseError || this.isRetryableError(error);
if (isRetryable && attempt < maxRetries) {
const backoffMs = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
this.logger.warn(
`JSON generation attempt ${attempt}/${maxRetries} failed (${error instanceof Error ? error.message : 'unknown'}). Retrying in ${backoffMs}ms...`,
);
await this.sleep(backoffMs);
continue;
}
// Log failure details
if (error instanceof ZodError) {
this.logger.error(
`Zod validation failed after ${attempt} attempts: ${error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
);
}
}
const data = JSON.parse(jsonStr) as T;
return { data, usage: response.usage };
} catch (error) {
this.logger.error('Failed to parse JSON response', error);
throw new Error('Failed to parse AI response as JSON');
}
// All retries exhausted
throw new GeminiException(
`Failed to generate valid JSON after ${maxRetries} attempts: ${lastError?.message}`,
GeminiErrorType.JSON_PARSE_FAILED,
lastError,
false,
);
}
// ============================================
// Image Generation
// ============================================
/**
* Generate an image using Google Imagen (Nano Banana)
*
@@ -246,9 +416,9 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
* @returns Base64 encoded image data URI
*/
async generateImage(prompt: string): Promise<string> {
if (!this.isAvailable()) {
throw new Error('Gemini AI is not available. Check your configuration.');
}
this.ensureAvailable();
const startTime = Date.now();
try {
// Use Imagen 3.0 (Nano Banana Pro)
@@ -263,6 +433,11 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
},
})) as any;
const durationMs = Date.now() - startTime;
this.logger.log(
`Image generated in ${durationMs}ms (model: ${model})`,
);
if (
response.images &&
response.images.length > 0 &&
@@ -272,11 +447,209 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
return `data:image/png;base64,${response.images[0].image}`;
}
throw new Error('No image returned from Gemini');
throw new GeminiException(
'No image returned from Gemini',
GeminiErrorType.INVALID_RESPONSE,
);
} catch (error) {
if (error instanceof GeminiException) throw error;
this.logger.error('Gemini image generation failed', error);
// Fallback or rethrow
throw error;
throw this.classifyError(error);
}
}
// ============================================
// Private Helpers
// ============================================
/**
* Ensure Gemini client is available, throw typed exception if not
*/
private ensureAvailable(): void {
if (!this.isAvailable()) {
throw new GeminiException(
'Gemini AI is not available. Check your configuration.',
GeminiErrorType.UNAVAILABLE,
);
}
}
/**
* Extract JSON from a raw AI response using multiple strategies:
* 1. Direct parse (cleanest case)
* 2. Strip markdown code blocks
* 3. Find first { or [ and match to closing bracket
* 4. Remove trailing commas and retry
*/
private extractJSON(raw: string): string {
// Strategy 1: Direct parse attempt
try {
JSON.parse(raw);
return raw;
} catch {
// Continue to next strategy
}
// Strategy 2: Strip markdown code blocks (```json ... ``` or ``` ... ```)
const codeBlockMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
if (codeBlockMatch) {
const extracted = codeBlockMatch[1].trim();
try {
JSON.parse(extracted);
return extracted;
} catch {
// Continue with the extracted content for further cleaning
raw = extracted;
}
}
// Strategy 3: Find the first { or [ and match to the last } or ]
const objectStart = raw.indexOf('{');
const arrayStart = raw.indexOf('[');
let start = -1;
let endChar = '';
if (objectStart >= 0 && (arrayStart < 0 || objectStart < arrayStart)) {
start = objectStart;
endChar = '}';
} else if (arrayStart >= 0) {
start = arrayStart;
endChar = ']';
}
if (start >= 0) {
const end = raw.lastIndexOf(endChar);
if (end > start) {
const candidate = raw.substring(start, end + 1);
try {
JSON.parse(candidate);
return candidate;
} catch {
// Strategy 4: Remove trailing commas and retry
const cleaned = candidate
.replace(/,\s*([\]}])/g, '$1') // Remove trailing commas
.replace(/'/g, '"') // Replace single quotes with double quotes
.replace(/(\w+)\s*:/g, '"$1":') // Quote unquoted keys
.replace(/""(\w+)""/g, '"$1"'); // Fix double-quoted keys
try {
JSON.parse(cleaned);
return cleaned;
} catch {
// Last resort: return the candidate anyway, caller will handle error
}
}
}
}
throw new Error(
`Failed to extract JSON from AI response (length: ${raw.length})`,
);
}
/**
* Classify an error into a typed GeminiException
*/
private classifyError(error: any): GeminiException {
if (error instanceof GeminiException) return error;
const message = error?.message || String(error);
const status = error?.status || error?.statusCode;
// Rate limiting
if (status === 429 || message.includes('429') || message.includes('RATE_LIMIT') || message.includes('rate limit')) {
return new GeminiException(
'Gemini API rate limit exceeded. Please wait before retrying.',
GeminiErrorType.RATE_LIMIT,
error,
true,
);
}
// Quota
if (message.includes('QUOTA') || message.includes('quota') || status === 403) {
return new GeminiException(
'Gemini API quota exceeded.',
GeminiErrorType.QUOTA_EXCEEDED,
error,
false,
);
}
// Safety
if (message.includes('SAFETY') || message.includes('safety') || message.includes('blocked')) {
return new GeminiException(
'Content was blocked by safety filters. Try rephrasing the prompt.',
GeminiErrorType.SAFETY_BLOCKED,
error,
false,
);
}
// Timeout
if (message.includes('TIMEOUT') || message.includes('timeout') || message.includes('DEADLINE_EXCEEDED')) {
return new GeminiException(
'Gemini API request timed out.',
GeminiErrorType.TIMEOUT,
error,
true,
);
}
// Generic
return new GeminiException(
`Gemini API error: ${message}`,
GeminiErrorType.UNKNOWN,
error,
true,
);
}
/**
* Check if an error is retryable
*/
private isRetryableError(error: any): boolean {
if (error instanceof GeminiException) return error.retryable;
const message = error?.message || '';
return (
message.includes('429') ||
message.includes('RATE_LIMIT') ||
message.includes('TIMEOUT') ||
message.includes('DEADLINE_EXCEEDED') ||
message.includes('UNAVAILABLE') ||
message.includes('INTERNAL')
);
}
/**
* Log AI usage metrics for monitoring
*/
private logUsage(
operation: string,
model: string,
usage: any,
durationMs: number,
): void {
if (usage) {
this.logger.log(
`AI Usage [${operation}] model=${model} ` +
`prompt=${usage.promptTokenCount || '?'} ` +
`completion=${usage.candidatesTokenCount || '?'} ` +
`total=${usage.totalTokenCount || '?'} ` +
`duration=${durationMs}ms`,
);
} else {
this.logger.log(
`AI Usage [${operation}] model=${model} duration=${durationMs}ms`,
);
}
}
/**
* Sleep helper for retry backoff
*/
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,191 @@
/**
* Analysis Prompt Builders
*
* Prompts for AI-powered content analysis:
* - Neuro Marketing Analysis (Cialdini's 6 Principles)
* - YouTube Algorithm Audit
* - Commercial Brief (Sponsorship Analysis)
* - Visual Asset Keywords
*
* Used in: AnalysisService
*/
// ============================================
// Neuro Marketing Analysis
// ============================================
export interface NeuroAnalysisInput {
fullScript: string;
}
export function buildNeuroAnalysisPrompt(input: NeuroAnalysisInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Analyze this script using Consumer Neuroscience and Cialdini's 6 Principles of Persuasion.
Script:
${input.fullScript.substring(0, 10000)}
Provide:
1. Engagement Score (0-100): How well does it capture attention?
2. Dopamine Score (0-100): Does it create anticipation & reward loops?
3. Clarity Score (0-100): Is the message clear and memorable?
4. Cialdini's Persuasion Metrics (0-100 each):
- Reciprocity: Does it give value first?
- Scarcity: Does it create urgency?
- Authority: Does it establish credibility?
- Consistency: Does it align with viewer beliefs?
- Liking: Is the tone likeable/relatable?
- Social Proof: Does it reference others' actions?
5. Neuro Metrics:
- Attention Hooks: Moments that grab attention
- Emotional Triggers: Points that evoke emotion
- Memory Anchors: Unique/memorable elements
- Action Drivers: CTAs or challenges
6. Suggestions: 3-5 specific improvements`,
temperature: 0.6,
schema: `{
"engagementScore": 0,
"dopamineScore": 0,
"clarityScore": 0,
"persuasionMetrics": {
"reciprocity": 0, "scarcity": 0, "authority": 0,
"consistency": 0, "liking": 0, "socialProof": 0
},
"neuroMetrics": {
"attentionHooks": ["..."], "emotionalTriggers": ["..."],
"memoryAnchors": ["..."], "actionDrivers": ["..."]
},
"suggestions": ["..."]
}`,
};
}
// ============================================
// YouTube Audit
// ============================================
export interface YoutubeAuditInput {
topic: string;
fullScript: string;
}
export function buildYoutubeAuditPrompt(input: YoutubeAuditInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Perform a YouTube Algorithm Audit on this script for topic "${input.topic}".
Script:
${input.fullScript.substring(0, 10000)}
Analyze and provide:
1. Hook Score (0-100): First 10 seconds effectiveness
2. Pacing Score (0-100): Does it maintain momentum?
3. Viral Potential (0-100): Shareability factor
4. Retention Analysis: 3-5 potential drop-off points with time, issue, suggestion, severity (High/Medium/Low)
5. Thumbnail Concepts: 3 high-CTR thumbnail ideas with:
- Concept name, Visual description, Text overlay
- Color psychology, Emotion target, AI generation prompt
6. Title Options: 5 clickable titles (curiosity gap, numbers, power words)
7. Community Post: Engaging post to tease the video
8. Pinned Comment: Engagement-driving first comment
9. SEO Description: Optimized video description with keywords
10. Keywords: 10 relevant search keywords`,
temperature: 0.7,
schema: `{
"hookScore": 0, "pacingScore": 0, "viralPotential": 0,
"retentionAnalysis": [{ "time": "0:30", "issue": "...", "suggestion": "...", "severity": "High" }],
"thumbnails": [{ "conceptName": "...", "visualDescription": "...", "textOverlay": "...", "colorPsychology": "...", "emotionTarget": "...", "aiPrompt": "..." }],
"titles": ["..."],
"communityPost": "...", "pinnedComment": "...",
"description": "...", "keywords": ["..."]
}`,
};
}
// ============================================
// Commercial Brief
// ============================================
export interface CommercialBriefInput {
topic: string;
targetAudience: string[];
contentType: string;
fullScript: string;
}
export function buildCommercialBriefPrompt(input: CommercialBriefInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Analyze this content for commercial viability and sponsorship opportunities.
Topic: "${input.topic}"
Audience: ${input.targetAudience.join(', ')}
Content Type: ${input.contentType}
Script excerpt:
${input.fullScript.substring(0, 5000)}
Provide:
1. Viability Score (1-10 scale as string): "8/10"
2. Viability Reason: Why this content is commercially viable
3. Sponsor Suggestions (3-5 potential sponsors):
- Company name, Industry
- Match reason (why this sponsor fits)
- Email draft (outreach template)`,
temperature: 0.6,
schema: `{
"viabilityScore": "8/10",
"viabilityReason": "...",
"sponsors": [{ "name": "...", "industry": "...", "matchReason": "...", "emailDraft": "..." }]
}`,
};
}
// ============================================
// Visual Asset Keywords
// ============================================
export interface VisualAssetKeywordsInput {
topic: string;
count: number;
}
export function buildVisualAssetKeywordsPrompt(
input: VisualAssetKeywordsInput,
): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Generate ${input.count} specific, simple visual keywords for an image generator about "${input.topic}".
Format: "subject action context style". Keep it English, concise, no special chars.`,
temperature: 0.8,
schema: '["keyword1", "keyword2", ...]',
};
}

View File

@@ -0,0 +1,38 @@
/**
* Character Generation Prompt Builder
*
* Uses Alan C. Hueth's "Triunity of Character" model to create
* rich character profiles for video content.
*
* Used in: ResearchService.generateCharacters()
*/
export interface CharacterGenerationInput {
contentType: string;
topic: string;
language: string;
}
export function buildCharacterGenerationPrompt(
input: CharacterGenerationInput,
): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Create Character Profiles for a ${input.contentType} about "${input.topic}".
Use Alan C. Hueth's "Triunity of Character" model:
1. Values (Inner belief)
2. Traits (Personality)
3. Mannerisms (External behavior)
If format is non-fiction (Youtube Doc), create a 'Host/Narrator' persona and potentially an 'Antagonist' (e.g., The Problem, Time, A Rival).
Language: ${input.language}.`,
temperature: 0.8,
schema:
'[{ "name": "Name", "role": "Protagonist", "values": "...", "traits": "...", "mannerisms": "..." }]',
};
}

View File

@@ -0,0 +1,51 @@
/**
* Deep Research Prompt Builders
*
* Two-stage prompts:
* 1. Generate search queries for a topic
* 2. Find high-quality web sources for each query
*
* Used in: ResearchService.performDeepResearch()
*/
export interface SearchQueryInput {
topic: string;
briefContext: string;
language: string;
}
export function buildSearchQueryPrompt(input: SearchQueryInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Generate 5 specific Google Search queries for "${input.topic}".
Context: ${input.briefContext}. Language: ${input.language}.
Return strictly a JSON array of strings.`,
temperature: 0.7,
schema: '["query1", "query2", ...]',
};
}
export interface SourceSearchInput {
query: string;
language: string;
}
export function buildSourceSearchPrompt(input: SourceSearchInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Find 3 high-quality web sources for: ${input.query}. Language: ${input.language}.
Return JSON array: [{ "title": string, "url": string, "snippet": string, "type": "article" }]`,
temperature: 0.5,
schema: '[{ "title": "...", "url": "...", "snippet": "...", "type": "article" }]',
};
}

View File

@@ -0,0 +1,53 @@
/**
* Discovery Questions Prompt Builder
*
* Generates provocative "Screenwriter's Room" style questions
* to help shape the narrative arc for a given topic.
*
* Used in: ResearchService.generateDiscoveryQuestions()
*/
export interface DiscoveryQuestionsInput {
topic: string;
language: string;
existingQuestions?: string[];
}
export function buildDiscoveryQuestionsPrompt(
input: DiscoveryQuestionsInput,
): {
prompt: string;
temperature: number;
schema: string;
} {
const existingContext =
input.existingQuestions && input.existingQuestions.length > 0
? `Avoid these questions: ${input.existingQuestions.join(', ')}`
: '';
return {
prompt: `You are an expert Screenwriter and Creative Director. Topic: "${input.topic}".
PHASE 1: DEEP DIVE
Think like a filmmaker. We are not just making a video; we are telling a story.
Analyze the topic "${input.topic}" to find the drama, the conflict, and the human element.
PHASE 2: INTERROGATION
Ask 3-4 provocative, "Screenwriter's Room" style questions to help shape the narrative arc.
DO NOT ASK: "What is the goal?" or "Who is the audience?".
INSTEAD ASK (Examples):
- "What is the 'Inciting Incident' that makes this topic urgent right now?"
- "If this topic was a character, what would be its fatal flaw?"
- "What is the 'Villain' (opposing force or misconception) we are fighting against?"
- "What is the emotional climax you want the viewer to feel at the end?"
${existingContext}
Output Language: ${input.language}.`,
temperature: 0.9,
schema: '{ "questions": ["Question 1", "Question 2", "Question 3", "Question 4"] }',
};
}

View File

@@ -0,0 +1,54 @@
/**
* SkriptAI Prompt Index
*
* Centralized exports for all AI prompt builders.
* Each prompt is a pure function that takes typed input and returns
* { prompt, temperature, schema } — ready to pass to GeminiService methods.
*/
// Discovery & Research
export {
buildDiscoveryQuestionsPrompt,
type DiscoveryQuestionsInput,
} from './discovery-questions.prompt';
export {
buildSearchQueryPrompt,
buildSourceSearchPrompt,
type SearchQueryInput,
type SourceSearchInput,
} from './deep-research.prompt';
// Characters & Logline
export {
buildCharacterGenerationPrompt,
type CharacterGenerationInput,
} from './character-generation.prompt';
export { buildLoglinePrompt, type LoglineInput } from './logline.prompt';
// Script Generation
export {
buildScriptOutlinePrompt,
buildChapterSegmentPrompt,
buildSegmentRewritePrompt,
buildSegmentImagePrompt,
calculateTargetWordCount,
calculateEstimatedChapters,
type ScriptOutlineInput,
type ChapterSegmentInput,
type SegmentRewriteInput,
type SegmentImagePromptInput,
} from './script-generation.prompt';
// Analysis
export {
buildNeuroAnalysisPrompt,
buildYoutubeAuditPrompt,
buildCommercialBriefPrompt,
buildVisualAssetKeywordsPrompt,
type NeuroAnalysisInput,
type YoutubeAuditInput,
type CommercialBriefInput,
type VisualAssetKeywordsInput,
} from './analysis.prompt';

View File

@@ -0,0 +1,30 @@
/**
* Logline & High Concept Prompt Builder
*
* Uses Hollywood Producer persona with Dallas Jones formula
* to create compelling loglines and high concept premises.
*
* Used in: ResearchService.generateLogline()
*/
export interface LoglineInput {
topic: string;
sourceContext: string;
language: string;
}
export function buildLoglinePrompt(input: LoglineInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Act as a Hollywood Producer. Topic: ${input.topic}. Material: ${input.sourceContext}.
Create a "High Concept" premise and a "Logline" (Max 25 words, Dallas Jones formula).
Language: ${input.language}.`,
temperature: 0.9,
schema: '{ "logline": "...", "highConcept": "..." }',
};
}

View File

@@ -0,0 +1,191 @@
/**
* Script Generation Prompt Builders
*
* Two-phase script generation:
* - Phase 1: Content outline (chapters, SEO, thumbnails)
* - Phase 2: Per-chapter segment generation
* - Segment rewrite with style change
* - Segment image prompt generation
*
* Used in: ScriptsService.generateScript(), rewriteSegment(), generateSegmentImage()
*/
// ============================================
// Phase 1: Outline
// ============================================
export interface ScriptOutlineInput {
topic: string;
logline: string;
characterContext: string;
speechStyles: string[];
targetAudience: string[];
contentType: string;
targetDuration: string;
targetWordCount: number;
estimatedChapters: number;
sourceContext: string;
briefContext: string;
}
export function buildScriptOutlinePrompt(input: ScriptOutlineInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Create a CONTENT OUTLINE.
Topic: "${input.topic}"
Logline: "${input.logline}"
Characters: ${input.characterContext}
Styles: ${input.speechStyles.join(', ')}. Audience: ${input.targetAudience.join(', ')}.
Format: ${input.contentType}. Target Duration: ${input.targetDuration}. Target Total Word Count: ${input.targetWordCount}.
Generate exactly ${input.estimatedChapters} chapters.
Material: ${input.sourceContext.substring(0, 15000)}
Brief: ${input.briefContext}`,
temperature: 0.7,
schema: `{
"title": "Title",
"seoDescription": "Desc",
"tags": ["tag1"],
"thumbnailIdeas": ["Idea 1"],
"chapters": [{ "title": "Chap 1", "focus": "Summary", "type": "Intro" }]
}`,
};
}
// ============================================
// Phase 2: Chapter → Segments
// ============================================
export interface ChapterSegmentInput {
chapterIndex: number;
totalChapters: number;
chapterTitle: string;
chapterFocus: string;
chapterType: string;
speechStyles: string[];
targetAudience: string[];
characterContext: string;
language: string;
}
export function buildChapterSegmentPrompt(input: ChapterSegmentInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Write Script Segment ${input.chapterIndex + 1}/${input.totalChapters}.
Chapter: "${input.chapterTitle}". Focus: ${input.chapterFocus}.
Style: ${input.speechStyles.join(', ')}.
Audience: ${input.targetAudience.join(', ')}.
Characters: ${input.characterContext}.
Target Length: ~200 words.
Language: ${input.language}.`,
temperature: 0.8,
schema: `[{
"segmentType": "${input.chapterType || 'Body'}",
"narratorScript": "Full text...",
"visualDescription": "Detailed visual explanation...",
"videoPrompt": "Cinematic shot of [subject], 4k...",
"imagePrompt": "Hyper-realistic photo of [subject]...",
"onScreenText": "Overlay text...",
"stockQuery": "Pexels keyword",
"audioCues": "SFX..."
}]`,
};
}
// ============================================
// Segment Rewrite
// ============================================
export interface SegmentRewriteInput {
currentScript: string;
newStyle: string;
topic: string;
language: string;
}
export function buildSegmentRewritePrompt(input: SegmentRewriteInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Rewrite this script segment.
Current Text: "${input.currentScript}"
Goal: Change style to "${input.newStyle}".
Context: Topic is "${input.topic}". Language: ${input.language}.
Principles: Show Don't Tell, Subtext.`,
temperature: 0.85,
schema: `{
"narratorScript": "New text...",
"visualDescription": "Updated visual...",
"onScreenText": "Updated overlay...",
"audioCues": "Updated audio..."
}`,
};
}
// ============================================
// Segment Image Prompt
// ============================================
export interface SegmentImagePromptInput {
topic: string;
narratorScript: string;
visualDescription: string;
}
export function buildSegmentImagePrompt(input: SegmentImagePromptInput): {
prompt: string;
temperature: number;
schema: string;
} {
return {
prompt: `Create a detailed AI Image Generation Prompt and a Video Generation Prompt for this script segment.
Topic: "${input.topic}"
Segment Content: "${input.narratorScript}"
Visual Context: "${input.visualDescription}"
Goal: Create a highly detailed, cinematic, and artistic prompt optimized for tools like Midjourney, Flux, or Runway.
Style: Cinematic, highly detailed, 8k, professional lighting.`,
temperature: 0.7,
schema: `{
"imagePrompt": "Full detailed image prompt...",
"videoPrompt": "Full detailed video prompt..."
}`,
};
}
// ============================================
// Helpers
// ============================================
/**
* Calculate target word count based on duration string
*/
export function calculateTargetWordCount(targetDuration: string): number {
if (targetDuration.includes('Short')) return 140;
if (targetDuration.includes('Standard')) return 840;
if (targetDuration.includes('Long')) return 1680;
if (targetDuration.includes('Deep Dive')) return 2800;
return 840;
}
/**
* Calculate estimated chapters based on word count
*/
export function calculateEstimatedChapters(targetWordCount: number): number {
return Math.ceil(targetWordCount / 200);
}

View File

@@ -2,6 +2,12 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { GeminiService } from '../../gemini/gemini.service';
import { NeuroAnalysisResult, YoutubeAudit } from '../types/skriptai.types';
import {
buildNeuroAnalysisPrompt,
buildYoutubeAuditPrompt,
buildCommercialBriefPrompt,
buildVisualAssetKeywordsPrompt,
} from '../prompts';
/**
* AnalysisService
@@ -43,56 +49,12 @@ export class AnalysisService {
.map((s) => s.narratorScript)
.join('\n\n');
const prompt = `Analyze this script using Consumer Neuroscience and Cialdini's 6 Principles of Persuasion.
Script:
${fullScript.substring(0, 10000)}
Provide:
1. Engagement Score (0-100): How well does it capture attention?
2. Dopamine Score (0-100): Does it create anticipation & reward loops?
3. Clarity Score (0-100): Is the message clear and memorable?
4. Cialdini's Persuasion Metrics (0-100 each):
- Reciprocity: Does it give value first?
- Scarcity: Does it create urgency?
- Authority: Does it establish credibility?
- Consistency: Does it align with viewer beliefs?
- Liking: Is the tone likeable/relatable?
- Social Proof: Does it reference others' actions?
5. Neuro Metrics:
- Attention Hooks: Moments that grab attention
- Emotional Triggers: Points that evoke emotion
- Memory Anchors: Unique/memorable elements
- Action Drivers: CTAs or challenges
6. Suggestions: 3-5 specific improvements
Return JSON: {
"engagementScore": number,
"dopamineScore": number,
"clarityScore": number,
"persuasionMetrics": {
"reciprocity": number,
"scarcity": number,
"authority": number,
"consistency": number,
"liking": number,
"socialProof": number
},
"neuroMetrics": {
"attentionHooks": ["..."],
"emotionalTriggers": ["..."],
"memoryAnchors": ["..."],
"actionDrivers": ["..."]
},
"suggestions": ["..."]
}`;
const promptData = buildNeuroAnalysisPrompt({ fullScript });
const resp = await this.gemini.generateJSON<NeuroAnalysisResult>(
prompt,
'{ engagementScore, dopamineScore, clarityScore, persuasionMetrics, neuroMetrics, suggestions }',
promptData.prompt,
promptData.schema,
{ temperature: promptData.temperature },
);
// Save to project
@@ -124,52 +86,15 @@ Return JSON: {
.map((s) => s.narratorScript)
.join('\n\n');
const prompt = `Perform a YouTube Algorithm Audit on this script for topic "${project.topic}".
Script:
${fullScript.substring(0, 10000)}
Analyze and provide:
1. Hook Score (0-100): First 10 seconds effectiveness
2. Pacing Score (0-100): Does it maintain momentum?
3. Viral Potential (0-100): Shareability factor
4. Retention Analysis: 3-5 potential drop-off points with time, issue, suggestion, severity (High/Medium/Low)
5. Thumbnail Concepts: 3 high-CTR thumbnail ideas with:
- Concept name
- Visual description
- Text overlay
- Color psychology
- Emotion target
- AI generation prompt
6. Title Options: 5 clickable titles (curiosity gap, numbers, power words)
7. Community Post: Engaging post to tease the video
8. Pinned Comment: Engagement-driving first comment
9. SEO Description: Optimized video description with keywords
10. Keywords: 10 relevant search keywords
Return JSON: {
"hookScore": number,
"pacingScore": number,
"viralPotential": number,
"retentionAnalysis": [{ "time": "0:30", "issue": "...", "suggestion": "...", "severity": "High" }],
"thumbnails": [{ "conceptName": "...", "visualDescription": "...", "textOverlay": "...", "colorPsychology": "...", "emotionTarget": "...", "aiPrompt": "..." }],
"titles": ["..."],
"communityPost": "...",
"pinnedComment": "...",
"description": "...",
"keywords": ["..."]
}`;
const promptData = buildYoutubeAuditPrompt({
topic: project.topic,
fullScript,
});
const resp = await this.gemini.generateJSON<YoutubeAudit>(
prompt,
'{ hookScore, pacingScore, viralPotential, retentionAnalysis, thumbnails, titles, communityPost, pinnedComment, description, keywords }',
promptData.prompt,
promptData.schema,
{ temperature: promptData.temperature },
);
// Save to project
@@ -201,37 +126,12 @@ Return JSON: {
.map((s) => s.narratorScript)
.join('\n\n');
const prompt = `Analyze this content for commercial viability and sponsorship opportunities.
Topic: "${project.topic}"
Audience: ${project.targetAudience.join(', ')}
Content Type: ${project.contentType}
Script excerpt:
${fullScript.substring(0, 5000)}
Provide:
1. Viability Score (1-10 scale as string): "8/10"
2. Viability Reason: Why this content is commercially viable
3. Sponsor Suggestions (3-5 potential sponsors):
- Company name
- Industry
- Match reason (why this sponsor fits)
- Email draft (outreach template)
Return JSON: {
"viabilityScore": "8/10",
"viabilityReason": "...",
"sponsors": [
{
"name": "Company Name",
"industry": "Tech/Finance/etc",
"matchReason": "...",
"emailDraft": "..."
}
]
}`;
const promptData = buildCommercialBriefPrompt({
topic: project.topic,
targetAudience: project.targetAudience,
contentType: project.contentType,
fullScript,
});
const resp = await this.gemini.generateJSON<{
viabilityScore: string;
@@ -242,7 +142,9 @@ Return JSON: {
matchReason: string;
emailDraft: string;
}[];
}>(prompt, '{ viabilityScore, viabilityReason, sponsors }');
}>(promptData.prompt, promptData.schema, {
temperature: promptData.temperature,
});
// Save to project
await this.prisma.scriptProject.update({
@@ -253,12 +155,6 @@ Return JSON: {
return resp.data;
}
/**
* Generate thumbnails using external image service
*
* @param prompt - Image generation prompt
* @returns Generated image URL
*/
/**
* Generate thumbnails using external image service
* Applies "Nano Banana" prompt enrichment for high-quality results.
@@ -302,13 +198,15 @@ Return JSON: {
throw new NotFoundException(`Project with ID ${projectId} not found`);
}
const prompt = `Generate ${count} specific, simple visual keywords for an image generator about "${project.topic}".
Format: "subject action context style". Keep it English, concise, no special chars.
Return JSON array of strings.`;
const promptData = buildVisualAssetKeywordsPrompt({
topic: project.topic,
count,
});
const resp = await this.gemini.generateJSON<string[]>(
prompt,
'["keyword1", "keyword2", ...]',
promptData.prompt,
promptData.schema,
{ temperature: promptData.temperature },
);
// Generate image URLs and save to database

View File

@@ -7,6 +7,13 @@ import {
CreateCharacterDto,
} from '../dto';
import { CharacterRole } from '../types/skriptai.types';
import {
buildDiscoveryQuestionsPrompt,
buildSearchQueryPrompt,
buildSourceSearchPrompt,
buildCharacterGenerationPrompt,
buildLoglinePrompt,
} from '../prompts';
/**
* ResearchService
@@ -85,12 +92,8 @@ export class ResearchService {
: project.topic;
// Generate search queries
const queryPrompt = `Generate 5 specific Google Search queries for "${topic}".
Context: ${briefContext}. Language: ${project.language}.
Return strictly a JSON array of strings.`;
let searchQueries: string[] = [];
// Check if Gemini is available for queries
if (!this.gemini.isAvailable()) {
this.logger.warn('Gemini is disabled. Using mock search queries.');
searchQueries = [
@@ -100,10 +103,19 @@ export class ResearchService {
];
} else {
try {
const queryPromptData = buildSearchQueryPrompt({
topic,
briefContext,
language: project.language,
});
const queryResp = await this.gemini.generateJSON<string[]>(
queryPrompt,
'["query1", "query2", ...]',
{ tools: [{ googleSearch: {} }] },
queryPromptData.prompt,
queryPromptData.schema,
{
temperature: queryPromptData.temperature,
tools: [{ googleSearch: {} }],
},
);
searchQueries = queryResp.data;
} catch {
@@ -130,12 +142,15 @@ export class ResearchService {
continue;
}
const sourcePrompt = `Find 3 high-quality web sources for: ${query}. Language: ${project.language}.
Return JSON array: [{ "title": string, "url": string, "snippet": string, "type": "article" }]`;
const sourcePromptData = buildSourceSearchPrompt({
query,
language: project.language,
});
const sourceResp = await this.gemini.generateJSON<
{ title: string; url: string; snippet: string; type: string }[]
>(sourcePrompt, '[{ title, url, snippet, type }]', {
>(sourcePromptData.prompt, sourcePromptData.schema, {
temperature: sourcePromptData.temperature,
tools: [{ googleSearch: {} }],
});
@@ -221,11 +236,6 @@ export class ResearchService {
language: string,
existingQuestions: string[] = [],
) {
const existingContext =
existingQuestions.length > 0
? `Avoid these questions: ${existingQuestions.join(', ')}`
: '';
// Check if Gemini is available
if (!this.gemini.isAvailable()) {
this.logger.warn(
@@ -239,30 +249,16 @@ export class ResearchService {
];
}
const prompt = `You are an expert Screenwriter and Creative Director. Topic: "${topic}".
PHASE 1: DEEP DIVE
Think like a filmmaker. We are not just making a video; we are telling a story.
Analyze the topic "${topic}" to find the drama, the conflict, and the human element.
PHASE 2: INTERROGATION
Ask 3-4 provocative, "Screenwriter's Room" style questions to help shape the narrative arc.
DO NOT ASK: "What is the goal?" or "Who is the audience?".
INSTEAD ASK (Examples):
- "What is the 'Inciting Incident' that makes this topic urgent right now?"
- "If this topic was a character, what would be its fatal flaw?"
- "What is the 'Villain' (opposing force or misconception) we are fighting against?"
- "What is the emotional climax you want the viewer to feel at the end?"
${existingContext}
Output Language: ${language}.
Return JSON object: { "questions": ["Question 1", "Question 2", "Question 3", "Question 4"] }`;
const promptData = buildDiscoveryQuestionsPrompt({
topic,
language,
existingQuestions,
});
const resp = await this.gemini.generateJSON<{ questions: string[] }>(
prompt,
'{ questions: string[] }',
promptData.prompt,
promptData.schema,
{ temperature: promptData.temperature },
);
return resp.data.questions;
@@ -312,15 +308,11 @@ export class ResearchService {
throw new NotFoundException(`Project with ID ${projectId} not found`);
}
const prompt = `Create Character Profiles for a ${project.contentType} about "${project.topic}".
Use Alan C. Hueth's "Triunity of Character" model:
1. Values (Inner belief)
2. Traits (Personality)
3. Mannerisms (External behavior)
If format is non-fiction (Youtube Doc), create a 'Host/Narrator' persona and potentially an 'Antagonist' (e.g., The Problem, Time, A Rival).
Language: ${project.language}.
Return JSON Array: [{ "name": "Name", "role": "Protagonist", "values": "...", "traits": "...", "mannerisms": "..." }]`;
const promptData = buildCharacterGenerationPrompt({
contentType: project.contentType,
topic: project.topic,
language: project.language,
});
const resp = await this.gemini.generateJSON<
{
@@ -330,7 +322,9 @@ export class ResearchService {
traits: string;
mannerisms: string;
}[]
>(prompt, '[{ name, role, values, traits, mannerisms }]');
>(promptData.prompt, promptData.schema, {
temperature: promptData.temperature,
});
// Save characters to database
const characters = await Promise.all(
@@ -371,15 +365,18 @@ export class ResearchService {
const sourceContext = project.sources.map((s) => s.snippet).join('\n');
const prompt = `Act as a Hollywood Producer. Topic: ${project.topic}. Material: ${sourceContext}.
Create a "High Concept" premise and a "Logline" (Max 25 words, Dallas Jones formula).
Language: ${project.language}.
Return JSON: { "logline": "...", "highConcept": "..." }`;
const promptData = buildLoglinePrompt({
topic: project.topic,
sourceContext,
language: project.language,
});
const resp = await this.gemini.generateJSON<{
logline: string;
highConcept: string;
}>(prompt, '{ logline, highConcept }');
}>(promptData.prompt, promptData.schema, {
temperature: promptData.temperature,
});
// Update project
await this.prisma.scriptProject.update({

View File

@@ -2,10 +2,15 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { GeminiService } from '../../gemini/gemini.service';
import { CreateSegmentDto, UpdateSegmentDto } from '../dto';
import { AnalysisService } from './analysis.service';
// AI_CONFIG is only used for model selection reference
import {
buildScriptOutlinePrompt,
buildChapterSegmentPrompt,
buildSegmentRewritePrompt,
buildSegmentImagePrompt,
calculateTargetWordCount,
calculateEstimatedChapters,
} from '../prompts';
/**
* ScriptsService
@@ -138,34 +143,24 @@ export class ScriptsService {
)
.join('\n');
// Calculate target word count based on duration
let targetWordCount = 840;
if (project.targetDuration.includes('Short')) targetWordCount = 140;
else if (project.targetDuration.includes('Standard')) targetWordCount = 840;
else if (project.targetDuration.includes('Long')) targetWordCount = 1680;
else if (project.targetDuration.includes('Deep Dive'))
targetWordCount = 2800;
// Calculate target metrics
const targetWordCount = calculateTargetWordCount(project.targetDuration);
const estimatedChapters = calculateEstimatedChapters(targetWordCount);
const estimatedChapters = Math.ceil(targetWordCount / 200);
// PHASE 1: Generate Outline
const outlinePrompt = `
Create a CONTENT OUTLINE.
Topic: "${project.topic}"
Logline: "${project.logline || ''}"
Characters: ${characterContext}
Styles: ${project.speechStyle.join(', ')}. Audience: ${project.targetAudience.join(', ')}.
Format: ${project.contentType}. Target Duration: ${project.targetDuration}. Target Total Word Count: ${targetWordCount}.
Generate exactly ${estimatedChapters} chapters.
Material: ${sourceContext.substring(0, 15000)}
Brief: ${briefContext}
Return JSON: {
"title": "Title", "seoDescription": "Desc", "tags": ["tag1"],
"thumbnailIdeas": ["Idea 1"],
"chapters": [{ "title": "Chap 1", "focus": "Summary", "type": "Intro" }]
}
`;
// PHASE 1: Generate Outline using prompt builder
const outlinePromptData = buildScriptOutlinePrompt({
topic: project.topic,
logline: project.logline || '',
characterContext,
speechStyles: project.speechStyle,
targetAudience: project.targetAudience,
contentType: project.contentType,
targetDuration: project.targetDuration,
targetWordCount,
estimatedChapters,
sourceContext,
briefContext,
});
const outlineResp = await this.gemini.generateJSON<{
title: string;
@@ -173,10 +168,9 @@ export class ScriptsService {
tags: string[];
thumbnailIdeas: string[];
chapters: { title: string; focus: string; type: string }[];
}>(
outlinePrompt,
'{ title, seoDescription, tags, thumbnailIdeas, chapters }',
);
}>(outlinePromptData.prompt, outlinePromptData.schema, {
temperature: outlinePromptData.temperature,
});
const outlineData = outlineResp.data;
@@ -191,38 +185,30 @@ export class ScriptsService {
},
});
// PHASE 2: Generate each chapter
// PHASE 2: Generate each chapter using prompt builder
const generatedSegments: any[] = [];
let timeOffset = 0;
for (let i = 0; i < outlineData.chapters.length; i++) {
const chapter = outlineData.chapters[i];
const chapterPrompt = `
Write Script Segment ${i + 1}/${outlineData.chapters.length}.
Chapter: "${chapter.title}". Focus: ${chapter.focus}.
Style: ${project.speechStyle.join(', ')}.
Audience: ${project.targetAudience.join(', ')}.
Characters: ${characterContext}.
Target Length: ~200 words.
Language: ${project.language}.
Return JSON Array: [{
"segmentType": "${chapter.type || 'Body'}",
"narratorScript": "Full text...",
"visualDescription": "Detailed visual explanation...",
"videoPrompt": "Cinematic shot of [subject], 4k...",
"imagePrompt": "Hyper-realistic photo of [subject]...",
"onScreenText": "Overlay text...",
"stockQuery": "Pexels keyword",
"audioCues": "SFX..."
}]
`;
const chapterPromptData = buildChapterSegmentPrompt({
chapterIndex: i,
totalChapters: outlineData.chapters.length,
chapterTitle: chapter.title,
chapterFocus: chapter.focus,
chapterType: chapter.type,
speechStyles: project.speechStyle,
targetAudience: project.targetAudience,
characterContext,
language: project.language,
});
try {
const segmentResp = await this.gemini.generateJSON<any[]>(
chapterPrompt,
'[{ segmentType, narratorScript, visualDescription, videoPrompt, imagePrompt, onScreenText, stockQuery, audioCues }]',
chapterPromptData.prompt,
chapterPromptData.schema,
{ temperature: chapterPromptData.temperature },
);
for (const seg of segmentResp.data) {
@@ -293,30 +279,21 @@ export class ScriptsService {
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
}
const prompt = `
Rewrite this script segment.
Current Text: "${segment.narratorScript}"
Goal: Change style to "${newStyle}".
Context: Topic is "${segment.project.topic}". Language: ${segment.project.language}.
Principles: Show Don't Tell, Subtext.
Return JSON: {
"narratorScript": "New text...",
"visualDescription": "Updated visual...",
"onScreenText": "Updated overlay...",
"audioCues": "Updated audio..."
}
`;
const promptData = buildSegmentRewritePrompt({
currentScript: segment.narratorScript || '',
newStyle,
topic: segment.project.topic,
language: segment.project.language,
});
const rewriteResp = await this.gemini.generateJSON<{
narratorScript: string;
visualDescription: string;
onScreenText: string;
audioCues: string;
}>(
prompt,
'{ narratorScript, visualDescription, onScreenText, audioCues }',
);
}>(promptData.prompt, promptData.schema, {
temperature: promptData.temperature,
});
const data = rewriteResp.data;
const words = data.narratorScript
@@ -359,25 +336,18 @@ export class ScriptsService {
}
// 1. Generate/Refine Image Prompt using LLM
const promptGenPrompt = `
Create a detailed AI Image Generation Prompt and a Video Generation Prompt for this script segment.
Topic: "${segment.project.topic}"
Segment Content: "${segment.narratorScript}"
Visual Context: "${segment.visualDescription}"
Goal: Create a highly detailed, cinematic, and artistic prompt optimized for tools like Midjourney, Flux, or Runway.
Style: Cinematic, highly detailed, 8k, professional lighting.
Return JSON: {
"imagePrompt": "Full detailed image prompt...",
"videoPrompt": "Full detailed video prompt..."
}
`;
const promptData = buildSegmentImagePrompt({
topic: segment.project.topic,
narratorScript: segment.narratorScript || '',
visualDescription: segment.visualDescription || '',
});
const prompts = await this.gemini.generateJSON<{
imagePrompt: string;
videoPrompt: string;
}>(promptGenPrompt, '{ imagePrompt, videoPrompt }');
}>(promptData.prompt, promptData.schema, {
temperature: promptData.temperature,
});
// 2. Use the new image prompt for generation
const imageUrl = await this.analysisService.generateThumbnailImage(