diff --git a/apps/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts index b3284f44..cdbe146e 100644 --- a/apps/backend/src/api/routes/media.controller.ts +++ b/apps/backend/src/api/routes/media.controller.ts @@ -23,6 +23,7 @@ import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custo import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; +import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto'; @ApiTags('Media') @Controller('/media') @@ -108,10 +109,7 @@ export class MediaController { @GetOrgFromRequest() org: Organization, @Body() body: SaveMediaInformationDto ) { - return this._mediaService.saveMediaInformation( - org.id, - body - ); + return this._mediaService.saveMediaInformation(org.id, body); } @Post('/upload-simple') @@ -167,4 +165,18 @@ export class MediaController { ) { return this._mediaService.getMedia(org.id, page); } + + @Get('/video-options') + getVideos() { + return this._mediaService.getVideoOptions(); + } + + @Post('/generate-video/:type') + generateVideo( + @GetOrgFromRequest() org: Organization, + @Body() body: VideoDto, + @Param('type') type: string, + ) { + return this._mediaService.generateVideo(org.id, body, type); + } } diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 60967129..dd7b8213 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module'; import { McpModule } from '@gitroom/backend/mcp/mcp.module'; import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module'; +import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module'; @Global() @Module({ @@ -24,6 +25,7 @@ import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdpart AgentModule, McpModule, ThirdPartyModule, + VideoModule, ThrottlerModule.forRoot([ { ttl: 3600000, diff --git a/apps/backend/src/services/auth/permissions/permissions.service.test.ts b/apps/backend/src/services/auth/permissions/permissions.service.test.ts deleted file mode 100644 index 40dac276..00000000 --- a/apps/backend/src/services/auth/permissions/permissions.service.test.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; -import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; -import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; -import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service'; -import { PermissionsService } from './permissions.service'; -import { AuthorizationActions, Sections } from './permissions.service'; -import { Period, SubscriptionTier } from '@prisma/client'; - -// Mock of dependent services -const mockSubscriptionService = mock(); -const mockPostsService = mock(); -const mockIntegrationService = mock(); -const mockWebHookService = mock(); - -describe('PermissionsService', () => { - let service: PermissionsService; - - // Initial setup before each test - beforeEach(() => { - process.env.STRIPE_PUBLISHABLE_KEY = 'mock_stripe_key'; - service = new PermissionsService( - mockSubscriptionService, - mockPostsService, - mockIntegrationService, - mockWebHookService - ); - }); - - // Reusable mocks for `getPackageOptions` - const baseSubscription = { - id: 'mock-id', - organizationId: 'mock-org-id', - subscriptionTier: 'PRO' as SubscriptionTier, - identifier: 'mock-identifier', - cancelAt: new Date(), - period: {} as Period, - totalChannels: 5, - isLifetime: false, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - disabled: false, - tokenExpiration: new Date(), - profile: 'mock-profile', - postingTimes: '[]', - lastPostedAt: new Date(), - }; - - const baseOptions = { - channel: 10, - current: 'mock-current', - month_price: 20, - year_price: 200, - posts_per_month: 100, - team_members: true, - community_features: true, - featured_by_gitroom: true, - ai: true, - import_from_channels: true, - image_generator: false, - image_generation_count: 50, - public_api: true, - webhooks: 10, - autoPost: true, // Added the missing property - }; - - const baseIntegration = { - id: 'mock-integration-id', - organizationId: 'mock-org-id', - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: new Date(), - additionalSettings: '{}', - refreshNeeded: false, - refreshToken: 'mock-refresh-token', - name: 'Mock Integration', - internalId: 'mock-internal-id', - picture: 'mock-picture-url', - providerIdentifier: 'mock-provider', - token: 'mock-token', - type: 'social', - inBetweenSteps: false, - disabled: false, - tokenExpiration: new Date(), - profile: 'mock-profile', - postingTimes: '[]', - lastPostedAt: new Date(), - customInstanceDetails: 'mock-details', - customerId: 'mock-customer-id', - rootInternalId: 'mock-root-id', - customer: { - id: 'mock-customer-id', - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: new Date(), - name: 'Mock Customer', - orgId: 'mock-org-id', - }, - }; - - describe('check()', () => { - describe('Verification Bypass (64)', () => { - it('Bypass for Empty List', async () => { - // Setup: STRIPE_PUBLISHABLE_KEY exists and requestedPermission is empty - - // Execution: call the check method with an empty list of permissions - const result = await service.check( - 'mock-org-id', - new Date(), - 'ADMIN', - [] // empty requestedPermission - ); - - // Verification: not requested, no authorization - expect( - result.cannot(AuthorizationActions.Create, Sections.CHANNEL) - ).toBe(true); - }); - - it('Bypass for Missing Stripe', async () => { - // Setup: STRIPE_PUBLISHABLE_KEY does not exist - process.env.STRIPE_PUBLISHABLE_KEY = undefined; - // Necessary mock to avoid undefined filter error - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([{ ...baseIntegration, refreshNeeded: false }]); - // Mock of getPackageOptions (even if not used due to bypass) - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: baseSubscription, - options: baseOptions, - }); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Read, Sections.CHANNEL], - [AuthorizationActions.Create, Sections.AI], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should allow all requested actions due to the absence of the Stripe key - expect(result.can(AuthorizationActions.Read, Sections.CHANNEL)).toBe( - true - ); - expect(result.can(AuthorizationActions.Create, Sections.AI)).toBe(true); - }); - - it('No Bypass', async () => { - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Read, Sections.CHANNEL], - [AuthorizationActions.Create, Sections.AI], - ]; - // Mock of getPackageOptions to force a scenario without permissions - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 0 }, - options: { - ...baseOptions, - channel: 0, - ai: false, - }, - }); - // Mock of getIntegrationsList for the channel scenario - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([{ ...baseIntegration, refreshNeeded: false }]); - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should not allow the requested actions as there is no bypass - expect(result.can(AuthorizationActions.Read, Sections.CHANNEL)).toBe( - false - ); - expect(result.can(AuthorizationActions.Create, Sections.AI)).toBe( - false - ); - }); - }); - - describe('Channel Permission (82/87)', () => { - it('All Conditions True', async () => { - // Mock of getPackageOptions to set channel limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 10 }, - options: { ...baseOptions, channel: 10 }, - }); - - // Mock of getIntegrationsList to set existing channels - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([ - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - ]); - - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.CHANNEL], - ]; - - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should allow the requested action - expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe( - true - ); - }); - - it('Channel With Option Limit', async () => { - // Mock of getPackageOptions to set channel limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 3 }, - options: { ...baseOptions, channel: 10 }, - }); - // Mock of getIntegrationsList to set existing channels - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([ - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - ]); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.CHANNEL], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should allow the requested action - expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe( - true - ); - }); - - it('Channel With Subscription Limit', async () => { - // Mock of getPackageOptions to set channel limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 10 }, - options: { ...baseOptions, channel: 3 }, - }); - // Mock of getIntegrationsList to set existing channels - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([ - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - ]); - - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.CHANNEL], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should allow the requested action - expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe( - true - ); - }); - it('Channel Without Available Limits', async () => { - // Mock of getPackageOptions to set channel limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 3 }, - options: { ...baseOptions, channel: 3 }, - }); - // Mock of getIntegrationsList to set existing channels - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([ - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - ]); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.CHANNEL], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should not allow the requested action - expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe( - false - ); - }); - it('Section Different from Channel', async () => { - // Mock of getPackageOptions to set channel limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: { ...baseSubscription, totalChannels: 10 }, - options: { ...baseOptions, channel: 10 }, - }); - // Mock of getIntegrationsList to set existing channels - jest - .spyOn(mockIntegrationService, 'getIntegrationsList') - .mockResolvedValue([ - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - { ...baseIntegration, refreshNeeded: false }, - ]); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.AI], // Requesting permission for AI instead of CHANNEL - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should not allow the requested action in CHANNEL - expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe( - false - ); - }); - }); - describe('Monthly Posts Permission (97/110)', () => { - it('Posts Within Limit', async () => { - // Mock of getPackageOptions to set post limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: baseSubscription, - options: { ...baseOptions, posts_per_month: 100 }, - }); - // Mock of getSubscription - jest - .spyOn(mockSubscriptionService, 'getSubscription') - .mockResolvedValue({ - ...baseSubscription, - createdAt: new Date(), - }); - // Mock of countPostsFromDay to return quantity within the limit - jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(50); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.POSTS_PER_MONTH], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should allow the requested action - expect( - result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH) - ).toBe(true); - }); - it('Posts Exceed Limit', async () => { - // Mock of getPackageOptions to set post limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: baseSubscription, - options: { ...baseOptions, posts_per_month: 100 }, - }); - // Mock of getSubscription - jest - .spyOn(mockSubscriptionService, 'getSubscription') - .mockResolvedValue({ - ...baseSubscription, - createdAt: new Date(), - }); - // Mock of countPostsFromDay to return quantity above the limit - jest - .spyOn(mockPostsService, 'countPostsFromDay') - .mockResolvedValue(150); - - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.POSTS_PER_MONTH], - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should not allow the requested action - expect( - result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH) - ).toBe(false); - }); - it('Section Different with Posts Within Limit', async () => { - // Mock of getPackageOptions to set post limits - jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ - subscription: baseSubscription, - options: { ...baseOptions, posts_per_month: 100 }, - }); - // Mock of getSubscription - jest - .spyOn(mockSubscriptionService, 'getSubscription') - .mockResolvedValue({ - ...baseSubscription, - createdAt: new Date(), - }); - // Mock of countPostsFromDay to return quantity within the limit - jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(50); - // List of requested permissions - const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ - [AuthorizationActions.Create, Sections.AI], // Requesting permission for AI instead of POSTS_PER_MONTH - ]; - // Execution: call the check method - const result = await service.check( - 'mock-org-id', - new Date(), - 'USER', - requestedPermissions - ); - // Verification: should not allow the requested action in POSTS_PER_MONTH - expect( - result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH) - ).toBe(false); - }); - }); - }); -}); diff --git a/apps/frontend/src/components/launches/ai.image.tsx b/apps/frontend/src/components/launches/ai.image.tsx index 1fdc9f96..5d6e3469 100644 --- a/apps/frontend/src/components/launches/ai.image.tsx +++ b/apps/frontend/src/components/launches/ai.image.tsx @@ -98,7 +98,7 @@ ${type}
- {t('ai', 'AI')} + {t('ai', 'AI')} Image
diff --git a/apps/frontend/src/components/launches/ai.video.tsx b/apps/frontend/src/components/launches/ai.video.tsx new file mode 100644 index 00000000..00f5d4c3 --- /dev/null +++ b/apps/frontend/src/components/launches/ai.video.tsx @@ -0,0 +1,203 @@ +import { Button } from '@gitroom/react/form/button'; +import React, { FC, useCallback, useState } from 'react'; +import clsx from 'clsx'; +import Loading from 'react-loading'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; +import useSWR from 'swr'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { Input } from '@gitroom/react/form/input'; +import { timer } from '@gitroom/helpers/utils/timer'; + +export const Modal: FC<{ + close: () => void; + value: string; + type: any; + setLoading: (loading: boolean) => void; + onChange: (params: { id: string; path: string }) => void; +}> = (props) => { + const { type, value, onChange, close, setLoading } = props; + const fetch = useFetch(); + const setLocked = useLaunchStore(state => state.setLocked) + + const generate = useCallback( + (output: string) => async () => { + setLoading(true); + close(); + setLocked(true); + + await timer(5000); + const image = await ( + await fetch(`/media/generate-video/${type.identifier}`, { + method: 'POST', + body: JSON.stringify({ + prompt: [{ type: 'prompt', value }], + output: output, + }), + }) + ).json(); + + setLocked(false); + setLoading(false); + onChange(image); + }, + [type, value] + ); + + return ( +
+
+
+
+ +
+ +
+
+ + +
+
+
+ ); +}; + +export const AiVideo: FC<{ + value: string; + onChange: (params: { id: string; path: string }) => void; +}> = (props) => { + const t = useT(); + const { value, onChange } = props; + const [loading, setLoading] = useState(false); + const [type, setType] = useState(null); + const [modal, setModal] = useState(false); + const fetch = useFetch(); + + const loadVideoList = useCallback(async () => { + return (await (await fetch('/media/video-options')).json()).filter( + (f: any) => f.placement === 'text-to-image' + ); + }, []); + + const { isLoading, data } = useSWR('load-videos-ai', loadVideoList, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenHidden: false, + revalidateIfStale: false, + refreshWhenOffline: false, + keepPreviousData: true, + }); + + const generateVideo = useCallback( + (type: { identifier: string }) => async () => { + setType(type); + setModal(true); + }, + [value, onChange] + ); + + if (isLoading) { + return null; + } + + return ( + <> + {modal && ( + { + setModal(false); + setType(null); + }} + type={type} + value={props.value} + /> + )} +
+ + {value.length >= 30 && !loading && ( +
+
    + {data.map((p: any) => ( +
  • + {p.title} +
  • + ))} +
+
+ )} +
+ + ); +}; diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index c7f9fd2c..7fffddb6 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -32,6 +32,7 @@ import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/thir import { ReactSortable } from 'react-sortablejs'; import { useMediaSettings } from '@gitroom/frontend/components/launches/helpers/media.settings.component'; import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; +import { AiVideo } from '@gitroom/frontend/components/launches/ai.video'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); @@ -710,7 +711,10 @@ export const MultiMediaComponent: FC<{ {!!user?.tier?.ai && ( - + <> + + + )} diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index 031c29e0..db1756d1 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -39,6 +39,7 @@ import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository'; import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.repository'; import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.service'; +import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager'; @Global() @Module({ @@ -86,6 +87,7 @@ import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/thi SetsRepository, ThirdPartyRepository, ThirdPartyService, + VideoManager, ], get exports() { return this.providers; diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts index 496504f9..941559d5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts @@ -4,13 +4,19 @@ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { Organization } from '@prisma/client'; import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; +import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager'; +import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto'; +import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; @Injectable() export class MediaService { + private storage = UploadFactory.createStorage(); + constructor( private _mediaRepository: MediaRepository, private _openAi: OpenaiService, - private _subscriptionService: SubscriptionService + private _subscriptionService: SubscriptionService, + private _videoManager: VideoManager ) {} async deleteMedia(org: string, id: string) { @@ -49,4 +55,20 @@ export class MediaService { saveMediaInformation(org: string, data: SaveMediaInformationDto) { return this._mediaRepository.saveMediaInformation(org, data); } + + getVideoOptions() { + return this._videoManager.getAllVideos(); + } + + async generateVideo(org: string, body: VideoDto, type: string) { + const video = this._videoManager.getVideoByName(type); + if (!video) { + throw new Error(`Video type ${type} not found`); + } + + const loadedData = await video.instance.process(body.prompt, body.output); + + // const file = await this.storage.uploadSimple(loadedData); + // return this.saveFile(org, file.split('/').pop(), file); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index 6f7e9598..265c1a6a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -11,6 +11,7 @@ export interface PricingInnerInterface { import_from_channels: boolean; image_generator?: boolean; image_generation_count: number; + generate_videos: number; public_api: boolean; webhooks: number; autoPost: boolean; @@ -35,6 +36,7 @@ export const pricing: PricingInterface = { public_api: false, webhooks: 0, autoPost: false, + generate_videos: 0, }, STANDARD: { current: 'STANDARD', @@ -52,6 +54,7 @@ export const pricing: PricingInterface = { public_api: true, webhooks: 2, autoPost: false, + generate_videos: 3, }, TEAM: { current: 'TEAM', @@ -69,6 +72,7 @@ export const pricing: PricingInterface = { public_api: true, webhooks: 10, autoPost: true, + generate_videos: 10, }, PRO: { current: 'PRO', @@ -86,6 +90,7 @@ export const pricing: PricingInterface = { public_api: true, webhooks: 30, autoPost: true, + generate_videos: 50, }, ULTIMATE: { current: 'ULTIMATE', @@ -103,5 +108,6 @@ export const pricing: PricingInterface = { public_api: true, webhooks: 10000, autoPost: true, + generate_videos: 100, }, }; diff --git a/libraries/nestjs-libraries/src/dtos/videos/video.dto.ts b/libraries/nestjs-libraries/src/dtos/videos/video.dto.ts new file mode 100644 index 00000000..7a615666 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/videos/video.dto.ts @@ -0,0 +1,20 @@ +import { Type } from 'class-transformer'; +import { IsArray, IsIn, IsString, ValidateNested } from 'class-validator'; + +export class Prompt { + @IsIn(['prompt', 'image']) + type: 'prompt' | 'image'; + + @IsString() + value: string; +} + +export class VideoDto { + @ValidateNested({ each: true }) + @IsArray() + @Type(() => Prompt) + prompt: Prompt[]; + + @IsIn(['vertical', 'horizontal']) + output: 'vertical' | 'horizontal'; +} diff --git a/libraries/nestjs-libraries/src/openai/openai.service.ts b/libraries/nestjs-libraries/src/openai/openai.service.ts index fe0f5182..d772879a 100644 --- a/libraries/nestjs-libraries/src/openai/openai.service.ts +++ b/libraries/nestjs-libraries/src/openai/openai.service.ts @@ -18,12 +18,13 @@ const VoicePrompt = z.object({ @Injectable() export class OpenaiService { - async generateImage(prompt: string, isUrl: boolean) { + async generateImage(prompt: string, isUrl: boolean, isVertical = false) { const generate = ( await openai.images.generate({ prompt, response_format: isUrl ? 'url' : 'b64_json', model: 'dall-e-3', + ...(isVertical ? { size: '1024x1792' } : {}), }) ).data[0]; @@ -224,4 +225,36 @@ export class OpenaiService { ), }; } + + async generateSlidesFromText(text: string) { + const message = `You are an assistant that takes a text and break it into slides, each slide should have an image prompt and voice text to be later used to generate a video and voice, image prompt should capture the essence of the slide, generate between 3-5 slides maximum`; + return ( + ( + await openai.beta.chat.completions.parse({ + model: 'gpt-4.1', + messages: [ + { + role: 'system', + content: message, + }, + { + role: 'user', + content: text, + }, + ], + response_format: zodResponseFormat( + z.object({ + slides: z.array( + z.object({ + imagePrompt: z.string(), + voiceText: z.string(), + }) + ), + }), + 'slides' + ), + }) + ).choices[0].message.parsed?.slides || [] + ); + } } diff --git a/libraries/nestjs-libraries/src/videos/images-slides/images.slides.ts b/libraries/nestjs-libraries/src/videos/images-slides/images.slides.ts new file mode 100644 index 00000000..2828077a --- /dev/null +++ b/libraries/nestjs-libraries/src/videos/images-slides/images.slides.ts @@ -0,0 +1,207 @@ +import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; +import { + Prompt, + Video, + VideoAbstract, +} from '@gitroom/nestjs-libraries/videos/video.interface'; +import { chunk } from 'lodash'; +import Transloadit from 'transloadit'; +import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import { Readable } from 'stream'; +import { parseBuffer } from 'music-metadata'; +import { stringifySync } from 'subtitle'; + +const transloadit = new Transloadit({ + authKey: process.env.TRANSLOADIT_AUTH, + authSecret: process.env.TRANSLOADIT_SECRET, +}); + +async function getAudioDuration(buffer: Buffer): Promise { + const metadata = await parseBuffer(buffer, 'audio/mpeg'); + return metadata.format.duration || 0; +} + +@Video({ + identifier: 'image-text-slides', + title: 'Image Text Slides', + description: 'Generate videos slides from images and text', + placement: 'text-to-image', +}) +export class ImagesSlides extends VideoAbstract { + private storage = UploadFactory.createStorage(); + constructor(private _openaiService: OpenaiService) { + super(); + } + + async process( + prompt: Prompt[], + output: 'vertical' | 'horizontal' + ): Promise { + const list = await this._openaiService.generateSlidesFromText( + prompt[0].value + ); + const generated = await Promise.all( + list.reduce((all, current) => { + all.push( + new Promise(async (res) => { + res({ + len: 0, + url: await this._openaiService.generateImage( + current.imagePrompt + + (output === 'vertical' ? ', vertical composition' : ''), + true, + output === 'vertical' + ), + }); + }) + ); + + all.push( + new Promise(async (res) => { + const buffer = Buffer.from( + await ( + await fetch( + `https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb?output_format=mp3_44100_128`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'xi-api-key': process.env.ELEVENSLABS_API_KEY || '', + }, + body: JSON.stringify({ + text: current.voiceText, + voice_settings: { + stability: 0.75, + similarity_boost: 0.75, + }, + }), + } + ) + ).arrayBuffer() + ); + + const { path } = await this.storage.uploadFile({ + buffer, + mimetype: 'audio/mp3', + size: buffer.length, + path: '', + fieldname: '', + destination: '', + stream: new Readable(), + filename: '', + originalname: '', + encoding: '', + }); + + res({ + len: await getAudioDuration(buffer), + url: + path.indexOf('http') === -1 + ? process.env.FRONTEND_URL + + '/' + + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + + path + : path, + }); + }) + ); + + return all; + }, [] as Promise[]) + ); + + const split = chunk(generated, 2); + + const srt = stringifySync( + list + .reduce((all, current, index) => { + const start = all.length ? all[all.length - 1].end : 0; + const end = start + split[index][1].len * 1000 + 1000; + all.push({ + start: start, + end: end, + text: current.voiceText, + }); + + return all; + }, [] as { start: number; end: number; text: string }[]) + .map((item) => ({ + type: 'cue', + data: item, + })), + { format: 'SRT' } + ); + console.log(srt); + + await transloadit.createAssembly({ + uploads: { + 'subtitles.srt': srt, + }, + params: { + steps: { + ...split.reduce((all, current, index) => { + all[`image${index}`] = { + robot: '/http/import', + url: current[0].url, + }; + all[`audio${index}`] = { + robot: '/http/import', + url: current[1].url, + }; + all[`merge${index}`] = { + use: [ + { + name: `image${index}`, + as: 'image', + }, + { + name: `audio${index}`, + as: 'audio', + }, + ], + robot: '/video/merge', + duration: current[1].len + 1, + audio_delay: 0.5, + preset: 'hls-1080p', + resize_strategy: 'min_fit', + loop: true, + }; + return all; + }, {} as any), + concatenated: { + robot: '/video/concat', + result: false, + video_fade_seconds: 0.5, + use: split.map((p, index) => ({ + name: `merge${index}`, + as: `video_${index + 1}`, + })), + }, + subtitled: { + robot: '/video/subtitle', + result: true, + preset: 'hls-1080p', + use: { + bundle_steps: true, + steps: [ + { + name: 'concatenated', + as: 'video', + }, + { + name: ':original', + as: 'subtitles', + }, + ], + }, + position: 'center', + font_size: 10, + subtitles_type: 'burned', + }, + }, + }, + }); + + return ''; + } +} diff --git a/libraries/nestjs-libraries/src/videos/video.interface.ts b/libraries/nestjs-libraries/src/videos/video.interface.ts new file mode 100644 index 00000000..d5e4f2fa --- /dev/null +++ b/libraries/nestjs-libraries/src/videos/video.interface.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; + +export interface Prompt { + type: 'prompt' | 'image'; + value: string; +} + +export abstract class VideoAbstract { + abstract process( + prompt: Prompt[], + output: 'vertical' | 'horizontal' + ): Promise; +} + +export interface VideoParams { + identifier: string; + title: string; + description: string; + placement: 'text-to-image' | 'image-to-video' | 'video-to-video'; +} + +export function Video(params: VideoParams) { + return function (target: any) { + // Apply @Injectable decorator to the target class + Injectable()(target); + + // Retrieve existing metadata or initialize an empty array + const existingMetadata = Reflect.getMetadata('video', VideoAbstract) || []; + + // Add the metadata information for this method + existingMetadata.push({ target, ...params }); + + // Define metadata on the class prototype (so it can be retrieved from the class) + Reflect.defineMetadata('video', existingMetadata, VideoAbstract); + }; +} diff --git a/libraries/nestjs-libraries/src/videos/video.manager.ts b/libraries/nestjs-libraries/src/videos/video.manager.ts new file mode 100644 index 00000000..394a56ea --- /dev/null +++ b/libraries/nestjs-libraries/src/videos/video.manager.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; + +import { + VideoAbstract, + VideoParams, +} from '@gitroom/nestjs-libraries/videos/video.interface'; + +@Injectable() +export class VideoManager { + constructor(private _moduleRef: ModuleRef) {} + + getAllVideos(): any[] { + return (Reflect.getMetadata('video', VideoAbstract) || []).map( + (p: any) => ({ + identifier: p.identifier, + title: p.title, + description: p.description, + placement: p.placement, + }) + ); + } + + getVideoByName( + identifier: string + ): (VideoParams & { instance: VideoAbstract }) | undefined { + const video = (Reflect.getMetadata('video', VideoAbstract) || []).find( + (p: any) => p.identifier === identifier + ); + + return { + ...video, + instance: this._moduleRef.get(video.target, { + strict: false, + }), + }; + } +} diff --git a/libraries/nestjs-libraries/src/videos/video.module.ts b/libraries/nestjs-libraries/src/videos/video.module.ts new file mode 100644 index 00000000..c7d8f06a --- /dev/null +++ b/libraries/nestjs-libraries/src/videos/video.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; +import { ImagesSlides } from '@gitroom/nestjs-libraries/videos/images-slides/images.slides'; +import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager'; + +@Global() +@Module({ + providers: [ImagesSlides, VideoManager], + get exports() { + return this.providers; + }, +}) +export class VideoModule {} diff --git a/package.json b/package.json index ba642b58..57757e2e 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "mime": "^3.0.0", "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", + "music-metadata": "^11.6.0", "nestjs-command": "^3.1.4", "nestjs-real-ip": "^3.0.1", "next": "^14.2.14", @@ -197,12 +198,14 @@ "simple-statistics": "^7.8.3", "stripe": "^15.5.0", "striptags": "^3.2.0", + "subtitle": "4.2.2-alpha.0", "sweetalert2": "11.4.8", "swr": "^2.2.5", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", "tailwindcss-rtl": "^0.9.0", "tldts": "^6.1.47", + "transloadit": "^3.0.2", "tslib": "^2.3.0", "tweetnacl": "^1.0.3", "twitter-api-v2": "^1.23.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e78532a..3ce1b833 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: version: 4.0.0 axios: specifier: ^1.7.7 - version: 1.9.0(debug@4.4.0) + version: 1.9.0 bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -354,6 +354,9 @@ importers: multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.2 + music-metadata: + specifier: ^11.6.0 + version: 11.6.0 nestjs-command: specifier: ^3.1.4 version: 3.1.5(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.17)(yargs@17.7.2) @@ -468,6 +471,9 @@ importers: striptags: specifier: ^3.2.0 version: 3.2.0 + subtitle: + specifier: 4.2.2-alpha.0 + version: 4.2.2-alpha.0 sweetalert2: specifier: 11.4.8 version: 11.4.8 @@ -486,6 +492,9 @@ importers: tldts: specifier: ^6.1.47 version: 6.1.86 + transloadit: + specifier: ^3.0.2 + version: 3.0.2 tslib: specifier: ^2.3.0 version: 2.8.1 @@ -6227,6 +6236,9 @@ packages: '@types/multer@1.4.12': resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==} + '@types/multipipe@3.0.5': + resolution: {integrity: sha512-mHBbV67bsmUtLtio0gj/GPzGsjv+Y6K1ff/48iR6YAfFfLkBtRIR0M5lZPbkMCyHGrCZM9p3VNnfY1QCws4t4w==} + '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} @@ -6282,6 +6294,9 @@ packages: '@types/react@18.3.1': resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} + '@types/readable-stream@4.0.21': + resolution: {integrity: sha512-19eKVv9tugr03IgfXlA9UVUVRbW6IuqRO5B92Dl4a6pT7K8uaGrNS0GkxiZD0BOk6PLuXl5FhWl//eX/pzYdTQ==} + '@types/redis@2.8.32': resolution: {integrity: sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==} @@ -7037,6 +7052,10 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ai@4.3.13: resolution: {integrity: sha512-cC5HXItuOwGykSMacCPzNp6+NMTxeuTjOenztVgSJhdC9Z4OrzBxwkyeDAf4h1QP938ZFi7IBdq3u4lxVoVmvw==} engines: {node: '>=18'} @@ -7814,6 +7833,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -8405,6 +8428,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -8689,6 +8721,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -9301,6 +9336,10 @@ packages: resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} engines: {node: '>=18'} + file-type@21.0.0: + resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} + engines: {node: '>=20'} + file-type@3.9.0: resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} engines: {node: '>=0.10.0'} @@ -9439,6 +9478,10 @@ packages: resolution: {integrity: sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==} engines: {node: '>= 0.12'} + form-data@3.0.3: + resolution: {integrity: sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==} + engines: {node: '>= 6'} + form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -9470,6 +9513,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} @@ -10133,6 +10179,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -10175,6 +10225,10 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} + into-stream@6.0.0: + resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} + engines: {node: '>=10'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -10721,6 +10775,9 @@ packages: jpeg-exif@1.1.4: resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + js-base64@2.6.4: + resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} + js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} @@ -12025,6 +12082,13 @@ packages: multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + multipipe@4.0.0: + resolution: {integrity: sha512-jzcEAzFXoWwWwUbvHCNPwBlTz3WCWe/jPcXSmTfbo/VjRwRTfvLZ/bdvtiTdqCe8d4otCSsPCbhGYcX+eggpKQ==} + + music-metadata@11.6.0: + resolution: {integrity: sha512-l7MbWpuGM5GK8gol22L9tou8d/IoFyS8dnsfLbO6cocjlyMwgyLaCIqdwhp4sN1Nzz/Ql/K9kRLvRJDCVKjO3g==} + engines: {node: '>=18'} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -12482,6 +12546,10 @@ packages: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} + p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -12506,6 +12574,10 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -13128,6 +13200,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@2.0.1: + resolution: {integrity: sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==} + engines: {node: '>=4.0.0'} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -13830,6 +13906,9 @@ packages: peerDependencies: axios: '*' + retry@0.10.1: + resolution: {integrity: sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -14308,6 +14387,9 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -14499,6 +14581,10 @@ packages: resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} engines: {node: '>=18'} + strtok3@10.3.1: + resolution: {integrity: sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==} + engines: {node: '>=18'} + strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} @@ -14563,6 +14649,10 @@ packages: resolution: {integrity: sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==} hasBin: true + subtitle@4.2.2-alpha.0: + resolution: {integrity: sha512-IMS+L8lXjOLveg5BC/bVZy+36/x2NqMIQmVDhbquDpxLnXugzmz7/yHHFZ7b9YLfqNaBdXwh1lsnAds3g1FnCQ==} + engines: {node: '>=10'} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -14819,6 +14909,10 @@ packages: resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} engines: {node: '>=14.16'} + token-types@6.0.3: + resolution: {integrity: sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==} + engines: {node: '>=14.16'} + toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} @@ -14853,6 +14947,10 @@ packages: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} engines: {node: '>=14'} + transloadit@3.0.2: + resolution: {integrity: sha512-FvhKs0EBiQufK29irGLM/4aMIrfU5S/TiHB3h+DcO2hjRnVVM2WC278UQJCrNO4L/REE8IKWx/mQzQW2MrrLsg==} + engines: {node: '>= 10.0.0'} + tree-dump@1.0.2: resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} engines: {node: '>=10.0'} @@ -14998,6 +15096,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tus-js-client@2.3.2: + resolution: {integrity: sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==} + tus-js-client@4.3.1: resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} engines: {node: '>=18'} @@ -19134,7 +19235,7 @@ snapshots: '@module-federation/third-party-dts-extractor': 0.6.16 adm-zip: 0.5.16 ansi-colors: 4.1.3 - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0 chalk: 3.0.0 fs-extra: 9.1.0 isomorphic-ws: 5.0.0(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -23505,6 +23606,10 @@ snapshots: dependencies: '@types/express': 5.0.1 + '@types/multipipe@3.0.5': + dependencies: + '@types/node': 18.16.9 + '@types/node-fetch@2.6.12': dependencies: '@types/node': 18.16.9 @@ -23562,6 +23667,10 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/readable-stream@4.0.21': + dependencies: + '@types/node': 18.16.9 + '@types/redis@2.8.32': dependencies: '@types/node': 18.16.9 @@ -24772,7 +24881,7 @@ snapshots: '@wyw-in-js/shared@0.5.5': dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 find-up: 5.0.0 minimatch: 9.0.5 transitivePeerDependencies: @@ -24883,6 +24992,11 @@ snapshots: dependencies: humanize-ms: 1.2.1 + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ai@4.3.13(react@18.3.1)(zod@3.24.4): dependencies: '@ai-sdk/provider': 1.1.3 @@ -25177,7 +25291,7 @@ snapshots: transitivePeerDependencies: - debug - axios@1.9.0(debug@4.4.0): + axios@1.9.0: dependencies: follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.2 @@ -25185,6 +25299,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.9.0(debug@4.4.1): + dependencies: + follow-redirects: 1.15.9(debug@4.4.1) + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@3.2.4: {} axobject-query@4.1.0: {} @@ -25839,6 +25961,8 @@ snapshots: classnames@2.5.1: {} + clean-stack@2.2.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -26468,6 +26592,10 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decimal.js@10.5.0: {} @@ -26731,6 +26859,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + duplexer@0.1.2: {} duplexify@4.1.3: @@ -27533,7 +27665,7 @@ snapshots: facebook-nodejs-business-sdk@21.0.5: dependencies: - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0 currency-codes: 1.5.1 iso-3166-1: 2.1.1 js-sha256: 0.9.0 @@ -27665,6 +27797,15 @@ snapshots: transitivePeerDependencies: - supports-color + file-type@21.0.0: + dependencies: + '@tokenizer/inflate': 0.2.7 + strtok3: 10.3.1 + token-types: 6.0.3 + uint8array-extras: 1.4.0 + transitivePeerDependencies: + - supports-color + file-type@3.9.0: {} filelist@1.0.4: @@ -27774,6 +27915,10 @@ snapshots: optionalDependencies: debug: 4.4.0(supports-color@5.5.0) + follow-redirects@1.15.9(debug@4.4.1): + optionalDependencies: + debug: 4.4.1 + fontkit@1.9.0: dependencies: '@swc/helpers': 0.3.17 @@ -27847,6 +27992,13 @@ snapshots: mime-types: 2.1.35 safe-buffer: 5.2.1 + form-data@3.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -27875,6 +28027,11 @@ snapshots: fresh@2.0.0: {} + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + front-matter@4.0.2: dependencies: js-yaml: 3.14.1 @@ -28756,9 +28913,9 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.19.87 '@types/tough-cookie': 4.0.5 - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0(debug@4.4.1) camelcase: 6.3.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 dotenv: 16.5.0 extend: 3.0.2 file-type: 16.5.4 @@ -28827,6 +28984,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -28909,6 +29068,11 @@ snapshots: interpret@1.4.0: {} + into-stream@6.0.0: + dependencies: + from2: 2.3.0 + p-is-promise: 3.0.0 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -29644,6 +29808,8 @@ snapshots: jpeg-exif@1.1.4: {} + js-base64@2.6.4: {} + js-base64@3.7.7: {} js-beautify@1.15.4: @@ -29964,7 +30130,7 @@ snapshots: zod: 3.24.4 zod-to-json-schema: 3.24.5(zod@3.24.4) optionalDependencies: - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0 cheerio: 1.0.0 transitivePeerDependencies: - encoding @@ -30733,7 +30899,7 @@ snapshots: metro-file-map@0.82.2: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -30829,7 +30995,7 @@ snapshots: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -31423,6 +31589,24 @@ snapshots: multiformats@9.9.0: {} + multipipe@4.0.0: + dependencies: + duplexer2: 0.1.4 + object-assign: 4.1.1 + + music-metadata@11.6.0: + dependencies: + '@tokenizer/token': 0.3.0 + content-type: 1.0.5 + debug: 4.4.1 + file-type: 21.0.0 + media-typer: 1.1.0 + strtok3: 10.3.1 + token-types: 6.0.3 + uint8array-extras: 1.4.0 + transitivePeerDependencies: + - supports-color + mustache@4.2.0: {} mute-stream@0.0.8: {} @@ -31698,7 +31882,7 @@ snapshots: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.7 - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -31975,6 +32159,8 @@ snapshots: p-finally@1.0.0: {} + p-is-promise@3.0.0: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -31999,6 +32185,10 @@ snapshots: dependencies: p-limit: 4.0.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -32648,6 +32838,11 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@2.0.1: + dependencies: + graceful-fs: 4.2.11 + retry: 0.10.1 + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -33563,7 +33758,9 @@ snapshots: retry-axios@2.6.0(axios@1.9.0): dependencies: - axios: 1.9.0(debug@4.4.0) + axios: 1.9.0 + + retry@0.10.1: {} retry@0.12.0: {} @@ -34170,6 +34367,10 @@ snapshots: split-on-first@1.1.0: {} + split2@3.2.2: + dependencies: + readable-stream: 3.6.2 + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -34378,6 +34579,10 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 7.0.0 + strtok3@10.3.1: + dependencies: + '@tokenizer/token': 0.3.0 + strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 @@ -34444,6 +34649,15 @@ snapshots: transitivePeerDependencies: - supports-color + subtitle@4.2.2-alpha.0: + dependencies: + '@types/multipipe': 3.0.5 + '@types/readable-stream': 4.0.21 + multipipe: 4.0.0 + readable-stream: 4.7.0 + split2: 3.2.2 + strip-bom: 4.0.0 + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -34712,6 +34926,11 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + token-types@6.0.3: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + toposort@2.0.2: {} totalist@3.0.1: {} @@ -34744,6 +34963,20 @@ snapshots: dependencies: punycode: 2.3.1 + transloadit@3.0.2: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + form-data: 3.0.3 + got: 11.8.6 + into-stream: 6.0.0 + is-stream: 2.0.1 + lodash: 4.17.21 + p-map: 4.0.0 + tus-js-client: 2.3.2 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + tree-dump@1.0.2(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -34906,6 +35139,16 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tus-js-client@2.3.2: + dependencies: + buffer-from: 1.1.2 + combine-errors: 3.0.3 + is-stream: 2.0.1 + js-base64: 2.6.4 + lodash.throttle: 4.1.1 + proper-lockfile: 2.0.1 + url-parse: 1.5.10 + tus-js-client@4.3.1: dependencies: buffer-from: 1.1.2