Merge branch 'master' into bflat-fy-voice-designer-page

This commit is contained in:
BFlat authored and GitHub committed 2023-10-25 23:23:39 +07:00
commit 56d7dbbcf3
99 files changed
+2347 -433

No files matched your search

+6 -1
View File
@@ -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
+9 -3
View File
@@ -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
);
}
}
+2 -1
View File
@@ -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!",
+14 -6
View File
@@ -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";
@@ -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 -1
View File
@@ -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;
+28 -20
View File
@@ -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";
+18 -1
View File
@@ -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 {
+64 -26
View File
@@ -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;
};
@@ -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>
);
}
+18 -1
View File
@@ -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"