generated from fahricansecer/boilerplate-be
@@ -181,6 +181,10 @@ model ScriptProject {
|
||||
highConcept String? @db.Text
|
||||
includeInterviews Boolean @default(false)
|
||||
|
||||
// Project Status
|
||||
status String @default("DRAFT") // DRAFT, RESEARCHING, SCRIPTING, ANALYZING, COMPLETED
|
||||
currentVersionNumber Int @default(0)
|
||||
|
||||
// SEO Data (stored as JSON)
|
||||
seoTitle String?
|
||||
seoDescription String? @db.Text
|
||||
@@ -205,9 +209,11 @@ model ScriptProject {
|
||||
characters CharacterProfile[]
|
||||
briefItems BriefItem[]
|
||||
visualAssets VisualAsset[]
|
||||
versions ScriptVersion[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([topic])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model ScriptSegment {
|
||||
@@ -312,3 +318,36 @@ model VisualAsset {
|
||||
|
||||
@@index([projectId])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Version History
|
||||
// ============================================
|
||||
|
||||
model ScriptVersion {
|
||||
id String @id @default(uuid())
|
||||
projectId String
|
||||
versionNumber Int
|
||||
label String? // User-defined label, e.g. "Final Draft", "Before Rewrite"
|
||||
generatedBy String @default("AI") // AI | USER | AUTO_SAVE
|
||||
|
||||
// Snapshot data: complete segments at this point in time
|
||||
snapshotData Json // Array of segment objects
|
||||
|
||||
// Optional: SEO snapshot
|
||||
seoSnapshot Json? // { seoTitle, seoDescription, seoTags, thumbnailIdeas }
|
||||
|
||||
// Metadata
|
||||
segmentCount Int @default(0)
|
||||
totalWords Int @default(0)
|
||||
changeNote String? @db.Text // What changed in this version
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([projectId, versionNumber])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './projects.controller';
|
||||
export * from './scripts.controller';
|
||||
export * from './research.controller';
|
||||
export * from './analysis.controller';
|
||||
export * from './versions.controller';
|
||||
|
||||
@@ -99,4 +99,25 @@ export class ScriptsController {
|
||||
async generateSegmentImage(@Param('id') id: string) {
|
||||
return this.scriptsService.generateSegmentImage(id);
|
||||
}
|
||||
|
||||
@Post('segments/:id/regenerate')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Regenerate a single segment with AI' })
|
||||
async regenerateSegment(@Param('id') id: string) {
|
||||
return this.scriptsService.regenerateSegment(id);
|
||||
}
|
||||
|
||||
@Post('regenerate-partial')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Regenerate selected segments with AI' })
|
||||
async regeneratePartial(
|
||||
@Body() body: { projectId: string; segmentIds: string[] },
|
||||
) {
|
||||
return this.scriptsService.regeneratePartial(
|
||||
body.projectId,
|
||||
body.segmentIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
96
src/modules/skriptai/controllers/versions.controller.ts
Normal file
96
src/modules/skriptai/controllers/versions.controller.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { VersionsService } from '../services/versions.service';
|
||||
|
||||
/**
|
||||
* VersionsController
|
||||
*
|
||||
* REST API for managing script version history.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /projects/:projectId/versions — List all versions
|
||||
* - GET /projects/:projectId/versions/:id — Get version details
|
||||
* - POST /projects/:projectId/versions — Manual save (create snapshot)
|
||||
* - POST /projects/:projectId/versions/:id/restore — Restore to version
|
||||
* - DELETE /projects/:projectId/versions/:id — Delete version
|
||||
* - GET /projects/:projectId/versions/compare — Compare two versions
|
||||
*/
|
||||
@ApiTags('SkriptAI - Versions')
|
||||
@ApiBearerAuth()
|
||||
@Controller('skriptai/projects/:projectId/versions')
|
||||
export class VersionsController {
|
||||
private readonly logger = new Logger(VersionsController.name);
|
||||
|
||||
constructor(private readonly versionsService: VersionsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List all versions for a project' })
|
||||
@ApiParam({ name: 'projectId', description: 'Project ID' })
|
||||
async listVersions(@Param('projectId') projectId: string) {
|
||||
return this.versionsService.listVersions(projectId);
|
||||
}
|
||||
|
||||
@Get('compare')
|
||||
@ApiOperation({ summary: 'Compare two versions' })
|
||||
async compareVersions(
|
||||
@Param('projectId') projectId: string,
|
||||
@Query('versionA') versionAId: string,
|
||||
@Query('versionB') versionBId: string,
|
||||
) {
|
||||
return this.versionsService.compareVersions(
|
||||
projectId,
|
||||
versionAId,
|
||||
versionBId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a specific version with full snapshot data' })
|
||||
async getVersion(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('id') versionId: string,
|
||||
) {
|
||||
return this.versionsService.getVersion(projectId, versionId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Manually save current state as a new version' })
|
||||
async createSnapshot(
|
||||
@Param('projectId') projectId: string,
|
||||
@Body() body: { label?: string; changeNote?: string },
|
||||
) {
|
||||
return this.versionsService.createSnapshot(
|
||||
projectId,
|
||||
'USER',
|
||||
body.label,
|
||||
body.changeNote,
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@ApiOperation({ summary: 'Restore project to a specific version' })
|
||||
async restoreVersion(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('id') versionId: string,
|
||||
) {
|
||||
return this.versionsService.restoreVersion(projectId, versionId);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete a specific version' })
|
||||
async deleteVersion(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('id') versionId: string,
|
||||
) {
|
||||
return this.versionsService.deleteVersion(projectId, versionId);
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './projects.service';
|
||||
export * from './scripts.service';
|
||||
export * from './research.service';
|
||||
export * from './analysis.service';
|
||||
export * from './versions.service';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PrismaService } from '../../../database/prisma.service';
|
||||
import { GeminiService } from '../../gemini/gemini.service';
|
||||
import { CreateSegmentDto, UpdateSegmentDto } from '../dto';
|
||||
import { AnalysisService } from './analysis.service';
|
||||
import { VersionsService } from './versions.service';
|
||||
import {
|
||||
buildScriptOutlinePrompt,
|
||||
buildChapterSegmentPrompt,
|
||||
@@ -28,6 +29,7 @@ export class ScriptsService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly gemini: GeminiService,
|
||||
private readonly analysisService: AnalysisService,
|
||||
private readonly versionsService: VersionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -113,6 +115,20 @@ export class ScriptsService {
|
||||
async generateScript(projectId: string) {
|
||||
this.logger.log(`Generating script for project: ${projectId}`);
|
||||
|
||||
// Auto-snapshot current state before regeneration
|
||||
await this.versionsService.createSnapshot(
|
||||
projectId,
|
||||
'AUTO_SAVE',
|
||||
undefined,
|
||||
'Auto-save before script generation',
|
||||
).catch(() => { /* ignore if no segments yet */ });
|
||||
|
||||
// Update status
|
||||
await this.prisma.scriptProject.update({
|
||||
where: { id: projectId },
|
||||
data: { status: 'SCRIPTING' },
|
||||
});
|
||||
|
||||
const project = await this.prisma.scriptProject.findUnique({
|
||||
where: { id: projectId },
|
||||
include: {
|
||||
@@ -364,4 +380,103 @@ export class ScriptsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== REGENERATE (Tek segment / Partial) ==========
|
||||
|
||||
/**
|
||||
* Regenerate a single segment with AI.
|
||||
* Auto-snapshots current state before regeneration.
|
||||
*
|
||||
* @param segmentId - Segment ID to regenerate
|
||||
* @returns Updated segment
|
||||
*/
|
||||
async regenerateSegment(segmentId: string) {
|
||||
const segment = await this.prisma.scriptSegment.findUnique({
|
||||
where: { id: segmentId },
|
||||
include: { project: { include: { characters: true } } },
|
||||
});
|
||||
|
||||
if (!segment) {
|
||||
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
||||
}
|
||||
|
||||
// Auto-snapshot before regeneration
|
||||
await this.versionsService.createSnapshot(
|
||||
segment.projectId,
|
||||
'AUTO_SAVE',
|
||||
undefined,
|
||||
`Auto-save before regenerating segment #${segment.sortOrder + 1}`,
|
||||
).catch(() => {});
|
||||
|
||||
const characterContext = segment.project.characters
|
||||
.map((c) => `${c.name} (${c.role}): Values[${c.values}] Traits[${c.traits}]`)
|
||||
.join('\n');
|
||||
|
||||
const chapterPromptData = buildChapterSegmentPrompt({
|
||||
chapterIndex: segment.sortOrder,
|
||||
totalChapters: 1,
|
||||
chapterTitle: segment.segmentType,
|
||||
chapterFocus: segment.visualDescription || segment.segmentType,
|
||||
chapterType: segment.segmentType,
|
||||
speechStyles: segment.project.speechStyle,
|
||||
targetAudience: segment.project.targetAudience,
|
||||
characterContext,
|
||||
language: segment.project.language,
|
||||
});
|
||||
|
||||
const resp = await this.gemini.generateJSON<any[]>(
|
||||
chapterPromptData.prompt,
|
||||
chapterPromptData.schema,
|
||||
{ temperature: chapterPromptData.temperature },
|
||||
);
|
||||
|
||||
const newSeg = resp.data[0];
|
||||
if (!newSeg) return segment;
|
||||
|
||||
const words = newSeg.narratorScript ? newSeg.narratorScript.split(' ').length : 0;
|
||||
const dur = Math.max(5, Math.ceil(words / (140 / 60)));
|
||||
|
||||
return this.prisma.scriptSegment.update({
|
||||
where: { id: segmentId },
|
||||
data: {
|
||||
narratorScript: newSeg.narratorScript,
|
||||
visualDescription: newSeg.visualDescription,
|
||||
videoPrompt: newSeg.videoPrompt,
|
||||
imagePrompt: newSeg.imagePrompt,
|
||||
onScreenText: newSeg.onScreenText,
|
||||
audioCues: newSeg.audioCues,
|
||||
stockQuery: newSeg.stockQuery,
|
||||
duration: `${dur}s`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate multiple selected segments.
|
||||
*
|
||||
* @param projectId - Project ID
|
||||
* @param segmentIds - Array of segment IDs to regenerate
|
||||
* @returns Updated segments
|
||||
*/
|
||||
async regeneratePartial(projectId: string, segmentIds: string[]) {
|
||||
// Auto-snapshot
|
||||
await this.versionsService.createSnapshot(
|
||||
projectId,
|
||||
'AUTO_SAVE',
|
||||
undefined,
|
||||
`Auto-save before partial regeneration (${segmentIds.length} segments)`,
|
||||
).catch(() => {});
|
||||
|
||||
const results: any[] = [];
|
||||
for (const segId of segmentIds) {
|
||||
try {
|
||||
const result = await this.regenerateSegment(segId);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to regenerate segment ${segId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
382
src/modules/skriptai/services/versions.service.ts
Normal file
382
src/modules/skriptai/services/versions.service.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../database/prisma.service';
|
||||
|
||||
/**
|
||||
* VersionsService
|
||||
*
|
||||
* Manages script version history — automatic snapshots, manual saves,
|
||||
* version restoration, and diff comparison.
|
||||
*
|
||||
* TR: Script versiyon geçmişi yönetimi — otomatik snapshot, manuel kayıt,
|
||||
* versiyona geri dönüş ve karşılaştırma.
|
||||
*/
|
||||
@Injectable()
|
||||
export class VersionsService {
|
||||
private readonly logger = new Logger(VersionsService.name);
|
||||
|
||||
/** Maximum number of versions to keep per project */
|
||||
private readonly MAX_VERSIONS = 20;
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// ========== VERSION LISTING ==========
|
||||
|
||||
/**
|
||||
* List all versions for a project
|
||||
*/
|
||||
async listVersions(projectId: string) {
|
||||
await this.ensureProjectExists(projectId);
|
||||
|
||||
return this.prisma.scriptVersion.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
versionNumber: true,
|
||||
label: true,
|
||||
generatedBy: true,
|
||||
segmentCount: true,
|
||||
totalWords: true,
|
||||
changeNote: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific version with full snapshot data
|
||||
*/
|
||||
async getVersion(projectId: string, versionId: string) {
|
||||
const version = await this.prisma.scriptVersion.findFirst({
|
||||
where: { id: versionId, projectId },
|
||||
});
|
||||
|
||||
if (!version) {
|
||||
throw new NotFoundException(
|
||||
`Version ${versionId} not found for project ${projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
// ========== VERSION CREATION ==========
|
||||
|
||||
/**
|
||||
* Create a snapshot of the current script state.
|
||||
* Called automatically before AI generation or manually by user.
|
||||
*
|
||||
* @param projectId - Project ID
|
||||
* @param generatedBy - Who created this version: 'AI' | 'USER' | 'AUTO_SAVE'
|
||||
* @param label - Optional label for the version
|
||||
* @param changeNote - Optional note about what changed
|
||||
* @returns Created version
|
||||
*/
|
||||
async createSnapshot(
|
||||
projectId: string,
|
||||
generatedBy: string = 'AUTO_SAVE',
|
||||
label?: string,
|
||||
changeNote?: string,
|
||||
) {
|
||||
const project = await this.prisma.scriptProject.findUnique({
|
||||
where: { id: projectId },
|
||||
include: {
|
||||
segments: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||
}
|
||||
|
||||
// Don't create empty snapshots
|
||||
if (!project.segments || project.segments.length === 0) {
|
||||
this.logger.warn(
|
||||
`No segments to snapshot for project ${projectId}. Skipping.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate next version number
|
||||
const nextVersion = project.currentVersionNumber + 1;
|
||||
|
||||
// Build snapshot data (strip relation fields, keep only data)
|
||||
const snapshotData = project.segments.map((seg) => ({
|
||||
segmentType: seg.segmentType,
|
||||
timeStart: seg.timeStart,
|
||||
duration: seg.duration,
|
||||
narratorScript: seg.narratorScript,
|
||||
visualDescription: seg.visualDescription,
|
||||
videoPrompt: seg.videoPrompt,
|
||||
imagePrompt: seg.imagePrompt,
|
||||
onScreenText: seg.onScreenText,
|
||||
editorNotes: seg.editorNotes,
|
||||
generalNotes: seg.generalNotes,
|
||||
audioCues: seg.audioCues,
|
||||
stockQuery: seg.stockQuery,
|
||||
generatedImageUrl: seg.generatedImageUrl,
|
||||
sortOrder: seg.sortOrder,
|
||||
citationIndexes: seg.citationIndexes,
|
||||
}));
|
||||
|
||||
// Build SEO snapshot
|
||||
const seoSnapshot = {
|
||||
seoTitle: project.seoTitle,
|
||||
seoDescription: project.seoDescription,
|
||||
seoTags: project.seoTags,
|
||||
thumbnailIdeas: project.thumbnailIdeas,
|
||||
};
|
||||
|
||||
// Calculate total words
|
||||
const totalWords = project.segments.reduce((acc, seg) => {
|
||||
return acc + (seg.narratorScript?.split(' ').length || 0);
|
||||
}, 0);
|
||||
|
||||
// Create version
|
||||
const version = await this.prisma.scriptVersion.create({
|
||||
data: {
|
||||
projectId,
|
||||
versionNumber: nextVersion,
|
||||
label:
|
||||
label ||
|
||||
`v${nextVersion} — ${generatedBy === 'AI' ? 'AI Generated' : generatedBy === 'USER' ? 'Manual Save' : 'Auto Save'}`,
|
||||
generatedBy,
|
||||
snapshotData: snapshotData as any,
|
||||
seoSnapshot: seoSnapshot as any,
|
||||
segmentCount: project.segments.length,
|
||||
totalWords,
|
||||
changeNote,
|
||||
},
|
||||
});
|
||||
|
||||
// Update project's current version number
|
||||
await this.prisma.scriptProject.update({
|
||||
where: { id: projectId },
|
||||
data: { currentVersionNumber: nextVersion },
|
||||
});
|
||||
|
||||
// Cleanup old versions (keep only MAX_VERSIONS)
|
||||
await this.cleanupOldVersions(projectId);
|
||||
|
||||
this.logger.log(
|
||||
`Created version v${nextVersion} for project ${projectId} (${generatedBy})`,
|
||||
);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
// ========== VERSION RESTORE ==========
|
||||
|
||||
/**
|
||||
* Restore a project to a specific version.
|
||||
* Creates a snapshot of current state before restoring.
|
||||
*
|
||||
* @param projectId - Project ID
|
||||
* @param versionId - Version ID to restore to
|
||||
* @returns Restored segments
|
||||
*/
|
||||
async restoreVersion(projectId: string, versionId: string) {
|
||||
const version = await this.getVersion(projectId, versionId);
|
||||
|
||||
// First, snapshot current state before restoring
|
||||
await this.createSnapshot(
|
||||
projectId,
|
||||
'AUTO_SAVE',
|
||||
undefined,
|
||||
`Auto-save before restoring to v${version.versionNumber}`,
|
||||
);
|
||||
|
||||
// Parse snapshot data
|
||||
const segments = version.snapshotData as any[];
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
throw new Error('Version snapshot has no segment data');
|
||||
}
|
||||
|
||||
// Delete current segments
|
||||
await this.prisma.scriptSegment.deleteMany({ where: { projectId } });
|
||||
|
||||
// Restore segments from snapshot
|
||||
await this.prisma.scriptSegment.createMany({
|
||||
data: segments.map((seg) => ({
|
||||
projectId,
|
||||
segmentType: seg.segmentType || 'Body',
|
||||
timeStart: seg.timeStart || '00:00',
|
||||
duration: seg.duration || '0s',
|
||||
narratorScript: seg.narratorScript,
|
||||
visualDescription: seg.visualDescription,
|
||||
videoPrompt: seg.videoPrompt,
|
||||
imagePrompt: seg.imagePrompt,
|
||||
onScreenText: seg.onScreenText,
|
||||
editorNotes: seg.editorNotes,
|
||||
generalNotes: seg.generalNotes,
|
||||
audioCues: seg.audioCues,
|
||||
stockQuery: seg.stockQuery,
|
||||
generatedImageUrl: seg.generatedImageUrl,
|
||||
sortOrder: seg.sortOrder ?? 0,
|
||||
citationIndexes: seg.citationIndexes || [],
|
||||
})),
|
||||
});
|
||||
|
||||
// Restore SEO data if present
|
||||
const seoData = version.seoSnapshot as any;
|
||||
if (seoData) {
|
||||
await this.prisma.scriptProject.update({
|
||||
where: { id: projectId },
|
||||
data: {
|
||||
seoTitle: seoData.seoTitle,
|
||||
seoDescription: seoData.seoDescription,
|
||||
seoTags: seoData.seoTags || [],
|
||||
thumbnailIdeas: seoData.thumbnailIdeas || [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Restored project ${projectId} to version v${version.versionNumber}`,
|
||||
);
|
||||
|
||||
// Return the restored project data
|
||||
return this.prisma.scriptProject.findUnique({
|
||||
where: { id: projectId },
|
||||
include: {
|
||||
segments: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== VERSION DELETION ==========
|
||||
|
||||
/**
|
||||
* Delete a specific version
|
||||
*/
|
||||
async deleteVersion(projectId: string, versionId: string) {
|
||||
const version = await this.getVersion(projectId, versionId);
|
||||
|
||||
await this.prisma.scriptVersion.delete({
|
||||
where: { id: version.id },
|
||||
});
|
||||
|
||||
return { deleted: true, versionNumber: version.versionNumber };
|
||||
}
|
||||
|
||||
// ========== VERSION COMPARISON ==========
|
||||
|
||||
/**
|
||||
* Compare two versions and return differences
|
||||
*/
|
||||
async compareVersions(
|
||||
projectId: string,
|
||||
versionAId: string,
|
||||
versionBId: string,
|
||||
) {
|
||||
const [versionA, versionB] = await Promise.all([
|
||||
this.getVersion(projectId, versionAId),
|
||||
this.getVersion(projectId, versionBId),
|
||||
]);
|
||||
|
||||
const segmentsA = (versionA.snapshotData as any[]) || [];
|
||||
const segmentsB = (versionB.snapshotData as any[]) || [];
|
||||
|
||||
const maxLen = Math.max(segmentsA.length, segmentsB.length);
|
||||
const diffs: any[] = [];
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const segA = segmentsA[i] || null;
|
||||
const segB = segmentsB[i] || null;
|
||||
|
||||
if (!segA && segB) {
|
||||
diffs.push({
|
||||
index: i,
|
||||
type: 'added',
|
||||
segmentType: segB.segmentType,
|
||||
narratorScript: { before: null, after: segB.narratorScript },
|
||||
});
|
||||
} else if (segA && !segB) {
|
||||
diffs.push({
|
||||
index: i,
|
||||
type: 'removed',
|
||||
segmentType: segA.segmentType,
|
||||
narratorScript: { before: segA.narratorScript, after: null },
|
||||
});
|
||||
} else if (segA && segB) {
|
||||
const changed =
|
||||
segA.narratorScript !== segB.narratorScript ||
|
||||
segA.visualDescription !== segB.visualDescription ||
|
||||
segA.onScreenText !== segB.onScreenText;
|
||||
|
||||
if (changed) {
|
||||
diffs.push({
|
||||
index: i,
|
||||
type: 'modified',
|
||||
segmentType: segB.segmentType,
|
||||
narratorScript: {
|
||||
before: segA.narratorScript,
|
||||
after: segB.narratorScript,
|
||||
},
|
||||
visualDescription: {
|
||||
before: segA.visualDescription,
|
||||
after: segB.visualDescription,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
versionA: {
|
||||
id: versionA.id,
|
||||
versionNumber: versionA.versionNumber,
|
||||
label: versionA.label,
|
||||
createdAt: versionA.createdAt,
|
||||
},
|
||||
versionB: {
|
||||
id: versionB.id,
|
||||
versionNumber: versionB.versionNumber,
|
||||
label: versionB.label,
|
||||
createdAt: versionB.createdAt,
|
||||
},
|
||||
totalDiffs: diffs.length,
|
||||
diffs,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
/**
|
||||
* Verify project exists or throw
|
||||
*/
|
||||
private async ensureProjectExists(projectId: string) {
|
||||
const exists = await this.prisma.scriptProject.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!exists) {
|
||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old versions beyond the max limit per project
|
||||
*/
|
||||
private async cleanupOldVersions(projectId: string) {
|
||||
const versions = await this.prisma.scriptVersion.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
select: { id: true, versionNumber: true },
|
||||
});
|
||||
|
||||
if (versions.length > this.MAX_VERSIONS) {
|
||||
const toDelete = versions.slice(this.MAX_VERSIONS);
|
||||
await this.prisma.scriptVersion.deleteMany({
|
||||
where: {
|
||||
id: { in: toDelete.map((v) => v.id) },
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Cleaned up ${toDelete.length} old versions for project ${projectId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ScriptsController,
|
||||
ResearchController,
|
||||
AnalysisController,
|
||||
VersionsController,
|
||||
} from './controllers';
|
||||
|
||||
// Services
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
ScriptsService,
|
||||
ResearchService,
|
||||
AnalysisService,
|
||||
VersionsService,
|
||||
} from './services';
|
||||
|
||||
/**
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
* - Neuro Marketing analysis
|
||||
* - YouTube audit
|
||||
* - Commercial brief generation
|
||||
* - Version history & content management
|
||||
*
|
||||
* TR: SkriptAI ana modülü - AI destekli video script üretimi.
|
||||
* EN: Main module for the SkriptAI feature - AI-powered video script generation.
|
||||
@@ -41,13 +44,21 @@ import {
|
||||
ScriptsController,
|
||||
ResearchController,
|
||||
AnalysisController,
|
||||
VersionsController,
|
||||
],
|
||||
providers: [
|
||||
ProjectsService,
|
||||
ScriptsService,
|
||||
ResearchService,
|
||||
AnalysisService,
|
||||
VersionsService,
|
||||
],
|
||||
exports: [
|
||||
ProjectsService,
|
||||
ScriptsService,
|
||||
ResearchService,
|
||||
AnalysisService,
|
||||
VersionsService,
|
||||
],
|
||||
exports: [ProjectsService, ScriptsService, ResearchService, AnalysisService],
|
||||
})
|
||||
export class SkriptaiModule {}
|
||||
|
||||
Reference in New Issue
Block a user