Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions apps/api/src/subtitle/dto/burn-subtitle.dto.ts

This file was deleted.

6 changes: 0 additions & 6 deletions apps/api/src/subtitle/dto/create-subtitle.dto.ts

This file was deleted.

10 changes: 0 additions & 10 deletions apps/api/src/subtitle/dto/update-subtitle.dto.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/api/src/subtitle/dto/upload-video.dto.ts

This file was deleted.

50 changes: 32 additions & 18 deletions apps/api/src/subtitle/subtitle.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Req, Res, UseGuards, UnauthorizedException, BadRequestException, UseInterceptors, UploadedFile, ParseFilePipe, MaxFileSizeValidator, FileTypeValidator } from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete, Req, Res, UseGuards, UnauthorizedException, BadRequestException, UseInterceptors, UploadedFile, ParseFilePipe, MaxFileSizeValidator, FileTypeValidator, UsePipes } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { SubtitleService } from './subtitle.service';
import { CreateSubtitleDto } from './dto/create-subtitle.dto';
import { UpdateSubtitleDto } from './dto/update-subtitle.dto';
import { BurnSubtitleDto } from './dto/burn-subtitle.dto';
import { SupabaseAuthGuard } from '../guards/auth.guard';
import { Request, Response } from 'express';
import { UploadVideoDto } from './dto/upload-video.dto';
import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe';
import {
CreateSubtitleSchema,
UpdateSubtitleSchema,
UploadVideoSchema,
BurnSubtitleSchema,
type CreateSubtitleInput,
type UpdateSubtitleInput,
type UploadVideoInput,
type BurnSubtitleInput,
} from '@repo/validation';

interface AuthRequest extends Request {
user?: { id: string };
Expand All @@ -18,13 +25,13 @@ export class SubtitleController {
constructor(private readonly subtitleService: SubtitleService) { }

@Post()
create(@Body() createSubtitleDto: CreateSubtitleDto, @Req() req: AuthRequest) {
@UsePipes(new ZodValidationPipe(CreateSubtitleSchema))
create(@Body() body: CreateSubtitleInput, @Req() req: AuthRequest) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User not found');
}
console.log("working")
return this.subtitleService.create(createSubtitleDto, userId);
return this.subtitleService.create(body, userId);
}

@Get()
Expand All @@ -49,26 +56,26 @@ export class SubtitleController {
}),
)
file: Express.Multer.File,
@Body() uploadVideoDto: UploadVideoDto,
@Body(new ZodValidationPipe(UploadVideoSchema)) body: UploadVideoInput,
@Req() req: AuthRequest,
) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User not found');
}
const filename = file.originalname;
return this.subtitleService.upload(file, uploadVideoDto, userId, filename);
return this.subtitleService.upload(file, body, userId, file.originalname);
}



@Patch()
update(@Body() updateSubtitleDto: UpdateSubtitleDto, @Req() req: AuthRequest) {
@UsePipes(new ZodValidationPipe(UpdateSubtitleSchema))
update(@Body() body: UpdateSubtitleInput, @Req() req: AuthRequest) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User not found');
}
return this.subtitleService.update(updateSubtitleDto, userId);
return this.subtitleService.update(body, userId);
}

@Get(':id')
Expand All @@ -90,24 +97,31 @@ export class SubtitleController {
}

@Patch(':id')
updateSubtitles(@Param('id') id: string, @Body() updateSubtitleDto: UpdateSubtitleDto, @Req() req: AuthRequest) {
updateSubtitles(
@Param('id') id: string,
@Body(new ZodValidationPipe(UpdateSubtitleSchema)) body: UpdateSubtitleInput,
@Req() req: AuthRequest,
) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User not found');
}
// console.log(updateSubtitleDto);
return this.subtitleService.updateSubtitles(id, updateSubtitleDto, userId);
return this.subtitleService.updateSubtitles(id, body, userId);
}

@Post('burn')
async burnSubtitle(@Body() burnSubtitleDto: BurnSubtitleDto, @Res() res: Response, @Req() req: AuthRequest) {
async burnSubtitle(
@Body(new ZodValidationPipe(BurnSubtitleSchema)) body: BurnSubtitleInput,
@Res() res: Response,
@Req() req: AuthRequest,
) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User not found');
}

try {
const videoBuffer = await this.subtitleService.burnSubtitle(burnSubtitleDto);
const videoBuffer = await this.subtitleService.burnSubtitle(body);

res.setHeader('Content-Type', 'video/mp4');
res.setHeader('Content-Disposition', 'attachment; filename=video_with_subtitles.mp4');
Expand Down
62 changes: 28 additions & 34 deletions apps/api/src/subtitle/subtitle.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Injectable, BadRequestException, NotFoundException, ForbiddenException, InternalServerErrorException, PayloadTooLargeException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SupabaseService } from '../supabase/supabase.service';
import { CreateSubtitleDto } from './dto/create-subtitle.dto';
import { UpdateSubtitleDto } from './dto/update-subtitle.dto';
import { UploadVideoDto } from './dto/upload-video.dto';
import { BurnSubtitleDto } from './dto/burn-subtitle.dto';
import { GoogleGenAI } from '@google/genai';
import {
type CreateSubtitleInput,
type UpdateSubtitleInput,
type UploadVideoInput,
type BurnSubtitleInput,
} from '@repo/validation';
import {
createGoogleAI,
type GoogleAIInstance,
fetchVideoAsBuffer,
getFileNameFromUrl,
getMimeTypeFromUrl,
convertJsonToSrt,
configureFFmpeg,
} from '../utils';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs/promises';
import { fetchVideoAsBuffer, getFileNameFromUrl, getMimeTypeFromUrl } from './utils/video-url-tools';
import { convertJsonToSrt } from './utils/srt-converter';
import { configureFFmpeg } from './utils/ffmpeg-config';
import { createReadStream } from 'fs';
import { unlink } from 'fs/promises';

async function waitForFileActive(ai: GoogleGenAI, fileName: string, maxWaitTime = 120000) {
async function waitForFileActive(ai: GoogleAIInstance, fileName: string, maxWaitTime = 120000) {
const startTime = Date.now();
const pollInterval = 3000;

Expand Down Expand Up @@ -64,8 +69,8 @@ export class SubtitleService {
}
}

async create(createSubtitleDto: CreateSubtitleDto, userId: string) {
const { subtitleId, language, targetLanguage, duration } = createSubtitleDto;
async create(input: CreateSubtitleInput, userId: string) {
const { subtitleId, language, targetLanguage, duration } = input;
let tempFilePath: string | null = null;
console.log('started')

Expand Down Expand Up @@ -104,12 +109,7 @@ export class SubtitleService {
}

const video_url = subtitle.video_url;
const apiKey = this.configService.get<string>('GOOGLE_GENERATIVE_AI_API_KEY');
if (!apiKey) {
throw new InternalServerErrorException('Server configuration error');
}

const ai = new GoogleGenAI({ apiKey });
const ai = await createGoogleAI(this.configService);

const isAutoDetect = !language || language.toLowerCase() === 'auto detect' || language.toLowerCase() === 'auto';
const languageInstruction = isAutoDetect
Expand Down Expand Up @@ -302,8 +302,8 @@ Your task is to transcribe the provided audio file and generate precise, time-st
}
}

async update(updateSubtitleDto: UpdateSubtitleDto, userId: string) {
const { subtitle_json, subtitle_id } = updateSubtitleDto;
async update(input: UpdateSubtitleInput, userId: string) {
const { subtitle_json, subtitle_id } = input;

if (!Array.isArray(subtitle_json)) {
throw new BadRequestException('Invalid subtitle format');
Expand Down Expand Up @@ -367,9 +367,8 @@ Your task is to transcribe the provided audio file and generate precise, time-st

}

async updateSubtitles(id: string, updateSubtitleDto: UpdateSubtitleDto, userId: string) {
const { subtitle_json } = updateSubtitleDto;
console.log(subtitle_json);
async updateSubtitles(id: string, input: UpdateSubtitleInput, userId: string) {
const { subtitle_json } = input;
if (!Array.isArray(subtitle_json)) {
throw new BadRequestException('Invalid subtitle format');
}
Expand All @@ -393,11 +392,11 @@ Your task is to transcribe the provided audio file and generate precise, time-st

async upload(
file: Express.Multer.File,
uploadVideoDto: UploadVideoDto,
input: UploadVideoInput,
userId: string,
filename: string,
) {
const { duration } = uploadVideoDto;
const { duration } = input;

if (!file) {
throw new BadRequestException('No file provided');
Expand Down Expand Up @@ -441,8 +440,7 @@ Your task is to transcribe the provided audio file and generate precise, time-st
} catch (err) {
throw new InternalServerErrorException('Failed to upload video');
} finally {
// Always remove temp file
await unlink(file.path).catch(() => null);
await fs.unlink(file.path).catch(() => null);
}

// Get public URL
Expand Down Expand Up @@ -481,12 +479,8 @@ Your task is to transcribe the provided audio file and generate precise, time-st
};
}

async burnSubtitle(burnSubtitleDto: BurnSubtitleDto): Promise<Buffer> {
const { videoUrl, subtitles } = burnSubtitleDto;

if (!videoUrl || !subtitles || !Array.isArray(subtitles)) {
throw new BadRequestException('Missing videoUrl or subtitles');
}
async burnSubtitle(input: BurnSubtitleInput): Promise<Buffer> {
const { videoUrl, subtitles } = input;

console.log('Starting burnSubtitle process...');
console.log(' Video URL:', videoUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ export const configureFFmpeg = () => {

return (input?: string) => ffmpeg(input);
};

21 changes: 21 additions & 0 deletions apps/api/src/utils/genai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ConfigService } from '@nestjs/config';

let genaiModule: typeof import('@google/genai') | null = null;

async function loadGenAI() {
if (!genaiModule) {
genaiModule = await import('@google/genai');
}
return genaiModule;
}

export async function createGoogleAI(configService: ConfigService) {
const { GoogleGenAI } = await loadGenAI();
const apiKey = configService.get<string>('GOOGLE_GENERATIVE_AI_API_KEY');
if (!apiKey) {
throw new Error('GOOGLE_GENERATIVE_AI_API_KEY is not configured');
}
return new GoogleGenAI({ apiKey });
}

export type GoogleAIInstance = Awaited<ReturnType<typeof createGoogleAI>>;
4 changes: 4 additions & 0 deletions apps/api/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './genai';
export * from './ffmpeg-config';
export * from './srt-converter';
export * from './video-url-tools';
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
interface SubtitleLine {
start: string;
end: string;
text: string;
}
import { SubtitleLine } from '@repo/validation';

export function convertJsonToSrt(subtitles: SubtitleLine[]): string {
if (!subtitles || !Array.isArray(subtitles) || subtitles.length === 0) {
Expand All @@ -20,4 +16,3 @@ export function convertJsonToSrt(subtitles: SubtitleLine[]): string {
})
.join('\n\n');
}

Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,3 @@ export function getMimeTypeFromUrl(url: string): string {
return "application/octet-stream";
}
}

1 change: 1 addition & 0 deletions packages/validations/src/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./schema";
export * from "./dubbing.schema";
export * from "./subtitle.schema";

34 changes: 34 additions & 0 deletions packages/validations/src/schema/subtitle.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from 'zod';

export const SubtitleLineSchema = z.object({
start: z.string(),
end: z.string(),
text: z.string(),
});

export const CreateSubtitleSchema = z.object({
subtitleId: z.string().min(1),
language: z.string().optional(),
targetLanguage: z.string().optional(),
duration: z.number().optional(),
});

export const UpdateSubtitleSchema = z.object({
subtitle_json: z.array(SubtitleLineSchema),
subtitle_id: z.string().min(1),
});

export const UploadVideoSchema = z.object({
duration: z.string(),
});

export const BurnSubtitleSchema = z.object({
videoUrl: z.string().url(),
subtitles: z.array(SubtitleLineSchema),
});

export type SubtitleLine = z.infer<typeof SubtitleLineSchema>;
export type CreateSubtitleInput = z.infer<typeof CreateSubtitleSchema>;
export type UpdateSubtitleInput = z.infer<typeof UpdateSubtitleSchema>;
export type UploadVideoInput = z.infer<typeof UploadVideoSchema>;
export type BurnSubtitleInput = z.infer<typeof BurnSubtitleSchema>;
7 changes: 1 addition & 6 deletions packages/validations/src/types/SubtitleTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export type SubtitleResponse = {
id: string;
user_id: string;
Expand All @@ -15,8 +14,4 @@ export type SubtitleResponse = {
filename: string;
};

export type SubtitleLine = {
start: string;
end: string;
text: string;
};
// SubtitleLine is now exported from schema/subtitle.schema.ts
Loading