Merge branch 'master' into bflat-fy-translation

This commit is contained in:
BFlat authored and GitHub committed 2023-09-23 23:22:35 +07:00
commit b41ff7d13c
9 files changed
+680 -13

No files matched your search

+1
View File
@@ -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 };
@@ -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 };
@@ -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"