Merge pull request #270 from storytold/bflat-fy-v2v-model-page

sidebar addition + v2v subpages
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-10-05 16:24:45 -04:00
commit e55246d91c
42 files changed
+2363 -64

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "গাইড",
"communityLeaderboard": "লিডারবোর্ড",
"communityTitle": "সম্প্রদায়",
"communityUploadModels": "আপলোড মডেল",
"infoPricing": "মূল্য নির্ধারণ",
"logOutButton": "প্রস্থান",
"loginButton": "প্রবেশ করুন",
"profileButton": "প্রোফাইল",
"queueFaceAnimator": "অ্যানিমেশন সারিবদ্ধ",
"queueRvc": "RVC সারিবদ্ধ",
"queueSvc": "SVC সারিবদ্ধ",
"queueTitle": "পরিষেবা সারি",
"queueTts": "টিটিএস সারিবদ্ধ",
"signUpButton": "নিবন্ধন করুন",
"speechTitle": "স্পিচ জেনারেশন",
"speechTts": "টেক্সট টু স্পিচ",
"speechVc": "ভয়েস টু ভয়েস",
"videoFaceAnimator": "ফেস অ্যানিমেটর",
"videoTitle": "ভিডিও জেনারেশন",
"videoW2l": "ভিডিও লিপ সিঙ্ক"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Führung",
"communityLeaderboard": "Bestenliste",
"communityTitle": "Gemeinschaft",
"communityUploadModels": "Modelle hochladen",
"infoPricing": "Preisgestaltung",
"logOutButton": "Ausloggen",
"loginButton": "Anmeldung",
"profileButton": "Profil",
"queueFaceAnimator": "Animationen in der Warteschlange",
"queueRvc": "RVC in der Warteschlange",
"queueSvc": "SVC in der Warteschlange",
"queueTitle": "Service-Warteschlangen",
"queueTts": "TTS in der Warteschlange",
"signUpButton": "Melden Sie sich an",
"speechTitle": "Spracherzeugung",
"speechTts": "Text zu Sprache",
"speechVc": "Stimme zu Stimme",
"videoFaceAnimator": "Gesichtsanimator",
"videoTitle": "Videogenerierung",
"videoW2l": "Video-Lippensynchronisation"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Guide",
"communityLeaderboard": "Leaderboard",
"communityTitle": "Community",
"communityUploadModels": "Upload Models",
"infoPricing": "Pricing",
"logOutButton": "Log Out",
"loginButton": "Login",
"profileButton": "Profile",
"queueFaceAnimator": "Animations Queued",
"queueRvc": "RVC Queued",
"queueSvc": "SVC Queued",
"queueTitle": "Service Queues",
"queueTts": "TTS Queued",
"signUpButton": "Sign Up",
"speechTitle": "Speech Generation",
"speechTts": "Text to Speech",
"speechVc": "Voice to Voice",
"videoFaceAnimator": "Face Animator",
"videoTitle": "Video Generation",
"videoW2l": "Video Lip Sync"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Guía",
"communityLeaderboard": "Tabla de clasificación",
"communityTitle": "Comunidad",
"communityUploadModels": "Subir modelos",
"infoPricing": "Precios",
"logOutButton": "Cerrar sesión",
"loginButton": "Acceso",
"profileButton": "Perfil",
"queueFaceAnimator": "Animaciones en cola",
"queueRvc": "RVC en cola",
"queueSvc": "SVC en cola",
"queueTitle": "Colas de servicio",
"queueTts": "TTS en cola",
"signUpButton": "Inscribirse",
"speechTitle": "Generación de voz",
"speechTts": "Texto a voz",
"speechVc": "Voz a Voz",
"videoFaceAnimator": "Animador de rostros",
"videoTitle": "Generación de vídeo",
"videoW2l": "Sincronización de labios de vídeo"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Guide",
"communityLeaderboard": "Classement",
"communityTitle": "Communauté",
"communityUploadModels": "Télécharger des modèles",
"infoPricing": "Tarifs",
"logOutButton": "Se déconnecter",
"loginButton": "Se connecter",
"profileButton": "Profil",
"queueFaceAnimator": "Animations en file d'attente",
"queueRvc": "RVC en file d'attente",
"queueSvc": "SVC en file d'attente",
"queueTitle": "Files d'attente de service",
"queueTts": "TTS en file d'attente",
"signUpButton": "S'inscrire",
"speechTitle": "Génération de parole",
"speechTts": "Texte pour parler",
"speechVc": "Voix à voix",
"videoFaceAnimator": "Animateur de visage",
"videoTitle": "Génération vidéo",
"videoW2l": "Synchronisation labiale vidéo"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "मार्गदर्शक",
"communityLeaderboard": "लीडरबोर्ड",
"communityTitle": "समुदाय",
"communityUploadModels": "मॉडल अपलोड करें",
"infoPricing": "मूल्य निर्धारण",
"logOutButton": "लॉग आउट",
"loginButton": "लॉग इन करें",
"profileButton": "प्रोफ़ाइल",
"queueFaceAnimator": "एनिमेशन कतारबद्ध",
"queueRvc": "आरवीसी कतारबद्ध",
"queueSvc": "एसवीसी कतारबद्ध",
"queueTitle": "सेवा कतारें",
"queueTts": "टीटीएस कतारबद्ध",
"signUpButton": "साइन अप करें",
"speechTitle": "भाषण निर्माण",
"speechTts": "भाषण के पाठ",
"speechVc": "आवाज से आवाज",
"videoFaceAnimator": "फेस एनिमेटर",
"videoTitle": "वीडियो जनरेशन",
"videoW2l": "वीडियो लिप सिंक"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Memandu",
"communityLeaderboard": "Papan peringkat",
"communityTitle": "Masyarakat",
"communityUploadModels": "Unggah Model",
"infoPricing": "Harga",
"logOutButton": "Keluar",
"loginButton": "Gabung",
"profileButton": "Profil",
"queueFaceAnimator": "Animasi Antri",
"queueRvc": "RVC Mengantri",
"queueSvc": "SVC Mengantri",
"queueTitle": "Antrian Layanan",
"queueTts": "TTS Mengantri",
"signUpButton": "Mendaftar",
"speechTitle": "Generasi Pidato",
"speechTts": "Teks pidato",
"speechVc": "Suara ke Suara",
"videoFaceAnimator": "Animator Wajah",
"videoTitle": "Pembuatan Video",
"videoW2l": "Sinkronisasi Bibir Video"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Guida",
"communityLeaderboard": "Classifica",
"communityTitle": "Comunità",
"communityUploadModels": "Carica modelli",
"infoPricing": "Prezzi",
"logOutButton": "Disconnettersi",
"loginButton": "Login",
"profileButton": "Profilo",
"queueFaceAnimator": "Animazioni in coda",
"queueRvc": "RVC in coda",
"queueSvc": "SVC in coda",
"queueTitle": "Code di servizio",
"queueTts": "TTS in coda",
"signUpButton": "Iscrizione",
"speechTitle": "Generazione del discorso",
"speechTts": "Sintesi vocale da testo",
"speechVc": "Da voce a voce",
"videoFaceAnimator": "Animatore di volti",
"videoTitle": "Generazione video",
"videoW2l": "Sincronizzazione labiale video"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "ガイド",
"communityLeaderboard": "リーダーボード",
"communityTitle": "コミュニティ",
"communityUploadModels": "モデルのアップロード",
"infoPricing": "価格設定",
"logOutButton": "ログアウト",
"loginButton": "ログイン",
"profileButton": "プロフィール",
"queueFaceAnimator": "キューに入れられたアニメーション",
"queueRvc": "RVC がキューに登録されました",
"queueSvc": "SVC がキューに登録されました",
"queueTitle": "サービスキュー",
"queueTts": "TTS がキューに登録されました",
"signUpButton": "サインアップ",
"speechTitle": "音声生成",
"speechTts": "テキスト読み上げ",
"speechVc": "声から声へ",
"videoFaceAnimator": "フェイスアニメーター",
"videoTitle": "ビデオ生成",
"videoW2l": "ビデオリップシンク"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "가이드",
"communityLeaderboard": "리더보드",
"communityTitle": "지역 사회",
"communityUploadModels": "모델 업로드",
"infoPricing": "가격",
"logOutButton": "로그 아웃",
"loginButton": "로그인",
"profileButton": "프로필",
"queueFaceAnimator": "대기 중인 애니메이션",
"queueRvc": "RVC 대기 중",
"queueSvc": "SVC 대기 중",
"queueTitle": "서비스 대기열",
"queueTts": "TTS 대기 중",
"signUpButton": "가입하기",
"speechTitle": "음성 생성",
"speechTts": "텍스트 음성 변환",
"speechVc": "음성 대 음성",
"videoFaceAnimator": "얼굴 애니메이터",
"videoTitle": "비디오 생성",
"videoW2l": "비디오 립싱크"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Guia",
"communityLeaderboard": "Entre os melhores",
"communityTitle": "Comunidade",
"communityUploadModels": "Carregar modelos",
"infoPricing": "Preços",
"logOutButton": "Sair",
"loginButton": "Conecte-se",
"profileButton": "Perfil",
"queueFaceAnimator": "Animações na fila",
"queueRvc": "RVC na fila",
"queueSvc": "SVC na fila",
"queueTitle": "Filas de serviço",
"queueTts": "TTS na fila",
"signUpButton": "Inscrever-se",
"speechTitle": "Geração de fala",
"speechTts": "Texto para fala",
"speechVc": "Voz para Voz",
"videoFaceAnimator": "Animador facial",
"videoTitle": "Geração de Vídeo",
"videoW2l": "Sincronização labial de vídeo"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Гид",
"communityLeaderboard": "Таблица лидеров",
"communityTitle": "Сообщество",
"communityUploadModels": "Загрузить модели",
"infoPricing": "Цены",
"logOutButton": "Выйти",
"loginButton": "Авторизоваться",
"profileButton": "Профиль",
"queueFaceAnimator": "Анимации в очереди",
"queueRvc": "RVC в очереди",
"queueSvc": "SVC в очереди",
"queueTitle": "Очереди обслуживания",
"queueTts": "TTS в очереди",
"signUpButton": "Зарегистрироваться",
"speechTitle": "Генерация речи",
"speechTts": "Текст в речь",
"speechVc": "Голос в голос",
"videoFaceAnimator": "Лицевой Аниматор",
"videoTitle": "Создание видео",
"videoW2l": "Видео синхронизация губ"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "แนะนำ",
"communityLeaderboard": "ลีดเดอร์บอร์ด",
"communityTitle": "ชุมชน",
"communityUploadModels": "อัพโหลดโมเดล",
"infoPricing": "ราคา",
"logOutButton": "ออกจากระบบ",
"loginButton": "เข้าสู่ระบบ",
"profileButton": "ประวัติโดยย่อ",
"queueFaceAnimator": "ภาพเคลื่อนไหวอยู่ในคิว",
"queueRvc": "RVC อยู่ในคิว",
"queueSvc": "SVC อยู่ในคิว",
"queueTitle": "คิวบริการ",
"queueTts": "TTS อยู่ในคิว",
"signUpButton": "ลงชื่อ",
"speechTitle": "การสร้างคำพูด",
"speechTts": "ข้อความเป็นคำพูด",
"speechVc": "เสียงเป็นเสียง",
"videoFaceAnimator": "แอนิเมชั่นใบหน้า",
"videoTitle": "การสร้างวิดีโอ",
"videoW2l": "ลิปซิงค์วิดีโอ"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Rehber",
"communityLeaderboard": "Liderler Sıralaması",
"communityTitle": "Toplum",
"communityUploadModels": "Modelleri Yükle",
"infoPricing": "Fiyatlandırma",
"logOutButton": "Çıkış Yap",
"loginButton": "Giriş yapmak",
"profileButton": "Profil",
"queueFaceAnimator": "Animasyonlar Sıraya Alındı",
"queueRvc": "RVC Kuyruğa Alındı",
"queueSvc": "SVC Sıraya Alındı",
"queueTitle": "Hizmet Kuyrukları",
"queueTts": "TTS Sıraya Alındı",
"signUpButton": "Üye olmak",
"speechTitle": "Konuşma Üretimi",
"speechTts": "Konuşma metni",
"speechVc": "Sesten Sese",
"videoFaceAnimator": "Yüz Animatörü",
"videoTitle": "Video Oluşturma",
"videoW2l": "Video Dudak Senkronizasyonu"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "Hướng dẫn",
"communityLeaderboard": "Bảng xếp hạng",
"communityTitle": "Cộng đồng",
"communityUploadModels": "Tải lên mô hình",
"infoPricing": "Định giá",
"logOutButton": "Đăng xuất",
"loginButton": "Đăng nhập",
"profileButton": "Hồ sơ",
"queueFaceAnimator": "Hoạt ảnh được xếp hàng đợi",
"queueRvc": "RVC xếp hàng",
"queueSvc": "SVC xếp hàng",
"queueTitle": "Hàng đợi dịch vụ",
"queueTts": "TTS xếp hàng",
"signUpButton": "Đăng ký",
"speechTitle": "Tạo giọng nói",
"speechTts": "Chuyển văn bản thành giọng nói",
"speechVc": "Giọng nói thành giọng nói",
"videoFaceAnimator": "khuôn mặt hoạt hình",
"videoTitle": "Tạo video",
"videoW2l": "Hát nhép video"
}
@@ -0,0 +1,23 @@
{
"communityDiscord": "Discord",
"communityGuide": "指导",
"communityLeaderboard": "排行榜",
"communityTitle": "社区",
"communityUploadModels": "上传模型",
"infoPricing": "价钱",
"logOutButton": "登出",
"loginButton": "登录",
"profileButton": "轮廓",
"queueFaceAnimator": "动画排队",
"queueRvc": "RVC 排队",
"queueSvc": "SVC 排队",
"queueTitle": "服务队列",
"queueTts": "TTS 排队",
"signUpButton": "报名",
"speechTitle": "语音生成",
"speechTts": "文字转语音",
"speechVc": "语音到语音",
"videoFaceAnimator": "脸部动画师",
"videoTitle": "视频生成",
"videoW2l": "视频唇形同步"
}
+62 -35
View File
@@ -8,7 +8,7 @@ import {
} from "@storyteller/components/src/api/locale/DetectLocale";
import { Language } from "@storyteller/components/src/i18n/Language";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { PageContainer } from "./v2/view/PageContainer";
import PageContainer from "./v2/view/PageContainer";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
@@ -45,15 +45,21 @@ import {
AVAILABLE_LANGUAGE_MAP,
ENGLISH_LANGUAGE,
} from "./_i18n/AvailableLanguageMap";
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
import { GetModelInferenceJobStatus, GetModelInferenceJobStatusIsOk } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import {
GetModelInferenceJobStatus,
GetModelInferenceJobStatusIsOk,
} from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
import { VoiceConversionModelUploadJob } from "@storyteller/components/src/jobs/VoiceConversionModelUploadJob";
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import HttpBackend from 'i18next-http-backend'
import HttpBackend from "i18next-http-backend";
// NB: We're transitioning over to this instance of i18n-next that loads translations over HTTP from Json Files.
// The old i18n-next instance (see below) bakes in translations into the compiled javascript blob.
// This new instance uses the Locize paid service to manage translation strings on their website. It's automated,
// This new instance uses the Locize paid service to manage translation strings on their website. It's automated,
// can easily sync to version control, and makes translation easy to maintain across a wide number of languages.
export const i18n2 = i18n.createInstance();
@@ -79,16 +85,14 @@ i18n
},
});
i18n2
.use(HttpBackend)
.init({
fallbackLng: "en",
debug: false,
backend: {
// This is the path localizations are loaded from.
loadPath: '/locales/{{lng}}/{{ns}}.json',
}
});
i18n2.use(HttpBackend).init({
fallbackLng: "en",
debug: false,
backend: {
// This is the path localizations are loaded from.
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
});
enum MigrationMode {
NEW_VOCODES,
@@ -123,8 +127,8 @@ interface Props {
maybeSelectedTtsModel?: TtsModelListItem;
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
selectedTtsLanguageScope: string,
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void,
selectedTtsLanguageScope: string;
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
}
interface State {
@@ -310,7 +314,8 @@ class App extends React.Component<Props, State> {
let preferredLanguage = ENGLISH_LANGUAGE;
for (let languageCode of locale.language_codes) {
let maybeLanguage = AVAILABLE_LANGUAGE_MAP[languageCode as AvailableLanguageKey];
let maybeLanguage =
AVAILABLE_LANGUAGE_MAP[languageCode as AvailableLanguageKey];
if (maybeLanguage !== undefined) {
preferredLanguage = maybeLanguage;
@@ -373,12 +378,14 @@ class App extends React.Component<Props, State> {
};
setAllVoiceConversionModels = (models: VoiceConversionModelListItem[]) => {
this.setState({ voiceConversionModels: models })
}
this.setState({ voiceConversionModels: models });
};
setMaybeSelectedVoiceConversionModel =(model?: VoiceConversionModelListItem) => {
this.setState({ maybeSelectedVoiceConversionModel: model })
}
setMaybeSelectedVoiceConversionModel = (
model?: VoiceConversionModelListItem
) => {
this.setState({ maybeSelectedVoiceConversionModel: model });
};
checkInferenceJob = async (jobToken: string) => {
const lookupResult = await GetModelInferenceJobStatus(jobToken);
@@ -394,17 +401,21 @@ class App extends React.Component<Props, State> {
!jobStateCanChange(existingJob.jobState)
) {
updatedJobs.push(existingJob);
inferenceJobsByCategory.get(existingJob.frontendJobType)?.push(existingJob);
inferenceJobsByCategory
.get(existingJob.frontendJobType)
?.push(existingJob);
return;
}
let updatedJob = InferenceJob.fromResponse(
lookupResult.state!,
lookupResult.state!,
existingJob.frontendJobType
);
updatedJobs.push(updatedJob);
inferenceJobsByCategory.get(updatedJob.frontendJobType)?.push(updatedJob);
inferenceJobsByCategory
.get(updatedJob.frontendJobType)
?.push(updatedJob);
});
this.setState({
@@ -414,7 +425,10 @@ class App extends React.Component<Props, State> {
}
};
enqueueInferenceJob = (jobToken: string, frontendJobType: FrontendInferenceJobType) => {
enqueueInferenceJob = (
jobToken: string,
frontendJobType: FrontendInferenceJobType
) => {
const newJob = new InferenceJob(jobToken, frontendJobType);
let inferenceJobs = this.state.inferenceJobs.concat([newJob]);
@@ -689,7 +703,9 @@ class App extends React.Component<Props, State> {
return;
}
let updatedJob = VoiceConversionModelUploadJob.fromResponse(lookupResult.state!);
let updatedJob = VoiceConversionModelUploadJob.fromResponse(
lookupResult.state!
);
updatedJobs.push(updatedJob);
});
@@ -809,8 +825,12 @@ class App extends React.Component<Props, State> {
w2lTemplateUploadJobs={this.state.w2lTemplateUploadJobs}
enqueueVocoderUploadJob={this.enqueueVocoderUploadJob}
vocoderUploadJobs={this.state.vocoderUploadJobs}
enqueueVoiceConversionModelUploadJob={this.enqueueVoiceConversionModelUploadJob}
voiceConversionModelUploadJobs={this.state.voiceConversionModelUploadJobs}
enqueueVoiceConversionModelUploadJob={
this.enqueueVoiceConversionModelUploadJob
}
voiceConversionModelUploadJobs={
this.state.voiceConversionModelUploadJobs
}
textBuffer={this.state.textBuffer}
setTextBuffer={this.setTextBuffer}
clearTextBuffer={this.clearTextBuffer}
@@ -839,13 +859,20 @@ class App extends React.Component<Props, State> {
setMaybeSelectedTtsModel={
this.props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={this.props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={this.props.setSelectedTtsLanguageScope}
selectedTtsLanguageScope={
this.props.selectedTtsLanguageScope
}
setSelectedTtsLanguageScope={
this.props.setSelectedTtsLanguageScope
}
voiceConversionModels={this.state.voiceConversionModels}
setVoiceConversionModels={this.setAllVoiceConversionModels}
maybeSelectedVoiceConversionModel={this.state.maybeSelectedVoiceConversionModel}
setMaybeSelectedVoiceConversionModel={this.setMaybeSelectedVoiceConversionModel}
maybeSelectedVoiceConversionModel={
this.state.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={
this.setMaybeSelectedVoiceConversionModel
}
/>
</Route>
</Switch>
+1
View File
@@ -1,5 +1,6 @@
// @import "./scss/normalize.scss";
// @import-normalize; /* bring in normalize.css styles */
@import "./scss/nav.scss";
@import "./scss/colours.scss";
@import "./scss/site-refresh.scss";
// @import "./scss/CSSParticles.scss";
@@ -0,0 +1,76 @@
@import "scss/colours.scss";
.button {
border-radius: 0.5rem;
outline: none;
font-size: 15px;
font-weight: 500;
padding: 10px 20px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.button-primary,
.button-primary:focus,
.button-primary:active {
background-color: $primary;
border: 2.5px solid;
border-color: $primary;
transition: 0.2s;
color: $white;
}
.button-primary:hover {
background-color: lighten($primary, 5%);
border-color: lighten($primary, 5%);
color: $white;
}
.button-secondary,
.button-secondary:focus,
.button-secondary:active {
background-color: $secondary;
color: $white;
border: 2.5px solid;
border-color: $secondary;
transition: 0.2s;
}
.button-secondary:hover {
background-color: lighten($secondary, 5%);
color: $white;
border: 2.5px solid;
border-color: lighten($secondary, 5%);
}
.button-destructive,
.button-destructive:focus,
.button-destructive:active {
background-color: $danger;
color: $white;
border: 2.5px solid;
border-color: $danger;
transition: 0.2s;
}
.button-destructive:hover {
background-color: lighten($danger, 5%);
color: $white;
border: 2.5px solid;
border-color: lighten($danger, 5%);
}
.button-small,
.button-small:focus,
.button-small:active {
font-weight: 500;
font-size: 14px;
padding: 8.25px 16px;
transition: 0.2s;
border: none;
}
.button-small:hover {
border: none;
}
@@ -0,0 +1,37 @@
import React, { ButtonHTMLAttributes } from "react";
import "./Button.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label?: string;
icon?: IconDefinition;
small?: boolean;
secondary?: boolean;
danger?: boolean;
}
export default function Button({
label,
icon,
small,
secondary,
danger,
...rest
}: ButtonProps) {
const buttonClasses = `button ${small ? "button-small" : ""}
${
secondary
? "button-secondary"
: danger
? "button-destructive"
: "button-primary"
} `;
return (
<button className={buttonClasses} type="button" {...rest}>
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
{label}
</button>
);
}
@@ -0,0 +1,68 @@
@import "scss/colours.scss";
.select__control {
&.select {
height: 48px;
border-width: 2px;
padding-left: 0.15rem;
padding-right: 0.15rem;
background-color: $input-dark;
border-radius: 0.5rem;
border-color: $input-dark !important;
box-shadow: none;
cursor: pointer;
&.no-padding {
padding-left: 0.5rem;
}
&:hover {
background-color: lighten($input-dark, 5%);
border-color: lighten($input-dark, 5%) !important;
}
&.focused {
border-color: $primary !important;
}
&.with-icon {
padding-left: 2rem;
}
&.rounded-full {
border-radius: 50px;
}
}
&.select-option {
background-color: $input-dark;
cursor: pointer;
padding: 0.5rem 1rem;
&:hover {
background-color: lighten($input-dark, 8%);
}
}
&.select-input,
&.select-value {
color: $white;
}
&.select-placeholder {
color: $white !important;
}
&.select-container {
background: $input-dark;
margin-top: 6px;
padding: 0;
border-radius: 0.5rem;
z-index: 3;
}
&.select-separator {
width: 1px;
opacity: 0.3;
}
}
@@ -0,0 +1,92 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import "./Inputs.scss";
import {
default as ReactSelect,
Props as ReactSelectProps,
} from "react-select";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
icon?: IconDefinition;
label?: string;
textArea?: boolean;
}
interface TextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
textArea?: boolean;
}
interface SelectProps extends ReactSelectProps {
icon?: IconDefinition;
label?: string;
rounded?: boolean;
}
export function Input({ label, icon, textArea, ...rest }: InputProps) {
return (
<div>
{label && <label className="sub-title">{label}</label>}
<div className={`form-group ${icon ? "input-icon" : ""}`}>
{icon && (
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
)}
<input className="form-control" {...rest} />
</div>
</div>
);
}
export function TextArea({ label, textArea, ...rest }: TextAreaProps) {
return (
<div>
{label && <label className="sub-title">{label}</label>}
<div className="form-group">
<textarea className="form-control" {...rest} />
</div>
</div>
);
}
export function Select({ label, icon, rounded, ...rest }: SelectProps) {
const SelectFieldClass = {
control: (state: any) =>
state.isFocused
? `select focused ${icon && "with-icon"} ${rounded && "rounded-full"}`
: `select ${icon && "with-icon"} ${rounded && "rounded-full"}`,
option: (state: any) =>
state.isFocused ? "select-option" : "select-option",
input: (state: any) => (state.isFocused ? "select-input" : "select-input"),
placeholder: (state: any) =>
state.isFocused ? "select-placeholder" : "select-placeholder",
singleValue: (state: any) =>
state.isFocused ? "select-value" : "select-value",
menu: (state: any) =>
state.isFocused ? "select-container" : "select-container",
indicatorSeparator: (state: any) =>
state.isFocused ? "select-separator" : "select-separator",
};
return (
<div>
{label && <label className="sub-title">{label}</label>}
<div className={`form-group ${icon ? "input-icon" : ""}`}>
{icon && (
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
)}
<div className="w-100">
<ReactSelect
classNamePrefix="select"
classNames={SelectFieldClass}
{...rest}
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,24 @@
import { container } from "data/animation";
import { motion } from "framer-motion";
import React from "react";
interface PageContainerProps {
children: React.ReactNode;
padding?: boolean;
}
export default function PageContainer({
children,
padding,
}: PageContainerProps) {
return (
<motion.div
initial="hidden"
animate="visible"
variants={container}
className={`${padding ? "container" : ""}`}
>
{children}
</motion.div>
);
}
@@ -0,0 +1,23 @@
import React from "react";
import { motion } from "framer-motion";
import { panel } from "data/animation";
interface PanelProps {
children: React.ReactNode;
padding?: boolean;
mb?: boolean;
mt?: boolean;
}
export default function Panel({ children, padding, mb, mt }: PanelProps) {
return (
<motion.div className="container-panel" variants={panel}>
<div
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
${mb ? "mb-4" : ""} ${mt ? "mt-4" : ""}`}
>
{children}
</div>
</motion.div>
);
}
@@ -0,0 +1,46 @@
import React, { useState } from "react";
import Tippy from "@tippyjs/react";
import { faLink } from "@fortawesome/pro-solid-svg-icons";
import Button from "../Button/Button";
interface ShareButtonProps {
url: string;
}
export default function ShareButton(props: ShareButtonProps) {
const [tippyContent, setTippyContent] = useState("Share");
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(props.url);
setTippyContent("Link Copied");
setTimeout(() => {
setTippyContent("Share");
}, 800);
} catch (error) {
console.error("Failed to copy link to clipboard:", error);
}
};
return (
<div className="d-flex">
<Tippy
content={tippyContent}
hideOnClick={false}
placement="bottom"
theme="fakeyou"
arrow={false}
>
<div>
<Button
label="Share Link"
icon={faLink}
secondary
small
onClick={copyToClipboard}
/>
</div>
</Tippy>
</div>
);
}
@@ -0,0 +1,26 @@
@import "scss/colours.scss";
.mobile-menu-container {
padding-bottom: 4rem;
}
.mobile-menu {
z-index: 100;
position: fixed;
min-width: 0px;
top: 0rem;
left: 0rem;
right: 0rem;
border-radius: 0rem 0rem 0.5rem 0.5rem;
background-color: $panel;
border-color: #ffffff12 !important;
border: 1px solid;
align-items: center;
justify-content: space-between;
overflow: hidden;
transition: all 200ms ease 0s;
box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.4);
padding: 0.6rem 1rem;
}
@@ -0,0 +1,80 @@
import React from "react";
import "./MobileMenu.scss";
import Button from "components/common/Button/Button";
import { faBars } from "@fortawesome/pro-solid-svg-icons";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { useHistory } from "react-router-dom";
import { WebUrl } from "common/WebUrl";
import { faUser } from "@fortawesome/pro-solid-svg-icons";
interface MobileMenuProps {
sessionWrapper: SessionWrapper;
}
export default function MobileMenu(props: MobileMenuProps) {
let history = useHistory();
const handleMenuButtonClick = () => {
const wrapper = document.getElementById("wrapper");
if (window.innerWidth < 1200) {
if (wrapper) {
wrapper.classList.toggle("toggled");
}
}
};
const loggedIn = props.sessionWrapper.isLoggedIn();
let signupOrProfileButton = (
<>
<Button label="Sign Up" small onClick={() => history.push("/signup")} />
</>
);
if (loggedIn) {
let displayName = props.sessionWrapper.getDisplayName();
if (displayName === undefined) {
displayName = "My Account";
}
let url = WebUrl.userProfilePage(displayName);
signupOrProfileButton = (
<>
<Button
icon={faUser}
label="Profile"
small
secondary
onClick={() => history.push(url)}
/>
</>
);
}
return (
<div className="mobile-menu-container d-lg-none">
<div className="mobile-menu">
<div className="row">
<div className="d-flex col-4">
<Button
label="Menu"
icon={faBars}
secondary
small
onClick={handleMenuButtonClick}
/>
</div>
<div className="d-flex col-4 justify-content-center align-items-center">
<img
src="/fakeyou/FakeYou-Logo-Mobile.png"
alt="FakeYou Mobile Logo"
height="36"
/>
</div>
<div className="d-flex col-4 justify-content-end">
{signupOrProfileButton}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,96 @@
import React from "react";
import Panel from "../../common/Panel/Panel";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface PageHeaderModelViewProps {
titleIcon?: IconDefinition;
title: string;
subText: React.ReactNode;
tags?: string[];
ratingBtn?: React.ReactNode;
ratingStats?: React.ReactNode;
extras?: React.ReactNode;
view?: "regular" | "edit" | "delete";
modelType?: "TTS" | "V2V";
}
export default function PageHeaderModelView({
titleIcon,
title,
subText,
tags,
ratingBtn,
ratingStats,
extras,
view = "regular",
modelType = "TTS",
}: PageHeaderModelViewProps) {
const icon = (
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
);
return (
<div className="pt-2 pt-lg-4 pb-4">
{view === "regular" && (
<Panel padding>
<div className="row gy-3">
<div className="col-12 col-lg-8">
<h2 className="fw-bold">
{icon}
{title}
</h2>
<p>{subText}</p>
</div>
<div className="col-12 col-lg-4">
<div className="d-flex gap-2 flex-wrap justify-content-lg-end">
{tags &&
tags.map((tag, index) => (
<div key={index}>
<span className="badge badge-tag">{tag}</span>
</div>
))}
</div>
</div>
</div>
<hr className="my-4" />
<div className="d-flex flex-column flex-lg-row flex-column-reverse gap-3">
<div className="d-flex gap-3">
{ratingBtn}
<div className="d-lg-none">{extras}</div>
</div>
{ratingStats}
<div className="flex-grow-1 d-none d-lg-flex justify-content-end">
{extras}
</div>
</div>
</Panel>
)}
{view === "edit" && (
<Panel padding>
<div className="d-flex">
<div className="flex-grow-1">
<h2 className="fw-bold mb-0">
{icon}Edit {modelType} Model
</h2>
</div>
</div>
</Panel>
)}
{view === "delete" && (
<Panel padding>
<div className="d-flex">
<div className="flex-grow-1">
<h2 className="fw-bold mb-0">
{icon}Delete {modelType} Model
</h2>
</div>
</div>
</Panel>
)}
</div>
);
}
@@ -0,0 +1,371 @@
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import {
faBookOpen,
faMessageDots,
faTrophy,
faWaveformLines,
faStar,
faUser,
faSignOutAlt,
faFaceViewfinder,
faCloudUpload,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "components/common/Button/Button";
import React, { useEffect, useState } from "react";
import { Link, NavLink, useHistory, useRouteMatch } from "react-router-dom";
import {
GetQueueStats,
GetQueueStatsIsOk,
GetQueueStatsSuccessResponse,
} from "@storyteller/components/src/api/stats/queues/GetQueueStats";
import { WebUrl } from "common/WebUrl";
import { Logout } from "@storyteller/components/src/api/session/Logout";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { useLocalize } from "hooks";
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
interface SideNavProps {
sessionWrapper: SessionWrapper;
logoutHandler: () => void;
querySessionCallback: () => void;
querySessionSubscriptionsCallback: () => void;
}
export default function SideNav(props: SideNavProps) {
const { t } = useLocalize("SideNav");
let history = useHistory();
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const handleNavLinkClick = () => {
const wrapper = document.getElementById("wrapper");
if (window.innerWidth < 992) {
if (wrapper) {
wrapper.classList.toggle("toggled");
}
}
};
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
const wrapper = document.getElementById("wrapper");
const overlay = document.getElementById("overlay");
if (
(!wrapper?.contains(event.target as Node) ||
overlay?.contains(event.target as Node)) &&
window.innerWidth < 992
) {
wrapper?.classList.remove("toggled");
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, []);
const matchHome = useRouteMatch({
path: "/",
exact: true,
});
useEffect(() => {
// Update window width on resize
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
// Cleanup listener on unmount
window.removeEventListener("resize", handleResize);
};
}, []);
const shouldShowSidebar = windowWidth >= 992 && matchHome;
useEffect(() => {
const wrapper = document.getElementById("wrapper");
if (windowWidth >= 992) {
if (matchHome) {
wrapper?.classList.add("no-padding");
} else {
wrapper?.classList.remove("no-padding");
}
}
}, [matchHome, windowWidth]);
const [queueStats, setQueueStats] = useState<GetQueueStatsSuccessResponse>({
success: true,
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
inference: {
total_pending_job_count: 0,
pending_job_count: 0,
by_queue: {
pending_svc_jobs: 0,
pending_rvc_jobs: 0,
pending_face_animation_jobs: 0,
},
},
legacy_tts: {
pending_job_count: 0,
},
});
useEffect(() => {
const fetch = async () => {
const response = await GetQueueStats();
if (GetQueueStatsIsOk(response)) {
if (response.cache_time.getTime() > queueStats.cache_time.getTime()) {
setQueueStats(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,
queueStats.refresh_interval_millis
);
const interval = setInterval(async () => fetch(), refreshInterval);
fetch();
return () => clearInterval(interval);
}, [queueStats]);
const logoutHandler = async () => {
await Logout();
props.querySessionCallback();
props.querySessionSubscriptionsCallback();
// PosthogClient.reset();
// Analytics.accountLogout();
history.push("/");
};
const loggedIn = props.sessionWrapper.isLoggedIn();
let userOrLoginButton = (
<>
<Button
label={t("loginButton")}
small
secondary
onClick={() => {
history.push("/login");
handleNavLinkClick();
}}
/>
</>
);
let signupOrLogOutButton = (
<>
<Button
label={t("signUpButton")}
small
onClick={() => {
history.push("/signup");
handleNavLinkClick();
}}
/>
</>
);
if (loggedIn) {
let displayName = props.sessionWrapper.getDisplayName();
// let gravatarHash = props.sessionWrapper.getEmailGravatarHash();
// let gravatar = <span />;
if (displayName === undefined) {
displayName = "My Account";
}
let url = WebUrl.userProfilePage(displayName);
userOrLoginButton = (
<>
<Button
icon={faUser}
label={t("profileButton")}
small
secondary
onClick={() => {
history.push(url);
handleNavLinkClick();
}}
/>
</>
);
signupOrLogOutButton = (
<>
<Button
icon={faSignOutAlt}
label={t("logOutButton")}
small
danger
onClick={async () => {
await logoutHandler();
handleNavLinkClick();
}}
/>
</>
);
}
return (
<div
id="sidebar-wrapper"
className={`sidebar ${shouldShowSidebar ? "" : "visible"}`}
>
<ul className="sidebar-nav">
<div className="sidebar-brand">
<Link to="/" onClick={handleNavLinkClick}>
<img
src="/fakeyou/FakeYou-Logo.png"
alt="FakeYou: Cartoon and Celebrity Text to Speech"
height="36"
/>
</Link>
</div>
<div className="sidebar-buttons d-flex gap-2 mt-4">
{userOrLoginButton}
{signupOrLogOutButton}
</div>
<hr className="my-4" />
<li className="sidebar-heading">{t("speechTitle")}</li>
<li>
<NavLink
to="/tts"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faMessageDots}
className="sidebar-heading-icon"
/>
{t("speechTts")}
</NavLink>
</li>
<li>
<NavLink
to="/voice-conversion"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faWaveformLines}
className="sidebar-heading-icon"
/>
{t("speechVc")}
</NavLink>
</li>
<hr className="mb-4 mt-3" />
<li className="sidebar-heading">{t("videoTitle")}</li>
<li>
<NavLink
to="/face-animation"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faFaceViewfinder}
className="sidebar-heading-icon"
/>
{t("videoFaceAnimator")}
</NavLink>
</li>
<hr className="mb-4 mt-3" />
<li className="sidebar-heading">{t("communityTitle")}</li>
<li>
<NavLink
to="/contribute"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faCloudUpload}
className="sidebar-heading-icon"
/>
{t("communityUploadModels")}
</NavLink>
</li>
<li>
<a href="https://discord.gg/fakeyou" target="_blank" rel="noreferrer">
<FontAwesomeIcon
icon={faDiscord}
className="sidebar-heading-icon"
/>
{t("communityDiscord")}
</a>
<NavLink
to="/leaderboard"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon icon={faTrophy} className="sidebar-heading-icon" />
{t("communityLeaderboard")}
</NavLink>
<NavLink
to="/guide"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faBookOpen}
className="sidebar-heading-icon"
/>
{t("communityGuide")}
</NavLink>
</li>
<hr className="mb-3 mt-3" />
<li>
<NavLink
to="/pricing"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon icon={faStar} className="sidebar-heading-icon" />
{t("infoPricing")}
</NavLink>
</li>
<hr className="mb-4 mt-3" />
<li className="sidebar-heading">{t("queueTitle")}</li>
<li className="ps-4 fs-7 mb-5">
<div>
{t("queueTts")}:{" "}
<span className="text-red">
{queueStats.legacy_tts.pending_job_count}
</span>
</div>
<div>
{t("queueRvc")}:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_rvc_jobs}
</span>
</div>
<div>
{t("queueSvc")}:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_svc_jobs}
</span>
</div>
<div>
{t("queueFaceAnimator")}:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_face_animation_jobs}
</span>
</div>
</li>
</ul>
</div>
);
}
+176
View File
@@ -0,0 +1,176 @@
@import "./colours.scss";
/* Toggle Styles */
#wrapper {
padding-left: 0;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
#wrapper.toggled {
padding-left: 260px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
left: 260px;
width: 0;
height: 100%;
margin-left: -260px;
overflow: auto;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
#wrapper.toggled #sidebar-wrapper {
width: 260px;
}
#page-content-wrapper {
width: 100%;
position: relative;
}
#wrapper.toggled #page-content-wrapper {
position: absolute;
margin-right: -260px;
}
/* Sidebar Styles */
.sidebar-nav {
position: absolute;
top: 0;
width: 260px;
margin: 0;
padding: 0;
list-style: none;
min-height: 100vh;
background: $panel;
border-color: rgba(255, 255, 255, 0.0705882353) !important;
border-right: 1px solid;
}
// .sidebar-nav li {
// text-indent: 2rem;
// line-height: 38px;
// }
.sidebar-nav li a {
display: block;
text-decoration: none;
color: rgba($white, 0.7);
font-weight: 500;
margin: 0rem 1rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
.sidebar-nav li a:hover {
text-decoration: none;
color: $white;
}
.sidebar-nav li a:active,
.sidebar-nav li a:focus {
text-decoration: none;
}
.sidebar-brand {
font-size: 18px;
padding-top: 1.5rem;
line-height: 0;
margin-left: 1.5rem;
}
.sidebar-buttons {
margin-left: 1.5rem;
}
.sidebar-heading {
margin-left: 1.5rem;
margin-bottom: 0.5rem;
opacity: 0.5;
font-size: 14px;
}
.sidebar-heading-icon {
margin-right: 0.75rem;
}
.active-link {
color: $white !important;
background-color: $secondary;
}
#overlay {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5); /* Semi-black background */
z-index: 900; /* Appear on top of the content */
}
.toggled #page-content-wrapper {
position: relative;
z-index: 2; /* Ensure content is above the overlay */
}
.toggled #overlay {
display: block;
}
.no-padding {
padding-left: 0 !important;
}
.sidebar {
transform: translateX(
-100%
); /* move the sidebar out of the screen to the left */
transition: transform 0.3s ease-out; /* transition effect */
/* other necessary styles for the sidebar */
}
/* Visible state for the sidebar */
.sidebar.visible {
transform: translateX(0); /* sidebar occupies its regular position */
}
@media (min-width: 992px) {
#wrapper {
padding-left: 260px;
}
#wrapper.toggled {
padding-left: 0;
}
#sidebar-wrapper {
width: 260px;
}
#wrapper.toggled #sidebar-wrapper {
width: 0;
}
#page-content-wrapper {
position: relative;
}
#wrapper.toggled #page-content-wrapper {
position: relative;
margin-right: 0;
}
}
@@ -420,7 +420,7 @@ styles with important tags:
.form-control-feedback {
position: absolute;
z-index: 2;
width: 2.375rem;
width: 2.75rem;
// height: 2.375rem;
// line-height: 2.69rem;
text-align: center;
@@ -642,6 +642,13 @@ styles with important tags:
background-color: #fff3cd;
color: #664d03;
}
&.badge-tag {
font-size: 13px;
padding: 6px 12px;
background-color: $input-dark;
color: $white;
}
}
.badge-model {
@@ -740,7 +747,7 @@ styles with important tags:
padding-left: 2.25rem;
padding-right: 0.5rem;
background-color: $input-dark;
border-radius: 50px;
border-radius: 0.5rem;
border-color: $input-dark !important;
box-shadow: none;
cursor: pointer;
@@ -850,18 +857,18 @@ styles with important tags:
font-weight: 500;
font-size: 17px;
background-color: lighten($secondary, 4%);
border-radius: 50px;
border-radius: 0.5rem;
border: none;
padding: 6px 20px;
color: rgba($white, 0.5);
transition: 0.1s all;
&.left {
border-radius: 50px 0px 0px 50px;
border-radius: 0.5rem 0px 0px 0.5rem;
}
&.right {
border-radius: 0px 50px 50px 5px;
border-radius: 0px 0.5rem 0.5rem 0px;
}
&.rated {
@@ -874,6 +881,25 @@ styles with important tags:
}
}
.btn-action {
font-weight: 500;
font-size: 14px;
background-color: lighten($secondary, 4%);
border-radius: 0.5rem;
border: none;
padding: 8.25px 20px;
color: $white;
transition: 0.1s all;
&:hover {
background-color: lighten($secondary, 10%);
}
&:active {
background-color: lighten($secondary, 15%);
}
}
.rating-icon {
color: #ffbc39;
@@ -1442,7 +1468,7 @@ textarea {
}
.session-vc-section {
height: 565px;
height: 660px;
}
.session-tts-section::-webkit-scrollbar {
@@ -8,14 +8,17 @@ import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
import { Media } from "./pages/media";
import { LipsyncEditor } from "./pages/lipsync";
import { FooterNav } from "./nav/FooterNav";
import { TopNav } from "./nav/TopNav";
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SignupPage } from "./pages/signup/SignupPage";
import { Switch, Route } from "react-router-dom";
import {
Switch,
Route,
withRouter,
RouteComponentProps,
} from "react-router-dom";
import { TermsPage } from "./pages/about/terms_page/TermsPage";
import { TtsInferenceJob, W2lInferenceJob } from "../../App";
import { TtsModelDeletePage } from "./pages/tts/tts_model_delete/TtsModelDeletePage";
@@ -55,7 +58,6 @@ import { ModerationTtsCategoryEditPage } from "./pages/moderation/categories/Mod
import { ModerationCategoryDeletePage } from "./pages/moderation/categories/ModerationCategoryDeletePage";
import { TtsCategoryType } from "../../AppWrapper";
import { PatronPage } from "./pages/patrons/PatronPage";
import ScrollToTop from "./_common/ScrollToTop";
import { Language } from "@storyteller/components/src/i18n/Language";
import { VoiceCloneRequestPage } from "./pages/clone_voice_requests/VoiceCloneRequestPage";
import { VocodesPage } from "./pages/vocodes/VocodesPage";
@@ -80,6 +82,12 @@ import { UploadVoiceConversionModel } from "./pages/upload/UploadVoiceConversion
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import { CommunityCommissionsPage } from "./pages/contest/CommunityCommissionsPage";
import { ProductUsageInfoPage } from "./pages/product_usage_info/ProductUsageInfoPage";
import VcModelViewPage from "./pages/vc/vc_model_view/VcModelViewPage";
import VcModelEditPage from "./pages/vc/vc_model_edit/VcModelEditPage";
import VcModelDeletePage from "./pages/vc/vc_model_delete/VcModelDeletePage";
import SideNav from "components/layout/SideNav/SideNav";
import MobileMenu from "components/layout/MobileMenu/MobileMenu";
import { TopNav } from "./nav/TopNav";
import { TestingPage } from "./pages/testing/TestingPage";
interface Props {
@@ -176,8 +184,11 @@ interface Props {
interface State {}
class PageContainer extends React.Component<Props, State> {
constructor(props: Props) {
class PageContainer extends React.Component<
Props & RouteComponentProps,
State
> {
constructor(props: Props & RouteComponentProps) {
super(props);
this.state = {};
@@ -186,9 +197,23 @@ class PageContainer extends React.Component<Props, State> {
logout = () => {};
public render() {
const { location } = this.props;
const shouldRenderNavs = location.pathname === "/";
return (
<div id="main" className="mainwrap">
<div id="viewable">
<div id="wrapper" className="no-padding">
<div id="overlay"></div>
<SideNav
sessionWrapper={this.props.sessionWrapper}
logoutHandler={this.logout}
querySessionCallback={this.props.querySessionAction}
querySessionSubscriptionsCallback={
this.props.querySessionSubscriptionsAction
}
/>
{shouldRenderNavs && (
<TopNav
logoutHandler={this.logout}
sessionWrapper={this.props.sessionWrapper}
@@ -197,9 +222,10 @@ class PageContainer extends React.Component<Props, State> {
this.props.querySessionSubscriptionsAction
}
/>
)}
<ScrollToTop />
<div id="page-content-wrapper">
<MobileMenu sessionWrapper={this.props.sessionWrapper} />
<Switch>
<Route path="/firehose">
<FirehoseEventListPage
@@ -508,6 +534,27 @@ class PageContainer extends React.Component<Props, State> {
/>
</Route>
<Route path="/voice-conversion/:token/delete">
<VcModelDeletePage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/voice-conversion/:token/edit">
<VcModelEditPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/voice-conversion/:token">
<VcModelViewPage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
setMaybeSelectedInferenceJob={
this.props.maybeSelectedVoiceConversionModel
}
/>
</Route>
<Route path="/voice-conversion">
<VcModelListPage
sessionWrapper={this.props.sessionWrapper}
@@ -686,12 +733,10 @@ class PageContainer extends React.Component<Props, State> {
/>
</Route>
</Switch>
<FooterNav sessionWrapper={this.props.sessionWrapper} />
</div>
</div>
);
}
}
export { PageContainer };
export default withRouter(PageContainer);
@@ -159,7 +159,7 @@ function TopNav(props: Props) {
}
return (
<div>
<div className="d-none d-lg-block">
<nav
className="navbar navbar-expand-lg navbar-dark pt-3"
aria-label="Offcanvas navbar large"
@@ -13,12 +13,8 @@ import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
import { WebUrl } from "../../../../common/WebUrl";
import { faFileArrowUp, faSparkles } from "@fortawesome/pro-solid-svg-icons";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { FooterNav } from "v2/view/nav/FooterNav";
import Alert from "components/common/Alert/Alert";
// import {
// faFileArrowUp,
// faMicrophone,
// faRightLeft,
// } from "@fortawesome/pro-solid-svg-icons";
// import { Analytics } from "../../../../../common/Analytics";
interface Props {
@@ -535,6 +531,8 @@ function LandingPage(props: Props) {
/>
</div>
</div>
<FooterNav sessionWrapper={props.sessionWrapper} />
</div>
);
}
@@ -88,8 +88,8 @@ function LoginPage(props: Props) {
}
return (
<div>
<div className="container-panel pb-5 pt-lg-5 my-lg-5 login-panel">
<div className="pt-lg-5">
<div className="container-panel pb-5 pt-lg-5 login-panel">
<div className="panel p-3 p-lg-4 load-hidden mt-5 mt-lg-0 px-md-4">
<h1 className="panel-title fw-bold">Login</h1>
<div className="py-6">
@@ -346,8 +346,8 @@ function SignupPage(props: Props) {
}
return (
<div>
<div className="container-panel pb-5 pt-lg-5 my-lg-5 login-panel">
<div className="pt-lg-5">
<div className="container-panel pb-5 pt-lg-5 login-panel">
<div className="panel p-3 p-lg-4 load-hidden mt-5 mt-lg-0 px-md-4">
<h1 className="panel-title fw-bold ">
{t("account.SignUpPage.signUpTitle")}
@@ -0,0 +1,411 @@
import React, { useCallback, useEffect, useState } from "react";
import Panel from "components/common/Panel/Panel";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBarsStaggered,
faRightLeft,
faWaveformLines,
} from "@fortawesome/pro-solid-svg-icons";
import { SessionVoiceConversionResultsList } from "v2/view/_common/SessionVoiceConversionResultsList";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import UploadComponent from "./vc_model_list/components/UploadComponent";
import {
EnqueueVoiceConversion,
EnqueueVoiceConversionFrequencyMethod,
EnqueueVoiceConversionIsSuccess,
EnqueueVoiceConversionRequest,
} from "@storyteller/components/src/api/voice_conversion/EnqueueVoiceConversion";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import {
VoiceConversionModelListItem,
ListVoiceConversionModels,
} from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import { Analytics } from "common/Analytics";
import LoadingIcon from "components/common/FileActions/LoadingIcon";
import PitchEstimateMethodComponent from "./vc_model_list/components/PitchEstimateMethodComponent";
import PitchShiftComponent from "./vc_model_list/components/PitchShiftComponent";
import RecordComponent from "./vc_model_list/components/RecordComponent";
import { v4 as uuidv4 } from "uuid";
interface VcGenerateAudioPanelProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
voiceConversionModels: Array<VoiceConversionModelListItem>;
setVoiceConversionModels: (
ttsVoices: Array<VoiceConversionModelListItem>
) => void;
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
setMaybeSelectedVoiceConversionModel: (
maybeSelectedVoiceConversionModel: VoiceConversionModelListItem
) => void;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}
export default function VcGenerateAudioPanel(props: VcGenerateAudioPanelProps) {
const [convertLoading, setConvertLoading] = useState(false);
const [canConvert, setCanConvert] = useState(false);
const [mediaUploadToken, setMediaUploadToken] = useState<string | undefined>(
undefined
);
const [convertIdempotencyToken, setConvertIdempotencyToken] = useState(
uuidv4()
);
const [autoConvertF0, setAutoConvertF0] = useState(false);
const [maybeF0MethodOverride, setMaybeF0MethodOverride] = useState<
EnqueueVoiceConversionFrequencyMethod | undefined
>(undefined);
const [semitones, setSemitones] = useState(0);
// NB: Something of a UI hack here.
// The 3rd party microphone component doesn't let you clear it, so we emulate form clearing
// with this variable.
const [formIsCleared, setFormIsCleared] = useState(false);
let {
setVoiceConversionModels,
voiceConversionModels,
maybeSelectedVoiceConversionModel,
setMaybeSelectedVoiceConversionModel,
} = props;
const vcModelsLoaded = voiceConversionModels.length > 0;
const listModels = useCallback(async () => {
if (vcModelsLoaded) {
return; // Already queried.
}
const models = await ListVoiceConversionModels();
if (models) {
setVoiceConversionModels(models);
if (!maybeSelectedVoiceConversionModel && 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)];
}
setMaybeSelectedVoiceConversionModel(model);
}
}
}, [
setVoiceConversionModels,
maybeSelectedVoiceConversionModel,
setMaybeSelectedVoiceConversionModel,
vcModelsLoaded,
]);
useEffect(() => {
listModels();
}, [listModels]);
const changeConvertIdempotencyToken = () => {
setConvertIdempotencyToken(uuidv4());
};
const handleVoiceConversion = async () => {
if (
props.maybeSelectedVoiceConversionModel === undefined ||
mediaUploadToken === undefined
) {
return;
}
setConvertLoading(true);
let request: EnqueueVoiceConversionRequest = {
uuid_idempotency_token: convertIdempotencyToken,
voice_conversion_model_token:
props.maybeSelectedVoiceConversionModel.token,
source_media_upload_token: mediaUploadToken,
};
if (semitones !== 0) {
request.transpose = semitones;
}
if (maybeF0MethodOverride !== undefined) {
request.override_f0_method = maybeF0MethodOverride;
}
if (autoConvertF0) {
request.auto_predict_f0 = true;
}
Analytics.voiceConversionGenerate(
props.maybeSelectedVoiceConversionModel.token
);
let result = await EnqueueVoiceConversion(request);
if (EnqueueVoiceConversionIsSuccess(result)) {
props.enqueueInferenceJob(
result.inference_job_token,
FrontendInferenceJobType.VoiceConversion
);
}
setConvertLoading(false);
};
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
};
const handlePitchChange = (value: any) => {
setSemitones(value);
changeConvertIdempotencyToken();
};
const handlePitchMethodChange = (value: any) => {
setMaybeF0MethodOverride(value);
changeConvertIdempotencyToken();
};
const handleAutoF0Change = (ev: React.FormEvent<HTMLInputElement>) => {
const value = (ev.target as HTMLInputElement).checked;
setAutoConvertF0(value);
changeConvertIdempotencyToken();
};
const speakButtonClass = convertLoading
? "btn btn-primary w-100 disabled"
: "btn btn-primary w-100";
const enableConvertButton =
canConvert &&
mediaUploadToken !== undefined &&
props.maybeSelectedVoiceConversionModel !== undefined;
return (
<Panel padding mb>
<form onSubmit={handleFormSubmit}>
<div className="row g-5">
<div className="col-12 col-lg-6 d-flex flex-column gap-4">
<h4>
<FontAwesomeIcon icon={faWaveformLines} className="me-3" />
Use Voice
</h4>
<ul className="nav nav-tabs nav-vc" id="myTab" role="tablist">
<li className="nav-item w-100" role="presentation">
<button
className="nav-link active w-100 pt-0"
id="prerecorded-tab"
data-bs-toggle="tab"
data-bs-target="#prerecorded"
type="button"
role="tab"
aria-controls="prerecorded"
aria-selected="true"
>
Upload
</button>
</li>
<li className="nav-item w-100" role="presentation">
<button
className="nav-link w-100 pt-0"
id="recordaudio-tab"
data-bs-toggle="tab"
data-bs-target="#recordaudio"
type="button"
role="tab"
aria-controls="recordaudio"
aria-selected="false"
>
Microphone
</button>
</li>
</ul>
<div className="tab-content" id="myTabContent">
<div
className="tab-pane fade show active"
id="prerecorded"
role="tabpanel"
aria-labelledby="prerecorded-tab"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">Upload Input Audio</label>
<div className="d-flex flex-column gap-3 upload-component">
<UploadComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
/>
</div>
</div>
<div>
<label className="sub-title">Pitch Control</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0Checkbox"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0Checkbox"
>
Auto F0 (off for singing, on for speech)
</label>
</div>
</div>
</div>
<div>
<label className="sub-title">Convert Audio</label>
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon icon={faRightLeft} className="me-2" />
Convert
{convertLoading && <LoadingIcon />}
</button>
</div>
</div>
</div>
</div>
<div
className="tab-pane fade"
id="recordaudio"
role="tabpanel"
aria-labelledby="recordaudio-tab"
>
<div className="d-flex flex-column gap-4 h-100">
<div>
<label className="sub-title">Record Audio</label>
<div className="d-flex flex-column gap-3 upload-component">
<RecordComponent
setMediaUploadToken={setMediaUploadToken}
formIsCleared={formIsCleared}
setFormIsCleared={setFormIsCleared}
setCanConvert={setCanConvert}
changeConvertIdempotencyToken={
changeConvertIdempotencyToken
}
/>
</div>
</div>
<div>
<label className="sub-title">Pitch Control</label>
<div className="d-flex flex-column gap-3">
<div>
<PitchEstimateMethodComponent
pitchMethod={maybeF0MethodOverride}
onMethodChange={handlePitchMethodChange}
/>
</div>
<div>
<PitchShiftComponent
min={-36}
max={36}
step={1}
value={semitones}
onPitchChange={handlePitchChange}
/>
</div>
<div className="form-check">
<input
id="autoF0CheckboxMic"
className="form-check-input"
type="checkbox"
checked={autoConvertF0}
onChange={handleAutoF0Change}
/>
<label
className="form-check-label"
htmlFor="autoF0CheckboxMic"
>
Auto F0 (off for singing, on for speech)
</label>
</div>
</div>
</div>
<div>
<label className="sub-title">Convert Audio</label>
<div className="d-flex gap-3">
<button
className={speakButtonClass}
onClick={handleVoiceConversion}
type="submit"
disabled={!enableConvertButton}
>
<FontAwesomeIcon icon={faRightLeft} className="me-2" />
Convert
{convertLoading && <LoadingIcon />}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<h4 className="mb-4">
<FontAwesomeIcon icon={faBarsStaggered} className="me-3" />
Session V2V Results
</h4>
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
<SessionVoiceConversionResultsList
inferenceJobs={
props.inferenceJobsByCategory.get(
FrontendInferenceJobType.VoiceConversion
)!
}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
/>
</div>
</div>
</div>
</form>
</Panel>
);
}
@@ -0,0 +1,7 @@
import React from "react";
interface VoiceDetailsProps {}
export default function VoiceDetails(props: VoiceDetailsProps) {
return <div>VoiceDetails</div>;
}
@@ -0,0 +1,44 @@
import React from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
// import { useParams } from "react-router-dom";
import Panel from "components/common/Panel/Panel";
import { faSave, faTrash } from "@fortawesome/pro-solid-svg-icons";
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
import Button from "components/common/Button/Button";
import PageContainer from "components/common/PageContainer/PageContainer";
interface VcModelDeletePageProps {
sessionWrapper: SessionWrapper;
}
// const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// console.log(event.target.value);
// };
const handleSave = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("save");
};
const savebtn = <button className="btn btn-primary">Save Changes</button>;
export default function VcModelDeletePage(props: VcModelDeletePageProps) {
// let { token } = useParams() as { token: string };
return (
<PageContainer>
<PageHeaderModelView
title="Solid Snake"
subText="Solid Snake"
view="delete"
titleIcon={faTrash}
extras={savebtn}
modelType="V2V"
/>
<Panel padding>
<div className="d-flex flex-column gap-4">
<Button label="Save Changes" icon={faSave} onClick={handleSave} />
</div>
</Panel>
</PageContainer>
);
}
@@ -0,0 +1,72 @@
import React from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
// import { useParams } from "react-router-dom";
import { Input, TextArea, Select } from "components/common/Inputs/Inputs";
import Panel from "components/common/Panel/Panel";
import { faEye, faFilePen, faSave } from "@fortawesome/pro-solid-svg-icons";
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
import Button from "components/common/Button/Button";
import PageContainer from "components/common/PageContainer/PageContainer";
interface VcModelEditPageProps {
sessionWrapper: SessionWrapper;
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
const handleSave = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("save");
};
const savebtn = <button className="btn btn-primary">Save Changes</button>;
const visibility = [
{ value: "public", label: "Public" },
{ value: "hidden", label: "Hidden" },
];
export default function VcModelEditPage(props: VcModelEditPageProps) {
// let { token } = useParams() as { token: string };
return (
<PageContainer>
<PageHeaderModelView
title="Solid Snake"
subText="Solid Snake"
view="edit"
titleIcon={faFilePen}
extras={savebtn}
modelType="V2V"
/>
<Panel padding>
<div className="d-flex flex-column gap-4">
<Input
label="Name"
type="text"
placeholder="Model title"
onChange={handleChange}
/>
<TextArea
rows={3}
label="Description"
placeholder="Description"
onChange={() => {}}
/>
<Select
isSearchable={false}
icon={faEye}
defaultValue={visibility[0]}
options={visibility}
label="Visibility"
/>
<Button label="Save Changes" icon={faSave} onClick={handleSave} />
</div>
</Panel>
</PageContainer>
);
}
@@ -0,0 +1,185 @@
import React from "react";
import {
faEdit,
faEye,
faMemo,
faMemoCircleInfo,
faMessages,
faTrash,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Panel from "components/common/Panel/Panel";
import { Link } from "react-router-dom";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
import { CommentComponent } from "v2/view/_common/comments/CommentComponent";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { RatingButtons } from "v2/view/_common/ratings/RatingButtons";
import { RatingStats } from "v2/view/_common/ratings/RatingStats";
import ShareButton from "components/common/ShareButton/ShareButton";
import PageContainer from "components/common/PageContainer/PageContainer";
import VcGenerateAudioPanel from "../VcGenerateAudioPanel";
interface VcModelViewPageProps {
setMaybeSelectedInferenceJob: any;
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobsByCategory: any;
}
export default function VcModelViewPage(props: VcModelViewPageProps) {
// let { token } = useParams() as { token: string };
const title = "Solid Snake";
const subText = (
<div className="d-flex align-items-center gap-2">
<div>
<span className="badge-model badge-model-rvc fs-6">RVCv2</span>
</div>
<p>
V2V model by <Link to="/">Vegito1089</Link>
</p>
</div>
);
const tags = ["Speaking", "English", "Character", "Singing", "Spanish"];
let modelCreatorLink = <Link to="">Creator Name</Link>;
let modelTitle = title;
let modelDescription = "This is a description of the model";
let modelUseCount = 10000;
let modelLanguage = "English";
let modelType = "RVCv2";
let modelUploadDate = "2021-09-10T06:15:04Z";
let modelVisibility = (
<div>
<FontAwesomeIcon icon={faEye} className="me-2" />
Public
</div>
);
let modelCreatorBanned = "good standing";
let modelCreationIp = "0.0.0.0.0";
let modelUpdateIp = "0.0.0.0.0";
let frontPageFeatured = "no";
let moderatorDeletedAt = "not deleted";
let userDeletedAt = "not deleted";
const voiceDetails = [
{ label: "Creator", value: modelCreatorLink },
{ label: "Title", value: modelTitle },
{ label: "Use count", value: modelUseCount },
{ label: "Spoken language", value: modelLanguage },
{ label: "Model type", value: modelType },
{ label: "Upload date (UTC)", value: modelUploadDate },
{ label: "Visibility", value: modelVisibility },
];
const voiceDetailsModerator = [
{ label: "Creator is banned?", value: modelCreatorBanned },
{ label: "Creation IP address", value: modelCreationIp },
{ label: "Update IP address", value: modelUpdateIp },
{ label: "Mod deleted at (UTC)", value: moderatorDeletedAt },
{ label: "User deleted at (UTC)", value: userDeletedAt },
{ label: "Front page featured?", value: frontPageFeatured },
];
let ratingButtons = <></>;
if (props.sessionWrapper.isLoggedIn()) {
ratingButtons = (
<RatingButtons entity_type="v2v_model" entity_token="test" />
);
}
let ratingStats = (
<RatingStats positive_votes={100} negative_votes={0} total_votes={100} />
);
const shareUrl = window.location.href;
const shareButton = <ShareButton url={shareUrl} />;
return (
<PageContainer>
<PageHeaderModelView
title={title}
subText={subText}
tags={tags}
ratingBtn={ratingButtons}
ratingStats={ratingStats}
extras={shareButton}
/>
<VcGenerateAudioPanel
inferenceJobsByCategory={props.inferenceJobsByCategory}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
sessionWrapper={props.sessionWrapper}
setVoiceConversionModels={() => {}}
voiceConversionModels={[]}
setMaybeSelectedVoiceConversionModel={() => {}}
enqueueInferenceJob={() => {}}
inferenceJobs={[]}
/>
{modelDescription && (
<Panel padding mb>
<h4 className="mb-4">
<FontAwesomeIcon icon={faMemo} className="me-3" />
Description
</h4>
<p>{modelDescription}</p>
</Panel>
)}
<Panel padding mb>
<h4 className="mb-4">
<FontAwesomeIcon icon={faMemoCircleInfo} className="me-3" />
Voice Model Details
</h4>
<table className="table">
<tbody>
{voiceDetails.map((item, index) => (
<tr key={index}>
<th scope="row" className="fw-semibold">
{item.label}
</th>
<td>{item.value}</td>
</tr>
))}
{props.sessionWrapper.canBanUsers() &&
voiceDetailsModerator.map((item, index) => (
<tr key={index}>
<th scope="row" className="fw-semibold">
{item.label}
</th>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
{props.sessionWrapper.canBanUsers() && (
<div className="d-flex flex-column flex-md-row gap-3 mt-5">
<Link className={"btn btn-secondary w-100"} to="">
<FontAwesomeIcon icon={faEdit} className="me-2" />
Edit Model Details
</Link>
<Link className="btn btn-destructive w-100" to="">
<FontAwesomeIcon icon={faTrash} className="me-2" />
Delete Model
</Link>
</div>
)}
</Panel>
<Panel padding>
<h4 className="mb-4">
<FontAwesomeIcon icon={faMessages} className="me-3" />
Comments
</h4>
<CommentComponent
entityType="user"
entityToken="test"
sessionWrapper={props.sessionWrapper}
/>
</Panel>
</PageContainer>
);
}