Add - Classe Gemini, endpoint de criacao de video

This commit is contained in:
LeoMortari
2025-10-03 00:59:51 -03:00
parent f7857fdcbc
commit 558a625f11
13 changed files with 182 additions and 92 deletions

View File

@@ -1 +1,4 @@
DATABASE_URL="postgresql://username:password@ip_server:port/database?schema=public" DATABASE_URL="postgresql://username:password@ip_server:port/database?schema=public"
KEYCLOAK_URL="https://auth.clipperia.com.br"
YOUTUBE_API_URL="https://totally-real-dingo.ngrok-free.app"
GEMINI_API_KEY="GEMINI API KEY"

View File

@@ -33,7 +33,9 @@
"cross-env": "10.0.0", "cross-env": "10.0.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"jwks-rsa": "3.2.0", "jwks-rsa": "3.2.0",
"lodash": "4.17.21",
"ollama": "0.6.0", "ollama": "0.6.0",
"openai": "6.1.0",
"passport": "0.7.0", "passport": "0.7.0",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
@@ -48,6 +50,7 @@
"@types/bcrypt": "6.0.0", "@types/bcrypt": "6.0.0",
"@types/express": "5.0.0", "@types/express": "5.0.0",
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/lodash": "4.17.20",
"@types/node": "22.10.7", "@types/node": "22.10.7",
"@types/passport-jwt": "4.0.1", "@types/passport-jwt": "4.0.1",
"@types/supertest": "6.0.2", "@types/supertest": "6.0.2",

View File

@@ -10,6 +10,8 @@ import { UsuariosModule } from './modules/usuarios/usuarios.module';
import { LoggerMiddleware } from './middleware/logger.middleware'; import { LoggerMiddleware } from './middleware/logger.middleware';
import { RolesGuard } from './modules/auth/roles.guard'; import { RolesGuard } from './modules/auth/roles.guard';
import { OllamaModule } from './modules/ollama/ollama.module'; import { OllamaModule } from './modules/ollama/ollama.module';
import { GeminiService } from './gemini/gemini.service';
import { GeminiModule } from './gemini/gemini.module';
@Module({ @Module({
imports: [ imports: [
@@ -19,9 +21,10 @@ import { OllamaModule } from './modules/ollama/ollama.module';
AuthModule, AuthModule,
UsuariosModule, UsuariosModule,
OllamaModule, OllamaModule,
GeminiModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, RolesGuard], providers: [AppService, RolesGuard, GeminiService],
}) })
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GeminiController } from './gemini.controller';
describe('GeminiController', () => {
let controller: GeminiController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GeminiController],
}).compile();
controller = module.get<GeminiController>(GeminiController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class GeminiModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GeminiService } from './gemini.service';
describe('GeminiService', () => {
let service: GeminiService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GeminiService],
}).compile();
service = module.get<GeminiService>(GeminiService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';
@Injectable()
export class GeminiService {
private readonly model = 'gemini-2.0-flash';
private readonly systemPrompt =
'Você é um assistente de IA que responde em portugues.';
private readonly gemini = new OpenAI({
apiKey: process.env.GEMINI_API_KEY,
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
});
public async generate({
model = this.model,
message,
systemPrompt = this.systemPrompt,
}: {
model?: string;
message: string;
systemPrompt?: string;
}) {
try {
const response = await this.gemini.chat.completions.create({
model,
messages: [
{
role: 'system',
content: systemPrompt,
},
{
role: 'user',
content: message,
},
],
});
return response;
} catch (error) {
throw new Error(error as string);
}
}
}

View File

@@ -1,44 +0,0 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { OllamaService } from './ollama.service';
import { ChatDto } from './dto/chat.dto';
@Controller('ollama')
export class OllamaController {
constructor(private readonly ollamaService: OllamaService) {}
@Get('models')
public async getModels(
@Query('onlyChats') { onlyChats }: { onlyChats: boolean },
) {
return await this.ollamaService.getModels({ onlyChats });
}
@Post('chat')
public async chat(@Body() body: ChatDto) {
const { message, model } = body;
if (!message) {
throw new Error('O atributo message é obrigatório');
}
if (!model) {
const [defaultModel] = await this.ollamaService.getModels({
onlyChats: true,
});
if (!defaultModel) {
throw new Error('Nenhum modelo encontrado');
}
return await this.ollamaService.generateChat({
model: defaultModel.name,
message,
});
}
return await this.ollamaService.generateChat({
model,
message,
});
}
}

View File

@@ -1,9 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { OllamaController } from './ollama.controller';
import { OllamaService } from './ollama.service'; import { OllamaService } from './ollama.service';
@Module({ @Module({
controllers: [OllamaController],
providers: [OllamaService], providers: [OllamaService],
}) })
export class OllamaModule {} export class OllamaModule {}

View File

@@ -1,34 +1,35 @@
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { Transform } from 'class-transformer';
import { import {
IsEnum, IsArray,
IsInt, IsNumber,
IsISO8601,
IsOptional, IsOptional,
IsString, IsString,
IsUrl, IsUrl,
MaxLength, MaxLength,
} from 'class-validator'; } from 'class-validator';
import { video_situation } from '@root/generated/prisma';
dayjs.extend(duration);
export class CreateVideoDto { export class CreateVideoDto {
@IsOptional()
@IsString({ message: 'Id deve ser uma string' })
@MaxLength(244)
id?: string;
@IsOptional()
@IsArray({ message: 'Tags deve ser um array' })
tags?: string[];
@IsUrl() @IsUrl()
@MaxLength(244) @MaxLength(244)
url!: string; url!: string;
@IsOptional() @IsOptional()
@IsEnum(video_situation) @IsArray({ message: 'Categories deve ser um array' })
situation?: video_situation; categories?: string[];
@IsOptional()
@IsString()
@MaxLength(244)
error_message?: string;
@IsOptional()
@IsInt()
clips_quantity?: number;
@IsOptional()
times?: unknown;
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -36,24 +37,38 @@ export class CreateVideoDto {
title?: string; title?: string;
@IsOptional() @IsOptional()
@IsString() @IsUrl()
@MaxLength(244) @MaxLength(244)
filename?: string; thumbnail?: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'Videoid deve ser uma string' })
@MaxLength(244) @MaxLength(244)
videoid?: string; videoid?: string;
@IsOptional() @IsOptional()
@IsISO8601() @IsString({ message: 'Duration deve ser uma string' })
datetime_download?: string; @Transform(({ value }: { value: number }) => {
const duration = dayjs.duration(value, 'seconds');
return duration.format('HH:mm:ss');
})
duration?: string;
@IsOptional() @IsOptional()
@IsISO8601() @IsString()
datetime_convert?: string; @MaxLength(244)
description?: string;
@IsOptional() @IsOptional()
@IsISO8601() @IsNumber(
datetime_posted?: string; { allowNaN: false, allowInfinity: false },
{ message: 'Timestamp deve ser um número' },
)
@Transform(({ value }: { value: number }) => {
const duration = dayjs(value * 1000);
return duration.format('DD/MM/YYYY HH:mm:ss');
})
timestamp?: string;
} }

View File

@@ -7,27 +7,20 @@ import {
Body, Body,
Delete, Delete,
UseGuards, UseGuards,
Post,
} from '@nestjs/common'; } from '@nestjs/common';
import { Prisma, videos, video_situation } from 'generated/prisma'; import { Prisma, videos, video_situation } from 'generated/prisma';
import { VideosService } from './videos.service'; import { KeycloakAuthGuard } from '@modules/auth/keycloak-auth.guard';
import { VideoResponseDto } from './dto/video-response.dto'; import { Roles } from '@modules/auth/decorator/roles.decorator';
import { KeycloakAuthGuard } from '../auth/keycloak-auth.guard'; import { VideoMetadataDto } from '@shared/dto/video-metadata';
import { Roles } from '../auth/decorator/roles.decorator';
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
import { VideoMetadataDto } from 'src/shared/dto/video-metadata';
type PaginatedResponse<T> = { import { ListVideosQueryDto } from './dto/list-videos-query.dto';
content: T[]; import { VideoResponseDto } from './dto/video-response.dto';
pagination: { import { VideosService } from './videos.service';
page: number;
perPage: number; import type { PaginatedResponse } from '@shared/types/pagination';
total: number; import { CreateVideoDto } from './dto/create-video-dto';
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
@Controller('videos') @Controller('videos')
@UseGuards(KeycloakAuthGuard) @UseGuards(KeycloakAuthGuard)
@@ -63,6 +56,12 @@ export class VideosController {
}); });
} }
@Post()
@Roles('user', 'admin')
async create(@Body() body: CreateVideoDto): Promise<void> {
await this.videosService.createNewVideo(body);
}
@Get(':id') @Get(':id')
async get(@Param('id') id: string): Promise<videos | null> { async get(@Param('id') id: string): Promise<videos | null> {
return this.videosService.get(Number(id)); return this.videosService.get(Number(id));

View File

@@ -1,4 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import isEmpty from 'lodash/isEmpty';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
@@ -9,6 +11,7 @@ import { VideoResponseDto } from './dto/video-response.dto';
import { PaginatedResponse } from '@shared/dto/paginated'; import { PaginatedResponse } from '@shared/dto/paginated';
import { ListVideosQueryDto } from './dto/list-videos-query.dto'; import { ListVideosQueryDto } from './dto/list-videos-query.dto';
import { VideoMetadataDto } from '@shared/dto/video-metadata'; import { VideoMetadataDto } from '@shared/dto/video-metadata';
import { CreateVideoDto } from './dto/create-video-dto';
@Injectable() @Injectable()
export class VideosService { export class VideosService {
@@ -133,6 +136,20 @@ export class VideosService {
} }
} }
async createNewVideo(data: CreateVideoDto): Promise<void> {
const { url, videoid } = data;
const searchUrl = await this.prisma.videos.findFirst({
where: { url, videoid },
});
if (!isEmpty(searchUrl)) {
throw new Error(
`Esta video já foi renderizado, e se encontra na situação ${searchUrl.situation}`,
);
}
}
async update(id: number, data: Prisma.videosUpdateInput): Promise<videos> { async update(id: number, data: Prisma.videosUpdateInput): Promise<videos> {
return this.prisma.videos.update({ return this.prisma.videos.update({
where: { id }, where: { id },

View File

@@ -0,0 +1,11 @@
export interface PaginatedResponse<T> {
content: T[];
pagination: {
page: number;
perPage: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}