feat: refactor and optimize frontend with Next.js App Router, TypeScript, and Tailwind CSS

- Complete refactoring of old frontend into Next.js App Router workspace
- Redesigned sidebar collapsing animation with absolute toggle positioning
- Resolved visual canvas bleed transitions between light/dark themes
- Added custom dark theme variant for toggle switch buttons
- Implemented full localization across Indonesian, English, Spanish, Japanese, and Chinese
- Synchronized HTML document themes to apply dark mode styles to portals/overlays
This commit is contained in:
akukanara
2026-05-31 16:46:57 +07:00
Unverified
parent 19e5138525
commit 9d876de930
81 changed files with 10554 additions and 1583 deletions
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+5
View File
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+50
View File
@@ -0,0 +1,50 @@
const fs = require('fs');
const path = require('path');
const srcDir = path.join(__dirname, 'out');
const destDir = path.join(__dirname, '..', 'frontend');
function copyDirSync(src, dest) {
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (let entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirSync(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
function cleanDirSync(dir) {
if (!fs.existsSync(dir)) return;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (let entry of entries) {
if (entry.name === '.git' || entry.name === '.antigravitycli') continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
fs.rmSync(fullPath, { recursive: true, force: true });
} else {
fs.unlinkSync(fullPath);
}
}
}
try {
console.log('Wiping old static files in frontend/ ...');
cleanDirSync(destDir);
console.log('Copying static Next.js assets to frontend/ ...');
if (fs.existsSync(srcDir)) {
copyDirSync(srcDir, destDir);
console.log('Build output synchronization successful!');
} else {
console.error('Error: "out/" directory not found. Compile the Next.js build first.');
}
} catch (err) {
console.error('Synchronization failed:', err);
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+10
View File
@@ -0,0 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'export',
images: {
unoptimized: true,
},
};
export default nextConfig;
+6829
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "frontend-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && node copy-build.js",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"clsx": "^2.1.1",
"framer-motion": "^12.40.0",
"lucide-react": "^1.17.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+82
View File
@@ -0,0 +1,82 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--track-bg: #e4e4e7;
}
.dark {
--track-bg: #27272a;
}
@theme {
--color-background: #fafafa;
--color-foreground: #18181b;
--color-primary: #84cc16; /* Lime-500 */
--color-hover: #65a30d; /* Lime-600 */
--color-soft-accent: #d9f99d; /* Lime-200 */
--color-success: #10b981; /* Emerald-500 */
--color-text-primary: #18181b;
--color-text-secondary: #52525b;
--radius-lg: 1rem; /* rounded-2xl */
--radius-md: 0.75rem; /* rounded-xl */
--radius-sm: 0.5rem; /* rounded-lg */
}
/* Custom styling presets */
body {
background-color: #fafafa;
color: #18181b;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
overflow-x: hidden;
}
/* Glowing Aura Background */
.glow-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -10;
pointer-events: none;
background-image:
radial-gradient(circle at 10% 20%, rgba(132, 204, 22, 0.04) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.04) 0%, transparent 40%);
}
/* Glassmorphism panel overlay */
.glass-panel {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(24, 24, 27, 0.05);
}
/* Custom pulse animations for recording signals */
@keyframes signal-pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(132, 204, 22, 0.4);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 10px 4px rgba(132, 204, 22, 0.2);
}
}
.pulse-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 9999px;
background-color: #84cc16;
}
.pulse-indicator.active {
animation: signal-pulse 2s infinite ease-in-out;
}
+33
View File
@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "🎙️ ONNX VC - Real-Time AI Voice Changer",
description: "ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
import * as React from "react";
import { twMerge } from "tailwind-merge";
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
variant?: 'default' | 'secondary' | 'outline' | 'success' | 'warning' | 'danger' | 'info';
}
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant = "default", ...props }, ref) => {
return (
<span
ref={ref}
className={twMerge(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold select-none border tracking-wide uppercase",
variant === 'default' && "bg-[var(--accent-color)] text-white border-transparent",
variant === 'secondary' && "bg-[var(--accent-soft)] text-[var(--accent-text)] border-transparent",
variant === 'success' && "bg-[#10b981]/10 text-[#059669] border-[#10b981]/20",
variant === 'warning' && "bg-amber-500/10 text-amber-700 border-amber-500/20",
variant === 'danger' && "bg-red-500/10 text-red-700 border-red-500/20",
variant === 'info' && "bg-sky-500/10 text-sky-700 border-sky-500/20",
variant === 'outline' && "text-zinc-600 dark:text-zinc-400 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900",
className
)}
{...props}
/>
);
}
);
Badge.displayName = "Badge";
export { Badge };
export default Badge;
@@ -0,0 +1,40 @@
import * as React from "react";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'accent' | 'success' | 'danger';
size?: 'default' | 'sm' | 'lg' | 'icon';
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "primary", size = "default", ...props }, ref) => {
return (
<button
ref={ref}
className={twMerge(
"inline-flex items-center justify-center font-medium rounded-xl transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] cursor-pointer",
// Sizing
size === 'default' && "h-11 px-5 py-2.5 text-sm",
size === 'sm' && "h-9 px-3.5 text-xs rounded-lg",
size === 'lg' && "h-12 px-7 py-3 text-base rounded-2xl",
size === 'icon' && "h-10 w-10 rounded-xl",
// Variants
variant === 'primary' && "bg-[var(--accent-color)] hover:bg-[var(--accent-hover)] text-white shadow-sm shadow-lime-500/10 font-semibold",
variant === 'secondary' && "bg-[var(--accent-soft)] hover:bg-[var(--accent-soft)]/80 text-[var(--accent-text)] font-semibold",
variant === 'accent' && "bg-zinc-900 dark:bg-[var(--accent-color)] hover:bg-zinc-850 dark:hover:bg-[var(--accent-hover)] text-white dark:text-zinc-950 shadow-sm font-semibold",
variant === 'outline' && "border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-zinc-700 dark:text-zinc-300",
variant === 'ghost' && "hover:bg-zinc-50 dark:hover:bg-zinc-800 text-zinc-700 dark:text-zinc-300",
variant === 'success' && "bg-[#10b981] hover:bg-[#059669] text-white shadow-sm font-semibold",
variant === 'danger' && "bg-red-500 hover:bg-red-600 text-white shadow-sm font-semibold",
className
)}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button };
export default Button;
@@ -0,0 +1,78 @@
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import { twMerge } from "tailwind-merge";
export interface DialogProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
className?: string;
}
export const Dialog: React.FC<DialogProps> = ({
isOpen,
onClose,
title,
children,
className
}) => {
// Close dialog on escape key press
React.useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop Overlay */}
<motion.div
className="fixed inset-0 bg-zinc-950/40 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* Modal content */}
<motion.div
className={twMerge(
"relative bg-white dark:bg-zinc-900 w-full max-w-lg rounded-2xl p-6 shadow-xl border border-zinc-200/50 dark:border-zinc-800/80 z-10 overflow-hidden flex flex-col max-h-[85vh] text-zinc-800 dark:text-zinc-100",
className
)}
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{/* Header */}
<div className="flex justify-between items-center mb-4 pb-2 border-b border-zinc-100 dark:border-zinc-800">
<h2 className="text-lg font-bold text-zinc-900 dark:text-zinc-100 leading-none">
{title}
</h2>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-zinc-400 dark:text-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--accent-color)] cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Body */}
<div className="overflow-y-auto pr-1 flex-1">
{children}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
};
export default Dialog;
@@ -0,0 +1,50 @@
import * as React from "react";
import { twMerge } from "tailwind-merge";
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
options: { value: string | number; label: string }[];
}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, options, ...props }, ref) => {
return (
<div className="relative w-full">
<select
ref={ref}
className={twMerge(
"w-full h-11 px-4 text-sm bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--accent-color)] focus:border-[var(--accent-color)] transition-all cursor-pointer appearance-none text-zinc-800 dark:text-zinc-100 pr-10",
className
)}
{...props}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{/* Custom Arrow Icon */}
<div className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 flex items-center justify-center text-zinc-400">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
);
}
);
Select.displayName = "Select";
export { Select };
export default Select;
@@ -0,0 +1,46 @@
import * as React from "react";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
value: number;
min: number;
max: number;
step?: number;
onValueChange?: (val: number) => void;
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
({ className, value, min, max, step = 1, onValueChange, ...props }, ref) => {
const percentage = ((value - min) / (max - min)) * 100;
return (
<div className="relative w-full flex items-center select-none group">
<input
type="range"
ref={ref}
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onValueChange?.(parseFloat(e.target.value))}
className={twMerge(
"w-full h-2 rounded-lg bg-zinc-200 dark:bg-zinc-800 appearance-none cursor-pointer outline-none focus:outline-none",
// custom thumb styles
"[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-[var(--accent-color)] [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:active:scale-110",
"[&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-[var(--accent-color)] [&::-moz-range-thumb]:shadow-md [&::-moz-range-thumb]:transition-all [&::-moz-range-thumb]:active:scale-110",
className
)}
style={{
background: `linear-gradient(to right, var(--accent-color) 0%, var(--accent-color) ${percentage}%, var(--track-bg, #e4e4e7) ${percentage}%, var(--track-bg, #e4e4e7) 100%)`
}}
{...props}
/>
</div>
);
}
);
Slider.displayName = "Slider";
export { Slider };
export default Slider;
@@ -0,0 +1,52 @@
import * as React from "react";
import { twMerge } from "tailwind-merge";
export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
checked: boolean;
onCheckedChange?: (checked: boolean) => void;
label?: string;
variant?: 'default' | 'dark';
}
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
({ className, checked, onCheckedChange, label, variant = 'default', ...props }, ref) => {
return (
<label className="flex items-center gap-3 cursor-pointer select-none group">
<div className="relative">
<input
type="checkbox"
ref={ref}
checked={checked}
onChange={(e) => onCheckedChange?.(e.target.checked)}
className="sr-only"
{...props}
/>
<div
className={twMerge(
"w-10 h-6 bg-zinc-200 dark:bg-zinc-800 rounded-full transition-all duration-200 group-focus-within:ring-2 group-focus-within:ring-offset-2",
variant === 'dark' ? "group-focus-within:ring-zinc-500" : "group-focus-within:ring-[var(--accent-color)]",
checked && (variant === 'dark' ? "bg-zinc-800 dark:bg-zinc-700 border border-zinc-700/50 dark:border-zinc-650/80" : "bg-[var(--accent-color)]"),
className
)}
/>
<div
className={twMerge(
"absolute left-0.5 top-0.5 w-5 h-5 bg-white dark:bg-zinc-900 rounded-full transition-all duration-200 shadow-sm border border-zinc-200/50 dark:border-zinc-800/80",
checked && "translate-x-4",
checked && (variant === 'dark' ? "border-zinc-500" : "border-[var(--accent-color)]")
)}
/>
</div>
{label && (
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300 select-none">
{label}
</span>
)}
</label>
);
}
);
Switch.displayName = "Switch";
export { Switch };
export default Switch;
@@ -0,0 +1,126 @@
'use client';
import React, { useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useWaveformCanvas } from '../../../hooks/useWaveformCanvas';
import { usePictureInPicture } from '../../../hooks/usePictureInPicture';
import { Button } from '../../../components/ui/button';
import { Maximize2, MonitorOff, Activity } from 'lucide-react';
import { translations, Language } from '../../../utils/translations';
interface WaveformPanelProps {
title: string;
buffer: React.MutableRefObject<Float32Array>;
strokeColor: string;
isTalking?: boolean;
lineWidth?: number;
traceFade?: number;
isDark?: boolean;
lang?: Language;
}
export const WaveformPanel: React.FC<WaveformPanelProps> = ({
title,
buffer,
strokeColor,
isTalking = false,
lineWidth = 2,
traceFade = 0.4,
isDark = false,
lang = 'en',
}) => {
const t = translations[lang];
// Background clear color: white for light mode, dark zinc-950 for dark mode
const canvasBgColor = isDark ? `rgba(9, 9, 11, ${traceFade})` : `rgba(255, 255, 255, ${traceFade})`;
const { canvasRef, updateData } = useWaveformCanvas({
strokeColor,
fillColor: canvasBgColor, // dynamic trail alpha blending
scaleAmplitude: 2.0,
lineWidth,
});
const { togglePip, isPipActive, isSupported } = usePictureInPicture();
// Draw buffer updates
useEffect(() => {
let active = true;
const loop = () => {
if (!active) return;
updateData(buffer.current);
requestAnimationFrame(loop);
};
loop();
return () => {
active = false;
};
}, [buffer, updateData]);
return (
<motion.div
className="bg-white dark:bg-zinc-900 border border-zinc-200/50 dark:border-zinc-800/80 shadow-sm rounded-2xl p-5 relative overflow-hidden flex flex-col h-full transition-colors"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-2">
{isTalking && (
<motion.span
className="w-2.5 h-2.5 rounded-full bg-[var(--accent-color)]"
animate={{ scale: [1, 1.4, 1], opacity: [1, 0.4, 1] }}
transition={{ repeat: Infinity, duration: 1.2 }}
/>
)}
<h3 className="font-bold text-zinc-800 dark:text-zinc-200 text-xs tracking-wider uppercase flex items-center gap-1.5">
<Activity className="w-4 h-4 text-[var(--accent-color)]" />
{title}
</h3>
</div>
{isSupported && (
<Button
variant="outline"
size="sm"
onClick={() => togglePip(canvasRef.current)}
className="text-[10px] h-8 px-2.5 flex items-center gap-1 border-zinc-200 dark:border-zinc-800 hover:bg-[var(--accent-soft)] dark:hover:bg-[var(--accent-soft)]/20 hover:text-[var(--accent-text)] dark:hover:text-[var(--accent-color)] text-zinc-700 dark:text-zinc-300 transition-colors"
>
{isPipActive ? (
<>
<MonitorOff className="w-3 h-3" />
{t.pipClose}
</>
) : (
<>
<Maximize2 className="w-3 h-3" />
{t.pipStream}
</>
)}
</Button>
)}
</div>
<div className="relative flex-1 min-h-[140px] bg-zinc-50 dark:bg-zinc-950 border border-zinc-100 dark:border-zinc-900 rounded-xl overflow-hidden shadow-inner transition-colors">
<canvas
ref={canvasRef}
className="w-full h-full block cursor-pointer"
/>
<AnimatePresence>
{isTalking && (
<motion.div
className="absolute top-2.5 right-2.5 bg-[var(--accent-color)]/90 text-white text-[9px] font-bold px-2 py-0.5 rounded-full shadow-sm uppercase tracking-wider backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
{t.activeSignal}
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
};
export default WaveformPanel;
+320
View File
@@ -0,0 +1,320 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { AudioConfig, ConnectionStatus, HardwareDevice } from '../types/audio';
export const useAudioPipeline = (
wsUrl: string,
config: AudioConfig,
onConfigSync: (sr: number, list: HardwareDevice[]) => void
) => {
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
const [rtt, setRtt] = useState<number | null>(null);
const [processingTime, setProcessingTime] = useState<number | null>(null);
const [isTalking, setIsTalking] = useState<boolean>(false);
const [isStreaming, setIsStreaming] = useState<boolean>(false);
const [playOutput, setPlayOutput] = useState<boolean>(true);
const socketRef = useRef<WebSocket | null>(null);
const audioCtxRef = useRef<AudioContext | null>(null);
const micStreamRef = useRef<MediaStream | null>(null);
const micSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
const processorRef = useRef<ScriptProcessorNode | null>(null);
const sampleRateRef = useRef<number>(40000);
// High-performance canvas rolling buffers
const inputDisplayBuf = useRef<Float32Array>(new Float32Array(4096));
const outputDisplayBuf = useRef<Float32Array>(new Float32Array(4096));
const micAccumulator = useRef<Float32Array>(new Float32Array(0));
// Playback scheduling & timing
const sentTimestamps = useRef<{ id: number; sent: number }[]>([]);
const nextPlaybackTime = useRef<number>(0);
const outputChunkQueue = useRef<{ data: Float32Array; startTime: number }[]>([]);
// Function to stringify and sync configs
const sendConfig = useCallback(() => {
const socket = socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN) return;
socket.send(JSON.stringify({
type: 'config',
model_name: config.model_name,
device: config.device,
f0_method: config.f0_method,
f0_up_key: config.f0_up_key,
noise_gate: config.noise_gate,
input_gain: config.input_gain,
output_gain: config.output_gain,
input_sr: audioCtxRef.current ? audioCtxRef.current.sampleRate : 44100,
routing_mode: config.routing_mode,
input_device: config.input_device,
output_device: config.output_device,
chunk_size: config.chunk_size
}));
}, [config]);
// Decodes array buffers from Python server
const handleServerAudio = useCallback((arrayBuffer: ArrayBuffer) => {
if (!audioCtxRef.current) return;
const now = performance.now();
if (sentTimestamps.current.length > 0) {
const oldest = sentTimestamps.current.shift();
if (oldest) {
setRtt(Math.round(now - oldest.sent));
}
}
const payload = new Float32Array(arrayBuffer);
const procTime = payload[0];
const pcmData = payload.subarray(1);
setProcessingTime(Math.max(0, Math.round(procTime)));
const ctx = audioCtxRef.current;
const audioBuf = ctx.createBuffer(1, pcmData.length, sampleRateRef.current);
audioBuf.getChannelData(0).set(pcmData);
const source = ctx.createBufferSource();
source.buffer = audioBuf;
// Only route node to speaker output if user didn't mute local listening
if (playOutput) {
source.connect(ctx.destination);
}
// Precise schedule timelines
const currentTime = ctx.currentTime;
const duration = audioBuf.duration;
const adaptiveBuf = Math.min(duration * 2.5, 0.50);
if (nextPlaybackTime.current < currentTime) {
nextPlaybackTime.current = currentTime + adaptiveBuf;
} else if (nextPlaybackTime.current > currentTime + duration * 5.0) {
nextPlaybackTime.current = currentTime + adaptiveBuf; // Latency Buster
}
const startSchedule = nextPlaybackTime.current;
source.start(startSchedule);
nextPlaybackTime.current += duration;
// Queue for syncing waveform outputs
outputChunkQueue.current.push({ data: pcmData, startTime: startSchedule });
while (outputChunkQueue.current.length > 0) {
const c = outputChunkQueue.current[0];
if (c.startTime + c.data.length / sampleRateRef.current < ctx.currentTime - 2.0) {
outputChunkQueue.current.shift();
} else break;
}
// Push output PCM samples to rolling display buffers
const size = 4096;
const display = outputDisplayBuf.current;
if (pcmData.length >= size) {
display.set(pcmData.slice(pcmData.length - size));
} else {
display.copyWithin(0, pcmData.length);
display.set(pcmData, size - pcmData.length);
}
}, [playOutput]);
const disconnect = useCallback(() => {
if (socketRef.current) {
try {
socketRef.current.close();
} catch (e) {}
socketRef.current = null;
}
setStatus('disconnected');
}, []);
const connect = useCallback(() => {
disconnect();
setStatus('connecting');
try {
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
setStatus('connected');
socketRef.current = ws;
sendConfig();
};
ws.onclose = () => {
setStatus('disconnected');
socketRef.current = null;
};
ws.onerror = () => {
setStatus('disconnected');
socketRef.current = null;
};
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
try {
const data = JSON.parse(event.data);
if (data.type === 'config_success') {
sampleRateRef.current = data.target_sr;
} else if (data.type === 'init_devices') {
onConfigSync(data.target_sr || 40000, data.devices || []);
} else if (data.type === 'visualizer') {
// Hardware mode visualizer data stream
inputDisplayBuf.current.set(new Float32Array(data.input));
outputDisplayBuf.current.set(new Float32Array(data.output));
}
} catch (e) {
console.error('WS JSON parse error:', e);
}
} else if (event.data instanceof ArrayBuffer) {
handleServerAudio(event.data);
}
};
} catch (e) {
console.error('WS Connection failed:', e);
setStatus('disconnected');
}
}, [wsUrl, sendConfig, handleServerAudio, onConfigSync, disconnect]);
const stopStream = useCallback(() => {
setIsStreaming(false);
setIsTalking(false);
if (config.routing_mode === 'hardware') {
const socket = socketRef.current;
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'config',
routing_mode: 'browser' // tells server hardware stream to stop
}));
}
}
if (micStreamRef.current) {
micStreamRef.current.getTracks().forEach(t => t.stop());
micStreamRef.current = null;
}
if (micSourceRef.current) {
micSourceRef.current.disconnect();
micSourceRef.current = null;
}
if (processorRef.current) {
processorRef.current.disconnect();
processorRef.current = null;
}
micAccumulator.current = new Float32Array(0);
setRtt(null);
setProcessingTime(null);
}, [config.routing_mode]);
const startStream = useCallback(async () => {
if (config.routing_mode === 'hardware') {
setIsStreaming(true);
sendConfig();
return;
}
if (!audioCtxRef.current) {
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)({
latencyHint: 'interactive',
});
}
const ctx = audioCtxRef.current;
if (ctx.state === 'suspended') {
await ctx.resume();
}
try {
micStreamRef.current = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
micSourceRef.current = ctx.createMediaStreamSource(micStreamRef.current);
processorRef.current = ctx.createScriptProcessor(4096, 1, 1);
processorRef.current.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
// Update input waveform display buffer
const display = inputDisplayBuf.current;
display.copyWithin(0, inputData.length);
display.set(inputData, display.length - inputData.length);
// Append to local accumulator
const nextAcc = new Float32Array(micAccumulator.current.length + inputData.length);
nextAcc.set(micAccumulator.current);
nextAcc.set(inputData, micAccumulator.current.length);
micAccumulator.current = nextAcc;
const size = config.chunk_size;
while (micAccumulator.current.length >= size) {
const chunk = micAccumulator.current.slice(0, size);
micAccumulator.current = micAccumulator.current.slice(size);
// Simple RMS for Voice Activity Badge
let sum = 0;
for (let i = 0; i < chunk.length; i++) sum += chunk[i] * chunk[i];
const rms = Math.sqrt(sum / chunk.length);
setIsTalking(rms > 0.005);
// Stream raw float PCM bytes
const ws = socketRef.current;
if (ws && ws.readyState === WebSocket.OPEN) {
const time = performance.now();
sentTimestamps.current.push({ id: time, sent: time });
ws.send(chunk.buffer);
}
}
};
micSourceRef.current.connect(processorRef.current);
processorRef.current.connect(ctx.destination);
nextPlaybackTime.current = 0;
setIsStreaming(true);
} catch (e) {
console.error('Failed to start microphone streaming:', e);
alert('Microphone access failed: ' + (e instanceof Error ? e.message : String(e)));
stopStream();
}
}, [config.routing_mode, config.chunk_size, sendConfig, stopStream]);
// Sync config whenever React props config changes
useEffect(() => {
sendConfig();
}, [config, sendConfig]);
// Lifecycle cleanups
useEffect(() => {
return () => {
disconnect();
stopStream();
if (audioCtxRef.current) {
audioCtxRef.current.close().catch(() => {});
}
};
}, [disconnect, stopStream]);
return {
status,
rtt,
processingTime,
isTalking,
isStreaming,
playOutput,
setPlayOutput,
connect,
disconnect,
startStream,
stopStream,
inputBuffer: inputDisplayBuf,
outputBuffer: outputDisplayBuf
};
};
export default useAudioPipeline;
@@ -0,0 +1,64 @@
import { useEffect, useRef, useCallback } from 'react';
export interface ShortcutBinding {
keys: string; // e.g. "Control+k", " ", "m", "alt+1"
description: string;
action: () => void;
}
export const useKeyboardShortcuts = (bindings: ShortcutBinding[], enabled: boolean = true) => {
const bindingsRef = useRef<ShortcutBinding[]>(bindings);
useEffect(() => {
bindingsRef.current = bindings;
}, [bindings]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!enabled) return;
// Avoid hijacking keystrokes when editing inputs
const active = document.activeElement;
if (active) {
const name = active.tagName.toLowerCase();
if (name === 'input' || name === 'textarea' || active.getAttribute('contenteditable') === 'true') {
return;
}
}
const pressedKey = e.key.toLowerCase();
const isCtrl = e.ctrlKey || e.metaKey;
const isAlt = e.altKey;
const isShift = e.shiftKey;
for (const binding of bindingsRef.current) {
const keys = binding.keys.toLowerCase().split('+');
const requiresCtrl = keys.includes('control') || keys.includes('ctrl');
const requiresAlt = keys.includes('alt');
const requiresShift = keys.includes('shift');
const baseKey = keys.filter(k => !['control', 'ctrl', 'alt', 'shift'].includes(k))[0];
const matchesCtrl = requiresCtrl ? isCtrl : !isCtrl;
const matchesAlt = requiresAlt ? isAlt : !isAlt;
const matchesShift = requiresShift ? isShift : !isShift;
const normalizedBaseKey = baseKey === 'space' ? ' ' : baseKey;
const matchesBase = pressedKey === normalizedBaseKey;
if (matchesCtrl && matchesAlt && matchesShift && matchesBase) {
e.preventDefault();
binding.action();
break;
}
}
}, [enabled]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
};
export default useKeyboardShortcuts;
@@ -0,0 +1,71 @@
import { useState, useCallback, useRef, useEffect } from 'react';
export const usePictureInPicture = () => {
const [isPipActive, setIsPipActive] = useState(false);
const videoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
if (typeof window === 'undefined') return;
const video = document.createElement('video');
video.muted = true;
video.playsInline = true;
videoRef.current = video;
const handleLeavePip = () => {
setIsPipActive(false);
};
video.addEventListener('leavepictureinpicture', handleLeavePip);
return () => {
video.removeEventListener('leavepictureinpicture', handleLeavePip);
if (document.pictureInPictureElement === video) {
document.exitPictureInPicture().catch(() => {});
}
};
}, []);
const togglePip = useCallback(async (canvas: HTMLCanvasElement | null) => {
if (!canvas || !videoRef.current) return;
const video = videoRef.current;
try {
if (document.pictureInPictureElement === video) {
await document.exitPictureInPicture();
setIsPipActive(false);
} else {
// Capture a stream of the Canvas at 30 fps
const stream = canvas.captureStream
? canvas.captureStream(30)
: (canvas as any).mozCaptureStream
? (canvas as any).mozCaptureStream(30)
: null;
if (!stream) {
throw new Error("Canvas.captureStream() is not supported on this browser.");
}
video.srcObject = stream;
await new Promise<void>((resolve) => {
video.onloadedmetadata = () => {
video.play().then(() => resolve());
};
});
await video.requestPictureInPicture();
setIsPipActive(true);
}
} catch (error) {
console.error("Picture-in-Picture failed:", error);
alert("Picture-in-Picture error: " + (error instanceof Error ? error.message : String(error)));
}
}, []);
const isSupported = typeof window !== 'undefined' && 'pictureInPictureEnabled' in document;
return { togglePip, isPipActive, isSupported };
};
export default usePictureInPicture;
@@ -0,0 +1,101 @@
import { useEffect, useRef, useCallback } from 'react';
interface UseWaveformCanvasOptions {
strokeColor: string;
fillColor?: string;
scaleAmplitude?: number;
lineWidth?: number;
}
export const useWaveformCanvas = (options: UseWaveformCanvasOptions) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameRef = useRef<number | null>(null);
const bufferRef = useRef<Float32Array | null>(null);
const updateData = useCallback((data: Float32Array) => {
bufferRef.current = data;
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const handleResize = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
const baseColor = (options.fillColor || 'rgba(10, 10, 10, 0.4)').replace(/[\d.]+\)$/, '1)');
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
handleResize();
window.addEventListener('resize', handleResize);
// Initial canvas clear with solid color
const baseColor = (options.fillColor || 'rgba(10, 10, 10, 0.4)').replace(/[\d.]+\)$/, '1)');
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const draw = () => {
const width = canvas.width;
const height = canvas.height;
const dataArray = bufferRef.current;
// Dark transparent fill for trace/fade visual trails
ctx.fillStyle = options.fillColor || 'rgba(10, 10, 10, 0.4)';
ctx.fillRect(0, 0, width, height);
if (dataArray && dataArray.length > 0) {
ctx.lineWidth = (options.lineWidth ?? 2) * window.devicePixelRatio;
ctx.strokeStyle = options.strokeColor;
ctx.lineJoin = 'round';
ctx.beginPath();
const sliceWidth = width / dataArray.length;
let x = 0;
for (let i = 0; i < dataArray.length; i++) {
const v = dataArray[i] * (options.scaleAmplitude ?? 1.5);
const y = (v * (height / 2)) + (height / 2);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
}
// Draw subtle zero amplitude baseline
ctx.strokeStyle = options.fillColor?.includes('255') ? 'rgba(0, 0, 0, 0.06)' : 'rgba(255, 255, 255, 0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
animationFrameRef.current = requestAnimationFrame(draw);
};
animationFrameRef.current = requestAnimationFrame(draw);
return () => {
window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [options.strokeColor, options.fillColor, options.scaleAmplitude, options.lineWidth]);
return { canvasRef, updateData };
};
export default useWaveformCanvas;
+23
View File
@@ -0,0 +1,23 @@
export interface AudioConfig {
model_name: string;
device: 'cpu' | 'cuda' | 'dml';
f0_method: 'pm' | 'dio' | 'harvest' | 'rmvpe';
f0_up_key: number;
noise_gate: number;
input_gain: number;
output_gain: number;
input_sr: number;
routing_mode: 'browser' | 'hardware';
input_device: number | null;
output_device: number | null;
chunk_size: number;
}
export interface HardwareDevice {
id: number;
name: string;
max_input_channels: number;
max_output_channels: number;
}
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
+592
View File
@@ -0,0 +1,592 @@
export type Language = 'en' | 'ja' | 'zh' | 'es' | 'id';
export const languages: { code: Language; label: string; flag: string }[] = [
{ code: 'en', label: 'English', flag: '🇺🇸' },
{ code: 'id', label: 'Bahasa Indonesia', flag: '🇮🇩' },
{ code: 'ja', label: '日本語', flag: '🇯🇵' },
{ code: 'zh', label: '简体中文', flag: '🇨🇳' },
{ code: 'es', label: 'Español', flag: '🇪🇸' }
];
export const translations = {
en: {
appTitle: "🎙️ ONNX VC",
appSubtitle: "Low-latency real-time AI voice conversion powered by ONNX Runtime acceleration.",
wsServerUrl: "WebSocket Server URL",
wsPlaceholder: "ws://localhost:8765",
connectionStatus: "Connection Status",
disconnected: "Disconnected",
connecting: "Connecting",
connected: "Connected",
connect: "Connect Server",
disconnect: "Disconnect Server",
startChanger: "Start Voice Changer",
stopChanger: "Stop Voice Changer",
listeningActive: "Listening: ACTIVE",
listeningMute: "Listening: MUTED",
// Tabs
tabDashboard: "Workspace",
tabModel: "Model Settings",
tabDsp: "Audio DSP",
tabShortcuts: "Shortcuts",
// Model Config
modelConfigTitle: "Model & Device Configuration",
quickPresets: "Quick Presets (Performance Profile)",
latencyPreset: "⚡ Instant Response (PM)",
qualityPreset: "🎙️ High Fidelity (RMVPE)",
selectModel: "Select Character Model (RVC ONNX)",
executionProvider: "Execution Provider (GPU Acceleration)",
routingMode: "Audio Routing Mode",
clientMode: "Client Mode (Browser Streaming)",
serverMode: "Server Mode (Direct Sounddevice)",
serverInput: "Server Input Microphone",
serverOutput: "Server Output Speaker",
pitchMethod: "Pitch Extraction Method",
transpose: "Transpose (Pitch Modifier)",
transposeMale: "-24 (Male Pitch)",
transposeNormal: "0 (Original)",
transposeFemale: "+24 (Female/Anime Pitch)",
// DSP
dspTitle: "Audio Processing Settings (DSP)",
noiseGate: "Noise Gate (Threshold)",
noiseGateSens: "-60 dB (Sensitive)",
noiseGateDefault: "-40 dB (Default)",
noiseGateStrict: "-10 dB (Strict)",
inputGain: "Input Gain (Microphone)",
outputGain: "Output Gain (AI Volume)",
noiseCancel: "Noise Cancellation (Filter)",
noiseCancelDesc: "Filters browser echo & background hum",
bufferSize: "Buffer Size (Chunk Size - Latency vs Stability)",
// Visualizers
visualizerTitle: "Real-Time Audio Visualizer",
micSignal: "Microphone Input Signal",
aiSignal: "AI Voice Output Signal",
activeSignal: "Active Signal",
pipStream: "PiP Waveform",
pipClose: "Close PiP",
// HUD
hudLatency: "RTT Latency",
hudInference: "Inference Speed",
hudDetector: "Voice Detector",
hudTalking: "Speaking",
hudSilent: "Silent",
hudSr: "Model Frequency",
hudHelp: "Press ? to view hotkeys menu",
// Shortcuts Dialog
shortcutsTitle: "Keyboard Shortcuts Guide",
shortcutsDesc: "Use these keyboard shortcuts to navigate the dashboard without a mouse:",
shortcutsClose: "Close",
shortcutConnect: "Connect / Disconnect WebSocket Server",
shortcutStream: "Start / Stop AI Voice Changer",
shortcutMute: "Mute / Unmute Output Audio Local Listening",
shortcutPreset1: "Apply Preset: Instant Response (PM)",
shortcutPreset2: "Apply Preset: High Fidelity (RMVPE)",
shortcutHelp: "Open / Close Shortcuts Help Dialog",
// Premium layouts
characterCardTitle: "Active Voice Character",
characterAvatarDesc: "Currently loaded voice weight profile.",
welcomeBack: "Real-Time Audio Control Center",
currentLang: "Language",
themeSettings: "Interface Theme & Accent",
themeMode: "Theme Mode",
themeDark: "Dark Mode",
themeLight: "Light Mode",
accentColorLabel: "Global Accent Color",
tabCredits: "Credits",
creditsTitle: "💖 Open Source Credits",
creditsDescription: "ONNX VC is made possible thanks to the following incredible open-source projects and libraries:",
liveTuningTitle: "Live Settings Tuning",
customCanvasTitle: "Custom Canvas Visualizer",
showMicInput: "Show Mic Input",
showAiOutput: "Show AI Output",
lineWidthLabel: "Line Width",
traceDecayLabel: "Trace Decay (Fading)",
inputLineColorLabel: "Input Line Color",
outputLineColorLabel: "Output Line Color",
creditCreatorTitle: "Creator & Integrator",
creditNeuralTitle: "Neural Conversion",
creditEngineTitle: "Inference Engine",
creditPitchTitle: "Pitch Extraction",
creditPipelineTitle: "Streaming Pipeline",
creditFrameworkTitle: "Frontend Framework",
creditDesignTitle: "Design & Animation",
creditCreatorDesc: "Creators of the ONNX VC client interface and low-latency audio control workspace integration layer.",
creditNeuralDesc: "The core neural network architecture for real-time voice feature retrieval and vocal conversion.",
creditEngineDesc: "Cross-platform accelerator for machine learning models running on CPU, NVIDIA CUDA, and DirectML GPU backends.",
creditPitchDesc: "Robust Minimum Vocal Pitch Estimation model providing highly accurate vocals pitch tracking under ambient noise.",
creditPipelineDesc: "High-speed binary data transfer loops passing raw PCM float32 frames between the client browser and backend.",
creditFrameworkDesc: "Modern web framework compiling React client-side components to statically optimized static exports.",
creditDesignDesc: "Utility-first styling utility and fluid declarative animation libraries for interactive visual user interfaces."
},
id: {
appTitle: "🎙️ ONNX VC",
appSubtitle: "Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan akselerasi ONNX Runtime.",
wsServerUrl: "URL Server WebSocket",
wsPlaceholder: "ws://localhost:8765",
connectionStatus: "Status Koneksi",
disconnected: "Terputus",
connecting: "Menghubungkan",
connected: "Terhubung",
connect: "Hubungkan Server",
disconnect: "Putuskan Server",
startChanger: "Mulai Mengubah Suara",
stopChanger: "Hentikan Mengubah",
listeningActive: "Mendengarkan: AKTIF",
listeningMute: "Mendengarkan: SENYAP",
// Tabs
tabDashboard: "Ruang Kerja",
tabModel: "Setelan Model",
tabDsp: "Audio DSP",
tabShortcuts: "Shortcut",
// Model Config
modelConfigTitle: "Konfigurasi Model & Perangkat",
quickPresets: "Quick Presets (Profil Performa)",
latencyPreset: "⚡ Respon Kilat (PM)",
qualityPreset: "🎙️ Kualitas Tinggi (RMVPE)",
selectModel: "Pilih Model Suara (RVC ONNX)",
executionProvider: "Execution Provider (Akselerasi GPU)",
routingMode: "Mode Routing Audio",
clientMode: "Client Mode (Streaming Browser)",
serverMode: "Server Mode (Direct Sounddevice)",
serverInput: "Input Mikrofon Server",
serverOutput: "Output Speaker Server",
pitchMethod: "Metode Deteksi Nada (Pitch Extraction)",
transpose: "Transpose (Pengubah Nada)",
transposeMale: "-24 (Pria Berat)",
transposeNormal: "0 (Asli)",
transposeFemale: "+24 (Wanita/Anime)",
// DSP
dspTitle: "Pemrosesan Audio (DSP)",
noiseGate: "Noise Gate (Threshold)",
noiseGateSens: "-60 dB (Sensitif)",
noiseGateDefault: "-40 dB (Default)",
noiseGateStrict: "-10 dB (Ketat)",
inputGain: "Input Gain (Microphone)",
outputGain: "Output Gain (Volume AI)",
noiseCancel: "Peredam Bising (Noise Cancel)",
noiseCancelDesc: "Filter gema & desah di browser",
bufferSize: "Ukuran Buffer (Chunk Size - Latensi vs Stabilitas)",
// Visualizers
visualizerTitle: "Visualisasi Waveform Live",
micSignal: "Sinyal Mikrofon (Input)",
aiSignal: "Hasil AI Voice (Output)",
activeSignal: "Signal Aktif",
pipStream: "PiP Waveform",
pipClose: "Batal PiP",
// HUD
hudLatency: "Latensi Bulat (RTT)",
hudInference: "Kecepatan Inference",
hudDetector: "Detektor Suara",
hudTalking: "Bicara",
hudSilent: "Berdiam",
hudSr: "Frekuensi Model",
hudHelp: "Tekan ? untuk melihat menu hotkey",
// Shortcuts Dialog
shortcutsTitle: "Panduan Keyboard Shortcut",
shortcutsDesc: "Gunakan keyboard shortcuts berikut untuk navigasi dashboard tanpa mouse:",
shortcutsClose: "Tutup",
shortcutConnect: "Hubungkan / Putuskan Server WebSocket",
shortcutStream: "Mulai / Hentikan Pengubah Suara AI",
shortcutMute: "Bungkam / Dengarkan Audio Output Lokal",
shortcutPreset1: "Terapkan Profil: Respon Kilat (PM)",
shortcutPreset2: "Terapkan Profil: Kualitas Tinggi (RMVPE)",
shortcutHelp: "Buka / Tutup Dialog Panduan Shortcut",
// Premium layouts
characterCardTitle: "Karakter Suara Aktif",
characterAvatarDesc: "Profil bobot suara yang sedang dimuat saat ini.",
welcomeBack: "Pusat Kontrol Audio Real-Time",
currentLang: "Bahasa",
themeSettings: "Tema Antarmuka & Aksen",
themeMode: "Mode Tema",
themeDark: "Mode Gelap",
themeLight: "Mode Terang",
accentColorLabel: "Warna Aksen Global",
tabCredits: "Kredit Open Source",
creditsTitle: "💖 Kredit Lisensi & Open Source",
creditsDescription: "ONNX VC dimungkinkan berkat proyek dan pustaka open source luar biasa berikut:",
liveTuningTitle: "Setelan Cepat Pemrosesan",
customCanvasTitle: "Kustomisasi Canvas Visualizer",
showMicInput: "Tampilkan Input Mic",
showAiOutput: "Tampilkan Output AI",
lineWidthLabel: "Ketebalan Garis",
traceDecayLabel: "Intensitas Ekor (Trail Fading)",
inputLineColorLabel: "Warna Garis Input",
outputLineColorLabel: "Warna Garis Output",
creditCreatorTitle: "Pencipta & Integrator",
creditNeuralTitle: "Konversi Neural",
creditEngineTitle: "Mesin Inferensi",
creditPitchTitle: "Ekstraksi Nada Vokal",
creditPipelineTitle: "Streaming Pipeline",
creditFrameworkTitle: "Framework Frontend",
creditDesignTitle: "Desain & Animasi",
creditCreatorDesc: "Pengembang antarmuka audio ONNX VC dan pengintegrasi workspace kontrol audio real-time berlatensi ultra-rendah.",
creditNeuralDesc: "Kerangka kerja pengubah suara berbasis AI yang menggunakan fitur retrieval untuk transfer karakter suara berlatensi rendah.",
creditEngineDesc: "Mesin akselerasi inferensi model lintas platform untuk CPU, CUDA GPU, dan Windows DirectML GPU.",
creditPitchDesc: "Model deteksi pitch vokal berkinerja tinggi yang presisi terhadap desau latar belakang.",
creditPipelineDesc: "Pipa transfer data audio biner mentah PCM float32 yang berjalan lancar antara peramban dan server python.",
creditFrameworkDesc: "Kerangka kerja aplikasi web terstruktur yang dikompilasi ke statik HTML ekspor.",
creditDesignDesc: "Mesin animasi layout deklaratif dan utilitas CSS presisi untuk tampilan premium."
},
ja: {
appTitle: "🎙️ ONNX VC",
appSubtitle: "ONNX Runtime高速化による低遅延リアルタイムAI音声変換システム。",
wsServerUrl: "WebSocketサーバーURL",
wsPlaceholder: "ws://localhost:8765",
connectionStatus: "接続状態",
disconnected: "切断",
connecting: "接続中...",
connected: "接続完了",
connect: "サーバー接続",
disconnect: "接続解除",
startChanger: "音声変換開始",
stopChanger: "音声変換停止",
listeningActive: "モニター音:ON",
listeningMute: "モニター音:OFF",
// Tabs
tabDashboard: "ワークスペース",
tabModel: "モデル設定",
tabDsp: "オーディオDSP",
tabShortcuts: "ショートカット",
// Model Config
modelConfigTitle: "モデルとデバイスの構成",
quickPresets: "クイックプリセット (パフォーマンス)",
latencyPreset: "⚡ 低遅延優先 (PM)",
qualityPreset: "🎙️ 高音質優先 (RMVPE)",
selectModel: "キャラクターモデルの選択 (RVC ONNX)",
executionProvider: "実行プロバイダー (GPUアクセラレーション)",
routingMode: "音声ルーティングモード",
clientMode: "クライアントモード (ブラウザ再生)",
serverMode: "サーバーモード (ハードウェア直結)",
serverInput: "サーバー入力マイク",
serverOutput: "サーバー出力スピーカー",
pitchMethod: "ピッチ検出アルゴリズム",
transpose: "ピッチ変換 (トランスポーズ)",
transposeMale: "-24 (男声向け)",
transposeNormal: "0 (原音)",
transposeFemale: "+24 (女声/アニメ声)",
// DSP
dspTitle: "オーディオ処理設定 (DSP)",
noiseGate: "ノイズゲート (閾値)",
noiseGateSens: "-60 dB (高感度)",
noiseGateDefault: "-40 dB (推奨)",
noiseGateStrict: "-10 dB (厳格)",
inputGain: "入力ゲイン (マイク)",
outputGain: "出力ゲイン (AI音量)",
noiseCancel: "ノイズキャンセリング",
noiseCancelDesc: "ブラウザのエコーと環境音を除去します",
bufferSize: "バッファサイズ (遅延時間 vs 安定性)",
// Visualizers
visualizerTitle: "リアルタイム波形表示",
micSignal: "マイク入力信号",
aiSignal: "AI音声出力信号",
activeSignal: "音声検出中",
pipStream: "PiP波形ウィンドウ",
pipClose: "PiPを閉じる",
// HUD
hudLatency: "応答速度 (RTT)",
hudInference: "推論速度",
hudDetector: "音声検出",
hudTalking: "発話中",
hudSilent: "無音",
hudSr: "モデルサンプリングレート",
hudHelp: "?キーでショートカットヘルプを表示",
// Shortcuts Dialog
shortcutsTitle: "キーボードショートカット一覧",
shortcutsDesc: "キーボードを使ってマウスなしで素早く操作できます:",
shortcutsClose: "閉じる",
shortcutConnect: "WebSocketサーバーの接続 / 切断",
shortcutStream: "AI音声変換の開始 / 停止",
shortcutMute: "ローカル出力のミュート / 解除",
shortcutPreset1: "プリセット適用:低遅延優先 (PM)",
shortcutPreset2: "プリセット適用:高音質優先 (RMVPE)",
shortcutHelp: "ショートカット一覧の表示 / 非表示",
// Premium layouts
characterCardTitle: "現在のボイスモデル",
characterAvatarDesc: "現在ロードされている音声のキャラクタープロファイルです。",
welcomeBack: "リアルタイムオーディオコントロールセンター",
currentLang: "言語",
themeSettings: "テーマとアクセント",
themeMode: "テーマモード",
themeDark: "ダークモード",
themeLight: "ライトモード",
accentColorLabel: "グローバルアクセントカラー",
tabCredits: "オープンソース",
creditsTitle: "💖 オープンソースクレジット",
creditsDescription: "ONNX VCは、以下の素晴らしいオープンソースプロジェクトとライブラリのおかげで実現しました。",
liveTuningTitle: "常用パラメータ微調整",
customCanvasTitle: "カスタムビジュアライザ",
showMicInput: "マイク入力を表示",
showAiOutput: "AI出力を表示",
lineWidthLabel: "線の太さ",
traceDecayLabel: "残像フェード率",
inputLineColorLabel: "入力線の色",
outputLineColorLabel: "出力線の色",
creditCreatorTitle: "開発・統合元",
creditNeuralTitle: "ニューラル音声変換",
creditEngineTitle: "推推論エンジン",
creditPitchTitle: "ピッチ検出",
creditPipelineTitle: "ストリーミング・パイプライン",
creditFrameworkTitle: "フロントエンドフレームワーク",
creditDesignTitle: "デザインとアニメーション",
creditCreatorDesc: "ONNX VCクライアントインターフェースおよび超低遅延リアルタイムオーディオ制御ワークスペースの統合開発チーム。",
creditNeuralDesc: "リアルタイムの音声特徴抽出および声質変換のためのコアニューラルネットワークアーキテクチャ。",
creditEngineDesc: "CPU、NVIDIA CUDA、およびWindows DirectML GPUバックエンド上で動作する、クロスプラットフォームの推論高速化エンジン。",
creditPitchDesc: "周囲のノイズ下でも高精度にボーカルのピッチ追跡を行うことができる高性能ピッチ推定モデル。",
creditPipelineDesc: "ブラウザクライアントとPythonサーバー間で生のPCM float32フレームを高速に送受信するバイナリデータパイプライン。",
creditFrameworkDesc: "Reactクライアントコンポーネントを静的に最適化されたHTMLにエクスポートするモダンウェブフレームワーク。",
creditDesignDesc: "インタラクティブで高品質なUIデザインのための、ユーティリティ優先CSSおよび宣言的アニメーションライブラリ。"
},
zh: {
appTitle: "🎙️ ONNX VC",
appSubtitle: "基于 ONNX 运行时加速的低延迟实时 AI 变声器系统。",
wsServerUrl: "WebSocket 服务器地址",
wsPlaceholder: "ws://localhost:8765",
connectionStatus: "连接状态",
disconnected: "已断开",
connecting: "连接中...",
connected: "已连接",
connect: "连接服务器",
disconnect: "断开连接",
startChanger: "开启变声",
stopChanger: "停止变声",
listeningActive: "声音监听:开启",
listeningMute: "声音监听:静音",
// Tabs
tabDashboard: "控制工作台",
tabModel: "模型设置",
tabDsp: "音频 DSP",
tabShortcuts: "快捷键",
// Model Config
modelConfigTitle: "变声模型与硬件设备配置",
quickPresets: "快速预设 (性能配置)",
latencyPreset: "⚡ 极速响应 (PM)",
qualityPreset: "🎙️ 高清音质 (RMVPE)",
selectModel: "选择声音模型 (RVC ONNX)",
executionProvider: "运行加速提供商 (GPU 加速)",
routingMode: "音频路由模式",
clientMode: "客户端模式 (浏览器音频流转换)",
serverMode: "服务器模式 (直连服务端硬件)",
serverInput: "服务器输入麦克风",
serverOutput: "服务器输出扬声器",
pitchMethod: "基频检测算法 (Pitch)",
transpose: "变调参数 (Transpose)",
transposeMale: "-24 (男声声调)",
transposeNormal: "0 (原音)",
transposeFemale: "+24 (女声/动漫声调)",
// DSP
dspTitle: "音频效果器配置 (DSP)",
noiseGate: "噪声门限阈值 (Noise Gate)",
noiseGateSens: "-60 dB (灵敏)",
noiseGateDefault: "-40 dB (默认)",
noiseGateStrict: "-10 dB (严格)",
inputGain: "输入增益 (麦克风音量)",
outputGain: "输出增益 (变声后音量)",
noiseCancel: "回声抑噪过滤",
noiseCancelDesc: "过滤浏览器的回声和杂音",
bufferSize: "缓冲区大小 (延迟时间 vs 稳定性)",
// Visualizers
visualizerTitle: "实时音频波形图",
micSignal: "麦克风输入波形",
aiSignal: "AI变声输出波形",
activeSignal: "正在输入",
pipStream: "画中画波形图",
pipClose: "关闭画中画",
// HUD
hudLatency: "双向延迟 (RTT)",
hudInference: "推理用时",
hudDetector: "声控指示器",
hudTalking: "检测到讲话",
hudSilent: "静音中",
hudSr: "模型音频采样率",
hudHelp: "按 ? 键打开快捷键指南",
// Shortcuts Dialog
shortcutsTitle: "键盘快捷键指南",
shortcutsDesc: "使用键盘快捷键可以在没有鼠标的情况下极速控制工作台:",
shortcutsClose: "关闭",
shortcutConnect: "连接 / 断开 WebSocket 服务器",
shortcutStream: "开启 / 停止 AI 变声器",
shortcutMute: "静音 / 开启本地输出监听",
shortcutPreset1: "加载预设:极速响应 (PM)",
shortcutPreset2: "加载预设:高清音质 (RMVPE)",
shortcutHelp: "打开 / 关闭快捷键帮助面板",
// Premium layouts
characterCardTitle: "当前声音人物",
characterAvatarDesc: "当前正在承载的音频权重包与神经网络特征。",
welcomeBack: "实时音频变声控制台",
currentLang: "语言",
themeSettings: "界面主题与强调色",
themeMode: "主题模式",
themeDark: "深色模式",
themeLight: "浅色模式",
accentColorLabel: "全局强调颜色",
tabCredits: "开源鸣谢",
creditsTitle: "💖 开源软件鸣谢",
creditsDescription: "ONNX VC 的诞生离不开以下优秀的开源项目与函数库的支持:",
liveTuningTitle: "常用变声微调",
customCanvasTitle: "画布自定设置",
showMicInput: "显示麦克风输入",
showAiOutput: "显示AI变声输出",
lineWidthLabel: "线条宽度",
traceDecayLabel: "余晖消退率 (渐变)",
inputLineColorLabel: "输入线颜色",
outputLineColorLabel: "输出线颜色",
creditCreatorTitle: "核心集成开发商",
creditNeuralTitle: "声线转换算法",
creditEngineTitle: "深度学习推理引擎",
creditPitchTitle: "基频音高提取",
creditPipelineTitle: "数据流通通道",
creditFrameworkTitle: "前端应用框架",
creditDesignTitle: "界面设计与动效",
creditCreatorDesc: "ONNX VC 客户端界面设计与超低延迟音频控制工作台的集成开发者。",
creditNeuralDesc: "基于检索的神经网络架构,用于实现低延迟的实时声音特征提取与音色转换。",
creditEngineDesc: "跨平台的机器学习模型推理加速引擎,支持 CPU、NVIDIA CUDA 以及 Windows DirectML GPU 后端。",
creditPitchDesc: "高性能人声基频检测模型,在背景嘈杂的环境下仍能提供极高精度的音高跟踪。",
creditPipelineDesc: "在浏览器客户端与 Python 服务端之间高速传输原始 PCM Float32 音频帧的双向二进制数据通道。",
creditFrameworkDesc: "现代网页开发框架,支持将 React 客户端组件编译并打包为高度优化的静态资源导出。",
creditDesignDesc: "功能类优先 CSS 框架与流式声明式动画库,用以打造流畅的高级交互式视觉界面。"
},
es: {
appTitle: "🎙️ ONNX VC",
appSubtitle: "Modulador de voz por IA en tiempo real y baja latencia acelerado por ONNX Runtime.",
wsServerUrl: "URL del Servidor WebSocket",
wsPlaceholder: "ws://localhost:8765",
connectionStatus: "Estado de la Conexión",
disconnected: "Desconectado",
connecting: "Conectando...",
connected: "Conectado",
connect: "Conectar Servidor",
disconnect: "Desconectar Servidor",
startChanger: "Iniciar Modulador",
stopChanger: "Detener Modulador",
listeningActive: "Escucha: ACTIVA",
listeningMute: "Escucha: SILENCIADO",
// Tabs
tabDashboard: "Espacio Trabajo",
tabModel: "Ajustes Modelo",
tabDsp: "Audio DSP",
tabShortcuts: "Atajos Teclado",
// Model Config
modelConfigTitle: "Configuración de Modelo y Dispositivo",
quickPresets: "Ajustes Rápidos (Perfil de Rendimiento)",
latencyPreset: "⚡ Respuesta Instantánea (PM)",
qualityPreset: "🎙️ Alta Fidelidad (RMVPE)",
selectModel: "Seleccionar Modelo de Voz (RVC ONNX)",
executionProvider: "Proveedor de Ejecución (Aceleración GPU)",
routingMode: "Modo de Ruta de Audio",
clientMode: "Modo Cliente (Streaming en Navegador)",
serverMode: "Modo Servidor (Sounddevice Directo)",
serverInput: "Micrófono de Entrada del Servidor",
serverOutput: "Altavoz de Salida del Servidor",
pitchMethod: "Método de Extracción de Tono",
transpose: "Transposición (Modificador de Tono)",
transposeMale: "-24 (Tono Grave Masculino)",
transposeNormal: "0 (Original)",
transposeFemale: "+24 (Tono Agudo/Anime)",
// DSP
dspTitle: "Configuración de Procesamiento de Audio (DSP)",
noiseGate: "Puerta de Ruido (Umbral)",
noiseGateSens: "-60 dB (Sensible)",
noiseGateDefault: "-40 dB (Predeterminado)",
noiseGateStrict: "-10 dB (Estricto)",
inputGain: "Ganancia de Entrada (Micrófono)",
outputGain: "Ganancia de Salida (Volumen IA)",
noiseCancel: "Cancelación de Ruido (Filtro)",
noiseCancelDesc: "Filtra el eco y el zumbido de fondo",
bufferSize: "Tamaño de Búfer (Tamaño de Chunk - Latencia vs Estabilidad)",
// Visualizers
visualizerTitle: "Visualizador de Ondas de Audio",
micSignal: "Señal de Entrada del Micrófono",
aiSignal: "Señal de Salida de Voz IA",
activeSignal: "Señal Activa",
pipStream: "Forma de Onda PiP",
pipClose: "Cerrar PiP",
// HUD
hudLatency: "Latencia RTT",
hudInference: "Velocidad de Inferencia",
hudDetector: "Detector de Voz",
hudTalking: "Hablando",
hudSilent: "Silencio",
hudSr: "Frecuencia del Modelo",
hudHelp: "Presione ? para ver el menú de atajos",
// Shortcuts Dialog
shortcutsTitle: "Guía de Atajos de Teclado",
shortcutsDesc: "Utilice los siguientes atajos para controlar el panel de control sin el mouse:",
shortcutsClose: "Cerrar",
shortcutConnect: "Conectar / Desconectar Servidor WebSocket",
shortcutStream: "Iniciar / Detener Modulador de Voz IA",
shortcutMute: "Silenciar / Activar Escucha Local de Salida",
shortcutPreset1: "Cargar Ajuste: Respuesta Instantánea (PM)",
shortcutPreset2: "Cargar Ajuste: Alta Fidelidad (RMVPE)",
shortcutHelp: "Abrir / Cerrar Diálogo de Ayuda de Atajos",
// Premium layouts
characterCardTitle: "Voz del Personaje Activo",
characterAvatarDesc: "Perfil de pesos de voz cargado actualmente.",
welcomeBack: "Centro de Control de Audio en Tiempo Real",
currentLang: "Idioma",
themeSettings: "Tema de Interfaz y Acento",
themeMode: "Modo de Tema",
themeDark: "Modo Oscuro",
themeLight: "Modo Claro",
accentColorLabel: "Color de Acento Global",
tabCredits: "Créditos",
creditsTitle: "💖 Créditos de Código Abierto",
creditsDescription: "ONNX VC es posible gracias a los siguientes increíbles proyectos y bibliotecas de código abierto:",
liveTuningTitle: "Ajustes en Vivo",
customCanvasTitle: "Ajustes de Canvas",
showMicInput: "Mostrar Entrada Mic",
showAiOutput: "Mostrar Salida IA",
lineWidthLabel: "Grosor de Línea",
traceDecayLabel: "Decaimiento del Trazo",
inputLineColorLabel: "Color de Línea de Entrada",
outputLineColorLabel: "Color de Línea de Salida",
creditCreatorTitle: "Creador e Integrador",
creditNeuralTitle: "Conversión Neuronal",
creditEngineTitle: "Motor de Inferencia",
creditPitchTitle: "Extracción de Tono",
creditPipelineTitle: "Línea de Transmisión",
creditFrameworkTitle: "Marco Frontend",
creditDesignTitle: "Diseño y Animación",
creditCreatorDesc: "Creadores de la interfaz de cliente ONNX VC e integradores del entorno de control de audio en tiempo real.",
creditNeuralDesc: "Arquitectura central de red neuronal para la extracción de características de voz y conversión vocal.",
creditEngineDesc: "Acelerador multiplataforma de inferencia de modelos de IA para CPU, GPU CUDA y GPU DirectML de Windows.",
creditPitchDesc: "Modelo robusto de estimación de tono mínimo para un seguimiento de tono vocal de alta precisión.",
creditPipelineDesc: "Tubería binaria de alta velocidad para la transferencia de tramas PCM float32 nativas entre el cliente y el servidor.",
creditFrameworkDesc: "Marco de desarrollo web moderno que compila componentes de React para exportaciones estáticas optimizadas.",
creditDesignDesc: "Utilidad de estilos CSS y librerías de animación declarativa para interfaces de usuario interactivas de primera calidad."
}
};
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}