Add - Classe Gemini, endpoint de criacao de video
This commit is contained in:
@@ -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"
|
||||
@@ -33,7 +33,9 @@
|
||||
"cross-env": "10.0.0",
|
||||
"dayjs": "1.11.13",
|
||||
"jwks-rsa": "3.2.0",
|
||||
"lodash": "4.17.21",
|
||||
"ollama": "0.6.0",
|
||||
"openai": "6.1.0",
|
||||
"passport": "0.7.0",
|
||||
"passport-jwt": "4.0.1",
|
||||
"reflect-metadata": "0.2.2",
|
||||
@@ -48,6 +50,7 @@
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/passport-jwt": "4.0.1",
|
||||
"@types/supertest": "6.0.2",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { UsuariosModule } from './modules/usuarios/usuarios.module';
|
||||
import { LoggerMiddleware } from './middleware/logger.middleware';
|
||||
import { RolesGuard } from './modules/auth/roles.guard';
|
||||
import { OllamaModule } from './modules/ollama/ollama.module';
|
||||
import { GeminiService } from './gemini/gemini.service';
|
||||
import { GeminiModule } from './gemini/gemini.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -19,9 +21,10 @@ import { OllamaModule } from './modules/ollama/ollama.module';
|
||||
AuthModule,
|
||||
UsuariosModule,
|
||||
OllamaModule,
|
||||
GeminiModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, RolesGuard],
|
||||
providers: [AppService, RolesGuard, GeminiService],
|
||||
})
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
|
||||
18
src/gemini/gemini.controller.spec.ts
Normal file
18
src/gemini/gemini.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
4
src/gemini/gemini.module.ts
Normal file
4
src/gemini/gemini.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class GeminiModule {}
|
||||
18
src/gemini/gemini.service.spec.ts
Normal file
18
src/gemini/gemini.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
45
src/gemini/gemini.service.ts
Normal file
45
src/gemini/gemini.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OllamaController } from './ollama.controller';
|
||||
import { OllamaService } from './ollama.service';
|
||||
|
||||
@Module({
|
||||
controllers: [OllamaController],
|
||||
providers: [OllamaService],
|
||||
})
|
||||
export class OllamaModule {}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsISO8601,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { video_situation } from '@root/generated/prisma';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
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()
|
||||
@MaxLength(244)
|
||||
url!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(video_situation)
|
||||
situation?: video_situation;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
error_message?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
clips_quantity?: number;
|
||||
|
||||
@IsOptional()
|
||||
times?: unknown;
|
||||
@IsArray({ message: 'Categories deve ser um array' })
|
||||
categories?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -36,24 +37,38 @@ export class CreateVideoDto {
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
@MaxLength(244)
|
||||
filename?: string;
|
||||
thumbnail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsString({ message: 'Videoid deve ser uma string' })
|
||||
@MaxLength(244)
|
||||
videoid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_download?: string;
|
||||
@IsString({ message: 'Duration deve ser uma string' })
|
||||
@Transform(({ value }: { value: number }) => {
|
||||
const duration = dayjs.duration(value, 'seconds');
|
||||
|
||||
return duration.format('HH:mm:ss');
|
||||
})
|
||||
duration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_convert?: string;
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_posted?: string;
|
||||
@IsNumber(
|
||||
{ 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;
|
||||
}
|
||||
|
||||
@@ -7,27 +7,20 @@ import {
|
||||
Body,
|
||||
Delete,
|
||||
UseGuards,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, videos, video_situation } from 'generated/prisma';
|
||||
|
||||
import { VideosService } from './videos.service';
|
||||
import { VideoResponseDto } from './dto/video-response.dto';
|
||||
import { KeycloakAuthGuard } from '../auth/keycloak-auth.guard';
|
||||
import { Roles } from '../auth/decorator/roles.decorator';
|
||||
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
|
||||
import { VideoMetadataDto } from 'src/shared/dto/video-metadata';
|
||||
import { KeycloakAuthGuard } from '@modules/auth/keycloak-auth.guard';
|
||||
import { Roles } from '@modules/auth/decorator/roles.decorator';
|
||||
import { VideoMetadataDto } from '@shared/dto/video-metadata';
|
||||
|
||||
type PaginatedResponse<T> = {
|
||||
content: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
};
|
||||
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
|
||||
import { VideoResponseDto } from './dto/video-response.dto';
|
||||
import { VideosService } from './videos.service';
|
||||
|
||||
import type { PaginatedResponse } from '@shared/types/pagination';
|
||||
import { CreateVideoDto } from './dto/create-video-dto';
|
||||
|
||||
@Controller('videos')
|
||||
@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')
|
||||
async get(@Param('id') id: string): Promise<videos | null> {
|
||||
return this.videosService.get(Number(id));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
@@ -9,6 +11,7 @@ import { VideoResponseDto } from './dto/video-response.dto';
|
||||
import { PaginatedResponse } from '@shared/dto/paginated';
|
||||
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
|
||||
import { VideoMetadataDto } from '@shared/dto/video-metadata';
|
||||
import { CreateVideoDto } from './dto/create-video-dto';
|
||||
|
||||
@Injectable()
|
||||
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> {
|
||||
return this.prisma.videos.update({
|
||||
where: { id },
|
||||
|
||||
11
src/shared/types/pagination.ts
Normal file
11
src/shared/types/pagination.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PaginatedResponse<T> {
|
||||
content: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user