mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 05:12:50 +00:00
trump page
This commit is contained in:
1 parent
ff9dc8f49e
commit
7c8cfab5f4
4 files changed
+586
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 575 KiB |
@@ -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/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="/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}
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
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";
|
||||
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);
|
||||
|
||||
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)];
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
usePrefixedDocumentTitle(undefined); // NB: Sets to default title
|
||||
|
||||
// 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 */}
|
||||
|
||||
{/*
|
||||
|
||||
|
||||
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,
|
||||
faWaveformLines,
|
||||
} 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.png"];
|
||||
|
||||
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={faWaveformLines} className="me-3" />;
|
||||
const title = <>Donald Trump TTS</>;
|
||||
const subText = (
|
||||
<>Type your text below and hear it in the voice of Donald Trump.</>
|
||||
);
|
||||
const actionButtons = (
|
||||
<>
|
||||
{signUpButton}
|
||||
{viewPricingButton}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeaderWithImage
|
||||
headerImage={randomImage}
|
||||
titleIcon={titleIcon}
|
||||
title={title}
|
||||
subText={subText}
|
||||
showButtons={true}
|
||||
actionButtons={actionButtons}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user