Merge branch 'master' into bflat-storyteller-team
No files matched your search
@@ -7,6 +7,7 @@
|
||||
/checkout_success* /index.html 200
|
||||
/clone* /index.html 200
|
||||
/coming-soon* /index.html 200
|
||||
/character* /index.html 200
|
||||
/contribute* /index.html 200
|
||||
/firehose* /index.html 200
|
||||
/guide* /index.html 200
|
||||
@@ -34,16 +35,15 @@
|
||||
/upload* /index.html 200
|
||||
/video* /index.html 200
|
||||
/w2l* /index.html 200
|
||||
|
||||
# This is for YCombinator.
|
||||
# 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
|
||||
# PLEASE DO NOT ACECSS THIS URL OR WATCH. IT WILL CHANGE THE
|
||||
# VIEW STATISTICS AND WE WON'T BE ABLE TO TELL IF THEY SAW IT
|
||||
#/demo https://www.youtube.com/watch?v=wfFpi-Mv3fY 302
|
||||
|
||||
# This is for AI Grant.
|
||||
# This is the *OLD* video for Summer 2022. We'll re-edit.
|
||||
# PLEASE DO NOT ACECSS THIS URL OR WATCH. IT WILL CHANGE THE
|
||||
# PLEASE DO NOT ACECSS THIS URL OR WATCH. IT WILL CHANGE THE
|
||||
# VIEW STATISTICS AND WE WON'T BE ABLE TO TELL IF THEY SAW IT
|
||||
#/ai-grant-founder-video https://www.youtube.com/watch?v=Cj7rchSSuLg 302
|
||||
#/ai-grant-demo https://www.youtube.com/watch?v=0EOZsrH7jxI 302
|
||||
|
||||
@@ -95,6 +95,9 @@ class ApiConfig {
|
||||
}
|
||||
}
|
||||
|
||||
//v2ApiHost = "api.dev.fakeyou.com:12345";
|
||||
//useSsl = false;
|
||||
|
||||
this.domain = domain;
|
||||
this.useSsl = useSsl;
|
||||
this.v2ApiHost = v2ApiHost;
|
||||
|
||||
@@ -2,14 +2,16 @@ import { ApiConfig } from "@storyteller/components";
|
||||
|
||||
export interface Leaderboard {
|
||||
success: boolean,
|
||||
tts_leaderboard: Array<LeaderboardEntryForList>
|
||||
w2l_leaderboard: Array<LeaderboardEntryForList>
|
||||
tts_leaderboard: Array<LeaderboardRow>
|
||||
w2l_leaderboard: Array<LeaderboardRow>
|
||||
}
|
||||
|
||||
export interface LeaderboardEntryForList {
|
||||
export interface LeaderboardRow {
|
||||
username: string,
|
||||
display_name: string,
|
||||
gravatar_hash: string,
|
||||
default_avatar_index: number,
|
||||
default_avatar_color_index: number,
|
||||
uploaded_count: number,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface User {
|
||||
username: string,
|
||||
display_name: string,
|
||||
email_gravatar_hash: string,
|
||||
default_avatar_index: number,
|
||||
default_avatar_color_index: number,
|
||||
profile_markdown: string,
|
||||
profile_rendered_html: string,
|
||||
user_role_slug: string,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export function getBackgroundColor(backgroundIndex?: number): string {
|
||||
switch (backgroundIndex) {
|
||||
case 0:
|
||||
return "#E66462";
|
||||
case 1:
|
||||
return "#FD881B";
|
||||
case 2:
|
||||
return "#E7C13C";
|
||||
case 3:
|
||||
return "#4BA905";
|
||||
case 4:
|
||||
return "#25B8A0";
|
||||
case 5:
|
||||
return "#0078D1";
|
||||
case 6:
|
||||
return "#7F52C1";
|
||||
case 7:
|
||||
return "#FF66AC";
|
||||
case 8:
|
||||
return "#259FEC";
|
||||
default:
|
||||
return `#1a1a27`;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
import React from "react";
|
||||
import { getBackgroundColor } from "./GetBGColor";
|
||||
|
||||
interface Props {
|
||||
size: number;
|
||||
email_hash?: string;
|
||||
username?: string;
|
||||
avatarIndex?: number;
|
||||
backgroundIndex?: number;
|
||||
}
|
||||
|
||||
function Gravatar(props: Props) {
|
||||
const gravatarUrl = `https://www.gravatar.com/avatar/${props.email_hash}?s=${props.size}&d=https://fakeyou.com/images/avatars/default-pfp.png`;
|
||||
// TODO: staging domain + local dev support
|
||||
let defaultImageUrl =
|
||||
props.avatarIndex === undefined
|
||||
? "https://fakeyou.com/images/avatars/default-pfp.png"
|
||||
: `https://fakeyou.com/images/avatars/2000x2000/${props.avatarIndex}.webp`;
|
||||
|
||||
// NB: Gravatar suggests URI encoding these:
|
||||
// https://en.gravatar.com/site/implement/images/
|
||||
defaultImageUrl = encodeURIComponent(defaultImageUrl);
|
||||
|
||||
const gravatarUrl = `https://www.gravatar.com/avatar/${props.email_hash}?s=${props.size}&d=${defaultImageUrl}`;
|
||||
|
||||
let altText = "gravatar";
|
||||
if (props.username !== undefined) {
|
||||
@@ -21,6 +34,7 @@ function Gravatar(props: Props) {
|
||||
src={gravatarUrl}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -18,4 +18,7 @@
|
||||
<url>
|
||||
<loc>https://fakeyou.com/guide</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fakeyou.com/character/donald-trump</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -7,6 +7,6 @@ export function usePrefixedDocumentTitle(title?: string) {
|
||||
// NB: Choice of a vertical bar "|" separator is due to conserving pixels, which *might* matter to SEO.
|
||||
// I haven't fully investigated the veracity of this, nor the position of prefixes, suffixes, etc.
|
||||
// https://www.searchenginejournal.com/pipe-or-dash-in-title-tag/378099/#close
|
||||
const outputTitle = fixed.length === 0 ? DEFAULT_FOR_EMPTY_TITLE : `FakeYou | ${fixed}`;
|
||||
const outputTitle = fixed.length === 0 ? DEFAULT_FOR_EMPTY_TITLE : `${fixed} | FakeYou`;
|
||||
useDocumentTitle(outputTitle);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ styles with important tags:
|
||||
|
||||
select {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
@@ -94,11 +94,11 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 8px !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.rounded-5 {
|
||||
border-radius: 5px !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -143,6 +143,11 @@ 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;
|
||||
@@ -171,11 +176,11 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 10px 24px;
|
||||
padding: 10px 20px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -275,7 +280,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.panel-tts-results {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
background-color: lighten($panel, 4%);
|
||||
}
|
||||
|
||||
@@ -308,13 +313,13 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.menu {
|
||||
border-radius: 8px !important;
|
||||
border-radius: 0.5rem !important;
|
||||
color: $white;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
border-color: rgba($white, 0.12) !important;
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0px 5px 20px rgba($black, 0.3);
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
@@ -353,7 +358,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
background-color: $input-dark !important;
|
||||
border-color: $input-dark !important;
|
||||
color: $white !important;
|
||||
@@ -388,7 +393,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
margin-bottom: 1rem;
|
||||
@@ -454,7 +459,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
#resultLink {
|
||||
border-radius: 5px 0 0 5px;
|
||||
border-radius: 0.5rem 0 0 5px;
|
||||
}
|
||||
|
||||
#copyBtn {
|
||||
@@ -531,7 +536,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.video-card {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
border: 4px solid;
|
||||
border-color: #fff;
|
||||
padding: 1rem;
|
||||
@@ -625,7 +630,7 @@ styles with important tags:
|
||||
color: $primary;
|
||||
|
||||
&.btn {
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
background-color: #fdcfce;
|
||||
@@ -671,7 +676,7 @@ styles with important tags:
|
||||
.direct-view-link {
|
||||
background-color: lighten($panel, 4%);
|
||||
border: 1px solid #39394c;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
padding: 12px 16px;
|
||||
min-height: 63px;
|
||||
height: 100%;
|
||||
@@ -740,7 +745,7 @@ styles with important tags:
|
||||
background: $input-dark;
|
||||
margin-top: 6px;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@@ -757,7 +762,7 @@ styles with important tags:
|
||||
background-color: $panel;
|
||||
border-color: lighten($panel, 8%) !important;
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
|
||||
-webkit-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
|
||||
-moz-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
|
||||
@@ -877,7 +882,7 @@ styles with important tags:
|
||||
|
||||
.twitch-chat-container {
|
||||
border: 1px solid #ffffff12 !important;
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
background-color: $panel !important;
|
||||
min-height: 650px;
|
||||
box-shadow: 0 0 0 1px rgba(0 0 0 / 30%), 0 70px 65px rgb(0 0 0 / 10%),
|
||||
@@ -923,8 +928,8 @@ styles with important tags:
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
top: 18%;
|
||||
right: 6%;
|
||||
top: 22%;
|
||||
right: 9%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@@ -976,7 +981,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #ffffff15;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
@@ -1146,7 +1151,7 @@ textarea {
|
||||
.panel-select {
|
||||
transition: 0.2s all;
|
||||
cursor: pointer;
|
||||
border-radius: 8px !important;
|
||||
border-radius: 0.5rem !important;
|
||||
max-width: 500px;
|
||||
border-width: 1px !important;
|
||||
|
||||
@@ -1234,11 +1239,11 @@ textarea {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 1080px;
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
ul.patrons-list {
|
||||
@@ -1252,7 +1257,7 @@ textarea {
|
||||
.twitch-video-container {
|
||||
min-height: 391px;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.p-md-4 {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { GetComputedTtsCategoryAssignmentsSuccessResponse } from "@storyteller/c
|
||||
import { NewsPage } from "./pages/news/NewsPage";
|
||||
import { LandingPage } from "./pages/landing/LandingPage";
|
||||
import { ChannelsPage } from "./pages/channels/Channels";
|
||||
import { TrumpTtsPage } from "./pages/character/trump/TrumpTtsPage";
|
||||
//import { LandingPage } from "./pages/landing/LandingPage";
|
||||
//import { VcModelListPage } from "./pages/vc/vc_model_list/VcModelListPage";
|
||||
|
||||
@@ -479,6 +480,48 @@ class PageContainer extends React.Component<Props, State> {
|
||||
<VocodesPage />
|
||||
</Route>
|
||||
|
||||
<Route path="/character/donald-trump">
|
||||
<TrumpTtsPage
|
||||
sessionWrapper={this.props.sessionWrapper}
|
||||
sessionSubscriptionsWrapper={
|
||||
this.props.sessionSubscriptionsWrapper
|
||||
}
|
||||
enqueueTtsJob={this.props.enqueueTtsJob}
|
||||
ttsInferenceJobs={this.props.ttsInferenceJobs}
|
||||
ttsModelUploadJobs={this.props.ttsModelUploadJobs}
|
||||
w2lInferenceJobs={this.props.w2lInferenceJobs}
|
||||
w2lTemplateUploadJobs={this.props.w2lTemplateUploadJobs}
|
||||
textBuffer={this.props.textBuffer}
|
||||
setTextBuffer={this.props.setTextBuffer}
|
||||
clearTextBuffer={this.props.clearTextBuffer}
|
||||
ttsModels={this.props.ttsModels}
|
||||
setTtsModels={this.props.setTtsModels}
|
||||
allTtsCategories={this.props.allTtsCategories}
|
||||
setAllTtsCategories={this.props.setAllTtsCategories}
|
||||
allTtsCategoriesByTokenMap={
|
||||
this.props.allTtsCategoriesByTokenMap
|
||||
}
|
||||
computedTtsCategoryAssignments={
|
||||
this.props.computedTtsCategoryAssignments
|
||||
}
|
||||
setComputedTtsCategoryAssignments={
|
||||
this.props.setComputedTtsCategoryAssignments
|
||||
}
|
||||
allTtsModelsByTokenMap={this.props.allTtsModelsByTokenMap}
|
||||
ttsModelsByCategoryToken={this.props.ttsModelsByCategoryToken}
|
||||
dropdownCategories={this.props.dropdownCategories}
|
||||
setDropdownCategories={this.props.setDropdownCategories}
|
||||
selectedCategories={this.props.selectedCategories}
|
||||
setSelectedCategories={this.props.setSelectedCategories}
|
||||
maybeSelectedTtsModel={this.props.maybeSelectedTtsModel}
|
||||
setMaybeSelectedTtsModel={this.props.setMaybeSelectedTtsModel}
|
||||
selectedTtsLanguageScope={this.props.selectedTtsLanguageScope}
|
||||
setSelectedTtsLanguageScope={
|
||||
this.props.setSelectedTtsLanguageScope
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<TtsModelListPage
|
||||
sessionWrapper={this.props.sessionWrapper}
|
||||
|
||||
@@ -10,15 +10,14 @@ interface Props {
|
||||
|
||||
function PageHeader(props: Props) {
|
||||
return (
|
||||
<div className="container-panel hero-section pt-4 pt-lg-5 pb-5">
|
||||
<div className="container-panel hero-section py-4">
|
||||
<div className="panel">
|
||||
<div className="p-3 py-4 p-md-4">
|
||||
<h1 className="fw-bold text-center text-md-start">
|
||||
{props.titleIcon}
|
||||
{props.title}
|
||||
</h1>
|
||||
<hr />
|
||||
<p className="text-center text-md-start">{props.subText}</p>
|
||||
<p className="text-center text-md-start pt-1">{props.subText}</p>
|
||||
{props.showButtons && (
|
||||
<div className="d-flex flex-column flex-md-row gap-3 justify-content-center justify-content-md-start mt-4">
|
||||
{props.actionButtons}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
function PageHeaderWithImage(props: Props) {
|
||||
return (
|
||||
<div className="container-panel hero-section py-4 py-lg-5">
|
||||
<div className="container-panel hero-section py-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">
|
||||
@@ -30,10 +30,12 @@ function PageHeaderWithImage(props: Props) {
|
||||
{props.titleIcon}
|
||||
{props.title}
|
||||
</h1>
|
||||
<hr />
|
||||
<p className="text-center text-md-start">{props.subText}</p>
|
||||
|
||||
<p className="text-center text-md-start opacity-75 pt-1">
|
||||
{props.subText}
|
||||
</p>
|
||||
{props.showButtons && (
|
||||
<div className="d-flex flex-column flex-md-row gap-3 justify-content-center justify-content-md-start mt-4">
|
||||
<div className="d-flex flex-column flex-sm-row gap-3 justify-content-center justify-content-md-start mt-4 pt-2">
|
||||
{props.actionButtons}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -109,7 +109,7 @@ function FooterNav(props: Props) {
|
||||
href={ThirdPartyLinks.FAKEYOU_TIKTOK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Support us by becoming a patreon"
|
||||
title="Follow us on Tiktok"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTiktok} className="me-2" />
|
||||
</a>
|
||||
@@ -145,10 +145,6 @@ function FooterNav(props: Props) {
|
||||
<Link to="/leaderboard">Leaderboard</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to={WebUrl.patronsPage()}>Patrons</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to="/firehose">Feed</Link>
|
||||
</li>
|
||||
@@ -184,8 +180,10 @@ function FooterNav(props: Props) {
|
||||
|
||||
<div className="d-flex flex-column flex-lg-row pt-2 align-items-center gap-0 gap-lg-4">
|
||||
<span className="flex-grow-1">
|
||||
© 2023 FakeYou, built by{" "}
|
||||
<a href="https://twitter.com/intent/follow?screen_name=echelon">echelon</a> in Atlanta.
|
||||
© 2023 FakeYou by{" "}
|
||||
<a href="https://storyteller.ai" target="_blank" rel="noreferrer">
|
||||
Storyteller.ai
|
||||
</a>
|
||||
</span>
|
||||
<div className="d-flex flex-column flex-lg-row align-items-center mt-4 mt-lg-0">
|
||||
{moderationLink}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
faBook,
|
||||
faStar,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDiscord, faPatreon } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { Logout } from "@storyteller/components/src/api/session/Logout";
|
||||
// import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import {
|
||||
@@ -96,8 +96,11 @@ function TopNav(props: Props) {
|
||||
};
|
||||
// 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, pendingTtsJobs.refresh_interval_millis);
|
||||
console.log('new interval', refreshInterval);
|
||||
const refreshInterval = Math.max(
|
||||
DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
|
||||
pendingTtsJobs.refresh_interval_millis
|
||||
);
|
||||
console.log("new interval", refreshInterval);
|
||||
const interval = setInterval(async () => fetch(), refreshInterval);
|
||||
fetch();
|
||||
return () => clearInterval(interval);
|
||||
@@ -463,16 +466,6 @@ function TopNav(props: Props) {
|
||||
{t("nav.TopNav.main.discordOption")}
|
||||
</a>
|
||||
</li>
|
||||
<li data-bs-toggle="offcanvas">
|
||||
<Link
|
||||
className="dropdown-item"
|
||||
to="/patrons"
|
||||
title="to patron list"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPatreon} className="me-2" />
|
||||
{t("nav.TopNav.main.patronsOption")}
|
||||
</Link>
|
||||
</li>
|
||||
<li data-bs-toggle="offcanvas">
|
||||
<Link
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocum
|
||||
interface Props {}
|
||||
|
||||
function AboutPage(props: Props) {
|
||||
|
||||
usePrefixedDocumentTitle("About Us");
|
||||
|
||||
return (
|
||||
@@ -29,10 +28,7 @@ function AboutPage(props: Props) {
|
||||
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<div>
|
||||
<motion.h1
|
||||
className="display-5 fw-bold lh-1 mb-4"
|
||||
variants={item}
|
||||
>
|
||||
<motion.h1 className=" fw-bold lh-1 mb-4" variants={item}>
|
||||
A Glimpse of the Future
|
||||
</motion.h1>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocum
|
||||
interface Props {}
|
||||
|
||||
function GuidePage(props: Props) {
|
||||
|
||||
usePrefixedDocumentTitle("Guide to all things Deep Fake");
|
||||
|
||||
return (
|
||||
@@ -30,10 +29,7 @@ function GuidePage(props: Props) {
|
||||
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<div>
|
||||
<motion.h1
|
||||
className="display-5 fw-bold lh-1 mb-3"
|
||||
variants={item}
|
||||
>
|
||||
<motion.h1 className=" fw-bold lh-1 mb-3" variants={item}>
|
||||
FakeYou TTS Guide
|
||||
</motion.h1>
|
||||
</div>
|
||||
@@ -64,81 +60,90 @@ function GuidePage(props: Props) {
|
||||
<h1 className="panel-title fw-bold">Quick Tips</h1>
|
||||
<div className="py-6 d-flex flex-column gap-4">
|
||||
<p>
|
||||
Here are some quick tips to generate the best sounding text-to-speech.
|
||||
Here are some quick tips to generate the best sounding
|
||||
text-to-speech.
|
||||
</p>
|
||||
<div>
|
||||
<h2 className="mb-4">Fixing nonsense “babbling”</h2>
|
||||
<p>(This is not a typical problem.)</p>
|
||||
<br />
|
||||
<p>
|
||||
(This is not a typical problem.)
|
||||
Some of our very earliest models were built using a limited
|
||||
amount of data and are sometimes prone to producing speech that
|
||||
sounds like “babbling” or incoherent vocal
|
||||
convulsions.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
Some of our very earliest models were built using a limited amount of data
|
||||
and are sometimes prone to producing speech that sounds like “babbling”
|
||||
or incoherent vocal convulsions.
|
||||
This problem can typically be fixed by{" "}
|
||||
<strong>
|
||||
<em>adding ending punctuation to your sentence</em>
|
||||
</strong>
|
||||
. If the audio still has problems,{" "}
|
||||
<strong>
|
||||
<em>add a comma at the start and end of your text</em>
|
||||
</strong>{" "}
|
||||
so that the program can add brief pauses. This usually corrects
|
||||
the problem.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
This problem can typically be fixed by
|
||||
{" "}
|
||||
<strong><em>adding ending punctuation to your sentence</em></strong>.
|
||||
If the audio still has problems,
|
||||
{" "}
|
||||
<strong><em>add a comma at the start and end of your text</em></strong>
|
||||
{" "}
|
||||
so that the program can add brief pauses. This usually corrects the problem.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
If the incoherent audio is at the end, you can always edit it with software
|
||||
such as Audacity.
|
||||
If the incoherent audio is at the end, you can always edit it
|
||||
with software such as Audacity.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="mb-4">
|
||||
Getting the correct pronunciation
|
||||
</h2>
|
||||
<h2 className="mb-4">Getting the correct pronunciation</h2>
|
||||
<p>
|
||||
Sometimes a TTS model will not pronounce words correctly or in the
|
||||
way you like.
|
||||
You can <strong><em>experiment with misspellings</em></strong> of words until you get something
|
||||
that sounds better. For example <em>“peekahchuu”</em> might sound different
|
||||
than <em>“pikachu”</em>.
|
||||
Sometimes a TTS model will not pronounce words correctly or in
|
||||
the way you like. You can{" "}
|
||||
<strong>
|
||||
<em>experiment with misspellings</em>
|
||||
</strong>{" "}
|
||||
of words until you get something that sounds better. For example{" "}
|
||||
<em>“peekahchuu”</em> might sound different than{" "}
|
||||
<em>“pikachu”</em>.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
Many of our models also support Arpabet phonetic annotation. It's an advanced feature you can
|
||||
learn to use by joining our <DiscordLink2 />.
|
||||
Many of our models also support Arpabet phonetic annotation.
|
||||
It's an advanced feature you can learn to use by joining our{" "}
|
||||
<DiscordLink2 />.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="mb-4">Emotional conditioning</h2>
|
||||
<p>
|
||||
To get a certain emotion and personality in the message you
|
||||
want, add some extra emotional “conditioning”.
|
||||
want, add some extra emotional “conditioning”.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
To do this, think of a strong emotional sentence or two you could start with.
|
||||
Then add this to the very beginning of your text.
|
||||
For example, to add some anger to <em>“I can't believe you said that to me!”</em>,
|
||||
you could input the following instead:
|
||||
To do this, think of a strong emotional sentence or two you
|
||||
could start with. Then add this to the very beginning of your
|
||||
text. For example, to add some anger to{" "}
|
||||
<em>“I can't believe you said that to me!”</em>, you
|
||||
could input the following instead:
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<em>“<strong>Darn it! I hate you!</strong> I can't believe you said that to me!”</em>
|
||||
<em>
|
||||
“<strong>Darn it! I hate you!</strong> I can't believe
|
||||
you said that to me!”
|
||||
</em>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
You can later remove these extra leading sentences with an audio editor such as Audacity, leaving
|
||||
just the speech you want.
|
||||
You can later remove these extra leading sentences with an audio
|
||||
editor such as Audacity, leaving just the speech you want.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="mb-4">Yelling emotions</h2>
|
||||
<p>
|
||||
While not all models support it, some of our models incorporate rich emotional samples
|
||||
that were transcripted with an emphasis mark – typically an exclamation mark (!).
|
||||
While not all models support it, some of our models incorporate
|
||||
rich emotional samples that were transcripted with an emphasis
|
||||
mark – typically an exclamation mark (!).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +208,7 @@ function CreateCategoryPage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pb-4 pt-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
{categoryActionName} Category
|
||||
</motion.h1>
|
||||
<motion.div className="pt-3" variants={item}>
|
||||
|
||||
@@ -48,7 +48,7 @@ function ChannelsPage(props: Props) {
|
||||
animate="visible"
|
||||
variants={container}
|
||||
>
|
||||
<motion.h1 className="display-5 fw-bold mb-4" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-4" variants={item}>
|
||||
Channels
|
||||
</motion.h1>
|
||||
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
import React, { useEffect, useCallback, useState } from "react";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SessionTtsInferenceResultList } from "../../../_common/SessionTtsInferenceResultsList";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
|
||||
import { TtsModelUploadJob } from "@storyteller/components/src/jobs/TtsModelUploadJobs";
|
||||
import { W2lInferenceJob } from "@storyteller/components/src/jobs/W2lInferenceJobs";
|
||||
import { W2lTemplateUploadJob } from "@storyteller/components/src/jobs/W2lTemplateUploadJobs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
ListTtsModels,
|
||||
TtsModelListItem,
|
||||
} from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import {
|
||||
GenerateTtsAudio,
|
||||
GenerateTtsAudioErrorType,
|
||||
GenerateTtsAudioIsError,
|
||||
GenerateTtsAudioIsOk,
|
||||
} from "@storyteller/components/src/api/tts/GenerateTtsAudio";
|
||||
import {
|
||||
ListTtsCategories,
|
||||
ListTtsCategoriesIsError,
|
||||
ListTtsCategoriesIsOk,
|
||||
} from "@storyteller/components/src/api/category/ListTtsCategories";
|
||||
import { TtsCategoryType } from "../../../../../AppWrapper";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBarsStaggered,
|
||||
faDeleteLeft,
|
||||
faVolumeHigh,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { container, panel } from "../../../../../data/animation";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { TtsPageHero } from "./TrumpTtsPageHero";
|
||||
import { Analytics } from "../../../../../common/Analytics";
|
||||
import {
|
||||
GetComputedTtsCategoryAssignments,
|
||||
GetComputedTtsCategoryAssignmentsIsError,
|
||||
GetComputedTtsCategoryAssignmentsIsOk,
|
||||
GetComputedTtsCategoryAssignmentsSuccessResponse,
|
||||
} from "@storyteller/components/src/api/category/GetComputedTtsCategoryAssignments";
|
||||
import { DynamicallyCategorizeModels } from "../../../../../model/categories/SyntheticCategory";
|
||||
|
||||
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
|
||||
import { SearchOmnibar } from "./search/SearchOmnibar";
|
||||
|
||||
const PAGE_MODEL_TOKENS = new Set<string>([
|
||||
"TM:pmd1wm3kf6az", // Development: "Fake Donald Trump #1"
|
||||
"TM:7rrwdhdq8ezq", // Development: "Fake Donald Trump #2"
|
||||
//"TM:aejrk66wq3ss", // Production: "Donald Trump (bibby's model)"
|
||||
"TM:pyzss4phqk6r", // Production: "Donald Trump (Sarcastic)"
|
||||
"TM:4v0ft4j72y2g", // Production: "Donald Trump (Angry)"
|
||||
"TM:03690khwpsbz", // Production: "Donald Trump (Casual Speech)"
|
||||
]);
|
||||
|
||||
export interface EnqueueJobResponsePayload {
|
||||
success: boolean;
|
||||
inference_job_token?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
|
||||
enqueueTtsJob: (jobToken: string) => void;
|
||||
ttsInferenceJobs: Array<TtsInferenceJob>;
|
||||
ttsModelUploadJobs: Array<TtsModelUploadJob>;
|
||||
w2lInferenceJobs: Array<W2lInferenceJob>;
|
||||
w2lTemplateUploadJobs: Array<W2lTemplateUploadJob>;
|
||||
textBuffer: string;
|
||||
setTextBuffer: (textBuffer: string) => void;
|
||||
clearTextBuffer: () => void;
|
||||
|
||||
ttsModels: Array<TtsModelListItem>;
|
||||
setTtsModels: (ttsVoices: Array<TtsModelListItem>) => void;
|
||||
|
||||
allTtsCategories: TtsCategoryType[];
|
||||
setAllTtsCategories: (allTtsCategories: TtsCategoryType[]) => void;
|
||||
|
||||
computedTtsCategoryAssignments?: GetComputedTtsCategoryAssignmentsSuccessResponse;
|
||||
setComputedTtsCategoryAssignments: (
|
||||
categoryAssignments: GetComputedTtsCategoryAssignmentsSuccessResponse
|
||||
) => void;
|
||||
|
||||
allTtsCategoriesByTokenMap: Map<string, TtsCategoryType>;
|
||||
allTtsModelsByTokenMap: Map<string, TtsModelListItem>;
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
|
||||
|
||||
dropdownCategories: TtsCategoryType[][];
|
||||
setDropdownCategories: (dropdownCategories: TtsCategoryType[][]) => void;
|
||||
selectedCategories: TtsCategoryType[];
|
||||
setSelectedCategories: (selectedCategories: TtsCategoryType[]) => void;
|
||||
|
||||
maybeSelectedTtsModel?: TtsModelListItem;
|
||||
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
|
||||
|
||||
selectedTtsLanguageScope: string;
|
||||
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
|
||||
}
|
||||
|
||||
function TrumpTtsPage(props: Props) {
|
||||
//Loading spinning icon
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAudioLimitAlertVisible, setAudioLimitAlertVisible] = useState(false);
|
||||
|
||||
usePrefixedDocumentTitle("Donald Trump TTS and Donald Trump AI Voice");
|
||||
|
||||
const handleLoading = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
let {
|
||||
setTtsModels,
|
||||
setAllTtsCategories,
|
||||
setComputedTtsCategoryAssignments,
|
||||
ttsModels,
|
||||
allTtsCategories,
|
||||
computedTtsCategoryAssignments,
|
||||
maybeSelectedTtsModel,
|
||||
setMaybeSelectedTtsModel,
|
||||
} = props;
|
||||
|
||||
const [maybeTtsError, setMaybeTtsError] = useState<
|
||||
GenerateTtsAudioErrorType | undefined
|
||||
>(undefined);
|
||||
|
||||
const ttsModelsLoaded = ttsModels.length > 0;
|
||||
const ttsCategoriesLoaded = allTtsCategories.length > 0;
|
||||
const computedTtsCategoryAssignmentsLoaded =
|
||||
computedTtsCategoryAssignments !== undefined &&
|
||||
computedTtsCategoryAssignments.category_token_to_tts_model_tokens.recursive
|
||||
.size > 0;
|
||||
|
||||
const listModels = useCallback(async () => {
|
||||
if (ttsModelsLoaded) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const models = await ListTtsModels();
|
||||
if (models) {
|
||||
DynamicallyCategorizeModels(models);
|
||||
setTtsModels(models);
|
||||
if (!maybeSelectedTtsModel && models.length > 0) {
|
||||
//let model = models[0];
|
||||
//const featuredModels = models.filter((m) => m.is_front_page_featured);
|
||||
//if (featuredModels.length > 0) {
|
||||
// // Random featured model
|
||||
// model =
|
||||
// featuredModels[Math.floor(Math.random() * featuredModels.length)];
|
||||
//}
|
||||
|
||||
let trumpModels = models.filter((model) => {
|
||||
return PAGE_MODEL_TOKENS.has(model.model_token);
|
||||
})
|
||||
|
||||
let model = trumpModels[Math.floor(Math.random() * trumpModels.length)];
|
||||
|
||||
setMaybeSelectedTtsModel(model);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
setTtsModels,
|
||||
maybeSelectedTtsModel,
|
||||
setMaybeSelectedTtsModel,
|
||||
ttsModelsLoaded,
|
||||
]);
|
||||
|
||||
const listTtsCategories = useCallback(async () => {
|
||||
if (ttsCategoriesLoaded) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const categoryList = await ListTtsCategories();
|
||||
if (ListTtsCategoriesIsOk(categoryList)) {
|
||||
let categories = categoryList.categories;
|
||||
|
||||
// NB: We'll use the frontend to order the synthetic categories first.
|
||||
|
||||
const LATEST_MODELS_CATEGORY_TOKEN = "SYNTHETIC_CATEGORY:LATEST_MODELS";
|
||||
const TRENDING_MODELS_CATEGORY_TOKEN =
|
||||
"SYNTHETIC_CATEGORY:TRENDING_MODELS";
|
||||
|
||||
let maybeLatestCategory = categories.find(
|
||||
(category) => category.category_token === LATEST_MODELS_CATEGORY_TOKEN
|
||||
);
|
||||
|
||||
let maybeTrendingCategory = categories.find(
|
||||
(category) => category.category_token === TRENDING_MODELS_CATEGORY_TOKEN
|
||||
);
|
||||
|
||||
let otherCategories = categories
|
||||
.filter(
|
||||
(category) => category.category_token !== LATEST_MODELS_CATEGORY_TOKEN
|
||||
)
|
||||
.filter(
|
||||
(category) =>
|
||||
category.category_token !== TRENDING_MODELS_CATEGORY_TOKEN
|
||||
);
|
||||
|
||||
categories = [];
|
||||
|
||||
if (maybeLatestCategory !== undefined) {
|
||||
categories.push(maybeLatestCategory);
|
||||
}
|
||||
|
||||
if (maybeTrendingCategory !== undefined) {
|
||||
categories.push(maybeTrendingCategory);
|
||||
}
|
||||
|
||||
categories.push(...otherCategories);
|
||||
|
||||
setAllTtsCategories(categories);
|
||||
} else if (ListTtsCategoriesIsError(categoryList)) {
|
||||
// TODO: Retry on decay function
|
||||
}
|
||||
}, [setAllTtsCategories, ttsCategoriesLoaded]);
|
||||
|
||||
const getComputedAssignments = useCallback(async () => {
|
||||
if (computedTtsCategoryAssignmentsLoaded) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const computedAssignments = await GetComputedTtsCategoryAssignments();
|
||||
if (GetComputedTtsCategoryAssignmentsIsOk(computedAssignments)) {
|
||||
setComputedTtsCategoryAssignments(computedAssignments);
|
||||
} else if (GetComputedTtsCategoryAssignmentsIsError(computedAssignments)) {
|
||||
// TODO: Retry on decay function
|
||||
}
|
||||
}, [setComputedTtsCategoryAssignments, computedTtsCategoryAssignmentsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
listModels();
|
||||
listTtsCategories();
|
||||
getComputedAssignments();
|
||||
}, [listModels, listTtsCategories, getComputedAssignments]);
|
||||
|
||||
const handleChangeText = (ev: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const textValue = (ev.target as HTMLTextAreaElement).value;
|
||||
props.setTextBuffer(textValue);
|
||||
setAudioLimitAlertVisible(textValue.length > 100);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!props.maybeSelectedTtsModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!props.textBuffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const modelToken = props.maybeSelectedTtsModel!.model_token;
|
||||
|
||||
const request = {
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
tts_model_token: modelToken,
|
||||
inference_text: props.textBuffer,
|
||||
};
|
||||
|
||||
const response = await GenerateTtsAudio(request);
|
||||
|
||||
Analytics.ttsGenerate(modelToken, props.textBuffer.length);
|
||||
|
||||
if (GenerateTtsAudioIsOk(response)) {
|
||||
setMaybeTtsError(undefined);
|
||||
props.enqueueTtsJob(response.inference_job_token);
|
||||
} else if (GenerateTtsAudioIsError(response)) {
|
||||
setMaybeTtsError(response.error);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleClearClick = (ev: React.FormEvent<HTMLButtonElement>) => {
|
||||
ev.preventDefault();
|
||||
props.clearTextBuffer();
|
||||
|
||||
Analytics.ttsClear(props.maybeSelectedTtsModel?.model_token);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Show errors on TTS failure
|
||||
let maybeError = <></>;
|
||||
if (!!maybeTtsError) {
|
||||
let hasMessage = false;
|
||||
let message = <></>;
|
||||
switch (maybeTtsError) {
|
||||
case GenerateTtsAudioErrorType.TooManyRequests:
|
||||
hasMessage = true;
|
||||
message = (
|
||||
<Trans i18nKey="pages.ttsList.errorTooManyRequests">
|
||||
<strong>You're sending too many requests!</strong>
|
||||
Slow down a little. We have to slow things down a little when the
|
||||
server gets busy.
|
||||
</Trans>
|
||||
);
|
||||
break;
|
||||
case GenerateTtsAudioErrorType.ServerError |
|
||||
GenerateTtsAudioErrorType.BadRequest |
|
||||
GenerateTtsAudioErrorType.NotFound:
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasMessage) {
|
||||
maybeError = (
|
||||
<div
|
||||
className="alert alert-primary alert-dismissible fade show m-0"
|
||||
role="alert"
|
||||
>
|
||||
<button
|
||||
className="btn-close"
|
||||
onClick={() => setMaybeTtsError(undefined)}
|
||||
data-bs-dismiss="alert"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// NB: If the text is too long, don't allow submission
|
||||
let remainingCharactersButtonDisabled = props.textBuffer.trim().length > 1024;
|
||||
let noTextInputButtonDisabled = props.textBuffer.trim() === "";
|
||||
|
||||
const speakButtonClass = loading
|
||||
? "btn btn-primary w-100 disabled"
|
||||
: "btn btn-primary w-100";
|
||||
|
||||
let audioLimitAlert = <></>;
|
||||
if (
|
||||
isAudioLimitAlertVisible &&
|
||||
!props.sessionSubscriptionsWrapper.hasPaidFeatures()
|
||||
) {
|
||||
audioLimitAlert = (
|
||||
<>
|
||||
<div className="alert alert-warning fs-7 mb-0">
|
||||
<span className="fw-semibold">
|
||||
<u>Note:</u> Non-premium is limited to 12 seconds of audio.{" "}
|
||||
<Link className="fw-semibold" to="/pricing">
|
||||
Upgrade now
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let trumpModels = props.ttsModels.filter((model) => {
|
||||
return PAGE_MODEL_TOKENS.has(model.model_token);
|
||||
})
|
||||
|
||||
let maybeSelectedModel = props.maybeSelectedTtsModel;
|
||||
|
||||
if (maybeSelectedModel !== undefined && !PAGE_MODEL_TOKENS.has(maybeSelectedModel.model_token)) {
|
||||
// If we don't select a trump model by default (which is rare), the list will be empty.
|
||||
// We'll set it here.
|
||||
maybeSelectedModel = trumpModels[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<TtsPageHero
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
/>
|
||||
|
||||
<motion.div className="container-panel pb-5 mb-4" variants={panel}>
|
||||
<div className="panel p-3 py-4 p-md-4">
|
||||
<i className="fas fa-volume-high"></i>
|
||||
|
||||
<div className="d-flex gap-4">
|
||||
<form
|
||||
className="w-100 d-flex flex-column"
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
{/* Explore Rollout */}
|
||||
<SearchOmnibar
|
||||
allTtsCategories={props.allTtsCategories}
|
||||
allTtsModels={trumpModels}
|
||||
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
|
||||
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
|
||||
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
|
||||
dropdownCategories={props.dropdownCategories}
|
||||
setDropdownCategories={props.setDropdownCategories}
|
||||
selectedCategories={props.selectedCategories}
|
||||
setSelectedCategories={props.setSelectedCategories}
|
||||
maybeSelectedTtsModel={maybeSelectedModel}
|
||||
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
|
||||
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
|
||||
setSelectedTtsLanguageScope={props.setSelectedTtsLanguageScope}
|
||||
/>
|
||||
{/*
|
||||
|
||||
|
||||
EXPLORE OMNIBAR GOES HERE TODO
|
||||
|
||||
*/}
|
||||
|
||||
<div className="row gx-5 gy-5">
|
||||
<div className="col-12 col-lg-6 d-flex flex-column gap-3">
|
||||
<div className="d-flex flex-column gap-3 h-100">
|
||||
<div className="d-flex gap-2">
|
||||
<label className="sub-title pb-0">
|
||||
{t("tts.TtsModelListPage.form.yourTextLabel")}
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
onClick={() => {
|
||||
Analytics.ttsClickTextInputBox();
|
||||
}}
|
||||
onChange={handleChangeText}
|
||||
className="form-control text-message h-100"
|
||||
value={props.textBuffer}
|
||||
placeholder={t(
|
||||
"tts.TtsModelListPage.form.textInputHint",
|
||||
{
|
||||
voice:
|
||||
props.maybeSelectedTtsModel?.title || "the voice",
|
||||
}
|
||||
)}
|
||||
></textarea>
|
||||
{audioLimitAlert}
|
||||
<div className="d-flex gap-3">
|
||||
<button
|
||||
className={speakButtonClass}
|
||||
disabled={
|
||||
remainingCharactersButtonDisabled ||
|
||||
noTextInputButtonDisabled
|
||||
}
|
||||
onClick={handleLoading}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faVolumeHigh} className="me-2" />
|
||||
{t("tts.TtsModelListPage.form.buttons.speak")}
|
||||
|
||||
{loading && <LoadingIcon />}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-destructive w-100"
|
||||
onClick={handleClearClick}
|
||||
disabled={noTextInputButtonDisabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDeleteLeft} className="me-2" />
|
||||
{t("tts.TtsModelListPage.form.buttons.clear")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<h4 className="text-center text-lg-start">
|
||||
<FontAwesomeIcon
|
||||
icon={faBarsStaggered}
|
||||
className="me-3"
|
||||
/>
|
||||
{t("tts.TtsModelListPage.sessionTtsResultsLabel")}
|
||||
</h4>
|
||||
<div className="d-flex flex-column gap-3 session-tts-section">
|
||||
<SessionTtsInferenceResultList
|
||||
ttsInferenceJobs={props.ttsInferenceJobs}
|
||||
sessionSubscriptionsWrapper={
|
||||
props.sessionSubscriptionsWrapper
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{maybeError}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const LoadingIcon: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm ms-3"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { TrumpTtsPage };
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { Link } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faStar,
|
||||
faArrowRight,
|
||||
faVolume,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Analytics } from "../../../../../common/Analytics";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { PageHeaderWithImage } from "../../../_common/PageHeaderWithImage";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
}
|
||||
|
||||
export function TtsPageHero(props: Props) {
|
||||
const randomImage = useMemo(() => {
|
||||
const images = ["/mascot/trump.webp"];
|
||||
|
||||
return images[Math.floor(Math.random() * images.length)];
|
||||
}, []);
|
||||
|
||||
let signUpButton = <></>;
|
||||
let viewPricingButton = <></>;
|
||||
|
||||
signUpButton = (
|
||||
<>
|
||||
<Link to="/">
|
||||
<button type="button" className="btn btn-primary w-100">
|
||||
See 2000+ more voices
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
viewPricingButton = (
|
||||
<>
|
||||
<Link
|
||||
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
|
||||
onClick={() => {
|
||||
Analytics.ttsClickHeroViewPricing();
|
||||
}}
|
||||
>
|
||||
<button type="button" className="btn btn-secondary w-100">
|
||||
<FontAwesomeIcon icon={faStar} className="me-2" />
|
||||
{t("tts.TtsModelListPage.heroSection.buttons.viewPricing")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
const titleIcon = <FontAwesomeIcon icon={faVolume} className="me-3" />;
|
||||
const title = <>Donald Trump TTS</>;
|
||||
const subText = (
|
||||
<>FakeYou has the very best Donald Trump AI voice on the internet. Use deep fake Donald Trump to say your favorite memes.</>
|
||||
);
|
||||
const actionButtons = (
|
||||
<>
|
||||
{signUpButton}
|
||||
{viewPricingButton}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeaderWithImage
|
||||
headerImage={randomImage}
|
||||
titleIcon={titleIcon}
|
||||
title={title}
|
||||
subText={subText}
|
||||
showButtons={true}
|
||||
actionButtons={actionButtons}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { TtsCategoryType } from "../../../../../../AppWrapper";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { ScopedVoiceModelOptions } from "./components/ScopedVoiceModelOptions";
|
||||
import { VoiceCountLabel } from "./components/VoiceCountLabel";
|
||||
|
||||
interface Props {
|
||||
allTtsCategories: TtsCategoryType[];
|
||||
allTtsModels: TtsModelListItem[];
|
||||
|
||||
allTtsCategoriesByTokenMap: Map<string, TtsCategoryType>;
|
||||
allTtsModelsByTokenMap: Map<string, TtsModelListItem>;
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
|
||||
|
||||
dropdownCategories: TtsCategoryType[][];
|
||||
setDropdownCategories: (dropdownCategories: TtsCategoryType[][]) => void;
|
||||
|
||||
selectedCategories: TtsCategoryType[];
|
||||
setSelectedCategories: (selectedCategories: TtsCategoryType[]) => void;
|
||||
|
||||
maybeSelectedTtsModel?: TtsModelListItem;
|
||||
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
|
||||
|
||||
selectedTtsLanguageScope: string;
|
||||
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
|
||||
}
|
||||
|
||||
export function SearchOmnibar(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<div className="pb-4">
|
||||
<VoiceCountLabel
|
||||
allTtsModels={props.allTtsModels}
|
||||
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
|
||||
selectedCategories={props.selectedCategories}
|
||||
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
|
||||
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
|
||||
/>
|
||||
|
||||
<div className="d-flex flex-column flex-lg-row gap-3">
|
||||
<div className="flex-grow-1">
|
||||
<ScopedVoiceModelOptions
|
||||
allTtsCategories={props.allTtsCategories}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
|
||||
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
|
||||
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
|
||||
dropdownCategories={props.dropdownCategories}
|
||||
selectedCategories={props.selectedCategories}
|
||||
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
|
||||
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
|
||||
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { TtsCategoryType } from "../../../../../../../AppWrapper";
|
||||
import Select, { createFilter } from "react-select";
|
||||
import { SearchFieldClass } from "./SearchFieldClass";
|
||||
import { FastReactSelectOption } from "../../../../../_common/react_select/FastReactSelectOption";
|
||||
import { Analytics } from "../../../../../../../common/Analytics";
|
||||
import { FixedSingleValueSelectOption } from "../../../../../_common/react_select/FixedSingleValueSelectOption";
|
||||
import { faMicrophone } from "@fortawesome/pro-duotone-svg-icons";
|
||||
|
||||
interface Props {
|
||||
allTtsCategories: TtsCategoryType[];
|
||||
allTtsModels: TtsModelListItem[];
|
||||
|
||||
allTtsCategoriesByTokenMap: Map<string, TtsCategoryType>;
|
||||
allTtsModelsByTokenMap: Map<string, TtsModelListItem>;
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
|
||||
|
||||
dropdownCategories: TtsCategoryType[][];
|
||||
selectedCategories: TtsCategoryType[];
|
||||
|
||||
maybeSelectedTtsModel?: TtsModelListItem;
|
||||
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
|
||||
|
||||
selectedTtsLanguageScope: string;
|
||||
}
|
||||
|
||||
export function ScopedVoiceModelOptions(props: Props) {
|
||||
const {
|
||||
allTtsModels,
|
||||
ttsModelsByCategoryToken,
|
||||
selectedCategories,
|
||||
maybeSelectedTtsModel,
|
||||
} = props;
|
||||
|
||||
//const { t } = useTranslation();
|
||||
|
||||
const handleChange = (option: any, actionMeta: any) => {
|
||||
const ttsModelToken = option?.value;
|
||||
const maybeNewTtsModel = props.allTtsModelsByTokenMap.get(ttsModelToken);
|
||||
if (maybeNewTtsModel !== undefined) {
|
||||
props.setMaybeSelectedTtsModel(maybeNewTtsModel);
|
||||
}
|
||||
};
|
||||
|
||||
const leafiestCategory = selectedCategories[selectedCategories.length - 1];
|
||||
|
||||
let leafiestCategoryModels: Array<TtsModelListItem> = [];
|
||||
|
||||
if (leafiestCategory !== undefined) {
|
||||
leafiestCategoryModels = Array.from(
|
||||
ttsModelsByCategoryToken.get(leafiestCategory.category_token) || new Set()
|
||||
);
|
||||
} else {
|
||||
leafiestCategoryModels = Array.from(new Set(allTtsModels));
|
||||
}
|
||||
|
||||
interface DropdownOption {
|
||||
label: string;
|
||||
value: string;
|
||||
creatorName?: string;
|
||||
}
|
||||
|
||||
let options: DropdownOption[] = leafiestCategoryModels
|
||||
.filter((ttsModel) => {
|
||||
// Scope to currently selected language
|
||||
if (props.selectedTtsLanguageScope === "*") {
|
||||
return true; // NB: Sentinel value of "*" means all languages.
|
||||
}
|
||||
return (
|
||||
ttsModel.ietf_primary_language_subtag === props.selectedTtsLanguageScope
|
||||
);
|
||||
})
|
||||
.map((ttsModel) => {
|
||||
return {
|
||||
label: ttsModel.title,
|
||||
value: ttsModel.model_token,
|
||||
creatorName: ttsModel.creator_display_name,
|
||||
};
|
||||
});
|
||||
|
||||
let selectedOption = options.find(
|
||||
(option) => option.value === maybeSelectedTtsModel?.model_token
|
||||
);
|
||||
|
||||
if (selectedOption === undefined && options.length > 0) {
|
||||
// NB: We shouldn't select the first item in the list since that won't update the currently
|
||||
// selected model. If the user were to close the dialogue, they'd think they had picked a voice,
|
||||
// when in reality no state would have changed. By forcing the user to choose, the user will set
|
||||
// the state appropriately.
|
||||
selectedOption = {
|
||||
label: "Select voice...",
|
||||
value: "*",
|
||||
creatorName: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
let isLoading = false;
|
||||
|
||||
if (props.allTtsModels.length === 0) {
|
||||
// NB: react-select will cache values, even across different instances (!!!)
|
||||
// This can cause confusion when initializing a select instance before the data
|
||||
// is loaded, and the select will never update to show the new data.
|
||||
// The proper way to change voices after load from a placeholder "Loading..."
|
||||
// label is to use controlled props / value as is done here:
|
||||
isLoading = true;
|
||||
selectedOption = {
|
||||
label: "Loading...",
|
||||
value: "*",
|
||||
creatorName: undefined,
|
||||
};
|
||||
} else if (options.length === 0) {
|
||||
// NB: Perhaps the user has refined their search to be too narrow (langauge + category)
|
||||
selectedOption = {
|
||||
label: "No results (remove some filters)",
|
||||
value: "*",
|
||||
creatorName: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const numberVoices = options.length;
|
||||
|
||||
const canSearchVoices =
|
||||
numberVoices > 100 || // Always allow search when there are over 100 voices
|
||||
window.innerWidth >= 1000; // Always allow search when on desktop
|
||||
|
||||
let select;
|
||||
|
||||
// Function to build the options themselves, so we can introduce extra elements.
|
||||
const formatOptionLabel = (
|
||||
data: DropdownOption,
|
||||
formatOptionLabelMeta: any
|
||||
) => {
|
||||
let creatorName = <></>;
|
||||
if (data.creatorName !== undefined) {
|
||||
creatorName = <span className="opacity-50"> — {data.creatorName}</span>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{data.label}
|
||||
{creatorName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Cleanup
|
||||
|
||||
if (canSearchVoices) {
|
||||
select = (
|
||||
<Select
|
||||
value={selectedOption} // Controlled components use "value" instead of "defaultValue".
|
||||
options={options}
|
||||
classNames={SearchFieldClass}
|
||||
onChange={handleChange}
|
||||
onMenuOpen={() => {
|
||||
Analytics.ttsOpenPrimaryVoiceSelectMenu();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
isSearchable={true}
|
||||
// NB: The following settings improve upon performance.
|
||||
// See: https://github.com/JedWatson/react-select/issues/3128
|
||||
filterOption={createFilter({ ignoreAccents: false })}
|
||||
components={
|
||||
{
|
||||
SingleValue: FixedSingleValueSelectOption,
|
||||
Option: FastReactSelectOption,
|
||||
} as any
|
||||
}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
select = (
|
||||
<Select
|
||||
value={selectedOption}
|
||||
options={options}
|
||||
classNames={SearchFieldClass}
|
||||
onChange={handleChange}
|
||||
onMenuOpen={() => {
|
||||
Analytics.ttsOpenScopedVoiceSelectMenu();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
// On mobile, we don't want the onscreen keyboard to take up half the UI.
|
||||
autoFocus={false}
|
||||
isSearchable={false}
|
||||
// NB: The following settings improve upon performance.
|
||||
// See: https://github.com/JedWatson/react-select/issues/3128
|
||||
filterOption={createFilter({ ignoreAccents: false })}
|
||||
components={{ Option: FastReactSelectOption } as any}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col">
|
||||
<div className="input-icon-search">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faMicrophone} />
|
||||
</span>
|
||||
|
||||
{select}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const SearchFieldClass = {
|
||||
control: (state: any) =>
|
||||
state.isFocused ? "select-search focused" : "select-search",
|
||||
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",
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from "react";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { TtsCategoryType } from "../../../../../../../AppWrapper";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faShuffle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Analytics } from "../../../../../../../common/Analytics";
|
||||
import { GetRandomArrayValue } from "@storyteller/components/src/utils/GetRandomArrayValue";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
|
||||
interface Props {
|
||||
allTtsModels: TtsModelListItem[];
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
|
||||
selectedCategories: TtsCategoryType[];
|
||||
selectedTtsLanguageScope: string;
|
||||
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
|
||||
}
|
||||
|
||||
// NB/TODO: This duplicates the work of a sister component, but it was the fastest way
|
||||
// to hack this in without passing callbacks around or moving calculation higher up the
|
||||
// component tree.
|
||||
|
||||
export function VoiceCountLabel(props: Props) {
|
||||
const { allTtsModels, ttsModelsByCategoryToken, selectedCategories } = props;
|
||||
|
||||
const leafiestCategory = selectedCategories[selectedCategories.length - 1];
|
||||
|
||||
let leafiestCategoryModels: Array<TtsModelListItem> = [];
|
||||
|
||||
if (leafiestCategory !== undefined) {
|
||||
leafiestCategoryModels = Array.from(
|
||||
ttsModelsByCategoryToken.get(leafiestCategory.category_token) || new Set()
|
||||
);
|
||||
} else {
|
||||
leafiestCategoryModels = Array.from(new Set(allTtsModels));
|
||||
}
|
||||
|
||||
let possibleVoices = leafiestCategoryModels.filter((ttsModel) => {
|
||||
// Scope to currently selected language
|
||||
if (props.selectedTtsLanguageScope === "*") {
|
||||
return true; // NB: Sentinel value of "*" means all languages.
|
||||
}
|
||||
return (
|
||||
ttsModel.ietf_primary_language_subtag === props.selectedTtsLanguageScope
|
||||
);
|
||||
});
|
||||
|
||||
const selectRandomVoice = () => {
|
||||
let randomVoice = undefined;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// We're going to try to find a *good* voice.
|
||||
randomVoice = GetRandomArrayValue(possibleVoices);
|
||||
if (
|
||||
randomVoice.user_ratings.positive_count >
|
||||
randomVoice.user_ratings.negative_count
|
||||
) {
|
||||
break; // First "mostly good" voice is good.
|
||||
}
|
||||
if (i % 3 === 2 && randomVoice.user_ratings.total_count === 0) {
|
||||
break; // Sometimes we'll return a voice that hasn't been rated.
|
||||
}
|
||||
}
|
||||
if (randomVoice !== undefined) {
|
||||
props.setMaybeSelectedTtsModel(randomVoice);
|
||||
}
|
||||
};
|
||||
|
||||
const voiceCount = possibleVoices.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex gap-2">
|
||||
<label className="sub-title">
|
||||
Choose a Donald Trump Voice ({voiceCount} to choose from)
|
||||
<Tippy
|
||||
content="Random Voice"
|
||||
hideOnClick
|
||||
placement="top"
|
||||
theme="fakeyou"
|
||||
arrow={false}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
Analytics.ttsClickRandomVoice();
|
||||
selectRandomVoice();
|
||||
}}
|
||||
className="btn-link pt-0 ps-2 pe-0"
|
||||
type="button"
|
||||
>
|
||||
<FontAwesomeIcon icon={faShuffle} />
|
||||
</button>
|
||||
</Tippy>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRightLong, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { TtsCategoryType } from "../../../../../../../AppWrapper";
|
||||
//import { useTranslation } from "react-i18next";
|
||||
import Select from "react-select";
|
||||
import { SearchFieldClass } from "../components/SearchFieldClass";
|
||||
import { Analytics } from "../../../../../../../common/Analytics";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface Props {
|
||||
allTtsCategories: TtsCategoryType[];
|
||||
allTtsModels: TtsModelListItem[];
|
||||
|
||||
allTtsCategoriesByTokenMap: Map<string, TtsCategoryType>;
|
||||
allTtsModelsByTokenMap: Map<string, TtsModelListItem>;
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
|
||||
|
||||
dropdownCategories: TtsCategoryType[][];
|
||||
setDropdownCategories: (dropdownCategories: TtsCategoryType[][]) => void;
|
||||
|
||||
selectedCategories: TtsCategoryType[];
|
||||
setSelectedCategories: (selectedCategories: TtsCategoryType[]) => void;
|
||||
|
||||
maybeSelectedTtsModel?: TtsModelListItem;
|
||||
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
|
||||
|
||||
handleChangeCategory: (level: number, maybeToken?: string) => void;
|
||||
|
||||
selectedTtsLanguageScope: string;
|
||||
}
|
||||
|
||||
export function CategoryOptions(props: Props) {
|
||||
const {
|
||||
ttsModelsByCategoryToken,
|
||||
dropdownCategories,
|
||||
selectedCategories,
|
||||
handleChangeCategory,
|
||||
} = props;
|
||||
|
||||
//const { t } = useTranslation();
|
||||
|
||||
let categoryDropdowns = buildDropdowns(
|
||||
dropdownCategories,
|
||||
selectedCategories,
|
||||
ttsModelsByCategoryToken,
|
||||
handleChangeCategory
|
||||
);
|
||||
|
||||
const CATEGORY_SEPARATOR = (
|
||||
<div className="d-none d-md-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faArrowRightLong} className="fs-6 opacity-75" />
|
||||
</div>
|
||||
);
|
||||
|
||||
let categoryDropdownsWithSeparators = [];
|
||||
for (let i = 0; i < categoryDropdowns.length; i++) {
|
||||
categoryDropdownsWithSeparators.push(categoryDropdowns[i]);
|
||||
if (i < categoryDropdowns.length - 1) {
|
||||
categoryDropdownsWithSeparators.push(CATEGORY_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex flex-column flex-md-row gap-2">
|
||||
{categoryDropdownsWithSeparators}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ========= Build dropdowns ========
|
||||
|
||||
function buildDropdowns(
|
||||
dropdownCategories: TtsCategoryType[][],
|
||||
selectedCategories: TtsCategoryType[],
|
||||
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>,
|
||||
handleChangeCategory: (i: number, categoryToken?: string) => void
|
||||
) {
|
||||
if (dropdownCategories.length === 0 || ttsModelsByCategoryToken.size === 0) {
|
||||
// While the XHR requests are still completing, we may have nothing to build.
|
||||
// It's easier to return a fully disabled "loading" <Select /> component.
|
||||
return [
|
||||
<div className="w-100">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
</span>
|
||||
<Select
|
||||
isLoading={true}
|
||||
options={[]}
|
||||
inputValue={"Loading..."}
|
||||
classNames={SearchFieldClass}
|
||||
className={"w-100"}
|
||||
/>
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
let categoryDropdowns = [];
|
||||
|
||||
for (let i = 0; i < dropdownCategories.length; i++) {
|
||||
const currentDropdownCategories = dropdownCategories[i];
|
||||
const selectedCategory = selectedCategories[i];
|
||||
|
||||
let defaultName = i === 0 ? "All Voices" : "Select...";
|
||||
|
||||
let dropdownOptions = [];
|
||||
dropdownOptions.push(
|
||||
<option key={`option-${i}-*`} value="*">
|
||||
{defaultName}
|
||||
</option>
|
||||
);
|
||||
|
||||
// TODO(bt, 2023-01-18): Clean this up
|
||||
|
||||
// Transform into "react-select" library compatible options
|
||||
const options = currentDropdownCategories
|
||||
.filter((category) => {
|
||||
// If there are no models at the leaves, skip the category
|
||||
const models = ttsModelsByCategoryToken.get(category.category_token);
|
||||
return !(models === undefined || models.size === 0);
|
||||
})
|
||||
.map((category) => {
|
||||
return {
|
||||
value: category.category_token,
|
||||
label: category.name_for_dropdown,
|
||||
};
|
||||
});
|
||||
|
||||
currentDropdownCategories.forEach((category) => {
|
||||
const models = ttsModelsByCategoryToken.get(category.category_token);
|
||||
if (models === undefined || models.size === 0) {
|
||||
return; // If there are no models at the leaves, skip
|
||||
}
|
||||
dropdownOptions.push(
|
||||
<option
|
||||
key={`option-${i}-${category.category_token}`}
|
||||
value={category.category_token}
|
||||
>
|
||||
{category.name_for_dropdown}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
let selectedCategoryOption = undefined;
|
||||
if (selectedCategory !== undefined) {
|
||||
selectedCategoryOption = {
|
||||
value: selectedCategory.category_token,
|
||||
label: selectedCategory.name_for_dropdown,
|
||||
};
|
||||
}
|
||||
|
||||
if (dropdownOptions.length <= 1) {
|
||||
// We've run out of subcategories. (1 == "Select...")
|
||||
// No sense trying to build more.
|
||||
break;
|
||||
}
|
||||
|
||||
let selectProps: any = {
|
||||
options: options,
|
||||
classNames: SearchFieldClass,
|
||||
className: "w-100",
|
||||
autoFocus: false, // On mobile, we don't want the onscreen keyboard to take up half the UI.
|
||||
isSearchable: false, // On mobile, we don't want the onscreen keyboard to take up half the UI.
|
||||
onMenuOpen: () => {
|
||||
Analytics.ttsOpenCategorySelectMenu();
|
||||
},
|
||||
onChange: (option: any) => handleChangeCategory(i, option?.value),
|
||||
};
|
||||
|
||||
if (selectedCategoryOption === undefined) {
|
||||
// NB(bt, 2023-01-19): I'm not sure why we're having to do this to clear categories.
|
||||
// If I had more time to spend with this library, I might have a better solution than this hack.
|
||||
selectProps["value"] = {
|
||||
value: "*",
|
||||
label: t("tts.TtsModelListPage.exploreModal.selectCategoryText"),
|
||||
};
|
||||
} else {
|
||||
selectProps["value"] = selectedCategoryOption;
|
||||
}
|
||||
|
||||
categoryDropdowns.push(
|
||||
<React.Fragment key={`categoryDropdown-${i}`}>
|
||||
<div className="w-100">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
</span>
|
||||
<Select {...selectProps} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return categoryDropdowns;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Select from "react-select";
|
||||
import { SearchFieldClass } from "../components/SearchFieldClass";
|
||||
import { AVAILABLE_TTS_LANGUAGE_CATEGORY_MAP } from "../../../../../../../_i18n/AvailableLanguageMap";
|
||||
import { Analytics } from "../../../../../../../common/Analytics";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface Props {
|
||||
selectedTtsLanguageScope: string;
|
||||
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
|
||||
}
|
||||
|
||||
export function LanguageOptions(props: Props) {
|
||||
const handleChange = (option: any, actionMeta: any) => {
|
||||
props.setSelectedTtsLanguageScope(option.value);
|
||||
};
|
||||
|
||||
let languageOptions = Object.entries(AVAILABLE_TTS_LANGUAGE_CATEGORY_MAP).map(
|
||||
([languageCode, language]) => {
|
||||
let label = `${language.languageName}`;
|
||||
|
||||
if (language.languageNameLocalized !== undefined) {
|
||||
label = `${language.languageNameLocalized} / ${label}`;
|
||||
}
|
||||
|
||||
if (language.flags.length > 0) {
|
||||
label += ` ${language.flags.join(" ")}`;
|
||||
}
|
||||
|
||||
return {
|
||||
value: languageCode,
|
||||
label: label,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
languageOptions = [
|
||||
{
|
||||
label: `${t("tts.TtsModelListPage.exploreModal.allLanguagesOptionText")}`,
|
||||
value: "*",
|
||||
},
|
||||
...languageOptions,
|
||||
];
|
||||
|
||||
const currentValue =
|
||||
languageOptions.find((option) => {
|
||||
return option.value === props.selectedTtsLanguageScope;
|
||||
}) || languageOptions[0];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
</span>
|
||||
<Select
|
||||
value={currentValue}
|
||||
options={languageOptions}
|
||||
classNames={SearchFieldClass}
|
||||
isSearchable={false}
|
||||
onChange={handleChange}
|
||||
onMenuOpen={() => {
|
||||
Analytics.ttsOpenCategorySelectMenu();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocument
|
||||
interface Props {}
|
||||
|
||||
function VoiceCloneRequestPage(props: Props) {
|
||||
|
||||
usePrefixedDocumentTitle("Create a Custom Voice Clone with Deep Fake TTS");
|
||||
|
||||
// Contact
|
||||
@@ -262,7 +261,7 @@ function VoiceCloneRequestPage(props: Props) {
|
||||
</div>
|
||||
<div className="col-lg-6 px-3 px-md-2 px-lg-5 px-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<motion.h1 className="display-5 fw-bold lh-1" variants={item}>
|
||||
<motion.h1 className=" fw-bold lh-1" variants={item}>
|
||||
Professionally Clone Your Voice
|
||||
</motion.h1>
|
||||
<motion.h3 className="mb-4" variants={item}>
|
||||
|
||||
@@ -44,7 +44,7 @@ function ContributeIndexPage(props: Props) {
|
||||
You make FakeYou better by contributing
|
||||
</Trans>
|
||||
</h5>
|
||||
{t("pages.contributeIndex.introText")}
|
||||
<div className="opacity-75">{t("pages.contributeIndex.introText")}</div>
|
||||
</>
|
||||
);
|
||||
const titleIcon = (
|
||||
@@ -55,7 +55,7 @@ function ContributeIndexPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
{/* <div className="container py-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
{t("pages.contributeIndex.heroTitle")}
|
||||
</motion.h1>
|
||||
<motion.h3 className="mb-4" variants={item}>
|
||||
|
||||
@@ -29,6 +29,9 @@ interface FirehoseEventListResponsePayload {
|
||||
interface FirehoseEvent {
|
||||
event_token: string;
|
||||
event_type: string;
|
||||
|
||||
maybe_target_user_info?: TargetUserInfo;
|
||||
|
||||
maybe_target_user_token?: string;
|
||||
maybe_target_username?: string;
|
||||
maybe_target_display_name?: string;
|
||||
@@ -38,6 +41,15 @@ interface FirehoseEvent {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TargetUserInfo {
|
||||
user_token: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
gravatar_hash: string;
|
||||
default_avatar_index: number;
|
||||
default_avatar_color_index: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
@@ -95,7 +107,19 @@ function FirehoseEventListPage(props: Props) {
|
||||
firehoseEvents.slice(0, 16).forEach((event) => {
|
||||
let inner = <span />;
|
||||
let userLink = <span>Anonymous user</span>;
|
||||
let gravatar = <span />;
|
||||
let gravatar = (
|
||||
<div className="me-2">
|
||||
<Gravatar
|
||||
size={32}
|
||||
username={event.maybe_target_username}
|
||||
email_hash={event.maybe_target_user_gravatar_hash}
|
||||
avatarIndex={event.maybe_target_user_info?.default_avatar_index || 0}
|
||||
backgroundIndex={
|
||||
event.maybe_target_user_info?.default_avatar_color_index
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (
|
||||
event.maybe_target_username !== undefined &&
|
||||
@@ -106,11 +130,17 @@ function FirehoseEventListPage(props: Props) {
|
||||
let link = `/profile/${event.maybe_target_username}`;
|
||||
userLink = <Link to={link}>{event.maybe_target_display_name}</Link>;
|
||||
gravatar = (
|
||||
<div className="me-1">
|
||||
<div className="me-2">
|
||||
<Gravatar
|
||||
size={22}
|
||||
size={32}
|
||||
username={event.maybe_target_username}
|
||||
email_hash={event.maybe_target_user_gravatar_hash}
|
||||
avatarIndex={
|
||||
event.maybe_target_user_info?.default_avatar_index || 0
|
||||
}
|
||||
backgroundIndex={
|
||||
event.maybe_target_user_info?.default_avatar_color_index
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -122,7 +152,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faPenToSquare} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
signed up for FakeYou!
|
||||
</span>
|
||||
@@ -133,7 +162,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faAward} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
got a badge!
|
||||
</span>
|
||||
@@ -144,7 +172,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faPlay} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
started TTS model upload
|
||||
</span>
|
||||
@@ -155,7 +182,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faFlagCheckered} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
completed TTS model upload
|
||||
</span>
|
||||
@@ -166,7 +192,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faPlay} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
started TTS
|
||||
</span>
|
||||
@@ -177,7 +202,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faFlagCheckered} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
completed TTS
|
||||
</span>
|
||||
@@ -188,7 +212,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faPlay} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
started uploading a lipsync template.
|
||||
</span>
|
||||
@@ -199,7 +222,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faFlagCheckered} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
finished uploading a lipsync template.
|
||||
</span>
|
||||
@@ -210,7 +232,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faPlay} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
started a W2L lipsync video
|
||||
</span>
|
||||
@@ -221,7 +242,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faFlagCheckered} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
completed a W2L lipsync video
|
||||
</span>
|
||||
@@ -232,7 +252,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
mentioned us on twitter!
|
||||
</span>
|
||||
@@ -243,7 +262,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
retweeted us!
|
||||
</span>
|
||||
@@ -254,7 +272,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
joined discord!
|
||||
</span>
|
||||
@@ -265,7 +282,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
sent a discord message
|
||||
</span>
|
||||
@@ -276,7 +292,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faTwitch} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
subscribed to us on twitch!
|
||||
</span>
|
||||
@@ -287,7 +302,6 @@ function FirehoseEventListPage(props: Props) {
|
||||
<span className="d-flex align-items-center">
|
||||
<FontAwesomeIcon icon={faTwitch} className="me-3" />
|
||||
{gravatar}
|
||||
|
||||
{userLink}
|
||||
followed us on twitch!
|
||||
</span>
|
||||
@@ -310,7 +324,7 @@ function FirehoseEventListPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Firehose Event Feed
|
||||
</motion.h1>
|
||||
<motion.h4 className="mb-4" variants={item}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
GetLeaderboardIsErr,
|
||||
GetLeaderboardIsOk,
|
||||
Leaderboard,
|
||||
LeaderboardEntryForList,
|
||||
LeaderboardRow,
|
||||
LeaderboardLookupError,
|
||||
} from "@storyteller/components/src/api/leaderboard/GetLeaderboard";
|
||||
import { DiscordLink2 } from "@storyteller/components/src/elements/DiscordLink2";
|
||||
@@ -25,10 +25,10 @@ function LeaderboardPage(props: Props) {
|
||||
undefined
|
||||
);
|
||||
const [ttsLeaderboard, setTtsLeaderboard] = useState<
|
||||
Array<LeaderboardEntryForList> | undefined
|
||||
Array<LeaderboardRow> | undefined
|
||||
>(undefined);
|
||||
const [w2lLeaderboard, setW2lLeaderboard] = useState<
|
||||
Array<LeaderboardEntryForList> | undefined
|
||||
Array<LeaderboardRow> | undefined
|
||||
>(undefined);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
@@ -70,13 +70,17 @@ function LeaderboardPage(props: Props) {
|
||||
ttsRows.push(
|
||||
<tr>
|
||||
<td className="lb-name">
|
||||
<Link to={WebUrl.userProfilePage(ttsEntry.display_name)}>
|
||||
<Link
|
||||
to={WebUrl.userProfilePage(ttsEntry.display_name)}
|
||||
className="d-flex align-items-center gap-2"
|
||||
>
|
||||
<Gravatar
|
||||
size={12}
|
||||
size={32}
|
||||
username={ttsEntry.display_name}
|
||||
email_hash={ttsEntry.gravatar_hash}
|
||||
avatarIndex={ttsEntry.default_avatar_index}
|
||||
backgroundIndex={ttsEntry.default_avatar_color_index}
|
||||
/>
|
||||
|
||||
{ttsEntry.display_name}
|
||||
</Link>
|
||||
</td>
|
||||
@@ -93,13 +97,17 @@ function LeaderboardPage(props: Props) {
|
||||
w2lRows.push(
|
||||
<tr>
|
||||
<td className="lb-name">
|
||||
<Link to={WebUrl.userProfilePage(w2lEntry.display_name)}>
|
||||
<Link
|
||||
to={WebUrl.userProfilePage(w2lEntry.display_name)}
|
||||
className="d-flex align-items-center gap-2"
|
||||
>
|
||||
<Gravatar
|
||||
size={12}
|
||||
size={32}
|
||||
username={w2lEntry.display_name}
|
||||
email_hash={w2lEntry.gravatar_hash}
|
||||
avatarIndex={w2lEntry.default_avatar_index}
|
||||
backgroundIndex={w2lEntry.default_avatar_color_index}
|
||||
/>
|
||||
|
||||
{w2lEntry.display_name}
|
||||
</Link>
|
||||
</td>
|
||||
@@ -113,7 +121,7 @@ function LeaderboardPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Leaderboard
|
||||
</motion.h1>
|
||||
<motion.h3 className="mb-4" variants={item}>
|
||||
|
||||
@@ -138,16 +138,13 @@ function ModerationIpBanListFc(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="container py-5">
|
||||
<h1 className="display-5 fw-bold">Moderation IP Ban List</h1>
|
||||
<h1 className=" fw-bold">Moderation IP Ban List</h1>
|
||||
<p>
|
||||
IP Address bans will prevent bad actors from using and abusing the
|
||||
website.
|
||||
</p>
|
||||
<div className="pt-4">
|
||||
<BackLink
|
||||
link={WebUrl.moderationMain()}
|
||||
text="Back to moderation"
|
||||
/>
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -104,12 +104,9 @@ function ModerationJobStatsFc(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="container py-5">
|
||||
<h1 className="display-5 fw-bold">Job Stats</h1>
|
||||
<h1 className=" fw-bold">Job Stats</h1>
|
||||
<div className="pt-3">
|
||||
<BackLink
|
||||
link={WebUrl.moderationMain()}
|
||||
text="Back to moderation"
|
||||
/>
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -246,10 +243,7 @@ function ModerationJobStatsFc(props: Props) {
|
||||
</div>
|
||||
|
||||
<div className="container pb-5">
|
||||
<BackLink
|
||||
link={WebUrl.moderationMain()}
|
||||
text="Back to moderation"
|
||||
/>
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ function ModerationFc(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="container py-5">
|
||||
<h1 className="display-5 fw-bold text-center text-lg-start">
|
||||
<h1 className=" fw-bold text-center text-lg-start">
|
||||
Moderation Controls
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ interface Props {
|
||||
}
|
||||
|
||||
function PatronPage(props: Props) {
|
||||
|
||||
usePrefixedDocumentTitle("Thank you to our Patrons!");
|
||||
|
||||
return (
|
||||
@@ -21,7 +20,7 @@ function PatronPage(props: Props) {
|
||||
<div className="container py-5 pt-lg-0">
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-7 d-flex flex-column justify-content-center text-center text-lg-start px-3 px-md-2 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Thanks to our Patrons!
|
||||
</motion.h1>
|
||||
<motion.h3 className="mb-4 px-4 px-md-0" variants={item}>
|
||||
|
||||
@@ -29,28 +29,25 @@ function CheckoutCancelPage(props: Props) {
|
||||
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<div>
|
||||
<motion.h1
|
||||
className="display-5 fw-bold lh-1 mb-4"
|
||||
variants={item}
|
||||
>
|
||||
<motion.h1 className=" fw-bold lh-1 mb-4" variants={item}>
|
||||
Oh no!
|
||||
</motion.h1>
|
||||
</div>
|
||||
<div>
|
||||
<motion.p className="lead" variants={item}>
|
||||
It's okay if you don't want to support FakeYou right now, but we'd
|
||||
appreciate it if you reconsider us in the future.
|
||||
It's okay if you don't want to support FakeYou right now, but
|
||||
we'd appreciate it if you reconsider us in the future.
|
||||
</motion.p>
|
||||
<br />
|
||||
<motion.p className="lead" variants={item}>
|
||||
Paid plans go directly to helping us afford more GPUs, engineering,
|
||||
and research talent.
|
||||
Paid plans go directly to helping us afford more GPUs,
|
||||
engineering, and research talent.
|
||||
</motion.p>
|
||||
<br />
|
||||
<motion.p className="lead" variants={item}>
|
||||
We're trying to build a film and music production system that you
|
||||
can use to make any content you dream up. Please consider supporting
|
||||
us monetarily.
|
||||
We're trying to build a film and music production system that
|
||||
you can use to make any content you dream up. Please consider
|
||||
supporting us monetarily.
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,14 +62,12 @@ function CheckoutCancelPage(props: Props) {
|
||||
<div className="col-lg-6">
|
||||
<div className="d-flex justify-content-center">
|
||||
<Link to="/pricing" className="btn btn-primary fs-6">
|
||||
|
||||
Select a Premium Plan
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -30,20 +29,17 @@ function CheckoutSuccessPage(props: Props) {
|
||||
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<div>
|
||||
<motion.h1
|
||||
className="display-5 fw-bold lh-1 mb-4"
|
||||
variants={item}
|
||||
>
|
||||
<motion.h1 className=" fw-bold lh-1 mb-4" variants={item}>
|
||||
Thank you!
|
||||
</motion.h1>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<motion.p className="lead" variants={item}>
|
||||
We're grateful you're helping FakeYou build the future creative
|
||||
suite. We're building tools for small creators and have an eye
|
||||
on building a full AI-powered Hollywood and music production
|
||||
studio.
|
||||
We're grateful you're helping FakeYou build the future
|
||||
creative suite. We're building tools for small creators and
|
||||
have an eye on building a full AI-powered Hollywood and music
|
||||
production studio.
|
||||
</motion.p>
|
||||
<br />
|
||||
<motion.p className="lead" variants={item}>
|
||||
@@ -65,7 +61,6 @@ function CheckoutSuccessPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,7 @@ function PortalSuccessPage(props: Props) {
|
||||
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
|
||||
<div className="text-center text-lg-start">
|
||||
<div>
|
||||
<motion.h1
|
||||
className="display-5 fw-bold lh-1 mb-4"
|
||||
variants={item}
|
||||
>
|
||||
<motion.h1 className=" fw-bold lh-1 mb-4" variants={item}>
|
||||
Thank You!
|
||||
</motion.h1>
|
||||
</div>
|
||||
@@ -55,7 +52,6 @@ function PortalSuccessPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,8 @@ function PricingPage(props: Props) {
|
||||
|
||||
const beginStripeFlow = async (
|
||||
internal_plan_key: string,
|
||||
analyticsName: string,
|
||||
analyticsName: string
|
||||
): Promise<boolean> => {
|
||||
|
||||
switch (analyticsName) {
|
||||
case "plus":
|
||||
Analytics.premiumSelectPlanPlus();
|
||||
@@ -64,7 +63,6 @@ function PricingPage(props: Props) {
|
||||
history.push(signupUrl);
|
||||
|
||||
return false;
|
||||
|
||||
} else if (props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
|
||||
Analytics.premiumForwardToStripePortal();
|
||||
return await beginStripePortalFlow(); // NB: This redirects the user to Stripe
|
||||
@@ -77,16 +75,19 @@ function PricingPage(props: Props) {
|
||||
usePrefixedDocumentTitle("Premium Deep Fake TTS");
|
||||
|
||||
const environment = FakeYouFrontendEnvironment.getInstance();
|
||||
const planKey = environment.useProductionStripePlans() ? "production" : "development";
|
||||
const planKey = environment.useProductionStripePlans()
|
||||
? "production"
|
||||
: "development";
|
||||
|
||||
const userHasPaidPremium = props.sessionSubscriptionsWrapper.hasPaidFeatures();
|
||||
const userHasPaidPremium =
|
||||
props.sessionSubscriptionsWrapper.hasPaidFeatures();
|
||||
|
||||
let plusButtonText = "Buy Plus";
|
||||
let plusButtonDisabled = false;
|
||||
|
||||
let proButtonText = "Buy Pro";
|
||||
let proButtonDisabled = false;
|
||||
let proBorderCss = "rounded panel p-4 h-100"
|
||||
let proBorderCss = "rounded panel p-4 h-100";
|
||||
|
||||
let eliteButtonText = "Buy Elite";
|
||||
let eliteButtonDisabled = false;
|
||||
@@ -122,10 +123,7 @@ function PricingPage(props: Props) {
|
||||
unsubscribeButton = (
|
||||
<>
|
||||
<div className="container pt-5 pb-3 text-center">
|
||||
<motion.div
|
||||
className="mt-4"
|
||||
variants={item}
|
||||
>
|
||||
<motion.div className="mt-4" variants={item}>
|
||||
<button
|
||||
onClick={() => beginStripeFlow(unsubscribeKey, "unsubscribe")}
|
||||
className="btn btn-link w-100 fs-6"
|
||||
@@ -140,30 +138,28 @@ 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 panel-border";
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-3 text-center">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<div className="container pt-5 pb-4 text-center">
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Pricing
|
||||
</motion.h1>
|
||||
{/* <p className="fs-5">
|
||||
By purchasing FakeYou premium, you help us build more!
|
||||
</p> */}
|
||||
<motion.div
|
||||
className="alert alert-warning mt-4 alert-pricing"
|
||||
className="alert alert-warning mt-4 alert-pricing mb-2"
|
||||
variants={item}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHeart} className="text-red me-3" />
|
||||
By purchasing FakeYou premium, you help us build more!
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="container mt-3 mb-5">
|
||||
<div className="container mb-5">
|
||||
<div className="row gx-3 gy-4">
|
||||
|
||||
|
||||
{/* Starter Tier */}
|
||||
{/*<motion.div className="col-12 col-sm-6 col-lg-3" variants={panel}>
|
||||
<div className="rounded panel p-4 h-100">
|
||||
@@ -172,7 +168,7 @@ function PricingPage(props: Props) {
|
||||
</h2>
|
||||
|
||||
|
||||
<h2 className="display-5 fw-bold text-center my-5">
|
||||
<h2 className=" fw-bold text-center my-5">
|
||||
${FYP.starter.price}
|
||||
<span className="fs-5 opacity-75 fw-normal"> /month</span>
|
||||
</h2>
|
||||
@@ -224,7 +220,9 @@ function PricingPage(props: Props) {
|
||||
<div className="rounded panel p-4 h-100">
|
||||
<h2 className="text-center my-2 fw-bold mb-4">{FYP.plus.tier}</h2>
|
||||
<button
|
||||
onClick={() => beginStripeFlow(FYP.plus.internal_plan_key[planKey], "plus")}
|
||||
onClick={() =>
|
||||
beginStripeFlow(FYP.plus.internal_plan_key[planKey], "plus")
|
||||
}
|
||||
className="btn btn-primary w-100 fs-6"
|
||||
disabled={plusButtonDisabled}
|
||||
>
|
||||
@@ -310,7 +308,7 @@ function PricingPage(props: Props) {
|
||||
})} */}
|
||||
</ul>
|
||||
<hr className="my-4" />
|
||||
<h6 className="text-center fw-normal">
|
||||
<h6 className="text-center fw-normal opacity-50">
|
||||
+ Many more features coming soon!
|
||||
</h6>
|
||||
</div>
|
||||
@@ -321,7 +319,9 @@ function PricingPage(props: Props) {
|
||||
<div className={proBorderCss}>
|
||||
<h2 className="text-center my-2 fw-bold mb-4">{FYP.pro.tier}</h2>
|
||||
<button
|
||||
onClick={() => beginStripeFlow(FYP.pro.internal_plan_key[planKey], "pro")}
|
||||
onClick={() =>
|
||||
beginStripeFlow(FYP.pro.internal_plan_key[planKey], "pro")
|
||||
}
|
||||
className="btn btn-primary w-100 fs-6"
|
||||
disabled={proButtonDisabled}
|
||||
>
|
||||
@@ -433,7 +433,7 @@ function PricingPage(props: Props) {
|
||||
})} */}
|
||||
</ul>
|
||||
<hr className="my-4" />
|
||||
<h6 className="text-center fw-normal">
|
||||
<h6 className="text-center fw-normal opacity-50">
|
||||
+ Many more features coming soon!
|
||||
</h6>
|
||||
</div>
|
||||
@@ -446,7 +446,9 @@ function PricingPage(props: Props) {
|
||||
{FYP.elite.tier}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => beginStripeFlow(FYP.elite.internal_plan_key[planKey], "elite")}
|
||||
onClick={() =>
|
||||
beginStripeFlow(FYP.elite.internal_plan_key[planKey], "elite")
|
||||
}
|
||||
className="btn btn-primary w-100 fs-6"
|
||||
disabled={eliteButtonDisabled}
|
||||
>
|
||||
@@ -570,18 +572,16 @@ function PricingPage(props: Props) {
|
||||
})} */}
|
||||
</ul>
|
||||
<hr className="my-4" />
|
||||
<h6 className="text-center fw-normal">
|
||||
<h6 className="text-center fw-normal opacity-50">
|
||||
+ Many more features coming soon!
|
||||
</h6>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="container mt-3 mb-5">
|
||||
<div className="row gx-3 gy-4">
|
||||
{unsubscribeButton}
|
||||
<div className="container mt-3 mb-5">
|
||||
<div className="row gx-3 gy-4">{unsubscribeButton}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Starter Tier (to show for Latin American countries) */}
|
||||
{/* <div className="w-100 mt-4">
|
||||
|
||||
@@ -111,7 +111,7 @@ function ProfileBanFc(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
Profile & Preferences
|
||||
</motion.h1>
|
||||
<motion.div variants={item}>
|
||||
|
||||
@@ -236,7 +236,7 @@ function ProfileEditFc(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
Profile & Preferences
|
||||
</motion.h1>
|
||||
<motion.div variants={item}>
|
||||
|
||||
@@ -364,6 +364,8 @@ function ProfilePage(this: any, props: Props) {
|
||||
size={150}
|
||||
username={userData.display_name}
|
||||
email_hash={userEmailHash}
|
||||
avatarIndex={userData.default_avatar_index}
|
||||
backgroundIndex={userData.default_avatar_color_index}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-3 text-center d-lg-none">
|
||||
@@ -371,6 +373,8 @@ function ProfilePage(this: any, props: Props) {
|
||||
size={100}
|
||||
username={userData.display_name}
|
||||
email_hash={userEmailHash}
|
||||
avatarIndex={userData.default_avatar_index}
|
||||
backgroundIndex={userData.default_avatar_color_index}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -260,7 +260,7 @@ function TtsEditCategoriesPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Edit Categories
|
||||
</motion.h1>
|
||||
<motion.h4 className="mb-4" variants={item}>
|
||||
|
||||
@@ -148,9 +148,7 @@ function TtsModelDeletePage(props: Props) {
|
||||
let creatorLink = <span />;
|
||||
|
||||
if (!!ttsModel?.creator_display_name) {
|
||||
const creatorUrl = WebUrl.userProfilePage(
|
||||
ttsModel?.creator_display_name
|
||||
);
|
||||
const creatorUrl = WebUrl.userProfilePage(ttsModel?.creator_display_name);
|
||||
creatorLink = <Link to={creatorUrl}>{ttsModel?.creator_display_name}</Link>;
|
||||
}
|
||||
|
||||
@@ -218,7 +216,7 @@ function TtsModelDeletePage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
{h1Title}
|
||||
</motion.h1>
|
||||
<motion.p variants={item}>
|
||||
|
||||
@@ -79,10 +79,14 @@ function TtsModelEditPage(props: Props) {
|
||||
// Moderator-only fields (both observable and editable)
|
||||
const [useDefaultMFactor, setUseDefaultMFactor] = useState(false);
|
||||
// NB: The field is an f64, but string is easy to edit
|
||||
const [maybeCustomMFactor, setMaybeCustomMFactor] = useState<string| null>(null);
|
||||
const [maybeCustomMFactor, setMaybeCustomMFactor] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// Vocoder config (modern)
|
||||
const [maybeCustomVocoderToken, setMaybeCustomVocoderToken] = useState<string | null>(null);
|
||||
const [maybeCustomVocoderToken, setMaybeCustomVocoderToken] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Vocoder config (legacy)
|
||||
const [defaultPretrainedVocoder, setDefaultPretrainedVocoder] = useState(
|
||||
@@ -120,12 +124,12 @@ function TtsModelEditPage(props: Props) {
|
||||
model.maybe_moderator_fields?.use_default_m_factor || false
|
||||
);
|
||||
|
||||
let maybeCustomMFactor = (!!model.maybe_moderator_fields?.maybe_custom_m_factor) ?
|
||||
model.maybe_moderator_fields?.maybe_custom_m_factor.toString() :
|
||||
null;
|
||||
let maybeCustomMFactor = !!model.maybe_moderator_fields
|
||||
?.maybe_custom_m_factor
|
||||
? model.maybe_moderator_fields?.maybe_custom_m_factor.toString()
|
||||
: null;
|
||||
|
||||
setMaybeCustomMFactor(maybeCustomMFactor);
|
||||
|
||||
} else if (GetTtsModelIsErr(model)) {
|
||||
switch (model) {
|
||||
case TtsModelLookupError.NotFound:
|
||||
@@ -135,7 +139,6 @@ function TtsModelEditPage(props: Props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const listVocoderModels = useCallback(async () => {
|
||||
const models = await ListVocoderModels();
|
||||
|
||||
@@ -144,7 +147,6 @@ function TtsModelEditPage(props: Props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getModel(token);
|
||||
listVocoderModels();
|
||||
@@ -192,7 +194,6 @@ function TtsModelEditPage(props: Props) {
|
||||
setMaybeCustomVocoderToken((ev.target as HTMLSelectElement).value);
|
||||
};
|
||||
|
||||
|
||||
const handleIsFrontPageFeaturedChange = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
) => {
|
||||
@@ -213,12 +214,16 @@ function TtsModelEditPage(props: Props) {
|
||||
setSuggestedUniqueBotCommand(command);
|
||||
};
|
||||
|
||||
const handleUseDefaultMFactorChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const handleUseDefaultMFactorChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setUseDefaultMFactor(value);
|
||||
};
|
||||
|
||||
const handleMaybeCustomMFactorChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
|
||||
const handleMaybeCustomMFactorChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
ev.preventDefault();
|
||||
const textValue = (ev.target as HTMLInputElement).value.trim();
|
||||
setMaybeCustomMFactor(textValue);
|
||||
@@ -255,7 +260,10 @@ function TtsModelEditPage(props: Props) {
|
||||
text_pipeline_type: textPipelineType,
|
||||
};
|
||||
|
||||
if (!!maybeCustomVocoderToken && maybeCustomVocoderToken !== UNSET_CUSTOM_VOCODER_SENTINEL) {
|
||||
if (
|
||||
!!maybeCustomVocoderToken &&
|
||||
maybeCustomVocoderToken !== UNSET_CUSTOM_VOCODER_SENTINEL
|
||||
) {
|
||||
request.maybe_custom_vocoder_token = maybeCustomVocoderToken;
|
||||
}
|
||||
|
||||
@@ -266,7 +274,8 @@ function TtsModelEditPage(props: Props) {
|
||||
request.maybe_suggested_unique_bot_command = suggestedUniqueBotCommand;
|
||||
}
|
||||
request.use_default_m_factor = useDefaultMFactor;
|
||||
request.maybe_custom_m_factor = maybeCustomMFactor === null ? null : Number(maybeCustomMFactor.trim());
|
||||
request.maybe_custom_m_factor =
|
||||
maybeCustomMFactor === null ? null : Number(maybeCustomMFactor.trim());
|
||||
}
|
||||
|
||||
fetch(endpointUrl, {
|
||||
@@ -385,33 +394,29 @@ function TtsModelEditPage(props: Props) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Use m-factoring?
|
||||
</label>
|
||||
<label className="sub-title">Use m-factoring?</label>
|
||||
<div>
|
||||
<div className="form-group input-icon">
|
||||
|
||||
<div className="form-check form-switch">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onChange={handleUseDefaultMFactorChange}
|
||||
checked={useDefaultMFactor}
|
||||
/>
|
||||
/>
|
||||
<label className="form-check-label">
|
||||
Enable this for voices that sound "tinny" or "metalic".
|
||||
{" "}
|
||||
This will use a globally defined suggested value (that may change).
|
||||
Enable this for voices that sound "tinny" or "metalic". This
|
||||
will use a globally defined suggested value (that may change).
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Use custom m-factor? If set, this will be used instead of the default m-factor.
|
||||
Use custom m-factor? If set, this will be used instead of the
|
||||
default m-factor.
|
||||
</label>
|
||||
<div className="form-group input-icon">
|
||||
<FontAwesomeIcon
|
||||
@@ -427,20 +432,16 @@ function TtsModelEditPage(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let customVocoderOptions = vocoderModels.map((vocoder) => {
|
||||
let customVocoderOptions = vocoderModels.map((vocoder) => {
|
||||
return (
|
||||
<option
|
||||
key={vocoder.vocoder_token}
|
||||
value={vocoder.vocoder_token}
|
||||
>
|
||||
<option key={vocoder.vocoder_token} value={vocoder.vocoder_token}>
|
||||
{vocoder.title} (by {vocoder.creator_display_name})
|
||||
</option>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
let usableTextPipelines = isModerator
|
||||
@@ -468,7 +469,7 @@ function TtsModelEditPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
Edit Model
|
||||
</motion.h1>
|
||||
<motion.p variants={item}>
|
||||
@@ -612,7 +613,9 @@ function TtsModelEditPage(props: Props) {
|
||||
Public (visible from your profile)
|
||||
</option>
|
||||
<option value="hidden">Unlisted (shareable URLs)</option>
|
||||
<option value="private">Private (work in progress)</option>
|
||||
<option value="private">
|
||||
Private (work in progress)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ export function TtsPageHero(props: Props) {
|
||||
//"mascot/superbowl-PE.webp",
|
||||
//"mascot/valentines.webp",
|
||||
// "mascot/st_patricks.webp",
|
||||
//"mascot/april_fools.webp",
|
||||
];
|
||||
|
||||
return images[Math.floor(Math.random() * images.length)];
|
||||
|
||||
@@ -262,15 +262,14 @@ function TtsModelViewPage(props: Props) {
|
||||
if (!!ttsModel?.creator_display_name) {
|
||||
const creatorUrl = WebUrl.userProfilePage(ttsModel?.creator_username);
|
||||
creatorLink = (
|
||||
<span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Gravatar
|
||||
size={15}
|
||||
size={32}
|
||||
username={ttsModel.creator_display_name || ""}
|
||||
email_hash={ttsModel.creator_gravatar_hash || ""}
|
||||
/>
|
||||
|
||||
<Link to={creatorUrl}>{ttsModel.creator_display_name}</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -586,15 +585,16 @@ function TtsModelViewPage(props: Props) {
|
||||
<tr>
|
||||
<th>Vocoder created by</th>
|
||||
<td>
|
||||
<Gravatar
|
||||
size={15}
|
||||
username={ttsModel.maybe_custom_vocoder.creator_display_name}
|
||||
email_hash={ttsModel.maybe_custom_vocoder.creator_gravatar_hash}
|
||||
/>
|
||||
|
||||
<Link to={vocoderCreatorUrl}>
|
||||
{ttsModel.maybe_custom_vocoder.creator_display_name}
|
||||
</Link>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Gravatar
|
||||
size={32}
|
||||
username={ttsModel.maybe_custom_vocoder.creator_display_name}
|
||||
email_hash={ttsModel.maybe_custom_vocoder.creator_gravatar_hash}
|
||||
/>
|
||||
<Link to={vocoderCreatorUrl}>
|
||||
{ttsModel.maybe_custom_vocoder.creator_display_name}
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
@@ -705,7 +705,7 @@ function TtsModelViewPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className="fw-bold mb-3" variants={item}>
|
||||
{title}
|
||||
</motion.h1>
|
||||
{/* Rate Voice Model Buttons */}
|
||||
|
||||
@@ -133,9 +133,7 @@ function TtsResultDeletePage(props: Props) {
|
||||
return <div />; // Exit rendering until data loads.
|
||||
}
|
||||
|
||||
const modelLink = WebUrl.ttsModelPage(
|
||||
ttsInferenceResult.tts_model_token
|
||||
);
|
||||
const modelLink = WebUrl.ttsModelPage(ttsInferenceResult.tts_model_token);
|
||||
|
||||
let durationSeconds = ttsInferenceResult?.duration_millis / 1000;
|
||||
let modelName = ttsInferenceResult.tts_model_title;
|
||||
@@ -189,7 +187,7 @@ function TtsResultDeletePage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
{h1Title}
|
||||
</motion.h1>
|
||||
<motion.div variants={item}>
|
||||
|
||||
@@ -152,7 +152,7 @@ function TtsResultEditPage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold mb-3" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-3" variants={item}>
|
||||
Edit Result Visibility
|
||||
</motion.h1>
|
||||
<motion.div variants={item}>
|
||||
|
||||
@@ -219,17 +219,16 @@ function TtsResultViewPage(props: Props) {
|
||||
if (!!ttsInferenceResult.maybe_creator_user_token) {
|
||||
let creatorLink = `/profile/${ttsInferenceResult.maybe_creator_username}`;
|
||||
creatorDetails = (
|
||||
<span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Gravatar
|
||||
size={15}
|
||||
size={32}
|
||||
username={ttsInferenceResult.maybe_creator_display_name || ""}
|
||||
email_hash={ttsInferenceResult.maybe_creator_gravatar_hash || ""}
|
||||
/>
|
||||
|
||||
<Link to={creatorLink}>
|
||||
{ttsInferenceResult.maybe_creator_display_name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,19 +236,18 @@ function TtsResultViewPage(props: Props) {
|
||||
if (!!ttsInferenceResult.maybe_model_creator_user_token) {
|
||||
let modelCreatorLink = `/profile/${ttsInferenceResult.maybe_model_creator_username}`;
|
||||
modelCreatorDetails = (
|
||||
<span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Gravatar
|
||||
size={15}
|
||||
size={32}
|
||||
username={ttsInferenceResult.maybe_model_creator_display_name || ""}
|
||||
email_hash={
|
||||
ttsInferenceResult.maybe_model_creator_gravatar_hash || ""
|
||||
}
|
||||
/>
|
||||
|
||||
<Link to={modelCreatorLink}>
|
||||
{ttsInferenceResult.maybe_model_creator_display_name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,7 +432,7 @@ function TtsResultViewPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold mb-2" variants={item}>
|
||||
<motion.h1 className=" fw-bold mb-2" variants={item}>
|
||||
{headingTitle}
|
||||
</motion.h1>
|
||||
{/* Rate Result Buttons */}
|
||||
|
||||
@@ -113,7 +113,7 @@ function UploadTtsModelPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Upload Voice (TTS Model)
|
||||
</motion.h1>
|
||||
<motion.h4 variants={item}>This works just like YouTube!</motion.h4>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { DiscordLink } from "@storyteller/components/src/elements/DiscordLink";
|
||||
import { EnqueueRetrieval, EnqueueRetrievalIsOk } from "@storyteller/components/src/api/retrieval/EnqueueRetrieval";
|
||||
import {
|
||||
EnqueueRetrieval,
|
||||
EnqueueRetrievalIsOk,
|
||||
} from "@storyteller/components/src/api/retrieval/EnqueueRetrieval";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { BackLink } from "../../_common/BackLink";
|
||||
@@ -69,7 +72,7 @@ function UploadVocoderPage(props: Props) {
|
||||
idempotency_token: idempotencyToken,
|
||||
title: title,
|
||||
download_url: downloadUrl,
|
||||
generic_download_type: 'hifigan',
|
||||
generic_download_type: "hifigan",
|
||||
};
|
||||
|
||||
const response = await EnqueueRetrieval(request);
|
||||
@@ -86,7 +89,7 @@ function UploadVocoderPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Upload Vocoder Model
|
||||
</motion.h1>
|
||||
<motion.h4 variants={item}>Make your voice sound better!</motion.h4>
|
||||
@@ -113,9 +116,9 @@ function UploadVocoderPage(props: Props) {
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once your vocoder is successfully uploaded, you'll be able to
|
||||
add it to your voice models. Others will be able to use it too, and
|
||||
you'll get credited.
|
||||
Once your vocoder is successfully uploaded, you'll be able to add it
|
||||
to your voice models. Others will be able to use it too, and you'll
|
||||
get credited.
|
||||
</p>
|
||||
|
||||
{/* TODO TEMP (2022-03-08) <p>
|
||||
@@ -130,7 +133,8 @@ function UploadVocoderPage(props: Props) {
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Vocoder Title, eg. "High Pitched Voices (trained on Powerpuff Girls, MLP, and various anime characters)"
|
||||
Vocoder Title, eg. "High Pitched Voices (trained on Powerpuff
|
||||
Girls, MLP, and various anime characters)"
|
||||
</label>
|
||||
<div className="form-group">
|
||||
<input
|
||||
@@ -163,7 +167,6 @@ function UploadVocoderPage(props: Props) {
|
||||
<p className="help">{downloadUrlInvalidReason}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ function UploadW2lPhotoPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-3 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Upload Photo (w2l template)
|
||||
</motion.h1>
|
||||
<motion.div className="my-3" variants={item}>
|
||||
|
||||
@@ -94,7 +94,7 @@ function UploadW2lVideoPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-3 px-md-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Upload Video (w2l template)
|
||||
</motion.h1>
|
||||
<motion.div className="my-3" variants={item}>
|
||||
|
||||
@@ -106,7 +106,7 @@ function W2lResultDeletePage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-md-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
{h1Title}
|
||||
</motion.h1>
|
||||
<motion.div className="pt-3" variants={item}>
|
||||
|
||||
@@ -148,7 +148,7 @@ function W2lResultEditPage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pb-4 pt-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Edit Result Visibility
|
||||
</motion.h1>
|
||||
<motion.div className="pt-3" variants={item}>
|
||||
|
||||
@@ -264,7 +264,7 @@ function W2lResultViewPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<motion.h1
|
||||
className="display-5 fw-bold text-center text-lg-start"
|
||||
className=" fw-bold text-center text-lg-start"
|
||||
variants={item}
|
||||
>
|
||||
Lipsync Result
|
||||
|
||||
@@ -121,7 +121,7 @@ function W2lTemplateApprovePage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
{h1Title}
|
||||
</motion.h1>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +172,7 @@ function W2lTemplateDeletePage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
{h1Title}
|
||||
</motion.h1>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ function W2lTemplateEditPage(props: Props) {
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container pt-5 pb-4 px-lg-5 px-xl-3">
|
||||
<div className="d-flex flex-column">
|
||||
<h1 className="display-5 fw-bold">Edit Template</h1>
|
||||
<h1 className=" fw-bold">Edit Template</h1>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<BackLink link={templateLink} text="Back to template" />
|
||||
|
||||
@@ -412,7 +412,7 @@ function W2lTemplateViewPage(props: Props) {
|
||||
return (
|
||||
<motion.div initial="hidden" animate="visible" variants={container}>
|
||||
<div className="container py-5 px-md-4 px-lg-5 px-xl-3">
|
||||
<motion.h1 className="display-5 fw-bold" variants={item}>
|
||||
<motion.h1 className=" fw-bold" variants={item}>
|
||||
Video Lip Sync Template
|
||||
</motion.h1>
|
||||
<motion.h4 variants={item}>Template: {w2lTemplate?.title}</motion.h4>
|
||||
|
||||
@@ -92,7 +92,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading text-center fw-bold pt-2">Features</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3">
|
||||
<h1 className=" fw-bold mt-3">
|
||||
<span className="word">TTS Features</span>
|
||||
</h1>
|
||||
<div className="features-section">
|
||||
@@ -166,7 +166,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading text-center fw-bold pt-2">TTS Voices</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3">
|
||||
<h1 className=" fw-bold mt-3">
|
||||
<span className="word">Voice Previews</span>
|
||||
</h1>
|
||||
<div className="voices-section">
|
||||
@@ -206,7 +206,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading fw-bold pt-2">Statistics</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mb-4 mt-3">
|
||||
<h1 className=" fw-bold mb-4 mt-3">
|
||||
<span className="word">Our Insights</span>
|
||||
</h1>
|
||||
<p>
|
||||
@@ -249,10 +249,10 @@ function IndexPage(props: Props) {
|
||||
</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3 text-center d-none d-lg-block">
|
||||
<h1 className=" fw-bold mt-3 text-center d-none d-lg-block">
|
||||
<span className="word">Frequently Asked Questions</span>
|
||||
</h1>
|
||||
<h1 className="display-5 fw-bold mt-3 text-center d-lg-none">
|
||||
<h1 className=" fw-bold mt-3 text-center d-lg-none">
|
||||
<span className="word">FAQ</span>
|
||||
</h1>
|
||||
|
||||
@@ -362,7 +362,7 @@ function IndexPage(props: Props) {
|
||||
</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mb-4 mt-3">
|
||||
<h1 className=" fw-bold mb-4 mt-3">
|
||||
<span className="word">Join Our Community</span>
|
||||
</h1>
|
||||
<p className="lead text-center mb-5">
|
||||
@@ -388,7 +388,7 @@ function IndexPage(props: Props) {
|
||||
<img className="cta-img w-100" src="assets/cta-img.png" alt="" />
|
||||
</div>
|
||||
<div className="col-md-6 cta-right text-center text-sm-start px-5">
|
||||
<h1 className="display-5 fw-bold mb-5">
|
||||
<h1 className=" fw-bold mb-5">
|
||||
Get started with Storyteller Stream!
|
||||
</h1>
|
||||
<div className="d-flex gap-3">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GitSha } from "@storyteller/components/src/elements/GitSha";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -26,7 +25,7 @@ function TopNav(props: Props) {
|
||||
aria-label="Offcanvas navbar large"
|
||||
>
|
||||
<div className="container">
|
||||
<a className="navbar-brand me-5 pr-8" href="#">
|
||||
<a className="navbar-brand me-5 pr-8" href="#/">
|
||||
<img
|
||||
src="/assets/powerstream-logo.png"
|
||||
alt="PowerStream Logo"
|
||||
@@ -48,7 +47,7 @@ function TopNav(props: Props) {
|
||||
aria-labelledby="offcanvasNavbar2Label"
|
||||
>
|
||||
<div className="offcanvas-header" data-bs-toggle="offcanvas">
|
||||
<a className="navbar-brand me-5 pr-8" href="#">
|
||||
<a className="navbar-brand me-5 pr-8" href="#/">
|
||||
<img
|
||||
src="/assets/powerstream-logo.png"
|
||||
alt="PowerStream Logo"
|
||||
|
||||
@@ -95,7 +95,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading text-center fw-bold pt-2">Features</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3">
|
||||
<h1 className=" fw-bold mt-3">
|
||||
<span className="word">TTS Features</span>
|
||||
</h1>
|
||||
<div className="features-section">
|
||||
@@ -175,7 +175,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading text-center fw-bold pt-2">TTS Voices</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3">
|
||||
<h1 className=" fw-bold mt-3">
|
||||
<span className="word">Voice Previews</span>
|
||||
</h1>
|
||||
<div className="voices-section">
|
||||
@@ -215,7 +215,7 @@ function IndexPage(props: Props) {
|
||||
<h6 className="pre-heading fw-bold pt-2">It's Obvious</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mb-4 mt-3">
|
||||
<h1 className=" fw-bold mb-4 mt-3">
|
||||
<span className="word">Better Streams Grow</span>
|
||||
</h1>
|
||||
<p>
|
||||
@@ -258,10 +258,10 @@ function IndexPage(props: Props) {
|
||||
</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mt-3 text-center d-none d-lg-block">
|
||||
<h1 className=" fw-bold mt-3 text-center d-none d-lg-block">
|
||||
<span className="word">Frequently Asked Questions</span>
|
||||
</h1>
|
||||
<h1 className="display-5 fw-bold mt-3 text-center d-lg-none">
|
||||
<h1 className=" fw-bold mt-3 text-center d-lg-none">
|
||||
<span className="word">FAQ</span>
|
||||
</h1>
|
||||
|
||||
@@ -362,7 +362,7 @@ function IndexPage(props: Props) {
|
||||
</h6>
|
||||
<img src="assets/title-shape.png" alt="" />
|
||||
</div>
|
||||
<h1 className="display-5 fw-bold mb-4 mt-3">
|
||||
<h1 className=" fw-bold mb-4 mt-3">
|
||||
<span className="word">Join Our Community</span>
|
||||
</h1>
|
||||
<p className="lead text-center mb-5">
|
||||
@@ -404,9 +404,7 @@ function IndexPage(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6 cta-right text-center text-sm-start px-4 px-lg-5">
|
||||
<h1 className="display-5 fw-bold mb-5">
|
||||
Get started with Power Stream!
|
||||
</h1>
|
||||
<h1 className=" fw-bold mb-5">Get started with Power Stream!</h1>
|
||||
<div className="d-flex flex-column flex-sm-row gap-3">
|
||||
<a
|
||||
className="btn btn-cta w-100"
|
||||
|
||||
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -230,7 +230,7 @@ function IndexPage() {
|
||||
</div>
|
||||
<div className="container mt-5 pt-5">
|
||||
<div className="row gx-5 flex-row-reverse gy-4">
|
||||
<div className="col-lg-6 img-layers">
|
||||
<div className="col-lg-6 img-layers d-none d-xl-block">
|
||||
<Tilt trackOnWindow={true} tiltReverse={true}>
|
||||
<img
|
||||
src="/images/screenshots/fakeyou-screen.webp"
|
||||
@@ -244,8 +244,16 @@ function IndexPage() {
|
||||
className="img-front-right img-mascot"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6 d-xl-none">
|
||||
<img
|
||||
src="/images/screenshots/fakeyou-screen-mobile.webp"
|
||||
alt="FakeYou"
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-6 d-flex flex-column justify-content-center">
|
||||
<h1 className="fw-bold display-5">FakeYou</h1>
|
||||
<h1 className="fw-bold ">FakeYou</h1>
|
||||
<h4 className="fw-normal opacity-75 mb-4">
|
||||
Used by millions of people every month
|
||||
</h4>
|
||||
@@ -275,7 +283,7 @@ function IndexPage() {
|
||||
<div className="bg-dark section-2">
|
||||
<div className="container">
|
||||
<div className="row gx-5 gy-5">
|
||||
<div className="col-lg-6 img-layers">
|
||||
<div className="col-lg-6 img-layers d-none d-xl-block">
|
||||
<Tilt trackOnWindow={true} tiltReverse={true}>
|
||||
<img
|
||||
src="/images/screenshots/powerstream-screen.webp"
|
||||
@@ -289,8 +297,16 @@ function IndexPage() {
|
||||
className="img-front-left img-mascot"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6 d-xl-none">
|
||||
<img
|
||||
src="/images/screenshots/powerstream-screen-mobile.webp"
|
||||
alt="FakeYou"
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-6 d-flex flex-column justify-content-center">
|
||||
<h1 className="fw-bold display-5">PowerStream</h1>
|
||||
<h1 className="fw-bold ">PowerStream</h1>
|
||||
<h4 className="fw-normal opacity-75 mb-4">
|
||||
Twitch Streamers and Creators can engage and monetize
|
||||
</h4>
|
||||
@@ -720,27 +736,18 @@ function IndexPage() {
|
||||
</Marquee>
|
||||
</div>
|
||||
|
||||
<div className="container py-5 mt-5">
|
||||
<div className="row g-4 g-md-5 align-items-center">
|
||||
<div className="col-12 col-md-6">
|
||||
<div>
|
||||
<h1 className="fw-bold display-6 about-title">
|
||||
We're an interdiciplinary team building the one-person cloud
|
||||
studio
|
||||
</h1>
|
||||
<hr className="p-1 mt-4 text-red opacity-100 w-25" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="fw-normal opacity-75">
|
||||
Our dream is to empower anyone to create full feature-length
|
||||
content without institutional capital, large teams, huge amounts
|
||||
of time, or a deep reservoir of highly specialized talent. We
|
||||
want to empower everyone to turn their dreams into physical
|
||||
form.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container py-5 text-center d-flex flex-column align-items-center mt-3">
|
||||
<h1 className="fw-bold about-title mt-5">
|
||||
We’re building the future production cloud studio
|
||||
</h1>
|
||||
<p className="fw-normal opacity-75 mt-4">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec quis
|
||||
vehicula odio. Nulla non sem scelerisque, hendrerit libero sagittis,
|
||||
rhoncus orci. Etiam in molestie augue. Donec lobortis tincidunt
|
||||
massa, a posuere libero sagittis eget. Vivamus sagittis mauris non
|
||||
tristique ornare. Pellentesque rhoncus ipsum magna, nec lacinia odio
|
||||
sagittis et. Nunc aliquet cursus dui fermentum molestie.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="container mt-md-5">
|
||||
|
||||
@@ -16,12 +16,12 @@ function Footer(props: Props) {
|
||||
</p>*/}
|
||||
<h2>Want more Donald Trump TTS?</h2>
|
||||
<p>
|
||||
The best <a href="https://fakeyou.com">Donald Trump TTS</a> can be found
|
||||
The best <a href="https://fakeyou.com/character/donald-trump">Donald Trump TTS</a> can be found
|
||||
at FakeYou.
|
||||
</p>
|
||||
<p>
|
||||
FakeYou is the world's best Text to Speech website and has thousands of
|
||||
voices. Angry Donald J Trump, Sad DJ Trump, Hillary Clinton, Nixon, and
|
||||
voices. Angry <a href="https://fakeyou.com/character/donald-trump">Donald J Trump AI Voice</a>, Sad DJ Trump, Hillary Clinton, Nixon, and
|
||||
so many others!
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -128,9 +128,7 @@
|
||||
</div>
|
||||
<div class="col-lg-6 px-md-2 px-lg-5 px-xl-2">
|
||||
<div class="ani1 load-hidden text-center text-lg-start">
|
||||
<h1 class="display-5 fw-bold lh-1 mb-4">
|
||||
A Glimpse of the Future
|
||||
</h1>
|
||||
<h1 class="fw-bold lh-1 mb-4">A Glimpse of the Future</h1>
|
||||
<p class="lead">
|
||||
We're building FakeYou as just one component of a broad set of
|
||||
production and creative tooling.
|
||||
|
||||
@@ -128,9 +128,7 @@
|
||||
</div>
|
||||
<div class="col-lg-6 px-md-2 px-lg-5 px-xl-2">
|
||||
<div class="ani1 load-hidden text-center text-lg-start">
|
||||
<h1 class="display-5 fw-bold lh-1">
|
||||
Professionally Clone Your Voice
|
||||
</h1>
|
||||
<h1 class="fw-bold lh-1">Professionally Clone Your Voice</h1>
|
||||
<h3 class="mb-4">(or any voice) for just $70</h3>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<h3>Want a Custom Voice You Can Use?</h3>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="d-flex flex-column">
|
||||
<h1 class="display-5 fw-bold">Contribute to FakeYou!</h1>
|
||||
<h1 class="fw-bold">Contribute to FakeYou!</h1>
|
||||
<h3 class="mb-4">You make FakeYou better by contributing</h3>
|
||||
<p class="lead">
|
||||
You'll get credited for everything you contribute. You'll also get
|
||||
|
||||