Merge branch 'feature/mvp' into olivicmic-fy-engine-compositor
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 162 KiB |
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
”Deep fakes” 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>
|
||||
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,
|
||||
|
||||