Merge branch 'feature/mvp' into olivicmic-fy-engine-compositor

This commit is contained in:
Victor committed 2024-01-31 19:09:07 -08:00
commit dee91dbd7e
102 files changed
+3674 -2066

No files matched your search

+2
View File
@@ -12,6 +12,7 @@
/contribute* /index.html 200
/explore* /index.html 200
/face-animator* /index.html 200
/fbx-to-gltf* /index.html 200
/firehose* /index.html 200
/guide* /index.html 200
/jobs* /index.html 200
@@ -47,6 +48,7 @@
/voice-designer* /index.html 200
/w2l* /index.html 200
/weight* /index.html 200
/text-to-image* /index.html 200
# Test paths
/dev-tts* /index.html 200
+7 -3
View File
@@ -492,10 +492,14 @@ class ApiConfig {
}
// =============== Motion Capture / mocap ===============
enqueueVideoMotionCapture(): string{
return `${this.getApiOrigin()}/v1/mocap/mocapnet/create`
enqueueVideoMotionCapture(): string {
return `${this.getApiOrigin()}/v1/mocap/mocapnet/create`;
}
// =============== Convert FBX to glTF ===============
enqueueFbxToGltf(): string {
return `${this.getApiOrigin()}/v1/conversion/enqueue_fbx_to_gltf`;
}
// =============== Generic Model Downloads ===============
@@ -615,4 +619,4 @@ class ApiConfig {
}
}
export { ApiConfig };
export { ApiConfig };
@@ -2,6 +2,8 @@ export enum MediaFileType {
Audio = "audio",
Video = "video",
Image = "image",
Mocap = "mocap",
BVH = "bvh",
GLTF = "gltf",
FBX = "fbx",
None = "none",
}
@@ -0,0 +1,49 @@
import { ApiConfig } from "../ApiConfig";
export interface EnqueueFbxToGltfRequest {
media_file_token: string;
uuid_idempotency_token: string;
}
export interface EnqueueFbxToGltfResponse {
success: boolean;
inference_job_token?: string;
}
const EnqueueFbxToGltfIsSuccess = (response: EnqueueFbxToGltfResponse) =>
response.inference_job_token;
const EnqueueFbxToGltfIsError = (response: EnqueueFbxToGltfResponse) =>
!response.inference_job_token;
export { EnqueueFbxToGltfIsSuccess, EnqueueFbxToGltfIsError };
export async function EnqueueFbxToGltf(
request: EnqueueFbxToGltfRequest
): Promise<EnqueueFbxToGltfResponse> {
const endpoint = new ApiConfig().enqueueFbxToGltf();
return await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success: false };
}
if (res && "success" in res) {
return res;
} else {
return { success: false };
}
})
.catch(e => {
return { success: false };
});
}
@@ -2,7 +2,7 @@ import { ApiConfig } from "../ApiConfig";
export interface EnqueueImageGenRequest {
uuid_idempotency_token: string;
maybe_sd_model_token: string;
maybe_sd_model_token: string | undefined;
maybe_lora_model_token: string | null;
maybe_prompt: string;
maybe_n_prompt: string;
+2 -1
View File
@@ -9,7 +9,8 @@ export enum FrontendInferenceJobType {
VoiceDesignerCreateVoice,
VoiceDesignerTts,
ImageGeneration,
VideoMotionCapture
VideoMotionCapture,
ConvertFbxtoGltf,
}
// NB: Many of these fields are optional despite the response payload containing them
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+31 -29
View File
@@ -208,6 +208,8 @@ function initInferenceJobsByCategoryMap(): Map<
);
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceDesignerTts, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoMotionCapture, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ImageGeneration, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ConvertFbxtoGltf, []);
return inferenceJobsByCategory;
}
@@ -405,11 +407,11 @@ class App extends React.Component<Props, State> {
Accept: "application/json",
},
})
.then((_raw_response) => {
.then(_raw_response => {
this.querySession();
this.querySessionSubscriptions();
})
.catch((e) => {
.catch(e => {
/* Ignore. */
});
};
@@ -452,7 +454,7 @@ class App extends React.Component<Props, State> {
if (GetModelInferenceJobStatusIsOk(lookupResult)) {
let updatedJobs: Array<InferenceJob> = [];
this.state.inferenceJobs.forEach((existingJob) => {
this.state.inferenceJobs.forEach(existingJob => {
if (
existingJob.jobToken !== lookupResult.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -512,8 +514,8 @@ class App extends React.Component<Props, State> {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((response) => {
.then(res => res.json())
.then(response => {
const jobResponse: TtsInferenceJobStateResponsePayload = response;
if (jobResponse === undefined || jobResponse.state === undefined) {
@@ -522,7 +524,7 @@ class App extends React.Component<Props, State> {
let updatedJobs: Array<TtsInferenceJob> = [];
this.state.ttsInferenceJobs.forEach((existingJob) => {
this.state.ttsInferenceJobs.forEach(existingJob => {
if (
existingJob.jobToken !== jobResponse.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -539,7 +541,7 @@ class App extends React.Component<Props, State> {
ttsInferenceJobs: updatedJobs,
});
})
.catch((e) => {
.catch(e => {
/* Ignore. */
});
};
@@ -564,8 +566,8 @@ class App extends React.Component<Props, State> {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((response) => {
.then(res => res.json())
.then(response => {
const jobResponse: TtsModelUploadJobStateResponsePayload = response;
if (jobResponse === undefined || jobResponse.state === undefined) {
@@ -573,7 +575,7 @@ class App extends React.Component<Props, State> {
}
let updatedJobs: Array<TtsModelUploadJob> = [];
this.state.ttsModelUploadJobs.forEach((existingJob) => {
this.state.ttsModelUploadJobs.forEach(existingJob => {
if (
existingJob.jobToken !== jobResponse.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -590,7 +592,7 @@ class App extends React.Component<Props, State> {
ttsModelUploadJobs: updatedJobs,
});
})
.catch((e) => {
.catch(e => {
/* Ignore. */
});
};
@@ -615,8 +617,8 @@ class App extends React.Component<Props, State> {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((response) => {
.then(res => res.json())
.then(response => {
const jobResponse: W2lInferenceJobStateResponsePayload = response;
if (jobResponse === undefined || jobResponse.state === undefined) {
@@ -624,7 +626,7 @@ class App extends React.Component<Props, State> {
}
let updatedJobs: Array<W2lInferenceJob> = [];
this.state.w2lInferenceJobs.forEach((existingJob) => {
this.state.w2lInferenceJobs.forEach(existingJob => {
if (
existingJob.jobToken !== jobResponse.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -641,7 +643,7 @@ class App extends React.Component<Props, State> {
w2lInferenceJobs: updatedJobs,
});
})
.catch((e) => {
.catch(e => {
/* Ignore. */
});
};
@@ -666,8 +668,8 @@ class App extends React.Component<Props, State> {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((response) => {
.then(res => res.json())
.then(response => {
const jobResponse: W2lTemplateUploadJobStateResponsePayload = response;
if (jobResponse === undefined || jobResponse.state === undefined) {
@@ -676,7 +678,7 @@ class App extends React.Component<Props, State> {
let updatedJobs: Array<W2lTemplateUploadJob> = [];
this.state.w2lTemplateUploadJobs.forEach((existingJob) => {
this.state.w2lTemplateUploadJobs.forEach(existingJob => {
if (
existingJob.jobToken !== jobResponse.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -695,7 +697,7 @@ class App extends React.Component<Props, State> {
w2lTemplateUploadJobs: updatedJobs,
});
})
.catch((e) => {
.catch(e => {
/* Ignore. */
});
};
@@ -715,7 +717,7 @@ class App extends React.Component<Props, State> {
if (GetRemoteDownloadJobStatusIsOk(lookupResult)) {
let updatedJobs: Array<VocoderUploadJob> = [];
this.state.vocoderUploadJobs.forEach((existingJob) => {
this.state.vocoderUploadJobs.forEach(existingJob => {
if (
existingJob.jobToken !== lookupResult.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -749,7 +751,7 @@ class App extends React.Component<Props, State> {
if (GetRemoteDownloadJobStatusIsOk(lookupResult)) {
let updatedJobs: Array<VocoderUploadJob> = [];
this.state.voiceConversionModelUploadJobs.forEach((existingJob) => {
this.state.voiceConversionModelUploadJobs.forEach(existingJob => {
if (
existingJob.jobToken !== lookupResult.state!.job_token ||
!jobStateCanChange(existingJob.jobState)
@@ -771,37 +773,37 @@ class App extends React.Component<Props, State> {
};
pollJobs = () => {
this.state.inferenceJobs.forEach((job) => {
this.state.inferenceJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkInferenceJob(job.jobToken);
}
});
this.state.ttsInferenceJobs.forEach((job) => {
this.state.ttsInferenceJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkTtsJob(job.jobToken);
}
});
this.state.w2lInferenceJobs.forEach((job) => {
this.state.w2lInferenceJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkW2lJob(job.jobToken);
}
});
this.state.ttsModelUploadJobs.forEach((job) => {
this.state.ttsModelUploadJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkTtsModelUploadJob(job.jobToken);
}
});
this.state.w2lTemplateUploadJobs.forEach((job) => {
this.state.w2lTemplateUploadJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkW2lTemplateUploadJob(job.jobToken);
}
});
this.state.vocoderUploadJobs.forEach((job) => {
this.state.vocoderUploadJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkVocoderUploadJob(job.jobToken);
}
});
this.state.voiceConversionModelUploadJobs.forEach((job) => {
this.state.voiceConversionModelUploadJobs.forEach(job => {
if (jobStateCanChange(job.jobState)) {
this.checkVoiceConversionModelUploadJob(job.jobToken);
}
@@ -845,7 +847,7 @@ class App extends React.Component<Props, State> {
querySession: this.querySession,
querySubscriptions: this.querySessionSubscriptions,
sessionWrapper: this.state.sessionWrapper,
sessionFetched: this.state.sessionFetched
sessionFetched: this.state.sessionFetched,
}}
>
<Switch>
+13 -33
View File
@@ -38,12 +38,6 @@ $headings-font-family: "Fira Sans", sans-serif;
overflow-wrap: anywhere;
}
//old wrapper
// .mainwrap {
// max-width: 800px;
// margin: auto;
// }
canvas#spectrogram {
margin: 0.5em 0;
transform: scaleY(-1);
@@ -58,37 +52,10 @@ $headings-font-family: "Fira Sans", sans-serif;
box-shadow: none;
}
/*p {
a:link,
a:visited {
color: rgb(119, 165, 235);
font-weight: bold;
}
a:hover,
a:active{
color: rgb(85, 133, 204);
font-weight: bold;
}
}*/
// p {
// a:before {
// content: " ";
// }
// }
#main {
position: relative; // Footer attachment.
min-height: 100vh;
overflow: hidden;
&.low-spec {
.animation-wrapper {
display: none;
visibility: hidden;
content-visibility: hidden;
}
}
}
.social {
@@ -362,6 +329,19 @@ $headings-font-family: "Fira Sans", sans-serif;
height: 100%;
pointer-events: none;
}
.to-top-button-off-screen {
position: fixed;
width: fit-content;
white-space: nowrap;
top: 5rem;
margin: 0 auto;
left: 0;
right: 0;
z-index: 3;
box-shadow: 0px 5px 20px rgba($black, 0.3);
}
}
@media screen and (max-height: 740px) {
@@ -0,0 +1,27 @@
@import "scss/colours.scss";
.fy-premium-lock {
position: relative;
background-color: $panel-inner;
pointer-events: none;
overflow: hidden;
box-shadow: none !important;
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba($panel, 0.8);
display: flex;
justify-content: center;
align-items: center;
color: rgba($white, 0.8);
font-size: 16px;
pointer-events: all;
text-shadow: 0 0 10px rgba(0, 0, 0, 1);
border: 1px solid rgba($primary, 0.4);
border-radius: 0.5rem;
}
}
@@ -0,0 +1,61 @@
import { Panel } from "components/common";
import { Link } from "react-router-dom";
import React from "react";
import "./PremiumLock.scss";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faLock } from "@fortawesome/pro-solid-svg-icons";
interface PremiumLockProps {
requiredPlan?: "plus" | "pro" | "elite";
children: React.ReactNode;
session?: any;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export default function PremiumLock({
requiredPlan = "plus",
children,
sessionSubscriptionsWrapper,
}: PremiumLockProps) {
const hasAccess = () => {
switch (requiredPlan) {
case "plus":
return (
sessionSubscriptionsWrapper.hasActivePlusSubscription() ||
sessionSubscriptionsWrapper.hasActiveProSubscription() ||
sessionSubscriptionsWrapper.hasActiveEliteSubscription()
);
case "pro":
return (
sessionSubscriptionsWrapper.hasActiveProSubscription() ||
sessionSubscriptionsWrapper.hasActiveEliteSubscription()
);
case "elite":
return sessionSubscriptionsWrapper.hasActiveEliteSubscription();
default:
return false;
}
};
return (
<>
{hasAccess() ? (
children
) : (
<Panel className="fy-premium-lock rounded px-3 py-4">
{children}
<div className="overlay">
<div className="d-flex flex-column align-items-center gap-2 text-center">
<FontAwesomeIcon icon={faLock} className="me-2 fs-5" />
<div>
This feature requires a{" "}
<Link to="/pricing">{requiredPlan} subscription</Link>
</div>
</div>
</div>
</Panel>
)}
</>
);
}
@@ -0,0 +1 @@
export { default } from "./PremiumLock";
@@ -7,6 +7,7 @@ import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
export default function Button({
buttonRef,
label,
icon,
small,
@@ -81,7 +82,7 @@ export default function Button({
{ButtonContent}
</a>
) : (
<button type="button" {...commonProps} {...rest}>
<button {...{ ...commonProps, ...rest, type: "button", ref: buttonRef }}>
{ButtonContent}
</button>
);
@@ -2,6 +2,7 @@ import { ButtonHTMLAttributes } from "react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
export default interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
buttonRef?: any;
label?: string | null;
icon?: IconDefinition;
small?: boolean;
@@ -85,15 +85,15 @@ export default function AudioCard({
}
}
const onClick = () => {
if (inClick) inClick(data);
};
// const onClick = () => {
// if (inClick) inClick(data);
// };
const card = (
<Card
padding={true}
canHover={true}
onClick={onClick}
onClick={handleSelectModalResultSelect}
>
{type === "media" && (
<>
@@ -176,7 +176,7 @@ export default function AudioCard({
variant="link"
label="Select"
className="fs-7"
onClick={onClick}
onClick={handleSelectModalResultSelect}
/>
) : (
<Button
@@ -8,9 +8,9 @@ import CreatorName from "../CreatorName";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPersonWalking } from "@fortawesome/pro-solid-svg-icons";
import getCardUrl from "../getCardUrl";
// import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import useMediaFileTypeInfo from "hooks/useMediaFileTypeInfo";
interface ImageCardProps {
interface BVHCardProps {
bookmarks: any;
data: any;
onClick?: (e:any) => any;
@@ -20,7 +20,7 @@ interface ImageCardProps {
type: "media" | "weights";
}
export default function MocapCard({
export default function BVHCard({
bookmarks,
data,
onClick: inClick,
@@ -28,8 +28,8 @@ export default function MocapCard({
source = "",
ratings,
type,
}: ImageCardProps) {
const linkUrl = getCardUrl(data,source,type);
}: BVHCardProps) {
const linkUrl = getCardUrl(data, source, type);
const handleInnerClick = (event: any) => {
event.stopPropagation();
@@ -37,6 +37,11 @@ export default function MocapCard({
const timeAgo = useTimeAgo(data.created_at);
const { label: mediaBadgeLabel, color: mediaBadgeColor } =
useMediaFileTypeInfo(
data.media_type || data.details?.maybe_media_data?.media_type
);
const Wrapper = ({ children }: { children: any }) => inClick ? <div {...{ onClick: () => { inClick(data) } }}>{ children }</div> : <Link {...{ to: linkUrl }}>{ children }</Link>;
// const bucketConfig = new BucketConfig();
@@ -44,13 +49,20 @@ export default function MocapCard({
return (
<Wrapper>
<Card padding={false} canHover={true}>
<FontAwesomeIcon {...{ className: "card-mocap", icon: faPersonWalking }}/>
<div className="card-img d-flex align-items-center justify-content-center">
<FontAwesomeIcon icon={faPersonWalking} className="card-img-icon" />
</div>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="d-flex flex-grow-1">
<Badge label="Mocap" color="pink" overlay={true} />
<Badge
label={mediaBadgeLabel}
color={mediaBadgeColor}
overlay={true}
/>
</div>
</div>
@@ -82,8 +94,14 @@ export default function MocapCard({
/>
</div>
)}
<LikeButton
{...{
...ratings.makeProps({
entityToken: data.token,
entityType: "media_file",
}),
}}
/>
</div>
</div>
</div>
@@ -0,0 +1 @@
export { default } from "./BVHCard";
@@ -16,19 +16,10 @@
min-height: 300px;
max-height: 500px;
background-color: #57577d;
}
.card-mocap {
width: 100%;
transition: 0.4s ease;
object-fit: cover;
object-position: center;
min-height: 300px;
max-height: 500px;
background-color: #57577d;
> path {
fill: lighten(#57577d, 15%);
.card-img-icon {
font-size: 12rem;
color: lighten(#57577d, 15%);
}
}
@@ -0,0 +1,112 @@
import React from "react";
import { Link } from "react-router-dom";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import Badge from "components/common/Badge";
import LikeButton from "components/common/LikeButton";
import CreatorName from "../CreatorName";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCube } from "@fortawesome/pro-solid-svg-icons";
import getCardUrl from "../getCardUrl";
import useMediaFileTypeInfo from "hooks/useMediaFileTypeInfo";
interface FBXCardProps {
bookmarks: any;
data: any;
ratings: any;
showCreator?: boolean;
source?: string;
type: "media" | "weights";
}
export default function FBXCard({
bookmarks,
data,
showCreator,
source = "",
ratings,
type,
}: FBXCardProps) {
const linkUrl = getCardUrl(data, source, type);
const handleInnerClick = (event: any) => {
event.stopPropagation();
};
const timeAgo = useTimeAgo(data.created_at);
const { label: mediaBadgeLabel, color: mediaBadgeColor } =
useMediaFileTypeInfo(
data.media_type || data.details?.maybe_media_data?.media_type
);
return (
<Link
{...{
to: linkUrl,
}}
>
<Card padding={false} canHover={true}>
<div className="card-img d-flex align-items-center justify-content-center">
<FontAwesomeIcon icon={faCube} className="card-img-icon" />
</div>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="d-flex flex-grow-1">
<Badge
label={mediaBadgeLabel}
color={mediaBadgeColor}
overlay={true}
/>
</div>
</div>
<div className="card-img-overlay-text">
<div>
<p className="fs-7 opacity-75 mb-0">{timeAgo}</p>
</div>
<hr className="my-2" />
<div
className="d-flex align-items-center gap-2"
onClick={handleInnerClick}
>
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={
data.maybe_creator?.display_name || "Anonymous"
}
gravatarHash={data.maybe_creator?.gravatar_hash || null}
avatarIndex={
data.maybe_creator?.default_avatar.image_index || 0
}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index || 0
}
username={data.maybe_creator?.username || "anonymous"}
/>
</div>
)}
<div>
<LikeButton
{...{
...ratings.makeProps({
entityToken: data.token,
entityType: "media_file",
}),
}}
/>
</div>
</div>
</div>
</div>
</Card>
</Link>
);
}
@@ -0,0 +1 @@
export { default } from "./FBXCard";
@@ -0,0 +1,112 @@
import React from "react";
import { Link } from "react-router-dom";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import Badge from "components/common/Badge";
import LikeButton from "components/common/LikeButton";
import CreatorName from "../CreatorName";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCube } from "@fortawesome/pro-solid-svg-icons";
import getCardUrl from "../getCardUrl";
import useMediaFileTypeInfo from "hooks/useMediaFileTypeInfo";
interface GLTFCardProps {
bookmarks: any;
data: any;
ratings: any;
showCreator?: boolean;
source?: string;
type: "media" | "weights";
}
export default function GLTFCard({
bookmarks,
data,
showCreator,
source = "",
ratings,
type,
}: GLTFCardProps) {
const linkUrl = getCardUrl(data, source, type);
const handleInnerClick = (event: any) => {
event.stopPropagation();
};
const timeAgo = useTimeAgo(data.created_at);
const { label: mediaBadgeLabel, color: mediaBadgeColor } =
useMediaFileTypeInfo(
data.media_type || data.details?.maybe_media_data?.media_type
);
return (
<Link
{...{
to: linkUrl,
}}
>
<Card padding={false} canHover={true}>
<div className="card-img d-flex align-items-center justify-content-center">
<FontAwesomeIcon icon={faCube} className="card-img-icon" />
</div>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="d-flex flex-grow-1">
<Badge
label={mediaBadgeLabel}
color={mediaBadgeColor}
overlay={true}
/>
</div>
</div>
<div className="card-img-overlay-text">
<div>
<p className="fs-7 opacity-75 mb-0">{timeAgo}</p>
</div>
<hr className="my-2" />
<div
className="d-flex align-items-center gap-2"
onClick={handleInnerClick}
>
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={
data.maybe_creator?.display_name || "Anonymous"
}
gravatarHash={data.maybe_creator?.gravatar_hash || null}
avatarIndex={
data.maybe_creator?.default_avatar.image_index || 0
}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index || 0
}
username={data.maybe_creator?.username || "anonymous"}
/>
</div>
)}
<div>
<LikeButton
{...{
...ratings.makeProps({
entityToken: data.token,
entityType: "media_file",
}),
}}
/>
</div>
</div>
</div>
</div>
</Card>
</Link>
);
}
@@ -0,0 +1 @@
export { default } from "./GLTFCard";
@@ -1,12 +1,14 @@
import React from "react";
import AudioCard from "./AudioCard";
import ImageCard from "./ImageCard";
import MocapCard from "./MocapCard";
import VideoCard from "./VideoCard";
import BVHCard from "./BVHCard";
import GLTFCard from "./GLTFCard";
import FBXCard from "./FBXCard";
interface Props {
props: any,
type: string
props: any;
type: string;
}
export default function MediaCards({ props, type }: Props) {
@@ -15,11 +17,15 @@ export default function MediaCards({ props, type }: Props) {
return <AudioCard {...props} />;
case "image":
return <ImageCard {...props} />;
case "mocap":
return <MocapCard {...props} />;
case "bvh":
return <BVHCard {...props} />;
case "gltf":
return <GLTFCard {...props} />;
case "fbx":
return <FBXCard {...props} />;
case "video":
return <VideoCard {...props} />;
default:
return <div>Unsupported media type</div>;
}
};
}
@@ -1 +0,0 @@
export { default } from "./MocapCard";
@@ -29,7 +29,8 @@ export default function PageContainer({
}
// Merge the classNames
const mergedClassNames = `${containerClass} ${className}`;
const mergedClassNames =
`${containerClass} ${className} min-height-100vh`.trim();
return <div className={mergedClassNames}>{children}</div>;
}
@@ -0,0 +1,12 @@
import React from 'react';
import { useLocalize } from "hooks";
export default function Error({message}:{message?:string}){
const {t} = useLocalize("App")
return (
<div className="row">
<div className="col-12">
<h1>{message ? message : t("error.unknownError")}</h1>
</div>
</div>
);
}
@@ -0,0 +1,48 @@
import React from "react";
import { FileDetails, FileLabel, FileWrapper } from "components/common";
import { faFileAudio } from "@fortawesome/pro-solid-svg-icons";
interface Props {
label?: string;
blob?: string;
children?: any;
clear?: (file?: any) => void;
file?: any;
hideDetails?: boolean;
hideClearDetails?: boolean;
inputProps?: any;
[x: string]: any;
fileTypes?: string[];
}
const n = () => {};
export default function FileInput({
label,
children,
clear = n,
file,
hideDetails,
hideClearDetails,
inputProps,
fileTypes = ["MP3"],
...rest
}: Props) {
return (
<div>
{label && <label className="sub-title">{label}</label>}
<FileWrapper
{...{ fileTypes, ...inputProps, panelClass: "p-3", ...rest }}
>
{file ? (
<FileDetails
{...{ clear, hideClearDetails, icon: faFileAudio, file }}
/>
) : (
<FileLabel {...{ fileTypes }} />
)}
</FileWrapper>
{children}
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./FileInput";
@@ -18,6 +18,7 @@
.fy-uploader-layout {
display: flex;
align-items: center;
width: 100%;
.fy-uploader-layout-icon {
font-size: 2.625rem;
@@ -1,19 +1,37 @@
import React from 'react';
import React from "react";
import "./Label.scss";
interface Props {
invalidReason?: string,
label?: any,
required?: boolean
invalidReason?: string;
label?: any;
required?: boolean;
disabled?: boolean;
}
export default function Label({ invalidReason, label, required }: Props) {
return <div {...{ className: "fy-input-label" }}>
{ label ? <label {...{ className: `{required ? " required" : ""}` }}>
{ label }
</label> : null }
{ invalidReason ? <span {...{ className: "label-error form-text red is-danger" }}>
{ invalidReason }
</span> : null }
</div>;
};
export default function Label({
invalidReason,
label,
required,
disabled,
}: Props) {
return (
<div
{...{
className: `fy-input-label ${disabled ? "opacity-50" : ""}`.trim(),
}}
>
{label ? (
<label
{...{ className: `fw-medium ${required ? " required" : ""}`.trim() }}
>
{label}
</label>
) : null}
{invalidReason ? (
<span {...{ className: "label-error form-text red is-danger" }}>
{invalidReason}
</span>
) : null}
</div>
);
}
@@ -51,5 +51,20 @@
margin-bottom: 0.5rem;
color: lighten($secondary, 60%);
}
&.fy-disabled-segment {
cursor: not-allowed;
opacity: 0.5;
background-color: darken($panel, 2%);
pointer-events: none;
&:hover {
background-color: darken($panel, 2%) !important;
}
.fy-segment-button-icon {
color: darken($secondary, 30%);
}
}
}
}
@@ -12,6 +12,7 @@ interface Props {
options?: any;
value?: string | number;
icon?: IconDefinition | undefined;
disabled?: boolean;
}
export default function SegmentButtons({
@@ -22,11 +23,12 @@ export default function SegmentButtons({
onChange,
options = [],
value: inValue = "",
disabled = false,
}: Props) {
// const onClick = ({ target }: any) => onChange();
return (
<div>
{label && <Label {...{ label }} />}
{label && <Label {...{ label, disabled: disabled }} />}
<ul {...{ className: "fy-segment-buttons mb-0" }}>
{options.map(
(
@@ -35,7 +37,9 @@ export default function SegmentButtons({
) => (
<li
{...{
...(value === inValue && { className: "fy-selected-segment" }),
className: `${value === inValue ? "fy-selected-segment" : ""} ${
disabled ? "fy-disabled-segment" : ""
}`.trim(),
key,
onClick: ({ target }: any) =>
onChange({ target: { name, type: "option", value } }),
@@ -12,7 +12,7 @@ import SelectWeightsList from "./SelectWeightsList";
interface TabConfig {
label: string;
tabKey: string;
type?: "media" | "weights";
type: "media" | "weights";
weightTypeFilter?: string;
mediaTypeFilter?: string;
searcher?: boolean;
@@ -21,12 +21,21 @@ interface SelectModalProps {
label?: string;
tabs: TabConfig[];
modalTitle?: string;
onSelect?: (token: string) => void;
required?: boolean;
}
const SelectModal = memo(
({ label, tabs, modalTitle = "Select" }: SelectModalProps) => {
({
label,
tabs,
modalTitle = "Select",
onSelect,
required,
}: SelectModalProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { token, setToken, weightTitle, setWeightTitle } = useToken();
const [selectedValue, setSelectedValue] = useState("");
const [activeTab, setActiveTab] = useState(tabs[0].tabKey);
const [mediaType, setMediaType] = useState(
tabs[0].mediaTypeFilter || "all"
@@ -39,9 +48,22 @@ const SelectModal = memo(
useEffect(() => {
const currentTab = tabs.find(tab => tab.tabKey === activeTab);
setMediaType(currentTab?.mediaTypeFilter || "all");
setWeightType(currentTab?.mediaTypeFilter || "all");
setWeightType(currentTab?.weightTypeFilter || "all");
}, [activeTab, tabs]);
useEffect(() => {
if (token && token !== selectedValue && onSelect) {
setSelectedValue(token);
onSelect(token);
} else if (weightTitle && weightTitle !== selectedValue && onSelect) {
setSelectedValue(weightTitle);
onSelect(weightTitle);
}
console.log("token", token, "weightTitle", weightTitle);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, weightTitle]);
const openModal = () => {
setIsModalOpen(true);
};
@@ -89,7 +111,11 @@ const SelectModal = memo(
return (
<>
<div>
{label && <label className="sub-title">{label}</label>}
{label && (
<label className={`sub-title ${required && "required"}`.trim()}>
{label}
</label>
)}
<div className="d-flex gap-2">
<Input
@@ -2,7 +2,6 @@ import React, { useEffect } from "react";
import { useSpring } from "@react-spring/web";
import { FileDetails, FileWrapper, FileLabel } from "components/common";
import { faFileVideo, faVideo } from "@fortawesome/pro-solid-svg-icons";
import "./videoinput.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface Props {
@@ -1,5 +0,0 @@
@import "scss/colours.scss";
.upload-details {
background-color: rgba($panel, .85);
}
@@ -5,6 +5,7 @@ import Button from "./Button";
import Checkbox from "./Checkbox";
import Container from "./Container";
import CoverImageInput from "./CoverImageInput";
import ErrorMessage from "./ErrorMessage"
import FileActions from "./FileActions";
import FileDetails from "./FileDetails";
import FileWrapper from "./FileWrapper";
@@ -23,11 +24,13 @@ import Skeleton from "./Skeleton";
import Slider from "./Slider";
import Spinner from "./Spinner";
import SplitPanel from "./SplitPanel";
import Tabs from "./Tabs";
import TempAudioPlayer from "./TempAudioPlayer";
import TempInput from "./TempInput";
import TempSelect from "./TempSelect";
import TempTextArea from "./TempTextArea";
import TintSpinner from "./TintSpinner";
import VideoInput from "./VideoInput";
export {
AudioBlobPreview,
@@ -37,6 +40,7 @@ export {
Checkbox,
Container,
CoverImageInput,
ErrorMessage,
FileActions,
FileDetails,
FileWrapper,
@@ -55,9 +59,11 @@ export {
Slider,
Spinner,
SplitPanel,
Tabs,
TempAudioPlayer,
TempInput,
TempSelect,
TempTextArea,
TintSpinner
TintSpinner,
VideoInput
};
@@ -4,7 +4,6 @@ import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
// import MediaCards from "components/common/Card/MediaCards";
import AudioCard from "components/common/Card/AudioCard";
import ImageCard from "components/common/Card/ImageCard";
import MocapCard from "components/common/Card/MocapCard";
import VideoCard from "components/common/Card/VideoCard";
import { BvhCard } from "components/entities";
@@ -24,8 +23,6 @@ const MediaCards = ({ props, type }: MediaCardsProps) => {
return <AudioCard {...props} />;
case "image":
return <ImageCard {...props} />;
case "mocap":
return <MocapCard {...props} />;
case "video":
return <VideoCard {...props} />;
case "bvh":
@@ -67,7 +67,9 @@ export default function PageHeader({
secondaryButtonOnClick,
}: PageHeaderProps) {
const icon = (
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
<>
{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3 fs-2" />}
</>
);
const buttonProps = {
@@ -116,8 +118,8 @@ export default function PageHeader({
{titleH2 ? (
<h2 className="fw-bold">{title}</h2>
) : (
<h1 className="fw-bold">
{/* {icon} */}
<h1 className="fw-bold d-flex align-items-center">
{icon}
{title}
</h1>
)}
@@ -142,8 +144,9 @@ export default function PageHeader({
<img
src={imageUrl}
alt="Header"
className="img-fluid my-3"
className="img-fluid"
height="235"
loading="lazy"
/>
)}
</div>
@@ -0,0 +1,51 @@
import { Panel } from "components/common";
import React from "react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface Props {
headerImage: string;
titleIcon?: IconDefinition;
title: React.ReactNode;
subText: React.ReactNode;
yOffset?: string;
}
function PageHeaderWithImage(props: Props) {
const imageStyle = {
top: props.yOffset || "50%",
};
return (
<Panel clear={true}>
<div className="row gx-3">
<div className="col-12 col-md-7 py-3 py-lg-4">
<div className="py-3">
<h1 className="fw-bold text-center text-md-start d-flex justify-content-center justify-content-md-start align-items-center">
{props.titleIcon && (
<FontAwesomeIcon icon={props.titleIcon} className="me-3 fs-2" />
)}
{props.title}
</h1>
<p className="text-center text-md-start opacity-75">
{props.subText}
</p>
</div>
</div>
<div className="col-12 col-md-5 d-none d-md-block">
<div className="hero-img-container">
<img
src={props.headerImage}
className="hero-img"
alt="Hero Header"
style={imageStyle}
/>
</div>
</div>
</div>
</Panel>
);
}
export default PageHeaderWithImage;
@@ -0,0 +1 @@
export { default } from "./PageHeaderWithImage";
@@ -14,6 +14,7 @@ import {
faSignOutAlt,
faCameraMovie,
faPersonRays,
faMessageImage,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useEffect, useState } from "react";
@@ -341,7 +342,7 @@ export default function SideNav({
{"Voice Designer"}
</NavLink>
</li>
<hr className="mb-4 mt-3" />
<hr className="mb-3 mt-3" />
<li className="sidebar-heading">{t("videoTitle")}</li>
<li>
<NavLink
@@ -384,6 +385,22 @@ export default function SideNav({
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<hr className="mb-3 mt-3" />
<li className="sidebar-heading">Image Generation</li>
<li>
<NavLink
to="/text-to-image"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faMessageImage}
className="sidebar-heading-icon"
/>
Text to Image
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<hr className="mb-3 mt-3" />
<li className="sidebar-heading">{t("communityTitle")}</li>
@@ -400,7 +417,7 @@ export default function SideNav({
{t("communityUploadModels")}
</NavLink>
</li>
<li>
<li className="mb-3">
<a
href="https://discord.gg/fakeyou"
target="_blank"
+2
View File
@@ -13,6 +13,7 @@ import useListContent from './useListContent';
import useLocalize from './useLocalize';
import useMedia from './useMedia';
import useModal from './useModal';
import useOnScreen from "./useOnScreen";
import useRatings from './useRatings';
import useSdUpload from './useSdUpload';
import useSession from './useSession';
@@ -35,6 +36,7 @@ export {
useLocalize,
useMedia,
useModal,
useOnScreen,
useRatings,
useSdUpload,
useSession,
@@ -32,7 +32,8 @@ export default function useLazyLists({
const { pathname, search: locSearch } = useLocation();
const history = useHistory();
const urlQueries = new URLSearchParams(locSearch);
const [next, nextSet] = useState(urlQueries.get("cursor") || "");
const urlCursor = urlQueries.get("cursor");
const [next, nextSet] = useState(urlCursor || "");
const [previous, previousSet] = useState(""); // I am not used for anything yet :)
const [sort, sortSet] = useState(urlQueries.get("sort_ascending") === "true");
const [status, statusSet] = useState(
@@ -44,6 +45,9 @@ export default function useLazyLists({
status === FetchStatus.ready || status === FetchStatus.in_progress;
const fetchError = status === FetchStatus.error;
const [goingtToTop,goingtToTopSet] = useState(false);
const [y,ySet] = useState(0);
const getMore = () => {
if (next) statusSet(1);
};
@@ -61,6 +65,15 @@ export default function useLazyLists({
statusSet(FetchStatus.ready);
};
const reset = () => {
listSet([]); // Reset list on filter/sort change
nextSet("");
previousSet("");
statusSet(FetchStatus.ready);
window.scrollTo(0,0);
goingtToTopSet(true);
}
useEffect(() => {
const queries = {
...(next ? { cursor: next } : {}),
@@ -68,7 +81,15 @@ export default function useLazyLists({
...(sort ? { sort_ascending: true } : {}),
};
if (status === FetchStatus.ready) {
const adjustY = () => ySet(window.pageYOffset);
if (goingtToTop) {
window.addEventListener('scroll', adjustY, { passive: true });
if (y === 0) setTimeout(() => goingtToTopSet(false),500);
}
else window.removeEventListener('scroll',adjustY);
if (status === FetchStatus.ready && !goingtToTop) {
let search = new URLSearchParams(queries).toString();
statusSet(FetchStatus.in_progress);
!disableUrlQueries && history.replace({ pathname, search });
@@ -98,7 +119,7 @@ export default function useLazyLists({
}
});
nextSet(res.pagination.maybe_next || "");
previousSet(res.pagination.maybe_next);
previousSet(res.pagination.maybe_previous);
}
});
}
@@ -106,6 +127,7 @@ export default function useLazyLists({
addQueries,
debug,
fetcher,
goingtToTop,
history,
listKeys,
listSet,
@@ -116,6 +138,7 @@ export default function useLazyLists({
status,
totalKeys,
disableUrlQueries,
y
]);
return {
@@ -127,10 +150,12 @@ export default function useLazyLists({
next,
onChange,
previous,
reset,
sort,
sortSet,
status,
statusSet,
totalKeys,
urlCursor
};
}
@@ -0,0 +1 @@
export { default } from "./useLoraUpload";
@@ -0,0 +1,59 @@
import { useState } from "react";
import { UploadModel } from "@storyteller/components/src/api/image_generation/UploadModel";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
import { useCoverImgUpload } from "hooks";
import { v4 as uuidv4 } from "uuid";
// this hook is mostly for organizational purposes while I work -V
export default function useLoraUpload() {
const [title, titleSet] = useState("");
const [uploadPath, uploadPathSet] = useState("");
const [visibility, visibilitySet] = useState("public");
const [descriptionMD, descriptionMDSet] = useState("");
const [writeStatus, writeStatusSet] = useState(FetchStatus.paused);
const coverImg = useCoverImgUpload();
const onChange = ({ target }: { target: { name: string; value: any } }) => {
const todo: { [key: string]: (x: any) => void } = {
descriptionMDSet,
uploadPathSet,
titleSet,
visibilitySet,
};
todo[target.name + "Set"](target.value);
};
const upload = () => {
writeStatusSet(FetchStatus.in_progress);
UploadModel("", {
...(coverImg.token
? { cover_image_media_file_token: coverImg.token }
: {}),
description: descriptionMD,
uuid_idempotency_token: uuidv4(),
type_of_inference: "inference",
maybe_upload_path: uploadPath,
title,
visibility,
})
.then((res: any) => {
writeStatusSet(FetchStatus.success);
console.log("🌠", res);
// history.replace(`/weight/${token}`);
})
.catch(err => {
writeStatusSet(FetchStatus.error);
});
};
return {
coverImg,
descriptionMD,
onChange,
title,
upload,
uploadPath,
visibility,
writeStatus,
};
}
@@ -17,10 +17,18 @@ const mediaFileTypeInfo: MediaFileTypeInfo = {
label: "Video",
color: "purple",
},
[MediaFileType.Mocap]: {
label: "Mocap",
[MediaFileType.BVH]: {
label: "BVH",
color: "pink",
},
[MediaFileType.GLTF]: {
label: "glTF",
color: "lime",
},
[MediaFileType.FBX]: {
label: "FBX",
color: "lime",
},
[MediaFileType.None]: {
label: "None",
color: "gray",
@@ -1,8 +1,10 @@
type Setter = (x: any) => void
type Setter = (x: any) => void;
export default function onChanger({ ...setters }: { [key: string]: Setter }) {
return ({ target }: { target: { name: string; value: any } }) => {
const targetName =
target.name.charAt(0).toUpperCase() + target.name.slice(1);
const todo: { [key: string]: Setter } = setters;
todo[target.name + "Set"](target.value);
todo["set" + targetName](target.value);
};
}
}
+4
View File
@@ -52,6 +52,10 @@ $topbar-height: 66px;
}
#page-content-wrapper {
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
position: relative;
padding-left: 260px;
@@ -29,16 +29,15 @@ styles with important tags:
}
.fakeyou-refresh {
//- BS5 classnames -//
.object-fit-cover{
.object-fit-cover {
object-fit: cover;
}
.ratio-1x1 {
aspect-ratio: 1/1;
}
//-end--//
::-webkit-scrollbar-thumb {
background-color: rgba($secondary, 1);
border-radius: 0.5rem;
@@ -1129,11 +1128,11 @@ styles with important tags:
.hero-img {
pointer-events: none;
position: absolute;
width: 90%;
margin: auto;
top: 22%;
right: 9%;
bottom: 0;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 95%;
}
.hero-img-mobile {
+694 -149
View File
@@ -106,6 +106,12 @@ import SearchPage from "./pages/search/SearchPage";
import { SearchProvider } from "context/SearchContext";
import WeightEditPage from "./pages/weight/WeightEditPage";
import UploadSdWeightPage from "./pages/upload/UploadSdWeightPage";
import { FooterNav } from "./nav/FooterNav";
import FbxToGltfPage from "./pages/fbx_to_gltf/FbxToGltfPage";
import UploadLoraWeightPage from "./pages/upload/UploadLoraWeightPage";
import StorytellerFilterPage from "./pages/storyteller_filter/StorytellerFilter";
import ScrollToTop from "./_common/ScrollToTop";
import TextToImagePage from "./pages/text_to_image/TextToImagePage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -207,7 +213,6 @@ class PageContainer extends React.Component<
> {
constructor(props: Props & RouteComponentProps) {
super(props);
this.state = {};
}
@@ -216,6 +221,7 @@ class PageContainer extends React.Component<
public render() {
return (
<SearchProvider>
<ScrollToTop />
<div id="wrapper" className="no-padding">
<div id="overlay"></div>
@@ -238,137 +244,552 @@ class PageContainer extends React.Component<
<div id="page-content-wrapper">
<MobileMenu sessionWrapper={this.props.sessionWrapper} />
<Switch>
<Route path="/firehose">
<FirehoseEventListPage
sessionWrapper={this.props.sessionWrapper}
<div id="main">
<Switch>
<Route path="/firehose">
<FirehoseEventListPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/news">
<NewsPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/channels">
<ChannelsPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/leaderboard">
<LeaderboardPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/login">
<LoginPage
sessionWrapper={this.props.sessionWrapper}
querySessionAction={this.props.querySessionAction}
querySessionSubscriptionsAction={
this.props.querySessionSubscriptionsAction
}
/>
</Route>
<Route path="/password-reset/verify">
<PasswordResetVerificationPage
sessionWrapper={this.props.sessionWrapper}
querySessionAction={this.props.querySessionAction}
querySessionSubscriptionsAction={
this.props.querySessionSubscriptionsAction
}
/>
</Route>
<Route path="/password-reset">
<PasswordResetEmailPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/profile/:username/edit">
<ProfileEditFc sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/profile/:username/ban">
<ProfileBanFc sessionWrapper={this.props.sessionWrapper} />
</Route>
{/* New Profile Page */}
<Route path="/profile/:username">
<NewProfilePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/signup">
<SignupPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/pricing" exact={true}>
<PricingPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/checkout_success" exact={true}>
<CheckoutSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/checkout_cancel" exact={true}>
<CheckoutCancelPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/portal_success" exact={true}>
<PortalSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/media/:token">
<MediaPage />
</Route>
<Route path="/explore">
<ExplorePage />
</Route>
<Route path="/weight/:weight_token/edit">
<WeightEditPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route
path="/weight/:weight_token"
render={props => (
<WeightPage
key={props.match.params.weight_token}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
inferenceJobs={this.props.inferenceJobs}
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobsByCategory={
this.props.inferenceJobsByCategory
}
ttsInferenceJobs={this.props.ttsInferenceJobs}
enqueueTtsJob={this.props.enqueueTtsJob}
/>
)}
/>
</Route>
<Route path="/news">
<NewsPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/search/weights">
<SearchPage />
</Route>
<Route path="/channels">
<ChannelsPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/tts/result/:token/edit">
<TtsResultEditPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/leaderboard">
<LeaderboardPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/tts/result/:token/delete">
<TtsResultDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/login">
<LoginPage
sessionWrapper={this.props.sessionWrapper}
querySessionAction={this.props.querySessionAction}
querySessionSubscriptionsAction={
this.props.querySessionSubscriptionsAction
}
/>
</Route>
<Route path="/tts/result/:token">
<TtsResultViewPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/password-reset/verify">
<PasswordResetVerificationPage
sessionWrapper={this.props.sessionWrapper}
querySessionAction={this.props.querySessionAction}
querySessionSubscriptionsAction={
this.props.querySessionSubscriptionsAction
}
/>
</Route>
<Route path="/tts/:token/edit">
<TtsModelEditPage
sessionWrapper={this.props.sessionWrapper}
enqueueTtsJob={this.props.enqueueTtsJob}
ttsInferenceJobs={this.props.ttsInferenceJobs}
/>
</Route>
<Route path="/password-reset">
<PasswordResetEmailPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/tts/:token/delete">
<TtsModelDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/profile/:username/edit">
<ProfileEditFc sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/tts/:token/categories">
<TtsEditCategoriesPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/profile/:username/ban">
<ProfileBanFc sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/tts/:token">
<TtsModelViewPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
enqueueTtsJob={this.props.enqueueTtsJob}
inferenceJobs={this.props.inferenceJobs}
ttsInferenceJobs={this.props.ttsInferenceJobs}
textBuffer={this.props.textBuffer}
setTextBuffer={this.props.setTextBuffer}
clearTextBuffer={this.props.clearTextBuffer}
/>
</Route>
{/* New Profile Page */}
<Route path="/profile/:username">
<NewProfilePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/w2l/result/:token/edit">
<W2lResultEditPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/signup">
<SignupPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/w2l/result/:token/delete">
<W2lResultDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/pricing" exact={true}>
<PricingPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/w2l/result/:token">
<W2lResultViewPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/checkout_success" exact={true}>
<CheckoutSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/checkout_cancel" exact={true}>
<CheckoutCancelPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/portal_success" exact={true}>
<PortalSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/w2l/:templateToken/edit">
<W2lTemplateEditPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/upload/sd">
<UploadSdWeightPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/w2l/:templateToken/approval">
<W2lTemplateApprovePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/media/:token">
<MediaPage />
</Route>
<Route path="/w2l/:templateToken/delete">
<W2lTemplateDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/explore">
<ExplorePage />
</Route>
<Route path="/w2l/:templateSlug">
<W2lTemplateViewPage
sessionWrapper={this.props.sessionWrapper}
enqueueW2lJob={this.props.enqueueW2lJob}
w2lInferenceJobs={this.props.w2lInferenceJobs}
/>
</Route>
<Route path="/weight/:weight_token/edit">
<WeightEditPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/video">
<W2lTemplateListPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route
path="/weight/:weight_token"
render={props => (
<WeightPage
key={props.match.params.weight_token}
<Route path="/upload/w2l_photo">
<UploadW2lPhotoPage
sessionWrapper={this.props.sessionWrapper}
w2lTemplateUploadJobs={this.props.w2lTemplateUploadJobs}
enqueueW2lTemplateUploadJob={
this.props.enqueueW2lTemplateUploadJob
}
/>
</Route>
<Route path="/upload/w2l_video">
<UploadW2lVideoPage
sessionWrapper={this.props.sessionWrapper}
w2lTemplateUploadJobs={this.props.w2lTemplateUploadJobs}
enqueueW2lTemplateUploadJob={
this.props.enqueueW2lTemplateUploadJob
}
/>
</Route>
<Route path="/upload/tts">
<UploadTtsModelPage
sessionWrapper={this.props.sessionWrapper}
ttsModelUploadJobs={this.props.ttsModelUploadJobs}
enqueueTtsModelUploadJob={
this.props.enqueueTtsModelUploadJob
}
/>
</Route>
<Route path="/upload/voice_conversion">
<UploadVoiceConversionModel
sessionWrapper={this.props.sessionWrapper}
voiceConversionModelUploadJobs={
this.props.voiceConversionModelUploadJobs
}
enqueueVoiceConversionModelUploadJob={
this.props.enqueueVoiceConversionModelUploadJob
}
/>
</Route>
<Route path="/upload/vocoder" exact={true}>
<UploadVocoderPage
sessionWrapper={this.props.sessionWrapper}
vocoderUploadJobs={this.props.vocoderUploadJobs}
enqueueVocoderUploadJob={this.props.enqueueVocoderUploadJob}
/>
</Route>
<Route path="/upload/sd">
<UploadSdWeightPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/upload/lora">
<UploadLoraWeightPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/contribute">
<ContributeIndexPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/category/create">
<CreateCategoryPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/user/list">
<ModerationUserListFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/ip_bans/:ipAddress">
<ModerationViewIpBanFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/ip_bans">
<ModerationIpBanListFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/voice_stats">
<ModerationVoiceStatsFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/job_stats">
<ModerationJobStatsFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/job_control">
<ModerationJobControlPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/tts_category/list">
<ModerationTtsCategoryListPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/tts_category/edit/:token">
<ModerationTtsCategoryEditPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/category/delete/:token">
<ModerationCategoryDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/approve/w2l_templates">
<ModerationPendingW2lTemplatesFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation">
<ModerationFc sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route exact={true} path="/clone">
<VoiceCloneRequestPage />
</Route>
<Route path="/patrons">
<PatronPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/product-usage">
<ProductUsageInfoPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/voice-conversion/:token/delete">
<VcModelDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/voice-conversion/:token/edit">
<VcModelEditPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/voice-conversion/:token">
<VcModelViewPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
setMaybeSelectedInferenceJob={
this.props.maybeSelectedVoiceConversionModel
}
/>
</Route>
<Route path="/voice-conversion">
<VcModelListPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
voiceConversionModels={this.props.voiceConversionModels}
setVoiceConversionModels={
this.props.setVoiceConversionModels
}
maybeSelectedVoiceConversionModel={
this.props.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={
this.props.setMaybeSelectedVoiceConversionModel
}
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobs={this.props.inferenceJobs}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
/>
</Route>
<Route path="/about">
<AboutPage />
</Route>
<Route path="/face-animator/:mediaToken?">
<FaceAnimator
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory:
this.props.inferenceJobsByCategory,
}}
/>
</Route>
<Route path="/fbx-to-gltf/:mediaToken?">
<FbxToGltfPage
enqueueInferenceJob={this.props.enqueueInferenceJob}
/>
</Route>
<Route path="/studio">
<StorytellerStudioListPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/commissions">
<CommunityCommissionsPage />
</Route>
<Route path="/terms">
<TermsPage />
</Route>
<Route path="/privacy">
<PrivacyPage />
</Route>
<Route path="/guide">
<GuidePage />
</Route>
<Route path="/old">
<VocodesPage />
</Route>
<Route path="/dev-upload">
<DevUpload />
</Route>
{/* Route for initial voice creation */}
<Route exact path="/voice-designer/create">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for editing the dataset details */}
<Route exact path="/voice-designer/dataset/:dataset_token/edit">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for handling dataset token for uploading samples */}
<Route
exact
path="/voice-designer/dataset/:dataset_token/upload"
>
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
<Route path="/voice-designer/voice/:voice_token/edit">
<VoiceDesignerVoiceEditPage />
</Route>
<Route path="/voice-designer/voice/:voice_token">
<VoiceDesignerUseVoicePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
@@ -376,48 +797,170 @@ class PageContainer extends React.Component<
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
ttsInferenceJobs={this.props.ttsInferenceJobs}
enqueueTtsJob={this.props.enqueueTtsJob}
/>
)}
/>
</Route>
<Route path="/search/weights">
<SearchPage />
</Route>
<Route path="/voice-designer">
<VoiceDesignerMainPage />
</Route>
<Route path="/tts/result/:token/edit">
<TtsResultEditPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/generate-speech">
<GenerateSpeechPage />
</Route>
<Route path="/tts/result/:token/delete">
<TtsResultDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/video-mocap/:mediaToken?">
<VideoMocap
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory:
this.props.inferenceJobsByCategory,
}}
/>
</Route>
<Route path="/tts/result/:token">
<TtsResultViewPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/storyteller-filter/:mediaToken?">
<StorytellerFilterPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory:
this.props.inferenceJobsByCategory,
}}
/>
</Route>
<Route path="/tts/:token/edit">
<TtsModelEditPage
sessionWrapper={this.props.sessionWrapper}
enqueueTtsJob={this.props.enqueueTtsJob}
ttsInferenceJobs={this.props.ttsInferenceJobs}
/>
</Route>
<Route path="/text-to-image">
<TextToImagePage
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
enqueueInferenceJob={this.props.enqueueInferenceJob}
/>
</Route>
<Route path="/tts/:token/delete">
<TtsModelDeletePage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/character/donald-trump">
<TrumpTtsPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
enqueueTtsJob={this.props.enqueueTtsJob}
inferenceJobs={this.props.inferenceJobs}
ttsInferenceJobs={this.props.ttsInferenceJobs}
ttsModelUploadJobs={this.props.ttsModelUploadJobs}
w2lInferenceJobs={this.props.w2lInferenceJobs}
w2lTemplateUploadJobs={this.props.w2lTemplateUploadJobs}
textBuffer={this.props.textBuffer}
setTextBuffer={this.props.setTextBuffer}
clearTextBuffer={this.props.clearTextBuffer}
ttsModels={this.props.ttsModels}
setTtsModels={this.props.setTtsModels}
allTtsCategories={this.props.allTtsCategories}
setAllTtsCategories={this.props.setAllTtsCategories}
allTtsCategoriesByTokenMap={
this.props.allTtsCategoriesByTokenMap
}
computedTtsCategoryAssignments={
this.props.computedTtsCategoryAssignments
}
setComputedTtsCategoryAssignments={
this.props.setComputedTtsCategoryAssignments
}
allTtsModelsByTokenMap={this.props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={
this.props.ttsModelsByCategoryToken
}
dropdownCategories={this.props.dropdownCategories}
setDropdownCategories={this.props.setDropdownCategories}
selectedCategories={this.props.selectedCategories}
setSelectedCategories={this.props.setSelectedCategories}
maybeSelectedTtsModel={this.props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={
this.props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={
this.props.selectedTtsLanguageScope
}
setSelectedTtsLanguageScope={
this.props.setSelectedTtsLanguageScope
}
/>
</Route>
<Route path="/tts/:token/categories">
<TtsEditCategoriesPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/tts" exact={true}>
<TtsModelListPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
isShowingVocodesNotice={this.props.isShowingVocodesNotice}
clearVocodesNotice={this.props.clearVocodesNotice}
isShowingLanguageNotice={this.props.isShowingLangaugeNotice}
clearLanguageNotice={this.props.clearLanguageNotice}
displayLanguage={this.props.displayLanguage}
isShowingTwitchTtsNotice={
this.props.isShowingTwitchTtsNotice
}
clearTwitchTtsNotice={this.props.clearTwitchTtsNotice}
isShowingPleaseFollowNotice={
this.props.isShowingPleaseFollowNotice
}
clearPleaseFollowNotice={this.props.clearPleaseFollowNotice}
isShowingBootstrapLanguageNotice={
this.props.isShowingBootstrapLanguageNotice
}
clearBootstrapLanguageNotice={
this.props.clearBootstrapLanguageNotice
}
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobs={this.props.inferenceJobs}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
enqueueTtsJob={this.props.enqueueTtsJob}
ttsInferenceJobs={this.props.ttsInferenceJobs}
ttsModelUploadJobs={this.props.ttsModelUploadJobs}
w2lInferenceJobs={this.props.w2lInferenceJobs}
w2lTemplateUploadJobs={this.props.w2lTemplateUploadJobs}
textBuffer={this.props.textBuffer}
setTextBuffer={this.props.setTextBuffer}
clearTextBuffer={this.props.clearTextBuffer}
ttsModels={this.props.ttsModels}
setTtsModels={this.props.setTtsModels}
allTtsCategories={this.props.allTtsCategories}
setAllTtsCategories={this.props.setAllTtsCategories}
allTtsCategoriesByTokenMap={
this.props.allTtsCategoriesByTokenMap
}
computedTtsCategoryAssignments={
this.props.computedTtsCategoryAssignments
}
setComputedTtsCategoryAssignments={
this.props.setComputedTtsCategoryAssignments
}
allTtsModelsByTokenMap={this.props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={
this.props.ttsModelsByCategoryToken
}
dropdownCategories={this.props.dropdownCategories}
setDropdownCategories={this.props.setDropdownCategories}
selectedCategories={this.props.selectedCategories}
setSelectedCategories={this.props.setSelectedCategories}
maybeSelectedTtsModel={this.props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={
this.props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={
this.props.selectedTtsLanguageScope
}
setSelectedTtsLanguageScope={
this.props.setSelectedTtsLanguageScope
}
/>
</Route>
<Route path="/tts/:token">
<TtsModelViewPage
@@ -898,7 +1441,9 @@ class PageContainer extends React.Component<
}
/>
</Route>
</Switch>
</Switch>
</div>
<FooterNav sessionWrapper={this.props.sessionWrapper} />
</div>
</div>
</SearchProvider>
@@ -1,47 +0,0 @@
import React from "react";
interface Props {
headerImage: string;
titleIcon?: JSX.Element;
title: JSX.Element;
subText: JSX.Element;
showButtons: boolean;
actionButtons?: JSX.Element;
}
function PageHeaderWithImage(props: Props) {
return (
<div className="container-panel pt-3 pb-4 pt-lg-4">
<div className="panel">
<div className="row gx-3 flex-md-row-reverse">
<div className="col-12 col-md-5 hero-img-container d-none d-md-block">
<img
src={props.headerImage}
className="hero-img"
alt="Hero Header"
/>
</div>
<div className="col-12 col-md-7">
<div className="p-3 py-4 p-md-4">
<h1 className="fw-bold text-center text-md-start">
{props.titleIcon}
{props.title}
</h1>
<p className="text-center text-md-start opacity-75 pt-1">
{props.subText}
</p>
{props.showButtons && (
<div className="d-flex flex-column flex-sm-row gap-3 justify-content-center justify-content-md-start mt-4 pt-2">
{props.actionButtons}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
export { PageHeaderWithImage };
@@ -1,27 +1,19 @@
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { History } from "history";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
// A solution for scroll-to-top behavior.
// From https://stackoverflow.com/a/54343182
// And also https://v5.reactrouter.com/web/guides/scroll-restoration
export default function ScrollToTop() {
const { pathname, search } = useLocation();
const [prevPath, setPrevPath] = useState("");
interface Props {
history : History,
}
function ScrollToTop(props: Props) {
let { history } = props;
useEffect(() => {
const unlisten = history.listen(() => {
const fullPathWithoutSearch = pathname;
const prevFullPathWithoutSearch = prevPath;
if (fullPathWithoutSearch !== prevFullPathWithoutSearch) {
window.scrollTo(0, 0);
});
return () => {
unlisten();
}
}, [history]);
return (null);
setPrevPath(fullPathWithoutSearch);
}, [pathname, prevPath, search]);
return null;
}
export default withRouter(ScrollToTop);
+141 -231
View File
@@ -10,7 +10,6 @@ import {
faDiscord,
faTwitter,
faTiktok,
} from "@fortawesome/free-brands-svg-icons";
import { ThirdPartyLinks } from "@storyteller/components/src/constants/ThirdPartyLinks";
import {
@@ -18,14 +17,8 @@ import {
GetServerInfoIsOk,
GetServerInfoSuccessResponse,
} from "@storyteller/components/src/api/server/GetServerInfo";
import {
GetQueueStats,
GetQueueStatsIsOk,
GetQueueStatsSuccessResponse,
} from "@storyteller/components/src/api/stats/queues/GetQueueStats";
import { useLocalize } from "hooks";
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
import { Container } from "components/common";
interface Props {
sessionWrapper: SessionWrapper;
@@ -48,46 +41,6 @@ function FooterNav(props: Props) {
getServerInfo();
}, [getServerInfo]);
const [queueStats, setQueueStats] = useState<GetQueueStatsSuccessResponse>({
success: true,
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
inference: {
total_pending_job_count: 0,
pending_job_count: 0,
by_queue: {
pending_face_animation_jobs: 0,
pending_rvc_jobs: 0,
pending_svc_jobs: 0,
pending_tacotron2_jobs: 0,
pending_voice_designer: 0,
},
},
legacy_tts: {
pending_job_count: 0,
},
});
useEffect(() => {
const fetch = async () => {
const response = await GetQueueStats();
if (GetQueueStatsIsOk(response)) {
if (response.cache_time.getTime() > queueStats.cache_time.getTime()) {
setQueueStats(response);
}
}
};
// TODO: We're having an outage and need to lower this.
//const interval = setInterval(async () => fetch(), 15000);
const refreshInterval = Math.max(
DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
queueStats.refresh_interval_millis
);
const interval = setInterval(async () => fetch(), refreshInterval);
fetch();
return () => clearInterval(interval);
}, [queueStats]);
let myDataLink = WebUrl.signupPage();
if (props.sessionWrapper.isLoggedIn()) {
@@ -124,194 +77,151 @@ function FooterNav(props: Props) {
);
}
// NB(bt,2023-11-28): These are representative of tacotron2 jobs handled by two queueing systems:
// The legacy queue (tts-inference-job) and the modern queue (inference-job). We'll add both totals
// while we migrate off of the legacy system, then eventually kill the legacy statistic.
const ttsQueuedCount = queueStats.legacy_tts.pending_job_count + queueStats.inference.by_queue.pending_tacotron2_jobs;
return (
<div>
<footer id="footer">
<div className="footer-bar text-center text-lg-start">
<div className="container fw-medium d-flex gap-2 justify-content-center justify-content-lg-start">
<div>
TTS Queued:{" "}
<span className="text-red">
{ttsQueuedCount}
</span>
</div>
<span className="opacity-25">•</span>
<div>
RVC Queued:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_rvc_jobs}
</span>
</div>
<span className="opacity-25">•</span>
<div>
SVC Queued:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_svc_jobs}
</span>
</div>
<div>
Animations Queued:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_face_animation_jobs}
</span>
</div>
<div>
Voice Designer Queued:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_voice_designer}
</span>
</div>
</div>
</div>
<div className="container py-5">
<div className="row gx-5 gy-5">
<div className="col-12 col-lg-3 d-flex flex-column gap-4 align-items-center align-items-lg-start">
<Link to="/">
<img
src="/fakeyou/FakeYou-Logo.png"
alt="FakeYou: Cartoon and Celebrity Text to Speech"
height="36"
/>
</Link>
<div className="d-flex gap-3">
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_DISCORD}
target="_blank"
rel="noopener noreferrer"
title="Join our Discord Server"
>
<FontAwesomeIcon icon={faDiscord} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TWITTER_WITH_FOLLOW_INTENT}
target="_blank"
rel="noopener noreferrer"
title="Follow us on Twitter"
>
<FontAwesomeIcon icon={faTwitter} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TIKTOK}
target="_blank"
rel="noopener noreferrer"
title="Follow us on Tiktok"
>
<FontAwesomeIcon icon={faTiktok} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TWITCH}
target="_blank"
rel="noopener noreferrer"
title="Subscribe to our Twitch Channel"
>
<FontAwesomeIcon icon={faTwitch} className="me-2" />
</a>
</div>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("productsTitle")}</p>
<li>
<Link to="/tts">{t("productTts")}</Link>
</li>
<li>
<Link to="/voice-conversion">{t("productVc")}</Link>
</li>
<li>
<Link to="/voice-designer">Voice Designer</Link>
</li>
<li>
<Link to="/face-animator">{t("productFaceAnimator")}</Link>
</li>
<li>
<Link to="/contribute">{t("productUploadModels")}</Link>
</li>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("communityTitle")}</p>
<li>
<a
href={ThirdPartyLinks.FAKEYOU_DISCORD}
target="_blank"
rel="noopener noreferrer"
>
{t("communityDiscord")}
</a>
</li>
<li>
<Link to="/leaderboard">{t("communityLeaderboard")}</Link>
</li>
<li>
<Link to="/guide">{t("communityGuide")}</Link>
</li>
<li>
<Link to={myDataLink}>{t("communityProfile")}</Link>
</li>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("infoTitle")}</p>
<li>
<Link to={WebUrl.pricingPageWithReferer("footer")}>
{t("infoPricing")}
</Link>
</li>
<li>
<Link to={WebUrl.aboutUsPage()}>{t("infoAbout")}</Link>
</li>
<li>
<Link to={WebUrl.termsPage()}>{t("infoTerms")}</Link>
</li>
<li>
<Link to={WebUrl.privacyPage()}>{t("infoPrivacyPolicy")}</Link>
</li>
<li>
<a href={WebUrl.developerDocs()}>{t("infoApiDocs")}</a>
</li>
</div>
</div>
<div className="pt-4">
<hr />
</div>
<div className="d-flex flex-column flex-lg-row pt-2 align-items-center gap-0 gap-lg-4">
<span className="flex-grow-1">
© 2023 FakeYou by{" "}
<a href="https://storyteller.ai" target="_blank" rel="noreferrer">
Storyteller.ai
<footer id="footer">
<Container type="panel" className="py-5">
<div className="row g-5">
<div className="col-12 col-lg-3 d-flex flex-column gap-4 align-items-center align-items-lg-start">
<Link to="/">
<img
src="/fakeyou/FakeYou-Logo.png"
alt="FakeYou: Cartoon and Celebrity Text to Speech"
height="34"
/>
</Link>
<div className="d-flex gap-3">
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_DISCORD}
target="_blank"
rel="noopener noreferrer"
title="Join our Discord Server"
>
<FontAwesomeIcon icon={faDiscord} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TWITTER_WITH_FOLLOW_INTENT}
target="_blank"
rel="noopener noreferrer"
title="Follow us on Twitter"
>
<FontAwesomeIcon icon={faTwitter} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TIKTOK}
target="_blank"
rel="noopener noreferrer"
title="Follow us on Tiktok"
>
<FontAwesomeIcon icon={faTiktok} className="me-2" />
</a>
<a
className="social-icon"
href={ThirdPartyLinks.FAKEYOU_TWITCH}
target="_blank"
rel="noopener noreferrer"
title="Subscribe to our Twitch Channel"
>
<FontAwesomeIcon icon={faTwitch} className="me-2" />
</a>
</span>
<div className="d-flex flex-column flex-lg-row align-items-center mt-4 mt-lg-0">
{moderationLink}
</div>
{serverGitSha}
<div className="d-flex flex-column flex-lg-row align-items-center">
<GitSha prefix="FE: " />
</div>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("productsTitle")}</p>
<li>
<Link to="/tts">{t("productTts")}</Link>
</li>
<li>
<Link to="/voice-conversion">{t("productVc")}</Link>
</li>
<li>
<Link to="/voice-designer">Voice Designer</Link>
</li>
<li>
<Link to="/face-animator">{t("productFaceAnimator")}</Link>
</li>
<li>
<Link to="/contribute">{t("productUploadModels")}</Link>
</li>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("communityTitle")}</p>
<li>
<a
href={ThirdPartyLinks.FAKEYOU_DISCORD}
target="_blank"
rel="noopener noreferrer"
>
{t("communityDiscord")}
</a>
</li>
<li>
<Link to="/leaderboard">{t("communityLeaderboard")}</Link>
</li>
<li>
<Link to="/guide">{t("communityGuide")}</Link>
</li>
<li>
<Link to={myDataLink}>{t("communityProfile")}</Link>
</li>
</div>
<div className="py-2 col-12 col-lg-3 d-flex flex-column gap-2 gap-lg-3 align-items-center align-items-lg-start">
<p className="fw-bold">{t("infoTitle")}</p>
<li>
<Link to={WebUrl.pricingPageWithReferer("footer")}>
{t("infoPricing")}
</Link>
</li>
<li>
<Link to={WebUrl.aboutUsPage()}>{t("infoAbout")}</Link>
</li>
<li>
<Link to={WebUrl.termsPage()}>{t("infoTerms")}</Link>
</li>
<li>
<Link to={WebUrl.privacyPage()}>{t("infoPrivacyPolicy")}</Link>
</li>
<li>
<a href={WebUrl.developerDocs()}>{t("infoApiDocs")}</a>
</li>
</div>
</div>
</footer>
</div>
<div className="pt-4">
<hr />
</div>
<div className="d-flex flex-column flex-lg-row pt-2 align-items-center gap-2 gap-xl-4 flex-wrap">
<span className="flex-grow-1">
© 2023 FakeYou by{" "}
<a href="https://storyteller.ai" target="_blank" rel="noreferrer">
Storyteller.ai
</a>
</span>
<div className="d-flex flex-column flex-lg-row align-items-center mt-4 mt-lg-0">
{moderationLink}
</div>
{serverGitSha}
<div className="d-flex flex-column flex-lg-row align-items-center">
<GitSha prefix="FE: " />
</div>
</div>
</Container>
</footer>
);
}
@@ -1,8 +1,9 @@
import React from "react";
import { Link } from "react-router-dom";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { Container, Panel } from "components/common";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
interface Props {}
@@ -10,300 +11,195 @@ function AboutPage(props: Props) {
PosthogClient.recordPageview();
usePrefixedDocumentTitle("About Us");
const teamMembers = [
{
name: "Michael",
role: "ML / Backend",
imageSrc: "/images/team/michael.webp",
},
{
name: "Justin",
role: "ML",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Adrian",
role: "ML",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Scott",
role: "Technical 3D Artist",
imageSrc: "/images/team/scott.webp",
},
{
name: "Danny",
role: "3D Engine",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Brooks",
role: "3D Engine",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Andrei",
role: "3D Engine",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Kasisnu",
role: "Backend",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Madhukar",
role: "Infra",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Wil",
role: "Frontend",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Victor",
role: "Frontend",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Bombay",
role: "Frontend",
imageSrc: "/images/team/bombay.webp",
},
{
name: "Ishaan",
role: "Growth",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Evgenii",
role: "Gen AI Artist",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Steven",
role: "Gen AI Artist",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Willis",
role: "Gen AI Artist",
imageSrc: "/images/avatars/default-pfp.png",
},
{
name: "Jose",
role: "Data Team",
imageSrc: "/images/team/jose.webp",
},
{
name: "Rodrigo",
role: "Data Team",
imageSrc: "/images/team/rodrigo.webp",
},
];
return (
<div>
<div className="container mb-4">
<div className="row gx-3 flex-lg-row-reverse align-items-center">
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<img
src="/mascot/kitsune_pose3.webp"
className="img-fluid"
width="516"
height="444"
alt="FakeYou Kitsune Mascot!"
/>
</div>
<Container type="panel" className="mb-5">
<PageHeaderWithImage
title="About Us"
subText="We're building FakeYou as just one component of a broad set of production and creative tooling."
headerImage="/mascot/kitsune_pose3.webp"
yOffset="85%"
/>
<Panel padding={true} mb={true}>
<h2 className="mb-3 fw-bold">Lorem Ipsum</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</Panel>
<Panel padding={true} mb={true}>
<h2 className="mb-3 fw-bold">Our Mission</h2>
<p>
Our mission is to empower anyone to create full feature-length content
from home without institutional capital, large teams, huge amounts of
time, or deep reservoirs of highly specialized talent. We give
everyone their turn in the director’s seat and turn dreams into
physical form.
</p>
</Panel>
<Panel padding={true}>
<h2 className="mb-3 fw-bold">The Team</h2>
<div className="row g-4 g-md-5">
<div className="col-6 col-md-3 mb-0">
<img
src="/images/team/brandon.webp"
className="img-fluid img-team img-brandon rounded"
alt="Brandon Thomas"
/>
</div>
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
<div className="text-center text-lg-start">
<div>
<h1 className=" fw-bold lh-1 mb-4">A Glimpse of the Future</h1>
</div>
<div>
<p className="lead">
We're building FakeYou as just one component of a broad set of
production and creative tooling.
</p>
</div>
<div className="col-12 col-md-9 text-start d-flex flex-column justify-content-center">
<div className="p-3 px-0">
<p className="fw-semibold opacity-100 mb-0 fs-5">
Brandon Thomas
</p>
<p className="team-role-text">Founder and CEO</p>
<hr className="my-3 w-25 opacity-25" />
<p className="fw-normal mt-3 mb-0">
Brandon worked 8 years as a distributed systems and AI/ML
engineer at Square. He’s spent the last decade making indie
films and being plugged into the Atlanta art scene. In college
he built a{" "}
<a
href="https://www.youtube.com/watch?v=x034jVB1avs"
target="_blank"
rel="noreferrer"
className="text-red"
>
laser projector
</a>{" "}
and programmed it to play video games on the side of
skyscrapers. Today he’s working on disrupting Hollywood and the
music industry and transforming narrative storytelling into
something the likes of which we’ve never seen before.
</p>
</div>
</div>
</div>
</div>
<div className="container-panel pt-4 pb-5">
<div className="panel p-3 p-lg-4 load-hidden mt-5 mt-lg-0">
<h1 className="panel-title fw-bold">About FakeYou</h1>
<div className="py-6 d-flex flex-column gap-4">
<p>
Your brain was already capable of imagining things spoken in other
people's voices. This is a demonstration of how far computers have
caught up. One day computers will be able to bring all of the rich
and vivid imagery of your hopes and dreams to life. There's never
been a better time throughout all history to be a creative than
now.
</p>
<div>
<h2 className="mb-4">Technology disclosure</h2>
<p>
<em>
We'll be happy to remove any of the voices featured here for
any reason. See our <Link to="/terms">terms page</Link> for
more info.
</em>
</p>
<br />
<p>
&rdquo;Deep fakes&rdquo; are kind of like Photoshop when it
first came out. They're impressive, maybe a little bit scary,
but they're about to become the new norm. People will become
accustomed to the technology, and the results will be used
mostly for creative good, unlocking previously costly and
unattainable high production values for individual creators.
It's our belief that the next Hollywood will be <em>you</em>.
</p>
<br />
<p>
The technology to clone voices is already out in the open, and
the voices here are built by a community of contributors. We're
not the only website doing this, and plenty of people are
producing these same results on their own at home, independent
of our work. You can see thousands of examples on YouTube and
social media.
</p>
<br />
<p>
Even if the United States chooses to ban this technology,
institutions in China, Japan, Canada, and other countries all
over the world are rapidly conducting and publishing research on
this topic. People everywhere have access to this easy-to-use
technology. State actors probably have even more sophisiticated
versions available.
</p>
<br />
<p>
As an interesting point, most if not all of the voices produced
by this website are actually
<em> Linda Johnson</em> with a little bit layered on top.
</p>
<p>
<a
href="https://github.com/NVIDIA/tacotron2"
target="_blank"
rel="noopener noreferrer"
>
NVIDIA makes these tools publicly available for you to make
your own voice models
</a>
. Check it out!
</p>
</div>
<div>
<h2 className="mb-4">Voice Acting & Musician Jobs</h2>
<p>
If you're a voice actor or musician, we're looking to hire
talented performers to help us build commercial-friendly AI
voices. (The user-submitted models on FakeYou are just a
technology demonstration and may not be used commercially.) We
want to empower small creators (game designers, musicians, and
more) to voice, sing, and speak emotion into their work. Please
reach out to <code>jobs@storyteller.io</code> if you'd like to
work with us. We can pay a flat fee and/or royalty. Please get
in touch to learn more!
</p>
</div>
<div>
<h2 className="mb-4">FakeYou was previously Vocodes</h2>
<p>
FakeYou.com was previously known as{" "}
<strong>https://vo.codes</strong>, but has been rebranded to
better reflect its purpose and direction. (Not many people know
what a <em>vocoder</em> is anyway, so it didn't make sense to
keep the name.)
</p>
</div>
<div>
<h2 className="mb-4">Thanks</h2>
<p>
Thanks to the following individuals (in no particular order) for
help with data gathering and annotation, Discord moderation, ML
advice, etc.
</p>
<br />
<p>
Vegito1089, Shin, Ashurath, MakaveliGH, Blutarch Mann, Yahia,
Tim Squid, Seuneramet, Matt, Seth, CookiePPP,{" "}
<a
href="https://twitter.com/r9y9/"
target="_blank"
rel="noopener noreferrer"
>
@r9y9
</a>
, BFlat, Raisin.
</p>
<br />
<p>
The following papers, models, and resources were used:
<br />
<br />
<a
href="https://github.com/NVIDIA/tacotron2"
target="_blank"
rel="noopener noreferrer"
>
Tacotron2
</a>{" "}
(BSD-3 license),{" "}
<a
href="https://github.com/jaywalnut310/glow-tts"
target="_blank"
rel="noopener noreferrer"
>
glow-tts
</a>{" "}
(MIT license), {/* MIT */}
<a
href="https://github.com/seungwonpark/melgan"
target="_blank"
rel="noopener noreferrer"
>
MelGAN
</a>{" "}
(BSD-3 license), {/* BSD3 */}
<a
href="https://arxiv.org/pdf/2005.05106.pdf"
target="_blank"
rel="noopener noreferrer"
>
Multi-band MelGAN
</a>{" "}
(paper),{" "}
<a
href="https://arxiv.org/abs/2010.05646"
target="_blank"
rel="noopener noreferrer"
>
HiFi-GAN
</a>{" "}
(paper),{" "}
<a
href="https://github.com/Rudrabha/Wav2Lip"
target="_blank"
rel="noopener noreferrer"
>
Wav2Lip
</a>{" "}
(MIT license), {/* Non-commercial */}
<a
href="http://www.speech.cs.cmu.edu/cgi-bin/cmudict"
target="_blank"
rel="noopener noreferrer"
>
CMUDict
</a>{" "}
(BSD-2 license),{" "}
<a
href="https://keithito.com/LJ-Speech-Dataset/"
target="_blank"
rel="noopener noreferrer"
>
LJSpeech
</a>{" "}
(public domain),{" "}
<a
href="https://datashare.is.ed.ac.uk/handle/10283/3443"
target="_blank"
rel="noopener noreferrer"
>
VCTK
</a>{" "}
(CC BY 4.0).
</p>
<br />
<p>
Videos are generated by the amazing Wav2Lip system{" "}
<a
href="https://doi.org/10.1145/3394171.3413532"
target="_blank"
rel="noopener noreferrer"
>
(paper)
</a>
, by Prajwal{" "}
<a
href="https://twitter.com/prajwalkr14"
target="_blank"
rel="noopener noreferrer"
>
(@prajwalkr14)
</a>
, K R and Mukhopadhyay, Rudrabha and Namboodiri, Vinay P. and
Jawahar, C.V.
</p>
</div>
<div>
<h2 className="mb-4">Contact</h2>
<p>
Reach out to "echelon" on{" "}
<a
href="https://twitter.com/echelon"
target="_blank"
rel="noopener noreferrer"
>
Twitter
</a>
&nbsp;and Hacker News. Say hi.
</p>
<p>
The Storyteller Company (registered as Learning Machines, LLC)
</p>
</div>
<div>
<h2 className="mb-4">Built With</h2>
<div className="row gx-3 gx-lg-4">
<div className="col-4 col-md-3">
<img
className="rounded img-fluid"
src="/logos/pytorch.png"
alt="models are written in pytorch"
/>
<div className="row g-3 gx-lg-5 gy-md-5 pt-3 pt-md-5">
{teamMembers.map((member, index) => (
<div className="col-6 col-md-3" key={index}>
<Panel className="overflow-hidden panel-inner rounded h-100">
<img
src={member.imageSrc}
className="img-fluid img-team"
alt=""
/>
<div className="p-3">
<p className="fw-semibold opacity-100 mb-0 fs-5">
{member.name}
</p>
<p className="fs-7">{member.role}</p>
</div>
<div className="col-4 col-md-3">
<img
className="rounded img-fluid"
src="/logos/rust.png"
alt="core server components are written in Rust"
/>
</div>
<div className="col-4 col-md-3">
<img
className="rounded img-fluid"
src="/logos/kubernetes.png"
alt="the cluster scales with k8s"
/>
</div>
</div>
</Panel>
</div>
</div>
))}
</div>
</div>
</div>
</Panel>
</Container>
);
}
@@ -29,11 +29,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBarsStaggered,
faDeleteLeft,
faVolumeHigh,
} from "@fortawesome/free-solid-svg-icons";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { TtsPageHero } from "./TrumpTtsPageHero";
import { Analytics } from "../../../../../common/Analytics";
import {
GetComputedTtsCategoryAssignments,
@@ -47,6 +45,8 @@ import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocum
import { SearchOmnibar } from "./search/SearchOmnibar";
import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { faVolumeHigh } from "@fortawesome/pro-solid-svg-icons";
const PAGE_MODEL_TOKENS = new Set<string>([
"TM:pmd1wm3kf6az", // Development: "Fake Donald Trump #1"
@@ -164,7 +164,7 @@ function TrumpTtsPage(props: Props) {
// featuredModels[Math.floor(Math.random() * featuredModels.length)];
//}
let trumpModels = models.filter((model) => {
let trumpModels = models.filter(model => {
return PAGE_MODEL_TOKENS.has(model.model_token);
});
@@ -195,20 +195,19 @@ function TrumpTtsPage(props: Props) {
"SYNTHETIC_CATEGORY:TRENDING_MODELS";
let maybeLatestCategory = categories.find(
(category) => category.category_token === LATEST_MODELS_CATEGORY_TOKEN
category => category.category_token === LATEST_MODELS_CATEGORY_TOKEN
);
let maybeTrendingCategory = categories.find(
(category) => category.category_token === TRENDING_MODELS_CATEGORY_TOKEN
category => category.category_token === TRENDING_MODELS_CATEGORY_TOKEN
);
let otherCategories = categories
.filter(
(category) => category.category_token !== LATEST_MODELS_CATEGORY_TOKEN
category => category.category_token !== LATEST_MODELS_CATEGORY_TOKEN
)
.filter(
(category) =>
category.category_token !== TRENDING_MODELS_CATEGORY_TOKEN
category => category.category_token !== TRENDING_MODELS_CATEGORY_TOKEN
);
categories = [];
@@ -363,7 +362,7 @@ function TrumpTtsPage(props: Props) {
);
}
let trumpModels = props.ttsModels.filter((model) => {
let trumpModels = props.ttsModels.filter(model => {
return PAGE_MODEL_TOKENS.has(model.model_token);
});
@@ -380,9 +379,12 @@ function TrumpTtsPage(props: Props) {
return (
<div>
<TtsPageHero
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
<PageHeaderWithImage
headerImage="/mascot/trump.webp"
titleIcon={faVolumeHigh}
title="Donald Trump TTS"
subText="FakeYou has the very best Donald Trump AI voice on the internet. Use deep
fake Donald Trump to say your favorite memes."
/>
<div className="container-panel pb-5 mb-4">
@@ -1,81 +0,0 @@
import React, { useMemo } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { Link } from "react-router-dom";
import { t } from "i18next";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faStar,
faArrowRight,
faVolume,
} from "@fortawesome/pro-solid-svg-icons";
import { Analytics } from "../../../../../common/Analytics";
import { WebUrl } from "../../../../../common/WebUrl";
import { PageHeaderWithImage } from "../../../_common/PageHeaderWithImage";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export function TtsPageHero(props: Props) {
const randomImage = useMemo(() => {
const images = ["/mascot/trump.webp"];
return images[Math.floor(Math.random() * images.length)];
}, []);
let signUpButton = <></>;
let viewPricingButton = <></>;
signUpButton = (
<>
<Link to="/">
<button type="button" className="btn btn-primary w-100">
See 2000+ more voices
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</button>
</Link>
</>
);
viewPricingButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
onClick={() => {
Analytics.ttsClickHeroViewPricing();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("tts.TtsModelListPage.heroSection.buttons.viewPricing")}
</button>
</Link>
</>
);
const titleIcon = <FontAwesomeIcon icon={faVolume} className="me-3" />;
const title = <>Donald Trump TTS</>;
const subText = (
<>FakeYou has the very best Donald Trump AI voice on the internet. Use deep fake Donald Trump to say your favorite memes.</>
);
const actionButtons = (
<>
{signUpButton}
{viewPricingButton}
</>
);
return (
<>
<PageHeaderWithImage
headerImage={randomImage}
titleIcon={titleIcon}
title={title}
subText={subText}
showButtons={true}
actionButtons={actionButtons}
/>
</>
);
}
@@ -17,10 +17,12 @@ import { PageHeader } from "../../_common/PageHeader";
import {
faFileArrowUp,
faHandHoldingHeart,
faImage,
faMicrophone,
} from "@fortawesome/pro-solid-svg-icons";
import { faMicrophoneStand } from "@fortawesome/pro-duotone-svg-icons";
import { faWaveformLines } from "@fortawesome/pro-regular-svg-icons";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { Button } from "components/common";
interface Props {
sessionWrapper: SessionWrapper;
@@ -96,25 +98,39 @@ function ContributeIndexPage(props: Props) {
</p>
<div className="row gx-3 gy-3">
<div className="col-12 col-md-6">
<Link
<Button
icon={faMicrophone}
label="Upload Voice to Voice Weight"
to="/upload/voice_conversion"
className="btn btn-primary w-100"
>
<FontAwesomeIcon icon={faMicrophoneStand} className="me-3" />
Upload Voice to Voice Model
</Link>
/>
</div>
<div className="col-12 col-md-6">
<Link to="/upload/tts" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faVolumeHigh} className="me-3" />
{t("pages.contributeIndex.buttonUploadVoice")}
</Link>
<Button
icon={faVolumeHigh}
label={t("pages.contributeIndex.buttonUploadVoice")}
to="/upload/tts"
/>
</div>
<div className="col-12 col-md-6">
<Link to="/upload/vocoder" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faWaveformLines} className="me-3" />
Upload Vocoder
</Link>
<Button
icon={faWaveformLines}
label="Upload Vocoder"
to="/upload/vocoder"
/>
</div>
<div className="col-12 col-md-6">
<Button
icon={faImage}
label="Upload Stable Diffusion Weight"
to="/upload/sd"
/>
</div>
<div className="col-12 col-md-6">
<Button
icon={faImage}
label="Upload LoRa Weight"
to="/upload/lora"
/>
</div>
</div>
</div>
@@ -2,7 +2,7 @@ import React, { useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import MediaCards from "components/common/Card/MediaCards";
import { TempSelect } from "components/common";
import { Button, TempSelect } from "components/common";
import {
faArrowDownWideShort,
faFilter,
@@ -11,7 +11,7 @@ import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContex
import SkeletonCard from "components/common/Card/SkeletonCard";
import { ListMediaFiles } from "@storyteller/components/src/api/media_files/ListMediaFiles";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { useBookmarks, useLazyLists, useRatings } from "hooks";
import { useBookmarks, useLazyLists, useOnScreen, useRatings } from "hooks";
import InfiniteScroll from "react-infinite-scroll-component";
import prepFilter from "resources/prepFilter";
@@ -20,6 +20,9 @@ export default function MediaTab() {
const urlQueries = new URLSearchParams(search);
const bookmarks = useBookmarks();
const ratings = useRatings();
const toTopBtnRef = useRef<HTMLDivElement | null>(null);
const onScreen = useOnScreen(toTopBtnRef,"0px");
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [mediaType, mediaTypeSet] = useState(
urlQueries.get("filter_media_type") || "all"
@@ -82,7 +85,20 @@ export default function MediaTab() {
}}
/>
</div>
{ media.urlCursor ?
<Button {...{
className: `to-top-button`,
buttonRef: toTopBtnRef,
label: "Back to top",
onClick: () => media.reset(),
}}/> : null }
</div>
{ media.urlCursor && !onScreen ?
<Button {...{
className: `to-top-button-off-screen`,
label: "Back to top",
onClick: () => media.reset(),
}}/> : null }
<AudioPlayerProvider>
{media.isLoading && !media.list.length ? (
<div className="row gx-3 gy-3">
@@ -91,6 +107,7 @@ export default function MediaTab() {
))}
</div>
) : (
<InfiniteScroll
dataLength={media.list.length}
next={media.getMore}
@@ -2,7 +2,7 @@ import React, { useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import WeightsCards from "components/common/Card/WeightsCards";
import { TempSelect } from "components/common";
import { Button, TempSelect } from "components/common";
import {
faArrowDownWideShort,
faFilter,
@@ -12,7 +12,7 @@ import SkeletonCard from "components/common/Card/SkeletonCard";
import { ListWeights } from "@storyteller/components/src/api/weights/ListWeights";
import { Weight } from "@storyteller/components/src/api/weights/GetWeight";
import InfiniteScroll from "react-infinite-scroll-component";
import { useBookmarks, useLazyLists, useRatings } from "hooks";
import { useBookmarks, useLazyLists, useOnScreen, useRatings } from "hooks";
import prepFilter from "resources/prepFilter";
export default function WeightsTab() {
@@ -25,6 +25,8 @@ export default function WeightsTab() {
const [weightCategory, weightCategorySet] = useState(urlQueries.get("maybe_scoped_weight_category") || "all");
const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [list, listSet] = useState<Weight[]>([]);
const toTopBtnRef = useRef<HTMLDivElement | null>(null);
const onScreen = useOnScreen(toTopBtnRef,"0px");
const weights = useLazyLists({
addQueries: {
page_size: 24,
@@ -145,7 +147,20 @@ export default function WeightsTab() {
/>
)} */}
</div>
{ weights.urlCursor ?
<Button {...{
className: `to-top-button`,
buttonRef: toTopBtnRef,
label: "Back to top",
onClick: () => weights.reset(),
}}/> : null }
</div>
{ weights.urlCursor && !onScreen ?
<Button {...{
className: `to-top-button-off-screen`,
label: "Back to top",
onClick: () => weights.reset(),
}}/> : null }
<AudioPlayerProvider>
{weights.isLoading && !weights.list.length ? (
<div className="row gx-3 gy-3">
@@ -29,7 +29,13 @@ import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import { Analytics } from "common/Analytics";
import "./FaceAnimator.scss";
export default function FaceAnimator({ enqueueInferenceJob, sessionSubscriptionsWrapper, inferenceJobs, inferenceJobsByCategory, ...rest }: FaceAnimatorCore) {
export default function FaceAnimator({
enqueueInferenceJob,
sessionSubscriptionsWrapper,
inferenceJobs,
inferenceJobsByCategory,
...rest
}: FaceAnimatorCore) {
const { mediaToken } = useParams<{ mediaToken: string }>();
const { media: presetAudio } = useMedia({ mediaToken });
const { t } = useLocalize("FaceAnimator");
@@ -39,7 +45,8 @@ export default function FaceAnimator({ enqueueInferenceJob, sessionSubscription
const [imageReady, imageReadySet] = useState<boolean>(false);
const [audioReady, audioReadySet] = useState<boolean>(false);
const readyMedia = (m: number) => (t: boolean) => [imageReadySet, audioReadySet][m](t);
const readyMedia = (m: number) => (t: boolean) =>
[imageReadySet, audioReadySet][m](t);
const audioProps = useFile({}); // contains upload inout state and controls, see docs
const imageProps = useFile({}); // contains upload inout state and controls, see docs
const [index, indexSet] = useState<number>(0); // index = slideshow slide position
@@ -50,12 +57,15 @@ export default function FaceAnimator({ enqueueInferenceJob, sessionSubscription
const [disableFaceEnhancement, disableFaceEnhancementSet] = useState(false);
const [still, stillSet] = useState(false);
const [preferPresetAudio,preferPresetAudioSet] = useState(!!mediaToken);
const [preferPresetAudio, preferPresetAudioSet] = useState(!!mediaToken);
//const animationChange = ({ target }: any) => animationStyleSet(target.value);
const frameDimensionsChange = ({ target }: any) => frameDimensionsSet(target.value);
const removeWatermarkChange = ({ target }: any) => removeWatermarkSet(target.checked);
const disableFaceEnhancementChange = ({ target }: any) => disableFaceEnhancementSet(target.checked);
const frameDimensionsChange = ({ target }: any) =>
frameDimensionsSet(target.value);
const removeWatermarkChange = ({ target }: any) =>
removeWatermarkSet(target.checked);
const disableFaceEnhancementChange = ({ target }: any) =>
disableFaceEnhancementSet(target.checked);
const stillChange = ({ target }: any) => stillSet(target.checked);
const clearInputs = () => {
//animationStyleSet(0);
@@ -72,75 +82,89 @@ export default function FaceAnimator({ enqueueInferenceJob, sessionSubscription
type: mode ? "image" : "audio",
});
const upImageAndMerge = async (audio: any) => ({ audio, image: await UploadImage(makeRequest(1)) });
const MergeAndEnque = (res: any) => upImageAndMerge(res)
.then((responses) => {
if ("upload_token" in responses.image) {
indexSet(3); // set face animator API working page
// NB(bt,2023-12-08): We currently still have "media_files" and "media_uploads" as distinct backend record
// types (and APIs for each). One of our ongoing tasks is to phase out media_uploads entirely and make
// everything a media_file.
//
// Since the flow for this code is somewhat rigid, we're going to change the token type based on the token
// structure. Ordinarily this should **NEVER** be done, but hopefully this migration state is short enough
// that we won't need to worry about multiple token formats for long.
//
// Tokens starting with:
//
// m_* => media file tokens
// mu_* => media upload tokens
//
const audioIsMediaFile = responses.audio.upload_token.startsWith("m_");
const imageIsMediaFile = responses.image.upload_token.startsWith("m_");
let request : any = {
uuid_idempotency_token: uuidv4(),
audio_source: undefined,
image_source: undefined,
//audio_source: {
// maybe_media_upload_token: responses.audio.upload_token,
//},
//image_source: {
// maybe_media_upload_token: responses.image.upload_token,
//},
make_still: still,
disable_face_enhancement: disableFaceEnhancement,
remove_watermark: removeWatermark,
dimensions: frameDimensions,
};
if (audioIsMediaFile) {
request.audio_source = { maybe_media_file_token: responses.audio.upload_token };
} else {
request.audio_source = { maybe_media_upload_token: responses.audio.upload_token };
}
if (imageIsMediaFile) {
request.image_source = { maybe_media_file_token: responses.image.upload_token };
} else {
request.image_source = { maybe_media_upload_token: responses.image.upload_token };
}
return EnqueueFaceAnimation(request);
}
})
.then((res) => {
if (res && res.inference_job_token) {
enqueueInferenceJob(
res.inference_job_token,
FrontendInferenceJobType.FaceAnimation
);
indexSet(4); // set face animator API success page
}
})
.catch((e) => {
return { success: false }; // we can do more user facing error handling
const upImageAndMerge = async (audio: any) => ({
audio,
image: await UploadImage(makeRequest(1)),
});
const MergeAndEnque = (res: any) =>
upImageAndMerge(res)
.then(responses => {
if ("upload_token" in responses.image) {
indexSet(3); // set face animator API working page
// NB(bt,2023-12-08): We currently still have "media_files" and "media_uploads" as distinct backend record
// types (and APIs for each). One of our ongoing tasks is to phase out media_uploads entirely and make
// everything a media_file.
//
// Since the flow for this code is somewhat rigid, we're going to change the token type based on the token
// structure. Ordinarily this should **NEVER** be done, but hopefully this migration state is short enough
// that we won't need to worry about multiple token formats for long.
//
// Tokens starting with:
//
// m_* => media file tokens
// mu_* => media upload tokens
//
const audioIsMediaFile =
responses.audio.upload_token.startsWith("m_");
const imageIsMediaFile =
responses.image.upload_token.startsWith("m_");
let request: any = {
uuid_idempotency_token: uuidv4(),
audio_source: undefined,
image_source: undefined,
//audio_source: {
// maybe_media_upload_token: responses.audio.upload_token,
//},
//image_source: {
// maybe_media_upload_token: responses.image.upload_token,
//},
make_still: still,
disable_face_enhancement: disableFaceEnhancement,
remove_watermark: removeWatermark,
dimensions: frameDimensions,
};
if (audioIsMediaFile) {
request.audio_source = {
maybe_media_file_token: responses.audio.upload_token,
};
} else {
request.audio_source = {
maybe_media_upload_token: responses.audio.upload_token,
};
}
if (imageIsMediaFile) {
request.image_source = {
maybe_media_file_token: responses.image.upload_token,
};
} else {
request.image_source = {
maybe_media_upload_token: responses.image.upload_token,
};
}
return EnqueueFaceAnimation(request);
}
})
.then(res => {
if (res && res.inference_job_token) {
enqueueInferenceJob(
res.inference_job_token,
FrontendInferenceJobType.FaceAnimation
);
indexSet(4); // set face animator API success page
}
})
.catch(e => {
return { success: false }; // we can do more user facing error handling
});
const submit = async () => {
if (!presetAudio && !audioProps.file) return false
if (!presetAudio && !audioProps.file) return false;
indexSet(1); // set audio working page
@@ -148,16 +172,28 @@ export default function FaceAnimator({ enqueueInferenceJob, sessionSubscription
MergeAndEnque({ upload_token: mediaToken }); // if there is a media token then we enque this like a "fake" audio/media response
} else {
UploadAudio(makeRequest(0)) // if there an audio file it uploads here
.then((res) => {
if ("upload_token" in res) {
indexSet(2); // set image working page
}
return MergeAndEnque(res); // start image upload, then combine both responses into an enqueue request
});
.then(res => {
if ("upload_token" in res) {
indexSet(2); // set image working page
}
return MergeAndEnque(res); // start image upload, then combine both responses into an enqueue request
});
}
};
const page = index === 0 ? 0 : index === 4 ? 2 : 1;
const headerProps = { audioProps, audioReady, clearInputs, imageProps, imageReady, indexSet, page, presetAudio, preferPresetAudio, submit, t };
const headerProps = {
audioProps,
audioReady,
clearInputs,
imageProps,
imageReady,
indexSet,
page,
presetAudio,
preferPresetAudio,
submit,
t,
};
const transitions = useTransition(index, {
...springs.soft,
@@ -168,51 +204,59 @@ export default function FaceAnimator({ enqueueInferenceJob, sessionSubscription
const failures = (fail = "") => {
switch (fail) {
case "face_not_detected": return "Face not detected, try another picture";
default: return "Uknown failure";
case "face_not_detected":
return "Face not detected, try another picture";
default:
return "Uknown failure";
}
};
return <div {...{ className: "face-animator-container container-panel pt-4" }}>
<div {...{ className: "panel face-animator-main" }}>
<FaceAnimatorTitle {...headerProps} />
{transitions((style, i) => {
const Page = FaceAnimatorSubViews[page];
return Page ? <Page
{...{
audioProps,
imageProps,
frameDimensions,
frameDimensionsChange,
disableFaceEnhancement,
disableFaceEnhancementChange,
enqueueInferenceJob,
preferPresetAudio,
preferPresetAudioSet,
presetAudio,
still,
stillChange,
sessionSubscriptionsWrapper,
index,
t,
toggle: { audio: readyMedia(1), image: readyMedia(0) },
style,
removeWatermark,
removeWatermarkChange,
}}
/> : null
})}
</div>
<InferenceJobsList {...{
failures,
onSelect: () => Analytics.voiceConversionClickDownload(),
jobType: FrontendInferenceJobType.FaceAnimation,
}}/>
<div className="face-animator-mobile-sample">
<BasicVideo
title="Face Animator Sample"
src="/videos/face-animator-instruction-en.mp4"
return (
<div {...{ className: "face-animator-container container-panel pt-4" }}>
<div {...{ className: "panel face-animator-main" }}>
<FaceAnimatorTitle {...headerProps} />
{transitions((style, i) => {
const Page = FaceAnimatorSubViews[page];
return Page ? (
<Page
{...{
audioProps,
imageProps,
frameDimensions,
frameDimensionsChange,
disableFaceEnhancement,
disableFaceEnhancementChange,
enqueueInferenceJob,
preferPresetAudio,
preferPresetAudioSet,
presetAudio,
still,
stillChange,
sessionSubscriptionsWrapper,
index,
t,
toggle: { audio: readyMedia(1), image: readyMedia(0) },
style,
removeWatermark,
removeWatermarkChange,
}}
/>
) : null;
})}
</div>
<InferenceJobsList
{...{
failures,
onSelect: () => Analytics.voiceConversionClickDownload(),
jobType: FrontendInferenceJobType.FaceAnimation,
}}
/>
<div className="face-animator-mobile-sample">
<BasicVideo
title="Face Animator Sample"
src="/videos/face-animator-instruction-en.mp4"
/>
</div>
</div>
</div>;
);
}
@@ -0,0 +1,209 @@
import {
faArrowRightArrowLeft,
faFile,
faTrashAlt,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Container, Panel } from "components/common";
import FileInput from "components/common/FileInput";
import PageHeader from "components/layout/PageHeader";
import { useFile, useInferenceJobs, useMedia } from "hooks";
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import { UploadMedia } from "@storyteller/components/src/api/media_files/UploadMedia";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import FbxToGltfJobList from "./components/FbxToGltfJobList";
import {
EnqueueFbxToGltf,
EnqueueFbxToGltfIsSuccess,
EnqueueFbxToGltfIsError,
} from "@storyteller/components/src/api/file_conversion/EnqueueFbxToGltf";
interface FbxToGltfPageProps {
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
}
export default function FbxToGltfPage({
enqueueInferenceJob,
}: FbxToGltfPageProps) {
const history = useHistory();
const { mediaToken: mediaTokenParam } = useParams<{ mediaToken: string }>();
const [mediaToken, setMediaToken] = useState<string | null>(mediaTokenParam);
const { media: presetFile } = useMedia({
mediaToken: mediaToken ?? undefined,
});
const fileProps = useFile({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isWorking, setIsWorking] = useState(false);
const { inferenceJobs } = useInferenceJobs(
FrontendInferenceJobType.ConvertFbxtoGltf
);
const hasConversionJobs = inferenceJobs && inferenceJobs.length > 0;
console.log("inferenceJobs", inferenceJobs);
useEffect(() => {
// If there's a mediaToken, automatically prepare the file for conversion
if (mediaToken && presetFile) {
}
}, [mediaToken, presetFile]);
const clearMediaToken = () => {
// Reset the media token state
setMediaToken(null);
history.push("/fbx-to-gltf");
};
const makeFBXUploadRequest = () => ({
uuid_idempotency_token: uuidv4(),
file: fileProps.file,
source: "file",
type: "fbx/model",
});
const handleUploadFBX = async () => {
try {
const res = await UploadMedia(makeFBXUploadRequest());
if (res.success && res.media_file_token) {
return { upload_token: res.media_file_token };
}
console.error("Upload failed:", res);
return null;
} catch (error) {
console.error("Error in upload:", error);
return null;
}
};
const submit = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
setIsWorking(true);
try {
let uploadToken = mediaToken;
// If no media token, upload the file first
if (!mediaToken && fileProps.file) {
const uploadResponse = await handleUploadFBX();
if (uploadResponse && uploadResponse.upload_token) {
uploadToken = uploadResponse.upload_token;
} else {
console.error("Failed to upload file. Response:", uploadResponse);
throw new Error("File upload failed");
}
}
// Enqueue the conversion job
if (uploadToken) {
await EnqueueConvert({ upload_token: uploadToken });
} else {
console.error("No upload token available for conversion");
}
} catch (error) {
console.error("Error in submit process: ", error);
} finally {
setIsWorking(false);
setIsSubmitting(false);
}
};
const EnqueueConvert = async ({ upload_token }: any) => {
if (!upload_token) return false;
try {
let request = {
uuid_idempotency_token: uuidv4(),
file_source: undefined,
media_file_token: upload_token,
};
const response = await EnqueueFbxToGltf(request);
if (EnqueueFbxToGltfIsSuccess(response)) {
console.log("Enqueue successful");
if (response && response.inference_job_token) {
enqueueInferenceJob(
response.inference_job_token,
FrontendInferenceJobType.ConvertFbxtoGltf
);
}
return true;
} else if (EnqueueFbxToGltfIsError(response)) {
throw new Error("Enqueue failed");
}
} catch (error) {
console.error("Error in enqueueing conversion: ", error);
return false;
}
};
return (
<Container type="panel">
<PageHeader
title="Convert FBX to glTF"
subText="For converting 3D model assets on FBX format to glTF 2.0 for use with Storyteller Studio."
imageUrl="/images/header/fbx-to-gltf.png"
/>
{hasConversionJobs && (
<div className="mb-4">
<FbxToGltfJobList />
</div>
)}
<Panel padding={true}>
<div className="d-flex flex-column gap-3">
{mediaToken && presetFile ? (
<div>
<label className="sub-title">FBX file from media</label>
<Panel className="panel-inner p-3 rounded">
<div className="d-flex gap-3 align-items-center flex-wrap">
<div className="d-flex gap-3 flex-grow-1 align-items-center">
<FontAwesomeIcon icon={faFile} className="display-6" />
<div>
<h6 className="mb-1">{presetFile.token}</h6>
<p className="opacity-75">
Created by {presetFile.maybe_creator_user?.display_name}
</p>
</div>
</div>
<Button
icon={faTrashAlt}
square={true}
onClick={clearMediaToken}
variant="danger"
small={true}
tooltip="Remove file"
/>
</div>
</Panel>
</div>
) : (
<FileInput
{...fileProps}
label="Select FBX File"
fileTypes={["FBX"]}
mediaToken={mediaToken}
/>
)}
<div className="d-flex justify-content-end">
<Button
icon={faArrowRightArrowLeft}
label="Convert to glTF"
onClick={submit}
disabled={!mediaToken && !fileProps.file}
isLoading={isWorking}
/>
</div>
</div>
</Panel>
</Container>
);
}
@@ -0,0 +1,26 @@
import React from "react";
import { Analytics } from "common/Analytics";
import InferenceJobsList from "components/layout/InferenceJobsList";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
export default function FbxToGltfJobList() {
const failures = (fail = "") => {
switch (fail) {
case "sample case":
return "Sample Case, this should not have been shown";
default:
return "Unknown failure";
}
};
return (
<InferenceJobsList
{...{
failures,
onSelect: () => Analytics.voiceConversionClickDownload(),
jobType: FrontendInferenceJobType.ConvertFbxtoGltf,
}}
/>
);
}
@@ -1,8 +1,6 @@
import React, { useState, useEffect } from "react";
import { faVolume } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import { PageHeaderWithImage } from "v2/view/_common/PageHeaderWithImage";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { Panel } from "v2/view/_common/Panel";
import { motion } from "framer-motion";
import { container } from "data/animation";
@@ -55,10 +53,8 @@ function GenerateSpeechPage() {
<motion.div initial="hidden" animate="visible" variants={container}>
<PageHeaderWithImage
headerImage="/mascot/kitsune_pose2.webp"
titleIcon={<FontAwesomeIcon icon={faVolume} className="me-3" />}
title={<>Generate Speech</>}
subText={<>Generate text to speech with your favorites characters</>}
showButtons={false}
/>
<Panel padding>
@@ -18,7 +18,6 @@ import {
faSparkles,
} from "@fortawesome/pro-solid-svg-icons";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { FooterNav } from "v2/view/nav/FooterNav";
import Alert from "components/common/Alert/Alert";
// import { Analytics } from "../../../../../common/Analytics";
@@ -559,8 +558,6 @@ function LandingPage(props: Props) {
/>
</div>
</div>
<FooterNav sessionWrapper={props.sessionWrapper} />
</div>
);
}
@@ -15,6 +15,8 @@ import {
faArrowDownToLine,
faSquareQuote,
faLink,
faFileCircleXmark,
faArrowRightArrowLeft,
} from "@fortawesome/pro-solid-svg-icons";
import Accordion from "components/common/Accordion";
import DataTable from "components/common/DataTable";
@@ -94,7 +96,7 @@ export default function MediaPage() {
sdMediaImage = bucketConfig.getGcsUrl(mediaFile.public_bucket_path);
}
return <SdCoverImagePanel src={sdMediaImage} />;
case MediaFileType.Mocap:
case MediaFileType.BVH:
const bvhUrl = bucketConfig.getGcsUrl(mediaFile.public_bucket_path);
return (
<Iframe
@@ -104,15 +106,45 @@ export default function MediaPage() {
}}
/>
);
case MediaFileType.GLTF:
const gltfUrl = bucketConfig.getGcsUrl(mediaFile.public_bucket_path);
return (
<Iframe
{...{
url: `https://engine.fakeyou.com?mode=viewer&gltf=${gltfUrl}`,
className: "fy-studio-frame",
}}
/>
);
case MediaFileType.FBX:
return (
<Panel padding={true}>
<div className="d-flex flex-column p-4 gap-3 text-center align-items-center">
<FontAwesomeIcon
icon={faFileCircleXmark}
className="display-5 mb-2"
/>
<h2 className="fw-semibold">FBX file not supported</h2>
<div className="d-flex gap-2">
<Button
icon={faArrowRightArrowLeft}
label="Convert FBX to glTF"
to={`/fbx-to-gltf/${mediaFile.token}`}
variant="primary"
/>
</div>
</div>
</Panel>
);
default:
return <div>Unsupported media type</div>;
}
}
const weightTypeInfo = useMediaFileTypeInfo(
const mediaTypeInfo = useMediaFileTypeInfo(
mediaFile?.media_type || MediaFileType.None
);
const { label: mediaType, color: mediaTagColor } = weightTypeInfo;
const { label: mediaType, color: mediaTagColor } = mediaTypeInfo;
let audioLink = new BucketConfig().getGcsUrl(mediaFile?.public_bucket_path);
@@ -228,20 +260,56 @@ export default function MediaPage() {
const videoDetails = [
{ property: "Type", value: mediaFile?.media_type || "" },
{ property: "Created at", value: dateCreated || "" },
{
property: "Visibility",
value: mediaFile?.creator_set_visibility.toString() || "",
},
{ property: "Created at", value: dateCreated || "" },
];
const imageDetails = [
{ property: "Type", value: mediaFile?.media_type || "" },
{ property: "Created at", value: dateCreated || "" },
{
property: "Visibility",
value: mediaFile?.creator_set_visibility.toString() || "",
},
{ property: "Created at", value: dateCreated || "" },
];
const bvhDetails = [
{
property: "Type",
value: mediaType || "",
},
{
property: "Visibility",
value: mediaFile?.creator_set_visibility.toString() || "",
},
{ property: "Created at", value: dateCreated || "" },
];
const gltfDetails = [
{
property: "Type",
value: mediaType || "",
},
{
property: "Visibility",
value: mediaFile?.creator_set_visibility.toString() || "",
},
{ property: "Created at", value: dateCreated || "" },
];
const fbxDetails = [
{
property: "Type",
value: mediaType || "",
},
{
property: "Visibility",
value: mediaFile?.creator_set_visibility.toString() || "",
},
{ property: "Created at", value: dateCreated || "" },
];
let mediaDetails;
@@ -256,6 +324,15 @@ export default function MediaPage() {
case MediaFileType.Image:
mediaDetails = imageDetails;
break;
case MediaFileType.BVH:
mediaDetails = bvhDetails;
break;
case MediaFileType.GLTF:
mediaDetails = gltfDetails;
break;
case MediaFileType.FBX:
mediaDetails = fbxDetails;
break;
default:
}
@@ -0,0 +1,56 @@
import React, {useReducer } from 'react';
import { useLocalize } from "hooks";
import { FrontendInferenceJobType, InferenceJob } from '@storyteller/components/src/jobs/InferenceJob';
import { Container, ErrorMessage } from "components/common";
import PageHeader from "components/layout/PageHeader";
import PageVideoProvision from './components/pageVideoProvision';
import PageFilterControls from './components/pageFilterControls';
import { states, reducer } from "./storytellerFilterReducer";
export default function StorytellerFilter(props:{
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}){
const debug=true;
const { t } = useLocalize("StorytellerFilter");
const { NO_FILE, FILE_STAGED, FILE_UPLOADING, FILE_UPLOADED, FILE_SELECTED, FILE_LOADING, FILE_LOADED } = states;
const [pageState, dispatchPageState] = useReducer(reducer, {
status: NO_FILE,
});
const pagePicker = () => {
switch (pageState.status){
case NO_FILE:
case FILE_STAGED:
case FILE_UPLOADING:
case FILE_SELECTED:
return <PageVideoProvision {...{debug, t, pageState, dispatchPageState}}/>
case FILE_UPLOADED:
case FILE_LOADING:
case FILE_LOADED:
return <PageFilterControls {...{debug, t, pageState, dispatchPageState}}/>
default:
return <ErrorMessage/>
}
}
const currPage = pagePicker();
return(
<Container type="panel" className="mb-5">
<PageHeader
title={t("headings.title")}
subText={t("headings.subtitle")}
// imageUrl="/images/header/video-mocap.png"
/>
{currPage}
</Container>
);
}
@@ -0,0 +1,54 @@
import React from "react";
import { useMedia } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { states, Action, State } from "../storytellerFilterReducer";
import { BasicVideo, ErrorMessage, Panel, Spinner } from "components/common";
export default function PageFilterControls({
debug=false, t, pageState, dispatchPageState
}: {
debug?: boolean;
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
useMedia({
mediaToken: pageState.mediaFileToken,
onSuccess: (res: any) => {
// ratings.gather({ res, key: "token" });
console.log(res)
dispatchPageState({
type: 'loadFileSuccess',
payload: {mediaFile: res}
})
},
});
if (pageState.mediaFile){
const mediaLink = new BucketConfig().getGcsUrl(pageState.mediaFile.public_bucket_path);
if (mediaLink)
return(
<Panel>
<div className="row g-3 p-3">
{/* <h1>{t("message.fileUploaded")}</h1> */}
{debug && <p>{`File Token: ${pageState.mediaFileToken}`}</p> }
<div className="col-6">
<BasicVideo src={mediaLink} />
</div>
<div className="col-6">
<BasicVideo src={mediaLink} />
</div>
</div>
</Panel>
);
}else if (pageState.status <= states.FILE_LOADING){
return (
<Panel>
{debug && <p>{`File Token: ${pageState.mediaFileToken}`}</p> }
<p>Loading Files</p>
<Spinner />
</Panel>
);
}
return <ErrorMessage />;
}
@@ -0,0 +1,60 @@
import React from "react";
import { Redirect, useLocation } from "react-router-dom";
import { BasicVideo, Panel, Tabs } from "components/common";
import { Action, State } from "../storytellerFilterReducer";
import TabContentUpload from "./tabContentUpload";
import TabContentLibrary from "./tabContentLibrary";
export default function PageVideoProvision({
debug=false, t, pageState, dispatchPageState
}: {
debug?: boolean;
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const tabs = [
{
label: t("tabTitle.upload"),
content: <TabContentUpload {...{
debug, t, pageState, dispatchPageState
}} />,
to: "/storyteller-filter/upload",
padding: true,
},
{
label: t("tabTitle.library"),
content: <TabContentLibrary {...{
debug, t, pageState, dispatchPageState
}} />,
to: "/storyteller-filter/select-media",
padding: true,
},
];
//pick default tab using pathname
const { pathname } = useLocation();
if (pathname === "/storyteller-filter" || pathname === "/storyteller-filter/") {
return <Redirect to={"/storyteller-filter/upload"} />;
}
return(
<Panel>
<div className="row g-0">
<div className="col-12 col-md-6">
<Tabs tabs={tabs} />
</div>
<div className="col-12 col-md-6">
<Panel padding={true} clear={true}>
<BasicVideo
title={t("video.sample")}
src="/videos/face-animator-instruction-en.mp4"
/>
</Panel>
</div>
</div>
</Panel>
);
}
@@ -0,0 +1,46 @@
import SelectModal from "components/common/SelectModal/SelectModal";
import React from "react";
import { states, Action, State } from "../storytellerFilterReducer";
import { Button } from "components/common";
export default function TabContentLibrary({
t, pageState, dispatchPageState
}: {
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}){
const handleProceed = ()=>{
dispatchPageState({type: "loadFile"})
}
const handleOnSelect = (token:string)=>{
dispatchPageState({
type: "selectedFile",
payload: {mediaFileToken: token}
});
};
return (
<div>
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
tabs={[
{
label: "All Videos",
tabKey: "allVideos",
mediaTypeFilter: "video",
searcher: false,
type: "media",
},
]}
/>
{pageState.status === states.FILE_SELECTED &&
<Button
label={t("button.proceed")}
onClick={handleProceed}
/>
}
</div>
);
}
@@ -0,0 +1,90 @@
import React from "react";
import { v4 as uuidv4 } from "uuid";
import { UploadMedia } from "@storyteller/components/src/api/media_files/UploadMedia";
import { useFile } from "hooks";
import { ErrorMessage, VideoInput, Spinner } from "components/common";
import { states, Action, State } from "../storytellerFilterReducer";
export default function TabContentUpload({
debug=false, t, pageState, dispatchPageState
}: {
debug?: boolean;
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const videoProps = useFile({});
const {NO_FILE, FILE_STAGED, FILE_UPLOADING} = states;
const makeVideoUploadRequest = () => ({
uuid_idempotency_token: uuidv4(),
file: videoProps.file,
source: "file",
type: "video",
});
const handleUploadVideo = () => {
dispatchPageState({type: 'uploadFile'});
UploadMedia(makeVideoUploadRequest()).then(res => {
if (res.success && res.media_file_token) {
dispatchPageState({
type: 'uploadFileSuccess',
payload:{
mediaFileToken :res.media_file_token
}
})
}
});
};
// contains upload inout state and controls, see docs
if (pageState.status === NO_FILE || pageState.status === FILE_STAGED) {
return (
<>
<div className="row">
<div className="col-12">
<VideoInput
{...{
t,
...videoProps,
onStateChange: () =>{
if (pageState.status === NO_FILE && videoProps.file)
dispatchPageState({type: "stagedFile"})
else if (pageState.status === FILE_STAGED && !videoProps.file)
dispatchPageState({type: "clearedFile"})
}
}}
/>
</div>
</div>
<div className="row py-3">
<div className="col-12">
<div className="d-flex justify-content-end gap-3">
<button
className="btn btn-primary"
disabled={pageState.status !== FILE_STAGED}
onClick={handleUploadVideo}
>
{t("button.upload")}
</button>
</div>
</div>
</div>
</>
);
}else if (pageState.status === FILE_UPLOADING){
return(
<div className="row">
<div className="col-12">
<h1>{t("message.fileUploading")}</h1>
</div>
<div className="col-12">
<Spinner />
</div>
</div>
);
}
return <ErrorMessage />;
}
@@ -0,0 +1 @@
export { default } from './StorytellerFilter';
@@ -0,0 +1,64 @@
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
export enum states{
NO_FILE,
FILE_STAGED,
FILE_SELECTED,
FILE_UPLOADING,
FILE_UPLOADED,
FILE_LOADING,
FILE_LOADED,
}
export type State = {
status: number;
mediaFileToken?: string;
mediaFile?: MediaFile;
inferenceJobToken?: string;
}
export type Action =
| {type: 'stagedFile'}
| {type: 'clearedFile'}
| {type: 'selectedFile', payload: { mediaFileToken: string}}
| {type: 'uploadFile'}
| {type: 'uploadFileSuccess', payload:{ mediaFileToken: string}}
| {type: 'loadFile'}
| {type: 'loadFileSuccess', payload:{mediaFile: MediaFile}}
export function reducer (state: State, action: Action): State {
console.log(action);
switch(action.type){
case 'stagedFile':
return {...state, status: states.FILE_STAGED}
case 'clearedFile':
return {...state, status: states.NO_FILE}
case 'selectedFile':
return {
...state,
status: states.FILE_SELECTED,
mediaFileToken: action.payload.mediaFileToken
}
case 'uploadFile':
return {...state,status: states.FILE_UPLOADING};
case 'uploadFileSuccess':
return {
...state,
status: states.FILE_UPLOADED,
mediaFileToken: action.payload.mediaFileToken
}
case 'loadFile':
return {...state, status: states.FILE_LOADING}
case 'loadFileSuccess':
console.log("LOAD FILE SUCCESS");
return {
...state,
status: states.FILE_LOADED,
mediaFile: action.payload.mediaFile
}
default:
return {status: states.NO_FILE};
}
}
@@ -1,9 +1,10 @@
import React from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import Iframe from "react-iframe";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SsPageHero } from "./components/SsPageHero";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { Container, Panel } from "components/common";
import { faCameraMovie } from "@fortawesome/pro-solid-svg-icons";
interface Props {
sessionWrapper: SessionWrapper;
@@ -12,19 +13,21 @@ interface Props {
function StorytellerStudioListPage(props: Props) {
return (
<div>
<SsPageHero
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
<Container type="panel">
<PageHeaderWithImage
headerImage="mascot/may4th.webp"
titleIcon={faCameraMovie}
title="Storyteller Studio"
subText="Anyone can make films."
/>
<div className="container mb-5">
<Panel className="overflow-hidden">
<Iframe
url="https://engine.fakeyou.com?mode=studio"
width="100%"
height="900px"
id=""
className=""
className="rounded"
display="block"
position="relative"
/>
@@ -38,8 +41,8 @@ function StorytellerStudioListPage(props: Props) {
See details <FontAwesomeIcon icon={faArrowRight} className="ms-1" />
</Link>
</div> */}
</div>
</div>
</Panel>
</Container>
);
}
@@ -1,131 +0,0 @@
import React, { useMemo } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faStar,
faUser,
faArrowRight,
faMicrophoneStand,
} from "@fortawesome/pro-solid-svg-icons";
import { Analytics } from "../../../../../../common/Analytics";
import { WebUrl } from "../../../../../../common/WebUrl";
import { PageHeaderWithImage } from "../../../../_common/PageHeaderWithImage";
import { useLocalize } from "hooks";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export function SsPageHero(props: Props) {
const { t } = useLocalize("VcModelListPage");
const randomImage = useMemo(() => {
const images = ["mascot/may4th.webp"];
return images[Math.floor(Math.random() * images.length)];
}, []);
let signUpButton = <></>;
let viewPricingButton = <></>;
let upgradeButton = <></>;
let myProfileButton = <></>;
if (!props.sessionWrapper.isLoggedIn()) {
signUpButton = (
<>
<Link
to="/signup"
onClick={() => {
Analytics.ttsClickHeroSignup();
}}
>
<button type="button" className="btn btn-primary w-100">
{t("heroButtonSignUp")}
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</button>
</Link>
</>
);
viewPricingButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
onClick={() => {
Analytics.ttsClickHeroViewPricing();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonPricing")}
</button>
</Link>
</>
);
}
if (props.sessionWrapper.isLoggedIn()) {
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
let url = WebUrl.userProfilePage(displayName);
myProfileButton = (
<>
<Link
to={url}
onClick={() => {
Analytics.ttsClickHeroViewProfile();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faUser} className="me-2" />
{t("heroButtonProfile")}
</button>
</Link>
</>
);
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
upgradeButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
onClick={() => {
Analytics.ttsClickHeroUpgradePlan();
}}
>
<button type="button" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonUpgradePlan")}
</button>
</Link>
</>
);
}
}
const titleIcon = (
<FontAwesomeIcon icon={faMicrophoneStand} className="me-3 me-lg-4" />
);
const title = <>Storyteller Studio</>;
const subText = <>Anyone can make films.</>;
const actionButtons = (
<>
{upgradeButton}
{signUpButton}
{viewPricingButton}
{myProfileButton}
</>
);
return (
<>
<PageHeaderWithImage
headerImage={randomImage}
titleIcon={titleIcon}
title={title}
subText={subText}
showButtons={true}
actionButtons={actionButtons}
/>
</>
);
}
@@ -0,0 +1,51 @@
import { Container } from "components/common";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import React from "react";
import SdInferencePanel from "../weight/inference_panels/SdInferencePanel";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { faMessageImage } from "@fortawesome/pro-solid-svg-icons";
import TextToImageJobsList from "./components/TextToImageJobsList";
import { useInferenceJobs } from "hooks";
interface TextToImagePageProps {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
}
export default function TextToImagePage({
sessionSubscriptionsWrapper,
enqueueInferenceJob,
}: TextToImagePageProps) {
const { inferenceJobs } = useInferenceJobs(
FrontendInferenceJobType.ImageGeneration
);
const hasImageGenJobs = inferenceJobs && inferenceJobs.length > 0;
return (
<Container type="panel">
<PageHeaderWithImage
headerImage="/mascot/text-to-image.webp"
titleIcon={faMessageImage}
title="Text to Image"
subText="Transform your thoughts into art."
yOffset="76%"
/>
{hasImageGenJobs && (
<div className="mb-4">
<TextToImageJobsList />
</div>
)}
<SdInferencePanel
sessionSubscriptionsWrapper={sessionSubscriptionsWrapper}
enqueueInferenceJob={enqueueInferenceJob}
isStandalone={true}
/>
</Container>
);
}
@@ -0,0 +1,26 @@
import React from "react";
import { Analytics } from "common/Analytics";
import InferenceJobsList from "components/layout/InferenceJobsList";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
export default function TextToImageJobsList() {
const failures = (fail = "") => {
switch (fail) {
case "sample case":
return "Sample Case, this should not have been shown";
default:
return "Unknown failure";
}
};
return (
<InferenceJobsList
{...{
failures,
onSelect: () => Analytics.voiceConversionClickDownload(),
jobType: FrontendInferenceJobType.ImageGeneration,
}}
/>
);
}
@@ -46,7 +46,6 @@ import {
// import { GenericNotice } from "./notices/GenericNotice";
// import { DiscordLink2 } from "@storyteller/components/src/elements/DiscordLink2";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { TtsPageHero } from "./TtsPageHero";
import { Analytics } from "../../../../../common/Analytics";
import {
GetComputedTtsCategoryAssignments,
@@ -67,6 +66,9 @@ import { RatingStats } from "../../../_common/ratings/RatingStats";
import { SearchOmnibar } from "./search/SearchOmnibar";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { useLocalize } from "hooks";
import { Container, Panel } from "components/common";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { faMessageDots } from "@fortawesome/pro-solid-svg-icons";
export interface EnqueueJobResponsePayload {
success: boolean;
@@ -194,7 +196,7 @@ function TtsModelListPage(props: Props) {
setTtsModels(models);
if (!maybeSelectedTtsModel && models.length > 0) {
let model = models[0];
const featuredModels = models.filter((m) => m.is_front_page_featured);
const featuredModels = models.filter(m => m.is_front_page_featured);
if (featuredModels.length > 0) {
// Random featured model
model =
@@ -225,20 +227,19 @@ function TtsModelListPage(props: Props) {
"SYNTHETIC_CATEGORY:TRENDING_MODELS";
let maybeLatestCategory = categories.find(
(category) => category.category_token === LATEST_MODELS_CATEGORY_TOKEN
category => category.category_token === LATEST_MODELS_CATEGORY_TOKEN
);
let maybeTrendingCategory = categories.find(
(category) => category.category_token === TRENDING_MODELS_CATEGORY_TOKEN
category => category.category_token === TRENDING_MODELS_CATEGORY_TOKEN
);
let otherCategories = categories
.filter(
(category) => category.category_token !== LATEST_MODELS_CATEGORY_TOKEN
category => category.category_token !== LATEST_MODELS_CATEGORY_TOKEN
)
.filter(
(category) =>
category.category_token !== TRENDING_MODELS_CATEGORY_TOKEN
category => category.category_token !== TRENDING_MODELS_CATEGORY_TOKEN
);
categories = [];
@@ -499,7 +500,7 @@ function TtsModelListPage(props: Props) {
}
return (
<div>
<Container type="panel">
{/* {bootstrapLanguageNotice} */}
{pleaseFollowNotice}
@@ -510,131 +511,127 @@ function TtsModelListPage(props: Props) {
{twitchTtsNotice}
<TtsPageHero
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
<PageHeaderWithImage
headerImage="/mascot/kitsune_pose2.webp"
titleIcon={faMessageDots}
title={t("heroTitle")}
subText={t("heroText")}
yOffset="65%"
/>
<div className="container-panel pb-5 mb-4">
<div className="panel p-3 py-4 p-md-4">
<i className="fas fa-volume-high"></i>
<Panel padding={true}>
<div className="d-flex gap-4">
<form
className="w-100 d-flex flex-column"
onSubmit={handleFormSubmit}
>
{/* Explore Rollout */}
<div className="d-flex gap-4">
<form
className="w-100 d-flex flex-column"
onSubmit={handleFormSubmit}
>
{/* Explore Rollout */}
<SearchOmnibar
allTtsCategories={props.allTtsCategories}
allTtsModels={props.ttsModels}
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={props.setSelectedTtsLanguageScope}
/>
<SearchOmnibar
allTtsCategories={props.allTtsCategories}
allTtsModels={props.ttsModels}
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={props.setSelectedTtsLanguageScope}
/>
{/*
{/*
EXPLORE OMNIBAR GOES HERE TODO
*/}
{directViewLink}
{directViewLink}
<div className="row gx-5 gy-5">
<div className="col-12 col-lg-6 d-flex flex-column gap-3">
<div className="d-flex flex-column gap-3 h-100">
<div className="d-flex gap-2">
<label className="sub-title pb-0">
{t("ttsTextInputLabel")}
</label>
{/*<a href="/" className="ms-1">
<div className="row gx-5 gy-5">
<div className="col-12 col-lg-6 d-flex flex-column gap-3">
<div className="d-flex flex-column gap-3 h-100">
<div className="d-flex gap-2">
<label className="sub-title pb-0">
{t("ttsTextInputLabel")}
</label>
{/*<a href="/" className="ms-1">
<FontAwesomeIcon icon={faShuffle} />
</a>*/}
</div>
<textarea
onClick={() => {
Analytics.ttsClickTextInputBox();
}}
onChange={handleChangeText}
className="form-control text-message h-100"
value={props.textBuffer}
placeholder={t("ttsTextInputPlaceholder", {
character:
props.maybeSelectedTtsModel?.title || "character",
})}
></textarea>
{audioLimitAlert}
<div className="d-flex gap-3">
<button
className={speakButtonClass}
disabled={
remainingCharactersButtonDisabled ||
noTextInputButtonDisabled
}
onClick={handleLoading}
type="submit"
>
<FontAwesomeIcon icon={faVolumeHigh} className="me-2" />
{t("ttsButtonSpeak")}
{loading && <LoadingIcon />}
</button>
<button
className="btn btn-destructive w-100"
onClick={handleClearClick}
disabled={noTextInputButtonDisabled}
>
<FontAwesomeIcon icon={faDeleteLeft} className="me-2" />
{t("ttsButtonClear")}
</button>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="d-flex flex-column gap-3">
<h4 className="text-center text-lg-start">
<FontAwesomeIcon
icon={faBarsStaggered}
className="me-3"
/>
{t("ttsResultsTitle")}
</h4>
<div className="d-flex flex-column gap-3 session-tts-section">
<SessionTtsInferenceResultList
inferenceJobs={
props.inferenceJobsByCategory.get(
FrontendInferenceJobType.TextToSpeech
)!
}
ttsInferenceJobs={props.ttsInferenceJobs}
sessionSubscriptionsWrapper={
props.sessionSubscriptionsWrapper
}
/>
</div>
<textarea
onClick={() => {
Analytics.ttsClickTextInputBox();
}}
onChange={handleChangeText}
className="form-control text-message h-100"
value={props.textBuffer}
placeholder={t("ttsTextInputPlaceholder", {
character:
props.maybeSelectedTtsModel?.title || "character",
})}
></textarea>
{audioLimitAlert}
<div className="d-flex gap-3">
<button
className={speakButtonClass}
disabled={
remainingCharactersButtonDisabled ||
noTextInputButtonDisabled
}
onClick={handleLoading}
type="submit"
>
<FontAwesomeIcon icon={faVolumeHigh} className="me-2" />
{t("ttsButtonSpeak")}
{loading && <LoadingIcon />}
</button>
<button
className="btn btn-destructive w-100"
onClick={handleClearClick}
disabled={noTextInputButtonDisabled}
>
<FontAwesomeIcon icon={faDeleteLeft} className="me-2" />
{t("ttsButtonClear")}
</button>
</div>
</div>
</div>
{maybeError}
</form>
</div>
<div className="col-12 col-lg-6">
<div className="d-flex flex-column gap-3">
<h4 className="text-center text-lg-start">
<FontAwesomeIcon icon={faBarsStaggered} className="me-3" />
{t("ttsResultsTitle")}
</h4>
<div className="d-flex flex-column gap-3 session-tts-section">
<SessionTtsInferenceResultList
inferenceJobs={
props.inferenceJobsByCategory.get(
FrontendInferenceJobType.TextToSpeech
)!
}
ttsInferenceJobs={props.ttsInferenceJobs}
sessionSubscriptionsWrapper={
props.sessionSubscriptionsWrapper
}
/>
</div>
</div>
</div>
</div>
{maybeError}
</form>
</div>
{/* <div className="pt-5">
<BackLink link="/" text="Back to main page" />
</div> */}
</div>
</Panel>
<SessionW2lInferenceResultList
w2lInferenceJobs={props.w2lInferenceJobs}
@@ -647,7 +644,7 @@ function TtsModelListPage(props: Props) {
<SessionTtsModelUploadResultList
modelUploadJobs={props.ttsModelUploadJobs}
/>
</div>
</Container>
);
}
@@ -1,128 +0,0 @@
import React from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faStar,
faUser,
faArrowRight,
faMessageDots,
} from "@fortawesome/pro-solid-svg-icons";
import { Analytics } from "../../../../../common/Analytics";
import { WebUrl } from "../../../../../common/WebUrl";
import { PageHeaderWithImage } from "../../../_common/PageHeaderWithImage";
import { useLocalize } from "hooks";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export function TtsPageHero(props: Props) {
const { t } = useLocalize("TtsModelListPage");
let signUpButton = <></>;
let viewPricingButton = <></>;
let upgradeButton = <></>;
let myProfileButton = <></>;
if (!props.sessionWrapper.isLoggedIn()) {
signUpButton = (
<>
<Link
to="/signup"
onClick={() => {
Analytics.ttsClickHeroSignup();
}}
>
<button type="button" className="btn btn-primary w-100">
{t("heroButtonSignUp")}
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</button>
</Link>
</>
);
viewPricingButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
onClick={() => {
Analytics.ttsClickHeroViewPricing();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonPricing")}
</button>
</Link>
</>
);
}
if (props.sessionWrapper.isLoggedIn()) {
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
let url = WebUrl.userProfilePage(displayName);
myProfileButton = (
<>
<Link
to={url}
onClick={() => {
Analytics.ttsClickHeroViewProfile();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faUser} className="me-2" />
{t("heroButtonProfile")}
</button>
</Link>
</>
);
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
upgradeButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
onClick={() => {
Analytics.ttsClickHeroUpgradePlan();
}}
>
<button type="button" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonUpgradePlan")}
</button>
</Link>
</>
);
}
}
const titleIcon = (
<FontAwesomeIcon icon={faMessageDots} className="me-3 me-lg-4" />
);
const title = <>{t("heroTitle")}</>;
const subText = <>{t("heroText")}</>;
const actionButtons = (
<>
{upgradeButton}
{signUpButton}
{viewPricingButton}
{myProfileButton}
</>
);
const ttsImage = "/mascot/kitsune_pose2.webp";
return (
<>
<PageHeaderWithImage
headerImage={ttsImage}
titleIcon={titleIcon}
title={title}
subText={subText}
showButtons={true}
actionButtons={actionButtons}
/>
</>
);
}
@@ -0,0 +1,149 @@
import React from "react";
import {
faCircleExclamation,
faEye,
faWaveform,
} from "@fortawesome/pro-solid-svg-icons";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import PageHeader from "components/layout/PageHeader";
import {
Button,
Container,
CoverImageInput,
SplitPanel,
TempInput,
TempSelect,
TempTextArea,
} from "components/common";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import useLoraUpload from "hooks/useLoraUpload";
interface UploadLoraWeightPageProps {
sessionWrapper: SessionWrapper;
}
export default function UploadLoraWeightPage({
sessionWrapper,
}: UploadLoraWeightPageProps) {
usePrefixedDocumentTitle("Edit Voice");
const visibilityOptions = [
{ label: "Public", value: "public" },
{ label: "Private", value: "private" },
];
const {
coverImg,
descriptionMD,
onChange,
title,
upload,
uploadPath,
visibility,
} = useLoraUpload();
if (!sessionWrapper.isLoggedIn()) {
return (
<Container type="panel">
<PageHeader
titleIcon={faCircleExclamation}
title="Access Denied"
subText="This weight does not exist or is not owned by you."
panel={true}
extension={
<div className="d-flex">
<Button
label="Back to homepage"
to={`/weight/{}`}
className="d-flex"
/>
</div>
}
/>
</Container>
);
}
return (
<Container type="panel">
<PageHeader
title="Upload LoRA Weight"
titleIcon={faWaveform}
subText="Upload a LoRA model weight. Once your weight is successfully uploaded, you'll be able to start using it and sharing it with others."
panel={false}
/>
<SplitPanel dividerFooter={true}>
<SplitPanel.Body padding={true}>
<div className="row gx-4 gy-3">
<div className="col-12 col-lg-5">
<label className="sub-title required">Cover Image</label>
<CoverImageInput
{...{
currentPath: "",
onClick: coverImg.upload,
status: coverImg.status,
...coverImg.fileProps,
}}
/>
</div>
<div className="col-12 col-lg-7 order-first order-lg-last">
<TempInput
{...{
label: "Title",
name: "title",
onChange,
placeholder: "Title",
value: title,
required: true,
}}
/>
<TempInput
{...{
label: "Download URL, eg. Google Drive link",
name: "uploadPath",
onChange,
placeholder: "Download URL",
value: uploadPath,
required: true,
}}
/>
<TempSelect
{...{
icon: faEye,
label: "Visibility",
name: "visibility",
onChange,
options: visibilityOptions,
placeholder: "Voice name",
value: visibility,
}}
/>
<TempTextArea
{...{
label: "Description",
name: "descriptionMD",
onChange,
placeholder: "Description",
value: descriptionMD,
}}
/>
</div>
</div>
</SplitPanel.Body>
<SplitPanel.Footer padding={true}>
<div className="d-flex gap-2 justify-content-end">
<Button
{...{
label: "Upload Weight",
onClick: upload,
}}
/>
</div>
</SplitPanel.Footer>
</SplitPanel>
</Container>
);
}
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import {
faCircleExclamation,
faEye,
@@ -10,8 +10,6 @@ import {
Button,
Container,
CoverImageInput,
Panel,
Skeleton,
SplitPanel,
TempInput,
TempSelect,
@@ -27,8 +25,6 @@ interface UploadSdWeightPageProps {
export default function UploadSdWeightPage({
sessionWrapper,
}: UploadSdWeightPageProps) {
const [isLoading] = useState(false);
usePrefixedDocumentTitle("Edit Voice");
const visibilityOptions = [
@@ -36,9 +32,17 @@ export default function UploadSdWeightPage({
{ label: "Private", value: "private" },
];
const { coverImg, descriptionMD, onChange, title, upload, uploadPath, visibility } = useSdUpload();
const {
coverImg,
descriptionMD,
onChange,
title,
upload,
uploadPath,
visibility,
} = useSdUpload();
if (!sessionWrapper.isLoggedIn() && !isLoading) {
if (!sessionWrapper.isLoggedIn()) {
return (
<Container type="panel">
<PageHeader
@@ -65,96 +69,81 @@ export default function UploadSdWeightPage({
<PageHeader
title="Upload Stable Diffusion Weight"
titleIcon={faWaveform}
subText="Upload a Stable Diffusion Image model weight. Once your weight is successfully uploaded, you'll be able to start using it and sharing it with others."
subText="Upload a Stable Diffusion image model weight. Once your weight is successfully uploaded, you'll be able to start using it and sharing it with others."
panel={false}
/>
<>
{isLoading ? (
<Panel padding={true}>
<div className="d-flex flex-column gap-3">
<Skeleton type="short" />
<Skeleton height="40px" />
<Skeleton type="short" />
<Skeleton height="40px" />
<div className="d-flex justify-content-end mt-3 gap-2">
<Skeleton height="40px" width="120px" />
<Skeleton height="40px" width="120px" />
</div>
<SplitPanel dividerFooter={true}>
<SplitPanel.Body padding={true}>
<div className="row gx-4 gy-3">
<div className="col-12 col-lg-5">
<label className="sub-title required">Cover Image</label>
<CoverImageInput
{...{
currentPath: "",
onClick: coverImg.upload,
status: coverImg.status,
...coverImg.fileProps,
}}
/>
</div>
</Panel>
) : (
<SplitPanel dividerFooter={true}>
<SplitPanel.Body padding={true}>
<div className="row gx-4 gy-3">
<div className="col-12 col-lg-5">
<label className="sub-title required">Cover Image</label>
<CoverImageInput {...{
currentPath: "",
onClick: coverImg.upload,
status: coverImg.status,
...coverImg.fileProps
}} />
</div>
<div className="col-12 col-lg-7 order-first order-lg-last">
<TempInput
{...{
label: "Title",
name: "title",
onChange,
placeholder: "Title",
value: title,
required: true,
}}
/>
<div className="col-12 col-lg-7 order-first order-lg-last">
<TempInput
{...{
label: "Title",
name: "title",
onChange,
placeholder: "Title",
value: title,
required: true,
}}
/>
<TempInput
{...{
label: "Download URL, eg. Google Drive link",
name: "uploadPath",
onChange,
placeholder: "Download URL",
value: uploadPath,
required: true,
}}
/>
<TempInput
{...{
label: "Download URL, eg. Google Drive link",
name: "uploadPath",
onChange,
placeholder: "Download URL",
value: uploadPath,
required: true,
}}
/>
<TempSelect
{...{
icon: faEye,
label: "Visibility",
name: "visibility",
onChange,
options: visibilityOptions,
placeholder: "Voice name",
value: visibility,
}}
/>
<TempTextArea
{...{
label: "Description",
name: "descriptionMD",
onChange,
placeholder: "Description",
value: descriptionMD,
}}
/>
</div>
</div>
</SplitPanel.Body>
<SplitPanel.Footer padding={true}>
<div className="d-flex gap-2 justify-content-end">
<Button
{...{
label: "Upload Weight",
onClick: upload
}}
/>
</div>
</SplitPanel.Footer>
</SplitPanel>
)}
</>
<TempSelect
{...{
icon: faEye,
label: "Visibility",
name: "visibility",
onChange,
options: visibilityOptions,
placeholder: "Voice name",
value: visibility,
}}
/>
<TempTextArea
{...{
label: "Description",
name: "descriptionMD",
onChange,
placeholder: "Description",
value: descriptionMD,
}}
/>
</div>
</div>
</SplitPanel.Body>
<SplitPanel.Footer padding={true}>
<div className="d-flex gap-2 justify-content-end">
<Button
{...{
label: "Upload Weight",
onClick: upload,
}}
/>
</div>
</SplitPanel.Footer>
</SplitPanel>
</Container>
);
}
@@ -5,7 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { v4 as uuidv4 } from "uuid";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { VcPageHero } from "./components/VcPageHero";
import {
faBarsStaggered,
faMicrophone,
@@ -35,6 +34,9 @@ import PitchShiftComponent from "./components/PitchShiftComponent";
import PitchEstimateMethodComponent from "./components/PitchEstimateMethodComponent";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { useLocalize } from "hooks";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { Container, Panel } from "components/common";
import { faWaveformLines } from "@fortawesome/pro-solid-svg-icons";
interface Props {
sessionWrapper: SessionWrapper;
@@ -60,7 +62,7 @@ interface Props {
function VcModelListPage(props: Props) {
usePrefixedDocumentTitle("AI Voice Conversion");
const { t } = useLocalize("VcModelListPage");
PosthogClient.recordPageview();
@@ -71,15 +73,15 @@ function VcModelListPage(props: Props) {
undefined
);
const [convertIdempotencyToken, setConvertIdempotencyToken] = useState(
uuidv4()
);
const [convertIdempotencyToken, setConvertIdempotencyToken] =
useState(uuidv4());
const [autoConvertF0, setAutoConvertF0] = useState(false);
const [maybeF0MethodOverride, setMaybeF0MethodOverride] = useState<
EnqueueVoiceConversionFrequencyMethod
>(EnqueueVoiceConversionFrequencyMethod.Rmvpe);
const [maybeF0MethodOverride, setMaybeF0MethodOverride] =
useState<EnqueueVoiceConversionFrequencyMethod>(
EnqueueVoiceConversionFrequencyMethod.Rmvpe
);
const [semitones, setSemitones] = useState(0);
@@ -106,7 +108,7 @@ function VcModelListPage(props: Props) {
setVoiceConversionModels(models);
if (!maybeSelectedVoiceConversionModel && models.length > 0) {
let model = models[0];
const featuredModels = models.filter((m) => m.is_front_page_featured);
const featuredModels = models.filter(m => m.is_front_page_featured);
if (featuredModels.length > 0) {
// Random featured model
model =
@@ -219,10 +221,13 @@ function VcModelListPage(props: Props) {
props.maybeSelectedVoiceConversionModel !== undefined;
return (
<div>
<VcPageHero
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
<Container type="panel">
<PageHeaderWithImage
headerImage="mascot/vc.webp"
titleIcon={faWaveformLines}
title={t("heroTitle")}
subText={t("heroText")}
yOffset="78%"
/>
{/* <div className="container">
@@ -238,127 +243,126 @@ function VcModelListPage(props: Props) {
</div>
</div> */}
<div className="container-panel pb-5 mb-4">
<div className="panel p-3 py-4 p-md-4">
<div className="d-flex gap-4">
<form
className="w-100 d-flex flex-column"
onSubmit={handleFormSubmit}
>
{/* Explore Rollout */}
<label className="sub-title">
{t("vcVoiceLabel", { 0: voiceConversionModels.length })}
</label>
<div className="input-icon-search pb-4">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faMicrophone} />
</span>
<Panel padding={true}>
<div className="d-flex gap-4">
<form
className="w-100 d-flex flex-column"
onSubmit={handleFormSubmit}
>
{/* Explore Rollout */}
<label className="sub-title">
{t("vcVoiceLabel", { 0: voiceConversionModels.length })}
</label>
<div className="input-icon-search pb-4">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faMicrophone} />
</span>
<VcModelListSearch
voiceConversionModels={props.voiceConversionModels}
setVoiceConversionModels={props.setVoiceConversionModels}
maybeSelectedVoiceConversionModel={
props.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={interceptModelChange}
/>
</div>
<VcModelListSearch
voiceConversionModels={props.voiceConversionModels}
setVoiceConversionModels={props.setVoiceConversionModels}
maybeSelectedVoiceConversionModel={
props.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={interceptModelChange}
/>
</div>
<div className="row gx-5 gy-5">
<div className="col-12 col-lg-6 d-flex flex-column gap-4">
<ul className="nav nav-tabs nav-vc" id="myTab" role="tablist">
<li className="nav-item w-100" role="presentation">
<button
className="nav-link active w-100"
id="prerecorded-tab"
data-bs-toggle="tab"
data-bs-target="#prerecorded"
type="button"
role="tab"
aria-controls="prerecorded"
aria-selected="true"
>
{t("vcTabUpload")}
</button>
</li>
<li className="nav-item w-100" role="presentation">
<button
className="nav-link w-100"
id="recordaudio-tab"
data-bs-toggle="tab"
data-bs-target="#recordaudio"
type="button"
role="tab"
aria-controls="recordaudio"
aria-selected="false"
>
{t("vcTabRecord")}
</button>
</li>
</ul>
<div className="tab-content" id="myTabContent">
<div
className="tab-pane fade show active"
id="prerecorded"
role="tabpanel"
aria-labelledby="prerecorded-tab"
<div className="row gx-5 gy-5">
<div className="col-12 col-lg-6 d-flex flex-column gap-4">
<ul className="nav nav-tabs nav-vc" id="myTab" role="tablist">
<li className="nav-item w-100" role="presentation">
<button
className="nav-link active w-100"
id="prerecorded-tab"
data-bs-toggle="tab"
data-bs-target="#prerecorded"
type="button"
role="tab"
aria-controls="prerecorded"
aria-selected="true"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">
{t("vcUploadFileLabel")}
</label>
<div className="d-flex flex-column gap-3 upload-component">
<UploadComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
{t("vcTabUpload")}
</button>
</li>
<li className="nav-item w-100" role="presentation">
<button
className="nav-link w-100"
id="recordaudio-tab"
data-bs-toggle="tab"
data-bs-target="#recordaudio"
type="button"
role="tab"
aria-controls="recordaudio"
aria-selected="false"
>
{t("vcTabRecord")}
</button>
</li>
</ul>
<div className="tab-content" id="myTabContent">
<div
className="tab-pane fade show active"
id="prerecorded"
role="tabpanel"
aria-labelledby="prerecorded-tab"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">
{t("vcUploadFileLabel")}
</label>
<div className="d-flex flex-column gap-3 upload-component">
<UploadComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
/>
</div>
</div>
<div>
<label className="sub-title">
{t("vcPitchControlLabel")}
</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
</div>
<div>
<label className="sub-title">
{t("vcPitchControlLabel")}
</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0Checkbox"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0Checkbox"
>
Auto F0 ({t("vcPitchControlF0")})
</label>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0Checkbox"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0Checkbox"
>
Auto F0 ({t("vcPitchControlF0")})
</label>
</div>
</div>
{/*<div>
</div>
{/*<div>
<label className="sub-title">
Or pick from your audio collection (5 files)
</label>
@@ -384,149 +388,145 @@ function VcModelListPage(props: Props) {
</div>
</div>*/}
<div>
<label className="sub-title">
{t("vcConvertLabel")}
</label>
<div>
<label className="sub-title">
{t("vcConvertLabel")}
</label>
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon
icon={faRightLeft}
className="me-2"
/>
{t("vcButtonConvert")}
{convertLoading && <LoadingIcon />}
</button>
</div>
</div>
</div>
</div>
<div
className="tab-pane fade"
id="recordaudio"
role="tabpanel"
aria-labelledby="recordaudio-tab"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">
{t("vcRecordAudioLabel")}
</label>
<div className="d-flex flex-column gap-3 upload-component">
<RecordComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon
icon={faRightLeft}
className="me-2"
/>
</div>
</div>
<div>
<label className="sub-title">
{t("vcPitchControlLabel")}
</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0CheckboxMic"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0CheckboxMic"
>
Auto F0 ({t("vcPitchControlF0")})
</label>
</div>
</div>
</div>
<div>
<label className="sub-title">
{t("vcConvertLabel")}
</label>
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon
icon={faRightLeft}
className="me-2"
/>
{t("vcButtonConvert")}
{convertLoading && <LoadingIcon />}
</button>
</div>
{t("vcButtonConvert")}
{convertLoading && <LoadingIcon />}
</button>
</div>
</div>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="d-flex flex-column gap-3">
<h4 className="text-center text-lg-start">
<FontAwesomeIcon
icon={faBarsStaggered}
className="me-3"
/>
{t("vcResultsTitle")}
</h4>
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
<SessionVoiceConversionResultsList
inferenceJobs={
props.inferenceJobsByCategory.get(
FrontendInferenceJobType.VoiceConversion
)!
}
sessionSubscriptionsWrapper={
props.sessionSubscriptionsWrapper
}
/>
<div
className="tab-pane fade"
id="recordaudio"
role="tabpanel"
aria-labelledby="recordaudio-tab"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">
{t("vcRecordAudioLabel")}
</label>
<div className="d-flex flex-column gap-3 upload-component">
<RecordComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
/>
</div>
</div>
<div>
<label className="sub-title">
{t("vcPitchControlLabel")}
</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0CheckboxMic"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0CheckboxMic"
>
Auto F0 ({t("vcPitchControlF0")})
</label>
</div>
</div>
</div>
<div>
<label className="sub-title">
{t("vcConvertLabel")}
</label>
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon
icon={faRightLeft}
className="me-2"
/>
{t("vcButtonConvert")}
{convertLoading && <LoadingIcon />}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div className="col-12 col-lg-6">
<div className="d-flex flex-column gap-3">
<h4 className="text-center text-lg-start">
<FontAwesomeIcon icon={faBarsStaggered} className="me-3" />
{t("vcResultsTitle")}
</h4>
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
<SessionVoiceConversionResultsList
inferenceJobs={
props.inferenceJobsByCategory.get(
FrontendInferenceJobType.VoiceConversion
)!
}
sessionSubscriptionsWrapper={
props.sessionSubscriptionsWrapper
}
/>
</div>
</div>
</div>
</div>
</form>
</div>
{/* <div className="pt-5">
<BackLink link="/" text="Back to main page" />
</div> */}
</div>
</div>
</Panel>
</Container>
);
}
@@ -1,131 +0,0 @@
import React, { useMemo } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faStar,
faUser,
faArrowRight,
faMicrophoneStand,
} from "@fortawesome/pro-solid-svg-icons";
import { Analytics } from "../../../../../../common/Analytics";
import { WebUrl } from "../../../../../../common/WebUrl";
import { PageHeaderWithImage } from "../../../../_common/PageHeaderWithImage";
import { useLocalize } from "hooks";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export function VcPageHero(props: Props) {
const { t } = useLocalize("VcModelListPage");
const randomImage = useMemo(() => {
const images = ["mascot/vc.webp"];
return images[Math.floor(Math.random() * images.length)];
}, []);
let signUpButton = <></>;
let viewPricingButton = <></>;
let upgradeButton = <></>;
let myProfileButton = <></>;
if (!props.sessionWrapper.isLoggedIn()) {
signUpButton = (
<>
<Link
to="/signup"
onClick={() => {
Analytics.ttsClickHeroSignup();
}}
>
<button type="button" className="btn btn-primary w-100">
{t("heroButtonSignUp")}
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</button>
</Link>
</>
);
viewPricingButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
onClick={() => {
Analytics.ttsClickHeroViewPricing();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonPricing")}
</button>
</Link>
</>
);
}
if (props.sessionWrapper.isLoggedIn()) {
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
let url = WebUrl.userProfilePage(displayName);
myProfileButton = (
<>
<Link
to={url}
onClick={() => {
Analytics.ttsClickHeroViewProfile();
}}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faUser} className="me-2" />
{t("heroButtonProfile")}
</button>
</Link>
</>
);
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
upgradeButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
onClick={() => {
Analytics.ttsClickHeroUpgradePlan();
}}
>
<button type="button" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonUpgradePlan")}
</button>
</Link>
</>
);
}
}
const titleIcon = (
<FontAwesomeIcon icon={faMicrophoneStand} className="me-3 me-lg-4" />
);
const title = <>{t("heroTitle")}</>;
const subText = <>{t("heroText")}</>;
const actionButtons = (
<>
{upgradeButton}
{signUpButton}
{viewPricingButton}
{myProfileButton}
</>
);
return (
<>
<PageHeaderWithImage
headerImage={randomImage}
titleIcon={titleIcon}
title={title}
subText={subText}
showButtons={true}
actionButtons={actionButtons}
/>
</>
);
}
@@ -6,10 +6,8 @@ import {
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { BasicVideo, Button, Container, Panel } from "components/common";
import PageHeader from "components/layout/PageHeader";
import Tabs from "components/common/Tabs";
import { useLocalize } from "hooks";
import { BasicVideo, Button, Container, Panel, Tabs } from "components/common";
import { useInferenceJobs, useLocalize } from "hooks";
import TabContentUpload from "./components/tabContentUpload";
import TabContentLibrary from "./components/tabContentLibrary";
@@ -20,7 +18,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowRotateLeft,
faPersonCircleCheck,
faPersonRays,
} from "@fortawesome/pro-solid-svg-icons";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
export default function VideoMotionCapture(props: {
enqueueInferenceJob: (
@@ -37,6 +37,11 @@ export default function VideoMotionCapture(props: {
status: NO_FILE,
});
const { inferenceJobs } = useInferenceJobs(
FrontendInferenceJobType.VideoMotionCapture
);
const hasMotionCaptureJobs = inferenceJobs && inferenceJobs.length > 0;
useEffect(() => {
if (
pageState.status === states.MOCAPNET_ENQUEUED &&
@@ -75,14 +80,21 @@ export default function VideoMotionCapture(props: {
return (
<Container type="panel" className="mb-5">
<PageHeader
<PageHeaderWithImage
title={t("headings.title")}
subText={t("headings.subtitle")}
headerImage="/mascot/video-mocap.webp"
yOffset="62%"
titleIcon={faPersonRays}
/>
<VideoMocapJobList />
{hasMotionCaptureJobs && (
<div className="mb-4">
<VideoMocapJobList />
</div>
)}
<Panel className="mt-3">
<Panel>
<div className="row g-0">
{pageState.status < FILE_UPLOADING && (
<>
@@ -120,7 +132,9 @@ export default function VideoMotionCapture(props: {
icon={faArrowRotateLeft}
iconFlip={true}
label="Generate Another"
onClick={() => {}} //back to first state
onClick={() => {
dispatchPageState({ type: "restart" });
}} //back to first state
variant="primary"
/>
</div>
@@ -21,8 +21,8 @@ export default function PageVideoMocapProgress({
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const { FILE_UPLOADING, FILE_UPLOADED, MOCAPNET_ENQUEUEING } = states;
const { FILE_UPLOADING, FILE_UPLOADED, FILE_SELECTED, MOCAPNET_ENQUEUEING } = states;
const handleEnqueueMocapNet = () => {
if (pageState.mediaFileToken) {
const request = {
@@ -43,6 +43,10 @@ export default function PageVideoMocapProgress({
}
};
if (pageState.status === FILE_SELECTED){
handleEnqueueMocapNet();
}
if (pageState.status === FILE_UPLOADING) {
return (
<div className="d-flex flex-column p-4 gap-3 text-center align-items-center">
@@ -1,17 +1,35 @@
import SelectModal from "components/common/SelectModal/SelectModal";
import React from "react";
import React, { useState } from "react";
import { Action, State } from "../videoMocapReducer";
import { Button } from "components/common";
export default function TabContentLibrary(props: {
export default function TabContentLibrary({
t,
pageState,
dispatchPageState,
}: {
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}){
}) {
const [token, setToken] = useState<string | undefined>();
const handleProceed = () => {
if (token)
dispatchPageState({
type: "selectedFile",
payload: { mediaFileToken: token },
});
};
const handleOnSelect = (token: string) => {
setToken(token);
};
return (
<div>
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
tabs={[
{
label: "All Videos",
@@ -22,6 +40,7 @@ export default function TabContentLibrary(props: {
},
]}
/>
{token && <Button label={t("button.proceed")} onClick={handleProceed} />}
</div>
);
}
@@ -4,6 +4,7 @@ export enum states{
FILE_STAGED,
FILE_UPLOADING,
FILE_UPLOADED,
FILE_SELECTED,
MOCAPNET_ENQUEUEING,
MOCAPNET_ENQUEUED,
}
@@ -15,20 +16,29 @@ export type State = {
}
export type Action =
| {type: 'uploadFile'}
| {type: 'restart'}
| {type: 'stagedFile'}
| {type: 'clearedFile'}
| {type: 'selectedFile', payload:{ mediaFileToken: string}}
| {type: 'uploadFile'}
| {type: 'uploadFileSuccess', payload:{ mediaFileToken: string}}
| {type: 'enqueueMocapNet'}
| {type: 'enqueueMocapNetSuccess', payload: {inferenceJobToken: string|undefined}}
export function reducer (state: State, action: Action): State {
console.log(action);
switch(action.type){
case 'restart':
return { status: states.NO_FILE }
case 'stagedFile':
return {...state, status: states.FILE_STAGED}
case 'clearedFile':
return {...state, status: states.NO_FILE}
case 'selectedFile':
return {
...state,
status: states.FILE_SELECTED,
mediaFileToken: action.payload.mediaFileToken
}
case 'uploadFile':
return {...state,status: states.FILE_UPLOADING};
case 'uploadFileSuccess':
@@ -1,6 +1,13 @@
import React, { useState } from "react";
import { Redirect, useHistory, useLocation } from "react-router-dom";
import { faPenToSquare, faPlus, faRightToBracket, faStar, faWaveform } from "@fortawesome/pro-solid-svg-icons";
import {
faPenToSquare,
faPlus,
faRightToBracket,
faStar,
faWandMagicSparkles,
faMicrophone,
} from "@fortawesome/pro-solid-svg-icons";
import InferenceJobsList from "components/layout/InferenceJobsList";
import { useLocalize, useSession } from "hooks";
import Panel from "components/common/Panel";
@@ -13,7 +20,6 @@ import { Button } from "components/common";
import useVoiceRequests from "./useVoiceRequests";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/pro-solid-svg-icons";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import "./VoiceDesigner.scss";
@@ -28,7 +34,9 @@ function VoiceDesignerMainPage() {
requestVoices: true,
});
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(pathname);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(
pathname
);
const [deleteItem, setDeleteItem] = useState("");
const [deleteType, setDeleteType] = useState("");
const [deleteText, setDeleteText] = useState({
@@ -146,7 +154,8 @@ function VoiceDesignerMainPage() {
const failures = (fail = "") => {
switch (fail) {
// case "face_not_detected": return "Face not detected, try another picture"; // voice designer can have failure states too!
default: return "Uknown failure";
default:
return "Uknown failure";
}
};
@@ -180,12 +189,13 @@ function VoiceDesignerMainPage() {
</div>
);
const logggedInView = <>
const logggedInView = (
<>
<InferenceJobsList
{...{
failures,
jobType: FrontendInferenceJobType.VoiceDesignerCreateVoice,
t
t,
}}
/>
<Panel mb={true}>
@@ -224,9 +234,11 @@ function VoiceDesignerMainPage() {
/>
</div>
</Panel>
</>;
</>
);
const loggedOutView = <Panel padding={true}>
const loggedOutView = (
<Panel padding={true}>
<div className="d-flex flex-column align-items-center py-3 my-3 py-md-4 my-md-4 gap-4">
<div className="text-center">
<h4 className="fw-bold">Please log in to access voice creation.</h4>
@@ -252,7 +264,8 @@ function VoiceDesignerMainPage() {
/>
</div>
</div>
</Panel>;
</Panel>
);
return (
<>
@@ -262,7 +275,7 @@ function VoiceDesignerMainPage() {
button: user ? createVoiceButton : signUpButton,
...(!user ? { secondaryButton: pricingButton } : {}),
title: "Voice Designer",
titleIcon: faWaveform,
titleIcon: faWandMagicSparkles,
subText:
"Create your own AI voice by providing audio files of the voice you want to clone.",
panel: false,
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more