mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 09:48:44 +00:00
Merge branch 'master' into bflat-fy-translation
This commit is contained in:
9 files changed
+680
-13
No files matched your search
@@ -10,6 +10,7 @@
|
||||
/coming-soon* /index.html 200
|
||||
/commissions* /index.html 200
|
||||
/contribute* /index.html 200
|
||||
/face-animation* /index.html 200
|
||||
/firehose* /index.html 200
|
||||
/guide* /index.html 200
|
||||
/jobs* /index.html 200
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface InferenceInfo {
|
||||
export interface ByQueueStats {
|
||||
pending_svc_jobs: number,
|
||||
pending_rvc_jobs: number,
|
||||
pending_face_animation_jobs: number,
|
||||
}
|
||||
|
||||
export interface LegacyTtsInfo {
|
||||
|
||||
@@ -689,6 +689,18 @@ styles with important tags:
|
||||
}
|
||||
}
|
||||
|
||||
.badge-new {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
opacity: 0.85;
|
||||
max-height: 18px;
|
||||
background-color: #fff3d0;
|
||||
color: #833c00;
|
||||
}
|
||||
|
||||
.rounded-circle {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ import { VoiceConversionModelListItem } from "@storyteller/components/src/api/vo
|
||||
import { CommunityCommissionsPage } from "./pages/contest/CommunityCommissionsPage";
|
||||
import { ProductUsageInfoPage } from "./pages/product_usage_info/ProductUsageInfoPage";
|
||||
import { TestingPage } from "./pages/testing/TestingPage";
|
||||
import { FaceAnimationPage } from "./pages/face-animation/FaceAnimationPage";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -542,6 +543,17 @@ class PageContainer extends React.Component<Props, State> {
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/face-animation">
|
||||
<FaceAnimationPage
|
||||
sessionSubscriptionsWrapper={
|
||||
this.props.sessionSubscriptionsWrapper
|
||||
}
|
||||
enqueueInferenceJob={this.props.enqueueInferenceJob}
|
||||
inferenceJobs={this.props.inferenceJobs}
|
||||
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/commissions">
|
||||
<CommunityCommissionsPage />
|
||||
</Route>
|
||||
|
||||
@@ -57,7 +57,8 @@ function FooterNav(props: Props) {
|
||||
by_queue: {
|
||||
pending_svc_jobs: 0,
|
||||
pending_rvc_jobs: 0,
|
||||
},
|
||||
pending_face_animation_jobs: 0,
|
||||
}
|
||||
},
|
||||
legacy_tts: {
|
||||
pending_job_count: 0,
|
||||
@@ -145,6 +146,12 @@ function FooterNav(props: Props) {
|
||||
{queueStats.inference.by_queue.pending_svc_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Animations Queued:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_face_animation_jobs}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container py-5">
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import {
|
||||
EnqueueFaceAnimation,
|
||||
EnqueueFaceAnimationIsSuccess,
|
||||
EnqueueFaceAnimationRequest,
|
||||
} from "@storyteller/components/src/api/face_animation/EnqueueFaceAnimation";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import FaceAnimationUploadComponent from "./FaceAnimationUploadComponent";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBarsStaggered } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FaceAnimationPageResultList } from "./FaceAnimationPageResultList";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
|
||||
interface Props {
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
enqueueInferenceJob: (
|
||||
jobToken: string,
|
||||
frontendInferenceJobType: FrontendInferenceJobType
|
||||
) => void;
|
||||
inferenceJobs: Array<InferenceJob>;
|
||||
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
|
||||
}
|
||||
|
||||
function FaceAnimationPage(props: Props) {
|
||||
PosthogClient.recordPageview();
|
||||
usePrefixedDocumentTitle("Face Animation");
|
||||
|
||||
const [audioToken, setAudioToken] = useState<string|undefined>(undefined);
|
||||
const [imageToken, setImageToken] = useState<string|undefined>(undefined);
|
||||
|
||||
const doTest = async () => {
|
||||
let request: EnqueueFaceAnimationRequest = {
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
audio_source: {
|
||||
//maybe_voice_conversion_result_token: "todo",
|
||||
//maybe_media_file_token: "audio_media_token",
|
||||
maybe_media_upload_token: audioToken,
|
||||
},
|
||||
image_source: {
|
||||
//maybe_media_file_token: "image_media_token",
|
||||
maybe_media_upload_token: imageToken,
|
||||
},
|
||||
};
|
||||
|
||||
let result = await EnqueueFaceAnimation(request);
|
||||
|
||||
if (EnqueueFaceAnimationIsSuccess(result)) {
|
||||
props.enqueueInferenceJob(
|
||||
result.inference_job_token,
|
||||
FrontendInferenceJobType.FaceAnimation
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const generateButtonDisabled = !!!audioToken || !!!imageToken;
|
||||
const generateButtonClass = generateButtonDisabled
|
||||
? "btn btn-uploaded w-100 disabled"
|
||||
: "btn btn-primary w-100";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container-panel pt-4 pb-5">
|
||||
<h1>Face Animation (Alpha)</h1>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2>Audio</h2>
|
||||
<FaceAnimationUploadComponent
|
||||
uploadTypeLabel={"Audio"}
|
||||
uploadTypesAllowed={["MP3", "WAV", "FLAC", "OGG"]}
|
||||
setMediaUploadToken={(token) => { setAudioToken(token) }}
|
||||
formIsCleared={false}
|
||||
setFormIsCleared={() => {}}
|
||||
setCanConvert={() => {}}
|
||||
changeConvertIdempotencyToken={() => {}}
|
||||
/>
|
||||
|
||||
{audioToken ? (
|
||||
<>
|
||||
<br />
|
||||
<div>Audio upload token: {audioToken}</div>
|
||||
</>
|
||||
) : (<></>)}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2>Image</h2>
|
||||
<FaceAnimationUploadComponent
|
||||
uploadTypeLabel={"Image"}
|
||||
uploadTypesAllowed={["JPG", "PNG"]}
|
||||
setMediaUploadToken={(token) => { setImageToken(token) }}
|
||||
formIsCleared={false}
|
||||
setFormIsCleared={() => {}}
|
||||
setCanConvert={() => {}}
|
||||
changeConvertIdempotencyToken={() => {}}
|
||||
/>
|
||||
|
||||
{imageToken ? (
|
||||
<>
|
||||
<br />
|
||||
<div>Image upload token: {imageToken}</div>
|
||||
</>
|
||||
) : (<></>)}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2>Generate Result </h2>
|
||||
<button
|
||||
disabled={generateButtonDisabled}
|
||||
className={generateButtonClass}
|
||||
onClick={() => doTest()}>
|
||||
Generate Video
|
||||
</button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h4 className="text-center text-lg-start">
|
||||
<FontAwesomeIcon
|
||||
icon={faBarsStaggered}
|
||||
className="me-3"
|
||||
/>
|
||||
Session Results
|
||||
</h4>
|
||||
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
|
||||
<FaceAnimationPageResultList
|
||||
inferenceJobs={
|
||||
props.inferenceJobsByCategory.get(
|
||||
FrontendInferenceJobType.FaceAnimation
|
||||
)!
|
||||
}
|
||||
sessionSubscriptionsWrapper={
|
||||
props.sessionSubscriptionsWrapper
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { FaceAnimationPage };
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { t } from "i18next";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { JobState } from "@storyteller/components/src/jobs/JobStates";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faHeadphonesSimple,
|
||||
faDownload,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { Analytics } from "../../../../common/Analytics";
|
||||
import {
|
||||
GetPendingTtsJobCount,
|
||||
GetPendingTtsJobCountIsOk,
|
||||
GetPendingTtsJobCountSuccessResponse,
|
||||
} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount";
|
||||
import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
|
||||
interface Props {
|
||||
inferenceJobs: Array<InferenceJob>;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
}
|
||||
|
||||
// TODO: This is duplicated in SessionTtsInferenceResultsList !
|
||||
// Default to querying every 15 seconds, but make it configurable serverside
|
||||
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
|
||||
|
||||
function FaceAnimationPageResultList(props: Props) {
|
||||
const [pendingTtsJobs, setPendingTtsJobs] =
|
||||
useState<GetPendingTtsJobCountSuccessResponse>({
|
||||
success: true,
|
||||
pending_job_count: 0,
|
||||
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
|
||||
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
const response = await GetPendingTtsJobCount();
|
||||
if (GetPendingTtsJobCountIsOk(response)) {
|
||||
if (
|
||||
response.cache_time.getTime() > pendingTtsJobs.cache_time.getTime()
|
||||
) {
|
||||
setPendingTtsJobs(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
// TODO: We're having an outage and need to lower this.
|
||||
//const interval = setInterval(async () => fetch(), 15000);
|
||||
const refreshInterval = Math.max(
|
||||
DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
|
||||
pendingTtsJobs.refresh_interval_millis
|
||||
);
|
||||
const interval = setInterval(async () => fetch(), refreshInterval);
|
||||
fetch();
|
||||
return () => clearInterval(interval);
|
||||
}, [pendingTtsJobs]);
|
||||
|
||||
let results: Array<JSX.Element> = [];
|
||||
|
||||
// TODO(bt,2023-04-08): Clean this utter garbage duplication up.
|
||||
|
||||
// ============================= GENERIC INFERENCE =============================
|
||||
|
||||
props.inferenceJobs.forEach((job) => {
|
||||
if (!job.maybeResultToken) {
|
||||
let cssStyle = "alert alert-secondary mb-0";
|
||||
let stateDescription = "Pending...";
|
||||
|
||||
switch (job.jobState) {
|
||||
case JobState.PENDING:
|
||||
case JobState.UNKNOWN:
|
||||
stateDescription =
|
||||
job.maybeExtraStatusDescription == null
|
||||
? t("common.SessionTtsInferenceResults.progress.pending")
|
||||
: job.maybeExtraStatusDescription;
|
||||
break;
|
||||
case JobState.STARTED:
|
||||
cssStyle = "alert alert-success mb-0";
|
||||
stateDescription =
|
||||
job.maybeExtraStatusDescription == null
|
||||
? t("common.SessionTtsInferenceResults.progress.started")
|
||||
: job.maybeExtraStatusDescription;
|
||||
break;
|
||||
case JobState.ATTEMPT_FAILED:
|
||||
cssStyle = "alert alert-danger mb-0";
|
||||
stateDescription = `Failed ${job.attemptCount} attempt(s). Will retry...`;
|
||||
break;
|
||||
case JobState.COMPLETE_FAILURE:
|
||||
case JobState.DEAD:
|
||||
cssStyle = "alert alert-danger mb-0";
|
||||
// TODO(bt,2023-01-23): Translate when I can test it
|
||||
stateDescription = t(
|
||||
"common.SessionTtsInferenceResults.progress.dead"
|
||||
);
|
||||
break;
|
||||
case JobState.COMPLETE_SUCCESS:
|
||||
cssStyle = "message is-success mb-0";
|
||||
// Not sure why we're here instead of other branch!
|
||||
stateDescription = t(
|
||||
"common.SessionTtsInferenceResults.progress.success"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(
|
||||
<div key={job.jobToken}>
|
||||
<div>
|
||||
<div>
|
||||
<div className={cssStyle}>{stateDescription}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let audioLink = new BucketConfig().getGcsUrl(
|
||||
job.maybeResultPublicBucketMediaPath
|
||||
);
|
||||
//let ttsPermalink = `/tts/result/${job.maybeResultToken}`;
|
||||
|
||||
//let wavesurfers = <SessionTtsAudioPlayer filename={audioLink} />;
|
||||
let wavesurfers : string[] = [];
|
||||
|
||||
let audioDownloadFilename = `fakeyou-${job.jobToken}.wav`;
|
||||
|
||||
results.push(
|
||||
<div key={job.jobToken}>
|
||||
{/*<div className="message-header">
|
||||
<p>{job.title}</p>
|
||||
<button className="delete" aria-label="delete"></button>
|
||||
</div>*/}
|
||||
<div>
|
||||
<div className="panel panel-tts-results p-4 gap-3 d-flex flex-column">
|
||||
<div>
|
||||
<h5 className="mb-2">{job.maybeModelTitle}</h5>
|
||||
<p>{job.maybeRawInferenceText}</p>
|
||||
</div>
|
||||
|
||||
{/* <audio
|
||||
className="w-100"
|
||||
controls
|
||||
src={audioLink}
|
||||
onClick={() => {
|
||||
Analytics.ttsClickResultInlinePlay();
|
||||
}}
|
||||
>
|
||||
Your browser does not support the
|
||||
<code>audio</code> element.
|
||||
</audio> */}
|
||||
|
||||
{wavesurfers}
|
||||
|
||||
<div className="mt-2">
|
||||
{/*<Link
|
||||
to={ttsPermalink}
|
||||
onClick={() => {
|
||||
Analytics.ttsClickResultLink();
|
||||
}}
|
||||
className="fw-semibold"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="me-2" />
|
||||
{t("common.SessionTtsInferenceResults.result.shareDownload")}
|
||||
</Link>*/}
|
||||
|
||||
<a
|
||||
className=" btn btn-primary w-100 mt-4"
|
||||
href={audioLink}
|
||||
onClick={() => {
|
||||
Analytics.voiceConversionClickDownload();
|
||||
}}
|
||||
download={audioDownloadFilename}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} className="me-2" />
|
||||
Download File{" "}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let noResultsSection = (
|
||||
<div className="panel panel-inner text-center p-5 rounded-5 h-100">
|
||||
<div className="d-flex flex-column opacity-75 h-100 justify-content-center">
|
||||
<FontAwesomeIcon icon={faHeadphonesSimple} className="fs-3 mb-3" />
|
||||
<h5 className="fw-semibold">
|
||||
{t("common.SessionTtsInferenceResults.noResults.title")}
|
||||
</h5>
|
||||
<p>{t("common.SessionTtsInferenceResults.noResults.subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (results.length === 0) {
|
||||
return <>{noResultsSection}</>;
|
||||
}
|
||||
|
||||
// Users have requested reverse chronological results
|
||||
results.reverse();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div className="d-flex flex-column gap-3">{results}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { FaceAnimationPageResultList };
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
faCheck,
|
||||
faFileArrowUp,
|
||||
faFileAudio,
|
||||
faTrash,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useState } from "react";
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
UploadAudio,
|
||||
UploadAudioIsOk,
|
||||
UploadAudioRequest,
|
||||
} from "@storyteller/components/src/api/upload/UploadAudio";
|
||||
import {
|
||||
UploadImage,
|
||||
UploadImageIsOk,
|
||||
UploadImageRequest,
|
||||
} from "@storyteller/components/src/api/upload/UploadImage";
|
||||
|
||||
interface Props {
|
||||
uploadTypeLabel: string,
|
||||
uploadTypesAllowed: string[],
|
||||
|
||||
setMediaUploadToken: (token?: string) => void;
|
||||
|
||||
formIsCleared: boolean;
|
||||
setFormIsCleared: (cleared: boolean) => void;
|
||||
|
||||
setCanConvert: (canConvert: boolean) => void;
|
||||
changeConvertIdempotencyToken: () => void;
|
||||
}
|
||||
|
||||
function FaceAnimationUploadComponent(props: Props) {
|
||||
const [file, setFile] = useState<any>(undefined);
|
||||
const [isUploadDisabled, setIsUploadDisabled] = useState<boolean>(false);
|
||||
const [uploadLoading, setUploadLoading] = useState(false);
|
||||
|
||||
// Auto generated
|
||||
const [idempotencyToken, setIdempotencyToken] = useState(uuidv4());
|
||||
|
||||
const handleChange = (file: any) => {
|
||||
console.log("handle change");
|
||||
setFile(file);
|
||||
setIdempotencyToken(uuidv4());
|
||||
props.setFormIsCleared(false);
|
||||
props.setCanConvert(false);
|
||||
props.changeConvertIdempotencyToken();
|
||||
setIsUploadDisabled(false);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.add("upload-zone-drag");
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.remove("upload-zone-drag");
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setFile(null);
|
||||
setIdempotencyToken(uuidv4());
|
||||
setIsUploadDisabled(false);
|
||||
props.setMediaUploadToken(undefined); // clear
|
||||
props.setFormIsCleared(true);
|
||||
props.setCanConvert(false);
|
||||
props.changeConvertIdempotencyToken();
|
||||
};
|
||||
|
||||
const handleUploadFile = async () => {
|
||||
if (file === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setUploadLoading(true);
|
||||
|
||||
// TODO/FIXME: Horrible way to control upload type.
|
||||
switch (props.uploadTypeLabel.toLocaleLowerCase()) {
|
||||
case "audio":
|
||||
uploadAudio();
|
||||
break;
|
||||
case "image":
|
||||
uploadImage();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
setUploadLoading(false);
|
||||
};
|
||||
|
||||
const uploadAudio = async () => {
|
||||
const request: UploadAudioRequest = {
|
||||
uuid_idempotency_token: idempotencyToken,
|
||||
file: file,
|
||||
source: "file",
|
||||
};
|
||||
|
||||
let result = await UploadAudio(request);
|
||||
|
||||
if (UploadAudioIsOk(result)) {
|
||||
setIsUploadDisabled(true);
|
||||
props.setMediaUploadToken(result.upload_token);
|
||||
props.setFormIsCleared(false);
|
||||
props.setCanConvert(true);
|
||||
}
|
||||
}
|
||||
|
||||
const uploadImage = async () => {
|
||||
const request: UploadImageRequest = {
|
||||
uuid_idempotency_token: idempotencyToken,
|
||||
file: file,
|
||||
source: "file",
|
||||
};
|
||||
|
||||
let result = await UploadImage(request);
|
||||
|
||||
if (UploadImageIsOk(result)) {
|
||||
setIsUploadDisabled(true);
|
||||
props.setMediaUploadToken(result.upload_token);
|
||||
props.setFormIsCleared(false);
|
||||
props.setCanConvert(true);
|
||||
}
|
||||
}
|
||||
|
||||
const fileSize =
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: file
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
const uploadBtnClass = isUploadDisabled
|
||||
? "btn btn-uploaded w-100 disabled"
|
||||
: "btn btn-primary w-100";
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{/* Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files */}
|
||||
<FileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
types={props.uploadTypesAllowed}
|
||||
maxSize={50}
|
||||
children={
|
||||
<div
|
||||
className="panel panel-inner upload-zone d-flex align-items-center p-3"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div className="me-4">
|
||||
{file ? (
|
||||
<FontAwesomeIcon icon={faFileAudio} className="upload-icon" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="upload-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="pb-0">
|
||||
{file ? (
|
||||
<span className="filename" title={file.name}>
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<u className="fw-medium">Upload {props.uploadTypeLabel} file</u> or drop it
|
||||
here...
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<div>
|
||||
{file ? (
|
||||
<p>
|
||||
<span className="opacity-50">
|
||||
{file && `${file.name.split(".").pop().toUpperCase()}`}{" "}
|
||||
file size: {fileSize}
|
||||
</span>{" "}
|
||||
<u className="fw-medium opacity-100 ms-1">Change file</u>
|
||||
</p>
|
||||
) : (
|
||||
<p className="opacity-50">
|
||||
{props.uploadTypesAllowed.join(", ").toString()} supported
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{file ? (
|
||||
<div className="d-flex gap-3">
|
||||
<button
|
||||
className={uploadBtnClass}
|
||||
onClick={() => {
|
||||
handleUploadFile();
|
||||
}}
|
||||
type="submit"
|
||||
disabled={uploadLoading || isUploadDisabled}
|
||||
>
|
||||
{isUploadDisabled ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faCheck} className="me-2" />
|
||||
Uploaded
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload Media
|
||||
</>
|
||||
)}
|
||||
{uploadLoading && <LoadingIcon />}
|
||||
</button>
|
||||
|
||||
<button className="btn btn-destructive w-100" onClick={handleClear}>
|
||||
<FontAwesomeIcon icon={faTrash} className="me-2" />
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</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 default FaceAnimationUploadComponent;
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import { WebUrl } from "../../../../common/WebUrl";
|
||||
import { faFileArrowUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faFileArrowUp, faSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
// import {
|
||||
// faFileArrowUp,
|
||||
@@ -250,7 +250,7 @@ function LandingPage(props: Props) {
|
||||
to="/tts"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-center w-100">
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productTtsTitle")}
|
||||
@@ -259,7 +259,7 @@ function LandingPage(props: Props) {
|
||||
{t("productTtsText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/tts" className="btn btn-square">
|
||||
<Link to="/tts" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -276,7 +276,7 @@ function LandingPage(props: Props) {
|
||||
to="/voice-conversion"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-center w-100">
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productVcTitle")}
|
||||
@@ -285,7 +285,7 @@ function LandingPage(props: Props) {
|
||||
{t("productVcText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/voice-conversion" className="btn btn-square">
|
||||
<Link to="/voice-conversion" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -299,19 +299,23 @@ function LandingPage(props: Props) {
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/video"
|
||||
to="/face-animation"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-center w-100">
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productVideoTitle")}
|
||||
</h3>
|
||||
<h4 className="fw-bold text-white mb-1 d-flex align-items-center text-nowrap">
|
||||
<span className="badge-new mt-0 me-2">
|
||||
<FontAwesomeIcon icon={faSparkles} className="me-1" />
|
||||
NEW
|
||||
</span>
|
||||
Face Animation
|
||||
</h4>
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productVideoText")}
|
||||
Create videos from audio
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/video" className="btn btn-square">
|
||||
<Link to="/face-animation" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -323,6 +327,24 @@ function LandingPage(props: Props) {
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/video"
|
||||
className="panel panel-select d-flex flex-column align-items-center h-auto pb-4"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-center w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">{t("productVideoTitle")}</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white mb-0">
|
||||
{t("productVideoText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/video" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
|
||||
Reference in new issue
Block a user