diff --git a/src/netlify_configs/fakeyou.com/_redirects b/src/netlify_configs/fakeyou.com/_redirects index 7ca43b89ba..6f260714c5 100644 --- a/src/netlify_configs/fakeyou.com/_redirects +++ b/src/netlify_configs/fakeyou.com/_redirects @@ -60,6 +60,7 @@ /product-usage* /website/index.html 200 /profile* /website/index.html 200 /search* /website/index.html 200 +/seed-vc* /website/index.html 200 /set-username* /website/index.html 200 /signup* /website/index.html 200 /stream* /website/index.html 200 diff --git a/src/website/package.json b/src/website/package.json index ebb5622954..91a7a97da1 100644 --- a/src/website/package.json +++ b/src/website/package.json @@ -40,6 +40,7 @@ "local-storage": "^2.0.0", "moment": "^2.29.4", "react-easy-crop": "^5.0.7", + "react-helmet-async": "^2.0.5", "react-webcam": "^7.2.0", "universal-cookie": "^6.1.1", "yarn": "^1.22.19" diff --git a/src/website/packages/fakeyou.com/public/images/landing/select-seed-vc.webp b/src/website/packages/fakeyou.com/public/images/landing/select-seed-vc.webp new file mode 100644 index 0000000000..85d970ca68 Binary files /dev/null and b/src/website/packages/fakeyou.com/public/images/landing/select-seed-vc.webp differ diff --git a/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.scss b/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.scss new file mode 100644 index 0000000000..f5fdbb2e3a --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.scss @@ -0,0 +1,71 @@ +@import "scss/colours.scss"; + +.countdown-container { + border-radius: 12px; + padding: 20px; + width: 300px; + color: white; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + text-align: center; + + @media (max-width: 768px) { + padding: 20px 0px; + } + + .title { + font-size: 18px; + margin-bottom: 0px; + } + + .description { + font-size: 20px; + color: rgba($white, 0.6); + margin-bottom: 32px; + text-align: center; + font-weight: 500; + } + + .countdown-icon { + height: 80px; + width: 80px; + font-size: 36px; + background-color: $secondary; + border-radius: 16px; + display: flex; + justify-content: center; + align-items: center; + border: 3px solid rgba($white, 0.2); + } + + .countdown-wrapper { + display: flex; + justify-content: space-around; + width: 100%; + background-color: $secondary; + border-radius: 8px; + padding: 24px 10px; + border: 3px solid $primary; + user-select: none; + pointer-events: none; + + .countdown-item { + display: flex; + flex-direction: column; + align-items: center; + + .countdown-value { + font-size: 46px; + font-weight: bold; + margin-top: -10px; + } + + .countdown-label { + font-size: 16px; + color: rgba($white, 0.6); + } + } + } +} diff --git a/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.tsx b/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.tsx new file mode 100644 index 0000000000..a62f3b56fe --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/Countdown/Countdown.tsx @@ -0,0 +1,110 @@ +import { faArrowRight, IconDefinition } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button, Container, Panel } from "components/common"; +import { AppStateContext } from "components/providers/AppStateProvider"; +import React, { useState, useEffect, useContext } from "react"; +import "./Countdown.scss"; + +interface TimeLeft { + days?: number; + hours?: number; + minutes?: number; + seconds?: number; +} + +interface CountdownProps { + endDate: Date; + title: string; + description: string; + icon?: IconDefinition; +} + +export default function Countdown({ + endDate, + title, + description, + icon, +}: CountdownProps): JSX.Element { + const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()); + const [localEndTime, setLocalEndTime] = useState(""); + const { sessionWrapper } = useContext(AppStateContext); + + useEffect(() => { + updateLocalEndTime(); + + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft()); + }, 1000); + + return () => clearInterval(timer); + }); + + function calculateTimeLeft(): TimeLeft { + const difference = +endDate - +new Date(); + let timeLeft: TimeLeft = {}; + + if (difference > 0) { + timeLeft = { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + seconds: Math.floor((difference / 1000) % 60), + }; + } + + return timeLeft; + } + + function updateLocalEndTime() { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZoneName: "short", + }; + setLocalEndTime(endDate.toLocaleDateString("en-US", options)); + } + + return ( + + +
+ {icon && ( +
+ +
+ )} +

{title}

+

{description}

+
+ {Object.entries(timeLeft).map(([interval, value]) => ( +
+ {value} + {interval} +
+ ))} +
+
+ Countdown ends local time: {localEndTime} +
+ + {!sessionWrapper.isLoggedIn() && ( +
+
+
+ ); +} diff --git a/src/website/packages/fakeyou.com/src/components/common/Countdown/index.tsx b/src/website/packages/fakeyou.com/src/components/common/Countdown/index.tsx new file mode 100644 index 0000000000..fe3ad4c24c --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/Countdown/index.tsx @@ -0,0 +1 @@ +export { default } from "./Countdown"; diff --git a/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.scss b/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.scss new file mode 100644 index 0000000000..51dae4cde7 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.scss @@ -0,0 +1,42 @@ +@import "scss/colours.scss"; + +.faq-section { + background-color: rgba($secondary, 0.8); + border-radius: 12px; + padding-bottom: 2rem; + margin-bottom: 2rem; + border: 3px solid rgba($secondary, 0.8); + + .faq-item { + h3 { + color: lighten($primary, 5%); + font-size: 20px; + margin-bottom: 10px; + font-weight: bold; + } + + p { + color: rgba($white, 0.8); + font-size: 16px; + line-height: 1.6; + } + } +} + +@media (max-width: 768px) { + .faq-section { + padding: 20px; + padding-top: 2.5rem; + border-radius: 0px; + + .faq-item { + h3 { + font-size: 18px; + } + + p { + font-size: 14px; + } + } + } +} diff --git a/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.tsx b/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.tsx new file mode 100644 index 0000000000..b109b993cd --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/FAQSection/FAQSection.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Container, Panel } from "components/common"; +import "./FAQSection.scss"; +import { Helmet } from "react-helmet-async"; + +interface FAQItem { + question: string; + answer: string; +} + +interface FAQSectionProps { + title?: string; + faqItems: FAQItem[]; + className?: string; +} + +const FAQSection: React.FC = ({ + title = "Frequently Asked Questions", + faqItems, + className, +}) => { + const structuredData = { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: faqItems.map(item => ({ + "@type": "Question", + name: item.question, + acceptedAnswer: { + "@type": "Answer", + text: item.answer, + }, + })), + }; + + return ( + <> + + + + + +

{title}

+
+ {faqItems.map((item, index) => ( + +
+

{item.question}

+

{item.answer}

+
+ {index < faqItems.length - 1 &&
} +
+ ))} +
+
+
+ + ); +}; + +export default FAQSection; diff --git a/src/website/packages/fakeyou.com/src/components/common/FAQSection/index.tsx b/src/website/packages/fakeyou.com/src/components/common/FAQSection/index.tsx new file mode 100644 index 0000000000..10ab02388c --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/FAQSection/index.tsx @@ -0,0 +1 @@ +export { default } from "./FAQSection"; diff --git a/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.scss b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.scss new file mode 100644 index 0000000000..87f1238948 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.scss @@ -0,0 +1,67 @@ +@import "scss/colours.scss"; + +.how-to-use-section { + background-color: rgba($secondary, 0.8); + border-radius: 12px; + padding-bottom: 2rem; + margin-top: 3rem; + margin-bottom: 3rem; + border: 3px solid rgba($white, 0.2); + + .how-to-use-icon { + height: 24px; + width: 24px; + padding: 14px; + background-color: lighten($secondary, 10%); + color: $white; + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 24px; + border: 3px solid lighten($secondary, 20%); + + @media (max-width: 768px) { + margin-bottom: 16px; + height: 16px; + width: 16px; + padding: 8px; + border-radius: 8px; + } + } + + .how-to-use-item { + margin-bottom: 30px; + + h3 { + color: lighten($primary, 6%); + font-size: 20px; + margin-bottom: 12px; + font-weight: bold; + } + + p { + color: rgba($white, 0.8); + font-size: 16px; + line-height: 1.6; + } + } +} + +@media (max-width: 768px) { + .how-to-use-section { + padding: 20px; + padding-top: 2.5rem; + border-radius: 0px; + + .how-to-use-item { + h3 { + font-size: 18px; + } + + p { + font-size: 14px; + } + } + } +} diff --git a/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.tsx b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.tsx new file mode 100644 index 0000000000..ae769cb560 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/HowToUseSection.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Container } from "components/common"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import "./HowToUseSection.scss"; + +interface StepItem { + icon: IconDefinition; + title: string; + description: React.ReactNode; +} + +interface HowToUseSectionProps { + title?: string; + steps: StepItem[]; + className?: string; +} + +const HowToUseSection: React.FC = ({ + title = "How to Use", + steps, + className, +}) => { + return ( + +
+

{title}

+
+ {steps.map((step, index) => ( +
+ +

{step.title}

+

{step.description}

+
+ ))} +
+
+
+ ); +}; + +export default HowToUseSection; diff --git a/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/index.tsx b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/index.tsx new file mode 100644 index 0000000000..057dae0b48 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/common/HowToUseSection/index.tsx @@ -0,0 +1 @@ +export { default } from "./HowToUseSection"; diff --git a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx index c404548820..99309475e9 100644 --- a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx +++ b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx @@ -13,7 +13,7 @@ export default function AITools() { to?: string; externalLink?: string; title: string; - text: string; + text?: string; imgSrc?: string; imgAlt: string; videoSrc?: string; @@ -107,6 +107,13 @@ export default function AITools() { imgSrc: "/images/landing/select-f5-tts.webp", imgAlt: "Voice Cloning", }, + { + to: "/seed-vc", + title: "Seed-VC Voice Conversion", + text: "Zero-shot voice conversion", + imgSrc: "/images/landing/select-seed-vc.webp", + imgAlt: "Zero-shot Voice Conversion", + }, { to: "/voice-designer", title: t("productVdTitle"), @@ -130,7 +137,8 @@ export default function AITools() { currentPath.includes("/live-portrait") || currentPath.includes("/dev-lp") || currentPath.includes("/beta/") || - currentPath.includes("/face-animator") + currentPath.includes("/face-animator") || + currentPath.includes("/webcam-acting") ) { items.push({ externalLink: "https://discord.gg/fakeyou", @@ -178,6 +186,8 @@ export default function AITools() { // ); // } + const discordItem = items.find(item => item.imgAlt === "Discord Link"); + if (currentPath.includes("/tts")) { items = items.filter(item => item.to !== "/tts"); } @@ -186,19 +196,6 @@ export default function AITools() { items = items.filter(item => item.to !== "/voice-conversion"); } - if (currentPath.includes("/beta/")) { - items = items.filter( - item => - item.to !== "/tts" && - item.to !== "/voice-conversion" && - item.to !== "/voice-designer" && - item.to !== "/face-animator" && - item.to !== "/beta/2d-video-compositor" && - item.to !== "/beta/3d-video-compositor" && - item.to !== "/beta/webcam-acting" - ); - } - if (currentPath.includes("/tts")) { items = [items[0], items[1], items[2], items[3], items[4], items[5]]; } @@ -212,15 +209,15 @@ export default function AITools() { } if (currentPath.includes("/style-video")) { - items = [items[1], items[2], items[8]]; + items = [items[1], items[2], discordItem].filter(Boolean) as Item[]; } if (currentPath.includes("/face-animator")) { - items = [items[0], items[1], items[8]]; + items = [items[0], items[1], discordItem].filter(Boolean) as Item[]; } if (currentPath.includes("/webcam-acting")) { - items = [items[0], items[1], items[8]]; + items = [items[0], items[1], discordItem].filter(Boolean) as Item[]; } if ( @@ -229,7 +226,7 @@ export default function AITools() { currentPath.includes("/live-portrait") || currentPath.includes("/dev-lp") ) { - items = [items[0], items[2], items[8]]; + items = [items[0], items[2], discordItem].filter(Boolean) as Item[]; } return ; diff --git a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AIToolsRow.tsx b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AIToolsRow.tsx index 002a4c794d..fb5d4664f2 100644 --- a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AIToolsRow.tsx +++ b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AIToolsRow.tsx @@ -107,13 +107,17 @@ export function AIToolsItem({

{title}

-
{text}
+ {text && ( +
{text}
+ )} )} {!badgeContent && ( <>

{title}

-
{text}
+ {text && ( +
{text}
+ )} )} diff --git a/src/website/packages/fakeyou.com/src/index.tsx b/src/website/packages/fakeyou.com/src/index.tsx index da8e998167..33365e13af 100644 --- a/src/website/packages/fakeyou.com/src/index.tsx +++ b/src/website/packages/fakeyou.com/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom"; // import * as serviceWorker from "./serviceWorker"; // import AppTranslated from "./AppTranslated"; import { App } from "./App"; +import { HelmetProvider } from "react-helmet-async"; const designSystemClass = "fakeyou-refresh"; @@ -24,7 +25,9 @@ document.getElementsByTagName("html")[0].classList.add(designSystemClass); ReactDOM.render( - + + + , document.getElementById("root") ); diff --git a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx index b367377798..a914bf348e 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -445,6 +445,10 @@ const routes = [ path: "/f5-tts", component: lazy(() => import("./pages/f5_tts/F5TTS")), }, + { + path: "/seed-vc", + component: lazy(() => import("./pages/seed_vc/SeedVC")), + }, { path: "/style-video/:mediaToken?", component: lazy(() => import("./pages/style-video")), diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/seed_vc/SeedVC.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/seed_vc/SeedVC.tsx new file mode 100644 index 0000000000..998541a3f6 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/v2/view/pages/seed_vc/SeedVC.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { faWaveform } from "@fortawesome/pro-solid-svg-icons"; +import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle"; +import Countdown from "components/common/Countdown"; +import FAQSection from "components/common/FAQSection"; +// + +const endDate = new Date("2024-10-23T12:00:00-04:00"); + +export default function SeedVC() { + usePrefixedDocumentTitle("Seed-VC Zero-shot Voice Conversion"); + + return ( + <> +
+ +
+ + {/* */} + + + + ); +} + +const faqItems = [ + { + question: "What is Seed-VC Zero-shot Voice Conversion?", + answer: + "Seed-VC Zero-shot Voice Conversion is an advanced AI-powered tool designed for state-of-the-art voice conversion and singing voice conversion. It uses in-context learning to transform one voice into another without any prior training, making it an ideal choice for projects that require versatile voice modification.", + }, + { + question: "How does Seed-VC work?", + answer: + "Seed-VC leverages cutting-edge AI techniques, including zero-shot learning, to convert voices with just a short audio reference (1–30 seconds). It can adapt to both speaking and singing voices, creating natural-sounding results without the need for extensive data or training.", + }, + { + question: "What kind of voice quality can I expect from Seed-VC?", + answer: + "Seed-VC delivers high-quality, natural-sounding voice conversions, suitable for professional applications like dubbing, voice-overs, and even singing voice transformations. The AI is designed to retain the unique qualities of the target voice while maintaining clear and lifelike output.", + }, + { + question: "Can Seed-VC be used for voice-over and dubbing work?", + answer: + "Definitely! Seed-VC's zero-shot capabilities make it perfect for voice-over and dubbing. Whether you need to create different voices for various characters or adapt a voice to a new language, it can handle the task seamlessly. It also supports singing voice conversion, adding flexibility to musical projects.", + }, +]; + +// const howToUseSteps = [ +// { +// icon: faWaveformLines, +// title: "Step 1: Upload Your Audio", +// description: +// "In the panel above, start by adding a reference audio, either record your own voice or upload an audio file. This audio will be used by F5-TTS to clone the voice, enabling the generation of speech that closely resembles the reference voice. For optimal results, ensure the audio is clear and of high quality.", +// }, +// { +// icon: faTextSize, +// title: "Step 2: Enter Your Text", +// description: +// "Next, input the text you wish to convert into speech. This text will be synthesized using the voice from your reference audio, allowing you to create personalized audio content. Ensure your text is clear and concise for the best results.", +// }, +// { +// icon: faArrowDownToLine, +// title: "Step 3: Generate and Save", +// description: ( +// <> +// With your audio and text prepared, click 'Generate Speech' to activate +// F5-TTS and transform your text into lifelike speech. Once the process is +// complete, you can listen to the synthesized audio directly in the output +// panel above. If you're happy with the result, click the download button +// to save the audio file and use it in your projects! +// +// ), +// }, +// ]; diff --git a/src/website/yarn.lock b/src/website/yarn.lock index 1f96efa0c0..3e730a232a 100644 --- a/src/website/yarn.lock +++ b/src/website/yarn.lock @@ -4489,6 +4489,7 @@ __metadata: prettier: ^3.1.0 pretty-quick: ^3.1.3 react-easy-crop: ^5.0.7 + react-helmet-async: ^2.0.5 react-webcam: ^7.2.0 universal-cookie: ^6.1.1 yarn: ^1.22.19 @@ -12559,6 +12560,15 @@ __metadata: languageName: node linkType: hard +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: ^1.0.0 + checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14 + languageName: node + linkType: hard + "ip@npm:^1.1.0, ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -17925,6 +17935,13 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:^3.2.2": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 + languageName: node + linkType: hard + "react-fast-marquee@npm:^1.3.5": version: 1.3.5 resolution: "react-fast-marquee@npm:1.3.5" @@ -17945,6 +17962,19 @@ __metadata: languageName: node linkType: hard +"react-helmet-async@npm:^2.0.5": + version: 2.0.5 + resolution: "react-helmet-async@npm:2.0.5" + dependencies: + invariant: ^2.2.4 + react-fast-compare: ^3.2.2 + shallowequal: ^1.1.0 + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + checksum: cc2d13496f6fdee6b5f9472d3f7369db3e70e4fc1a55793708c2bbd90d48b0dedc725fd066f987c7a3d74b03a29bd5e65b9f40fa29bd8239e7cfb526aff4d4b6 + languageName: node + linkType: hard + "react-i18next@npm:^11.15.4": version: 11.15.4 resolution: "react-i18next@npm:11.15.4"