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

@@ -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(