176 lines
6.7 KiB
TypeScript
176 lines
6.7 KiB
TypeScript
"use client";
|
|
import { useRef, useState, useEffect } from "react";
|
|
import { Button } from "./ui/button";
|
|
import { Play } from "lucide-react";
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "./ui/accordion";
|
|
import { Label } from "./ui/label";
|
|
|
|
export default function KokoroReader({ pages }: { pages: any[] }) {
|
|
// Create a reference to the worker object.
|
|
const worker = useRef<Worker>(null);
|
|
|
|
const [inputText, setInputText] = useState(
|
|
"Life is like a box of chocolates. You never know what you're gonna get."
|
|
);
|
|
const [selectedSpeaker, setSelectedSpeaker] = useState("af_heart");
|
|
|
|
const [voices, setVoices] = useState<any[]>([]);
|
|
const [status, setStatus] = useState<"ready" | "running" | null>(null);
|
|
const [error, setError] = useState(null);
|
|
const [loadingMessage, setLoadingMessage] = useState("Loading...");
|
|
|
|
const [results, setResults] = useState<{ text: string; src: string }[]>([]);
|
|
|
|
// We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
|
|
useEffect(() => {
|
|
// Create the worker if it does not yet exist.
|
|
console.log("Initializing worker...");
|
|
worker.current ??= new Worker("/workers/kokoro-worker.js", {
|
|
type: "module",
|
|
});
|
|
|
|
console.log("Worker initialized");
|
|
|
|
// Create a callback function for messages from the worker thread.
|
|
const onMessageReceived = (e: any) => {
|
|
switch (e.data.status) {
|
|
case "device":
|
|
setLoadingMessage(`Loading model (device="${e.data.device}")`);
|
|
break;
|
|
case "ready":
|
|
setStatus("ready");
|
|
setVoices(e.data.voices);
|
|
break;
|
|
case "error":
|
|
setError(e.data.data);
|
|
break;
|
|
case "complete":
|
|
const { audio, text } = e.data;
|
|
// Generation complete: re-enable the "Generate" button
|
|
setResults((prev) => [{ text, src: audio }, ...prev]);
|
|
setStatus("ready");
|
|
break;
|
|
}
|
|
};
|
|
|
|
console.log("onmessagereceived");
|
|
|
|
const onErrorReceived = (e: any) => {
|
|
console.error("Worker error:", e);
|
|
setError(e.message);
|
|
};
|
|
|
|
console.log("Attaching event listeners to worker");
|
|
|
|
// Attach the callback function as an event listener.
|
|
worker.current.addEventListener("message", onMessageReceived);
|
|
worker.current.addEventListener("error", onErrorReceived);
|
|
|
|
console.log(worker.current);
|
|
// Define a cleanup function for when the component is unmounted.
|
|
return () => {
|
|
worker.current!.removeEventListener("message", onMessageReceived);
|
|
worker.current!.removeEventListener("error", onErrorReceived);
|
|
};
|
|
}, []);
|
|
|
|
const handleSubmit = (e: any) => {
|
|
e.preventDefault();
|
|
setStatus("running");
|
|
|
|
worker.current!.postMessage({
|
|
type: "generate",
|
|
text: inputText.trim(),
|
|
voice: selectedSpeaker,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col items-center justify-center pt-4 relative overflow-hidden font-sans">
|
|
<div className="max-w-3xl w-full relative z-[2]">
|
|
<div className="items-center justify-center text-center">
|
|
<Button variant="ghost" size="icon" className="h-10 w-10">
|
|
<Play />
|
|
</Button>
|
|
</div>
|
|
|
|
<Accordion type="single" collapsible>
|
|
<AccordionItem value="item-1">
|
|
<AccordionTrigger className="text-white pb-2">
|
|
Settings
|
|
</AccordionTrigger>
|
|
<AccordionContent className="pb-2">
|
|
<Label>Voice</Label>
|
|
<select
|
|
value={selectedSpeaker}
|
|
onChange={(e) => setSelectedSpeaker(e.target.value)}
|
|
className="w-full bg-gray-700/50 backdrop-blur-sm border-2 border-gray-600 rounded-md text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
{Object.entries(voices).map(([id, voice]) => (
|
|
<option key={id} value={id}>
|
|
{voice.name} (
|
|
{voice.language === "en-us" ? "American" : "British"}{" "}
|
|
{voice.gender})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</div>
|
|
{/* <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700 rounded-lg p-6">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<textarea
|
|
placeholder="Enter text..."
|
|
value={inputText}
|
|
onChange={(e) => setInputText(e.target.value)}
|
|
className="w-full min-h-[100px] max-h-[300px] bg-gray-700/50 backdrop-blur-sm border-2 border-gray-600 rounded-xl resize-y text-gray-100 placeholder-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows={Math.min(8, inputText.split("\n").length)}
|
|
/>
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<select
|
|
value={selectedSpeaker}
|
|
onChange={(e) => setSelectedSpeaker(e.target.value)}
|
|
className="w-full bg-gray-700/50 backdrop-blur-sm border-2 border-gray-600 rounded-xl text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
{Object.entries(voices).map(([id, voice]) => (
|
|
<option key={id} value={id}>
|
|
{voice.name} (
|
|
{voice.language === "en-us" ? "American" : "British"}{" "}
|
|
{voice.gender})
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
type="submit"
|
|
className="inline-flex justify-center items-center px-6 py-2 text-lg font-semibold bg-gradient-to-t from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 transition-colors duration-300 rounded-xl text-white disabled:opacity-50"
|
|
disabled={status === "running" || inputText.trim() === ""}
|
|
>
|
|
{status === "running" ? "Generating..." : "Generate"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div> */}
|
|
|
|
{results.map((result, i) => (
|
|
<div key={i}>
|
|
<div className="text-white bg-gray-800/70 backdrop-blur-sm border border-gray-700 rounded-lg p-4 z-10">
|
|
<span className="absolute right-5 font-bold">
|
|
#{results.length - i}
|
|
</span>
|
|
<p className="mb-3 max-w-[95%]">{result.text}</p>
|
|
<audio controls src={result.src} className="w-full">
|
|
Your browser does not support the audio element.
|
|
</audio>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|