feat: videos
This commit is contained in:
parent
05720ec59e
commit
0f645846fc
|
|
@ -12,14 +12,9 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
|
|||
import { Organization } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';
|
||||
import dayjs from 'dayjs';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Autopost')
|
||||
@Controller('/autopost')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Logger, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
||||
import { Logger, Controller, Get, Post, Req, Res, Query } from '@nestjs/common';
|
||||
import {
|
||||
CopilotRuntime,
|
||||
OpenAIAdapter,
|
||||
|
|
@ -39,7 +39,10 @@ export class CopilotController {
|
|||
}
|
||||
|
||||
@Get('/credits')
|
||||
calculateCredits(@GetOrgFromRequest() organization: Organization) {
|
||||
return this._subscriptionService.checkCredits(organization);
|
||||
calculateCredits(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Query('type') type: 'ai_images' | 'ai_videos',
|
||||
) {
|
||||
return this._subscriptionService.checkCredits(organization, type || 'ai_images');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
|
|||
import { Organization, User } from '@prisma/client';
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
|
@ -37,6 +33,7 @@ import {
|
|||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
|
|||
|
|
@ -171,12 +171,21 @@ export class MediaController {
|
|||
return this._mediaService.getVideoOptions();
|
||||
}
|
||||
|
||||
@Post('/video/:identifier/:function')
|
||||
videoFunction(
|
||||
@Param('identifier') identifier: string,
|
||||
@Param('function') functionName: string,
|
||||
@Body('params') body: any
|
||||
) {
|
||||
return this._mediaService.videoFunction(identifier, functionName, body);
|
||||
}
|
||||
|
||||
@Post('/generate-video/:type')
|
||||
generateVideo(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: VideoDto,
|
||||
@Param('type') type: string,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: VideoDto,
|
||||
@Param('type') type: string
|
||||
) {
|
||||
return this._mediaService.generateVideo(org.id, body, type);
|
||||
return this._mediaService.generateVideo(org, body, type);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,9 @@ import {
|
|||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
|
|
@ -29,6 +24,7 @@ import { Response } from 'express';
|
|||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
|
||||
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Posts')
|
||||
@Controller('/posts')
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
|
|||
import { Organization } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import {
|
||||
UpdateSetsDto,
|
||||
SetsDto,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
|
|||
import { Organization } from '@prisma/client';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Settings')
|
||||
@Controller('/settings')
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ import { Response, Request } from 'express';
|
|||
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
|
@ -32,6 +28,7 @@ import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
|||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller('/user')
|
||||
|
|
|
|||
|
|
@ -13,14 +13,11 @@ import { Organization } from '@prisma/client';
|
|||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import {
|
||||
UpdateDto,
|
||||
WebhooksDto,
|
||||
} from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Webhooks')
|
||||
@Controller('/webhooks')
|
||||
|
|
|
|||
|
|
@ -15,16 +15,12 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
|
|||
import { Organization } from '@prisma/client';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Public API')
|
||||
@Controller('/public/v1')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
|
||||
export enum Sections {
|
||||
CHANNEL = 'channel',
|
||||
POSTS_PER_MONTH = 'posts_per_month',
|
||||
VIDEOS_PER_MONTH = 'videos_per_month',
|
||||
TEAM_MEMBERS = 'team_members',
|
||||
COMMUNITY_FEATURES = 'community_features',
|
||||
FEATURED_BY_GITROOM = 'featured_by_gitroom',
|
||||
AI = 'ai',
|
||||
IMPORT_FROM_CHANNELS = 'import_from_channels',
|
||||
ADMIN = 'admin',
|
||||
WEBHOOKS = 'webhooks',
|
||||
}
|
||||
|
||||
export enum AuthorizationActions {
|
||||
Create = 'create',
|
||||
Read = 'read',
|
||||
Update = 'update',
|
||||
Delete = 'delete',
|
||||
}
|
||||
|
||||
export class SubscriptionException extends HttpException {
|
||||
constructor(message: { section: Sections; action: AuthorizationActions }) {
|
||||
super(message, HttpStatus.PAYMENT_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import { SetMetadata } from '@nestjs/common';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { AuthorizationActions, Sections } from './permission.exception.class';
|
||||
|
||||
export const CHECK_POLICIES_KEY = 'check_policy';
|
||||
export type AbilityPolicy = [AuthorizationActions, Sections];
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
CHECK_POLICIES_KEY,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { SubscriptionException } from '@gitroom/backend/services/auth/permissions/subscription.exception';
|
||||
import { Request } from 'express';
|
||||
import { SubscriptionException } from './permission.exception.class';
|
||||
|
||||
@Injectable()
|
||||
export class PoliciesGuard implements CanActivate {
|
||||
|
|
|
|||
|
|
@ -6,25 +6,7 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
|
||||
export enum Sections {
|
||||
CHANNEL = 'channel',
|
||||
POSTS_PER_MONTH = 'posts_per_month',
|
||||
TEAM_MEMBERS = 'team_members',
|
||||
COMMUNITY_FEATURES = 'community_features',
|
||||
FEATURED_BY_GITROOM = 'featured_by_gitroom',
|
||||
AI = 'ai',
|
||||
IMPORT_FROM_CHANNELS = 'import_from_channels',
|
||||
ADMIN = 'admin',
|
||||
WEBHOOKS = 'webhooks',
|
||||
}
|
||||
|
||||
export enum AuthorizationActions {
|
||||
Create = 'create',
|
||||
Read = 'read',
|
||||
Update = 'update',
|
||||
Delete = 'delete',
|
||||
}
|
||||
import { AuthorizationActions, Sections } from './permission.exception.class';
|
||||
|
||||
export type AppAbility = Ability<[AuthorizationActions, Sections]>;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,8 @@ import {
|
|||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
|
||||
export class SubscriptionException extends HttpException {
|
||||
constructor(message: { section: Sections; action: AuthorizationActions }) {
|
||||
super(message, HttpStatus.PAYMENT_REQUIRED);
|
||||
}
|
||||
}
|
||||
import { AuthorizationActions, Sections, SubscriptionException } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@Catch(SubscriptionException)
|
||||
export class SubscriptionExceptionFilter implements ExceptionFilter {
|
||||
|
|
@ -55,5 +45,10 @@ const getErrorMessage = (error: {
|
|||
default:
|
||||
return 'You have reached the maximum number of webhooks for your subscription. Please upgrade your subscription to add more webhooks.';
|
||||
}
|
||||
case Sections.VIDEOS_PER_MONTH:
|
||||
switch (error.action) {
|
||||
default:
|
||||
return 'You have reached the maximum number of generated videos for your subscription. Please upgrade your subscription to generate more videos.';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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';
|
||||
import { VideoWrapper } from '@gitroom/frontend/components/videos/video.render.component';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export const Modal: FC<{
|
||||
close: () => void;
|
||||
|
|
@ -19,70 +21,122 @@ export const Modal: FC<{
|
|||
}> = (props) => {
|
||||
const { type, value, onChange, close, setLoading } = props;
|
||||
const fetch = useFetch();
|
||||
const setLocked = useLaunchStore(state => state.setLocked)
|
||||
const setLocked = useLaunchStore((state) => state.setLocked);
|
||||
const form = useForm();
|
||||
const [position, setPosition] = useState('vertical');
|
||||
|
||||
const generate = useCallback(
|
||||
(output: string) => async () => {
|
||||
setLoading(true);
|
||||
close();
|
||||
setLocked(true);
|
||||
const loadCredits = useCallback(async () => {
|
||||
return (
|
||||
await fetch(`/copilot/credits?type=ai_videos`, {
|
||||
method: 'GET',
|
||||
})
|
||||
).json();
|
||||
}, []);
|
||||
|
||||
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();
|
||||
const { data, mutate } = useSWR('copilot-credits', loadCredits);
|
||||
|
||||
setLocked(false);
|
||||
setLoading(false);
|
||||
onChange(image);
|
||||
},
|
||||
[type, value]
|
||||
);
|
||||
const generate = useCallback(async () => {
|
||||
setLoading(true);
|
||||
close();
|
||||
setLocked(true);
|
||||
|
||||
console.log('lock');
|
||||
const customParams = form.getValues();
|
||||
try {
|
||||
const image = await fetch(`/media/generate-video/${type.identifier}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: [{ type: 'prompt', value }],
|
||||
output: position,
|
||||
customParams,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(image);
|
||||
|
||||
if (image.status == 200 || image.status == 201) {
|
||||
onChange(await image.json());
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
console.log('remove lock');
|
||||
setLocked(false);
|
||||
setLoading(false);
|
||||
}, [type, value, position]);
|
||||
|
||||
return (
|
||||
<div className="text-textColor fixed start-0 top-0 bg-primary/80 z-[300] w-full h-full p-[60px] animate-fade justify-center flex bg-black/50">
|
||||
<div className="flex flex-col w-[500px] h-[250px] bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={'Video Type'} />
|
||||
<form
|
||||
onSubmit={form.handleSubmit(generate)}
|
||||
className="flex flex-col gap-[10px]"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="text-textColor fixed start-0 top-0 bg-primary/80 z-[300] w-full h-full p-[60px] animate-fade justify-center flex bg-black/50">
|
||||
<div>
|
||||
<div className="flex gap-[10px] flex-col w-[500px] h-auto bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={'Video Type'}>
|
||||
<div className="mr-[25px]">
|
||||
{data?.credits || 0} credits left
|
||||
</div>
|
||||
</TopTitle>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.close}
|
||||
className="outline-none absolute end-[10px] top-[10px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative h-[400px]">
|
||||
<div className="absolute left-0 top-0 w-full h-full overflow-hidden overflow-y-auto">
|
||||
<div className="mt-[10px] flex w-full justify-center items-center gap-[10px]">
|
||||
<div className="flex-1 flex">
|
||||
<Button
|
||||
className="!flex-1"
|
||||
onClick={() => setPosition('vertical')}
|
||||
secondary={position === 'horizontal'}
|
||||
>
|
||||
Vertical (Stories, Reels)
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex mt-[10px]">
|
||||
<Button
|
||||
className="!flex-1"
|
||||
onClick={() => setPosition('horizontal')}
|
||||
secondary={position === 'vertical'}
|
||||
>
|
||||
Horizontal (Normal Post)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<VideoWrapper identifier={type.identifier} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button type="submit" className="flex-1">
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.close}
|
||||
className="outline-none absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[10px] flex h-full w-full justify-center items-center gap-[10px]">
|
||||
<Button onClick={generate('vertical')}>
|
||||
Vertical (Stories, Reels)
|
||||
</Button>
|
||||
<Button onClick={generate('horizontal')}>
|
||||
Horizontal (Portrait)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -120,7 +174,7 @@ export const AiVideo: FC<{
|
|||
[value, onChange]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || data?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,8 +96,9 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
)
|
||||
) {
|
||||
window.open('/billing', '_blank');
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
import { videoWrapper } from '@gitroom/frontend/components/videos/video.wrapper';
|
||||
import { FC, useCallback, useRef, useState, useEffect } from 'react';
|
||||
import { useVideoFunction } from '@gitroom/frontend/components/videos/video.render.component';
|
||||
import useSWR from 'swr';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface Voices {
|
||||
voices: Voice[];
|
||||
}
|
||||
|
||||
export interface Voice {
|
||||
id: string;
|
||||
name: string;
|
||||
preview_url: string;
|
||||
}
|
||||
|
||||
const VoiceSelector: FC = () => {
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const videoFunction = useVideoFunction();
|
||||
const [currentlyPlaying, setCurrentlyPlaying] = useState<string | null>(null);
|
||||
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const loadVideos = useCallback(() => {
|
||||
return videoFunction('loadVoices', {});
|
||||
}, []);
|
||||
|
||||
const selectedVoice = watch('voice');
|
||||
const { isLoading, data } = useSWR<Voices>('load-voices', loadVideos);
|
||||
|
||||
// Auto-select first voice when data loads
|
||||
useEffect(() => {
|
||||
if (data?.voices?.length && !selectedVoice) {
|
||||
setValue('voice', data.voices[0].id);
|
||||
}
|
||||
}, [data, selectedVoice, setValue]);
|
||||
|
||||
const playVoice = useCallback(
|
||||
async (voiceId: string, previewUrl: string) => {
|
||||
try {
|
||||
setLoadingVoice(voiceId);
|
||||
|
||||
// Stop current audio if playing
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
}
|
||||
|
||||
// If clicking the same voice that's playing, stop it
|
||||
if (currentlyPlaying === voiceId) {
|
||||
setCurrentlyPlaying(null);
|
||||
setLoadingVoice(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new audio element
|
||||
const audio = new Audio(previewUrl);
|
||||
audioRef.current = audio;
|
||||
|
||||
audio.addEventListener('loadeddata', () => {
|
||||
setLoadingVoice(null);
|
||||
setCurrentlyPlaying(voiceId);
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
setCurrentlyPlaying(null);
|
||||
audioRef.current = null;
|
||||
});
|
||||
|
||||
audio.addEventListener('error', () => {
|
||||
setLoadingVoice(null);
|
||||
setCurrentlyPlaying(null);
|
||||
audioRef.current = null;
|
||||
});
|
||||
|
||||
await audio.play();
|
||||
} catch (error) {
|
||||
console.error('Error playing voice:', error);
|
||||
setLoadingVoice(null);
|
||||
setCurrentlyPlaying(null);
|
||||
}
|
||||
},
|
||||
[currentlyPlaying]
|
||||
);
|
||||
|
||||
const selectVoice = useCallback(
|
||||
(voiceId: string) => {
|
||||
setValue('voice', voiceId);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
if (isLoading || !data?.voices?.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="text-sm text-gray-500">Loading voices...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-textColor mb-4">
|
||||
Select a Voice
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{data.voices.map((voice) => (
|
||||
<div
|
||||
key={voice.id}
|
||||
className={clsx(
|
||||
'flex items-center justify-between p-3 rounded-lg border transition-colors cursor-pointer',
|
||||
selectedVoice === voice.id
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-tableBorder bg-sixth hover:bg-seventh'
|
||||
)}
|
||||
onClick={() => selectVoice(voice.id)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<input
|
||||
{...register('voice')}
|
||||
type="radio"
|
||||
value={voice.id}
|
||||
className="w-4 h-4 text-primary border-gray-300 focus:ring-primary"
|
||||
checked={selectedVoice === voice.id}
|
||||
onChange={() => selectVoice(voice.id)}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-textColor">
|
||||
{voice.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'px-3 py-1 text-xs',
|
||||
loadingVoice === voice.id && 'opacity-50 cursor-not-allowed',
|
||||
currentlyPlaying === voice.id && 'bg-red-500 hover:bg-red-600'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playVoice(voice.id, voice.preview_url);
|
||||
}}
|
||||
disabled={loadingVoice === voice.id}
|
||||
>
|
||||
{loadingVoice === voice.id
|
||||
? '...'
|
||||
: currentlyPlaying === voice.id
|
||||
? '⏹ Stop'
|
||||
: '▶ Play'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageSlidesComponent = () => {
|
||||
return <VoiceSelector />;
|
||||
};
|
||||
|
||||
videoWrapper('image-text-slides', ImageSlidesComponent);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { createContext, FC, useCallback, useContext } from 'react';
|
||||
import './providers/image-text-slides.provider';
|
||||
import { videosList } from '@gitroom/frontend/components/videos/video.wrapper';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
||||
const VideoFunctionWrapper = createContext({
|
||||
identifier: '',
|
||||
});
|
||||
|
||||
export const useVideoFunction = () => {
|
||||
const { identifier } = useContext(VideoFunctionWrapper);
|
||||
const fetch = useFetch();
|
||||
|
||||
return useCallback(
|
||||
async (funcName: string, params: any) => {
|
||||
return (
|
||||
await fetch(`/media/video/${identifier}/${funcName}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ params }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
},
|
||||
[identifier]
|
||||
);
|
||||
};
|
||||
|
||||
export const VideoWrapper: FC<{ identifier: string }> = (props) => {
|
||||
const { identifier } = props;
|
||||
const Component = videosList.find(
|
||||
(v) => v.identifier === identifier
|
||||
)?.Component;
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<VideoFunctionWrapper.Provider value={{ identifier }}>
|
||||
<Component />
|
||||
</VideoFunctionWrapper.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
export const videosList: {identifier: string, Component: FC}[] = [];
|
||||
|
||||
export const videoWrapper = (identifier: string, Component: any): null => {
|
||||
if (videosList.map(p => p.identifier).includes(identifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
videosList.push({
|
||||
identifier,
|
||||
Component
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/s
|
|||
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';
|
||||
import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -80,6 +81,7 @@ import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
|
|||
IntegrationManager,
|
||||
ExtractContentService,
|
||||
OpenaiService,
|
||||
FalService,
|
||||
EmailService,
|
||||
TrackService,
|
||||
ShortLinkService,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/sa
|
|||
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';
|
||||
import { AuthorizationActions, Sections, SubscriptionException } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
|
@ -60,15 +61,49 @@ export class MediaService {
|
|||
return this._videoManager.getAllVideos();
|
||||
}
|
||||
|
||||
async generateVideo(org: string, body: VideoDto, type: string) {
|
||||
async generateVideo(org: Organization, body: VideoDto, type: string) {
|
||||
const totalCredits = await this._subscriptionService.checkCredits(
|
||||
org,
|
||||
'ai_videos'
|
||||
);
|
||||
if (totalCredits.credits <= 0) {
|
||||
throw new SubscriptionException({
|
||||
action: AuthorizationActions.Create,
|
||||
section: Sections.VIDEOS_PER_MONTH,
|
||||
});
|
||||
}
|
||||
|
||||
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 loadedData = await video.instance.process(
|
||||
body.prompt,
|
||||
body.output,
|
||||
body.customParams
|
||||
);
|
||||
|
||||
// const file = await this.storage.uploadSimple(loadedData);
|
||||
// return this.saveFile(org, file.split('/').pop(), file);
|
||||
const file = await this.storage.uploadSimple(loadedData);
|
||||
const save = await this.saveFile(org.id, file.split('/').pop(), file);
|
||||
|
||||
await this._subscriptionService.useCredit(org, 'ai_videos');
|
||||
|
||||
return save;
|
||||
}
|
||||
|
||||
async videoFunction(identifier: string, functionName: string, body: any) {
|
||||
const video = this._videoManager.getVideoByName(identifier);
|
||||
if (!video) {
|
||||
throw new Error(`Video with identifier ${identifier} not found`);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const functionToCall = video.instance[functionName];
|
||||
if (typeof functionToCall !== 'function') {
|
||||
throw new Error(`Function ${functionName} not found on video instance`);
|
||||
}
|
||||
|
||||
return functionToCall(body);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,9 +177,9 @@ model ItemUser {
|
|||
userId String
|
||||
key String
|
||||
|
||||
@@unique([userId, key])
|
||||
@@index([userId])
|
||||
@@index([key])
|
||||
@@unique([userId, key])
|
||||
}
|
||||
|
||||
model Star {
|
||||
|
|
@ -263,6 +263,7 @@ model Credits {
|
|||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
organizationId String
|
||||
credits Int
|
||||
type String @default("ai_images")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
|
@ -332,11 +333,11 @@ model Integration {
|
|||
additionalSettings String? @default("[]")
|
||||
webhooks IntegrationsWebhooks[]
|
||||
|
||||
@@unique([organizationId, internalId])
|
||||
@@index([rootInternalId])
|
||||
@@index([updatedAt])
|
||||
@@index([deletedAt])
|
||||
@@index([customerId])
|
||||
@@unique([organizationId, internalId])
|
||||
}
|
||||
|
||||
model Signatures {
|
||||
|
|
@ -566,8 +567,8 @@ model IntegrationsWebhooks {
|
|||
webhookId String
|
||||
webhook Webhooks @relation(fields: [webhookId], references: [id])
|
||||
|
||||
@@unique([integrationId, webhookId])
|
||||
@@id([integrationId, webhookId])
|
||||
@@unique([integrationId, webhookId])
|
||||
@@index([integrationId])
|
||||
@@index([webhookId])
|
||||
}
|
||||
|
|
@ -632,9 +633,9 @@ model ThirdParty {
|
|||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@unique([organizationId, internalId])
|
||||
@@index([organizationId])
|
||||
@@index([deletedAt])
|
||||
@@unique([organizationId, internalId])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
|
|
|
|||
|
|
@ -201,11 +201,12 @@ export class SubscriptionRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getCreditsFrom(organizationId: string, from: dayjs.Dayjs) {
|
||||
async getCreditsFrom(organizationId: string, from: dayjs.Dayjs, type = 'ai_images') {
|
||||
const load = await this._credits.model.credits.groupBy({
|
||||
by: ['organizationId'],
|
||||
where: {
|
||||
organizationId,
|
||||
type,
|
||||
createdAt: {
|
||||
gte: from.toDate(),
|
||||
},
|
||||
|
|
@ -218,11 +219,12 @@ export class SubscriptionRepository {
|
|||
return load?.[0]?._sum?.credits || 0;
|
||||
}
|
||||
|
||||
useCredit(org: Organization) {
|
||||
useCredit(org: Organization, type = 'ai_images') {
|
||||
return this._credits.model.credits.create({
|
||||
data: {
|
||||
organizationId: org.id,
|
||||
credits: 1,
|
||||
type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export class SubscriptionService {
|
|||
);
|
||||
}
|
||||
|
||||
useCredit(organization: Organization) {
|
||||
return this._subscriptionRepository.useCredit(organization);
|
||||
useCredit(organization: Organization, type = 'ai_images') {
|
||||
return this._subscriptionRepository.useCredit(organization, type);
|
||||
}
|
||||
|
||||
getCode(code: string) {
|
||||
|
|
@ -189,7 +189,7 @@ export class SubscriptionService {
|
|||
return this._subscriptionRepository.getSubscription(organizationId);
|
||||
}
|
||||
|
||||
async checkCredits(organization: Organization) {
|
||||
async checkCredits(organization: Organization, checkType = 'ai_images') {
|
||||
// @ts-ignore
|
||||
const type = organization?.subscription?.subscriptionTier || 'FREE';
|
||||
|
||||
|
|
@ -204,11 +204,12 @@ export class SubscriptionService {
|
|||
}
|
||||
|
||||
const checkFromMonth = date.subtract(1, 'month');
|
||||
const imageGenerationCount = pricing[type].image_generation_count;
|
||||
const imageGenerationCount = checkType === 'ai_images' ? pricing[type].image_generation_count : pricing[type].generate_videos
|
||||
|
||||
const totalUse = await this._subscriptionRepository.getCreditsFrom(
|
||||
organization.id,
|
||||
checkFromMonth
|
||||
checkFromMonth,
|
||||
checkType
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -17,4 +17,6 @@ export class VideoDto {
|
|||
|
||||
@IsIn(['vertical', 'horizontal'])
|
||||
output: 'vertical' | 'horizontal';
|
||||
|
||||
customParams: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import pLimit from 'p-limit';
|
||||
const limit = pLimit(10);
|
||||
|
||||
@Injectable()
|
||||
export class FalService {
|
||||
async generateImageFromText(
|
||||
model: string,
|
||||
text: string,
|
||||
isVertical: boolean = false
|
||||
): Promise<string> {
|
||||
const { images, video, ...all } = await (
|
||||
await limit(() =>
|
||||
fetch(`https://fal.run/fal-ai/${model}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Key ${process.env.FAL_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: text,
|
||||
aspect_ratio: isVertical ? '9:16' : '16:9',
|
||||
resolution: '720p',
|
||||
num_images: 1,
|
||||
output_format: 'jpeg',
|
||||
expand_prompt: true,
|
||||
}),
|
||||
})
|
||||
)
|
||||
).json();
|
||||
|
||||
console.log(all, video, images);
|
||||
|
||||
if (video) {
|
||||
return video.url;
|
||||
}
|
||||
|
||||
return images[0].url as string;
|
||||
}
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ 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`;
|
||||
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 and also have a back dark gradient on top, image prompt should not contain text in the picture, generate between 3-5 slides maximum`;
|
||||
return (
|
||||
(
|
||||
await openai.beta.chat.completions.parse({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import {
|
||||
Prompt,
|
||||
URL,
|
||||
Video,
|
||||
VideoAbstract,
|
||||
} from '@gitroom/nestjs-libraries/videos/video.interface';
|
||||
|
|
@ -11,6 +12,10 @@ import { Readable } from 'stream';
|
|||
import { parseBuffer } from 'music-metadata';
|
||||
import { stringifySync } from 'subtitle';
|
||||
|
||||
import pLimit from 'p-limit';
|
||||
import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service';
|
||||
const limit = pLimit(2);
|
||||
|
||||
const transloadit = new Transloadit({
|
||||
authKey: process.env.TRANSLOADIT_AUTH,
|
||||
authSecret: process.env.TRANSLOADIT_SECRET,
|
||||
|
|
@ -26,30 +31,40 @@ async function getAudioDuration(buffer: Buffer): Promise<number> {
|
|||
title: 'Image Text Slides',
|
||||
description: 'Generate videos slides from images and text',
|
||||
placement: 'text-to-image',
|
||||
available:
|
||||
!!process.env.ELEVENSLABS_API_KEY &&
|
||||
!!process.env.TRANSLOADIT_AUTH &&
|
||||
!!process.env.TRANSLOADIT_SECRET &&
|
||||
!!process.env.OPENAI_API_KEY &&
|
||||
!!process.env.FAL_KEY,
|
||||
})
|
||||
export class ImagesSlides extends VideoAbstract {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(private _openaiService: OpenaiService) {
|
||||
constructor(
|
||||
private _openaiService: OpenaiService,
|
||||
private _falService: FalService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(
|
||||
prompt: Prompt[],
|
||||
output: 'vertical' | 'horizontal'
|
||||
): Promise<string> {
|
||||
output: 'vertical' | 'horizontal',
|
||||
customParams: { voice: string }
|
||||
): Promise<URL> {
|
||||
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,
|
||||
url: await this._falService.generateImageFromText(
|
||||
'ideogram/v2',
|
||||
current.imagePrompt,
|
||||
output === 'vertical'
|
||||
),
|
||||
});
|
||||
|
|
@ -60,22 +75,21 @@ export class ImagesSlides extends VideoAbstract {
|
|||
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,
|
||||
await limit(() =>
|
||||
fetch(
|
||||
`https://api.elevenlabs.io/v1/text-to-speech/${customParams.voice}?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,
|
||||
model_id: 'eleven_multilingual_v2'
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
).arrayBuffer()
|
||||
);
|
||||
|
|
@ -131,12 +145,12 @@ export class ImagesSlides extends VideoAbstract {
|
|||
})),
|
||||
{ format: 'SRT' }
|
||||
);
|
||||
console.log(srt);
|
||||
|
||||
await transloadit.createAssembly({
|
||||
const { results } = await transloadit.createAssembly({
|
||||
uploads: {
|
||||
'subtitles.srt': srt,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
params: {
|
||||
steps: {
|
||||
...split.reduce((all, current, index) => {
|
||||
|
|
@ -194,7 +208,7 @@ export class ImagesSlides extends VideoAbstract {
|
|||
},
|
||||
],
|
||||
},
|
||||
position: 'center',
|
||||
position: 'top',
|
||||
font_size: 10,
|
||||
subtitles_type: 'burned',
|
||||
},
|
||||
|
|
@ -202,6 +216,29 @@ export class ImagesSlides extends VideoAbstract {
|
|||
},
|
||||
});
|
||||
|
||||
return '';
|
||||
return results.subtitled[0].url;
|
||||
}
|
||||
|
||||
async loadVoices(data: any) {
|
||||
const { voices } = await (
|
||||
await fetch(
|
||||
'https://api.elevenlabs.io/v2/voices?page_size=40&category=premade',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'xi-api-key': process.env.ELEVENSLABS_API_KEY || '',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
voices: voices.map((voice: any) => ({
|
||||
id: voice.voice_id,
|
||||
name: voice.name,
|
||||
preview_url: voice.preview_url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@ export interface Prompt {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export type URL = string;
|
||||
|
||||
export abstract class VideoAbstract {
|
||||
abstract process(
|
||||
prompt: Prompt[],
|
||||
output: 'vertical' | 'horizontal'
|
||||
): Promise<string>;
|
||||
output: 'vertical' | 'horizontal',
|
||||
customParams?: any
|
||||
): Promise<URL>;
|
||||
}
|
||||
|
||||
export interface VideoParams {
|
||||
|
|
@ -17,6 +20,7 @@ export interface VideoParams {
|
|||
title: string;
|
||||
description: string;
|
||||
placement: 'text-to-image' | 'image-to-video' | 'video-to-video';
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export function Video(params: VideoParams) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export class VideoManager {
|
|||
constructor(private _moduleRef: ModuleRef) {}
|
||||
|
||||
getAllVideos(): any[] {
|
||||
return (Reflect.getMetadata('video', VideoAbstract) || []).map(
|
||||
return (Reflect.getMetadata('video', VideoAbstract) || []).filter((f: any) => f.available).map(
|
||||
(p: any) => ({
|
||||
identifier: p.identifier,
|
||||
title: p.title,
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@
|
|||
"nostr-tools": "^2.10.4",
|
||||
"nx": "19.7.2",
|
||||
"openai": "^4.47.1",
|
||||
"p-limit": "^6.2.0",
|
||||
"polotno": "^2.10.5",
|
||||
"posthog-js": "^1.178.0",
|
||||
"react": "18.3.1",
|
||||
|
|
|
|||
9874
pnpm-lock.yaml
9874
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue