Skip to content

Commit

Permalink
feature:FUR-13 [BE][Web] Integrate AI Generate text to 3D model produ…
Browse files Browse the repository at this point in the history
…ct API (#102)
  • Loading branch information
MinhhTien committed May 20, 2024
1 parent 65c4fd4 commit 2d17288
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
echo DISCORD_WEBHOOK_ID=${{ secrets.DISCORD_WEBHOOK_ID }} >> .env
echo DISCORD_WEBHOOK_TOKEN=${{ secrets.DISCORD_WEBHOOK_TOKEN }} >> .env
echo TRIPO_3D_AI_ENDPOINT=${{ secrets.TRIPO_3D_AI_ENDPOINT }} >> .env
echo TRIPO_3D_AI_API_KEY=${{ vars.TRIPO_3D_AI_API_KEY }} >> .env
- name: Deploy
run: pm2 restart furnique-api

Expand Down
8 changes: 6 additions & 2 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,22 @@ SERVER_URL=https://api.furnique.tech
DISCORD_WEBHOOK_ID=
DISCORD_WEBHOOK_TOKEN=

#Tripo 3D AI API
TRIPO_3D_AI_ENDPOINT=https://api.tripo3d.ai
TRIPO_3D_AI_API_KEY=

## PAYMENT
#MOMO
MOMO_PARTNER_CODE=
MOMO_ACCESS_KEY=
MOMO_SECRET_KEY=
MOMO_ENDPOINT=
MOMO_ENDPOINT=https://test-payment.momo.vn

#ZALOPAY
ZALOPAY_APP_ID=
ZALOPAY_KEY1=
ZALOPAY_KEY2=
ZALOPAY_ENDPOINT=
ZALOPAY_ENDPOINT=https://sb-openapi.zalopay.vn

#PAYOS
PAYOS_CLIENT_ID=
Expand Down
21 changes: 21 additions & 0 deletions src/ai-generation/ai-generation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Global, Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { HttpModule } from '@nestjs/axios'
import { AIGeneration, AIGenerationSchema } from './schemas/ai-generation.schema'
import { CustomerModule } from '@customer/customer.module'
import { AIGenerationTextToModelController } from './controllers/text-to-model.controller'
import { AIGenerationTextToModelService } from './services/text-to-model.service'
import { AIGenerationRepository } from './repositories/ai-generation.repository'

@Global()
@Module({
imports: [
MongooseModule.forFeature([{ name: AIGeneration.name, schema: AIGenerationSchema }]),
HttpModule,
CustomerModule
],
controllers: [AIGenerationTextToModelController],
providers: [AIGenerationTextToModelService, AIGenerationRepository],
exports: [AIGenerationTextToModelService, AIGenerationRepository]
})
export class AIGenerationModule {}
23 changes: 23 additions & 0 deletions src/ai-generation/contracts/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export enum AIGenerationPlan {
FREE = 'FREE',
PREMIUM = 'PREMIUM'
}

export enum AIGenerationType {
TEXT_TO_MODEL = 'TEXT_TO_MODEL'
}

export enum AIGenerationTaskStatus {
QUEUED = 'queued',
RUNNING = 'running',
SUCCESS = 'success',
FAILED = 'failed',
CANCELLED = 'cancelled',
UNKNOWN = 'unknown'
}

export enum AIGenerationTaskProgress {
QUEUEING = 0,
RUNNING = 'RUNNING',
SUCCESS = 100
}
38 changes: 38 additions & 0 deletions src/ai-generation/controllers/text-to-model.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Body, Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common'
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'
import * as _ from 'lodash'
import { Roles } from '@auth/decorators/roles.decorator'
import { UserRole } from '@common/contracts/constant'
import { RolesGuard } from '@auth/guards/roles.guard'
import { JwtAuthGuard } from '@auth/guards/jwt-auth.guard'
import { AIGenerationTextToModelService } from '@ai-generation/services/text-to-model.service'
import { GenerateTextToDraftModelDto, TextToModelTaskResponseDto } from '@ai-generation/dtos/text-to-model.dto'

@ApiTags('AIGeneration - TextToModel')
@ApiBearerAuth()
@Roles(UserRole.CUSTOMER)
@UseGuards(JwtAuthGuard.ACCESS_TOKEN, RolesGuard)
@Controller('text-to-model')
export class AIGenerationTextToModelController {
constructor(private readonly aiGenerationTextToModelService: AIGenerationTextToModelService) {}

@ApiOperation({
summary: 'Generate draft model from text'
})
@ApiOkResponse({ type: TextToModelTaskResponseDto })
@Post()
generate(@Req() req, @Body() generateTextToDraftModelDto: GenerateTextToDraftModelDto) {
generateTextToDraftModelDto.type = 'text_to_model'
generateTextToDraftModelDto.customerId = _.get(req, 'user._id')
return this.aiGenerationTextToModelService.generateTextToDraftModel(generateTextToDraftModelDto)
}

@ApiOperation({
summary: 'Get task of model'
})
@Get(':taskId')
@ApiOkResponse({ type: TextToModelTaskResponseDto })
getTask(@Param('taskId') taskId: string) {
return this.aiGenerationTextToModelService.getTask(taskId)
}
}
64 changes: 64 additions & 0 deletions src/ai-generation/dtos/text-to-model.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ApiProperty } from '@nestjs/swagger'
import { DataResponse } from '@common/contracts/openapi-builder'
import { IsNotEmpty, MaxLength } from 'class-validator'
import { AIGenerationTaskProgress, AIGenerationTaskStatus } from '@ai-generation/contracts/constant'

export class GenerateTextToDraftModelDto {
@ApiProperty()
@IsNotEmpty()
@MaxLength(1024)
prompt: string

// @ApiProperty()
// @IsNotEmpty()
// @MaxLength(255)
// negative_prompt?: string

type?: string
customerId?: string
}

export class TextToDraftModelDto {
@ApiProperty({
example: '1ec04ced-4b87-44f6-a296-beee80777941'
})
task_id: string
}

export class TextToDraftModelResponseDto extends DataResponse(TextToDraftModelDto) {}


export class TextToModelTaskDto {
@ApiProperty()
task_id: string

@ApiProperty()
type: string

@ApiProperty({ enum: AIGenerationTaskStatus })
status: AIGenerationTaskStatus

@ApiProperty()
input: object

@ApiProperty({
example: {
model: 'model url',
rendered_image: 'preview image'
}
})
output: {
model: string
rendered_image: string
}

@ApiProperty({
enum: AIGenerationTaskProgress
})
progress: AIGenerationTaskProgress

@ApiProperty()
create_time: string
}

export class TextToModelTaskResponseDto extends DataResponse(TextToModelTaskDto) {}
12 changes: 12 additions & 0 deletions src/ai-generation/repositories/ai-generation.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PaginateModel } from 'mongoose'
import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { AbstractRepository } from '@common/repositories'
import { AIGeneration, AIGenerationDocument } from '@ai-generation/schemas/ai-generation.schema'

@Injectable()
export class AIGenerationRepository extends AbstractRepository<AIGenerationDocument> {
constructor(@InjectModel(AIGeneration.name) model: PaginateModel<AIGenerationDocument>) {
super(model)
}
}
44 changes: 44 additions & 0 deletions src/ai-generation/schemas/ai-generation.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { HydratedDocument } from 'mongoose'
import * as paginate from 'mongoose-paginate-v2'
import { Transform } from 'class-transformer'
import { ApiProperty } from '@nestjs/swagger'
import { AIGenerationType } from '../contracts/constant'

export type AIGenerationDocument = HydratedDocument<AIGeneration>

@Schema({
collection: 'ai-generations',
timestamps: true,
toJSON: {
transform(doc, ret) {
delete ret.__v
}
}
})
export class AIGeneration {
constructor(id?: string) {
this._id = id
}
@ApiProperty()
@Transform(({ value }) => value?.toString())
_id: string

@ApiProperty()
@Prop({ type: String })
customerId: string

@ApiProperty({ enum: AIGenerationType })
@Prop({ enum: AIGenerationType, default: AIGenerationType.TEXT_TO_MODEL })
type: AIGenerationType

@ApiProperty()
@Prop({ type: String })
taskId: string // used for TEXT_TO_MODEL

// more prop for other type
}

export const AIGenerationSchema = SchemaFactory.createForClass(AIGeneration)

AIGenerationSchema.plugin(paginate)
69 changes: 69 additions & 0 deletions src/ai-generation/services/text-to-model.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable, Logger } from '@nestjs/common'
import { AIGenerationRepository } from '@ai-generation/repositories/ai-generation.repository'
import { HttpService } from '@nestjs/axios'
import { ConfigService } from '@nestjs/config'
import { catchError, firstValueFrom } from 'rxjs'
import { AxiosError } from 'axios'
import { GenerateTextToDraftModelDto } from '@ai-generation/dtos/text-to-model.dto'
import { AppException } from '@common/exceptions/app.exception'
import { Errors } from '@common/contracts/error'
import { AIGenerationType } from '@ai-generation/contracts/constant'

@Injectable()
export class AIGenerationTextToModelService {
private readonly logger = new Logger(AIGenerationTextToModelService.name)
private config
private headersRequest
constructor(
private readonly aiGenerationRepository: AIGenerationRepository,
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.config = this.configService.get('tripo3dAI')
this.headersRequest = {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`
}
}

async generateTextToDraftModel(generateTextToDraftModelDto: GenerateTextToDraftModelDto) {
const {customerId} = generateTextToDraftModelDto

// TODO: Check limit AI generation here

const { data } = await firstValueFrom(
this.httpService
.post(`${this.config.endpoint}/v2/openapi/task`, generateTextToDraftModelDto, {
headers: this.headersRequest
})
.pipe(
catchError((error: AxiosError) => {
this.logger.error(error.response.data)
throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data: error?.response?.data })
})
)
)
if (data.code !== 0) throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data })

await this.aiGenerationRepository.create({
customerId,
type: AIGenerationType.TEXT_TO_MODEL,
taskId: data?.data?.task_id
})

return data?.data
}

async getTask(taskId: string) {
const { data } = await firstValueFrom(
this.httpService.get(`${this.config.endpoint}/v2/openapi/task/${taskId}`, { headers: this.headersRequest }).pipe(
catchError((error: AxiosError) => {
this.logger.error(error.response.data)
throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data: error?.response?.data })
})
)
)
if (data.code !== 0) throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data })
return data?.data
}
}
8 changes: 7 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ConsultantBookingModule } from '@consultant-booking/booking.module'
import { TaskModule } from '@task/task.module'
import { AnalyticModule } from '@analytic/analytic.module'
import { PaymentModule } from '@payment/payment.module'
import { AIGenerationModule } from '@ai-generation/ai-generation.module'

@Module({
imports: [
Expand Down Expand Up @@ -106,6 +107,10 @@ import { PaymentModule } from '@payment/payment.module'
{
path: 'consultant-bookings',
module: ConsultantBookingModule
},
{
path: 'ai-generation',
module: AIGenerationModule
}
]),
CommonModule,
Expand All @@ -121,7 +126,8 @@ import { PaymentModule } from '@payment/payment.module'
ConsultantBookingModule,
TaskModule,
AnalyticModule,
PaymentModule
PaymentModule,
AIGenerationModule
],
controllers: [AppController],
providers: [AppService]
Expand Down
5 changes: 5 additions & 0 deletions src/common/contracts/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,10 @@ export const Errors: Record<string, ErrorResponse> = {
error: 'VISIT_SHOWROOM_BOOKING_NOT_FOUND',
message: 'Không tìm thấy lịch tham quan showroom. Vui lòng thử lại',
httpStatus: HttpStatus.BAD_REQUEST
},
TRIPO_3D_AI_ERROR: {
error: 'TRIPO_3D_AI_ERROR',
message: 'Có chút lỗi xảy ra. Vui lòng thử lại sau giây lát bạn nhé.',
httpStatus: HttpStatus.INTERNAL_SERVER_ERROR
}
}
4 changes: 2 additions & 2 deletions src/common/exceptions/app-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export class AppExceptionFilter extends BaseExceptionFilter {
},
{
name: 'data',
value: `${JSON.stringify(data).slice(0, 50)}...`
value: `${JSON.stringify(data).slice(0, 200)}...`
},
{
name: 'stackTrace',
value: `${JSON.stringify(exception.stack).slice(0, 50)}...`
value: `${JSON.stringify(exception.stack).slice(0, 200)}...`
}
]
})
Expand Down
4 changes: 4 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export default () => ({
webhookId: process.env.DISCORD_WEBHOOK_ID,
webhookToken: process.env.DISCORD_WEBHOOK_TOKEN,
},
tripo3dAI: {
endpoint: process.env.TRIPO_3D_AI_ENDPOINT,
apiKey: process.env.TRIPO_3D_AI_API_KEY
},
JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET || 'accessSecret',
JWT_ACCESS_EXPIRATION: process.env.JWT_ACCESS_EXPIRATION || 864000, // seconds
JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || 'refreshSecret',
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@consultant-booking/*": ["src/consultant-booking/*"],
"@analytic/*": ["src/analytic/*"],
"@payment/*": ["src/payment/*"],
"@ai-generation/*": ["src/ai-generation/*"],
"@src/*": ["src/*"]
},

Expand Down

0 comments on commit 2d17288

Please sign in to comment.