feat: veo3 videos
This commit is contained in:
parent
fcb3768493
commit
8eed4a4992
|
|
@ -30,6 +30,23 @@ export class BillingController {
|
|||
};
|
||||
}
|
||||
|
||||
@Post('/finish-trial')
|
||||
async finishTrial(@GetOrgFromRequest() org: Organization) {
|
||||
try {
|
||||
await this._stripeService.finishTrial(org.paymentId);
|
||||
} catch (err) {}
|
||||
return {
|
||||
finish: true,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/is-trial-finished')
|
||||
async isTrialFinished(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
finished: !org.isTrailing,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/subscribe')
|
||||
subscribe(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@ export class MediaController {
|
|||
return this._mediaService.videoFunction(identifier, functionName, body);
|
||||
}
|
||||
|
||||
@Get('/generate-video/:type/allowed')
|
||||
generateVideoAllowed(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('type') type: string
|
||||
) {
|
||||
return this._mediaService.generateVideoAllowed(org, type);
|
||||
}
|
||||
|
||||
@Post('/generate-video/:type')
|
||||
generateVideo(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
||||
export const FinishTrial: FC<{ close: () => void }> = (props) => {
|
||||
const [finished, setFinished] = useState(false);
|
||||
const fetch = useFetch();
|
||||
|
||||
const finishSubscription = useCallback(async () => {
|
||||
await fetch('/billing/finish-trial', {
|
||||
method: 'POST',
|
||||
});
|
||||
checkFinished();
|
||||
}, []);
|
||||
|
||||
const checkFinished = useCallback(async () => {
|
||||
const {finished} = await (await fetch('/billing/is-trial-finished')).json();
|
||||
if (!finished) {
|
||||
await timer(2000);
|
||||
return checkFinished();
|
||||
}
|
||||
|
||||
setFinished(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
finishSubscription();
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<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={'Finishing Trial'} />
|
||||
</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]">
|
||||
{!finished && <LoadingComponent height={150} width={150} />}
|
||||
{finished && (
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
You trial has been successfully finished and you have been charged.
|
||||
</div>
|
||||
<div className="flex gap-[10px] mt-[20px]">
|
||||
<Button className="flex-1" onClick={() => window.close()}>Close window</Button>
|
||||
<Button className="flex-1" onClick={() => props.close()}>Close dialog</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ import { FAQComponent } from '@gitroom/frontend/components/billing/faq.component
|
|||
import { useSWRConfig } from 'swr';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
|
|
@ -28,6 +28,7 @@ import { useTrack } from '@gitroom/react/helpers/use.track';
|
|||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { PurchaseCrypto } from '@gitroom/frontend/components/billing/purchase.crypto';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { FinishTrial } from '@gitroom/frontend/components/billing/finish.trial';
|
||||
export interface Tiers {
|
||||
month: Array<{
|
||||
name: 'Pro' | 'Standard';
|
||||
|
|
@ -224,6 +225,8 @@ export const MainBillingComponent: FC<{
|
|||
const tolt = useTolt();
|
||||
const track = useTrack();
|
||||
const t = useT();
|
||||
const queryParams = useSearchParams();
|
||||
const [finishTrial, setFinishTrial] = useState(!!queryParams.get('finishTrial'));
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | undefined>(
|
||||
sub
|
||||
|
|
@ -399,6 +402,10 @@ export const MainBillingComponent: FC<{
|
|||
<div>{t('yearly', 'YEARLY')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{finishTrial && (
|
||||
<FinishTrial close={() => setFinishTrial(false)} />
|
||||
)}
|
||||
<div className="flex gap-[16px] [@media(max-width:1024px)]:flex-col [@media(max-width:1024px)]:text-center">
|
||||
{Object.entries(pricing)
|
||||
.filter((f) => !isGeneral || f[0] !== 'FREE')
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import useSWR from 'swr';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { VideoWrapper } from '@gitroom/frontend/components/videos/video.render.component';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { VideoContextWrapper } from '@gitroom/frontend/components/videos/video.context.wrapper';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const Modal: FC<{
|
||||
close: () => void;
|
||||
|
|
@ -22,6 +25,7 @@ export const Modal: FC<{
|
|||
const setLocked = useLaunchStore((state) => state.setLocked);
|
||||
const form = useForm();
|
||||
const [position, setPosition] = useState('vertical');
|
||||
const toaster = useToaster();
|
||||
|
||||
const loadCredits = useCallback(async () => {
|
||||
return (
|
||||
|
|
@ -34,12 +38,16 @@ export const Modal: FC<{
|
|||
const { data, mutate } = useSWR('copilot-credits', loadCredits);
|
||||
|
||||
const generate = useCallback(async () => {
|
||||
await fetch(`/media/generate-video/${type.identifier}/allowed`);
|
||||
setLoading(true);
|
||||
close();
|
||||
setLocked(true);
|
||||
|
||||
console.log('lock');
|
||||
const customParams = form.getValues();
|
||||
if (!await form.trigger()) {
|
||||
toaster.show('Please fill all required fields', 'warning');
|
||||
return ;
|
||||
}
|
||||
try {
|
||||
const image = await fetch(`/media/generate-video/${type.identifier}`, {
|
||||
method: 'POST',
|
||||
|
|
@ -50,91 +58,90 @@ export const Modal: FC<{
|
|||
}),
|
||||
});
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<VideoContextWrapper.Provider value={{ value: value }}>
|
||||
<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>
|
||||
<VideoWrapper identifier={type.identifier} />
|
||||
<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 className="flex">
|
||||
<Button type="submit" className="flex-1">
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</VideoContextWrapper.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -148,6 +155,7 @@ export const AiVideo: FC<{
|
|||
const [type, setType] = useState<any | null>(null);
|
||||
const [modal, setModal] = useState(false);
|
||||
const fetch = useFetch();
|
||||
const { isTrailing } = useUser();
|
||||
|
||||
const loadVideoList = useCallback(async () => {
|
||||
return (await (await fetch('/media/video-options')).json()).filter(
|
||||
|
|
|
|||
|
|
@ -85,6 +85,21 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
}
|
||||
window.location.href = '/';
|
||||
}
|
||||
if (response.status === 406) {
|
||||
if (
|
||||
await deleteDialog(
|
||||
'You are currently on trial, in order to use the feature you must finish the trial',
|
||||
'Finish the trial, charge me now',
|
||||
'Trial',
|
||||
|
||||
)
|
||||
) {
|
||||
window.open('/billing?finishTrial=true', '_blank');
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.status === 402) {
|
||||
if (
|
||||
await deleteDialog(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const UserContext = createContext<
|
|||
isLifetime?: boolean;
|
||||
impersonate: boolean;
|
||||
allowTrial: boolean;
|
||||
isTrailing: boolean;
|
||||
})
|
||||
>(undefined);
|
||||
export const ContextWrapper: FC<{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { videoWrapper } from '@gitroom/frontend/components/videos/video.wrapper';
|
||||
import { FC, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useVideo } from '@gitroom/frontend/components/videos/video.context.wrapper';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
|
||||
export interface Voice {
|
||||
id: string;
|
||||
name: string;
|
||||
preview_url: string;
|
||||
}
|
||||
|
||||
const VEO3Settings: FC = () => {
|
||||
const { register, watch, setValue, formState } = useFormContext();
|
||||
const { value } = useVideo();
|
||||
const [videoValue, setVideoValue] = useState(value);
|
||||
|
||||
const media = register('media', {
|
||||
value: [],
|
||||
});
|
||||
|
||||
const mediaValue = watch('media');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Textarea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
{...register('prompt', {
|
||||
required: true,
|
||||
minLength: 5,
|
||||
value,
|
||||
})}
|
||||
error={formState?.errors?.prompt?.message}
|
||||
/>
|
||||
<div className="mb-[6px]">Images (max 3)</div>
|
||||
<MultiMediaComponent
|
||||
allData={[]}
|
||||
dummy={true}
|
||||
text="Images"
|
||||
description="Images"
|
||||
name="images"
|
||||
label="Media"
|
||||
value={mediaValue}
|
||||
onChange={(val) =>
|
||||
setValue(
|
||||
'images',
|
||||
val.target.value
|
||||
.filter((f) => f.path.indexOf('mp4') === -1)
|
||||
.slice(0, 3)
|
||||
)
|
||||
}
|
||||
error={formState?.errors?.media?.message}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VeoComponent = () => {
|
||||
return <VEO3Settings />;
|
||||
};
|
||||
|
||||
videoWrapper('veo3', VeoComponent);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const VideoContextWrapper = createContext({value: ''});
|
||||
export const useVideo = () => useContext(VideoContextWrapper);
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { createContext, FC, useCallback, useContext } from 'react';
|
||||
import { createContext, FC, useCallback, useContext, useEffect } from 'react';
|
||||
import './providers/image-text-slides.provider';
|
||||
import './providers/veo3.provider';
|
||||
import { videosList } from '@gitroom/frontend/components/videos/video.wrapper';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
|
||||
const VideoFunctionWrapper = createContext({
|
||||
identifier: '',
|
||||
|
|
@ -28,6 +30,14 @@ export const useVideoFunction = () => {
|
|||
};
|
||||
|
||||
export const VideoWrapper: FC<{ identifier: string }> = (props) => {
|
||||
const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton);
|
||||
useEffect(() => {
|
||||
setActivateExitButton(false);
|
||||
return () => {
|
||||
setActivateExitButton(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { identifier } = props;
|
||||
const Component = videosList.find(
|
||||
(v) => v.identifier === identifier
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
|
|
@ -7,7 +7,11 @@ 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';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
SubscriptionException,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
|
@ -61,6 +65,19 @@ export class MediaService {
|
|||
return this._videoManager.getAllVideos();
|
||||
}
|
||||
|
||||
async generateVideoAllowed(org: Organization, type: string) {
|
||||
const video = this._videoManager.getVideoByName(type);
|
||||
if (!video) {
|
||||
throw new Error(`Video type ${type} not found`);
|
||||
}
|
||||
|
||||
if (!video.trial && org.isTrailing) {
|
||||
throw new HttpException('This video is not available in trial mode', 406);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateVideo(org: Organization, body: VideoDto, type: string) {
|
||||
const totalCredits = await this._subscriptionService.checkCredits(
|
||||
org,
|
||||
|
|
@ -78,6 +95,10 @@ export class MediaService {
|
|||
throw new Error(`Video type ${type} not found`);
|
||||
}
|
||||
|
||||
if (!video.trial && org.isTrailing) {
|
||||
throw new HttpException('This video is not available in trial mode', 406);
|
||||
}
|
||||
|
||||
const loadedData = await video.instance.process(
|
||||
body.prompt,
|
||||
body.output,
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export class StripeService {
|
|||
}
|
||||
|
||||
return this._subscriptionService.createOrUpdateSubscription(
|
||||
event.data.object.status === 'trialing',
|
||||
event.data.object.status !== 'active',
|
||||
uniqueId,
|
||||
event.data.object.customer as string,
|
||||
pricing[billing].channel!,
|
||||
|
|
@ -169,13 +169,13 @@ export class StripeService {
|
|||
}
|
||||
|
||||
return this._subscriptionService.createOrUpdateSubscription(
|
||||
event.data.object.status === 'trialing',
|
||||
event.data.object.status !== 'active',
|
||||
uniqueId,
|
||||
event.data.object.customer as string,
|
||||
pricing[billing].channel!,
|
||||
billing,
|
||||
period,
|
||||
event.data.object.cancel_at,
|
||||
event.data.object.cancel_at
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -463,6 +463,18 @@ export class StripeService {
|
|||
return accountLink.url;
|
||||
}
|
||||
|
||||
async finishTrial(paymentId: string) {
|
||||
const list = (
|
||||
await stripe.subscriptions.list({
|
||||
customer: paymentId,
|
||||
})
|
||||
).data.filter((f) => f.status === 'trialing');
|
||||
|
||||
return stripe.subscriptions.update(list[0].id, {
|
||||
trial_end: 'now',
|
||||
});
|
||||
}
|
||||
|
||||
async checkSubscription(organizationId: string, subscriptionId: string) {
|
||||
const orgValue = await this._subscriptionService.checkSubscription(
|
||||
organizationId,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,10 @@
|
|||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import {
|
||||
Prompt,
|
||||
URL,
|
||||
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';
|
||||
|
||||
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 || 'just empty text',
|
||||
authSecret: process.env.TRANSLOADIT_SECRET || 'just empty text',
|
||||
});
|
||||
|
||||
async function getAudioDuration(buffer: Buffer): Promise<number> {
|
||||
const metadata = await parseBuffer(buffer, 'audio/mpeg');
|
||||
return metadata.format.duration || 0;
|
||||
}
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
@Video({
|
||||
identifier: 'veo3',
|
||||
|
|
@ -32,213 +12,71 @@ async function getAudioDuration(buffer: Buffer): Promise<number> {
|
|||
description: 'Generate videos with the most advanced video model.',
|
||||
placement: 'text-to-image',
|
||||
trial: false,
|
||||
available:
|
||||
!!process.env.TRANSLOADIT_AUTH &&
|
||||
!!process.env.TRANSLOADIT_SECRET &&
|
||||
!!process.env.OPENAI_API_KEY &&
|
||||
!!process.env.KIEAI_API_KEY,
|
||||
available: !!process.env.KIEAI_API_KEY,
|
||||
})
|
||||
export class Veo3 extends VideoAbstract {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _openaiService: OpenaiService,
|
||||
private _falService: FalService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(
|
||||
prompt: Prompt[],
|
||||
output: 'vertical' | 'horizontal',
|
||||
customParams: { voice: string }
|
||||
customParams: { prompt: string; images: { id: string; path: 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._falService.generateImageFromText(
|
||||
'ideogram/v2',
|
||||
current.imagePrompt,
|
||||
output === 'vertical'
|
||||
),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
all.push(
|
||||
new Promise(async (res) => {
|
||||
const buffer = Buffer.from(
|
||||
await (
|
||||
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()
|
||||
);
|
||||
|
||||
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<any>[])
|
||||
);
|
||||
|
||||
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' }
|
||||
);
|
||||
|
||||
const { results } = await transloadit.createAssembly({
|
||||
uploads: {
|
||||
'subtitles.srt': srt,
|
||||
},
|
||||
waitForCompletion: true,
|
||||
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: 8,
|
||||
subtitles_type: 'burned',
|
||||
},
|
||||
},
|
||||
console.log({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.KIEAI_API_KEY}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: customParams.prompt,
|
||||
imageUrls: customParams?.images?.map((p) => p.path) || [],
|
||||
model: 'veo3_fast',
|
||||
aspectRatio: output === 'horizontal' ? '16:9' : '9:16',
|
||||
}),
|
||||
});
|
||||
|
||||
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 || '',
|
||||
},
|
||||
}
|
||||
)
|
||||
const value = await (
|
||||
await fetch('https://api.kie.ai/api/v1/veo/generate', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.KIEAI_API_KEY}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: customParams.prompt,
|
||||
imageUrls: customParams?.images?.map((p) => p.path) || [],
|
||||
model: 'veo3_fast',
|
||||
aspectRatio: output === 'horizontal' ? '16:9' : '9:16',
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
voices: voices.map((voice: any) => ({
|
||||
id: voice.voice_id,
|
||||
name: voice.name,
|
||||
preview_url: voice.preview_url,
|
||||
})),
|
||||
};
|
||||
if (value.code !== 200 && value.code !== 201) {
|
||||
throw new Error(`Failed to generate video`);
|
||||
}
|
||||
|
||||
const taskId = value.data.taskId;
|
||||
let videoUrl = [];
|
||||
while (videoUrl.length === 0) {
|
||||
console.log('waiting for video to be ready');
|
||||
const data = await (
|
||||
await fetch(
|
||||
'https://api.kie.ai/api/v1/veo/record-info?taskId=' + taskId,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.KIEAI_API_KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
if (data.code !== 200 && data.code !== 400) {
|
||||
throw new Error(`Failed to get video info`);
|
||||
}
|
||||
|
||||
videoUrl = data?.data?.response?.resultUrls || [];
|
||||
await timer(10000);
|
||||
}
|
||||
|
||||
return videoUrl[0];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import i18next from '@gitroom/react/translation/i18next';
|
|||
export const deleteDialog = async (
|
||||
message: string,
|
||||
confirmButton?: string,
|
||||
title?: string
|
||||
title?: string,
|
||||
cancelButton?: string
|
||||
) => {
|
||||
const fire = await Swal.fire({
|
||||
title: title || i18next.t('are_you_sure', 'Are you sure?'),
|
||||
|
|
@ -13,7 +14,7 @@ export const deleteDialog = async (
|
|||
showCancelButton: true,
|
||||
confirmButtonText:
|
||||
confirmButton || i18next.t('yes_delete_it', 'Yes, delete it!'),
|
||||
cancelButtonText: i18next.t('no_cancel', 'No, cancel!'),
|
||||
cancelButtonText: cancelButton || i18next.t('no_cancel', 'No, cancel!'),
|
||||
});
|
||||
return fire.isConfirmed;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue