mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 05:12:50 +00:00
Merge branch 'master' into bflat-fy-voice-designer-page
This commit is contained in:
99 files changed
+2347
-433
No files matched your search
+6
-1
@@ -10,7 +10,7 @@
|
||||
/coming-soon* /index.html 200
|
||||
/commissions* /index.html 200
|
||||
/contribute* /index.html 200
|
||||
/face-animation* /index.html 200
|
||||
/face-animator* /index.html 200
|
||||
/firehose* /index.html 200
|
||||
/guide* /index.html 200
|
||||
/jobs* /index.html 200
|
||||
@@ -18,6 +18,7 @@
|
||||
/landing_temp* /index.html 200
|
||||
/leaderboard* /index.html 200
|
||||
/login* /index.html 200
|
||||
/media* /index.html 200
|
||||
/moderation* /index.html 200
|
||||
/news* /index.html 200
|
||||
/obs_configs* /index.html 200
|
||||
@@ -40,6 +41,10 @@
|
||||
/video* /index.html 200
|
||||
/voice-conversion* /index.html 200
|
||||
/w2l* /index.html 200
|
||||
|
||||
# Old paths
|
||||
/face-animation* /face-animator 302
|
||||
|
||||
# This is for YCombinator.
|
||||
# This is the *OLD* video for Summer 2022. We'll re-edit.
|
||||
# PLEASE DO NOT ACECSS THIS URL OR WATCH. IT WILL CHANGE THE
|
||||
|
||||
@@ -100,15 +100,15 @@ class ApiConfig {
|
||||
}
|
||||
|
||||
login(): string {
|
||||
return `${this.getApiOrigin()}/login`;
|
||||
return `${this.getApiOrigin()}/v1/login`;
|
||||
}
|
||||
|
||||
logout(): string {
|
||||
return `${this.getApiOrigin()}/logout`;
|
||||
return `${this.getApiOrigin()}/v1/logout`;
|
||||
}
|
||||
|
||||
sessionDetails(): string {
|
||||
return `${this.getApiOrigin()}/session`;
|
||||
return `${this.getApiOrigin()}/v1/session`;
|
||||
}
|
||||
|
||||
getQueueStats(): string {
|
||||
@@ -410,6 +410,12 @@ class ApiConfig {
|
||||
return `${this.getApiOrigin()}/voice_clone_requests/check`;
|
||||
}
|
||||
|
||||
// =============== Media Files ===============
|
||||
|
||||
getMediaFile(mediaFileToken: string): string {
|
||||
return `${this.getApiOrigin()}/v1/media_files/file/${mediaFileToken}`;
|
||||
}
|
||||
|
||||
// =============== File Uploads ===============
|
||||
|
||||
uploadAudio(): string {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/// A common type returned by several endpoints.
|
||||
/// Basic information to display a user and their avatar.
|
||||
export interface UserDetailsLight {
|
||||
user_token: string,
|
||||
/// Username (lowercase)
|
||||
username: string,
|
||||
/// Username with user-specified capitalization
|
||||
display_name: string,
|
||||
gravatar_hash: string,
|
||||
default_avatar: DefaultAvatarInfo,
|
||||
|
||||
}
|
||||
|
||||
export interface DefaultAvatarInfo {
|
||||
image_index: number,
|
||||
color_index: number,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export enum MediaFileType {
|
||||
Audio = "audio",
|
||||
Video = "video",
|
||||
Image = "image",
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ApiConfig } from "../ApiConfig";
|
||||
import { UserDetailsLight } from "../_common/UserDetailsLight";
|
||||
import { MediaFileType } from "../_common/enums/MediaFileType";
|
||||
|
||||
export interface MediaFile {
|
||||
token: string,
|
||||
media_type: MediaFileType,
|
||||
public_bucket_path: string,
|
||||
maybe_creator_user: UserDetailsLight | null,
|
||||
creator_set_visibility: string,
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
}
|
||||
|
||||
export enum MediaFileLookupError {
|
||||
NotFound,
|
||||
ServerError,
|
||||
FrontendError,
|
||||
}
|
||||
|
||||
export interface GetMediaFileResponse {
|
||||
success: boolean,
|
||||
media_file?: MediaFile,
|
||||
}
|
||||
|
||||
export async function GetMediaFile(mediaFileToken: string) : Promise<GetMediaFileResponse> {
|
||||
const endpoint = new ApiConfig().getMediaFile(mediaFileToken);
|
||||
|
||||
return await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
const response : GetMediaFileResponse = res;
|
||||
|
||||
if (response && response.success && response.media_file) {
|
||||
// NB: Timestamps aren't converted to Date objects on their own!
|
||||
response.media_file.created_at = new Date(response.media_file.created_at);
|
||||
response.media_file.updated_at = new Date(response.media_file.updated_at);
|
||||
return response;
|
||||
} else {
|
||||
return { success : false };
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
return { success : false };
|
||||
});
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export interface RequestDetails {
|
||||
export interface StatusDetails {
|
||||
status: string,
|
||||
maybe_extra_status_description?: string,
|
||||
maybe_failure_category?: string,
|
||||
attempt_count: number,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class InferenceJob {
|
||||
maybeResultType: string | undefined | null;
|
||||
maybeResultToken: string | undefined | null;
|
||||
maybeResultPublicBucketMediaPath: string | undefined | null;
|
||||
maybeFailureCategory: string | undefined | null;
|
||||
|
||||
constructor(
|
||||
// PK
|
||||
@@ -50,6 +51,7 @@ export class InferenceJob {
|
||||
maybeResultEntityType: string | undefined | null = null,
|
||||
maybeResultEntityToken: string | undefined | null = null,
|
||||
maybeResultPublicBucketMediaPath: string | undefined | null = null,
|
||||
maybeFailureCategory: string | undefined | null = null
|
||||
) {
|
||||
this.jobToken = jobToken;
|
||||
this.frontendJobType = frontendJobType;
|
||||
@@ -77,6 +79,10 @@ export class InferenceJob {
|
||||
}
|
||||
|
||||
this.maybeResultPublicBucketMediaPath = maybeResultPublicBucketMediaPath;
|
||||
|
||||
if (!!maybeFailureCategory) {
|
||||
this.maybeFailureCategory = maybeFailureCategory;
|
||||
}
|
||||
}
|
||||
|
||||
static fromResponse(response: ModelInferenceJobStatus, frontendJobType: FrontendInferenceJobType) : InferenceJob {
|
||||
@@ -93,6 +99,7 @@ export class InferenceJob {
|
||||
response.maybe_result?.entity_type,
|
||||
response.maybe_result?.entity_token,
|
||||
response.maybe_result?.maybe_public_bucket_media_path,
|
||||
response.status.maybe_failure_category
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,10 @@
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"bulma": "^0.9.0",
|
||||
"craco": "^0.0.3",
|
||||
"date-fns": "^2.22.1",
|
||||
"date-fns": "2.22.1",
|
||||
"framer-motion": "^6.5.1",
|
||||
"howler": "^2.2.0",
|
||||
"i18next": "^21.6.11",
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
"audio": "Audio",
|
||||
"image": "Image",
|
||||
"subtitle": "Select and image with a clear face, and an audio sample, and generate a lipsynced video.",
|
||||
"title": "Face Animator"
|
||||
"title": "Face Animator",
|
||||
"yourJobs": "Your jobs"
|
||||
},
|
||||
"inputs": {
|
||||
"downloadFile": "Download File",
|
||||
"generate": "Generate",
|
||||
"makeAnother": "Make another"
|
||||
"makeAnother": "Make another",
|
||||
"viewResult": "View result"
|
||||
},
|
||||
"status": {
|
||||
"animationComplete": "Animation complete!",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./AppNew.scss";
|
||||
import "scss/custom-bootstrap.scss";
|
||||
|
||||
import React from "react";
|
||||
import { ApiConfig } from "@storyteller/components";
|
||||
@@ -57,6 +58,8 @@ import { VoiceConversionModelUploadJob } from "@storyteller/components/src/jobs/
|
||||
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
|
||||
import { InferenceJobs } from "components/providers";
|
||||
|
||||
// NB: We're transitioning over to this instance of i18n-next that loads translations over HTTP from Json Files.
|
||||
// The old i18n-next instance (see below) bakes in translations into the compiled javascript blob.
|
||||
// This new instance uses the Locize paid service to manage translation strings on their website. It's automated,
|
||||
@@ -182,12 +185,15 @@ function newVocodes() {
|
||||
return discord || twitter || alphaCookie;
|
||||
}
|
||||
|
||||
function initInferenceJobsByCategoryMap() : Map<FrontendInferenceJobType, InferenceJob[]> {
|
||||
let inferenceJobsByCategory = new Map();
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.FaceAnimation, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.TextToSpeech, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceConversion, []);
|
||||
return inferenceJobsByCategory;
|
||||
function initInferenceJobsByCategoryMap(): Map<
|
||||
FrontendInferenceJobType,
|
||||
InferenceJob[]
|
||||
> {
|
||||
let inferenceJobsByCategory = new Map();
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.FaceAnimation, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.TextToSpeech, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceConversion, []);
|
||||
return inferenceJobsByCategory;
|
||||
}
|
||||
|
||||
function isMacOs() {
|
||||
@@ -779,6 +785,7 @@ class App extends React.Component<Props, State> {
|
||||
*/}
|
||||
|
||||
<div className="migrationComponentWrapper">
|
||||
<InferenceJobs {...{ byCategory: this.state.inferenceJobsByCategory }}>
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<PageContainer
|
||||
@@ -876,6 +883,7 @@ class App extends React.Component<Props, State> {
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</InferenceJobs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-accordion-item {
|
||||
border: 1px solid rgba($white, 0.12);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fy-accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: $secondary;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&.open {
|
||||
background-color: lighten($secondary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content {
|
||||
overflow: hidden;
|
||||
background-color: lighten($panel, 4%);
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
max-height: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.caret-icon {
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.caret-rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
createContext,
|
||||
useContext,
|
||||
} from "react";
|
||||
import "./Accordion.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { animated, useSpring } from "@react-spring/web";
|
||||
|
||||
interface AccordionProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionItemProps {
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionContextType {
|
||||
openItems: string[];
|
||||
toggleItem: (title: string) => void;
|
||||
}
|
||||
|
||||
const AccordionContext = createContext<AccordionContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
function AccordionItem({
|
||||
title,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
}: AccordionItemProps) {
|
||||
const { openItems, toggleItem } = useContext(
|
||||
AccordionContext
|
||||
) as AccordionContextType;
|
||||
const isOpen = openItems.includes(title);
|
||||
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(
|
||||
isOpen ? undefined : 0
|
||||
);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setContentHeight(isOpen ? contentRef.current.scrollHeight : 0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const heightProps = useSpring({
|
||||
height: isOpen ? `${contentHeight}px` : "0px",
|
||||
config: { tension: 300, friction: 25, clamp: true },
|
||||
});
|
||||
|
||||
const contentOpacityProps = useSpring({
|
||||
opacity: isOpen ? 1 : 0,
|
||||
config: { tension: 300, friction: 25, clamp: true },
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
toggleItem(title);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fy-accordion-item">
|
||||
<div
|
||||
className={`fy-accordion-header p-3 ${isOpen ? "open" : ""}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{title}
|
||||
<span className={`caret-icon ${isOpen ? "caret-rotated" : ""}`}>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</span>
|
||||
</div>
|
||||
<animated.div style={{ ...heightProps, overflow: "hidden" }}>
|
||||
<animated.div
|
||||
ref={contentRef}
|
||||
style={contentOpacityProps}
|
||||
className="fy-accordion-content"
|
||||
>
|
||||
{children}
|
||||
</animated.div>
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Accordion({ children }: AccordionProps) {
|
||||
const [openItems, setOpenItems] = useState<string[]>([]);
|
||||
|
||||
const toggleItem = (title: string) => {
|
||||
if (openItems.includes(title)) {
|
||||
setOpenItems(openItems.filter((item) => item !== title));
|
||||
} else {
|
||||
setOpenItems([...openItems, title]);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize openItems based on defaultOpen props of AccordionItem children
|
||||
useEffect(() => {
|
||||
const defaultOpenItems = React.Children.toArray(children)
|
||||
.filter((child: any) => child.props.defaultOpen)
|
||||
.map((child: any) => child.props.title);
|
||||
setOpenItems(defaultOpenItems);
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openItems, toggleItem }}>
|
||||
<div className="d-flex flex-column gap-3">{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Accordion.Item = AccordionItem;
|
||||
|
||||
export default Accordion;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Accordion";
|
||||
@@ -3,13 +3,24 @@
|
||||
.button {
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 15px !important;
|
||||
font-weight: 500 !important;
|
||||
padding: 10px 20px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $white !important;
|
||||
|
||||
&-square {
|
||||
width: 46.5px;
|
||||
height: 46.5px;
|
||||
}
|
||||
|
||||
&-square-small {
|
||||
width: 37.5px;
|
||||
height: 37.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
@@ -19,19 +30,19 @@
|
||||
border: 2.5px solid;
|
||||
border-color: $primary;
|
||||
transition: 0.2s;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
}
|
||||
.button-primary:hover {
|
||||
background-color: lighten($primary, 5%);
|
||||
border-color: lighten($primary, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.button-secondary,
|
||||
.button-secondary:focus,
|
||||
.button-secondary:active {
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: $secondary;
|
||||
transition: 0.2s;
|
||||
@@ -39,24 +50,24 @@
|
||||
|
||||
.button-secondary:hover {
|
||||
background-color: lighten($secondary, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.button-destructive,
|
||||
.button-destructive:focus,
|
||||
.button-destructive:active {
|
||||
.button-danger,
|
||||
.button-danger:focus,
|
||||
.button-danger:active {
|
||||
background-color: $danger;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: $danger;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.button-destructive:hover {
|
||||
.button-danger:hover {
|
||||
background-color: lighten($danger, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: lighten($danger, 5%);
|
||||
}
|
||||
@@ -65,7 +76,7 @@
|
||||
.button-small:focus,
|
||||
.button-small:active {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-size: 14px !important;
|
||||
padding: 8.25px 16px;
|
||||
transition: 0.2s;
|
||||
border: none;
|
||||
|
||||
@@ -1,37 +1,97 @@
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "./Button.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label?: string;
|
||||
label?: string | null;
|
||||
icon?: IconDefinition;
|
||||
small?: boolean;
|
||||
secondary?: boolean;
|
||||
danger?: boolean;
|
||||
variant?: "primary" | "secondary" | "danger";
|
||||
to?: string;
|
||||
href?: string;
|
||||
target?: "_blank" | "_self";
|
||||
square?: boolean;
|
||||
tooltip?: string;
|
||||
full?: boolean;
|
||||
iconFlip?: boolean;
|
||||
download?: boolean | string;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
label,
|
||||
icon,
|
||||
small,
|
||||
secondary,
|
||||
danger,
|
||||
variant = "primary",
|
||||
to,
|
||||
href,
|
||||
target = "_self",
|
||||
square,
|
||||
tooltip,
|
||||
full = false,
|
||||
iconFlip = false,
|
||||
download,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const buttonClasses = `button ${small ? "button-small" : ""}
|
||||
${
|
||||
secondary
|
||||
? "button-secondary"
|
||||
: danger
|
||||
? "button-destructive"
|
||||
: "button-primary"
|
||||
} `;
|
||||
let iconMarginClass = !square && label ? (iconFlip ? "ms-2" : "me-2") : "";
|
||||
let IconComponent = icon ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconMarginClass} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<button className={buttonClasses} type="button" {...rest}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
let LabelComponent = !square ? label : null;
|
||||
|
||||
const externalClass = rest.className || "";
|
||||
|
||||
const commonProps = {
|
||||
className: `${externalClass} button ${small ? "button-small" : ""} ${
|
||||
square ? (small ? "button-square-small" : "button-square") : ""
|
||||
} button-${variant} ${full ? "w-100" : ""}`,
|
||||
};
|
||||
|
||||
delete rest.className;
|
||||
|
||||
const ButtonContent = iconFlip ? (
|
||||
<>
|
||||
{LabelComponent}
|
||||
{IconComponent}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{IconComponent}
|
||||
{LabelComponent}
|
||||
</>
|
||||
);
|
||||
|
||||
const WrappedButton = to ? (
|
||||
<Link to={to} {...commonProps}>
|
||||
{ButtonContent}
|
||||
</Link>
|
||||
) : href ? (
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel="noopener noreferrer"
|
||||
download={
|
||||
download ? (typeof download === "string" ? download : true) : undefined
|
||||
}
|
||||
{...commonProps}
|
||||
>
|
||||
{ButtonContent}
|
||||
</a>
|
||||
) : (
|
||||
<button type="button" {...commonProps} {...rest}>
|
||||
{ButtonContent}
|
||||
</button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tippy theme="fakeyou" content={tooltip}>
|
||||
{WrappedButton}
|
||||
</Tippy>
|
||||
) : (
|
||||
WrappedButton
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Button";
|
||||
@@ -0,0 +1,39 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.checkable-tag {
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
border: 2px solid $secondary;
|
||||
|
||||
&.checked {
|
||||
background-color: lighten($secondary, 10%) !important;
|
||||
color: $white;
|
||||
border: 2px solid $primary !important;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($secondary, 5%);
|
||||
border: 2px solid lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
&:hover > .remove-icon {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import "./CheckableTag.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface CheckableTagProps {
|
||||
tag: string;
|
||||
isSelected: boolean;
|
||||
onToggle: (tag: string) => void;
|
||||
}
|
||||
|
||||
export default function CheckableTag({
|
||||
tag,
|
||||
isSelected,
|
||||
onToggle,
|
||||
}: CheckableTagProps) {
|
||||
return (
|
||||
<div
|
||||
className={`d-flex align-items-center checkable-tag ${
|
||||
isSelected ? "checked" : ""
|
||||
}`}
|
||||
onClick={() => onToggle(tag)}
|
||||
>
|
||||
{tag}
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon icon={faXmark} className="ms-2 remove-icon" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CheckableTag";
|
||||
@@ -25,6 +25,6 @@ export default function Checkbox({ checked, className = "", label = "", name = "
|
||||
<a.svg {...{ className: "fy-checkbox", onClick, style, ...hoverEvents }}>
|
||||
<Check {...{ checked }}/>
|
||||
</a.svg>
|
||||
{ label && <label {...{ className: "form-check-label", for: id, onClick }}>{ label }</label> }
|
||||
{ label && <label {...{ className: "form-check-label", htmlFor: id, onClick }}>{ label }</label> }
|
||||
</div>
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
type?: "full" | "padded" | "panel";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PageContainer({
|
||||
children,
|
||||
type = "full",
|
||||
className = "",
|
||||
}: ContainerProps) {
|
||||
let containerClass = "";
|
||||
|
||||
switch (type) {
|
||||
case "full":
|
||||
containerClass = "container-fluid";
|
||||
break;
|
||||
case "padded":
|
||||
containerClass = "container";
|
||||
break;
|
||||
case "panel":
|
||||
containerClass = "container-panel";
|
||||
break;
|
||||
default:
|
||||
containerClass = "container-fluid";
|
||||
break;
|
||||
}
|
||||
|
||||
// Merge the classNames
|
||||
const mergedClassNames = `${containerClass} ${className}`;
|
||||
|
||||
return <div className={mergedClassNames}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Container";
|
||||
@@ -0,0 +1,34 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 16px !important;
|
||||
min-width: 124px !important;
|
||||
}
|
||||
|
||||
.no-outer-border {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:first-child,
|
||||
.no-outer-border > tbody > tr > td:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:last-child,
|
||||
.no-outer-border > tbody > tr > td:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > tbody > tr:last-child > td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.data-table-property {
|
||||
font-weight: 500 !important;
|
||||
background-color: lighten($panel, 7%) !important;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import "./DataTable.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Data = {
|
||||
property: string;
|
||||
value: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
interface DataTableProps {
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export default function DataTable({ data }: DataTableProps) {
|
||||
return (
|
||||
<table className="table no-outer-border">
|
||||
<tbody>
|
||||
{data.map((row, index) => (
|
||||
<tr key={index}>
|
||||
<td className="data-table-property">{row.property}</td>
|
||||
{row.link ? (
|
||||
<td>
|
||||
<Link to={row.link}>{row.value}</Link>
|
||||
</td>
|
||||
) : (
|
||||
<td>{row.value}</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DataTable";
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
}
|
||||
|
||||
export default function Input({ label, icon, textArea, ...rest }: InputProps) {
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div className={`form-group ${icon ? "input-icon" : ""}`}>
|
||||
{icon && (
|
||||
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
|
||||
)}
|
||||
<input className="form-control" {...rest} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Input";
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import Input from "../Input";
|
||||
import { faSearch } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ModelSearchProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function ModelSearch({ value, onChange }: ModelSearchProps) {
|
||||
return (
|
||||
<Input
|
||||
icon={faSearch}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelSearch";
|
||||
@@ -0,0 +1,40 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.model-search-results {
|
||||
background-color: lighten($panel, 6%);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid lighten($panel, 12%);
|
||||
|
||||
.creator-name {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: $white;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
font-weight: 600;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
transition: 0.2s;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
|
||||
&.tt2 {
|
||||
color: #494ea9;
|
||||
background-color: #c6c7e1;
|
||||
}
|
||||
}
|
||||
|
||||
.model-details {
|
||||
color: rgba($white, 0.8);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import "./ModelSearchResults.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../Button";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChartSimple,
|
||||
faComment,
|
||||
faHeart,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ModelSearchResultsProps {
|
||||
data: any[];
|
||||
}
|
||||
|
||||
function shortenNumber(num: number): string {
|
||||
if (Math.abs(num) < 1e3) return num.toString();
|
||||
if (Math.abs(num) < 1e6) return (num / 1e3).toFixed(1) + "k";
|
||||
if (Math.abs(num) < 1e9) return (num / 1e6).toFixed(1) + "M";
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
export default function ModelSearchResults({ data }: ModelSearchResultsProps) {
|
||||
return (
|
||||
<div className="row g-3">
|
||||
{data.map((item) => {
|
||||
return (
|
||||
<div className="col-12 col-lg-6" key={item.id}>
|
||||
<div className="model-search-results p-3">
|
||||
<h5 className="fw-semibold mb-0">{item.name}</h5>
|
||||
<p className="creator-name">
|
||||
by{" "}
|
||||
<Link className="fw-medium" to={`/profile/${item.creator}`}>
|
||||
{item.creator}
|
||||
</Link>
|
||||
</p>
|
||||
<div className="d-flex align-items-end">
|
||||
<div className="flex-grow-1">
|
||||
<span
|
||||
className={`type-tag ${
|
||||
item.type === "Tacotron2" ? "tt2" : "tt2"
|
||||
}`}
|
||||
>
|
||||
{item.type}
|
||||
</span>
|
||||
<div className="d-flex gap-3 model-details fw-medium mt-3">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faHeart} className="me-2" />
|
||||
{item.likes && shortenNumber(item.likes)}
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faChartSimple} className="me-2" />
|
||||
{item.uses && shortenNumber(item.uses)}
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faComment} className="me-2" />
|
||||
{item.comments && shortenNumber(item.comments)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button label="Use Voice" small={true} to="/" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelSearchResults";
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import CheckableTag from "../CheckableTag";
|
||||
|
||||
interface ModelTagsProps {
|
||||
tags: string[];
|
||||
selectedTags: string[];
|
||||
onSelectTag: (tag: string) => void;
|
||||
}
|
||||
|
||||
export default function ModelTags({
|
||||
tags,
|
||||
selectedTags,
|
||||
onSelectTag,
|
||||
}: ModelTagsProps) {
|
||||
return (
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<CheckableTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
isSelected={selectedTags.includes(tag)}
|
||||
onToggle={onSelectTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelTags";
|
||||
@@ -1,24 +0,0 @@
|
||||
import { container } from "data/animation";
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
|
||||
interface PageContainerProps {
|
||||
children: React.ReactNode;
|
||||
padding?: boolean;
|
||||
}
|
||||
|
||||
export default function PageContainer({
|
||||
children,
|
||||
padding,
|
||||
}: PageContainerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={container}
|
||||
className={`${padding ? "container" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { panel } from "data/animation";
|
||||
|
||||
interface PanelProps {
|
||||
children: React.ReactNode;
|
||||
@@ -11,13 +9,11 @@ interface PanelProps {
|
||||
|
||||
export default function Panel({ children, padding, mb, mt }: PanelProps) {
|
||||
return (
|
||||
<motion.div className="container-panel" variants={panel}>
|
||||
<div
|
||||
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
|
||||
<div
|
||||
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
|
||||
${mb ? "mb-4" : ""} ${mt ? "mt-4" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Panel";
|
||||
@@ -11,8 +11,9 @@ interface Props {
|
||||
export default function SegmentButtons({ name, onChange, options = [], value: inValue = "" }: Props) {
|
||||
// const onClick = ({ target }: any) => onChange();
|
||||
return <ul {...{ className: "fy-segment-buttons" }}>
|
||||
{ options.map(({ label = "", value = "" },i: number) => <li {...{
|
||||
{ options.map(({ label = "", value = "" },key: number) => <li {...{
|
||||
...value === inValue && { className: "fy-selected-segment" },
|
||||
key,
|
||||
onClick: ({ target }: any) => onChange({ target: { name, type: "option", value } })
|
||||
}}>{ label }</li>) }
|
||||
</ul>;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.select__control {
|
||||
&.select {
|
||||
height: 48px;
|
||||
border-width: 2px;
|
||||
padding-left: 0.15rem;
|
||||
padding-right: 0.15rem;
|
||||
background-color: $input-dark;
|
||||
border-radius: 0.5rem;
|
||||
border-color: $input-dark !important;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.no-padding {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($input-dark, 5%);
|
||||
border-color: lighten($input-dark, 5%) !important;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: $primary !important;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
&.rounded-full {
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-option {
|
||||
background-color: $input-dark;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($input-dark, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.select-value {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
background: $input-dark;
|
||||
margin-top: 6px;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.select-separator {
|
||||
width: 1px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.select-placeholder {
|
||||
color: rgba($white, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.select-small {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import "./Select.scss";
|
||||
import {
|
||||
default as ReactSelect,
|
||||
Props as ReactSelectProps,
|
||||
} from "react-select";
|
||||
|
||||
interface SelectProps extends ReactSelectProps {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
rounded?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export default function Select({
|
||||
label,
|
||||
icon,
|
||||
rounded,
|
||||
small,
|
||||
...rest
|
||||
}: SelectProps) {
|
||||
const SelectFieldClass = {
|
||||
control: (state: any) =>
|
||||
state.isFocused
|
||||
? `select focused ${icon && "with-icon"} ${rounded && "rounded-full"}`
|
||||
: `select ${icon && "with-icon"} ${rounded && "rounded-full"}`,
|
||||
option: (state: any) =>
|
||||
state.isFocused ? "select-option" : "select-option",
|
||||
input: (state: any) => (state.isFocused ? "select-input" : "select-input"),
|
||||
placeholder: (state: any) =>
|
||||
state.isFocused ? "select-placeholder" : "select-placeholder",
|
||||
singleValue: (state: any) =>
|
||||
state.isFocused ? "select-value" : "select-value",
|
||||
menu: (state: any) =>
|
||||
state.isFocused ? "select-container" : "select-container",
|
||||
indicatorSeparator: (state: any) =>
|
||||
state.isFocused ? "select-separator" : "select-separator",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div
|
||||
className={`form-group ${icon ? "input-icon" : ""} ${
|
||||
small && "select-small"
|
||||
}`}
|
||||
>
|
||||
{icon && (
|
||||
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
|
||||
)}
|
||||
<div className="w-100">
|
||||
<ReactSelect
|
||||
classNamePrefix="select"
|
||||
classNames={SelectFieldClass}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Select";
|
||||
@@ -35,8 +35,8 @@ export default function ShareButton(props: ShareButtonProps) {
|
||||
<Button
|
||||
label="Share Link"
|
||||
icon={faLink}
|
||||
secondary
|
||||
small
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
-90deg,
|
||||
rgba($white, 10%) 25%,
|
||||
rgba(darken($white, 20%), 10%) 50%,
|
||||
rgba($white, 10%) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 0.25rem;
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton.rounded {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.skeleton.medium {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.skeleton.short {
|
||||
width: 20%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import "./Skeleton.scss";
|
||||
|
||||
interface SkeletonProps {
|
||||
type?: "full" | "medium" | "short";
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
export default function Skeleton({ type = "full", rounded }: SkeletonProps) {
|
||||
const skeletonClass = `skeleton ${type} ${rounded && "rounded"}`;
|
||||
|
||||
return <div className={skeletonClass}></div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Skeleton";
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
interface TextAreaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
}
|
||||
|
||||
export default function TextArea({ label, textArea, ...rest }: TextAreaProps) {
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div className="form-group">
|
||||
<textarea className="form-control" {...rest} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TextArea";
|
||||
@@ -1,5 +1,6 @@
|
||||
import AudioInput from "./AudioInput";
|
||||
import BasicVideo from "./BasicVideo";
|
||||
import Button from "./Button";
|
||||
import Check from "./Check";
|
||||
import Checkbox from "./Checkbox";
|
||||
import FileActions from "./FileActions";
|
||||
@@ -16,6 +17,7 @@ import Spinner from "./Spinner";
|
||||
export {
|
||||
AudioInput,
|
||||
BasicVideo,
|
||||
Button,
|
||||
Check,
|
||||
Checkbox,
|
||||
FileActions,
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function MobileMenu(props: MobileMenuProps) {
|
||||
icon={faUser}
|
||||
label="Profile"
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => history.push(url)}
|
||||
/>
|
||||
</>
|
||||
@@ -58,7 +58,7 @@ export default function MobileMenu(props: MobileMenuProps) {
|
||||
<Button
|
||||
label="Menu"
|
||||
icon={faBars}
|
||||
secondary
|
||||
variant="secondary"
|
||||
small
|
||||
onClick={handleMenuButtonClick}
|
||||
/>
|
||||
|
||||
@@ -1,33 +1,67 @@
|
||||
import React from "react";
|
||||
import Panel from "../../common/Panel";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import Button from "components/common/Button";
|
||||
|
||||
interface Props {
|
||||
childProps?: any;
|
||||
titleIcon?: JSX.Element;
|
||||
titleComponent?: React.ComponentType;
|
||||
title?: string;
|
||||
subText: string;
|
||||
showButtons: boolean;
|
||||
actionButtons?: JSX.Element;
|
||||
interface PageHeaderProps {
|
||||
titleIcon?: IconDefinition;
|
||||
title: string | React.ReactNode;
|
||||
subText: string | React.ReactNode;
|
||||
full?: boolean;
|
||||
showButton?: boolean;
|
||||
extension?: React.ReactNode;
|
||||
buttonLabel?: string;
|
||||
buttonVariant?: "primary" | "secondary" | "danger";
|
||||
buttonTo?: string;
|
||||
buttonIcon?: IconDefinition;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
titleIcon,
|
||||
title,
|
||||
subText,
|
||||
full,
|
||||
showButton,
|
||||
extension,
|
||||
buttonLabel,
|
||||
buttonVariant = "primary",
|
||||
buttonTo,
|
||||
buttonIcon,
|
||||
buttonOnClick,
|
||||
}: PageHeaderProps) {
|
||||
const icon = (
|
||||
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
|
||||
);
|
||||
|
||||
export default function PageHeader({ actionButtons, childProps, showButtons, subText, titleComponent: Title, title, titleIcon }: Props) {
|
||||
return <div className="container-panel hero-section py-4">
|
||||
<div className="panel">
|
||||
<div className="p-3 py-4 p-md-4">
|
||||
{ Title ? <Title {...childProps }/> : <h1 className="fw-bold text-center text-md-start">
|
||||
{ titleIcon }
|
||||
{ title }
|
||||
</h1> }
|
||||
<p className="text-center text-md-start pt-1">{ subText }</p>
|
||||
{ showButtons && (
|
||||
<div className="d-flex flex-column flex-md-row gap-3 justify-content-center justify-content-md-start mt-4">
|
||||
{ actionButtons }
|
||||
return (
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
<Panel padding>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<div className="d-flex">
|
||||
<h2 className="fw-bold flex-grow-1">
|
||||
{icon}
|
||||
{title}
|
||||
</h2>
|
||||
<div className="d-none d-md-block">
|
||||
{showButton && (
|
||||
<Button
|
||||
icon={buttonIcon}
|
||||
variant={buttonVariant}
|
||||
label={buttonLabel}
|
||||
to={buttonTo}
|
||||
onClick={buttonOnClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p>{subText}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{extension && <div>{extension}</div>}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
// export default PageHeader;
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './PageHeader';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PageHeader";
|
||||
+3
-1
@@ -13,6 +13,7 @@ interface PageHeaderModelViewProps {
|
||||
extras?: React.ReactNode;
|
||||
view?: "regular" | "edit" | "delete";
|
||||
modelType?: "TTS" | "V2V";
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export default function PageHeaderModelView({
|
||||
@@ -25,13 +26,14 @@ export default function PageHeaderModelView({
|
||||
extras,
|
||||
view = "regular",
|
||||
modelType = "TTS",
|
||||
full,
|
||||
}: PageHeaderModelViewProps) {
|
||||
const icon = (
|
||||
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pt-2 pt-lg-4 pb-4">
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
{view === "regular" && (
|
||||
<Panel padding>
|
||||
<div className="row gy-3">
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
<Button
|
||||
label={t("loginButton")}
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
handleNavLinkClick();
|
||||
@@ -194,7 +194,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
icon={faUser}
|
||||
label={t("profileButton")}
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push(url);
|
||||
handleNavLinkClick();
|
||||
@@ -209,7 +209,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
icon={faSignOutAlt}
|
||||
label={t("logOutButton")}
|
||||
small
|
||||
danger
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
handleNavLinkClick();
|
||||
@@ -271,7 +271,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
<li className="sidebar-heading">{t("videoTitle")}</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/face-animation"
|
||||
to="/face-animator"
|
||||
activeClassName="active-link"
|
||||
onClick={handleNavLinkClick}
|
||||
>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import PageHeader from './PageHeader';
|
||||
|
||||
export {
|
||||
PageHeader,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { JobState } from "@storyteller/components/src/jobs/JobStates";
|
||||
import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { InferenceJobsContext } from 'context';
|
||||
|
||||
interface Props {
|
||||
children?: any;
|
||||
byCategory?: any;
|
||||
}
|
||||
|
||||
export default function InferenceJobs({ children, byCategory }: Props) {
|
||||
const processStatus = (job: InferenceJob) => {
|
||||
switch (job.jobState) {
|
||||
case JobState.PENDING:
|
||||
case JobState.UNKNOWN: return 0;
|
||||
case JobState.STARTED: return 1
|
||||
case JobState.ATTEMPT_FAILED: return 2;
|
||||
case JobState.COMPLETE_FAILURE:
|
||||
case JobState.DEAD: return 3;
|
||||
case JobState.COMPLETE_SUCCESS: return 4;
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
return <InferenceJobsContext.Provider {...{ value: { byCategory, processStatus } }}>
|
||||
{ children }
|
||||
</InferenceJobsContext.Provider>
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './InferenceJobs';
|
||||
@@ -0,0 +1,3 @@
|
||||
import InferenceJobs from "./InferenceJobs";
|
||||
|
||||
export { InferenceJobs };
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createContext } from 'react';
|
||||
import { JobState } from "@storyteller/components/src/jobs/JobStates";
|
||||
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
|
||||
function initInferenceJobsByCategoryMap() : Map<FrontendInferenceJobType, InferenceJob[]> {
|
||||
let inferenceJobsByCategory = new Map();
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.FaceAnimation, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.TextToSpeech, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceConversion, []);
|
||||
return inferenceJobsByCategory;
|
||||
};
|
||||
|
||||
const processStatus = (job: InferenceJob) => {
|
||||
switch (job.jobState) {
|
||||
case JobState.PENDING:
|
||||
case JobState.UNKNOWN: return 0;
|
||||
case JobState.STARTED: return 1
|
||||
case JobState.ATTEMPT_FAILED: return 2;
|
||||
case JobState.COMPLETE_FAILURE:
|
||||
case JobState.DEAD: return 3;
|
||||
case JobState.COMPLETE_SUCCESS: return 4;
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// export default createContext({
|
||||
// byCategory: initInferenceJobsByCategoryMap(),
|
||||
// nahmean: false
|
||||
// });
|
||||
|
||||
export default createContext({ byCategory: initInferenceJobsByCategoryMap(), processStatus });
|
||||
@@ -0,0 +1,3 @@
|
||||
import InferenceJobsContext from "./InferenceJobsContext";
|
||||
|
||||
export { InferenceJobsContext };
|
||||
@@ -2,7 +2,8 @@ import useFile from './useFile';
|
||||
import useHover from './useHover';
|
||||
import useId from './useId';
|
||||
import useIdempotency from './useIdempotency';
|
||||
import useInferenceJobs from './useInferenceJobs';
|
||||
import useLocalize from './useLocalize';
|
||||
import useVideo from './useVideo';
|
||||
|
||||
export { useFile, useHover, useId, useIdempotency, useLocalize, useVideo };
|
||||
export { useFile, useHover, useId, useIdempotency, useInferenceJobs, useLocalize, useVideo };
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './useInferenceJobs';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useContext } from 'react';
|
||||
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { InferenceJobsContext } from 'context';
|
||||
|
||||
|
||||
interface Props {
|
||||
inferenceJobsByCategory?: any;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export default function useInferenceJobs({ inferenceJobsByCategory, type }: Props) {
|
||||
const { byCategory, processStatus } = useContext(InferenceJobsContext);
|
||||
const jbs = byCategory.get(FrontendInferenceJobType.FaceAnimation) || [];
|
||||
|
||||
return {
|
||||
inferenceJobs: jbs.map((job,i) => ({
|
||||
...job!,
|
||||
statusIndex: processStatus(job!)
|
||||
})),
|
||||
processStatus
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useSearch";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
export default function useSearch(initialData: any[]) {
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let tempData = [...initialData];
|
||||
|
||||
if (searchTerm) {
|
||||
tempData = tempData.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTags.length) {
|
||||
tempData = tempData.filter((item) =>
|
||||
selectedTags.every((tag) => item.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
return tempData;
|
||||
}, [searchTerm, selectedTags, initialData]);
|
||||
|
||||
return {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
filteredData,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useShortenNumber";
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function useShortenNumber(num: number): string {
|
||||
if (Math.abs(num) < 1e3) return num.toString();
|
||||
if (Math.abs(num) < 1e6) return (num / 1e3).toFixed(1) + "k";
|
||||
if (Math.abs(num) < 1e9) return (num / 1e6).toFixed(1) + "M";
|
||||
return num.toString();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useTimeAgo";
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function useTimeAgo(timestamp: string) {
|
||||
const [timeAgo, setTimeAgo] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const calculateTimeAgo = () => {
|
||||
const now = new Date();
|
||||
const then = new Date(timestamp);
|
||||
|
||||
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(months / 12);
|
||||
|
||||
if (years > 0) {
|
||||
setTimeAgo(`${years} ${years === 1 ? "year" : "years"} ago`);
|
||||
} else if (months > 0) {
|
||||
setTimeAgo(`${months} ${months === 1 ? "month" : "months"} ago`);
|
||||
} else if (days > 0) {
|
||||
setTimeAgo(`${days} ${days === 1 ? "day" : "days"} ago`);
|
||||
} else if (hours > 0) {
|
||||
setTimeAgo(`${hours} ${hours === 1 ? "hour" : "hours"} ago`);
|
||||
} else if (minutes > 0) {
|
||||
setTimeAgo(`${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`);
|
||||
} else {
|
||||
setTimeAgo(`${seconds} ${seconds === 1 ? "second" : "seconds"} ago`);
|
||||
}
|
||||
};
|
||||
|
||||
calculateTimeAgo();
|
||||
|
||||
// Update the time difference every minute
|
||||
const intervalId = setInterval(calculateTimeAgo, 60000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [timestamp]);
|
||||
|
||||
return timeAgo;
|
||||
}
|
||||
|
||||
export default useTimeAgo;
|
||||
@@ -1,32 +1,40 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import AppTranslated from './AppTranslated';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import AppTranslated from "./AppTranslated";
|
||||
|
||||
const IS_IOS : boolean = /iPad|iPhone|iPod/.test(navigator.platform || "");
|
||||
const IS_IOS: boolean = /iPad|iPhone|iPod/.test(navigator.platform || "");
|
||||
|
||||
const enableSpectrograms = !IS_IOS;
|
||||
|
||||
const flashVocodesNotice = new URLSearchParams(window.location.search).has('vocodes');
|
||||
const flashVocodesNotice = new URLSearchParams(window.location.search).has(
|
||||
"vocodes"
|
||||
);
|
||||
|
||||
const designSystemClass = 'fakeyou-refresh';
|
||||
const designSystemClass = "fakeyou-refresh";
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(designSystemClass);
|
||||
document.getElementsByTagName("html")[0].classList.add(designSystemClass);
|
||||
|
||||
// We can't include Bootstrap CSS along with Bulma since some of the class names conflict.
|
||||
// TODO(echelon): Once ported, statically move CSS to "index.html".
|
||||
const bootstrapCss = document.createElement("link");
|
||||
bootstrapCss.setAttribute("rel", "stylesheet");
|
||||
bootstrapCss.setAttribute("crossorigin", "anonymous");
|
||||
bootstrapCss.setAttribute("integrity", "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3");
|
||||
bootstrapCss.setAttribute("href", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css");
|
||||
document.getElementsByTagName("head")[0].appendChild(bootstrapCss);
|
||||
// const bootstrapCss = document.createElement("link");
|
||||
// bootstrapCss.setAttribute("rel", "stylesheet");
|
||||
// bootstrapCss.setAttribute("crossorigin", "anonymous");
|
||||
// bootstrapCss.setAttribute("integrity", "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3");
|
||||
// bootstrapCss.setAttribute("href", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css");
|
||||
// document.getElementsByTagName("head")[0].appendChild(bootstrapCss);
|
||||
|
||||
const bootstrapJs = document.createElement("script");
|
||||
bootstrapJs.setAttribute("rel", "stylesheet");
|
||||
bootstrapJs.setAttribute("crossorigin", "anonymous");
|
||||
bootstrapJs.setAttribute("integrity", "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p");
|
||||
bootstrapJs.setAttribute("src", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js");
|
||||
bootstrapJs.setAttribute(
|
||||
"integrity",
|
||||
"sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
);
|
||||
bootstrapJs.setAttribute(
|
||||
"src",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
);
|
||||
document.getElementsByTagName("body")[0].appendChild(bootstrapJs);
|
||||
|
||||
// NB: Posthog turned off for most users (anonymous users) due to cost.
|
||||
@@ -37,11 +45,11 @@ document.getElementsByTagName("body")[0].appendChild(bootstrapJs);
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<AppTranslated
|
||||
enableSpectrograms={enableSpectrograms}
|
||||
flashVocodesNotice={flashVocodesNotice}
|
||||
/>
|
||||
enableSpectrograms={enableSpectrograms}
|
||||
flashVocodesNotice={flashVocodesNotice}
|
||||
/>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Override the $spacers variable
|
||||
$spacers: (
|
||||
0: 0,
|
||||
1: 0.25rem,
|
||||
2: 0.5rem,
|
||||
3: 1rem,
|
||||
4: 1.75rem,
|
||||
5: 2rem,
|
||||
);
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@@ -21,11 +21,28 @@
|
||||
width: 0;
|
||||
height: 100%;
|
||||
margin-left: -260px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-transition: all 0.2s ease;
|
||||
-moz-transition: all 0.2s ease;
|
||||
-o-transition: all 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba($secondary, 1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba($panel, 1) !important;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
|
||||
@@ -12,6 +12,19 @@ styles with important tags:
|
||||
// Once the rollout is complete, this outer wrapper class can be removed.
|
||||
|
||||
.fakeyou-refresh {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba($secondary, 1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba($bg, 1) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
margin-bottom: 0;
|
||||
@@ -135,7 +148,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1080px !important;
|
||||
max-width: 1200px !important;
|
||||
}
|
||||
|
||||
.container,
|
||||
@@ -155,11 +168,6 @@ styles with important tags:
|
||||
padding-bottom: 0.4rem !important;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 2rem !important;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: rgba($white, 0.8);
|
||||
font-size: 17px;
|
||||
@@ -275,18 +283,42 @@ styles with important tags:
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.bg-panel {
|
||||
background-color: lighten($panel, 1%);
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: $panel;
|
||||
border-color: #ffffff12 !important;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0 1px rgba(0 0 0 / 16%), 0 70px 65px rgb(0 0 0 / 8%),
|
||||
0 30px 30px rgb(0 0 0 / 7%), 0 15px 15px rgb(0 0 0 / 5%),
|
||||
0 10px 8px rgb(0 0 0 / 3%), 0 4px 4px rgb(0 0 0 / 2%),
|
||||
0 2px 2px rgb(0 0 0 / 1%) !important;
|
||||
|
||||
&.panel-clear {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.video-bg {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
.panel-border {
|
||||
border-color: #ffffff12 !important;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.panel-inner {
|
||||
background-color: lighten($panel, 4%);
|
||||
}
|
||||
@@ -487,7 +519,7 @@ styles with important tags:
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.panel-border {
|
||||
.pricing-border {
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0px 0px 0px 3px $primary;
|
||||
}
|
||||
@@ -790,7 +822,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.select-placeholder {
|
||||
color: $white !important;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
@@ -921,10 +953,6 @@ styles with important tags:
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="fakeyou"] > .tippy-svg-arrow {
|
||||
fill: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="range-slider"] {
|
||||
margin-top: -5px;
|
||||
background-color: lighten($panel, 10%);
|
||||
@@ -932,6 +960,23 @@ styles with important tags:
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="top"]
|
||||
> .tippy-arrow::before {
|
||||
border-top-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="bottom"]
|
||||
> .tippy-arrow::before {
|
||||
border-bottom-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="left"]
|
||||
> .tippy-arrow::before {
|
||||
border-left-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="right"]
|
||||
> .tippy-arrow::before {
|
||||
border-right-color: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.sliding-content {
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
@@ -1527,15 +1572,6 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.p-lg-4 {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
|
||||
.px-lg-4 {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 2rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -1581,12 +1617,18 @@ textarea {
|
||||
padding-left: var(--bs-gutter-x, 1rem);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 1080px;
|
||||
max-width: 1200px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
|
||||
&.panel-clear {
|
||||
padding-left: 0rem;
|
||||
padding-right: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul.patrons-list {
|
||||
@@ -1602,10 +1644,6 @@ textarea {
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.p-md-4 {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LoginPage } from "./pages/login/LoginPage";
|
||||
import { ModerationFc } from "./pages/moderation/moderation_main/ModerationFc";
|
||||
import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list/ModerationIpBanListFc";
|
||||
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
|
||||
import { Media } from "./pages/media";
|
||||
import { LipsyncEditor } from "./pages/lipsync";
|
||||
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
|
||||
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
|
||||
@@ -91,6 +90,9 @@ import VcModelDeletePage from "./pages/vc/vc_model_delete/VcModelDeletePage";
|
||||
import SideNav from "components/layout/SideNav/SideNav";
|
||||
import MobileMenu from "components/layout/MobileMenu/MobileMenu";
|
||||
import { TopNav } from "./nav/TopNav";
|
||||
import { TestingPage } from "./pages/testing/TestingPage";
|
||||
import TtsModelSearchPage from "./pages/tts/tts_model_search/TtsModelSearchPage";
|
||||
import MediaPage from "./pages/media/MediaPage";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -319,6 +321,14 @@ class PageContainer extends React.Component<
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<MediaPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/dev-tts">
|
||||
<TtsModelSearchPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/tts/result/:token/edit">
|
||||
<TtsResultEditPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
@@ -459,11 +469,7 @@ class PageContainer extends React.Component<
|
||||
<Route path="/category/create">
|
||||
<CreateCategoryPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<Media />
|
||||
</Route>
|
||||
|
||||
|
||||
<Route path="/moderation/user/list">
|
||||
<ModerationUserListFc
|
||||
sessionWrapper={this.props.sessionWrapper}
|
||||
@@ -592,7 +598,7 @@ class PageContainer extends React.Component<
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/face-animation">
|
||||
<Route path="/face-animator">
|
||||
<LipsyncEditor
|
||||
{...{
|
||||
enqueueInferenceJob: this.props.enqueueInferenceJob,
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
function PageHeaderWithImage(props: Props) {
|
||||
return (
|
||||
<div className="container-panel hero-section pt-3 pb-4 pt-lg-4">
|
||||
<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">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faClock,
|
||||
faHeadphonesSimple,
|
||||
faDownload,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
@@ -120,12 +120,10 @@ function SessionVoiceConversionResultsList(props: Props) {
|
||||
let audioLink = new BucketConfig().getGcsUrl(
|
||||
job.maybeResultPublicBucketMediaPath
|
||||
);
|
||||
//let ttsPermalink = `/tts/result/${job.maybeResultToken}`;
|
||||
let audioPermalink = `/media/${job.maybeResultToken}`;
|
||||
|
||||
let wavesurfers = <SessionTtsAudioPlayer filename={audioLink} />;
|
||||
|
||||
let audioDownloadFilename = `fakeyou-${job.jobToken}.wav`;
|
||||
|
||||
results.push(
|
||||
<div key={job.jobToken}>
|
||||
{/*<div className="message-header">
|
||||
@@ -154,28 +152,16 @@ function SessionVoiceConversionResultsList(props: Props) {
|
||||
{wavesurfers}
|
||||
|
||||
<div className="mt-2">
|
||||
{/*<Link
|
||||
to={ttsPermalink}
|
||||
<Link
|
||||
to={audioPermalink}
|
||||
onClick={() => {
|
||||
Analytics.ttsClickResultLink();
|
||||
//Analytics.ttsClickResultLink();
|
||||
}}
|
||||
className="fw-semibold"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="me-2" />
|
||||
{t("common.SessionTtsInferenceResults.result.shareDownload")}
|
||||
</Link>*/}
|
||||
|
||||
<a
|
||||
className=" btn btn-primary w-100 mt-4"
|
||||
href={audioLink}
|
||||
onClick={() => {
|
||||
Analytics.voiceConversionClickDownload();
|
||||
}}
|
||||
download={audioDownloadFilename}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} className="me-2" />
|
||||
{t("resultsAudioDownload")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,7 +214,7 @@ function FooterNav(props: Props) {
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to="/face-animation">{t("productFaceAnimator")}</Link>
|
||||
<Link to="/face-animator">{t("productFaceAnimator")}</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
||||
@@ -245,7 +245,7 @@ function TopNav(props: Props) {
|
||||
</Link>
|
||||
</li>
|
||||
<li data-bs-toggle="offcanvas">
|
||||
<Link className="dropdown-item" to="/face-animation">
|
||||
<Link className="dropdown-item" to="/face-animator">
|
||||
<FontAwesomeIcon
|
||||
icon={faFaceViewfinder}
|
||||
className="me-2"
|
||||
|
||||
@@ -8,8 +8,6 @@ import { Trans } from "react-i18next";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faVolumeHigh,
|
||||
faVideo,
|
||||
faImage,
|
||||
faTags,
|
||||
faHandsHelping,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -118,18 +116,6 @@ function ContributeIndexPage(props: Props) {
|
||||
Upload Vocoder
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<Link to="/upload/w2l_video" className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faVideo} className="me-3" />
|
||||
{t("pages.contributeIndex.buttonUploadW2lVideo")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<Link to="/upload/w2l_photo" className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faImage} className="me-3" />
|
||||
{t("pages.contributeIndex.buttonUploadW2lPhoto")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +142,7 @@ function ContributeIndexPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-panel pt-3">
|
||||
<div className="container-panel pt-3 pb-5">
|
||||
<div className="panel p-3 py-4 p-md-4 text-center text-lg-start">
|
||||
<h2 className="fw-bold">
|
||||
<FontAwesomeIcon icon={faHandsHelping} className="me-3" />
|
||||
|
||||
@@ -137,8 +137,11 @@ function LandingPage(props: Props) {
|
||||
const randomHeroImage = useMemo(() => {
|
||||
const images = [
|
||||
// Main Images:
|
||||
"mascot/kitsune_pose2.webp",
|
||||
// "mascot/kitsune_pose2.webp",
|
||||
// "mascot/may4th.webp",
|
||||
"mascot/halloween_1.webp",
|
||||
"mascot/halloween_2.webp",
|
||||
"mascot/halloween_3.webp",
|
||||
];
|
||||
|
||||
return images[Math.floor(Math.random() * images.length)];
|
||||
@@ -153,7 +156,7 @@ function LandingPage(props: Props) {
|
||||
icon={faSparkles}
|
||||
message="Have you tried our new Face Animator? Turn photos of faces into animated lip-synced videos with just a picture and some audio!"
|
||||
alertVariant="new"
|
||||
link="/face-animation"
|
||||
link="/face-animator"
|
||||
linkText="Try it now"
|
||||
/>
|
||||
|
||||
@@ -297,7 +300,7 @@ function LandingPage(props: Props) {
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/face-animation"
|
||||
to="/face-animator"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
@@ -317,7 +320,7 @@ function LandingPage(props: Props) {
|
||||
{t("productFaceAnimatorText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/face-animation" className="btn btn-square mt-1">
|
||||
<Link to="/face-animator" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -325,30 +328,10 @@ function LandingPage(props: Props) {
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-w2l.webp"
|
||||
alt="Video Lip Sync"
|
||||
alt="Face Animator"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/video"
|
||||
className="panel panel-select d-flex flex-column align-items-center h-auto pb-4"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-center w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productVideoTitle")}
|
||||
</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white mb-0">
|
||||
{t("productVideoText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/video" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
// import { a } from '@react-spring/web';
|
||||
import {
|
||||
GetPendingTtsJobCount,
|
||||
GetPendingTtsJobCountIsOk,
|
||||
GetPendingTtsJobCountSuccessResponse,
|
||||
} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount";
|
||||
// import { springs } from "resources";
|
||||
import { useInferenceJobs } from "hooks";
|
||||
import { Button } from 'components/common';
|
||||
import { Analytics } from "common/Analytics";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronRight, faHourglass1, faRemove, faTrophy, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
|
||||
|
||||
export default function FaceAnimatorJobs({ t }: { t: any }) {
|
||||
const { inferenceJobs = [] } = useInferenceJobs({ type: 0 });
|
||||
const [pending, pendingSet] = useState<GetPendingTtsJobCountSuccessResponse>({
|
||||
success: true,
|
||||
pending_job_count: 0,
|
||||
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
|
||||
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
|
||||
});
|
||||
const statusIcons = [faHourglass1,faHourglass1,faWarning,faRemove,faTrophy];
|
||||
const statusTxt = (which: number, config = {}) => ["animationPending","animationInProgress","animationFailed","animationDead","animationSuccess"].map((str,i) => t(`status.${str}`,config))[which];
|
||||
|
||||
const processFail = (fail = "") => {
|
||||
switch (fail) {
|
||||
case "face_not_detected": return "Face not detected, try another picture";
|
||||
default: return "Uknown failure";
|
||||
}
|
||||
};
|
||||
|
||||
// const transitions = useTransition(inferenceJobs, { // not today
|
||||
// ...springs.soft,
|
||||
// from: { opacity: 0, position: "absolute" },
|
||||
// enter: { opacity: 1, position: "relative" },
|
||||
// leave: { opacity: 0, position: "absolute" },
|
||||
// });
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
const response = await GetPendingTtsJobCount();
|
||||
if (GetPendingTtsJobCountIsOk(response)) {
|
||||
if (
|
||||
response.cache_time.getTime() > pending.cache_time.getTime()
|
||||
) {
|
||||
pendingSet(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,
|
||||
pending.refresh_interval_millis
|
||||
);
|
||||
const interval = setInterval(async () => fetch(), refreshInterval);
|
||||
fetch();
|
||||
return () => clearInterval(interval);
|
||||
}, [pending]);
|
||||
|
||||
return inferenceJobs.length ? <div {...{ className: "face-animator-jobs panel" }}>
|
||||
<h5>{ t("headings.yourJobs") }</h5>
|
||||
{ inferenceJobs.map((job, key) => {
|
||||
return <div {...{ className: "panel face-animator-job", key }}>
|
||||
<FontAwesomeIcon {...{ className: `job-status-icon job-status-${job.statusIndex}`, icon: statusIcons[job.statusIndex] }}/>
|
||||
<div {...{ className: "job-details" }}>
|
||||
<h4>
|
||||
{ statusTxt(job.statusIndex,{ attemptCount: job.attemptCount || "" }) }
|
||||
</h4>
|
||||
<span>
|
||||
{ job.maybeFailureCategory ? `${ processFail(job.maybeFailureCategory) }` : "" }
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
job.maybeResultToken ? <Button {...{
|
||||
href: `media/${job.maybeResultToken}`,
|
||||
icon: faChevronRight,
|
||||
iconFlip: true,
|
||||
label: t("inputs.viewResult"),
|
||||
onClick:() => Analytics.voiceConversionClickDownload()
|
||||
}} />: null
|
||||
}
|
||||
</div>
|
||||
}).reverse()}
|
||||
</div> : null;
|
||||
};
|
||||
+8
-79
@@ -1,82 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { animated } from '@react-spring/web';
|
||||
import {
|
||||
GetPendingTtsJobCount,
|
||||
GetPendingTtsJobCountIsOk,
|
||||
GetPendingTtsJobCountSuccessResponse,
|
||||
} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount";
|
||||
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { JobState } from "@storyteller/components/src/jobs/JobStates";
|
||||
import { Analytics } from "common/Analytics";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import React from 'react';
|
||||
import { a } from '@react-spring/web';
|
||||
import { FaceAnimatorSlide } from "./FaceAnimatorTypes";
|
||||
|
||||
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
|
||||
|
||||
export default function FaceAnimatorSuccess({ audioProps, imageProps, style, enqueueInferenceJob, sessionSubscriptionsWrapper, inferenceJobsByCategory, t }: FaceAnimatorSlide) {
|
||||
const [pending, pendingSet] = useState<GetPendingTtsJobCountSuccessResponse>({
|
||||
success: true,
|
||||
pending_job_count: 0,
|
||||
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
|
||||
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
|
||||
});
|
||||
const inferenceJobs = inferenceJobsByCategory.get(FrontendInferenceJobType.FaceAnimation)!;
|
||||
const mediaLink = (path: string) => new BucketConfig().getGcsUrl(path);
|
||||
const processStatus = (job: InferenceJob) => {
|
||||
switch (job.jobState) {
|
||||
case JobState.PENDING:
|
||||
case JobState.UNKNOWN: return t("status.animationPending");
|
||||
case JobState.STARTED: return t("status.animationInProgress");
|
||||
case JobState.ATTEMPT_FAILED: return t("status.animationPending",{ attemptCount: job.attemptCount });
|
||||
case JobState.COMPLETE_FAILURE:
|
||||
case JobState.DEAD: return t("status.animationDead");
|
||||
case JobState.COMPLETE_SUCCESS: return t("status.animationSuccess");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
const response = await GetPendingTtsJobCount();
|
||||
if (GetPendingTtsJobCountIsOk(response)) {
|
||||
if (
|
||||
response.cache_time.getTime() > pending.cache_time.getTime()
|
||||
) {
|
||||
pendingSet(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,
|
||||
pending.refresh_interval_millis
|
||||
);
|
||||
const interval = setInterval(async () => fetch(), refreshInterval);
|
||||
fetch();
|
||||
return () => clearInterval(interval);
|
||||
}, [pending]);
|
||||
|
||||
return <animated.div {...{ className: "lipsync-success", style }}>
|
||||
<div {...{ className: "face-animator-results" }}>{
|
||||
inferenceJobs.map((job: any) => !job.maybeResultToken ?
|
||||
<div>
|
||||
<h3>{ processStatus(job) }</h3>
|
||||
<span {...{ className: "job-id" }}>id: { job.jobToken }</span>
|
||||
</div> : <div>
|
||||
<h3>{ t("status.animationComplete") }</h3>
|
||||
<span {...{ className: "job-id" }}>id: { job.jobToken }</span>
|
||||
<a {...{
|
||||
className: "btn btn-primary w-100 mt-4",
|
||||
download: `fakeyou-${job.jobToken}.mp4`,
|
||||
href: mediaLink(job.maybeResultPublicBucketMediaPath),
|
||||
onClick:() => Analytics.voiceConversionClickDownload()
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faDownload} className="me-2" />
|
||||
{ t("inputs.downloadFile") }
|
||||
</a>
|
||||
</div>).reverse()
|
||||
}</div>
|
||||
</animated.div>;
|
||||
export default function FaceAnimatorSuccess({ style, t }: FaceAnimatorSlide) {
|
||||
return <a.div {...{ className: "lipsync-success", style }}>
|
||||
<div {...{ className: "face-animator-results" }}>
|
||||
<h4>Animation added to queue!</h4>
|
||||
</div>
|
||||
</a.div>;
|
||||
};
|
||||
@@ -86,7 +86,7 @@ export default function FaceAnimatorTitle({ ...rest }) {
|
||||
index: page,
|
||||
}}
|
||||
/>
|
||||
<p {...{ className: "progress-description" }}>{t("headings.subtitle")}</p>
|
||||
<p {...{ className: "progress-description fa-light-txt" }}>{t("headings.subtitle")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,22 @@
|
||||
$under992: max-width 992px;
|
||||
$over992: min-width 992px;
|
||||
|
||||
@mixin FAltTxt { color: #b6b6bb; }
|
||||
@mixin FApanel {
|
||||
padding: 1.5rem;
|
||||
@include breakpoint($under992) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.face-animator-main {
|
||||
@include FApanel;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
h1 { line-height: 1em;}
|
||||
};
|
||||
|
||||
.lipsync-editor {
|
||||
padding-top: 2rem;
|
||||
|
||||
@@ -35,6 +51,7 @@ $over992: min-width 992px;
|
||||
|
||||
.input-block {
|
||||
legend {
|
||||
@include FAltTxt;
|
||||
font-size: .875rem;
|
||||
};
|
||||
flex-grow: 1;
|
||||
@@ -77,6 +94,7 @@ $over992: min-width 992px;
|
||||
}
|
||||
|
||||
.progress-description {
|
||||
@include FAltTxt;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -97,11 +115,6 @@ $over992: min-width 992px;
|
||||
}
|
||||
}
|
||||
|
||||
.lipsync-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lipsync-working,
|
||||
.lipsync-working-notice {
|
||||
display: flex;
|
||||
@@ -122,31 +135,96 @@ $over992: min-width 992px;
|
||||
.lipsync-success {
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
|
||||
h3 {
|
||||
h4 {
|
||||
@include FAltTxt;
|
||||
text-align: center;
|
||||
margin: 1rem auto 0 0;
|
||||
}
|
||||
|
||||
.job-id {
|
||||
color: rgba(#fff, 0.66);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.face-animator-mobile-sample {
|
||||
.face-animator-mobile-sample {
|
||||
padding: 1rem;
|
||||
|
||||
@include breakpoint($over992) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.face-animator-wide-sample {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
|
||||
@include breakpoint($under992) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
video { position: absolute ;}
|
||||
}
|
||||
|
||||
.face-animator-jobs {
|
||||
@include FApanel;
|
||||
h5 {
|
||||
@include FAltTxt;
|
||||
line-height: 1em;
|
||||
}
|
||||
margin-top: 1rem;
|
||||
.face-animator-job {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
@include breakpoint($over992) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.face-animator-wide-sample {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
|
||||
@include breakpoint($under992) {
|
||||
display: none;
|
||||
margin-top: .5rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
video { position: absolute ;}
|
||||
.job-status-icon {
|
||||
// background-color: red;
|
||||
padding: .5rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: .5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.job-status-1 { background-color: rgba(#89f, .6); } // started = blue
|
||||
.job-status-2 { background-color: rgba(#efb700, .9); } // failed = yellow
|
||||
.job-status-3 { background-color: rgba($primary, .6); } // dead = red
|
||||
.job-status-4 { background-color: $success } // success = green
|
||||
|
||||
.job-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
// height: 100%;
|
||||
width: min-content;
|
||||
|
||||
h4, span { white-space: nowrap; }
|
||||
|
||||
h4 {
|
||||
line-height: 1em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
a {
|
||||
white-space: nowrap;
|
||||
margin: 0 0 0 auto;
|
||||
|
||||
@include breakpoint($under992) {
|
||||
margin: .5rem 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-id {
|
||||
@include FAltTxt;
|
||||
line-height: 1em;
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,15 @@ import {
|
||||
// EnqueueFaceAnimationRequest,
|
||||
} from "@storyteller/components/src/api/face_animation/EnqueueFaceAnimation";
|
||||
import FaceAnimatorTitle from "./FaceAnimatorTitle";
|
||||
import FaceAnimatorInput from './FaceAnimatorInput';
|
||||
import FaceAnimatorWorking from './FaceAnimatorWorking';
|
||||
import FaceAnimatorInput from "./FaceAnimatorInput";
|
||||
import FaceAnimatorWorking from "./FaceAnimatorWorking";
|
||||
import FaceAnimatorSuccess from "./FaceAnimatorSuccess";
|
||||
import FaceAnimatorJobs from "./FaceAnimatorJobs";
|
||||
import { FaceAnimatorCore } from "./FaceAnimatorTypes";
|
||||
import { BasicVideo } from "components/common";
|
||||
import "./LipsyncEditor.scss";
|
||||
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
|
||||
|
||||
export default function LipsyncEditor({
|
||||
enqueueInferenceJob,
|
||||
@@ -34,6 +36,7 @@ export default function LipsyncEditor({
|
||||
...rest
|
||||
}: FaceAnimatorCore) {
|
||||
const { t } = useLocalize("FaceAnimator");
|
||||
usePrefixedDocumentTitle("AI Face Animator");
|
||||
|
||||
// the ready states are set by functions which run after the upload input animation is completed, which then illuminates the respective checkmark in a staggered way to draw attention to the workflow, and reduces concurrent animations
|
||||
|
||||
@@ -46,23 +49,26 @@ export default function LipsyncEditor({
|
||||
const [index, indexSet] = useState<number>(0); // index = slideshow slide position
|
||||
|
||||
//const [animationStyle,animationStyleSet] = useState(0);
|
||||
const [frameDimensions,frameDimensionsSet] = useState("twitter_square");
|
||||
const [removeWatermark,removeWatermarkSet] = useState(false);
|
||||
const [disableFaceEnhancement,disableFaceEnhancementSet] = useState(false);
|
||||
const [still,stillSet] = useState(false);
|
||||
const [frameDimensions, frameDimensionsSet] = useState("twitter_square");
|
||||
const [removeWatermark, removeWatermarkSet] = useState(false);
|
||||
const [disableFaceEnhancement, disableFaceEnhancementSet] = useState(false);
|
||||
const [still, stillSet] = useState(false);
|
||||
|
||||
//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);
|
||||
stillSet(false);
|
||||
frameDimensionsSet("twitter_square");
|
||||
removeWatermarkSet(false);
|
||||
const clearInputs = () => {
|
||||
//animationStyleSet(0);
|
||||
stillSet(false);
|
||||
frameDimensionsSet("twitter_square");
|
||||
removeWatermarkSet(false);
|
||||
disableFaceEnhancementSet(false);
|
||||
}
|
||||
};
|
||||
|
||||
const makeRequest = (mode: number) => ({
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
@@ -141,10 +147,14 @@ export default function LipsyncEditor({
|
||||
|
||||
return (
|
||||
<div {...{ className: "container-panel pt-4" }}>
|
||||
<div {...{ className: "panel lipsync-panel p-3 py-4 p-md-4" }}>
|
||||
<div {...{ className: "panel face-animator-main" }}>
|
||||
<FaceAnimatorTitle {...headerProps} />
|
||||
{transitions((style, i) => {
|
||||
const Page = [FaceAnimatorInput, FaceAnimatorWorking, FaceAnimatorSuccess][page];
|
||||
const Page = [
|
||||
FaceAnimatorInput,
|
||||
FaceAnimatorWorking,
|
||||
FaceAnimatorSuccess,
|
||||
][page];
|
||||
return Page ? (
|
||||
<Page
|
||||
{...{
|
||||
@@ -172,8 +182,9 @@ export default function LipsyncEditor({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<FaceAnimatorJobs {...{ inferenceJobsByCategory, t }}/>
|
||||
<div {...{ className: "face-animator-mobile-sample" }}>
|
||||
<BasicVideo {...{ src: "/videos/face-animator-instruction-en.mp4" }}/>
|
||||
<BasicVideo {...{ src: "/videos/face-animator-instruction-en.mp4" }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import MediaAudioPlayer from "./MediaAudioPlayer";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
|
||||
interface MediaAudioComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaAudioComponent({
|
||||
mediaFile,
|
||||
}: MediaAudioComponentProps) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
<MediaAudioPlayer mediaFile={mediaFile} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import React from "react";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import {
|
||||
faPlay,
|
||||
faPause,
|
||||
faRepeat,
|
||||
faArrowRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import Button from "components/common/Button";
|
||||
|
||||
enum PlaybackSpeed {
|
||||
HALF,
|
||||
NORMAL,
|
||||
DOUBLE,
|
||||
}
|
||||
|
||||
interface MediaAudioPlayerProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
|
||||
let [isPlaying, setIsPlaying] = useState(false);
|
||||
let [isRepeating, setIsRepeating] = useState(false);
|
||||
let [playbackSpeed, setPlaybackSpeed] = useState(PlaybackSpeed.NORMAL);
|
||||
let [waveSurfer, setWaveSurfer] = useState<WaveSurfer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wavesurferInstance = WaveSurfer.create({
|
||||
container: "#waveform", // Previousy I used 'this.ref.current' and React.createRef()
|
||||
height: 200,
|
||||
responsive: true,
|
||||
waveColor: "#cbcbcb",
|
||||
progressColor: "#fc8481",
|
||||
cursorColor: "#fc6b68",
|
||||
cursorWidth: 2,
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
setWaveSurfer(wavesurferInstance);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const audioLink = new BucketConfig().getGcsUrl(
|
||||
mediaFile.public_bucket_path
|
||||
);
|
||||
if (waveSurfer) {
|
||||
waveSurfer.load(audioLink);
|
||||
}
|
||||
}, [waveSurfer, mediaFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (waveSurfer) {
|
||||
waveSurfer.unAll(); // NB: Otherwise we keep reinstalling the hooks and cause chaos
|
||||
waveSurfer.on("pause", () => {
|
||||
setIsPlaying(waveSurfer!.isPlaying());
|
||||
});
|
||||
waveSurfer.on("play", () => {
|
||||
setIsPlaying(waveSurfer!.isPlaying());
|
||||
});
|
||||
waveSurfer.on("finish", () => {
|
||||
if (waveSurfer && isRepeating) {
|
||||
waveSurfer!.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [waveSurfer, isRepeating]);
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (waveSurfer) {
|
||||
waveSurfer.playPause();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleIsRepeating = () => {
|
||||
setIsRepeating(!isRepeating);
|
||||
};
|
||||
|
||||
const togglePlaybackSpeed = () => {
|
||||
let nextSpeed = PlaybackSpeed.NORMAL;
|
||||
switch (playbackSpeed) {
|
||||
case PlaybackSpeed.NORMAL:
|
||||
nextSpeed = PlaybackSpeed.DOUBLE;
|
||||
waveSurfer!.setPlaybackRate(1.5); // Okay, so a lie...
|
||||
break;
|
||||
case PlaybackSpeed.DOUBLE:
|
||||
nextSpeed = PlaybackSpeed.HALF;
|
||||
waveSurfer!.setPlaybackRate(0.5);
|
||||
break;
|
||||
case PlaybackSpeed.HALF:
|
||||
nextSpeed = PlaybackSpeed.NORMAL;
|
||||
waveSurfer!.setPlaybackRate(1.0);
|
||||
break;
|
||||
}
|
||||
setPlaybackSpeed(nextSpeed);
|
||||
};
|
||||
|
||||
let playButtonIcon = faPlay;
|
||||
if (isPlaying) {
|
||||
playButtonIcon = faPause;
|
||||
}
|
||||
|
||||
let repeatButtonIcon = isRepeating ? faRepeat : faArrowRight;
|
||||
|
||||
let speedButtonText = "1x";
|
||||
switch (playbackSpeed) {
|
||||
case PlaybackSpeed.NORMAL:
|
||||
speedButtonText = "1x";
|
||||
break;
|
||||
case PlaybackSpeed.DOUBLE:
|
||||
speedButtonText = "2x";
|
||||
break;
|
||||
case PlaybackSpeed.HALF:
|
||||
speedButtonText = "0.5x";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="waveform"></div>
|
||||
<div className="d-flex justify-content-center gap-2 mt-3">
|
||||
<Button
|
||||
square={true}
|
||||
icon={playButtonIcon}
|
||||
onClick={() => togglePlayPause()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
tooltip="Toggle Repeat"
|
||||
variant="secondary"
|
||||
square={true}
|
||||
icon={repeatButtonIcon}
|
||||
onClick={() => toggleIsRepeating()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
tooltip="Speed"
|
||||
label={speedButtonText}
|
||||
variant="secondary"
|
||||
onClick={() => togglePlaybackSpeed()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import React from "react";
|
||||
|
||||
interface MediaImageComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaImageComponent({
|
||||
mediaFile,
|
||||
}: MediaImageComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={mediaFile.public_bucket_path}
|
||||
alt="test"
|
||||
width="400"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import MediaAudioComponent from "./MediaAudioComponent";
|
||||
import MediaVideoComponent from "./MediaVideoComponent";
|
||||
import MediaImageComponent from "./MediaImageComponent";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import {
|
||||
GetMediaFile,
|
||||
MediaFile,
|
||||
} from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import Container from "components/common/Container";
|
||||
import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Skeleton from "components/common/Skeleton";
|
||||
import Button from "components/common/Button";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faArrowDownToLine,
|
||||
// faCirclePlay,
|
||||
// faShare,
|
||||
faSquareQuote,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import Accordion from "components/common/Accordion";
|
||||
import DataTable from "components/common/DataTable";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import useTimeAgo from "hooks/useTimeAgo";
|
||||
import { CommentComponent } from "v2/view/_common/comments/CommentComponent";
|
||||
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
|
||||
interface MediaPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const [mediaFile, setMediaFile] = useState<MediaFile | undefined | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const timeCreated = useTimeAgo(mediaFile?.created_at.toISOString() || "");
|
||||
|
||||
const getMediaFile = useCallback(async (mediaFileToken: string) => {
|
||||
let result = await GetMediaFile(mediaFileToken);
|
||||
if (result.media_file) {
|
||||
setMediaFile(result.media_file);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getMediaFile(token);
|
||||
}, [token, getMediaFile]);
|
||||
|
||||
function renderMediaComponent(mediaFile: MediaFile) {
|
||||
switch (mediaFile.media_type) {
|
||||
case MediaFileType.Audio:
|
||||
return (
|
||||
<div className="panel p-3 p-lg-4 d-flex flex-column">
|
||||
{/* Voice model name that is used to generate the audio */}
|
||||
{/*<h3 className="fw-bold mb-4">[Voice Model Name]</h3> */}
|
||||
|
||||
<MediaAudioComponent mediaFile={mediaFile} />
|
||||
|
||||
{/* Show TTS text input if it is a TTS result */}
|
||||
{mediaFile.public_bucket_path.includes("tts_inference_output") && (
|
||||
<div className="mt-4">
|
||||
<h5 className="fw-semibold">
|
||||
<FontAwesomeIcon icon={faSquareQuote} className="me-2" />
|
||||
Audio Text
|
||||
</h5>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
|
||||
do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Pellentesque elit ullamcorper dignissim cras tincidunt
|
||||
lobortis. Integer malesuada nunc vel risus commodo viverra
|
||||
maecenas accumsan lacus.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case MediaFileType.Video:
|
||||
return (
|
||||
<>
|
||||
<div className="panel panel-clear">
|
||||
<div className="ratio ratio-16x9 video-bg panel-border rounded">
|
||||
<MediaVideoComponent mediaFile={mediaFile} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case MediaFileType.Image:
|
||||
return <MediaImageComponent mediaFile={mediaFile} />;
|
||||
default:
|
||||
return <div>Unsupported media type</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let audioLink = new BucketConfig().getGcsUrl(mediaFile?.public_bucket_path);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<>
|
||||
<Container type="padded" className="pt-4 pt-lg-5">
|
||||
<div className="row g-4">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="panel p-3 py-4 p-md-4">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="panel p-3 py-4 p-md-4 mt-4 d-none d-xl-block">
|
||||
<h4 className="fw-semibold mb-3">
|
||||
<Skeleton type="short" />
|
||||
</h4>
|
||||
<h1>
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-4 d-flex flex-column gap-2">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton type="medium" />
|
||||
</h1>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="d-xl-none mt-4">
|
||||
<Panel padding>
|
||||
<h4 className="fw-semibold mb-3">
|
||||
<Skeleton type="short" />
|
||||
</h4>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (error || !mediaFile)
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
titleIcon={faCircleExclamation}
|
||||
title="Media not found"
|
||||
subText="This media does not exist or is private."
|
||||
extension={
|
||||
<div className="d-flex">
|
||||
<Button label="Back to homepage" to="/" className="d-flex" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const audioDetails = [
|
||||
{ property: "Type", value: mediaFile.media_type },
|
||||
{ property: "Created at", value: mediaFile.created_at.toString() },
|
||||
{
|
||||
property: "Visibility",
|
||||
value: mediaFile.creator_set_visibility.toString(),
|
||||
},
|
||||
/*{
|
||||
property: "Model",
|
||||
value: mediaFile.model_used,
|
||||
link: mediaFile.model_link,
|
||||
},
|
||||
{
|
||||
property: "Vocoder",
|
||||
value: "Test",
|
||||
},
|
||||
{
|
||||
property: "Language",
|
||||
value: "English",
|
||||
},
|
||||
{
|
||||
property: "Model",
|
||||
value: mediaFile.model_used,
|
||||
},*/
|
||||
];
|
||||
|
||||
const videoDetails = [
|
||||
{ property: "Type", value: mediaFile.media_type },
|
||||
{ property: "Created at", value: mediaFile.created_at.toString() },
|
||||
{
|
||||
property: "Visibility",
|
||||
value: mediaFile.creator_set_visibility.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
const imageDetails = [
|
||||
{ property: "Type", value: mediaFile.media_type },
|
||||
{ property: "Created at", value: mediaFile.created_at.toString() },
|
||||
{
|
||||
property: "Visibility",
|
||||
value: mediaFile.creator_set_visibility.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
let mediaDetails = undefined;
|
||||
|
||||
switch (mediaFile.media_type) {
|
||||
case MediaFileType.Audio:
|
||||
mediaDetails = <DataTable data={audioDetails} />;
|
||||
break;
|
||||
case MediaFileType.Video:
|
||||
mediaDetails = <DataTable data={videoDetails} />;
|
||||
break;
|
||||
case MediaFileType.Image:
|
||||
mediaDetails = <DataTable data={imageDetails} />;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
let modMediaDetails = undefined;
|
||||
|
||||
const modDetails = [
|
||||
{ property: "Model creator is banned", value: "good standing" },
|
||||
{
|
||||
property: "Result creator is banned (if user)",
|
||||
value: "good standing",
|
||||
},
|
||||
{
|
||||
property: "Result creator IP address",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
property: "Mod deleted at (UTC)",
|
||||
value: "not deleted",
|
||||
},
|
||||
{
|
||||
property: "Result creator deleted at (UTC)",
|
||||
value: "not deleted",
|
||||
},
|
||||
];
|
||||
|
||||
if (sessionWrapper.canBanUsers()) {
|
||||
modMediaDetails = (
|
||||
<Accordion.Item title="Moderator Details" defaultOpen={false}>
|
||||
<DataTable data={modDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container type="panel" className="pt-4 pt-lg-5">
|
||||
<div className="row g-4">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="media-wrapper">
|
||||
{renderMediaComponent(mediaFile)}
|
||||
</div>
|
||||
|
||||
<div className="panel p-3 py-4 p-md-4 mt-4 d-none d-xl-block">
|
||||
<h4 className="fw-semibold mb-3">Comments</h4>
|
||||
<CommentComponent
|
||||
entityType="user"
|
||||
entityToken={mediaFile.token}
|
||||
sessionWrapper={sessionWrapper}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-4">
|
||||
<div className="panel panel-clear d-flex flex-column gap-4">
|
||||
<div className="d-flex gap-2">
|
||||
<Gravatar
|
||||
size={48}
|
||||
username={mediaFile.maybe_creator_user?.display_name}
|
||||
avatarIndex={
|
||||
mediaFile.maybe_creator_user?.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
mediaFile.maybe_creator_user?.default_avatar.color_index
|
||||
}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<Link
|
||||
className="fw-medium"
|
||||
to={`/profile/${mediaFile.maybe_creator_user?.display_name}`}
|
||||
>
|
||||
{mediaFile.maybe_creator_user?.display_name}
|
||||
</Link>
|
||||
{timeCreated}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
<Button
|
||||
icon={faArrowDownToLine}
|
||||
label="Download"
|
||||
className="flex-grow-1"
|
||||
href={audioLink}
|
||||
download={audioLink}
|
||||
/>
|
||||
{/* Share and Create Buttons */}
|
||||
{/* <div className="d-flex gap-2">
|
||||
<Button
|
||||
square={true}
|
||||
variant="secondary"
|
||||
icon={faCirclePlay}
|
||||
onClick={() => {}}
|
||||
tooltip="Create"
|
||||
/>
|
||||
<Button
|
||||
square={true}
|
||||
variant="secondary"
|
||||
icon={faShare}
|
||||
onClick={() => {}}
|
||||
tooltip="Share"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<Accordion>
|
||||
<Accordion.Item title="Media Details" defaultOpen={true}>
|
||||
{mediaDetails}
|
||||
</Accordion.Item>
|
||||
|
||||
{modMediaDetails}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="d-xl-none my-4">
|
||||
<Container type="panel">
|
||||
<Panel padding={true}>
|
||||
<h4 className="fw-semibold mb-3">Comments</h4>
|
||||
<CommentComponent
|
||||
entityType="user"
|
||||
entityToken={mediaFile.token}
|
||||
sessionWrapper={sessionWrapper}
|
||||
/>
|
||||
</Panel>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import React from "react";
|
||||
|
||||
interface MediaVideoComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaVideoComponent({
|
||||
mediaFile,
|
||||
}: MediaVideoComponentProps) {
|
||||
let mediaLink = new BucketConfig().getGcsUrl(
|
||||
mediaFile.public_bucket_path
|
||||
);
|
||||
|
||||
return (
|
||||
<video className="rounded" controls width="100%" height="auto">
|
||||
<source src={mediaLink} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import media from './media';
|
||||
|
||||
export { media as Media };
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export default function media({ value }: Props) {
|
||||
return <div>
|
||||
<h1>Media</h1>
|
||||
</div>;
|
||||
};
|
||||
@@ -143,7 +143,7 @@ function PricingPage(props: Props) {
|
||||
|
||||
// Highlight the mid-tier plan if nothing is subscribed
|
||||
if (!userHasPaidPremium) {
|
||||
proBorderCss = "rounded panel p-4 h-100 panel-border";
|
||||
proBorderCss = "rounded panel p-4 h-100 pricing-border";
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import Container from "components/common/Container";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import { faClock, faMessageDots } from "@fortawesome/pro-solid-svg-icons";
|
||||
import ModelSearch from "components/common/ModelSearch";
|
||||
import Panel from "components/common/Panel";
|
||||
import useSearch from "hooks/useSearch/useSearch";
|
||||
import ModelSearchResults from "components/common/ModelSearchResults";
|
||||
import ModelTags from "components/common/ModelTags";
|
||||
import Select from "components/common/Select";
|
||||
|
||||
interface TtsModelSearchPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
const dummyData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Spongebob",
|
||||
tags: ["English", "High-pitched", "Character"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 14000,
|
||||
uses: 1000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Mariano Closs",
|
||||
tags: ["Spanish", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 308290,
|
||||
uses: 30000000,
|
||||
comments: 1200,
|
||||
time: new Date("2023-10-12T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Cristiano Ronaldo",
|
||||
tags: ["Portuguese", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 17500,
|
||||
uses: 40000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Messi",
|
||||
tags: ["Portuguese", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 17500,
|
||||
uses: 40000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Morty",
|
||||
tags: ["English", "High-pitched", "Character"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 14000,
|
||||
uses: 1000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Bad Bunny",
|
||||
tags: ["Spanish", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 308290,
|
||||
uses: 30000000,
|
||||
comments: 1200,
|
||||
time: new Date("2023-10-12T12:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const allTags = [
|
||||
"English",
|
||||
"Spanish",
|
||||
"Portuguese",
|
||||
"High-pitched",
|
||||
"Low-pitched",
|
||||
"Character",
|
||||
];
|
||||
|
||||
export default function TtsModelSearchPage(props: TtsModelSearchPageProps) {
|
||||
const {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
filteredData,
|
||||
} = useSearch(dummyData);
|
||||
|
||||
const handleSelectTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
if (prev.includes(tag)) {
|
||||
return prev.filter((t) => t !== tag);
|
||||
}
|
||||
return [...prev, tag];
|
||||
});
|
||||
};
|
||||
|
||||
const searchTts = (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<ModelSearch value={searchTerm} onChange={setSearchTerm} />
|
||||
<ModelTags
|
||||
tags={allTags}
|
||||
selectedTags={selectedTags}
|
||||
onSelectTag={handleSelectTag}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "most liked", label: "Most Liked" },
|
||||
{ value: "most used", label: "Most Used" },
|
||||
{ value: "moset recent", label: "Most Recent" },
|
||||
];
|
||||
const sortTimeOptions = [
|
||||
{ value: "all time", label: "All Time" },
|
||||
{ value: "today", label: "Today" },
|
||||
{ value: "this week", label: "This Week" },
|
||||
{ value: "this month", label: "This Month" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
title="Text to Speech"
|
||||
titleIcon={faMessageDots}
|
||||
subText="Turn text into your favorite character's voice."
|
||||
showButton={true}
|
||||
buttonLabel="Create a voice"
|
||||
buttonVariant="secondary"
|
||||
buttonTo="/voice-designer"
|
||||
extension={searchTts}
|
||||
/>
|
||||
<Panel padding={true}>
|
||||
<div className="d-flex gap-2 mb-4">
|
||||
<Select
|
||||
small={true}
|
||||
options={sortOptions}
|
||||
defaultValue={sortOptions[0]}
|
||||
/>
|
||||
<Select
|
||||
small={true}
|
||||
icon={faClock}
|
||||
options={sortTimeOptions}
|
||||
defaultValue={sortTimeOptions[0]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModelSearchResults data={filteredData} />
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -434,46 +434,6 @@ function TtsResultViewPage(props: Props) {
|
||||
<div className="container py-5">
|
||||
<div className="d-flex flex-column">
|
||||
<h1 className=" fw-bold mb-2">{headingTitle}</h1>
|
||||
{/* Rate Result Buttons */}
|
||||
{/* Commented out because not ready yet. */}
|
||||
{/*<div className="d-flex gap-3">
|
||||
<div className="d-flex">
|
||||
<Tippy
|
||||
content="I like this audio"
|
||||
hideOnClick
|
||||
placement="bottom"
|
||||
theme="fakeyou"
|
||||
arrow={false}
|
||||
>
|
||||
<button className="btn-rate left rated">
|
||||
<FontAwesomeIcon icon={faThumbsUp} />
|
||||
</button>
|
||||
</Tippy>
|
||||
|
||||
<div className="vr"></div>
|
||||
|
||||
<Tippy
|
||||
content="I dislike this audio"
|
||||
hideOnClick
|
||||
placement="bottom"
|
||||
theme="fakeyou"
|
||||
arrow={false}
|
||||
>
|
||||
<button className="btn-rate right">
|
||||
<FontAwesomeIcon icon={faThumbsDown} />
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faHeart}
|
||||
className="me-2 rating-icon heart"
|
||||
/>
|
||||
<p>
|
||||
<span className="fw-medium">1,518 likes</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
<p className="mb-3 result-text pt-2">
|
||||
<TextExpander
|
||||
@@ -494,14 +454,6 @@ function TtsResultViewPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Without wavesurfer,
|
||||
<audio
|
||||
controls
|
||||
src={audioLink}>
|
||||
Your browser does not support the
|
||||
<code>audio</code> element.
|
||||
</audio>*/}
|
||||
|
||||
<div className="container-panel pt-3 pb-5">{socialSharing}</div>
|
||||
|
||||
<div className="container-panel pt-3 pb-5">
|
||||
@@ -517,12 +469,6 @@ function TtsResultViewPage(props: Props) {
|
||||
<div className="py-6">
|
||||
<table className="table tts-result-table">
|
||||
<tbody>
|
||||
{/* <tr>
|
||||
<th scope="row">Original text</th>
|
||||
<td className="overflow-fix">
|
||||
{ttsInferenceResult.raw_inference_text}
|
||||
</td>
|
||||
</tr> */}
|
||||
<tr>
|
||||
<th scope="row">Audio creator</th>
|
||||
<td>{creatorDetails}</td>
|
||||
|
||||
@@ -5,7 +5,7 @@ import Panel from "components/common/Panel/Panel";
|
||||
import { faSave, faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
|
||||
import Button from "components/common/Button/Button";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import PageContainer from "components/common/Container";
|
||||
|
||||
interface VcModelDeletePageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
|
||||
@@ -6,7 +6,7 @@ import Panel from "components/common/Panel/Panel";
|
||||
import { faEye, faFilePen, faSave } from "@fortawesome/pro-solid-svg-icons";
|
||||
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
|
||||
import Button from "components/common/Button/Button";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import PageContainer from "components/common/Container";
|
||||
|
||||
interface VcModelEditPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapp
|
||||
import { RatingButtons } from "v2/view/_common/ratings/RatingButtons";
|
||||
import { RatingStats } from "v2/view/_common/ratings/RatingStats";
|
||||
import ShareButton from "components/common/ShareButton/ShareButton";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import Container from "components/common/Container/Container";
|
||||
import VcGenerateAudioPanel from "../VcGenerateAudioPanel";
|
||||
|
||||
interface VcModelViewPageProps {
|
||||
@@ -97,7 +97,7 @@ export default function VcModelViewPage(props: VcModelViewPageProps) {
|
||||
const shareButton = <ShareButton url={shareUrl} />;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Container type="full">
|
||||
<PageHeaderModelView
|
||||
title={title}
|
||||
subText={subText}
|
||||
@@ -180,6 +180,6 @@ export default function VcModelViewPage(props: VcModelViewPageProps) {
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
/>
|
||||
</Panel>
|
||||
</PageContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -4440,9 +4440,10 @@ __metadata:
|
||||
"@types/uuid": ^8.3.0
|
||||
"@types/wavesurfer.js": ^5.2.0
|
||||
axios: ^0.21.0
|
||||
bootstrap: 5.1.3
|
||||
bulma: ^0.9.0
|
||||
craco: ^0.0.3
|
||||
date-fns: ^2.22.1
|
||||
date-fns: 2.22.1
|
||||
framer-motion: ^6.5.1
|
||||
howler: ^2.2.0
|
||||
i18next: ^21.6.11
|
||||
@@ -7281,6 +7282,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bootstrap@npm:5.1.3":
|
||||
version: 5.1.3
|
||||
resolution: "bootstrap@npm:5.1.3"
|
||||
peerDependencies:
|
||||
"@popperjs/core": ^2.10.2
|
||||
checksum: 301b5ed872efba061104cf22ac93568e3837867fb5527ab9326a51510fb752bd4883e1d488225c8be72f86d9d3a55ef5b166aa7fa62c2fdd077c3f05b65752f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
@@ -8967,6 +8977,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:2.22.1":
|
||||
version: 2.22.1
|
||||
resolution: "date-fns@npm:2.22.1"
|
||||
checksum: 7ff97cd605af50c02f341687c2cafd218839a1aace67965374989855a13f76dc4fe52e0e38c343c1ad1f8399787cb6839a0b14a669c44b30550c287300b1bb50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^2.22.1":
|
||||
version: 2.28.0
|
||||
resolution: "date-fns@npm:2.28.0"
|
||||
|
||||
Reference in new issue
Block a user