feat: add tts service
This commit is contained in:
@@ -134,6 +134,9 @@ export const enum CommonMessage {
|
||||
|
||||
export const enum UploaderMessage {
|
||||
UPLOAD_FILE_INVALID = "فایل معتبر نیست",
|
||||
UPLOAD_FILE_TOO_LARGE = "فایل بزرگتر از حد مجاز است . حداکثر حجم فایل [MAX_FILE_SIZE] مگابایت است",
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = "نوع فایل معتبر نیست . مجاز است : [MIME_TYPES]",
|
||||
UPLOAD_FILE_SUCCESS = "فایل با موفقیت آپلود شد",
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
|
||||
@@ -239,6 +239,7 @@ export const aiConfigProvider: FactoryProvider<AIConfig> = {
|
||||
baseUrl: configService.getOrThrow<string>("AI_BASE_URL"),
|
||||
apiKey: configService.getOrThrow<string>("AI_API_KEY"),
|
||||
defaultModel: configService.getOrThrow<string>("AI_DEFAULT_MODEL"),
|
||||
defaultTTSModel: configService.getOrThrow<string>("AI_DEFAULT_TTS_MODEL"),
|
||||
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
|
||||
maxTokens: configService.get<number>("AI_MAX_TOKENS", 5000),
|
||||
timeout: configService.get<number>("AI_TIMEOUT", 80000),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export enum VoiceEnum {
|
||||
ALLOY = "alloy",
|
||||
ECHO = "echo",
|
||||
FABLE = "fable",
|
||||
ONYX = "onyx",
|
||||
NOVA = "nova",
|
||||
SHIMMER = "shimmer",
|
||||
}
|
||||
|
||||
export enum SpeedEnum {
|
||||
SLOW = 0.5,
|
||||
NORMAL = 1.0,
|
||||
FAST = 1.3,
|
||||
VERY_FAST = 2.0,
|
||||
}
|
||||
|
||||
export enum AudioFormatEnum {
|
||||
MP3 = "mp3",
|
||||
OPUS = "opus",
|
||||
AAC = "aac",
|
||||
FLAC = "flac",
|
||||
}
|
||||
|
||||
export class TextToSpeechDto {
|
||||
@IsNotEmpty({ message: "Message ID is required" })
|
||||
@IsNumber({}, { message: "Message ID must be a number" })
|
||||
@ApiProperty({ description: "The message ID to fetch and convert to speech", example: 12345 })
|
||||
messageId: number;
|
||||
|
||||
@IsNotEmpty({ message: "Mailbox ID is required" })
|
||||
@IsString({ message: "Mailbox ID must be a string" })
|
||||
@ApiProperty({ description: "The mailbox ID where the message is located", example: "507f1f77bcf86cd799439012" })
|
||||
mailbox: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(VoiceEnum, { message: "Voice must be one of: alloy, echo, fable, onyx, nova, shimmer" })
|
||||
@ApiPropertyOptional({
|
||||
description: "Voice to use for text-to-speech",
|
||||
enum: VoiceEnum,
|
||||
default: VoiceEnum.ALLOY,
|
||||
})
|
||||
voice?: VoiceEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(SpeedEnum, { message: "Speed must be one of: 0.5, 1.0, 1.5, 2.0" })
|
||||
@ApiPropertyOptional({
|
||||
description: "Speech speed",
|
||||
enum: SpeedEnum,
|
||||
default: SpeedEnum.NORMAL,
|
||||
})
|
||||
speed?: SpeedEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AudioFormatEnum, { message: "Format must be one of: mp3, opus, aac, flac" })
|
||||
@ApiPropertyOptional({
|
||||
description: "Audio output format",
|
||||
enum: AudioFormatEnum,
|
||||
default: AudioFormatEnum.MP3,
|
||||
})
|
||||
format?: AudioFormatEnum;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||
|
||||
import { GenerateTemplateDto } from "./DTO/generate-template.dto";
|
||||
import { SummarizeEmailDto } from "./DTO/summarize-email.dto";
|
||||
import { TextToSpeechDto } from "./DTO/text-to-speech.dto";
|
||||
import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto";
|
||||
import { AiAssistantService } from "./services/ai-assistant.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
@@ -37,4 +38,14 @@ export class AiAssistantController {
|
||||
async generateTemplate(@Body() generateTemplateDto: GenerateTemplateDto) {
|
||||
return this.aiAssistantService.generateTemplate(generateTemplateDto);
|
||||
}
|
||||
|
||||
@Post("text-to-speech")
|
||||
@ApiOperation({ summary: "Convert email summary to speech audio and save as file" })
|
||||
@ApiResponse({ status: 201, description: "Audio file generated and saved successfully" })
|
||||
@ApiResponse({ status: 400, description: "Bad request - invalid message ID or mailbox" })
|
||||
@ApiResponse({ status: 404, description: "Message not found or access denied" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized - invalid authentication" })
|
||||
async textToSpeech(@Body() textToSpeechDto: TextToSpeechDto, @UserDec("wildduckUserId") wildduckUserId: string) {
|
||||
return this.aiAssistantService.textToSpeechEmailSummary(textToSpeechDto, wildduckUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { AiAssistantService } from "./services/ai-assistant.service";
|
||||
import { AiService } from "./services/ai.service";
|
||||
import { aiConfigProvider } from "../../configs/ai.config";
|
||||
import { MailServerModule } from "../mail-server/mail-server.module";
|
||||
import { UploaderModule } from "../uploader/uploader.module";
|
||||
|
||||
@Module({
|
||||
imports: [MailServerModule],
|
||||
imports: [MailServerModule, UploaderModule],
|
||||
controllers: [AiAssistantController],
|
||||
providers: [AiAssistantService, AiService, aiConfigProvider],
|
||||
exports: [AiAssistantService, AiService],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PriorityEnum } from "../../email/enums/email-header.enum";
|
||||
import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface";
|
||||
import { PersonalityDataType } from "../../templates/interfaces/structure.interface";
|
||||
import { TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||
import { AudioFormatEnum, SpeedEnum, VoiceEnum } from "../DTO/text-to-speech.dto";
|
||||
import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||
|
||||
export interface IEmailData {
|
||||
@@ -66,8 +67,31 @@ export interface AIWritingResponse {
|
||||
improvements: string[];
|
||||
}
|
||||
|
||||
export interface TextToSpeechOptions {
|
||||
voice: VoiceEnum;
|
||||
speed: SpeedEnum;
|
||||
format: AudioFormatEnum;
|
||||
}
|
||||
|
||||
export interface AITextToSpeechResponse {
|
||||
audioBuffer: Buffer;
|
||||
format: AudioFormatEnum;
|
||||
duration?: number;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface TextToSpeechFileResponse {
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
format: AudioFormatEnum;
|
||||
contentType: string;
|
||||
fileSize: number;
|
||||
s3Key: string;
|
||||
}
|
||||
|
||||
export interface AIServiceInterface {
|
||||
summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
|
||||
assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
|
||||
generateTemplate(request: ITemplateGenerationRequest): Promise<AITemplateGenerationResponse>;
|
||||
textToSpeech(text: string, options: TextToSpeechOptions): Promise<AITextToSpeechResponse>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface AIConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
defaultModel: string;
|
||||
defaultTTSModel: string;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
timeout: number;
|
||||
|
||||
@@ -5,17 +5,27 @@ import { AiService } from "./ai.service";
|
||||
import { AiMessage } from "../../../common/enums/message.enum";
|
||||
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
|
||||
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
|
||||
import { S3Service } from "../../uploader/providers/s3.service";
|
||||
import { GenerateTemplateDto, TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
|
||||
import { AudioFormatEnum, SpeedEnum, TextToSpeechDto, VoiceEnum } from "../DTO/text-to-speech.dto";
|
||||
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
|
||||
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||
import { IEmailData, ITemplateGenerationRequest, SummarizationOptions, TemplateGenerationOptions } from "../interfaces/ai-service.interface";
|
||||
import { AITextToSpeechResponse } from "../interfaces/ai-service.interface";
|
||||
import {
|
||||
IEmailData,
|
||||
ITemplateGenerationRequest,
|
||||
SummarizationOptions,
|
||||
TemplateGenerationOptions,
|
||||
TextToSpeechOptions,
|
||||
} from "../interfaces/ai-service.interface";
|
||||
|
||||
@Injectable()
|
||||
export class AiAssistantService {
|
||||
constructor(
|
||||
private readonly messagesService: WildDuckMessagesService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly s3Service: S3Service,
|
||||
) {}
|
||||
|
||||
//************************************************* */
|
||||
@@ -72,6 +82,35 @@ export class AiAssistantService {
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
async textToSpeechEmailSummary(textToSpeechDto: TextToSpeechDto, userId: string) {
|
||||
const message = await firstValueFrom(this.messagesService.getMessage(userId, textToSpeechDto.mailbox, textToSpeechDto.messageId));
|
||||
|
||||
if (!message) throw new NotFoundException(AiMessage.MESSAGE_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
|
||||
const emailData = this.extractEmailData(message);
|
||||
const summaryResponse = await this.generateEmailSummary(emailData, SummaryLengthEnum.MEDIUM, SummaryToneEnum.PROFESSIONAL);
|
||||
|
||||
// const textForSpeech = this.buildTextForSpeech(summaryResponse, emailData);
|
||||
const textForSpeech = summaryResponse.summary;
|
||||
|
||||
const ttsOptions: TextToSpeechOptions = {
|
||||
voice: textToSpeechDto.voice || VoiceEnum.ALLOY,
|
||||
speed: textToSpeechDto.speed || SpeedEnum.NORMAL,
|
||||
format: textToSpeechDto.format || AudioFormatEnum.MP3,
|
||||
};
|
||||
|
||||
const audioResponse = await this.aiService.textToSpeech(textForSpeech, ttsOptions);
|
||||
|
||||
const fileResponse = await this.saveAudioFile(audioResponse, textToSpeechDto, emailData);
|
||||
|
||||
return {
|
||||
file: fileResponse,
|
||||
summary: summaryResponse,
|
||||
textUsed: textForSpeech,
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private buildTemplateGenerationOptions(dto: GenerateTemplateDto): TemplateGenerationOptions {
|
||||
return {
|
||||
@@ -176,6 +215,65 @@ export class AiAssistantService {
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private async saveAudioFile(audioResponse: AITextToSpeechResponse, textToSpeechDto: TextToSpeechDto, emailData: IEmailData) {
|
||||
const timestamp = Date.now();
|
||||
const sanitizedSubject = emailData.subject?.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 50) || "email_summary";
|
||||
const fileName = `tts_${sanitizedSubject}_${timestamp}.${audioResponse.format}`;
|
||||
|
||||
const s3Key = this.s3Service.generateFileKey(fileName, "audio");
|
||||
|
||||
const s3Result = await this.s3Service.uploadFile(audioResponse.audioBuffer, s3Key, audioResponse.contentType, {
|
||||
originalName: fileName,
|
||||
messageId: textToSpeechDto.messageId.toString(),
|
||||
mailbox: textToSpeechDto.mailbox,
|
||||
voice: textToSpeechDto.voice || VoiceEnum.ALLOY,
|
||||
speed: (textToSpeechDto.speed || SpeedEnum.NORMAL).toString(),
|
||||
format: audioResponse.format,
|
||||
generatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
fileUrl: s3Result.url,
|
||||
fileName,
|
||||
format: audioResponse.format,
|
||||
contentType: audioResponse.contentType,
|
||||
fileSize: audioResponse.audioBuffer.length,
|
||||
s3Key: s3Result.key,
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
// private buildTextForSpeech(summaryResponse: AISummaryResponse, emailData: IEmailData): string {
|
||||
// let text = `Email Summary: `;
|
||||
|
||||
// if (emailData.subject) {
|
||||
// text += `Subject: ${emailData.subject}. `;
|
||||
// }
|
||||
|
||||
// if (emailData.from) {
|
||||
// text += `From: ${emailData.from}. `;
|
||||
// }
|
||||
|
||||
// if (summaryResponse.summary) {
|
||||
// text += `Summary: ${summaryResponse.summary}. `;
|
||||
// }
|
||||
|
||||
// if (summaryResponse.keyPoints && summaryResponse.keyPoints.length > 0) {
|
||||
// text += `Key Points: ${summaryResponse.keyPoints.join(". ")}. `;
|
||||
// }
|
||||
|
||||
// if (summaryResponse.suggestedActions && summaryResponse.suggestedActions.length > 0) {
|
||||
// text += `Suggested Actions: ${summaryResponse.suggestedActions.join(". ")}. `;
|
||||
// }
|
||||
|
||||
// if (summaryResponse.priority) {
|
||||
// text += `Priority: ${summaryResponse.priority}.`;
|
||||
// }
|
||||
|
||||
// return text;
|
||||
// }
|
||||
|
||||
//************************************************* */
|
||||
private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) {
|
||||
const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.prompt);
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
AIServiceInterface,
|
||||
AISummaryResponse,
|
||||
AITemplateGenerationResponse,
|
||||
AITextToSpeechResponse,
|
||||
AIWritingResponse,
|
||||
IEmailData,
|
||||
ITemplateGenerationRequest,
|
||||
SummarizationOptions,
|
||||
TextToSpeechOptions,
|
||||
} from "../interfaces/ai-service.interface";
|
||||
import { AIConfig } from "../interfaces/ai.interface";
|
||||
|
||||
@@ -118,6 +120,37 @@ export class AiService implements AIServiceInterface {
|
||||
|
||||
return parsedResponse;
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
async textToSpeech(text: string, options: TextToSpeechOptions): Promise<AITextToSpeechResponse> {
|
||||
try {
|
||||
const response = await this.openai.audio.speech.create({
|
||||
model: this.aiConfig.defaultTTSModel,
|
||||
voice: options.voice,
|
||||
input: text,
|
||||
response_format: options.format,
|
||||
speed: options.speed,
|
||||
});
|
||||
|
||||
const audioBuffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
const contentTypeMap = {
|
||||
mp3: "audio/mpeg",
|
||||
opus: "audio/opus",
|
||||
aac: "audio/aac",
|
||||
flac: "audio/flac",
|
||||
};
|
||||
|
||||
return {
|
||||
audioBuffer,
|
||||
format: options.format,
|
||||
contentType: contentTypeMap[options.format],
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Error generating text-to-speech", error);
|
||||
throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
private parseJsonResponse<T>(response: string): T {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
|
||||
export interface FileUploadResult {
|
||||
file: File;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FileStreamResult {
|
||||
stream: NodeJS.ReadableStream;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface FileMetadata {
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
codec?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown";
|
||||
|
||||
export interface FileValidationOptions {
|
||||
maxFileSize: number;
|
||||
allowedMimeTypes: string[];
|
||||
}
|
||||
|
||||
export interface FileUploadOptions {
|
||||
roomId?: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// File processing status
|
||||
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed";
|
||||
|
||||
export interface FileProcessingResult {
|
||||
status: FileProcessingStatus;
|
||||
metadata?: FileMetadata;
|
||||
error?: string;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Readable } from "stream";
|
||||
import { randomInt } from "node:crypto";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { FileType } from "../interfaces/file.interface";
|
||||
import { S3UploadResult } from "../interfaces/s3.interface";
|
||||
|
||||
@Injectable()
|
||||
@@ -118,12 +120,21 @@ export class S3Service {
|
||||
/**
|
||||
* Generate S3 key for file
|
||||
*/
|
||||
generateFileKey(originalName: string): string {
|
||||
generateFileKey(originalName: string, fileType: FileType): string {
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.round(Math.random() * 1e9);
|
||||
const randomSuffix = randomInt(1000000, 9999999);
|
||||
const extension = originalName.split(".").pop();
|
||||
|
||||
const basePath = `images`;
|
||||
const basePathMap: Record<FileType, string> = {
|
||||
image: "images",
|
||||
video: "videos",
|
||||
audio: "audio",
|
||||
document: "documents",
|
||||
text: "texts",
|
||||
unknown: "unknowns",
|
||||
};
|
||||
|
||||
const basePath = basePathMap[fileType];
|
||||
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { S3Service } from "./s3.service";
|
||||
import { UploaderMessage } from "../../../common/enums/message.enum";
|
||||
import { FileType } from "../interfaces/file.interface";
|
||||
|
||||
@Injectable()
|
||||
export class UploaderService {
|
||||
@@ -38,6 +39,27 @@ export class UploaderService {
|
||||
"image/svg+xml",
|
||||
"image/tiff",
|
||||
"image/bmp",
|
||||
"application/pdf",
|
||||
"text/plain",
|
||||
"text/csv",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
"application/vnd.ms-excel.template.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
"application/vnd.ms-excel.addin.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
||||
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint.presentation.binary.macroEnabled.12",
|
||||
"application/vnd.ms-excel.template.binary.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint.template.binary.macroEnabled.12",
|
||||
"application/vnd.ms-excel.addin.binary.macroEnabled.12",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -54,14 +76,11 @@ export class UploaderService {
|
||||
}
|
||||
this.logger.log(`Uploading file: ${file.originalname}`);
|
||||
|
||||
// Validate file
|
||||
this.validateFile(file);
|
||||
|
||||
// Determine file type
|
||||
this.determineFileType(file.mimetype);
|
||||
const fileType = await this.determineFileType(file.mimetype);
|
||||
|
||||
// Generate S3 key
|
||||
const s3Key = this.s3Service.generateFileKey(file.originalname);
|
||||
const s3Key = this.s3Service.generateFileKey(file.originalname, fileType);
|
||||
|
||||
// Upload to S3
|
||||
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
|
||||
@@ -83,27 +102,28 @@ export class UploaderService {
|
||||
private validateFile(file: File): void {
|
||||
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||
|
||||
if (file.size && file.size > this.maxFileSize) throw new BadRequestException(`File too large. Maximum size is ${this.maxFileSize} bytes`);
|
||||
if (file.size && file.size > this.maxFileSize)
|
||||
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TOO_LARGE.replace("[MAX_FILE_SIZE]", this.maxFileSize.toString()));
|
||||
|
||||
if (!this.allowedMimeTypes.includes(file.mimetype)) throw new BadRequestException(`File type not supported: ${file.mimetype}`);
|
||||
if (!this.allowedMimeTypes.includes(file.mimetype))
|
||||
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TYPE_NOT_SUPPORTED.replace("[MIME_TYPES]", file.mimetype));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine file type based on mimetype
|
||||
*/
|
||||
private determineFileType(mimetype: string) {
|
||||
private async determineFileType(mimetype: string): Promise<FileType> {
|
||||
if (mimetype.startsWith("video/")) {
|
||||
return "video";
|
||||
} else if (mimetype.startsWith("audio/")) {
|
||||
return "audio";
|
||||
} else if (mimetype.startsWith("image/")) {
|
||||
return "image";
|
||||
} else if (mimetype.startsWith("application/")) {
|
||||
return "application";
|
||||
} else if (mimetype.startsWith("text/")) {
|
||||
return "text";
|
||||
} else if (mimetype.startsWith("application/")) {
|
||||
return "document";
|
||||
}
|
||||
// Default to video for unknown types
|
||||
return "video";
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class UploaderController {
|
||||
@UseInterceptors(FileInterceptor("file"))
|
||||
@ApiBody({ type: UploadSingleFileDto })
|
||||
@Post("single-file")
|
||||
async uploadFile(@UploadedFile() file: File) {
|
||||
uploadFile(@UploadedFile() file: File) {
|
||||
return this.uploaderService.uploadFile(file);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ import { UploaderController } from "./uploader.controller";
|
||||
@Module({
|
||||
controllers: [UploaderController],
|
||||
providers: [UploaderService, S3Service],
|
||||
exports: [UploaderService],
|
||||
exports: [UploaderService, S3Service],
|
||||
})
|
||||
export class UploaderModule {}
|
||||
|
||||
Reference in New Issue
Block a user