feat: Support i18n
This commit is contained in:
198
qwen3-tts-frontend/@/components/ui/dropdown-menu.tsx
Normal file
198
qwen3-tts-frontend/@/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
187
qwen3-tts-frontend/package-lock.json
generated
187
qwen3-tts-frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
@@ -27,12 +28,15 @@
|
||||
"axios": "^1.13.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.8.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2",
|
||||
"react-dom": "^19.2",
|
||||
"react-h5-audio-player": "^3.10.1",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
@@ -1481,6 +1485,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
|
||||
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-menu": "2.1.16",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
@@ -1585,6 +1618,64 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
|
||||
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
@@ -4288,6 +4379,55 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.8.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.3.tgz",
|
||||
"integrity": "sha512-IC/pp2vkczdu1sBheq1eC92bLavN6fM5jH61c7Xa23PGio5ePEd+EP+re1IkO7KEM9eyeJHUxvIRxsaYTlsSyQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
||||
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -5396,6 +5536,33 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
|
||||
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
@@ -5941,7 +6108,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -6066,6 +6233,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -6148,6 +6324,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
@@ -29,12 +30,15 @@
|
||||
"axios": "^1.13.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.8.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2",
|
||||
"react-dom": "^19.2",
|
||||
"react-h5-audio-player": "^3.10.1",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState, useEffect, useCallback, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AudioPlayerLib from 'react-h5-audio-player'
|
||||
import 'react-h5-audio-player/lib/styles.css'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -16,6 +17,7 @@ const isMobileDevice = () => {
|
||||
}
|
||||
|
||||
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
||||
const { t } = useTranslation('common')
|
||||
const [blobUrl, setBlobUrl] = useState<string>('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
@@ -60,7 +62,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
||||
} catch (error) {
|
||||
console.error("Failed to load audio:", error)
|
||||
if (active) {
|
||||
setLoadError('Failed to load audio')
|
||||
setLoadError(t('failedToLoadAudio'))
|
||||
}
|
||||
} finally {
|
||||
if (active) {
|
||||
@@ -92,7 +94,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-4 border rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">Loading...</span>
|
||||
<span className="text-sm text-muted-foreground">{t('loadingAudio')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Mic, Trash2, RotateCcw, FileAudio } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -10,6 +11,7 @@ interface AudioRecorderProps {
|
||||
}
|
||||
|
||||
export function AudioRecorder({ onChange }: AudioRecorderProps) {
|
||||
const { t } = useTranslation('voice')
|
||||
const {
|
||||
isRecording,
|
||||
recordingDuration,
|
||||
@@ -54,7 +56,7 @@ export function AudioRecorder({ onChange }: AudioRecorderProps) {
|
||||
setAudioInfo({ duration: result.duration, size: file.size })
|
||||
setValidationError(null)
|
||||
} else {
|
||||
setValidationError(result.error || '录音验证失败')
|
||||
setValidationError(result.error || t('recordingValidationFailed'))
|
||||
clearRecording()
|
||||
onChange(null)
|
||||
}
|
||||
@@ -98,7 +100,7 @@ export function AudioRecorder({ onChange }: AudioRecorderProps) {
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<div className="p-4 border rounded bg-muted text-muted-foreground text-sm">
|
||||
您的浏览器不支持录音功能
|
||||
{t('browserNotSupported')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -109,9 +111,9 @@ export function AudioRecorder({ onChange }: AudioRecorderProps) {
|
||||
<div className="flex items-center gap-2 p-3 border rounded">
|
||||
<FileAudio className="h-5 w-5 text-muted-foreground" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">录制完成</p>
|
||||
<p className="text-sm font-medium">{t('recordingComplete')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} 秒
|
||||
{(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} {t('seconds')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -148,10 +150,10 @@ export function AudioRecorder({ onChange }: AudioRecorderProps) {
|
||||
{isRecording ? (
|
||||
<>
|
||||
<span className="text-lg font-semibold">{recordingDuration.toFixed(1)}s</span>
|
||||
<span className="text-xs">松开完成</span>
|
||||
<span className="text-xs">{t('releaseToFinish')}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>按住录音</span>
|
||||
<span>{t('holdToRecord')}</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState, type ChangeEvent } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Upload, X, FileAudio } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -16,6 +17,7 @@ interface FileUploaderProps {
|
||||
}
|
||||
|
||||
export function FileUploader({ value, onChange, error }: FileUploaderProps) {
|
||||
const { t } = useTranslation('voice')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { validateAudioFile } = useAudioValidation()
|
||||
const [isValidating, setIsValidating] = useState(false)
|
||||
@@ -33,7 +35,7 @@ export function FileUploader({ value, onChange, error }: FileUploaderProps) {
|
||||
onChange(file)
|
||||
setAudioInfo({ duration: result.duration, size: file.size })
|
||||
} else {
|
||||
toast.error(result.error || '文件验证失败')
|
||||
toast.error(result.error || t('validationFailed'))
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
@@ -56,7 +58,7 @@ export function FileUploader({ value, onChange, error }: FileUploaderProps) {
|
||||
disabled={isValidating}
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{isValidating ? '验证中...' : '选择音频文件'}
|
||||
{isValidating ? t('validating') : t('selectAudioFile')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 p-3 border rounded">
|
||||
@@ -65,7 +67,7 @@ export function FileUploader({ value, onChange, error }: FileUploaderProps) {
|
||||
<p className="text-sm font-medium truncate">{value.name}</p>
|
||||
{audioInfo && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} 秒
|
||||
{(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} {t('seconds')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Job } from '@/types/job'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -28,17 +29,19 @@ const jobTypeBadgeVariant = {
|
||||
voice_clone: 'outline' as const,
|
||||
}
|
||||
|
||||
const jobTypeLabel = {
|
||||
custom_voice: '自定义音色',
|
||||
voice_design: '音色设计',
|
||||
voice_clone: '声音克隆',
|
||||
}
|
||||
|
||||
const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
|
||||
const { t } = useTranslation('job')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false)
|
||||
|
||||
const jobTypeLabel = {
|
||||
custom_voice: t('typeCustomVoice'),
|
||||
voice_design: t('typeVoiceDesign'),
|
||||
voice_clone: t('typeVoiceClone'),
|
||||
}
|
||||
|
||||
const getLanguageDisplay = (lang: string | undefined) => {
|
||||
if (!lang || lang === 'Auto') return '自动检测'
|
||||
if (!lang || lang === 'Auto') return t('autoDetect')
|
||||
return lang
|
||||
}
|
||||
|
||||
@@ -68,31 +71,31 @@ const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
|
||||
<div className="space-y-2 text-sm">
|
||||
{job.parameters?.text && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">文本内容: </span>
|
||||
<span className="text-muted-foreground">{t('synthesisText')}: </span>
|
||||
<span className="line-clamp-2">{job.parameters.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
语言: {getLanguageDisplay(job.parameters?.language)}
|
||||
{t('language')}{getLanguageDisplay(job.parameters?.language)}
|
||||
</div>
|
||||
|
||||
{job.type === 'custom_voice' && job.parameters?.speaker && (
|
||||
<div className="text-muted-foreground">
|
||||
发音人: {job.parameters.speaker}
|
||||
{t('speaker')}{job.parameters.speaker}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.type === 'voice_design' && job.parameters?.instruct && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">音色描述: </span>
|
||||
<span className="text-muted-foreground">{t('voiceDescription')}: </span>
|
||||
<span className="text-xs line-clamp-2">{job.parameters.instruct}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.type === 'voice_clone' && job.parameters?.ref_text && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">参考文本: </span>
|
||||
<span className="text-muted-foreground">{t('referenceText')}: </span>
|
||||
<span className="text-xs line-clamp-1">{job.parameters.ref_text}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -101,14 +104,14 @@ const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
|
||||
{job.status === 'processing' && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>处理中...</span>
|
||||
<span>{t('statusProcessing')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.status === 'pending' && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>等待处理...</span>
|
||||
<span>{t('statusPending')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -132,18 +135,18 @@ const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteJob')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除这条历史记录吗?此操作无法撤销。
|
||||
{t('deleteJobConfirm')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{tCommon('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onDelete(job.id)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
删除
|
||||
{tCommon('delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||
import { HistoryItem } from '@/components/HistoryItem'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
@@ -12,6 +13,7 @@ interface HistorySidebarProps {
|
||||
}
|
||||
|
||||
function HistorySidebarContent() {
|
||||
const { t } = useTranslation('job')
|
||||
const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistoryContext()
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -35,8 +37,8 @@ function HistorySidebarContent() {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">历史记录</h2>
|
||||
<p className="text-sm text-muted-foreground">共 {jobs.length} 条记录</p>
|
||||
<h2 className="text-lg font-semibold">{t('historyTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('historyCount', { count: jobs.length })}</p>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
@@ -50,15 +52,15 @@ function HistorySidebarContent() {
|
||||
<p className="text-sm text-destructive text-center">{error}</p>
|
||||
<Button onClick={retry} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重试
|
||||
{t('retry')}
|
||||
</Button>
|
||||
</div>
|
||||
) : jobs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<FileAudio className="w-12 h-12 text-muted-foreground/50" />
|
||||
<p className="text-sm font-medium text-muted-foreground">暂无历史记录</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('noHistory')}</p>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
生成语音后,记录将会显示在这里
|
||||
{t('historyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Job } from '@/types/job'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -31,14 +32,8 @@ const jobTypeBadgeVariant = {
|
||||
voice_clone: 'outline' as const,
|
||||
}
|
||||
|
||||
const jobTypeLabel = {
|
||||
custom_voice: '自定义音色',
|
||||
voice_design: '音色设计',
|
||||
voice_clone: '声音克隆',
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString('zh-CN', {
|
||||
const formatTimestamp = (timestamp: string, locale: string) => {
|
||||
return new Date(timestamp).toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -47,18 +42,26 @@ const formatTimestamp = (timestamp: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps) => {
|
||||
const { t, i18n } = useTranslation(['job', 'common'])
|
||||
|
||||
if (!job) return null
|
||||
|
||||
const jobTypeLabel = {
|
||||
custom_voice: t('job:typeCustomVoice'),
|
||||
voice_design: t('job:typeVoiceDesign'),
|
||||
voice_clone: t('job:typeVoiceClone'),
|
||||
}
|
||||
|
||||
const getLanguageDisplay = (lang: string | undefined) => {
|
||||
if (!lang || lang === 'Auto') return '自动检测'
|
||||
if (!lang || lang === 'Auto') return t('job:autoDetect')
|
||||
return lang
|
||||
}
|
||||
|
||||
const formatBooleanDisplay = (value: boolean | undefined) => {
|
||||
return value ? '是' : '否'
|
||||
return value ? t('common:yes') : t('common:no')
|
||||
}
|
||||
|
||||
const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps) => {
|
||||
if (!job) return null
|
||||
|
||||
const canPlay = job.status === 'completed'
|
||||
const audioUrl = canPlay ? jobApi.getAudioUrl(job.id, job.audio_url) : ''
|
||||
|
||||
@@ -74,35 +77,35 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<span className="text-sm text-muted-foreground">#{job.id}</span>
|
||||
</DialogTitle>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatTimestamp(job.created_at)}
|
||||
{formatTimestamp(job.created_at, i18n.language)}
|
||||
</span>
|
||||
</div>
|
||||
<DialogDescription>查看任务的详细参数和生成结果</DialogDescription>
|
||||
<DialogDescription>{t('job:detailsDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[calc(90vh-120px)] pr-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">基本信息</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:basicInfo')}</h3>
|
||||
<div className="space-y-1.5 text-sm bg-muted/30 p-3 rounded-lg">
|
||||
{job.type === 'custom_voice' && job.parameters?.speaker && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">发音人: </span>
|
||||
<span className="text-muted-foreground">{t('job:speaker')}</span>
|
||||
<span>{job.parameters.speaker}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-muted-foreground">语言: </span>
|
||||
<span className="text-muted-foreground">{t('job:language')}</span>
|
||||
<span>{getLanguageDisplay(job.parameters?.language)}</span>
|
||||
</div>
|
||||
{job.type === 'voice_clone' && (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-muted-foreground">快速模式: </span>
|
||||
<span className="text-muted-foreground">{t('job:fastMode')}</span>
|
||||
<span>{formatBooleanDisplay(job.parameters?.x_vector_only_mode)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">使用缓存: </span>
|
||||
<span className="text-muted-foreground">{t('job:useCache')}</span>
|
||||
<span>{formatBooleanDisplay(job.parameters?.use_cache)}</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -113,9 +116,9 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">合成文本</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:synthesisText')}</h3>
|
||||
<div className="text-sm bg-muted/30 p-3 rounded-lg border">
|
||||
{job.parameters?.text || <span className="text-muted-foreground">未设置</span>}
|
||||
{job.parameters?.text || <span className="text-muted-foreground">{t('job:notSet')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +126,7 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">音色描述</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:voiceDescription')}</h3>
|
||||
<div className="text-sm bg-blue-50 dark:bg-blue-950/30 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
{job.parameters.instruct}
|
||||
</div>
|
||||
@@ -135,7 +138,7 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">情绪指导</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:emotionGuidance')}</h3>
|
||||
<div className="text-sm bg-muted/30 p-3 rounded-lg border">
|
||||
{job.parameters.instruct}
|
||||
</div>
|
||||
@@ -147,9 +150,9 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">参考文本</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:referenceText')}</h3>
|
||||
<div className="text-sm bg-muted/30 p-3 rounded-lg border">
|
||||
{job.parameters?.ref_text || <span className="text-muted-foreground">未提供</span>}
|
||||
{job.parameters?.ref_text || <span className="text-muted-foreground">{t('job:notProvided')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -159,38 +162,38 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-semibold hover:text-foreground transition-colors w-full">
|
||||
高级参数
|
||||
{t('job:advancedParameters')}
|
||||
<ChevronDown className="w-4 h-4 transition-transform ui-expanded:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-3">
|
||||
<div className="space-y-1.5 text-sm bg-muted/30 p-3 rounded-lg border">
|
||||
{job.parameters?.max_new_tokens !== undefined && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">最大生成长度: </span>
|
||||
<span className="text-muted-foreground">{t('job:maxNewTokens')}</span>
|
||||
<span>{job.parameters.max_new_tokens}</span>
|
||||
</div>
|
||||
)}
|
||||
{job.parameters?.temperature !== undefined && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">温度: </span>
|
||||
<span className="text-muted-foreground">{t('job:temperature')}</span>
|
||||
<span>{job.parameters.temperature}</span>
|
||||
</div>
|
||||
)}
|
||||
{job.parameters?.top_k !== undefined && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Top K: </span>
|
||||
<span className="text-muted-foreground">{t('job:topK')}</span>
|
||||
<span>{job.parameters.top_k}</span>
|
||||
</div>
|
||||
)}
|
||||
{job.parameters?.top_p !== undefined && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Top P: </span>
|
||||
<span className="text-muted-foreground">{t('job:topP')}</span>
|
||||
<span>{job.parameters.top_p}</span>
|
||||
</div>
|
||||
)}
|
||||
{job.parameters?.repetition_penalty !== undefined && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">重复惩罚: </span>
|
||||
<span className="text-muted-foreground">{t('job:repetitionPenalty')}</span>
|
||||
<span>{job.parameters.repetition_penalty}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -204,7 +207,7 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<div className="flex items-start gap-2 p-3 bg-red-50 dark:bg-red-950/30 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<AlertCircle className="w-4 h-4 text-destructive mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm text-destructive mb-1">错误信息</h3>
|
||||
<h3 className="font-semibold text-sm text-destructive mb-1">{t('job:errorMessage')}</h3>
|
||||
<p className="text-sm text-destructive">{job.error_message}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,7 +218,7 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">音频播放</h3>
|
||||
<h3 className="font-semibold text-sm">{t('job:audioPlayback')}</h3>
|
||||
<AudioPlayer audioUrl={audioUrl} jobId={job.id} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface LoadingStateProps {
|
||||
elapsedTime: number
|
||||
}
|
||||
|
||||
const LoadingState = memo(({ elapsedTime }: LoadingStateProps) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const displayText = elapsedTime > 60
|
||||
? '生成用时较长,请耐心等待...'
|
||||
: '正在生成音频,请稍候...'
|
||||
? t('generationTakingLong')
|
||||
: t('generatingAudio')
|
||||
|
||||
return (
|
||||
<div className="space-y-4 py-6">
|
||||
<p className="text-center text-muted-foreground">{displayText}</p>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
已等待 {elapsedTime} 秒
|
||||
{t('waitedSeconds', { seconds: elapsedTime })}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Menu, LogOut, Users, Settings, AudioLines } from 'lucide-react'
|
||||
import { Menu, LogOut, Users, Settings, AudioLines, Globe } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
|
||||
interface NavbarProps {
|
||||
onToggleSidebar?: () => void
|
||||
@@ -10,6 +18,8 @@ interface NavbarProps {
|
||||
|
||||
export function Navbar({ onToggleSidebar }: NavbarProps) {
|
||||
const { logout, user } = useAuth()
|
||||
const { changeLanguage } = useUserPreferences()
|
||||
const { t, i18n } = useTranslation(['nav', 'constants'])
|
||||
|
||||
return (
|
||||
<nav className="h-16 border-b bg-background flex items-center px-4 gap-4">
|
||||
@@ -47,6 +57,30 @@ export function Navbar({ onToggleSidebar }: NavbarProps) {
|
||||
</Button>
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Globe className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => changeLanguage('zh-CN')}>
|
||||
{t('constants:uiLanguages.zh-CN')} {i18n.language === 'zh-CN' && '✓'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => changeLanguage('zh-TW')}>
|
||||
{t('constants:uiLanguages.zh-TW')} {i18n.language === 'zh-TW' && '✓'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => changeLanguage('en-US')}>
|
||||
{t('constants:uiLanguages.en-US')} {i18n.language === 'en-US' && '✓'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => changeLanguage('ja-JP')}>
|
||||
{t('constants:uiLanguages.ja-JP')} {i18n.language === 'ja-JP' && '✓'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => changeLanguage('ko-KR')}>
|
||||
{t('constants:uiLanguages.ko-KR')} {i18n.language === 'ko-KR' && '✓'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="ghost" size="icon" onClick={logout}>
|
||||
<LogOut className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -26,23 +27,25 @@ import { Label } from '@/components/ui/label'
|
||||
import { authApi } from '@/lib/api'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
api_key: z.string().min(1, '请输入 API 密钥'),
|
||||
const createApiKeySchema = (t: (key: string) => string) => z.object({
|
||||
api_key: z.string().min(1, t('auth:validation.apiKeyRequired')),
|
||||
})
|
||||
|
||||
type ApiKeyFormValues = z.infer<typeof apiKeySchema>
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
open: boolean
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
const { t } = useTranslation(['onboarding', 'auth', 'common'])
|
||||
const [step, setStep] = useState(1)
|
||||
const [selectedBackend, setSelectedBackend] = useState<'local' | 'aliyun'>('aliyun')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { updatePreferences, refetchPreferences, isBackendAvailable } = useUserPreferences()
|
||||
|
||||
const apiKeySchema = createApiKeySchema(t)
|
||||
type ApiKeyFormValues = z.infer<typeof apiKeySchema>
|
||||
|
||||
const form = useForm<ApiKeyFormValues>({
|
||||
resolver: zodResolver(apiKeySchema),
|
||||
defaultValues: {
|
||||
@@ -56,10 +59,10 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
default_backend: 'local',
|
||||
onboarding_completed: true,
|
||||
})
|
||||
toast.success('已跳过配置,默认使用本地模式')
|
||||
toast.success(t('onboarding:skipSuccess'))
|
||||
onComplete()
|
||||
} catch (error) {
|
||||
toast.error('操作失败,请重试')
|
||||
toast.error(t('onboarding:operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +81,10 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
default_backend: backend,
|
||||
onboarding_completed: true,
|
||||
})
|
||||
toast.success(`配置完成,默认使用${backend === 'local' ? '本地' : '阿里云'}模式`)
|
||||
toast.success(backend === 'local' ? t('onboarding:configComplete') : t('onboarding:configCompleteAliyun'))
|
||||
onComplete()
|
||||
} catch (error) {
|
||||
toast.error('保存配置失败,请重试')
|
||||
toast.error(t('onboarding:saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -94,7 +97,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
await refetchPreferences()
|
||||
await handleComplete('aliyun')
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'API 密钥验证失败,请检查后重试')
|
||||
toast.error(error.message || t('onboarding:verifyFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -105,12 +108,12 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
<DialogContent className="sm:max-w-[500px]" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 1 ? '欢迎使用 Qwen3 TTS' : '配置阿里云 API 密钥'}
|
||||
{step === 1 ? t('onboarding:welcome') : t('onboarding:configureApiKey')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{step === 1
|
||||
? '请选择您的 TTS 后端模式,后续可在设置中修改'
|
||||
: '请输入您的阿里云 API 密钥,系统将验证其有效性'}
|
||||
? t('onboarding:selectBackendDescription')
|
||||
: t('onboarding:enterApiKeyDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -121,17 +124,17 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
<div className={`flex items-center space-x-3 border rounded-lg p-4 ${isBackendAvailable('local') ? 'hover:bg-accent/50 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||
<RadioGroupItem value="local" id="local" disabled={!isBackendAvailable('local')} />
|
||||
<Label htmlFor="local" className={`flex-1 ${isBackendAvailable('local') ? 'cursor-pointer' : 'cursor-not-allowed'}`}>
|
||||
<div className="font-medium">本地模型</div>
|
||||
<div className="font-medium">{t('onboarding:localModel')}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{isBackendAvailable('local') ? '免费使用本地 Qwen3-TTS 模型' : '无本地模型权限,请联系管理员'}
|
||||
{isBackendAvailable('local') ? t('onboarding:localModelDescription') : t('onboarding:localModelNoPermission')}
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 border rounded-lg p-4 hover:bg-accent/50 cursor-pointer">
|
||||
<RadioGroupItem value="aliyun" id="aliyun" />
|
||||
<Label htmlFor="aliyun" className="flex-1 cursor-pointer">
|
||||
<div className="font-medium">阿里云 API<span className="ml-2 text-xs text-primary">(推荐)</span></div>
|
||||
<div className="text-sm text-muted-foreground">需要配置 API 密钥,按量计费</div>
|
||||
<div className="font-medium">{t('onboarding:aliyunApi')}<span className="ml-2 text-xs text-primary">{t('onboarding:aliyunApiRecommended')}</span></div>
|
||||
<div className="text-sm text-muted-foreground">{t('onboarding:aliyunApiDescription')}</div>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
@@ -140,11 +143,11 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
<DialogFooter>
|
||||
{isBackendAvailable('local') && (
|
||||
<Button type="button" variant="outline" onClick={handleSkip}>
|
||||
跳过配置
|
||||
{t('onboarding:skipConfig')}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" onClick={handleNextStep}>
|
||||
下一步
|
||||
{t('onboarding:nextStep')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@@ -158,7 +161,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
name="api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API 密钥</FormLabel>
|
||||
<FormLabel>{t('onboarding:apiKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
@@ -175,7 +178,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
如何获取 API 密钥?
|
||||
{t('onboarding:howToGetApiKey')}
|
||||
</a>
|
||||
</p>
|
||||
</FormItem>
|
||||
@@ -189,10 +192,10 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
onClick={() => setStep(1)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
返回
|
||||
{t('onboarding:back')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? '验证中...' : '验证并完成'}
|
||||
{isLoading ? t('onboarding:verifying') : t('onboarding:verifyAndComplete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
@@ -19,13 +20,36 @@ import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { PresetSelector } from '@/components/PresetSelector'
|
||||
import { PRESET_INSTRUCTS, ADVANCED_PARAMS_INFO } from '@/lib/constants'
|
||||
import type { Language, UnifiedSpeakerItem } from '@/types/tts'
|
||||
|
||||
type FormData = {
|
||||
text: string
|
||||
language: string
|
||||
speaker: string
|
||||
instruct?: string
|
||||
max_new_tokens?: number
|
||||
temperature?: number
|
||||
top_k?: number
|
||||
top_p?: number
|
||||
repetition_penalty?: number
|
||||
}
|
||||
|
||||
export interface CustomVoiceFormHandle {
|
||||
loadParams: (params: any) => void
|
||||
}
|
||||
|
||||
const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
const { t } = useTranslation('tts')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tErrors } = useTranslation('errors')
|
||||
const { t: tConstants } = useTranslation('constants')
|
||||
|
||||
const PRESET_INSTRUCTS = useMemo(() => tConstants('presetInstructs', { returnObjects: true }) as Array<{ label: string; instruct: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, '请输入要合成的文本').max(5000, '文本长度不能超过 5000 字符'),
|
||||
language: z.string().min(1, '请选择语言'),
|
||||
speaker: z.string().min(1, '请选择发音人'),
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
language: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.language') })),
|
||||
speaker: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.speaker') })),
|
||||
instruct: z.string().optional(),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
@@ -33,14 +57,6 @@ const formSchema = z.object({
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
})
|
||||
|
||||
type FormData = z.infer<typeof formSchema>
|
||||
|
||||
export interface CustomVoiceFormHandle {
|
||||
loadParams: (params: any) => void
|
||||
}
|
||||
|
||||
const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [unifiedSpeakers, setUnifiedSpeakers] = useState<UnifiedSpeakerItem[]>([])
|
||||
const [selectedSpeakerId, setSelectedSpeakerId] = useState<string>('')
|
||||
@@ -122,7 +138,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
|
||||
const designItems: UnifiedSpeakerItem[] = savedDesigns.designs.map(d => ({
|
||||
id: `design-${d.id}`,
|
||||
displayName: `${d.name} (自定义)`,
|
||||
displayName: `${d.name}`,
|
||||
description: d.instruct.substring(0, 60) + (d.instruct.length > 60 ? '...' : ''),
|
||||
source: 'saved-design',
|
||||
designId: d.id,
|
||||
@@ -140,11 +156,11 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
setLanguages(langs)
|
||||
setUnifiedSpeakers([...designItems, ...builtinItems])
|
||||
} catch (error) {
|
||||
toast.error('加载数据失败')
|
||||
toast.error(t('loadDataFailed'))
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [preferences?.default_backend])
|
||||
}, [preferences?.default_backend, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSpeaker?.source === 'saved-design' && selectedSpeaker.instruct) {
|
||||
@@ -184,13 +200,13 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
result = await ttsApi.createCustomVoiceJob(data)
|
||||
}
|
||||
|
||||
toast.success('任务已创建')
|
||||
toast.success(t('taskCreated'))
|
||||
startPolling(result.job_id)
|
||||
try {
|
||||
await refresh()
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
toast.error('创建任务失败')
|
||||
toast.error(t('taskCreateFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -204,7 +220,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Globe2} tooltip="语言" required />
|
||||
<IconLabel icon={Globe2} tooltip={t('languageLabel')} required />
|
||||
<Select
|
||||
value={watch('language')}
|
||||
onValueChange={(value: string) => setValue('language', value)}
|
||||
@@ -215,7 +231,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
{tConstants(`languages.${lang.code}`, { defaultValue: lang.name })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -226,7 +242,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={User} tooltip="发音人" required />
|
||||
<IconLabel icon={User} tooltip={t('speakerLabel')} required />
|
||||
<Select
|
||||
value={selectedSpeakerId}
|
||||
onValueChange={(value: string) => {
|
||||
@@ -244,7 +260,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择发音人">
|
||||
<SelectValue placeholder={t('speakerPlaceholder')}>
|
||||
{selectedSpeakerId && (() => {
|
||||
const item = unifiedSpeakers.find(s => s.id === selectedSpeakerId)
|
||||
if (!item) return null
|
||||
@@ -258,7 +274,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<SelectContent>
|
||||
{unifiedSpeakers.filter(s => s.source === 'saved-design').length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel className="text-xs text-muted-foreground">我的音色设计</SelectLabel>
|
||||
<SelectLabel className="text-xs text-muted-foreground">{t('myVoiceDesigns')}</SelectLabel>
|
||||
{unifiedSpeakers
|
||||
.filter(s => s.source === 'saved-design')
|
||||
.map(item => (
|
||||
@@ -273,7 +289,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
)}
|
||||
|
||||
<SelectGroup>
|
||||
<SelectLabel className="text-xs text-muted-foreground">内置发音人</SelectLabel>
|
||||
<SelectLabel className="text-xs text-muted-foreground">{t('builtinSpeakers')}</SelectLabel>
|
||||
{unifiedSpeakers
|
||||
.filter(s => s.source === 'builtin')
|
||||
.map(item => (
|
||||
@@ -290,10 +306,10 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Type} tooltip="合成文本" required />
|
||||
<IconLabel icon={Type} tooltip={t('textLabel')} required />
|
||||
<Textarea
|
||||
{...register('text')}
|
||||
placeholder="输入要合成的文本..."
|
||||
placeholder={t('textPlaceholder')}
|
||||
className="min-h-[40px] md:min-h-[60px]"
|
||||
/>
|
||||
{errors.text && (
|
||||
@@ -302,12 +318,12 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Sparkles} tooltip="情绪指导(可选)" />
|
||||
<IconLabel icon={Sparkles} tooltip={t('instructLabel')} />
|
||||
<Textarea
|
||||
{...register('instruct')}
|
||||
placeholder={isInstructDisabled
|
||||
? "已使用音色设计的预设指导"
|
||||
: "例如:温柔体贴,语速平缓,充满关怀"
|
||||
? t('instructPlaceholderDesign')
|
||||
: t('instructPlaceholderDefault')
|
||||
}
|
||||
className="min-h-[40px] md:min-h-[60px]"
|
||||
disabled={isInstructDisabled}
|
||||
@@ -343,18 +359,18 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
高级选项
|
||||
{t('advancedOptions')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>高级参数设置</DialogTitle>
|
||||
<DialogDescription>调整生成参数以控制音频质量和生成长度</DialogDescription>
|
||||
<DialogTitle>{t('advancedOptionsTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('advancedOptionsDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-max_new_tokens">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
|
||||
{t('advancedParams.maxNewTokens.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
@@ -368,12 +384,12 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.description}
|
||||
{t('advancedParams.maxNewTokens.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-temperature">
|
||||
{ADVANCED_PARAMS_INFO.temperature.label}
|
||||
{t('advancedParams.temperature.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-temperature"
|
||||
@@ -388,12 +404,12 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.temperature.description}
|
||||
{t('advancedParams.temperature.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-top_k">
|
||||
{ADVANCED_PARAMS_INFO.top_k.label}
|
||||
{t('advancedParams.topK.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-top_k"
|
||||
@@ -407,12 +423,12 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.top_k.description}
|
||||
{t('advancedParams.topK.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-top_p">
|
||||
{ADVANCED_PARAMS_INFO.top_p.label}
|
||||
{t('advancedParams.topP.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-top_p"
|
||||
@@ -427,12 +443,12 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.top_p.description}
|
||||
{t('advancedParams.topP.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-repetition_penalty">
|
||||
{ADVANCED_PARAMS_INFO.repetition_penalty.label}
|
||||
{t('advancedParams.repetitionPenalty.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-repetition_penalty"
|
||||
@@ -447,7 +463,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.repetition_penalty.description}
|
||||
{t('advancedParams.repetitionPenalty.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -466,7 +482,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -479,7 +495,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
确定
|
||||
{tCommon('ok')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -490,11 +506,11 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="submit" className="w-full" disabled={isLoading || isPolling}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
{isLoading ? '创建中...' : '生成语音'}
|
||||
{isLoading ? t('creating') : t('generate')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>生成语音</p>
|
||||
<p>{t('generate')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useForm, Controller } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
@@ -21,14 +22,36 @@ import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { FileUploader } from '@/components/FileUploader'
|
||||
import { AudioRecorder } from '@/components/AudioRecorder'
|
||||
import { PresetSelector } from '@/components/PresetSelector'
|
||||
import { PRESET_REF_TEXTS, ADVANCED_PARAMS_INFO } from '@/lib/constants'
|
||||
import type { Language } from '@/types/tts'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
type FormData = {
|
||||
text: string
|
||||
language?: string
|
||||
ref_audio: File
|
||||
ref_text?: string
|
||||
use_cache?: boolean
|
||||
x_vector_only_mode?: boolean
|
||||
max_new_tokens?: number
|
||||
temperature?: number
|
||||
top_k?: number
|
||||
top_p?: number
|
||||
repetition_penalty?: number
|
||||
}
|
||||
|
||||
function VoiceCloneForm() {
|
||||
const { t } = useTranslation('tts')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tVoice } = useTranslation('voice')
|
||||
const { t: tErrors } = useTranslation('errors')
|
||||
const { t: tConstants } = useTranslation('constants')
|
||||
|
||||
const PRESET_REF_TEXTS = useMemo(() => tConstants('presetRefTexts', { returnObjects: true }) as Array<{ label: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, '请输入要合成的文本').max(5000, '文本长度不能超过 5000 字符'),
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
language: z.string().optional(),
|
||||
ref_audio: z.instanceof(File, { message: '请上传参考音频' }),
|
||||
ref_audio: z.instanceof(File, { message: tErrors('validation.required', { field: tErrors('fieldNames.reference_audio') }) }),
|
||||
ref_text: z.string().optional(),
|
||||
use_cache: z.boolean().optional(),
|
||||
x_vector_only_mode: z.boolean().optional(),
|
||||
@@ -38,10 +61,6 @@ const formSchema = z.object({
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
})
|
||||
|
||||
type FormData = z.infer<typeof formSchema>
|
||||
|
||||
function VoiceCloneForm() {
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
@@ -84,11 +103,11 @@ function VoiceCloneForm() {
|
||||
const langs = await ttsApi.getLanguages()
|
||||
setLanguages(langs)
|
||||
} catch (error) {
|
||||
toast.error('加载数据失败')
|
||||
toast.error(t('loadDataFailed'))
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputTab === 'record' && PRESET_REF_TEXTS.length > 0) {
|
||||
@@ -96,7 +115,7 @@ function VoiceCloneForm() {
|
||||
} else if (inputTab === 'upload') {
|
||||
setValue('ref_text', '')
|
||||
}
|
||||
}, [inputTab])
|
||||
}, [inputTab, setValue])
|
||||
|
||||
const handleNextStep = async () => {
|
||||
// Validate step 1 fields
|
||||
@@ -113,13 +132,13 @@ function VoiceCloneForm() {
|
||||
...data,
|
||||
ref_audio: data.ref_audio,
|
||||
})
|
||||
toast.success('任务已创建')
|
||||
toast.success(t('taskCreated'))
|
||||
startPolling(result.job_id)
|
||||
try {
|
||||
await refresh()
|
||||
} catch { }
|
||||
} catch (error) {
|
||||
toast.error('创建任务失败')
|
||||
toast.error(t('taskCreateFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -136,12 +155,12 @@ function VoiceCloneForm() {
|
||||
<div className="flex items-center justify-center space-x-4 mb-6">
|
||||
<div className={`flex items-center space-x-2 ${step === 1 ? 'text-primary' : 'text-muted-foreground'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${step === 1 ? 'border-primary bg-primary/10' : 'border-muted'}`}>1</div>
|
||||
<span className="text-sm font-medium">音频素材</span>
|
||||
<span className="text-sm font-medium">{tVoice('step1Title')}</span>
|
||||
</div>
|
||||
<div className="w-8 h-[2px] bg-muted" />
|
||||
<div className={`flex items-center space-x-2 ${step === 2 ? 'text-primary' : 'text-muted-foreground'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${step === 2 ? 'border-primary bg-primary/10' : 'border-muted'}`}>2</div>
|
||||
<span className="text-sm font-medium">合成设置</span>
|
||||
<span className="text-sm font-medium">{tVoice('step2Title')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -151,17 +170,17 @@ function VoiceCloneForm() {
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="upload" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
上传音频
|
||||
{tVoice('uploadTab')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="record" className="flex items-center gap-2">
|
||||
<Mic className="h-4 w-4" />
|
||||
在线录制
|
||||
{tVoice('recordTab')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="upload" className="space-y-4 mt-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label>参考音频文件</Label>
|
||||
<Label>{tVoice('refAudioLabel')}</Label>
|
||||
<Controller
|
||||
name="ref_audio"
|
||||
control={control}
|
||||
@@ -175,10 +194,10 @@ function VoiceCloneForm() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<Label>参考文稿(可选,提高准确率)</Label>
|
||||
<Label>{tVoice('refTextLabel')}</Label>
|
||||
<Textarea
|
||||
{...register('ref_text')}
|
||||
placeholder="参考音频对应的文本内容..."
|
||||
placeholder={tVoice('refTextPlaceholder')}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<PresetSelector
|
||||
@@ -188,14 +207,14 @@ function VoiceCloneForm() {
|
||||
</div>
|
||||
|
||||
<Button type="button" className="w-full mt-6" onClick={handleNextStep}>
|
||||
下一步
|
||||
{tVoice('nextStep')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="record" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">请朗读以下任一段落:</Label>
|
||||
<Label className="text-base font-medium">{tVoice('readPrompt')}</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{PRESET_REF_TEXTS.map((preset, i) => {
|
||||
const isSelected = watch('ref_text') === preset.text
|
||||
@@ -213,10 +232,10 @@ function VoiceCloneForm() {
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-0.5 pt-2">
|
||||
<Label>当前参考文本</Label>
|
||||
<Label>{tVoice('currentRefText')}</Label>
|
||||
<Textarea
|
||||
{...register('ref_text')}
|
||||
placeholder="选中的文本将显示在这里..."
|
||||
placeholder={tVoice('currentRefTextPlaceholder')}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
</div>
|
||||
@@ -227,7 +246,7 @@ function VoiceCloneForm() {
|
||||
<div className="space-y-3">
|
||||
{watch('ref_audio') && (
|
||||
<Button type="button" className="w-full" onClick={handleNextStep}>
|
||||
下一步
|
||||
{tVoice('nextStep')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -254,7 +273,7 @@ function VoiceCloneForm() {
|
||||
<div className={step === 2 ? 'block space-y-4' : 'hidden'}>
|
||||
{/* Step 2: Synthesis Options */}
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Globe2} tooltip="语言(可选)" />
|
||||
<IconLabel icon={Globe2} tooltip={tVoice('languageOptional')} />
|
||||
<Select
|
||||
value={watch('language')}
|
||||
onValueChange={(value: string) => setValue('language', value)}
|
||||
@@ -265,7 +284,7 @@ function VoiceCloneForm() {
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
{tConstants(`languages.${lang.code}`, { defaultValue: lang.name })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -273,10 +292,10 @@ function VoiceCloneForm() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Type} tooltip="合成文本" required />
|
||||
<IconLabel icon={Type} tooltip={t('textLabel')} required />
|
||||
<Textarea
|
||||
{...register('text')}
|
||||
placeholder="输入要合成的文本..."
|
||||
placeholder={t('textPlaceholder')}
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<PresetSelector
|
||||
@@ -296,7 +315,7 @@ function VoiceCloneForm() {
|
||||
onCheckedChange={(c) => setValue('x_vector_only_mode', c as boolean)}
|
||||
/>
|
||||
<Label htmlFor="x_vector_only_mode" className="text-sm font-normal cursor-pointer">
|
||||
快速模式
|
||||
{tVoice('fastMode')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +326,7 @@ function VoiceCloneForm() {
|
||||
onCheckedChange={(c) => setValue('use_cache', c as boolean)}
|
||||
/>
|
||||
<Label htmlFor="use_cache" className="text-sm font-normal cursor-pointer">
|
||||
使用缓存
|
||||
{tVoice('useCache')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,18 +342,18 @@ function VoiceCloneForm() {
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
高级选项
|
||||
{t('advancedOptions')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>高级参数设置</DialogTitle>
|
||||
<DialogDescription>调整生成参数以控制音频质量和生成长度</DialogDescription>
|
||||
<DialogTitle>{t('advancedOptionsTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('advancedOptionsDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-max_new_tokens">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
|
||||
{t('advancedParams.maxNewTokens.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
@@ -348,7 +367,7 @@ function VoiceCloneForm() {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.description}
|
||||
{t('advancedParams.maxNewTokens.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -360,7 +379,7 @@ function VoiceCloneForm() {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -369,7 +388,7 @@ function VoiceCloneForm() {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
确定
|
||||
{tCommon('ok')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -378,18 +397,18 @@ function VoiceCloneForm() {
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button type="button" variant="outline" onClick={() => setStep(1)} className="w-1/3">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
上一步
|
||||
{tVoice('prevStep')}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="submit" className="flex-1" disabled={isLoading || isPolling}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
{isLoading ? '创建中...' : '生成语音'}
|
||||
{isLoading ? t('creating') : t('generate')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>生成语音</p>
|
||||
<p>{t('generate')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
@@ -19,27 +20,41 @@ import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { PresetSelector } from '@/components/PresetSelector'
|
||||
import { PRESET_VOICE_DESIGNS, ADVANCED_PARAMS_INFO } from '@/lib/constants'
|
||||
import type { Language } from '@/types/tts'
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, '请输入要合成的文本').max(5000, '文本长度不能超过 5000 字符'),
|
||||
language: z.string().min(1, '请选择语言'),
|
||||
instruct: z.string().min(10, '音色描述至少需要 10 个字符').max(500, '音色描述不能超过 500 字符'),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
top_k: z.number().min(1).max(100).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
})
|
||||
|
||||
type FormData = z.infer<typeof formSchema>
|
||||
type FormData = {
|
||||
text: string
|
||||
language: string
|
||||
instruct: string
|
||||
max_new_tokens?: number
|
||||
temperature?: number
|
||||
top_k?: number
|
||||
top_p?: number
|
||||
repetition_penalty?: number
|
||||
}
|
||||
|
||||
export interface VoiceDesignFormHandle {
|
||||
loadParams: (params: any) => void
|
||||
}
|
||||
|
||||
const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
const { t } = useTranslation('tts')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tErrors } = useTranslation('errors')
|
||||
const { t: tConstants } = useTranslation('constants')
|
||||
|
||||
const PRESET_VOICE_DESIGNS = useMemo(() => tConstants('presetVoiceDesigns', { returnObjects: true }) as Array<{ label: string; instruct: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
language: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.language') })),
|
||||
instruct: z.string().min(10, tErrors('validation.minLength', { field: tErrors('fieldNames.instruct'), min: 10 })).max(500, tErrors('validation.maxLength', { field: tErrors('fieldNames.instruct'), max: 500 })),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
top_k: z.number().min(1).max(100).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
})
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
@@ -97,23 +112,23 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
const langs = await ttsApi.getLanguages()
|
||||
setLanguages(langs)
|
||||
} catch (error) {
|
||||
toast.error('加载数据失败')
|
||||
toast.error(t('loadDataFailed'))
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await ttsApi.createVoiceDesignJob(data)
|
||||
toast.success('任务已创建')
|
||||
toast.success(t('taskCreated'))
|
||||
startPolling(result.job_id)
|
||||
try {
|
||||
await refresh()
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
toast.error('创建任务失败')
|
||||
toast.error(t('taskCreateFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -122,11 +137,11 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
const handleSaveDesign = async () => {
|
||||
const instruct = watch('instruct')
|
||||
if (!instruct || instruct.length < 10) {
|
||||
toast.error('请先填写音色描述')
|
||||
toast.error(t('fillDesignDescription'))
|
||||
return
|
||||
}
|
||||
if (!saveDesignName.trim()) {
|
||||
toast.error('请输入设计名称')
|
||||
toast.error(t('fillDesignName'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,15 +155,15 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
preview_text: text || `${saveDesignName}的声音`
|
||||
})
|
||||
|
||||
toast.success('音色设计已保存')
|
||||
toast.success(t('designSaved'))
|
||||
|
||||
if (backend === 'local') {
|
||||
setIsPreparing(true)
|
||||
try {
|
||||
await voiceDesignApi.prepareClone(design.id)
|
||||
toast.success('音色克隆准备完成')
|
||||
toast.success(t('clonePrepared'))
|
||||
} catch (error) {
|
||||
toast.error('准备克隆失败,但设计已保存')
|
||||
toast.error(t('clonePrepareFailed'))
|
||||
} finally {
|
||||
setIsPreparing(false)
|
||||
}
|
||||
@@ -157,7 +172,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
setShowSaveDialog(false)
|
||||
setSaveDesignName('')
|
||||
} catch (error) {
|
||||
toast.error('保存失败')
|
||||
toast.error(t('saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +184,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Globe2} tooltip="语言" required />
|
||||
<IconLabel icon={Globe2} tooltip={t('languageLabel')} required />
|
||||
<Select
|
||||
value={watch('language')}
|
||||
onValueChange={(value: string) => setValue('language', value)}
|
||||
@@ -180,7 +195,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
{tConstants(`languages.${lang.code}`, { defaultValue: lang.name })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -191,10 +206,10 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Type} tooltip="合成文本" required />
|
||||
<IconLabel icon={Type} tooltip={t('textLabel')} required />
|
||||
<Textarea
|
||||
{...register('text')}
|
||||
placeholder="输入要合成的文本..."
|
||||
placeholder={t('textPlaceholder')}
|
||||
className="min-h-[40px] md:min-h-[60px]"
|
||||
/>
|
||||
{errors.text && (
|
||||
@@ -203,10 +218,10 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<IconLabel icon={Palette} tooltip="音色描述" required />
|
||||
<IconLabel icon={Palette} tooltip={t('designDescriptionLabel')} required />
|
||||
<Textarea
|
||||
{...register('instruct')}
|
||||
placeholder="例如:成熟男性,低沉磁性,充满权威感"
|
||||
placeholder={t('designDescriptionPlaceholder')}
|
||||
className="min-h-[40px] md:min-h-[60px]"
|
||||
/>
|
||||
<PresetSelector
|
||||
@@ -226,15 +241,15 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>保存音色设计</DialogTitle>
|
||||
<DialogDescription>为当前音色设计命名并保存,以便后续快速使用</DialogDescription>
|
||||
<DialogTitle>{t('saveDesignTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('saveDesignDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="design-name">设计名称</Label>
|
||||
<Label htmlFor="design-name">{t('designNameLabel')}</Label>
|
||||
<Input
|
||||
id="design-name"
|
||||
placeholder="例如:磁性男声"
|
||||
placeholder={t('designNamePlaceholder')}
|
||||
value={saveDesignName}
|
||||
onChange={(e) => setSaveDesignName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -246,7 +261,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>音色描述</Label>
|
||||
<Label>{t('designDescriptionLabel')}</Label>
|
||||
<p className="text-sm text-muted-foreground">{watch('instruct')}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,10 +270,10 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
setShowSaveDialog(false)
|
||||
setSaveDesignName('')
|
||||
}}>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSaveDesign} disabled={isPreparing}>
|
||||
{isPreparing ? '准备中...' : '保存'}
|
||||
{isPreparing ? t('preparing') : tCommon('save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -279,18 +294,18 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
高级选项
|
||||
{t('advancedOptions')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>高级参数设置</DialogTitle>
|
||||
<DialogDescription>调整生成参数以控制音频质量和生成长度</DialogDescription>
|
||||
<DialogTitle>{t('advancedOptionsTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('advancedOptionsDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-max_new_tokens">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
|
||||
{t('advancedParams.maxNewTokens.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
@@ -304,12 +319,12 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.max_new_tokens.description}
|
||||
{t('advancedParams.maxNewTokens.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-temperature">
|
||||
{ADVANCED_PARAMS_INFO.temperature.label}
|
||||
{t('advancedParams.temperature.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-temperature"
|
||||
@@ -324,12 +339,12 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.temperature.description}
|
||||
{t('advancedParams.temperature.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-top_k">
|
||||
{ADVANCED_PARAMS_INFO.top_k.label}
|
||||
{t('advancedParams.topK.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-top_k"
|
||||
@@ -343,12 +358,12 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.top_k.description}
|
||||
{t('advancedParams.topK.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-top_p">
|
||||
{ADVANCED_PARAMS_INFO.top_p.label}
|
||||
{t('advancedParams.topP.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-top_p"
|
||||
@@ -363,12 +378,12 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.top_p.description}
|
||||
{t('advancedParams.topP.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dialog-repetition_penalty">
|
||||
{ADVANCED_PARAMS_INFO.repetition_penalty.label}
|
||||
{t('advancedParams.repetitionPenalty.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="dialog-repetition_penalty"
|
||||
@@ -383,7 +398,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{ADVANCED_PARAMS_INFO.repetition_penalty.description}
|
||||
{t('advancedParams.repetitionPenalty.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,7 +417,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -415,7 +430,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
>
|
||||
确定
|
||||
{tCommon('ok')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -426,11 +441,11 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="submit" className="w-full" disabled={isLoading || isPolling}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
{isLoading ? '创建中...' : '生成语音'}
|
||||
{isLoading ? t('creating') : t('generate')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>生成语音</p>
|
||||
<p>{t('generate')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -450,7 +465,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
onClick={() => setShowSaveDialog(true)}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
保存音色设计
|
||||
{t('saveDesignButton')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
198
qwen3-tts-frontend/src/components/ui/dropdown-menu.tsx
Normal file
198
qwen3-tts-frontend/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -23,22 +24,20 @@ import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { User } from '@/types/auth'
|
||||
|
||||
const userFormSchema = z.object({
|
||||
username: z.string().min(3, '用户名至少3个字符').max(20, '用户名最多20个字符'),
|
||||
email: z.string().email('请输入有效的邮箱地址'),
|
||||
const createUserFormSchema = (t: (key: string) => string) => z.object({
|
||||
username: z.string().min(3, t('user:validation.usernameMinLength')).max(20, t('user:validation.usernameMaxLength')),
|
||||
email: z.string().email(t('user:validation.emailInvalid')),
|
||||
password: z.string().optional(),
|
||||
is_active: z.boolean(),
|
||||
is_superuser: z.boolean(),
|
||||
can_use_local_model: z.boolean(),
|
||||
})
|
||||
|
||||
type UserFormValues = z.infer<typeof userFormSchema>
|
||||
|
||||
interface UserDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
user?: User | null
|
||||
onSubmit: (data: UserFormValues) => Promise<void>
|
||||
onSubmit: (data: any) => Promise<void>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
@@ -49,8 +48,12 @@ export function UserDialog({
|
||||
onSubmit,
|
||||
isLoading,
|
||||
}: UserDialogProps) {
|
||||
const { t } = useTranslation(['user', 'common'])
|
||||
const isEditing = !!user
|
||||
|
||||
const userFormSchema = createUserFormSchema(t)
|
||||
type UserFormValues = z.infer<typeof userFormSchema>
|
||||
|
||||
const form = useForm<UserFormValues>({
|
||||
resolver: zodResolver(userFormSchema),
|
||||
defaultValues: {
|
||||
@@ -94,9 +97,9 @@ export function UserDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? '编辑用户' : '创建用户'}</DialogTitle>
|
||||
<DialogTitle>{isEditing ? t('user:editUserDialog') : t('user:createUserDialog')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing ? '修改用户信息和权限设置' : '创建新用户并配置基本信息'}
|
||||
{isEditing ? t('user:editUserDescription') : t('user:createUserDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -107,7 +110,7 @@ export function UserDialog({
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>用户名</FormLabel>
|
||||
<FormLabel>{t('user:username')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -121,7 +124,7 @@ export function UserDialog({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>邮箱</FormLabel>
|
||||
<FormLabel>{t('user:email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
@@ -136,7 +139,7 @@ export function UserDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
密码{isEditing && ' (留空则不修改)'}
|
||||
{isEditing ? t('user:passwordOptional') : t('user:password')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
@@ -158,7 +161,7 @@ export function UserDialog({
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>激活状态</FormLabel>
|
||||
<FormLabel>{t('user:isActive')}</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -176,7 +179,7 @@ export function UserDialog({
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>超级管理员</FormLabel>
|
||||
<FormLabel>{t('user:isSuperuser')}</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -194,9 +197,9 @@ export function UserDialog({
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>本地模型权限</FormLabel>
|
||||
<FormLabel>{t('user:canUseLocalModel')}</FormLabel>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许用户使用本地 TTS 模型
|
||||
{t('user:canUseLocalModelDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -211,10 +214,10 @@ export function UserDialog({
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
取消
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading} className="w-full sm:w-auto">
|
||||
{isLoading ? '保存中...' : '保存'}
|
||||
{isLoading ? t('user:saving') : t('common:save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Edit, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -11,10 +12,12 @@ interface UserTableProps {
|
||||
}
|
||||
|
||||
export function UserTable({ users, isLoading, onEdit, onDelete }: UserTableProps) {
|
||||
const { t, i18n } = useTranslation(['user', 'common'])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
<div className="text-muted-foreground">{t('common:loading')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +25,7 @@ export function UserTable({ users, isLoading, onEdit, onDelete }: UserTableProps
|
||||
if (users.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">暂无用户</div>
|
||||
<div className="text-muted-foreground">{t('user:noUsers')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -34,13 +37,13 @@ export function UserTable({ users, isLoading, onEdit, onDelete }: UserTableProps
|
||||
<thead className="border-b">
|
||||
<tr className="text-left">
|
||||
<th className="px-4 py-3 font-medium">ID</th>
|
||||
<th className="px-4 py-3 font-medium">用户名</th>
|
||||
<th className="px-4 py-3 font-medium">邮箱</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 font-medium">角色</th>
|
||||
<th className="px-4 py-3 font-medium">权限</th>
|
||||
<th className="px-4 py-3 font-medium">创建时间</th>
|
||||
<th className="px-4 py-3 font-medium text-right">操作</th>
|
||||
<th className="px-4 py-3 font-medium">{t('user:username')}</th>
|
||||
<th className="px-4 py-3 font-medium">{t('user:email')}</th>
|
||||
<th className="px-4 py-3 font-medium">{t('common:status')}</th>
|
||||
<th className="px-4 py-3 font-medium">{t('user:role')}</th>
|
||||
<th className="px-4 py-3 font-medium">{t('common:actions')}</th>
|
||||
<th className="px-4 py-3 font-medium">{t('user:createdAt')}</th>
|
||||
<th className="px-4 py-3 font-medium text-right">{t('common:actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -51,21 +54,21 @@ export function UserTable({ users, isLoading, onEdit, onDelete }: UserTableProps
|
||||
<td className="px-4 py-3">{user.email}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
||||
{user.is_active ? '活跃' : '停用'}
|
||||
{user.is_active ? t('user:active') : t('user:inactive')}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={user.is_superuser ? 'destructive' : 'outline'}>
|
||||
{user.is_superuser ? '超级管理员' : '普通用户'}
|
||||
{user.is_superuser ? t('user:superuser') : t('user:normalUser')}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{(user.is_superuser || user.can_use_local_model) && (
|
||||
<Badge variant="secondary">本地模型</Badge>
|
||||
<Badge variant="secondary">{t('user:localModelPermission')}</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{new Date(user.created_at).toLocaleString('zh-CN')}
|
||||
{new Date(user.created_at).toLocaleString(i18n.language)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex justify-end gap-2">
|
||||
@@ -120,32 +123,32 @@ export function UserTable({ users, isLoading, onEdit, onDelete }: UserTableProps
|
||||
<span>{user.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">邮箱:</span>
|
||||
<span className="text-muted-foreground">{t('user:email')}:</span>
|
||||
<span className="truncate ml-2">{user.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">状态:</span>
|
||||
<span className="text-muted-foreground">{t('common:status')}:</span>
|
||||
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
||||
{user.is_active ? '活跃' : '停用'}
|
||||
{user.is_active ? t('user:active') : t('user:inactive')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">角色:</span>
|
||||
<span className="text-muted-foreground">{t('user:role')}:</span>
|
||||
<Badge variant={user.is_superuser ? 'destructive' : 'outline'}>
|
||||
{user.is_superuser ? '超级管理员' : '普通用户'}
|
||||
{user.is_superuser ? t('user:superuser') : t('user:normalUser')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">权限:</span>
|
||||
<span className="text-muted-foreground">{t('common:actions')}:</span>
|
||||
{(user.is_superuser || user.can_use_local_model) ? (
|
||||
<Badge variant="secondary">本地模型</Badge>
|
||||
<Badge variant="secondary">{t('user:localModelPermission')}</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">无</span>
|
||||
<span className="text-xs text-muted-foreground">{t('user:noPermission')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">创建时间:</span>
|
||||
<span className="text-xs">{new Date(user.created_at).toLocaleString('zh-CN')}</span>
|
||||
<span className="text-muted-foreground">{t('user:createdAt')}:</span>
|
||||
<span className="text-xs">{new Date(user.created_at).toLocaleString(i18n.language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { authApi } from '@/lib/api'
|
||||
import type { User, LoginRequest, AuthState } from '@/types/auth'
|
||||
|
||||
@@ -12,6 +13,7 @@ interface AuthContextType extends AuthState {
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { t } = useTranslation('auth')
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@@ -49,10 +51,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const currentUser = await authApi.getCurrentUser()
|
||||
setUser(currentUser)
|
||||
|
||||
toast.success('登录成功')
|
||||
toast.success(t('loginSuccess'))
|
||||
navigate('/')
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.detail || '登录失败,请检查用户名和密码'
|
||||
const message = error.response?.data?.detail || t('loginFailedCheckCredentials')
|
||||
toast.error(message)
|
||||
throw error
|
||||
}
|
||||
@@ -62,7 +64,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
localStorage.removeItem('token')
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
toast.success('已退出登录')
|
||||
toast.success(t('logoutSuccess'))
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, type ReactNode } from '
|
||||
import { authApi } from '@/lib/api'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import type { UserPreferences } from '@/types/auth'
|
||||
import i18n from '@/locales'
|
||||
|
||||
interface UserPreferencesContextType {
|
||||
preferences: UserPreferences | null
|
||||
@@ -10,6 +11,7 @@ interface UserPreferencesContextType {
|
||||
hasAliyunKey: boolean
|
||||
refetchPreferences: () => Promise<void>
|
||||
isBackendAvailable: (backend: string) => boolean
|
||||
changeLanguage: (lang: 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR') => Promise<void>
|
||||
}
|
||||
|
||||
const UserPreferencesContext = createContext<UserPreferencesContextType | undefined>(undefined)
|
||||
@@ -36,6 +38,10 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
||||
setPreferences(prefs)
|
||||
setHasAliyunKey(keyVerification.valid)
|
||||
|
||||
if (prefs.language) {
|
||||
i18n.changeLanguage(prefs.language)
|
||||
}
|
||||
|
||||
const cacheKey = `user_preferences_${user.id}`
|
||||
localStorage.setItem(cacheKey, JSON.stringify(prefs))
|
||||
} catch (error) {
|
||||
@@ -80,6 +86,11 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
||||
return preferences.available_backends.includes(backend)
|
||||
}
|
||||
|
||||
const changeLanguage = async (lang: 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR') => {
|
||||
await i18n.changeLanguage(lang)
|
||||
await updatePreferences({ language: lang })
|
||||
}
|
||||
|
||||
return (
|
||||
<UserPreferencesContext.Provider
|
||||
value={{
|
||||
@@ -89,6 +100,7 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
||||
hasAliyunKey,
|
||||
refetchPreferences: fetchPreferences,
|
||||
isBackendAvailable,
|
||||
changeLanguage,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
25
qwen3-tts-frontend/src/locales/en-US/auth.json
Normal file
25
qwen3-tts-frontend/src/locales/en-US/auth.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"loginButton": "Login",
|
||||
"loggingIn": "Logging in...",
|
||||
"welcome": "Welcome to Qwen TTS",
|
||||
"loginPrompt": "Please login to continue",
|
||||
"loginSuccess": "Login successful",
|
||||
"loginFailed": "Login failed",
|
||||
"loginFailedCheckCredentials": "Login failed, please check username and password",
|
||||
"logoutSuccess": "Logged out successfully",
|
||||
"unauthorized": "Unauthorized, please login",
|
||||
"sessionExpired": "Session expired, please login again",
|
||||
"noPermission": "You don't have permission to perform this action",
|
||||
"adminOnly": "This feature is only available to administrators",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"validation": {
|
||||
"usernameMinLength": "Username must be at least {{min}} characters",
|
||||
"usernameMaxLength": "Username cannot exceed {{max}} characters",
|
||||
"passwordMinLength": "Password must be at least {{min}} characters",
|
||||
"apiKeyRequired": "Please enter API key"
|
||||
}
|
||||
}
|
||||
40
qwen3-tts-frontend/src/locales/en-US/common.json
Normal file
40
qwen3-tts-frontend/src/locales/en-US/common.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"submit": "Submit",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"clear": "Clear",
|
||||
"reset": "Reset",
|
||||
"loading": "Loading...",
|
||||
"noData": "No data",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"ok": "OK",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"view": "View",
|
||||
"details": "Details",
|
||||
"actions": "Actions",
|
||||
"generatingAudio": "Generating audio, please wait...",
|
||||
"generationTakingLong": "Generation is taking longer, please be patient...",
|
||||
"waitedSeconds": "Waited {{seconds}} seconds",
|
||||
"loadingAudio": "Loading...",
|
||||
"failedToLoadAudio": "Failed to load audio"
|
||||
}
|
||||
152
qwen3-tts-frontend/src/locales/en-US/constants.json
Normal file
152
qwen3-tts-frontend/src/locales/en-US/constants.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"languages": {
|
||||
"Auto": "Auto Detect",
|
||||
"Chinese": "Chinese",
|
||||
"English": "English",
|
||||
"Japanese": "Japanese",
|
||||
"Korean": "Korean",
|
||||
"German": "German",
|
||||
"French": "French",
|
||||
"Russian": "Russian",
|
||||
"Portuguese": "Portuguese",
|
||||
"Spanish": "Spanish",
|
||||
"Italian": "Italian",
|
||||
"Cantonese": "Cantonese",
|
||||
"zh": "Chinese",
|
||||
"en": "English",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"yue": "Cantonese"
|
||||
},
|
||||
"speakers": {
|
||||
"Vivian": "Female, professional and clear",
|
||||
"Serena": "Female, gentle and warm",
|
||||
"Aria": "Female, lively and cheerful",
|
||||
"Emma": "Female, mature and steady",
|
||||
"Sophie": "Female, elegant and intelligent",
|
||||
"Isabella": "Female, graceful and amiable",
|
||||
"Ava": "Female, young and trendy",
|
||||
"Oliver": "Male, magnetic and calm",
|
||||
"Lucas": "Male, sunny and cheerful",
|
||||
"Ethan": "Male, professional and grand",
|
||||
"Noah": "Male, gentle and friendly",
|
||||
"Liam": "Male, young and energetic"
|
||||
},
|
||||
"uiLanguages": {
|
||||
"zh-CN": "Simplified Chinese",
|
||||
"zh-TW": "Traditional Chinese",
|
||||
"en-US": "English",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean"
|
||||
},
|
||||
"uiLanguagesShort": {
|
||||
"zh-CN": "ZH-CN",
|
||||
"zh-TW": "ZH-TW",
|
||||
"en-US": "EN",
|
||||
"ja-JP": "JA",
|
||||
"ko-KR": "KO"
|
||||
},
|
||||
"presetInstructs": [
|
||||
{
|
||||
"label": "Happy",
|
||||
"instruct": "very happy",
|
||||
"text": "The weather is so nice today, let's go to the park together!"
|
||||
},
|
||||
{
|
||||
"label": "Sad",
|
||||
"instruct": "very sad, with a crying tone",
|
||||
"text": "I'm sorry, I really tried my best, but I still let you down."
|
||||
},
|
||||
{
|
||||
"label": "Angry",
|
||||
"instruct": "very angry, with intense tone",
|
||||
"text": "How could you do this! This is absolutely unacceptable!"
|
||||
},
|
||||
{
|
||||
"label": "Gentle Care",
|
||||
"instruct": "gentle and caring, slow pace, soft tone, full of care and comfort",
|
||||
"text": "Don't worry, everything will be fine. I'll always be here with you."
|
||||
},
|
||||
{
|
||||
"label": "Excited",
|
||||
"instruct": "very excited, faster pace, rising tone, full of energy and enthusiasm",
|
||||
"text": "Awesome! We finally made it! This is so exciting!"
|
||||
},
|
||||
{
|
||||
"label": "Anxious",
|
||||
"instruct": "anxious tone, slightly faster pace, unstable tone, with tension and worry",
|
||||
"text": "What should we do? We're running out of time, we won't make it, what can we do?"
|
||||
},
|
||||
{
|
||||
"label": "Professional Broadcaster",
|
||||
"instruct": "Professional news broadcaster. Pace: standard broadcasting speed, clear articulation. Emotion: calm and professional, without personal emotion. Tone: mostly flat with slight variations, emphasis on key words. Character: rigorous, objective, authoritative.",
|
||||
"text": "According to Reuters, our space program has achieved a major breakthrough, with the successful completion of manned space missions."
|
||||
},
|
||||
{
|
||||
"label": "Warm Mentor",
|
||||
"instruct": "Warm mentor. Pace: unhurried, speaking slowly. Tone: stable with encouraging rises. Emotion: caring, patient, encouraging. Character: understanding, guiding, full of positive energy.",
|
||||
"text": "Everyone has their own pace, don't rush. Take your time, you will definitely find your own path."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Full of energy. Pace: fast, crisp articulation. Emotion: cheerful and optimistic, energetic. Tone: strong sense of rhythm, cadence. Character: outgoing, confident, enthusiastic, full of youthful spirit.",
|
||||
"text": "Wow, this game is so cool! Let's team up and play together, I promise I'll carry you!"
|
||||
}
|
||||
],
|
||||
"presetVoiceDesigns": [
|
||||
{
|
||||
"label": "Sweet Girl",
|
||||
"instruct": "Young female, sweet and bright voice, with a touch of girlish charm. High pitch, lively and varied intonation. Moderate pace, clear articulation. Cheerful and relaxed emotion, full of youthful energy. Suitable for: customer service, voice assistant, entertainment content.",
|
||||
"text": "Hello, I'm happy to help you! How may I assist you today?"
|
||||
},
|
||||
{
|
||||
"label": "Mature Woman",
|
||||
"instruct": "Mature and intellectual female voice, warm and full tone, with professional woman's capable temperament. Medium pitch, stable range. Moderate to fast pace, clear and organized. Calm and confident emotion, conveying professionalism and reliability.",
|
||||
"text": "According to the latest market analysis report, this quarter's performance shows steady growth, with all indicators meeting expected targets."
|
||||
},
|
||||
{
|
||||
"label": "Magnetic Male",
|
||||
"instruct": "Mid-low male voice, deep and magnetic tone, very appealing. Slow pace, steady rhythm. Moderate volume, thick and powerful voice. Suitable for emotional content, storytelling, brand promotion.",
|
||||
"text": "Night falls, yet the city lights remain brilliant. Under each light, there's a story about dreams."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Energetic young male, bright and clear tone, with youthful vigor. Fast pace, strong sense of rhythm. Enthusiastic and positive emotion, very appealing. Suitable for sports, gaming, entertainment.",
|
||||
"text": "Brothers, are you ready? Today we're going to challenge the new dungeon, let's go!"
|
||||
},
|
||||
{
|
||||
"label": "Authority Expert",
|
||||
"instruct": "Middle-aged male expert image, calm and authoritative tone, thick and powerful voice. Moderate pace, clear and standard articulation. Serious and professional emotion, conveying trust and expertise. Suitable for academic lectures, knowledge popularization, formal occasions.",
|
||||
"text": "From a historical development perspective, technological innovation has always been the core driving force for social progress."
|
||||
},
|
||||
{
|
||||
"label": "Gentle Mother",
|
||||
"instruct": "Gentle and loving middle-aged female, soft and warm tone, full of maternal care. Slow pace, calm and soothing tone. Warm and caring emotion, giving a sense of security. Suitable for children's content, emotional companionship, bedtime stories.",
|
||||
"text": "Sweetie, it's time to sleep. Mom will tell you a story. Once upon a time, there was a little rabbit who lived in the forest..."
|
||||
},
|
||||
{
|
||||
"label": "Broadcasting Host",
|
||||
"instruct": "Professional broadcasting host voice, full and round tone, standard pronunciation. Medium pitch, wide range. Standard pace, precise rhythm control. Professional and calm emotion, clear articulation. Suitable for news broadcasting, program hosting, formal reading.",
|
||||
"text": "Hello dear listeners, welcome to today's program. Next, we bring you today's news."
|
||||
},
|
||||
{
|
||||
"label": "Playful Girl",
|
||||
"instruct": "Playful and cute girl voice, light and lively sound, with unique girlish liveliness. High and varied tone, with coquettish and cute elements. Varying pace, clear articulation with cute interjections.",
|
||||
"text": "Oh no, I didn't mean to~ Can you forgive me this time? Please please~"
|
||||
}
|
||||
],
|
||||
"presetRefTexts": [
|
||||
{
|
||||
"label": "Natural Life",
|
||||
"text": "In this fast-paced world, we're always rushing forward, forgetting to pause and listen to our inner voice. Life is not just about the busyness before us, but also the poetry in the distance and the little moments of happiness we discover. May this recording bring you a touch of gentleness and strength, like an afternoon breeze. No matter how the future changes, remember to keep your love for life and embrace every bright tomorrow."
|
||||
},
|
||||
{
|
||||
"label": "Professional Formal",
|
||||
"text": "Technological progress allows us to transcend the boundaries of time and space, continuing emotions and memories through digitalization. Voice cloning is not only precise code logic, but also a bridge connecting humanity with future intelligence. Through the continuous evolution of deep learning and neural networks, every subtle intonation can be accurately captured. Let us witness together how technology gives voice a more vital expression."
|
||||
},
|
||||
{
|
||||
"label": "Literary Narrative",
|
||||
"text": "The spring breeze brushes the willow tips, carrying the fragrance of earth and news of blooming flowers. Have you ever anticipated meeting your long-lost self at some street corner? Whether it's hearty laughter or low whispers, each voice is a unique mark of life. Let us record this moment, let memories flow in the sound, becoming an eternal melody."
|
||||
}
|
||||
]
|
||||
}
|
||||
42
qwen3-tts-frontend/src/locales/en-US/errors.json
Normal file
42
qwen3-tts-frontend/src/locales/en-US/errors.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"http": {
|
||||
"400": "Invalid request parameters",
|
||||
"401": "Unauthorized, please login",
|
||||
"403": "Access forbidden",
|
||||
"404": "Requested resource not found",
|
||||
"500": "Internal server error",
|
||||
"502": "Gateway error",
|
||||
"503": "Service temporarily unavailable",
|
||||
"default": "Request failed, please try again later"
|
||||
},
|
||||
"fieldNames": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"text": "Text",
|
||||
"language": "Language",
|
||||
"speaker": "Speaker",
|
||||
"instruct": "Emotion instruction",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"audio_file": "Audio file",
|
||||
"reference_audio": "Reference audio",
|
||||
"reference_text": "Reference text",
|
||||
"api_key": "API key",
|
||||
"backend": "Backend service"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{{field}} is required",
|
||||
"minLength": "{{field}} must be at least {{min}} characters",
|
||||
"maxLength": "{{field}} cannot exceed {{max}} characters",
|
||||
"invalid": "{{field}} format is invalid",
|
||||
"notFound": "{{field}} not found",
|
||||
"alreadyExists": "{{field}} already exists",
|
||||
"uploadFailed": "{{field}} upload failed",
|
||||
"fileTooLarge": "File size cannot exceed {{size}}MB",
|
||||
"invalidFileType": "Unsupported file type"
|
||||
},
|
||||
"networkError": "Network connection failed, please check your network",
|
||||
"unknownError": "Unknown error",
|
||||
"operationFailed": "Operation failed",
|
||||
"tryAgain": "Please try again"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/en-US/index.ts
Normal file
25
qwen3-tts-frontend/src/locales/en-US/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import common from './common.json'
|
||||
import nav from './nav.json'
|
||||
import auth from './auth.json'
|
||||
import tts from './tts.json'
|
||||
import voice from './voice.json'
|
||||
import job from './job.json'
|
||||
import settings from './settings.json'
|
||||
import user from './user.json'
|
||||
import errors from './errors.json'
|
||||
import constants from './constants.json'
|
||||
import onboarding from './onboarding.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
nav,
|
||||
auth,
|
||||
tts,
|
||||
voice,
|
||||
job,
|
||||
settings,
|
||||
user,
|
||||
errors,
|
||||
constants,
|
||||
onboarding,
|
||||
}
|
||||
55
qwen3-tts-frontend/src/locales/en-US/job.json
Normal file
55
qwen3-tts-frontend/src/locales/en-US/job.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"jobHistory": "Job History",
|
||||
"jobList": "Job List",
|
||||
"jobDetails": "Job Details",
|
||||
"jobId": "Job ID",
|
||||
"status": "Status",
|
||||
"createdAt": "Created At",
|
||||
"completedAt": "Completed At",
|
||||
"duration": "Duration",
|
||||
"statusPending": "Pending",
|
||||
"statusProcessing": "Processing",
|
||||
"statusCompleted": "Completed",
|
||||
"statusFailed": "Failed",
|
||||
"noJobs": "No jobs",
|
||||
"viewJob": "View Job",
|
||||
"deleteJob": "Delete Job",
|
||||
"deleteJobConfirm": "Are you sure you want to delete this job?",
|
||||
"jobDeleted": "Job deleted",
|
||||
"refreshJobs": "Refresh Jobs",
|
||||
"inputText": "Input Text",
|
||||
"parameters": "Parameters",
|
||||
"result": "Result",
|
||||
"errorMessage": "Error Message",
|
||||
"downloadResult": "Download Result",
|
||||
"retryJob": "Retry",
|
||||
"cancelJob": "Cancel Job",
|
||||
"historyTitle": "History",
|
||||
"historyCount": "{{count}} records",
|
||||
"retry": "Retry",
|
||||
"noHistory": "No history",
|
||||
"historyDescription": "Records will appear here after generating speech",
|
||||
"detailsDescription": "View task details and generation results",
|
||||
"basicInfo": "Basic Information",
|
||||
"speaker": "Speaker: ",
|
||||
"language": "Language: ",
|
||||
"autoDetect": "Auto Detect",
|
||||
"fastMode": "Fast Mode: ",
|
||||
"useCache": "Use Cache: ",
|
||||
"synthesisText": "Synthesis Text",
|
||||
"notSet": "Not set",
|
||||
"voiceDescription": "Voice Description",
|
||||
"emotionGuidance": "Emotion Guidance",
|
||||
"referenceText": "Reference Text",
|
||||
"notProvided": "Not provided",
|
||||
"advancedParameters": "Advanced Parameters",
|
||||
"maxNewTokens": "Max New Tokens: ",
|
||||
"temperature": "Temperature: ",
|
||||
"topK": "Top K: ",
|
||||
"topP": "Top P: ",
|
||||
"repetitionPenalty": "Repetition Penalty: ",
|
||||
"audioPlayback": "Audio Playback",
|
||||
"typeCustomVoice": "Custom Voice",
|
||||
"typeVoiceDesign": "Voice Design",
|
||||
"typeVoiceClone": "Voice Clone"
|
||||
}
|
||||
12
qwen3-tts-frontend/src/locales/en-US/nav.json
Normal file
12
qwen3-tts-frontend/src/locales/en-US/nav.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"userManagement": "User Management",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"toggleTheme": "Toggle Theme",
|
||||
"changeLanguage": "Change Language",
|
||||
"customVoiceTab": "Custom",
|
||||
"voiceDesignTab": "Design",
|
||||
"voiceCloneTab": "Clone"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/en-US/onboarding.json
Normal file
25
qwen3-tts-frontend/src/locales/en-US/onboarding.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"welcome": "Welcome to Qwen3 TTS",
|
||||
"configureApiKey": "Configure Aliyun API Key",
|
||||
"selectBackendDescription": "Please select your TTS backend mode, you can change it later in settings",
|
||||
"enterApiKeyDescription": "Please enter your Aliyun API key, the system will verify its validity",
|
||||
"localModel": "Local Model",
|
||||
"localModelDescription": "Free to use local Qwen3-TTS model",
|
||||
"localModelNoPermission": "No local model permission, please contact administrator",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiRecommended": "(Recommended)",
|
||||
"aliyunApiDescription": "Requires API key configuration, pay-as-you-go",
|
||||
"skipConfig": "Skip Configuration",
|
||||
"nextStep": "Next",
|
||||
"back": "Back",
|
||||
"verifying": "Verifying...",
|
||||
"verifyAndComplete": "Verify and Complete",
|
||||
"apiKey": "API Key",
|
||||
"howToGetApiKey": "How to get API key?",
|
||||
"skipSuccess": "Configuration skipped, using local mode by default",
|
||||
"operationFailed": "Operation failed, please retry",
|
||||
"configComplete": "Configuration complete, using local mode by default",
|
||||
"configCompleteAliyun": "Configuration complete, using Aliyun mode by default",
|
||||
"saveFailed": "Failed to save configuration, please retry",
|
||||
"verifyFailed": "API key verification failed, please check and retry"
|
||||
}
|
||||
61
qwen3-tts-frontend/src/locales/en-US/settings.json
Normal file
61
qwen3-tts-frontend/src/locales/en-US/settings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"settings": "Settings",
|
||||
"generalSettings": "General Settings",
|
||||
"backendSettings": "Backend Settings",
|
||||
"apiSettings": "API Settings",
|
||||
"preferencesSaved": "Preferences saved",
|
||||
"preferencesSaveFailed": "Preferences save failed",
|
||||
"defaultBackend": "Default Backend",
|
||||
"local": "Local Model",
|
||||
"aliyun": "Aliyun API",
|
||||
"aliyunApiKey": "Aliyun API Key",
|
||||
"apiKeyPlaceholder": "Enter API key",
|
||||
"apiKeyDescription": "Used for Aliyun TTS service authentication",
|
||||
"saveApiKey": "Save Key",
|
||||
"apiKeySaved": "API key saved",
|
||||
"apiKeySaveFailed": "API key save failed",
|
||||
"showApiKey": "Show Key",
|
||||
"hideApiKey": "Hide Key",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionSuccess": "Connection successful",
|
||||
"connectionFailed": "Connection failed",
|
||||
"language": "Interface Language",
|
||||
"languageDescription": "Select interface display language",
|
||||
"theme": "Theme",
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"themeSystem": "System",
|
||||
"title": "Settings",
|
||||
"description": "Manage your account settings and preferences",
|
||||
"backendPreference": "Backend Preference",
|
||||
"backendPreferenceDescription": "Choose default TTS backend mode",
|
||||
"localModel": "Local Model",
|
||||
"localModelDescription": "Free to use local Qwen3-TTS model",
|
||||
"localModelNoPermission": "Please contact admin to enable local model access",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiDescription": "Use Aliyun TTS service",
|
||||
"switchedToLocal": "Switched to local mode",
|
||||
"switchedToAliyun": "Switched to Aliyun mode",
|
||||
"saveFailed": "Save failed, please retry",
|
||||
"apiKeyUpdated": "API key updated and verified successfully",
|
||||
"apiKeyVerifyFailed": "API key verification failed",
|
||||
"verifyFailed": "Verification failed",
|
||||
"currentStatus": "Current Status:",
|
||||
"configured": "Configured and valid",
|
||||
"notConfigured": "Not configured",
|
||||
"apiKey": "API Key",
|
||||
"updating": "Updating...",
|
||||
"addKey": "Add Key",
|
||||
"updateKey": "Update Key",
|
||||
"verifyKey": "Verify Key",
|
||||
"deleteKey": "Delete Key",
|
||||
"deleteKeyConfirm": "Are you sure you want to delete Aliyun API key? Will switch to local mode automatically.",
|
||||
"keyDeleted": "API key deleted, switched to local mode",
|
||||
"deleteFailed": "Delete failed",
|
||||
"accountInfo": "Account Information",
|
||||
"accountInfoDescription": "Your account basic information",
|
||||
"email": "Email",
|
||||
"changePassword": "Change Password",
|
||||
"passwordChangeSuccess": "Password changed successfully",
|
||||
"passwordChangeFailed": "Password change failed"
|
||||
}
|
||||
80
qwen3-tts-frontend/src/locales/en-US/tts.json
Normal file
80
qwen3-tts-frontend/src/locales/en-US/tts.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"customVoice": "Custom Voice",
|
||||
"voiceDesign": "Voice Design",
|
||||
"voiceClone": "Voice Clone",
|
||||
"text": "Text",
|
||||
"textPlaceholder": "Enter text to synthesize...",
|
||||
"language": "Language",
|
||||
"speaker": "Speaker",
|
||||
"instruct": "Emotion Instruction",
|
||||
"instructPlaceholder": "e.g., very happy, a bit sad...",
|
||||
"customSpeaker": "Custom Speaker",
|
||||
"customSpeakerPlaceholder": "e.g., Vivian, Alice...",
|
||||
"presetInstructs": "Preset Emotions",
|
||||
"generate": "Generate Speech",
|
||||
"generating": "Generating...",
|
||||
"generationSuccess": "Speech generated successfully",
|
||||
"generationFailed": "Speech generation failed",
|
||||
"audioPlayer": "Audio Player",
|
||||
"noAudio": "No audio",
|
||||
"downloadAudio": "Download Audio",
|
||||
"playAudio": "Play",
|
||||
"pauseAudio": "Pause",
|
||||
"backend": "Backend Service",
|
||||
"localBackend": "Local Model",
|
||||
"aliyunBackend": "Aliyun API",
|
||||
"backendSwitched": "Backend switched",
|
||||
"backendError": "Backend service error",
|
||||
"languageLabel": "Language",
|
||||
"speakerLabel": "Speaker",
|
||||
"speakerPlaceholder": "Select speaker",
|
||||
"textLabel": "Text to synthesize",
|
||||
"instructLabel": "Emotion guidance (optional)",
|
||||
"instructPlaceholderDesign": "Using preset guidance from voice design",
|
||||
"instructPlaceholderDefault": "e.g., gentle and caring, slow pace, full of warmth",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"advancedOptionsTitle": "Advanced Parameter Settings",
|
||||
"advancedOptionsDescription": "Adjust generation parameters to control audio quality and generation length",
|
||||
"creating": "Creating...",
|
||||
"taskCreated": "Task created",
|
||||
"taskCreateFailed": "Failed to create task",
|
||||
"loadDataFailed": "Failed to load data",
|
||||
"myVoiceDesigns": "My Voice Designs",
|
||||
"builtinSpeakers": "Built-in Speakers",
|
||||
"designDescriptionLabel": "Voice description",
|
||||
"designDescriptionPlaceholder": "e.g., Mature male, deep and magnetic, authoritative",
|
||||
"saveDesignButton": "Save Voice Design",
|
||||
"saveDesignTitle": "Save Voice Design",
|
||||
"saveDesignDescription": "Name and save the current voice design for quick future use",
|
||||
"designNameLabel": "Design name",
|
||||
"designNamePlaceholder": "e.g., Magnetic Male Voice",
|
||||
"preparing": "Preparing...",
|
||||
"designSaved": "Voice design saved",
|
||||
"clonePrepared": "Voice clone preparation completed",
|
||||
"clonePrepareFailed": "Clone preparation failed, but design saved",
|
||||
"saveFailed": "Save failed",
|
||||
"fillDesignDescription": "Please fill in voice description first",
|
||||
"fillDesignName": "Please enter design name",
|
||||
"advancedParams": {
|
||||
"maxNewTokens": {
|
||||
"label": "Max Generation Length",
|
||||
"description": "Controls the maximum length of generated audio. Higher values allow longer audio but increase processing time"
|
||||
},
|
||||
"temperature": {
|
||||
"label": "Temperature",
|
||||
"description": "Controls generation randomness. Higher values produce more varied output, lower values are more stable and consistent"
|
||||
},
|
||||
"topK": {
|
||||
"label": "Top K",
|
||||
"description": "Only considers the K most probable candidates during sampling. Smaller values are more deterministic, larger values are more diverse"
|
||||
},
|
||||
"topP": {
|
||||
"label": "Top P (Nucleus Sampling)",
|
||||
"description": "Cumulative probability threshold for sampling. Controls output diversity by sampling from candidates with cumulative probability up to P"
|
||||
},
|
||||
"repetitionPenalty": {
|
||||
"label": "Repetition Penalty",
|
||||
"description": "Penalizes repetitive content generation. Higher values avoid repetition more, but excessive values may affect naturalness"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/en-US/user.json
Normal file
59
qwen3-tts-frontend/src/locales/en-US/user.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"userManagement": "User Management",
|
||||
"userList": "User List",
|
||||
"userId": "User ID",
|
||||
"username": "Username",
|
||||
"role": "Role",
|
||||
"createdAt": "Created At",
|
||||
"lastLogin": "Last Login",
|
||||
"actions": "Actions",
|
||||
"addUser": "Add User",
|
||||
"editUser": "Edit User",
|
||||
"deleteUser": "Delete User",
|
||||
"deleteUserConfirm": "Are you sure you want to delete user {{username}}?",
|
||||
"userDeleted": "User deleted",
|
||||
"userAdded": "User added",
|
||||
"userUpdated": "User updated",
|
||||
"userOperationFailed": "User operation failed",
|
||||
"roleAdmin": "Admin",
|
||||
"roleUser": "User",
|
||||
"password": "Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"passwordMismatch": "Passwords do not match",
|
||||
"changePassword": "Change Password",
|
||||
"passwordChanged": "Password changed",
|
||||
"noUsers": "No users",
|
||||
"searchUsers": "Search Users",
|
||||
"filterByRole": "Filter by Role",
|
||||
"allRoles": "All Roles",
|
||||
"createUser": "Create User",
|
||||
"loadUsersFailed": "Failed to load user list",
|
||||
"userUpdateSuccess": "User updated successfully",
|
||||
"userCreateSuccess": "User created successfully",
|
||||
"operationFailed": "Operation failed",
|
||||
"userDeleteSuccess": "User deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"createUserDialog": "Create User",
|
||||
"editUserDialog": "Edit User",
|
||||
"createUserDescription": "Create new user and configure basic information",
|
||||
"editUserDescription": "Modify user information and permission settings",
|
||||
"email": "Email",
|
||||
"passwordOptional": "Password (leave blank to keep unchanged)",
|
||||
"isActive": "Active Status",
|
||||
"isSuperuser": "Super Administrator",
|
||||
"canUseLocalModel": "Local Model Permission",
|
||||
"canUseLocalModelDescription": "Allow user to use local TTS model",
|
||||
"saving": "Saving...",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"superuser": "Super Admin",
|
||||
"normalUser": "Normal User",
|
||||
"localModelPermission": "Local Model",
|
||||
"noPermission": "None",
|
||||
"validation": {
|
||||
"usernameMinLength": "Username must be at least 3 characters",
|
||||
"usernameMaxLength": "Username cannot exceed 20 characters",
|
||||
"emailInvalid": "Please enter a valid email address"
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/en-US/voice.json
Normal file
59
qwen3-tts-frontend/src/locales/en-US/voice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"voiceDesign": "Voice Design",
|
||||
"voiceClone": "Voice Clone",
|
||||
"designName": "Voice Name",
|
||||
"designNamePlaceholder": "Enter voice name",
|
||||
"designDescription": "Voice Description",
|
||||
"designDescriptionPlaceholder": "Describe voice characteristics...",
|
||||
"referenceAudio": "Reference Audio",
|
||||
"uploadReference": "Upload Reference Audio",
|
||||
"referenceText": "Reference Text",
|
||||
"referenceTextPlaceholder": "Enter text content of reference audio...",
|
||||
"cloneName": "Clone Name",
|
||||
"cloneNamePlaceholder": "Enter clone voice name",
|
||||
"cloneDescription": "Clone Description",
|
||||
"cloneDescriptionPlaceholder": "Describe clone voice...",
|
||||
"uploadAudio": "Upload Audio",
|
||||
"audioFile": "Audio File",
|
||||
"audioText": "Audio Text",
|
||||
"audioTextPlaceholder": "Enter text corresponding to audio...",
|
||||
"saveVoice": "Save Voice",
|
||||
"savingVoice": "Saving...",
|
||||
"voiceSaved": "Voice saved",
|
||||
"voiceSaveFailed": "Voice save failed",
|
||||
"deleteVoice": "Delete Voice",
|
||||
"deleteVoiceConfirm": "Are you sure you want to delete this voice?",
|
||||
"voiceDeleted": "Voice deleted",
|
||||
"voiceList": "Voice List",
|
||||
"noVoices": "No voices",
|
||||
"selectVoice": "Select Voice",
|
||||
"voiceDetails": "Voice Details",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"step1Title": "Audio Material",
|
||||
"step2Title": "Synthesis Settings",
|
||||
"uploadTab": "Upload Audio",
|
||||
"recordTab": "Record Online",
|
||||
"refAudioLabel": "Reference Audio File",
|
||||
"refTextLabel": "Reference transcript (optional, improves accuracy)",
|
||||
"refTextPlaceholder": "Text content corresponding to reference audio...",
|
||||
"nextStep": "Next",
|
||||
"prevStep": "Previous",
|
||||
"readPrompt": "Please read one of the following paragraphs:",
|
||||
"currentRefText": "Current reference text",
|
||||
"currentRefTextPlaceholder": "Selected text will appear here...",
|
||||
"languageOptional": "Language (optional)",
|
||||
"fastMode": "Fast mode",
|
||||
"useCache": "Use cache",
|
||||
"uploadAudioFile": "Upload Audio",
|
||||
"recordOnline": "Record Online",
|
||||
"validationFailed": "File validation failed",
|
||||
"validating": "Validating...",
|
||||
"selectAudioFile": "Select audio file",
|
||||
"seconds": "seconds",
|
||||
"recordingValidationFailed": "Recording validation failed",
|
||||
"browserNotSupported": "Your browser does not support recording",
|
||||
"recordingComplete": "Recording complete",
|
||||
"releaseToFinish": "Release to finish",
|
||||
"holdToRecord": "Hold to record"
|
||||
}
|
||||
34
qwen3-tts-frontend/src/locales/index.ts
Normal file
34
qwen3-tts-frontend/src/locales/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
|
||||
import zhCN from './zh-CN'
|
||||
import zhTW from './zh-TW'
|
||||
import enUS from './en-US'
|
||||
import jaJP from './ja-JP'
|
||||
import koKR from './ko-KR'
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW,
|
||||
'en-US': enUS,
|
||||
'ja-JP': jaJP,
|
||||
'ko-KR': koKR,
|
||||
},
|
||||
fallbackLng: 'zh-CN',
|
||||
debug: false,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
25
qwen3-tts-frontend/src/locales/ja-JP/auth.json
Normal file
25
qwen3-tts-frontend/src/locales/ja-JP/auth.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"login": "ログイン",
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"loginButton": "ログイン",
|
||||
"loggingIn": "ログイン中...",
|
||||
"welcome": "Qwen TTSへようこそ",
|
||||
"loginPrompt": "続行するにはログインしてください",
|
||||
"loginSuccess": "ログインしました",
|
||||
"loginFailed": "ログインに失敗しました",
|
||||
"loginFailedCheckCredentials": "ログインに失敗しました。ユーザー名とパスワードを確認してください",
|
||||
"logoutSuccess": "ログアウトしました",
|
||||
"unauthorized": "未認証です。ログインしてください",
|
||||
"sessionExpired": "セッションが期限切れです。再度ログインしてください",
|
||||
"noPermission": "この操作を実行する権限がありません",
|
||||
"adminOnly": "この機能は管理者のみ利用できます",
|
||||
"usernamePlaceholder": "ユーザー名を入力",
|
||||
"passwordPlaceholder": "パスワードを入力",
|
||||
"validation": {
|
||||
"usernameMinLength": "ユーザー名は {{min}} 文字以上である必要があります",
|
||||
"usernameMaxLength": "ユーザー名は {{max}} 文字以下である必要があります",
|
||||
"passwordMinLength": "パスワードは {{min}} 文字以上である必要があります",
|
||||
"apiKeyRequired": "APIキーを入力してください"
|
||||
}
|
||||
}
|
||||
40
qwen3-tts-frontend/src/locales/ja-JP/common.json
Normal file
40
qwen3-tts-frontend/src/locales/ja-JP/common.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "確認",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"add": "追加",
|
||||
"create": "作成",
|
||||
"update": "更新",
|
||||
"submit": "送信",
|
||||
"close": "閉じる",
|
||||
"back": "戻る",
|
||||
"next": "次へ",
|
||||
"previous": "前へ",
|
||||
"search": "検索",
|
||||
"filter": "絞り込み",
|
||||
"clear": "クリア",
|
||||
"reset": "リセット",
|
||||
"loading": "読み込み中...",
|
||||
"noData": "データがありません",
|
||||
"success": "成功",
|
||||
"error": "エラー",
|
||||
"warning": "警告",
|
||||
"info": "情報",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"ok": "OK",
|
||||
"download": "ダウンロード",
|
||||
"upload": "アップロード",
|
||||
"copy": "コピー",
|
||||
"copied": "コピーしました",
|
||||
"view": "表示",
|
||||
"details": "詳細",
|
||||
"actions": "操作",
|
||||
"generatingAudio": "音声を生成中です、お待ちください...",
|
||||
"generationTakingLong": "生成に時間がかかっています、しばらくお待ちください...",
|
||||
"waitedSeconds": "{{seconds}}秒待機中",
|
||||
"loadingAudio": "読み込み中...",
|
||||
"failedToLoadAudio": "音声の読み込みに失敗しました"
|
||||
}
|
||||
152
qwen3-tts-frontend/src/locales/ja-JP/constants.json
Normal file
152
qwen3-tts-frontend/src/locales/ja-JP/constants.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"languages": {
|
||||
"Auto": "自動検出",
|
||||
"Chinese": "中国語",
|
||||
"English": "英語",
|
||||
"Japanese": "日本語",
|
||||
"Korean": "韓国語",
|
||||
"German": "ドイツ語",
|
||||
"French": "フランス語",
|
||||
"Russian": "ロシア語",
|
||||
"Portuguese": "ポルトガル語",
|
||||
"Spanish": "スペイン語",
|
||||
"Italian": "イタリア語",
|
||||
"Cantonese": "広東語",
|
||||
"zh": "中国語",
|
||||
"en": "英語",
|
||||
"ja": "日本語",
|
||||
"ko": "韓国語",
|
||||
"yue": "広東語"
|
||||
},
|
||||
"speakers": {
|
||||
"Vivian": "女性、プロフェッショナルで明瞭",
|
||||
"Serena": "女性、優しく温かい",
|
||||
"Aria": "女性、活発で明るい",
|
||||
"Emma": "女性、成熟して落ち着いた",
|
||||
"Sophie": "女性、優雅で知的",
|
||||
"Isabella": "女性、穏やかで親しみやすい",
|
||||
"Ava": "女性、若々しくスタイリッシュ",
|
||||
"Oliver": "男性、磁性のある落ち着いた",
|
||||
"Lucas": "男性、明るく爽やか",
|
||||
"Ethan": "男性、プロフェッショナルで堂々とした",
|
||||
"Noah": "男性、温和で親しみやすい",
|
||||
"Liam": "男性、若々しく活力的"
|
||||
},
|
||||
"presetInstructs": [
|
||||
{
|
||||
"label": "Happy",
|
||||
"instruct": "very happy",
|
||||
"text": "The weather is so nice today, let's go to the park together!"
|
||||
},
|
||||
{
|
||||
"label": "Sad",
|
||||
"instruct": "very sad, with a crying tone",
|
||||
"text": "I'm sorry, I really tried my best, but I still let you down."
|
||||
},
|
||||
{
|
||||
"label": "Angry",
|
||||
"instruct": "very angry, with intense tone",
|
||||
"text": "How could you do this! This is absolutely unacceptable!"
|
||||
},
|
||||
{
|
||||
"label": "Gentle Care",
|
||||
"instruct": "gentle and caring, slow pace, soft tone, full of care and comfort",
|
||||
"text": "Don't worry, everything will be fine. I'll always be here with you."
|
||||
},
|
||||
{
|
||||
"label": "Excited",
|
||||
"instruct": "very excited, faster pace, rising tone, full of energy and enthusiasm",
|
||||
"text": "Awesome! We finally made it! This is so exciting!"
|
||||
},
|
||||
{
|
||||
"label": "Anxious",
|
||||
"instruct": "anxious tone, slightly faster pace, unstable tone, with tension and worry",
|
||||
"text": "What should we do? We're running out of time, we won't make it, what can we do?"
|
||||
},
|
||||
{
|
||||
"label": "Professional Broadcaster",
|
||||
"instruct": "Professional news broadcaster. Pace: standard broadcasting speed, clear articulation. Emotion: calm and professional, without personal emotion. Tone: mostly flat with slight variations, emphasis on key words. Character: rigorous, objective, authoritative.",
|
||||
"text": "According to Reuters, our space program has achieved a major breakthrough, with the successful completion of manned space missions."
|
||||
},
|
||||
{
|
||||
"label": "Warm Mentor",
|
||||
"instruct": "Warm mentor. Pace: unhurried, speaking slowly. Tone: stable with encouraging rises. Emotion: caring, patient, encouraging. Character: understanding, guiding, full of positive energy.",
|
||||
"text": "Everyone has their own pace, don't rush. Take your time, you will definitely find your own path."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Full of energy. Pace: fast, crisp articulation. Emotion: cheerful and optimistic, energetic. Tone: strong sense of rhythm, cadence. Character: outgoing, confident, enthusiastic, full of youthful spirit.",
|
||||
"text": "Wow, this game is so cool! Let's team up and play together, I promise I'll carry you!"
|
||||
}
|
||||
],
|
||||
"presetVoiceDesigns": [
|
||||
{
|
||||
"label": "Sweet Girl",
|
||||
"instruct": "Young female, sweet and bright voice, with a touch of girlish charm. High pitch, lively and varied intonation. Moderate pace, clear articulation. Cheerful and relaxed emotion, full of youthful energy. Suitable for: customer service, voice assistant, entertainment content.",
|
||||
"text": "Hello, I'm happy to help you! How may I assist you today?"
|
||||
},
|
||||
{
|
||||
"label": "Mature Woman",
|
||||
"instruct": "Mature and intellectual female voice, warm and full tone, with professional woman's capable temperament. Medium pitch, stable range. Moderate to fast pace, clear and organized. Calm and confident emotion, conveying professionalism and reliability.",
|
||||
"text": "According to the latest market analysis report, this quarter's performance shows steady growth, with all indicators meeting expected targets."
|
||||
},
|
||||
{
|
||||
"label": "Magnetic Male",
|
||||
"instruct": "Mid-low male voice, deep and magnetic tone, very appealing. Slow pace, steady rhythm. Moderate volume, thick and powerful voice. Suitable for emotional content, storytelling, brand promotion.",
|
||||
"text": "Night falls, yet the city lights remain brilliant. Under each light, there's a story about dreams."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Energetic young male, bright and clear tone, with youthful vigor. Fast pace, strong sense of rhythm. Enthusiastic and positive emotion, very appealing. Suitable for sports, gaming, entertainment.",
|
||||
"text": "Brothers, are you ready? Today we're going to challenge the new dungeon, let's go!"
|
||||
},
|
||||
{
|
||||
"label": "Authority Expert",
|
||||
"instruct": "Middle-aged male expert image, calm and authoritative tone, thick and powerful voice. Moderate pace, clear and standard articulation. Serious and professional emotion, conveying trust and expertise. Suitable for academic lectures, knowledge popularization, formal occasions.",
|
||||
"text": "From a historical development perspective, technological innovation has always been the core driving force for social progress."
|
||||
},
|
||||
{
|
||||
"label": "Gentle Mother",
|
||||
"instruct": "Gentle and loving middle-aged female, soft and warm tone, full of maternal care. Slow pace, calm and soothing tone. Warm and caring emotion, giving a sense of security. Suitable for children's content, emotional companionship, bedtime stories.",
|
||||
"text": "Sweetie, it's time to sleep. Mom will tell you a story. Once upon a time, there was a little rabbit who lived in the forest..."
|
||||
},
|
||||
{
|
||||
"label": "Broadcasting Host",
|
||||
"instruct": "Professional broadcasting host voice, full and round tone, standard pronunciation. Medium pitch, wide range. Standard pace, precise rhythm control. Professional and calm emotion, clear articulation. Suitable for news broadcasting, program hosting, formal reading.",
|
||||
"text": "Hello dear listeners, welcome to today's program. Next, we bring you today's news."
|
||||
},
|
||||
{
|
||||
"label": "Playful Girl",
|
||||
"instruct": "Playful and cute girl voice, light and lively sound, with unique girlish liveliness. High and varied tone, with coquettish and cute elements. Varying pace, clear articulation with cute interjections.",
|
||||
"text": "Oh no, I didn't mean to~ Can you forgive me this time? Please please~"
|
||||
}
|
||||
],
|
||||
"presetRefTexts": [
|
||||
{
|
||||
"label": "Natural Life",
|
||||
"text": "In this fast-paced world, we're always rushing forward, forgetting to pause and listen to our inner voice. Life is not just about the busyness before us, but also the poetry in the distance and the little moments of happiness we discover. May this recording bring you a touch of gentleness and strength, like an afternoon breeze. No matter how the future changes, remember to keep your love for life and embrace every bright tomorrow."
|
||||
},
|
||||
{
|
||||
"label": "Professional Formal",
|
||||
"text": "Technological progress allows us to transcend the boundaries of time and space, continuing emotions and memories through digitalization. Voice cloning is not only precise code logic, but also a bridge connecting humanity with future intelligence. Through the continuous evolution of deep learning and neural networks, every subtle intonation can be accurately captured. Let us witness together how technology gives voice a more vital expression."
|
||||
},
|
||||
{
|
||||
"label": "Literary Narrative",
|
||||
"text": "The spring breeze brushes the willow tips, carrying the fragrance of earth and news of blooming flowers. Have you ever anticipated meeting your long-lost self at some street corner? Whether it's hearty laughter or low whispers, each voice is a unique mark of life. Let us record this moment, let memories flow in the sound, becoming an eternal melody."
|
||||
}
|
||||
],
|
||||
"uiLanguages": {
|
||||
"zh-CN": "Simplified Chinese",
|
||||
"zh-TW": "Traditional Chinese",
|
||||
"en-US": "English",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean"
|
||||
},
|
||||
"uiLanguagesShort": {
|
||||
"zh-CN": "ZH-CN",
|
||||
"zh-TW": "ZH-TW",
|
||||
"en-US": "EN",
|
||||
"ja-JP": "JA",
|
||||
"ko-KR": "KO"
|
||||
}
|
||||
}
|
||||
42
qwen3-tts-frontend/src/locales/ja-JP/errors.json
Normal file
42
qwen3-tts-frontend/src/locales/ja-JP/errors.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"http": {
|
||||
"400": "リクエストパラメータエラー",
|
||||
"401": "認証されていません。ログインしてください",
|
||||
"403": "アクセスが禁止されています",
|
||||
"404": "リクエストされたリソースが存在しません",
|
||||
"500": "サーバー内部エラー",
|
||||
"502": "ゲートウェイエラー",
|
||||
"503": "サービスが一時的に利用できません",
|
||||
"default": "リクエストに失敗しました。後でもう一度お試しください"
|
||||
},
|
||||
"fieldNames": {
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"text": "テキスト",
|
||||
"language": "言語",
|
||||
"speaker": "話者",
|
||||
"instruct": "感情指示",
|
||||
"name": "名前",
|
||||
"description": "説明",
|
||||
"audio_file": "オーディオファイル",
|
||||
"reference_audio": "参照オーディオ",
|
||||
"reference_text": "参照テキスト",
|
||||
"api_key": "APIキー",
|
||||
"backend": "バックエンドサービス"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{{field}}を入力してください",
|
||||
"minLength": "{{field}}は少なくとも{{min}}文字必要です",
|
||||
"maxLength": "{{field}}は{{max}}文字を超えることはできません",
|
||||
"invalid": "{{field}}の形式が正しくありません",
|
||||
"notFound": "{{field}}が見つかりません",
|
||||
"alreadyExists": "{{field}}は既に存在します",
|
||||
"uploadFailed": "{{field}}のアップロードに失敗しました",
|
||||
"fileTooLarge": "ファイルサイズは{{size}}MBを超えることはできません",
|
||||
"invalidFileType": "サポートされていないファイルタイプです"
|
||||
},
|
||||
"networkError": "ネットワーク接続に失敗しました。ネットワークを確認してください",
|
||||
"unknownError": "不明なエラー",
|
||||
"operationFailed": "操作に失敗しました",
|
||||
"tryAgain": "もう一度お試しください"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/ja-JP/index.ts
Normal file
25
qwen3-tts-frontend/src/locales/ja-JP/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import common from './common.json'
|
||||
import nav from './nav.json'
|
||||
import auth from './auth.json'
|
||||
import tts from './tts.json'
|
||||
import voice from './voice.json'
|
||||
import job from './job.json'
|
||||
import settings from './settings.json'
|
||||
import user from './user.json'
|
||||
import errors from './errors.json'
|
||||
import constants from './constants.json'
|
||||
import onboarding from './onboarding.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
nav,
|
||||
auth,
|
||||
tts,
|
||||
voice,
|
||||
job,
|
||||
settings,
|
||||
user,
|
||||
errors,
|
||||
constants,
|
||||
onboarding,
|
||||
}
|
||||
55
qwen3-tts-frontend/src/locales/ja-JP/job.json
Normal file
55
qwen3-tts-frontend/src/locales/ja-JP/job.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"jobHistory": "ジョブ履歴",
|
||||
"jobList": "ジョブリスト",
|
||||
"jobDetails": "ジョブ詳細",
|
||||
"jobId": "ジョブID",
|
||||
"status": "ステータス",
|
||||
"createdAt": "作成日時",
|
||||
"completedAt": "完了日時",
|
||||
"duration": "処理時間",
|
||||
"statusPending": "待機中",
|
||||
"statusProcessing": "処理中",
|
||||
"statusCompleted": "完了",
|
||||
"statusFailed": "失敗",
|
||||
"noJobs": "ジョブがありません",
|
||||
"viewJob": "ジョブを表示",
|
||||
"deleteJob": "ジョブを削除",
|
||||
"deleteJobConfirm": "このジョブを削除してもよろしいですか?",
|
||||
"jobDeleted": "ジョブを削除しました",
|
||||
"refreshJobs": "ジョブを更新",
|
||||
"inputText": "入力テキスト",
|
||||
"parameters": "パラメータ",
|
||||
"result": "結果",
|
||||
"errorMessage": "エラーメッセージ",
|
||||
"downloadResult": "結果をダウンロード",
|
||||
"retryJob": "再試行",
|
||||
"cancelJob": "ジョブをキャンセル",
|
||||
"historyTitle": "履歴",
|
||||
"historyCount": "{{count}}件の記録",
|
||||
"retry": "再試行",
|
||||
"noHistory": "履歴がありません",
|
||||
"historyDescription": "音声生成後、記録がここに表示されます",
|
||||
"detailsDescription": "ジョブの詳細パラメータと生成結果を表示",
|
||||
"basicInfo": "基本情報",
|
||||
"speaker": "話者: ",
|
||||
"language": "言語: ",
|
||||
"autoDetect": "自動検出",
|
||||
"fastMode": "高速モード: ",
|
||||
"useCache": "キャッシュ使用: ",
|
||||
"synthesisText": "合成テキスト",
|
||||
"notSet": "未設定",
|
||||
"voiceDescription": "音声説明",
|
||||
"emotionGuidance": "感情ガイダンス",
|
||||
"referenceText": "参照テキスト",
|
||||
"notProvided": "提供なし",
|
||||
"advancedParameters": "詳細パラメータ",
|
||||
"maxNewTokens": "最大生成長: ",
|
||||
"temperature": "Temperature: ",
|
||||
"topK": "Top K: ",
|
||||
"topP": "Top P: ",
|
||||
"repetitionPenalty": "繰り返しペナルティ: ",
|
||||
"audioPlayback": "音声再生",
|
||||
"typeCustomVoice": "カスタム音声",
|
||||
"typeVoiceDesign": "音声デザイン",
|
||||
"typeVoiceClone": "音声クローン"
|
||||
}
|
||||
12
qwen3-tts-frontend/src/locales/ja-JP/nav.json
Normal file
12
qwen3-tts-frontend/src/locales/ja-JP/nav.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "ホーム",
|
||||
"settings": "設定",
|
||||
"userManagement": "ユーザー管理",
|
||||
"logout": "ログアウト",
|
||||
"login": "ログイン",
|
||||
"toggleTheme": "テーマ切替",
|
||||
"changeLanguage": "言語変更",
|
||||
"customVoiceTab": "カスタム",
|
||||
"voiceDesignTab": "デザイン",
|
||||
"voiceCloneTab": "クローン"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/ja-JP/onboarding.json
Normal file
25
qwen3-tts-frontend/src/locales/ja-JP/onboarding.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"welcome": "Qwen3 TTSへようこそ",
|
||||
"configureApiKey": "Aliyun APIキーを設定",
|
||||
"selectBackendDescription": "TTSバックエンドモードを選択してください。後で設定で変更できます",
|
||||
"enterApiKeyDescription": "Aliyun APIキーを入力してください。システムがその有効性を検証します",
|
||||
"localModel": "ローカルモデル",
|
||||
"localModelDescription": "無料でローカルQwen3-TTSモデルを使用",
|
||||
"localModelNoPermission": "ローカルモデルの権限がありません。管理者にお問い合わせください",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiRecommended": "(推奨)",
|
||||
"aliyunApiDescription": "APIキーの設定が必要。従量課金制",
|
||||
"skipConfig": "設定をスキップ",
|
||||
"nextStep": "次へ",
|
||||
"back": "戻る",
|
||||
"verifying": "検証中...",
|
||||
"verifyAndComplete": "検証して完了",
|
||||
"apiKey": "APIキー",
|
||||
"howToGetApiKey": "APIキーの取得方法",
|
||||
"skipSuccess": "設定をスキップしました。デフォルトでローカルモードを使用します",
|
||||
"operationFailed": "操作に失敗しました。再試行してください",
|
||||
"configComplete": "設定が完了しました。デフォルトでローカルモードを使用します",
|
||||
"configCompleteAliyun": "設定が完了しました。デフォルトでAliyunモードを使用します",
|
||||
"saveFailed": "設定の保存に失敗しました。再試行してください",
|
||||
"verifyFailed": "APIキーの検証に失敗しました。確認して再試行してください"
|
||||
}
|
||||
61
qwen3-tts-frontend/src/locales/ja-JP/settings.json
Normal file
61
qwen3-tts-frontend/src/locales/ja-JP/settings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"settings": "設定",
|
||||
"generalSettings": "一般設定",
|
||||
"backendSettings": "バックエンド設定",
|
||||
"apiSettings": "API設定",
|
||||
"preferencesSaved": "設定を保存しました",
|
||||
"preferencesSaveFailed": "設定の保存に失敗しました",
|
||||
"defaultBackend": "デフォルトバックエンド",
|
||||
"local": "ローカルモデル",
|
||||
"aliyun": "Aliyun API",
|
||||
"aliyunApiKey": "Aliyun APIキー",
|
||||
"apiKeyPlaceholder": "APIキーを入力してください",
|
||||
"apiKeyDescription": "Aliyun TTSサービスの認証に使用",
|
||||
"saveApiKey": "キーを保存",
|
||||
"apiKeySaved": "APIキーを保存しました",
|
||||
"apiKeySaveFailed": "APIキーの保存に失敗しました",
|
||||
"showApiKey": "キーを表示",
|
||||
"hideApiKey": "キーを非表示",
|
||||
"testConnection": "接続テスト",
|
||||
"connectionSuccess": "接続成功",
|
||||
"connectionFailed": "接続失敗",
|
||||
"language": "表示言語",
|
||||
"languageDescription": "インターフェース言語を選択",
|
||||
"theme": "テーマ",
|
||||
"themeLight": "ライト",
|
||||
"themeDark": "ダーク",
|
||||
"themeSystem": "システム設定",
|
||||
"title": "設定",
|
||||
"description": "アカウント設定と環境設定を管理",
|
||||
"backendPreference": "バックエンド設定",
|
||||
"backendPreferenceDescription": "デフォルトのTTSバックエンドモードを選択",
|
||||
"localModel": "ローカルモデル",
|
||||
"localModelDescription": "無料でローカルQwen3-TTSモデルを使用",
|
||||
"localModelNoPermission": "管理者にローカルモデルの使用権限をお問い合わせください",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiDescription": "Aliyun TTSサービスを使用",
|
||||
"switchedToLocal": "ローカルモードに切り替えました",
|
||||
"switchedToAliyun": "Aliyunモードに切り替えました",
|
||||
"saveFailed": "保存に失敗しました。再試行してください",
|
||||
"apiKeyUpdated": "APIキーを更新し、検証しました",
|
||||
"apiKeyVerifyFailed": "APIキーの検証に失敗しました",
|
||||
"verifyFailed": "検証に失敗しました",
|
||||
"currentStatus": "現在のステータス:",
|
||||
"configured": "設定済みで有効",
|
||||
"notConfigured": "未設定",
|
||||
"apiKey": "APIキー",
|
||||
"updating": "更新中...",
|
||||
"addKey": "キーを追加",
|
||||
"updateKey": "キーを更新",
|
||||
"verifyKey": "キーを検証",
|
||||
"deleteKey": "キーを削除",
|
||||
"deleteKeyConfirm": "Aliyun APIキーを削除してもよろしいですか?削除後は自動的にローカルモードに切り替わります。",
|
||||
"keyDeleted": "APIキーを削除しました。ローカルモードに切り替えました",
|
||||
"deleteFailed": "削除に失敗しました",
|
||||
"accountInfo": "アカウント情報",
|
||||
"accountInfoDescription": "アカウントの基本情報",
|
||||
"email": "メールアドレス",
|
||||
"changePassword": "パスワード変更",
|
||||
"passwordChangeSuccess": "パスワードを変更しました",
|
||||
"passwordChangeFailed": "パスワードの変更に失敗しました"
|
||||
}
|
||||
80
qwen3-tts-frontend/src/locales/ja-JP/tts.json
Normal file
80
qwen3-tts-frontend/src/locales/ja-JP/tts.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"customVoice": "カスタム音声",
|
||||
"voiceDesign": "音声デザイン",
|
||||
"voiceClone": "音声クローン",
|
||||
"text": "テキスト",
|
||||
"textPlaceholder": "合成するテキストを入力してください...",
|
||||
"language": "言語",
|
||||
"speaker": "話者",
|
||||
"instruct": "感情指示",
|
||||
"instructPlaceholder": "例:とても嬉しい、少し悲しい...",
|
||||
"customSpeaker": "カスタム話者",
|
||||
"customSpeakerPlaceholder": "例:Vivian、Alice...",
|
||||
"presetInstructs": "プリセット感情",
|
||||
"generate": "音声生成",
|
||||
"generating": "生成中...",
|
||||
"generationSuccess": "音声の生成に成功しました",
|
||||
"generationFailed": "音声の生成に失敗しました",
|
||||
"audioPlayer": "オーディオプレーヤー",
|
||||
"noAudio": "オーディオがありません",
|
||||
"downloadAudio": "オーディオをダウンロード",
|
||||
"playAudio": "再生",
|
||||
"pauseAudio": "一時停止",
|
||||
"backend": "バックエンドサービス",
|
||||
"localBackend": "ローカルモデル",
|
||||
"aliyunBackend": "Alibaba Cloud API",
|
||||
"backendSwitched": "バックエンドを切り替えました",
|
||||
"backendError": "バックエンドサービスエラー",
|
||||
"languageLabel": "言語",
|
||||
"speakerLabel": "話者",
|
||||
"speakerPlaceholder": "話者を選択",
|
||||
"textLabel": "合成テキスト",
|
||||
"instructLabel": "感情ガイダンス(オプション)",
|
||||
"instructPlaceholderDesign": "音声デザインのプリセットガイダンスを使用",
|
||||
"instructPlaceholderDefault": "例:優しく思いやりのある、ゆっくりとしたペース、温かみのある",
|
||||
"advancedOptions": "詳細オプション",
|
||||
"advancedOptionsTitle": "詳細パラメータ設定",
|
||||
"advancedOptionsDescription": "生成パラメータを調整して、オーディオ品質と生成長を制御します",
|
||||
"creating": "作成中...",
|
||||
"taskCreated": "タスクを作成しました",
|
||||
"taskCreateFailed": "タスクの作成に失敗しました",
|
||||
"loadDataFailed": "データの読み込みに失敗しました",
|
||||
"myVoiceDesigns": "マイ音声デザイン",
|
||||
"builtinSpeakers": "組み込み話者",
|
||||
"designDescriptionLabel": "音声説明",
|
||||
"designDescriptionPlaceholder": "例:成熟した男性、低くて磁性的、権威的",
|
||||
"saveDesignButton": "音声デザインを保存",
|
||||
"saveDesignTitle": "音声デザインを保存",
|
||||
"saveDesignDescription": "現在の音声デザインに名前を付けて保存し、今後すばやく使用できるようにします",
|
||||
"designNameLabel": "デザイン名",
|
||||
"designNamePlaceholder": "例:磁性的な男性の声",
|
||||
"preparing": "準備中...",
|
||||
"designSaved": "音声デザインを保存しました",
|
||||
"clonePrepared": "音声クローンの準備が完了しました",
|
||||
"clonePrepareFailed": "クローンの準備に失敗しましたが、デザインは保存されました",
|
||||
"saveFailed": "保存に失敗しました",
|
||||
"fillDesignDescription": "音声説明を入力してください",
|
||||
"fillDesignName": "デザイン名を入力してください",
|
||||
"advancedParams": {
|
||||
"maxNewTokens": {
|
||||
"label": "最大生成長",
|
||||
"description": "生成されるオーディオの最大長を制御します。値が大きいほど長いオーディオを生成できますが、処理時間も増加します"
|
||||
},
|
||||
"temperature": {
|
||||
"label": "温度",
|
||||
"description": "生成のランダム性を制御します。値が高いほど多様な出力、低いほど安定した一貫性のある出力になります"
|
||||
},
|
||||
"topK": {
|
||||
"label": "Top K",
|
||||
"description": "サンプリング時に確率が最も高いK個の候補のみを考慮します。値が小さいほど確定的、大きいほど多様になります"
|
||||
},
|
||||
"topP": {
|
||||
"label": "Top P (ニュークリアスサンプリング)",
|
||||
"description": "累積確率の閾値。累積確率がPに達する候補からサンプリングします。出力の多様性を制御します"
|
||||
},
|
||||
"repetitionPenalty": {
|
||||
"label": "繰り返しペナルティ",
|
||||
"description": "繰り返しコンテンツの生成を抑制します。値が大きいほど繰り返しを避けますが、過度に大きいと自然さに影響する可能性があります"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/ja-JP/user.json
Normal file
59
qwen3-tts-frontend/src/locales/ja-JP/user.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"userManagement": "ユーザー管理",
|
||||
"userList": "ユーザーリスト",
|
||||
"userId": "ユーザーID",
|
||||
"username": "ユーザー名",
|
||||
"role": "ロール",
|
||||
"createdAt": "作成日時",
|
||||
"lastLogin": "最終ログイン",
|
||||
"actions": "操作",
|
||||
"addUser": "ユーザーを追加",
|
||||
"editUser": "ユーザーを編集",
|
||||
"deleteUser": "ユーザーを削除",
|
||||
"deleteUserConfirm": "ユーザー {{username}} を削除してもよろしいですか?",
|
||||
"userDeleted": "ユーザーを削除しました",
|
||||
"userAdded": "ユーザーを追加しました",
|
||||
"userUpdated": "ユーザーを更新しました",
|
||||
"userOperationFailed": "ユーザー操作に失敗しました",
|
||||
"roleAdmin": "管理者",
|
||||
"roleUser": "一般ユーザー",
|
||||
"password": "パスワード",
|
||||
"newPassword": "新しいパスワード",
|
||||
"confirmPassword": "パスワード確認",
|
||||
"passwordMismatch": "パスワードが一致しません",
|
||||
"changePassword": "パスワード変更",
|
||||
"passwordChanged": "パスワードを変更しました",
|
||||
"noUsers": "ユーザーがいません",
|
||||
"searchUsers": "ユーザーを検索",
|
||||
"filterByRole": "ロールで絞り込み",
|
||||
"allRoles": "すべてのロール",
|
||||
"createUser": "ユーザーを作成",
|
||||
"loadUsersFailed": "ユーザーリストの読み込みに失敗しました",
|
||||
"userUpdateSuccess": "ユーザーを更新しました",
|
||||
"userCreateSuccess": "ユーザーを作成しました",
|
||||
"operationFailed": "操作に失敗しました",
|
||||
"userDeleteSuccess": "ユーザーを削除しました",
|
||||
"deleteFailed": "削除に失敗しました",
|
||||
"createUserDialog": "ユーザーを作成",
|
||||
"editUserDialog": "ユーザーを編集",
|
||||
"createUserDescription": "新しいユーザーを作成し、基本情報を設定",
|
||||
"editUserDescription": "ユーザー情報と権限設定を変更",
|
||||
"email": "メールアドレス",
|
||||
"passwordOptional": "パスワード (空欄の場合は変更なし)",
|
||||
"isActive": "アクティブ状態",
|
||||
"isSuperuser": "スーパー管理者",
|
||||
"canUseLocalModel": "ローカルモデル権限",
|
||||
"canUseLocalModelDescription": "ユーザーにローカルTTSモデルの使用を許可",
|
||||
"saving": "保存中...",
|
||||
"active": "アクティブ",
|
||||
"inactive": "非アクティブ",
|
||||
"superuser": "スーパー管理者",
|
||||
"normalUser": "一般ユーザー",
|
||||
"localModelPermission": "ローカルモデル",
|
||||
"noPermission": "なし",
|
||||
"validation": {
|
||||
"usernameMinLength": "ユーザー名は3文字以上である必要があります",
|
||||
"usernameMaxLength": "ユーザー名は20文字以下である必要があります",
|
||||
"emailInvalid": "有効なメールアドレスを入力してください"
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/ja-JP/voice.json
Normal file
59
qwen3-tts-frontend/src/locales/ja-JP/voice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"voiceDesign": "音声デザイン",
|
||||
"voiceClone": "音声クローン",
|
||||
"designName": "音声名",
|
||||
"designNamePlaceholder": "音声名を入力してください",
|
||||
"designDescription": "音声説明",
|
||||
"designDescriptionPlaceholder": "音声の特徴を説明してください...",
|
||||
"referenceAudio": "参照オーディオ",
|
||||
"uploadReference": "参照オーディオをアップロード",
|
||||
"referenceText": "参照テキスト",
|
||||
"referenceTextPlaceholder": "参照オーディオのテキスト内容を入力してください...",
|
||||
"cloneName": "クローン名",
|
||||
"cloneNamePlaceholder": "クローン音声名を入力してください",
|
||||
"cloneDescription": "クローン説明",
|
||||
"cloneDescriptionPlaceholder": "クローン音声を説明してください...",
|
||||
"uploadAudio": "オーディオをアップロード",
|
||||
"audioFile": "オーディオファイル",
|
||||
"audioText": "オーディオテキスト",
|
||||
"audioTextPlaceholder": "オーディオに対応するテキストを入力してください...",
|
||||
"saveVoice": "音声を保存",
|
||||
"savingVoice": "保存中...",
|
||||
"voiceSaved": "音声を保存しました",
|
||||
"voiceSaveFailed": "音声の保存に失敗しました",
|
||||
"deleteVoice": "音声を削除",
|
||||
"deleteVoiceConfirm": "この音声を削除してもよろしいですか?",
|
||||
"voiceDeleted": "音声を削除しました",
|
||||
"voiceList": "音声リスト",
|
||||
"noVoices": "音声がありません",
|
||||
"selectVoice": "音声を選択",
|
||||
"voiceDetails": "音声詳細",
|
||||
"createdAt": "作成日時",
|
||||
"updatedAt": "更新日時",
|
||||
"step1Title": "オーディオ素材",
|
||||
"step2Title": "合成設定",
|
||||
"uploadTab": "オーディオをアップロード",
|
||||
"recordTab": "オンライン録音",
|
||||
"refAudioLabel": "参照オーディオファイル",
|
||||
"refTextLabel": "参照トランスクリプト(オプション、精度向上)",
|
||||
"refTextPlaceholder": "参照オーディオに対応するテキスト内容...",
|
||||
"nextStep": "次へ",
|
||||
"prevStep": "前へ",
|
||||
"readPrompt": "次のいずれかの段落を読んでください:",
|
||||
"currentRefText": "現在の参照テキスト",
|
||||
"currentRefTextPlaceholder": "選択したテキストがここに表示されます...",
|
||||
"languageOptional": "言語(オプション)",
|
||||
"fastMode": "高速モード",
|
||||
"useCache": "キャッシュを使用",
|
||||
"uploadAudioFile": "オーディオをアップロード",
|
||||
"recordOnline": "オンライン録音",
|
||||
"validationFailed": "ファイル検証に失敗しました",
|
||||
"validating": "検証中...",
|
||||
"selectAudioFile": "オーディオファイルを選択",
|
||||
"seconds": "秒",
|
||||
"recordingValidationFailed": "録音検証に失敗しました",
|
||||
"browserNotSupported": "お使いのブラウザは録音機能をサポートしていません",
|
||||
"recordingComplete": "録音完了",
|
||||
"releaseToFinish": "離して完了",
|
||||
"holdToRecord": "長押しで録音"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/ko-KR/auth.json
Normal file
25
qwen3-tts-frontend/src/locales/ko-KR/auth.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"login": "로그인",
|
||||
"username": "사용자 이름",
|
||||
"password": "비밀번호",
|
||||
"loginButton": "로그인",
|
||||
"loggingIn": "로그인 중...",
|
||||
"welcome": "Qwen TTS에 오신 것을 환영합니다",
|
||||
"loginPrompt": "계속하려면 로그인하세요",
|
||||
"loginSuccess": "로그인했습니다",
|
||||
"loginFailed": "로그인에 실패했습니다",
|
||||
"loginFailedCheckCredentials": "로그인에 실패했습니다. 사용자 이름과 비밀번호를 확인하세요",
|
||||
"logoutSuccess": "로그아웃했습니다",
|
||||
"unauthorized": "인증되지 않았습니다. 로그인하세요",
|
||||
"sessionExpired": "세션이 만료되었습니다. 다시 로그인하세요",
|
||||
"noPermission": "이 작업을 수행할 권한이 없습니다",
|
||||
"adminOnly": "이 기능은 관리자만 사용할 수 있습니다",
|
||||
"usernamePlaceholder": "사용자 이름 입력",
|
||||
"passwordPlaceholder": "비밀번호 입력",
|
||||
"validation": {
|
||||
"usernameMinLength": "사용자 이름은 {{min}}자 이상이어야 합니다",
|
||||
"usernameMaxLength": "사용자 이름은 {{max}}자 이하여야 합니다",
|
||||
"passwordMinLength": "비밀번호는 {{min}}자 이상이어야 합니다",
|
||||
"apiKeyRequired": "API 키를 입력하세요"
|
||||
}
|
||||
}
|
||||
40
qwen3-tts-frontend/src/locales/ko-KR/common.json
Normal file
40
qwen3-tts-frontend/src/locales/ko-KR/common.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"save": "저장",
|
||||
"cancel": "취소",
|
||||
"confirm": "확인",
|
||||
"delete": "삭제",
|
||||
"edit": "편집",
|
||||
"add": "추가",
|
||||
"create": "생성",
|
||||
"update": "업데이트",
|
||||
"submit": "제출",
|
||||
"close": "닫기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전",
|
||||
"search": "검색",
|
||||
"filter": "필터",
|
||||
"clear": "지우기",
|
||||
"reset": "재설정",
|
||||
"loading": "로딩 중...",
|
||||
"noData": "데이터 없음",
|
||||
"success": "성공",
|
||||
"error": "오류",
|
||||
"warning": "경고",
|
||||
"info": "정보",
|
||||
"yes": "예",
|
||||
"no": "아니오",
|
||||
"ok": "확인",
|
||||
"download": "다운로드",
|
||||
"upload": "업로드",
|
||||
"copy": "복사",
|
||||
"copied": "복사됨",
|
||||
"view": "보기",
|
||||
"details": "상세정보",
|
||||
"actions": "작업",
|
||||
"generatingAudio": "오디오 생성 중, 잠시 기다려주세요...",
|
||||
"generationTakingLong": "생성에 시간이 오래 걸리고 있습니다, 조금만 기다려주세요...",
|
||||
"waitedSeconds": "{{seconds}}초 대기 중",
|
||||
"loadingAudio": "로딩 중...",
|
||||
"failedToLoadAudio": "오디오 로드에 실패했습니다"
|
||||
}
|
||||
152
qwen3-tts-frontend/src/locales/ko-KR/constants.json
Normal file
152
qwen3-tts-frontend/src/locales/ko-KR/constants.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"languages": {
|
||||
"Auto": "자동 감지",
|
||||
"Chinese": "중국어",
|
||||
"English": "영어",
|
||||
"Japanese": "일본어",
|
||||
"Korean": "한국어",
|
||||
"German": "독일어",
|
||||
"French": "프랑스어",
|
||||
"Russian": "러시아어",
|
||||
"Portuguese": "포르투갈어",
|
||||
"Spanish": "스페인어",
|
||||
"Italian": "이탈리아어",
|
||||
"Cantonese": "광둥어",
|
||||
"zh": "중국어",
|
||||
"en": "영어",
|
||||
"ja": "일본어",
|
||||
"ko": "한국어",
|
||||
"yue": "광둥어"
|
||||
},
|
||||
"speakers": {
|
||||
"Vivian": "여성, 전문적이고 명확함",
|
||||
"Serena": "여성, 부드럽고 따뜻함",
|
||||
"Aria": "여성, 활발하고 명랑함",
|
||||
"Emma": "여성, 성숙하고 안정적임",
|
||||
"Sophie": "여성, 우아하고 지적임",
|
||||
"Isabella": "여성, 온화하고 친근함",
|
||||
"Ava": "여성, 젊고 세련됨",
|
||||
"Oliver": "남성, 자성적이고 침착함",
|
||||
"Lucas": "남성, 밝고 쾌활함",
|
||||
"Ethan": "남성, 전문적이고 당당함",
|
||||
"Noah": "남성, 온화하고 친절함",
|
||||
"Liam": "남성, 젊고 활력적임"
|
||||
},
|
||||
"presetInstructs": [
|
||||
{
|
||||
"label": "Happy",
|
||||
"instruct": "very happy",
|
||||
"text": "The weather is so nice today, let's go to the park together!"
|
||||
},
|
||||
{
|
||||
"label": "Sad",
|
||||
"instruct": "very sad, with a crying tone",
|
||||
"text": "I'm sorry, I really tried my best, but I still let you down."
|
||||
},
|
||||
{
|
||||
"label": "Angry",
|
||||
"instruct": "very angry, with intense tone",
|
||||
"text": "How could you do this! This is absolutely unacceptable!"
|
||||
},
|
||||
{
|
||||
"label": "Gentle Care",
|
||||
"instruct": "gentle and caring, slow pace, soft tone, full of care and comfort",
|
||||
"text": "Don't worry, everything will be fine. I'll always be here with you."
|
||||
},
|
||||
{
|
||||
"label": "Excited",
|
||||
"instruct": "very excited, faster pace, rising tone, full of energy and enthusiasm",
|
||||
"text": "Awesome! We finally made it! This is so exciting!"
|
||||
},
|
||||
{
|
||||
"label": "Anxious",
|
||||
"instruct": "anxious tone, slightly faster pace, unstable tone, with tension and worry",
|
||||
"text": "What should we do? We're running out of time, we won't make it, what can we do?"
|
||||
},
|
||||
{
|
||||
"label": "Professional Broadcaster",
|
||||
"instruct": "Professional news broadcaster. Pace: standard broadcasting speed, clear articulation. Emotion: calm and professional, without personal emotion. Tone: mostly flat with slight variations, emphasis on key words. Character: rigorous, objective, authoritative.",
|
||||
"text": "According to Reuters, our space program has achieved a major breakthrough, with the successful completion of manned space missions."
|
||||
},
|
||||
{
|
||||
"label": "Warm Mentor",
|
||||
"instruct": "Warm mentor. Pace: unhurried, speaking slowly. Tone: stable with encouraging rises. Emotion: caring, patient, encouraging. Character: understanding, guiding, full of positive energy.",
|
||||
"text": "Everyone has their own pace, don't rush. Take your time, you will definitely find your own path."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Full of energy. Pace: fast, crisp articulation. Emotion: cheerful and optimistic, energetic. Tone: strong sense of rhythm, cadence. Character: outgoing, confident, enthusiastic, full of youthful spirit.",
|
||||
"text": "Wow, this game is so cool! Let's team up and play together, I promise I'll carry you!"
|
||||
}
|
||||
],
|
||||
"presetVoiceDesigns": [
|
||||
{
|
||||
"label": "Sweet Girl",
|
||||
"instruct": "Young female, sweet and bright voice, with a touch of girlish charm. High pitch, lively and varied intonation. Moderate pace, clear articulation. Cheerful and relaxed emotion, full of youthful energy. Suitable for: customer service, voice assistant, entertainment content.",
|
||||
"text": "Hello, I'm happy to help you! How may I assist you today?"
|
||||
},
|
||||
{
|
||||
"label": "Mature Woman",
|
||||
"instruct": "Mature and intellectual female voice, warm and full tone, with professional woman's capable temperament. Medium pitch, stable range. Moderate to fast pace, clear and organized. Calm and confident emotion, conveying professionalism and reliability.",
|
||||
"text": "According to the latest market analysis report, this quarter's performance shows steady growth, with all indicators meeting expected targets."
|
||||
},
|
||||
{
|
||||
"label": "Magnetic Male",
|
||||
"instruct": "Mid-low male voice, deep and magnetic tone, very appealing. Slow pace, steady rhythm. Moderate volume, thick and powerful voice. Suitable for emotional content, storytelling, brand promotion.",
|
||||
"text": "Night falls, yet the city lights remain brilliant. Under each light, there's a story about dreams."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Energetic young male, bright and clear tone, with youthful vigor. Fast pace, strong sense of rhythm. Enthusiastic and positive emotion, very appealing. Suitable for sports, gaming, entertainment.",
|
||||
"text": "Brothers, are you ready? Today we're going to challenge the new dungeon, let's go!"
|
||||
},
|
||||
{
|
||||
"label": "Authority Expert",
|
||||
"instruct": "Middle-aged male expert image, calm and authoritative tone, thick and powerful voice. Moderate pace, clear and standard articulation. Serious and professional emotion, conveying trust and expertise. Suitable for academic lectures, knowledge popularization, formal occasions.",
|
||||
"text": "From a historical development perspective, technological innovation has always been the core driving force for social progress."
|
||||
},
|
||||
{
|
||||
"label": "Gentle Mother",
|
||||
"instruct": "Gentle and loving middle-aged female, soft and warm tone, full of maternal care. Slow pace, calm and soothing tone. Warm and caring emotion, giving a sense of security. Suitable for children's content, emotional companionship, bedtime stories.",
|
||||
"text": "Sweetie, it's time to sleep. Mom will tell you a story. Once upon a time, there was a little rabbit who lived in the forest..."
|
||||
},
|
||||
{
|
||||
"label": "Broadcasting Host",
|
||||
"instruct": "Professional broadcasting host voice, full and round tone, standard pronunciation. Medium pitch, wide range. Standard pace, precise rhythm control. Professional and calm emotion, clear articulation. Suitable for news broadcasting, program hosting, formal reading.",
|
||||
"text": "Hello dear listeners, welcome to today's program. Next, we bring you today's news."
|
||||
},
|
||||
{
|
||||
"label": "Playful Girl",
|
||||
"instruct": "Playful and cute girl voice, light and lively sound, with unique girlish liveliness. High and varied tone, with coquettish and cute elements. Varying pace, clear articulation with cute interjections.",
|
||||
"text": "Oh no, I didn't mean to~ Can you forgive me this time? Please please~"
|
||||
}
|
||||
],
|
||||
"presetRefTexts": [
|
||||
{
|
||||
"label": "Natural Life",
|
||||
"text": "In this fast-paced world, we're always rushing forward, forgetting to pause and listen to our inner voice. Life is not just about the busyness before us, but also the poetry in the distance and the little moments of happiness we discover. May this recording bring you a touch of gentleness and strength, like an afternoon breeze. No matter how the future changes, remember to keep your love for life and embrace every bright tomorrow."
|
||||
},
|
||||
{
|
||||
"label": "Professional Formal",
|
||||
"text": "Technological progress allows us to transcend the boundaries of time and space, continuing emotions and memories through digitalization. Voice cloning is not only precise code logic, but also a bridge connecting humanity with future intelligence. Through the continuous evolution of deep learning and neural networks, every subtle intonation can be accurately captured. Let us witness together how technology gives voice a more vital expression."
|
||||
},
|
||||
{
|
||||
"label": "Literary Narrative",
|
||||
"text": "The spring breeze brushes the willow tips, carrying the fragrance of earth and news of blooming flowers. Have you ever anticipated meeting your long-lost self at some street corner? Whether it's hearty laughter or low whispers, each voice is a unique mark of life. Let us record this moment, let memories flow in the sound, becoming an eternal melody."
|
||||
}
|
||||
],
|
||||
"uiLanguages": {
|
||||
"zh-CN": "Simplified Chinese",
|
||||
"zh-TW": "Traditional Chinese",
|
||||
"en-US": "English",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean"
|
||||
},
|
||||
"uiLanguagesShort": {
|
||||
"zh-CN": "ZH-CN",
|
||||
"zh-TW": "ZH-TW",
|
||||
"en-US": "EN",
|
||||
"ja-JP": "JA",
|
||||
"ko-KR": "KO"
|
||||
}
|
||||
}
|
||||
42
qwen3-tts-frontend/src/locales/ko-KR/errors.json
Normal file
42
qwen3-tts-frontend/src/locales/ko-KR/errors.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"http": {
|
||||
"400": "요청 매개변수 오류",
|
||||
"401": "인증되지 않았습니다. 로그인하세요",
|
||||
"403": "접근이 금지되었습니다",
|
||||
"404": "요청한 리소스가 존재하지 않습니다",
|
||||
"500": "서버 내부 오류",
|
||||
"502": "게이트웨이 오류",
|
||||
"503": "서비스를 일시적으로 사용할 수 없습니다",
|
||||
"default": "요청에 실패했습니다. 나중에 다시 시도하세요"
|
||||
},
|
||||
"fieldNames": {
|
||||
"username": "사용자명",
|
||||
"password": "비밀번호",
|
||||
"text": "텍스트",
|
||||
"language": "언어",
|
||||
"speaker": "화자",
|
||||
"instruct": "감정 지시",
|
||||
"name": "이름",
|
||||
"description": "설명",
|
||||
"audio_file": "오디오 파일",
|
||||
"reference_audio": "참조 오디오",
|
||||
"reference_text": "참조 텍스트",
|
||||
"api_key": "API 키",
|
||||
"backend": "백엔드 서비스"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{{field}}을(를) 입력해주세요",
|
||||
"minLength": "{{field}}은(는) 최소 {{min}}자 이상이어야 합니다",
|
||||
"maxLength": "{{field}}은(는) {{max}}자를 초과할 수 없습니다",
|
||||
"invalid": "{{field}} 형식이 올바르지 않습니다",
|
||||
"notFound": "{{field}}을(를) 찾을 수 없습니다",
|
||||
"alreadyExists": "{{field}}이(가) 이미 존재합니다",
|
||||
"uploadFailed": "{{field}} 업로드에 실패했습니다",
|
||||
"fileTooLarge": "파일 크기는 {{size}}MB를 초과할 수 없습니다",
|
||||
"invalidFileType": "지원되지 않는 파일 형식입니다"
|
||||
},
|
||||
"networkError": "네트워크 연결에 실패했습니다. 네트워크를 확인하세요",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"operationFailed": "작업에 실패했습니다",
|
||||
"tryAgain": "다시 시도하세요"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/ko-KR/index.ts
Normal file
25
qwen3-tts-frontend/src/locales/ko-KR/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import common from './common.json'
|
||||
import nav from './nav.json'
|
||||
import auth from './auth.json'
|
||||
import tts from './tts.json'
|
||||
import voice from './voice.json'
|
||||
import job from './job.json'
|
||||
import settings from './settings.json'
|
||||
import user from './user.json'
|
||||
import errors from './errors.json'
|
||||
import constants from './constants.json'
|
||||
import onboarding from './onboarding.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
nav,
|
||||
auth,
|
||||
tts,
|
||||
voice,
|
||||
job,
|
||||
settings,
|
||||
user,
|
||||
errors,
|
||||
constants,
|
||||
onboarding,
|
||||
}
|
||||
55
qwen3-tts-frontend/src/locales/ko-KR/job.json
Normal file
55
qwen3-tts-frontend/src/locales/ko-KR/job.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"jobHistory": "작업 기록",
|
||||
"jobList": "작업 목록",
|
||||
"jobDetails": "작업 세부정보",
|
||||
"jobId": "작업 ID",
|
||||
"status": "상태",
|
||||
"createdAt": "생성 일시",
|
||||
"completedAt": "완료 일시",
|
||||
"duration": "처리 시간",
|
||||
"statusPending": "대기 중",
|
||||
"statusProcessing": "처리 중",
|
||||
"statusCompleted": "완료",
|
||||
"statusFailed": "실패",
|
||||
"noJobs": "작업이 없습니다",
|
||||
"viewJob": "작업 보기",
|
||||
"deleteJob": "작업 삭제",
|
||||
"deleteJobConfirm": "이 작업을 삭제하시겠습니까?",
|
||||
"jobDeleted": "작업이 삭제되었습니다",
|
||||
"refreshJobs": "작업 새로고침",
|
||||
"inputText": "입력 텍스트",
|
||||
"parameters": "매개변수",
|
||||
"result": "결과",
|
||||
"errorMessage": "오류 메시지",
|
||||
"downloadResult": "결과 다운로드",
|
||||
"retryJob": "재시도",
|
||||
"cancelJob": "작업 취소",
|
||||
"historyTitle": "기록",
|
||||
"historyCount": "{{count}}개 기록",
|
||||
"retry": "재시도",
|
||||
"noHistory": "기록이 없습니다",
|
||||
"historyDescription": "음성 생성 후 기록이 여기에 표시됩니다",
|
||||
"detailsDescription": "작업의 세부 매개변수 및 생성 결과 보기",
|
||||
"basicInfo": "기본 정보",
|
||||
"speaker": "화자: ",
|
||||
"language": "언어: ",
|
||||
"autoDetect": "자동 감지",
|
||||
"fastMode": "빠른 모드: ",
|
||||
"useCache": "캐시 사용: ",
|
||||
"synthesisText": "합성 텍스트",
|
||||
"notSet": "설정되지 않음",
|
||||
"voiceDescription": "음성 설명",
|
||||
"emotionGuidance": "감정 가이드",
|
||||
"referenceText": "참조 텍스트",
|
||||
"notProvided": "제공되지 않음",
|
||||
"advancedParameters": "고급 매개변수",
|
||||
"maxNewTokens": "최대 생성 길이: ",
|
||||
"temperature": "Temperature: ",
|
||||
"topK": "Top K: ",
|
||||
"topP": "Top P: ",
|
||||
"repetitionPenalty": "반복 페널티: ",
|
||||
"audioPlayback": "오디오 재생",
|
||||
"typeCustomVoice": "사용자 정의 음성",
|
||||
"typeVoiceDesign": "음성 디자인",
|
||||
"typeVoiceClone": "음성 복제"
|
||||
}
|
||||
12
qwen3-tts-frontend/src/locales/ko-KR/nav.json
Normal file
12
qwen3-tts-frontend/src/locales/ko-KR/nav.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "홈",
|
||||
"settings": "설정",
|
||||
"userManagement": "사용자 관리",
|
||||
"logout": "로그아웃",
|
||||
"login": "로그인",
|
||||
"toggleTheme": "테마 전환",
|
||||
"changeLanguage": "언어 변경",
|
||||
"customVoiceTab": "커스텀",
|
||||
"voiceDesignTab": "디자인",
|
||||
"voiceCloneTab": "복제"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/ko-KR/onboarding.json
Normal file
25
qwen3-tts-frontend/src/locales/ko-KR/onboarding.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"welcome": "Qwen3 TTS에 오신 것을 환영합니다",
|
||||
"configureApiKey": "Aliyun API 키 설정",
|
||||
"selectBackendDescription": "TTS 백엔드 모드를 선택하세요. 나중에 설정에서 변경할 수 있습니다",
|
||||
"enterApiKeyDescription": "Aliyun API 키를 입력하세요. 시스템이 유효성을 검증합니다",
|
||||
"localModel": "로컬 모델",
|
||||
"localModelDescription": "무료로 로컬 Qwen3-TTS 모델 사용",
|
||||
"localModelNoPermission": "로컬 모델 권한이 없습니다. 관리자에게 문의하세요",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiRecommended": "(권장)",
|
||||
"aliyunApiDescription": "API 키 설정 필요. 사용량에 따라 과금",
|
||||
"skipConfig": "설정 건너뛰기",
|
||||
"nextStep": "다음",
|
||||
"back": "뒤로",
|
||||
"verifying": "검증 중...",
|
||||
"verifyAndComplete": "검증 및 완료",
|
||||
"apiKey": "API 키",
|
||||
"howToGetApiKey": "API 키를 얻는 방법",
|
||||
"skipSuccess": "설정을 건너뛰었습니다. 기본적으로 로컬 모드를 사용합니다",
|
||||
"operationFailed": "작업에 실패했습니다. 다시 시도하세요",
|
||||
"configComplete": "설정이 완료되었습니다. 기본적으로 로컬 모드를 사용합니다",
|
||||
"configCompleteAliyun": "설정이 완료되었습니다. 기본적으로 Aliyun 모드를 사용합니다",
|
||||
"saveFailed": "설정 저장에 실패했습니다. 다시 시도하세요",
|
||||
"verifyFailed": "API 키 검증에 실패했습니다. 확인 후 다시 시도하세요"
|
||||
}
|
||||
61
qwen3-tts-frontend/src/locales/ko-KR/settings.json
Normal file
61
qwen3-tts-frontend/src/locales/ko-KR/settings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"settings": "설정",
|
||||
"generalSettings": "일반 설정",
|
||||
"backendSettings": "백엔드 설정",
|
||||
"apiSettings": "API 설정",
|
||||
"preferencesSaved": "환경 설정이 저장되었습니다",
|
||||
"preferencesSaveFailed": "환경 설정 저장에 실패했습니다",
|
||||
"defaultBackend": "기본 백엔드",
|
||||
"local": "로컬 모델",
|
||||
"aliyun": "Aliyun API",
|
||||
"aliyunApiKey": "Aliyun API 키",
|
||||
"apiKeyPlaceholder": "API 키를 입력하세요",
|
||||
"apiKeyDescription": "Aliyun TTS 서비스 인증에 사용",
|
||||
"saveApiKey": "키 저장",
|
||||
"apiKeySaved": "API 키가 저장되었습니다",
|
||||
"apiKeySaveFailed": "API 키 저장에 실패했습니다",
|
||||
"showApiKey": "키 표시",
|
||||
"hideApiKey": "키 숨기기",
|
||||
"testConnection": "연결 테스트",
|
||||
"connectionSuccess": "연결 성공",
|
||||
"connectionFailed": "연결 실패",
|
||||
"language": "인터페이스 언어",
|
||||
"languageDescription": "인터페이스 표시 언어 선택",
|
||||
"theme": "테마",
|
||||
"themeLight": "라이트",
|
||||
"themeDark": "다크",
|
||||
"themeSystem": "시스템 설정",
|
||||
"title": "설정",
|
||||
"description": "계정 설정 및 환경 설정 관리",
|
||||
"backendPreference": "백엔드 설정",
|
||||
"backendPreferenceDescription": "기본 TTS 백엔드 모드 선택",
|
||||
"localModel": "로컬 모델",
|
||||
"localModelDescription": "무료로 로컬 Qwen3-TTS 모델 사용",
|
||||
"localModelNoPermission": "관리자에게 로컬 모델 사용 권한을 문의하세요",
|
||||
"aliyunApi": "Aliyun API",
|
||||
"aliyunApiDescription": "Aliyun TTS 서비스 사용",
|
||||
"switchedToLocal": "로컬 모드로 전환했습니다",
|
||||
"switchedToAliyun": "Aliyun 모드로 전환했습니다",
|
||||
"saveFailed": "저장에 실패했습니다. 다시 시도하세요",
|
||||
"apiKeyUpdated": "API 키가 업데이트되고 검증되었습니다",
|
||||
"apiKeyVerifyFailed": "API 키 검증에 실패했습니다",
|
||||
"verifyFailed": "검증에 실패했습니다",
|
||||
"currentStatus": "현재 상태:",
|
||||
"configured": "설정됨 및 유효함",
|
||||
"notConfigured": "설정되지 않음",
|
||||
"apiKey": "API 키",
|
||||
"updating": "업데이트 중...",
|
||||
"addKey": "키 추가",
|
||||
"updateKey": "키 업데이트",
|
||||
"verifyKey": "키 검증",
|
||||
"deleteKey": "키 삭제",
|
||||
"deleteKeyConfirm": "Aliyun API 키를 삭제하시겠습니까? 삭제 후 자동으로 로컬 모드로 전환됩니다.",
|
||||
"keyDeleted": "API 키가 삭제되었습니다. 로컬 모드로 전환했습니다",
|
||||
"deleteFailed": "삭제에 실패했습니다",
|
||||
"accountInfo": "계정 정보",
|
||||
"accountInfoDescription": "계정 기본 정보",
|
||||
"email": "이메일",
|
||||
"changePassword": "비밀번호 변경",
|
||||
"passwordChangeSuccess": "비밀번호가 변경되었습니다",
|
||||
"passwordChangeFailed": "비밀번호 변경에 실패했습니다"
|
||||
}
|
||||
80
qwen3-tts-frontend/src/locales/ko-KR/tts.json
Normal file
80
qwen3-tts-frontend/src/locales/ko-KR/tts.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"customVoice": "맞춤 음성",
|
||||
"voiceDesign": "음성 디자인",
|
||||
"voiceClone": "음성 복제",
|
||||
"text": "텍스트",
|
||||
"textPlaceholder": "합성할 텍스트를 입력하세요...",
|
||||
"language": "언어",
|
||||
"speaker": "화자",
|
||||
"instruct": "감정 지시",
|
||||
"instructPlaceholder": "예: 매우 기쁨, 조금 슬픔...",
|
||||
"customSpeaker": "맞춤 화자",
|
||||
"customSpeakerPlaceholder": "예: Vivian, Alice...",
|
||||
"presetInstructs": "프리셋 감정",
|
||||
"generate": "음성 생성",
|
||||
"generating": "생성 중...",
|
||||
"generationSuccess": "음성이 성공적으로 생성되었습니다",
|
||||
"generationFailed": "음성 생성에 실패했습니다",
|
||||
"audioPlayer": "오디오 플레이어",
|
||||
"noAudio": "오디오 없음",
|
||||
"downloadAudio": "오디오 다운로드",
|
||||
"playAudio": "재생",
|
||||
"pauseAudio": "일시정지",
|
||||
"backend": "백엔드 서비스",
|
||||
"localBackend": "로컬 모델",
|
||||
"aliyunBackend": "Alibaba Cloud API",
|
||||
"backendSwitched": "백엔드가 전환되었습니다",
|
||||
"backendError": "백엔드 서비스 오류",
|
||||
"languageLabel": "언어",
|
||||
"speakerLabel": "화자",
|
||||
"speakerPlaceholder": "화자 선택",
|
||||
"textLabel": "합성 텍스트",
|
||||
"instructLabel": "감정 가이던스 (선택사항)",
|
||||
"instructPlaceholderDesign": "음성 디자인의 프리셋 가이던스 사용",
|
||||
"instructPlaceholderDefault": "예: 부드럽고 배려심 있는, 느린 속도, 따뜻함이 가득한",
|
||||
"advancedOptions": "고급 옵션",
|
||||
"advancedOptionsTitle": "고급 매개변수 설정",
|
||||
"advancedOptionsDescription": "생성 매개변수를 조정하여 오디오 품질 및 생성 길이를 제어합니다",
|
||||
"creating": "생성 중...",
|
||||
"taskCreated": "작업이 생성되었습니다",
|
||||
"taskCreateFailed": "작업 생성에 실패했습니다",
|
||||
"loadDataFailed": "데이터 로드에 실패했습니다",
|
||||
"myVoiceDesigns": "내 음성 디자인",
|
||||
"builtinSpeakers": "내장 화자",
|
||||
"designDescriptionLabel": "음성 설명",
|
||||
"designDescriptionPlaceholder": "예: 성숙한 남성, 낮고 자성적인, 권위적인",
|
||||
"saveDesignButton": "음성 디자인 저장",
|
||||
"saveDesignTitle": "음성 디자인 저장",
|
||||
"saveDesignDescription": "현재 음성 디자인에 이름을 지정하고 저장하여 빠르게 사용할 수 있습니다",
|
||||
"designNameLabel": "디자인 이름",
|
||||
"designNamePlaceholder": "예: 자성적인 남성 목소리",
|
||||
"preparing": "준비 중...",
|
||||
"designSaved": "음성 디자인이 저장되었습니다",
|
||||
"clonePrepared": "음성 복제 준비가 완료되었습니다",
|
||||
"clonePrepareFailed": "복제 준비에 실패했지만 디자인은 저장되었습니다",
|
||||
"saveFailed": "저장에 실패했습니다",
|
||||
"fillDesignDescription": "음성 설명을 먼저 입력하세요",
|
||||
"fillDesignName": "디자인 이름을 입력하세요",
|
||||
"advancedParams": {
|
||||
"maxNewTokens": {
|
||||
"label": "최대 생성 길이",
|
||||
"description": "생성되는 오디오의 최대 길이를 제어합니다. 값이 클수록 더 긴 오디오를 생성할 수 있지만 처리 시간도 증가합니다"
|
||||
},
|
||||
"temperature": {
|
||||
"label": "온도",
|
||||
"description": "생성의 무작위성을 제어합니다. 값이 높을수록 더 다양한 출력, 낮을수록 더 안정적이고 일관된 출력이 됩니다"
|
||||
},
|
||||
"topK": {
|
||||
"label": "Top K",
|
||||
"description": "샘플링 시 확률이 가장 높은 K개의 후보만 고려합니다. 값이 작을수록 확정적이고, 클수록 다양해집니다"
|
||||
},
|
||||
"topP": {
|
||||
"label": "Top P (핵 샘플링)",
|
||||
"description": "누적 확률 임계값. 누적 확률이 P에 도달하는 후보에서 샘플링합니다. 출력 다양성을 제어합니다"
|
||||
},
|
||||
"repetitionPenalty": {
|
||||
"label": "반복 페널티",
|
||||
"description": "반복적인 콘텐츠 생성을 억제합니다. 값이 클수록 반복을 더 피하지만, 과도하게 크면 자연스러움에 영향을 줄 수 있습니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/ko-KR/user.json
Normal file
59
qwen3-tts-frontend/src/locales/ko-KR/user.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"userManagement": "사용자 관리",
|
||||
"userList": "사용자 목록",
|
||||
"userId": "사용자 ID",
|
||||
"username": "사용자 이름",
|
||||
"role": "역할",
|
||||
"createdAt": "생성 일시",
|
||||
"lastLogin": "마지막 로그인",
|
||||
"actions": "작업",
|
||||
"addUser": "사용자 추가",
|
||||
"editUser": "사용자 편집",
|
||||
"deleteUser": "사용자 삭제",
|
||||
"deleteUserConfirm": "사용자 {{username}}을(를) 삭제하시겠습니까?",
|
||||
"userDeleted": "사용자가 삭제되었습니다",
|
||||
"userAdded": "사용자가 추가되었습니다",
|
||||
"userUpdated": "사용자가 업데이트되었습니다",
|
||||
"userOperationFailed": "사용자 작업에 실패했습니다",
|
||||
"roleAdmin": "관리자",
|
||||
"roleUser": "일반 사용자",
|
||||
"password": "비밀번호",
|
||||
"newPassword": "새 비밀번호",
|
||||
"confirmPassword": "비밀번호 확인",
|
||||
"passwordMismatch": "비밀번호가 일치하지 않습니다",
|
||||
"changePassword": "비밀번호 변경",
|
||||
"passwordChanged": "비밀번호가 변경되었습니다",
|
||||
"noUsers": "사용자가 없습니다",
|
||||
"searchUsers": "사용자 검색",
|
||||
"filterByRole": "역할별 필터링",
|
||||
"allRoles": "모든 역할",
|
||||
"createUser": "사용자 생성",
|
||||
"loadUsersFailed": "사용자 목록 로드에 실패했습니다",
|
||||
"userUpdateSuccess": "사용자가 업데이트되었습니다",
|
||||
"userCreateSuccess": "사용자가 생성되었습니다",
|
||||
"operationFailed": "작업에 실패했습니다",
|
||||
"userDeleteSuccess": "사용자가 삭제되었습니다",
|
||||
"deleteFailed": "삭제에 실패했습니다",
|
||||
"createUserDialog": "사용자 생성",
|
||||
"editUserDialog": "사용자 편집",
|
||||
"createUserDescription": "새 사용자를 생성하고 기본 정보를 설정",
|
||||
"editUserDescription": "사용자 정보 및 권한 설정 수정",
|
||||
"email": "이메일",
|
||||
"passwordOptional": "비밀번호 (비워두면 변경하지 않음)",
|
||||
"isActive": "활성 상태",
|
||||
"isSuperuser": "슈퍼 관리자",
|
||||
"canUseLocalModel": "로컬 모델 권한",
|
||||
"canUseLocalModelDescription": "사용자가 로컬 TTS 모델을 사용할 수 있도록 허용",
|
||||
"saving": "저장 중...",
|
||||
"active": "활성",
|
||||
"inactive": "비활성",
|
||||
"superuser": "슈퍼 관리자",
|
||||
"normalUser": "일반 사용자",
|
||||
"localModelPermission": "로컬 모델",
|
||||
"noPermission": "없음",
|
||||
"validation": {
|
||||
"usernameMinLength": "사용자 이름은 3자 이상이어야 합니다",
|
||||
"usernameMaxLength": "사용자 이름은 20자 이하여야 합니다",
|
||||
"emailInvalid": "유효한 이메일 주소를 입력하세요"
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/ko-KR/voice.json
Normal file
59
qwen3-tts-frontend/src/locales/ko-KR/voice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"voiceDesign": "음성 디자인",
|
||||
"voiceClone": "음성 복제",
|
||||
"designName": "음성 이름",
|
||||
"designNamePlaceholder": "음성 이름을 입력하세요",
|
||||
"designDescription": "음성 설명",
|
||||
"designDescriptionPlaceholder": "음성 특징을 설명하세요...",
|
||||
"referenceAudio": "참조 오디오",
|
||||
"uploadReference": "참조 오디오 업로드",
|
||||
"referenceText": "참조 텍스트",
|
||||
"referenceTextPlaceholder": "참조 오디오의 텍스트 내용을 입력하세요...",
|
||||
"cloneName": "복제 이름",
|
||||
"cloneNamePlaceholder": "복제 음성 이름을 입력하세요",
|
||||
"cloneDescription": "복제 설명",
|
||||
"cloneDescriptionPlaceholder": "복제 음성을 설명하세요...",
|
||||
"uploadAudio": "오디오 업로드",
|
||||
"audioFile": "오디오 파일",
|
||||
"audioText": "오디오 텍스트",
|
||||
"audioTextPlaceholder": "오디오에 해당하는 텍스트를 입력하세요...",
|
||||
"saveVoice": "음성 저장",
|
||||
"savingVoice": "저장 중...",
|
||||
"voiceSaved": "음성이 저장되었습니다",
|
||||
"voiceSaveFailed": "음성 저장에 실패했습니다",
|
||||
"deleteVoice": "음성 삭제",
|
||||
"deleteVoiceConfirm": "이 음성을 삭제하시겠습니까?",
|
||||
"voiceDeleted": "음성이 삭제되었습니다",
|
||||
"voiceList": "음성 목록",
|
||||
"noVoices": "음성 없음",
|
||||
"selectVoice": "음성 선택",
|
||||
"voiceDetails": "음성 상세정보",
|
||||
"createdAt": "생성 시간",
|
||||
"updatedAt": "업데이트 시간",
|
||||
"step1Title": "오디오 소재",
|
||||
"step2Title": "합성 설정",
|
||||
"uploadTab": "오디오 업로드",
|
||||
"recordTab": "온라인 녹음",
|
||||
"refAudioLabel": "참조 오디오 파일",
|
||||
"refTextLabel": "참조 스크립트 (선택사항, 정확도 향상)",
|
||||
"refTextPlaceholder": "참조 오디오에 해당하는 텍스트 내용...",
|
||||
"nextStep": "다음",
|
||||
"prevStep": "이전",
|
||||
"readPrompt": "다음 단락 중 하나를 읽어주세요:",
|
||||
"currentRefText": "현재 참조 텍스트",
|
||||
"currentRefTextPlaceholder": "선택한 텍스트가 여기에 표시됩니다...",
|
||||
"languageOptional": "언어 (선택사항)",
|
||||
"fastMode": "고속 모드",
|
||||
"useCache": "캐시 사용",
|
||||
"uploadAudioFile": "오디오 업로드",
|
||||
"recordOnline": "온라인 녹음",
|
||||
"validationFailed": "파일 검증 실패",
|
||||
"validating": "검증 중...",
|
||||
"selectAudioFile": "오디오 파일 선택",
|
||||
"seconds": "초",
|
||||
"recordingValidationFailed": "녹음 검증 실패",
|
||||
"browserNotSupported": "브라우저가 녹음 기능을 지원하지 않습니다",
|
||||
"recordingComplete": "녹음 완료",
|
||||
"releaseToFinish": "놓아서 완료",
|
||||
"holdToRecord": "길게 눌러서 녹음"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-CN/auth.json
Normal file
25
qwen3-tts-frontend/src/locales/zh-CN/auth.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"login": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"loginButton": "登录",
|
||||
"loggingIn": "登录中...",
|
||||
"welcome": "欢迎使用 Qwen TTS",
|
||||
"loginPrompt": "请登录以继续使用",
|
||||
"loginSuccess": "登录成功",
|
||||
"loginFailed": "登录失败",
|
||||
"loginFailedCheckCredentials": "登录失败,请检查用户名和密码",
|
||||
"logoutSuccess": "已退出登录",
|
||||
"unauthorized": "未授权,请登录",
|
||||
"sessionExpired": "会话已过期,请重新登录",
|
||||
"noPermission": "您没有权限执行此操作",
|
||||
"adminOnly": "此功能仅管理员可用",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"validation": {
|
||||
"usernameMinLength": "用户名至少需要 {{min}} 个字符",
|
||||
"usernameMaxLength": "用户名不能超过 {{max}} 个字符",
|
||||
"passwordMinLength": "密码至少需要 {{min}} 个字符",
|
||||
"apiKeyRequired": "请输入 API 密钥"
|
||||
}
|
||||
}
|
||||
40
qwen3-tts-frontend/src/locales/zh-CN/common.json
Normal file
40
qwen3-tts-frontend/src/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"submit": "提交",
|
||||
"close": "关闭",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"clear": "清空",
|
||||
"reset": "重置",
|
||||
"loading": "加载中...",
|
||||
"noData": "暂无数据",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"ok": "确定",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"view": "查看",
|
||||
"details": "详情",
|
||||
"actions": "操作",
|
||||
"generatingAudio": "正在生成音频,请稍候...",
|
||||
"generationTakingLong": "生成用时较长,请耐心等待...",
|
||||
"waitedSeconds": "已等待 {{seconds}} 秒",
|
||||
"loadingAudio": "加载中...",
|
||||
"failedToLoadAudio": "加载音频失败"
|
||||
}
|
||||
152
qwen3-tts-frontend/src/locales/zh-CN/constants.json
Normal file
152
qwen3-tts-frontend/src/locales/zh-CN/constants.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"languages": {
|
||||
"Auto": "自动检测",
|
||||
"Chinese": "中文",
|
||||
"English": "英语",
|
||||
"Japanese": "日语",
|
||||
"Korean": "韩语",
|
||||
"German": "德语",
|
||||
"French": "法语",
|
||||
"Russian": "俄语",
|
||||
"Portuguese": "葡萄牙语",
|
||||
"Spanish": "西班牙语",
|
||||
"Italian": "意大利语",
|
||||
"Cantonese": "粤语",
|
||||
"zh": "中文",
|
||||
"en": "英语",
|
||||
"ja": "日语",
|
||||
"ko": "韩语",
|
||||
"yue": "粤语"
|
||||
},
|
||||
"speakers": {
|
||||
"Vivian": "女性,专业清晰",
|
||||
"Serena": "女性,温柔温暖",
|
||||
"Aria": "女性,活泼开朗",
|
||||
"Emma": "女性,成熟稳重",
|
||||
"Sophie": "女性,优雅知性",
|
||||
"Isabella": "女性,温婉亲和",
|
||||
"Ava": "女性,年轻时尚",
|
||||
"Oliver": "男性,磁性沉稳",
|
||||
"Lucas": "男性,阳光开朗",
|
||||
"Ethan": "男性,专业大气",
|
||||
"Noah": "男性,温和亲切",
|
||||
"Liam": "男性,年轻活力"
|
||||
},
|
||||
"uiLanguages": {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"en-US": "English",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어"
|
||||
},
|
||||
"uiLanguagesShort": {
|
||||
"zh-CN": "简中",
|
||||
"zh-TW": "繁中",
|
||||
"en-US": "EN",
|
||||
"ja-JP": "日",
|
||||
"ko-KR": "한"
|
||||
},
|
||||
"presetInstructs": [
|
||||
{
|
||||
"label": "开心",
|
||||
"instruct": "非常开心",
|
||||
"text": "今天天气真好,我们一起去公园玩吧!"
|
||||
},
|
||||
{
|
||||
"label": "悲伤",
|
||||
"instruct": "很悲伤,带着哭腔",
|
||||
"text": "对不起,我真的尽力了,但还是让你失望了。"
|
||||
},
|
||||
{
|
||||
"label": "愤怒",
|
||||
"instruct": "非常愤怒,语气激烈",
|
||||
"text": "你怎么能这样做!这简直太过分了!"
|
||||
},
|
||||
{
|
||||
"label": "温柔关怀",
|
||||
"instruct": "温柔体贴,语速平缓,音调柔和,充满关怀和安慰",
|
||||
"text": "别担心,一切都会好起来的。我会一直陪在你身边。"
|
||||
},
|
||||
{
|
||||
"label": "兴奋激动",
|
||||
"instruct": "非常兴奋激动,语速加快,音调上扬,充满活力和热情",
|
||||
"text": "太棒了!我们终于成功了!这真是太令人激动了!"
|
||||
},
|
||||
{
|
||||
"label": "焦虑紧张",
|
||||
"instruct": "焦虑不安的语气,语速略快,音调不稳定,带有紧张和担忧",
|
||||
"text": "怎么办?时间不够了,我们来不及了,这可怎么办才好?"
|
||||
},
|
||||
{
|
||||
"label": "专业播音员",
|
||||
"instruct": "专业新闻播音员。语速:标准播音语速,吐字清晰。情绪:沉稳专业,不带个人感情色彩。语调:平直中略有起伏,重点词汇加重。性格特征:严谨、客观、权威。",
|
||||
"text": "据新华社报道,我国航天事业取得重大突破,神舟系列飞船成功完成载人飞行任务。"
|
||||
},
|
||||
{
|
||||
"label": "温暖导师",
|
||||
"instruct": "温暖导师。语速:不急不缓,娓娓道来。音调:平稳中带有鼓励性上扬。情绪:关怀、耐心、鼓励。性格:善解人意,循循善诱,充满正能量。",
|
||||
"text": "每个人都有自己的节奏,不要着急。慢慢来,你一定能找到属于自己的那条路。"
|
||||
},
|
||||
{
|
||||
"label": "活力少年",
|
||||
"instruct": "充满活力。语速:偏快,吐字利落。情绪:开朗乐观,精力充沛。语调:跳跃感强,抑扬顿挫。性格:外向、自信、热情,充满青春气息。",
|
||||
"text": "哇,这个游戏太酷了!咱们组队一起玩吧,我保证带你们飞!"
|
||||
}
|
||||
],
|
||||
"presetVoiceDesigns": [
|
||||
{
|
||||
"label": "甜美少女",
|
||||
"instruct": "年轻女性,音色清甜明亮,略带少女的娇俏感。音高偏高,语调活泼富于变化。语速适中,吐字清晰。情绪愉悦轻松,充满青春活力。适合场景:客服语音、语音助手、娱乐内容。",
|
||||
"text": "您好,很高兴为您服务!请问有什么可以帮助您的吗?"
|
||||
},
|
||||
{
|
||||
"label": "成熟女性",
|
||||
"instruct": "成熟知性的女性声音,音色温润饱满,带有职业女性的干练气质。音高中等,音域稳定。语速适中偏快,条理清晰。情绪从容自信,传递专业可靠的感觉。",
|
||||
"text": "根据最新的市场分析报告,本季度业绩呈现稳步增长态势,各项指标均达到预期目标。"
|
||||
},
|
||||
{
|
||||
"label": "磁性男声",
|
||||
"instruct": "中低音男性声音,音色深沉磁性,富有感染力。语速偏慢,节奏沉稳。音量适中,声音浑厚有力。适合情感类、故事讲述、品牌宣传等场景。",
|
||||
"text": "夜深了,城市的灯火依然璀璨。每一盏灯下,都有一个关于梦想的故事。"
|
||||
},
|
||||
{
|
||||
"label": "活力青年",
|
||||
"instruct": "充满活力的年轻男性,音色明亮清晰,带有青春朝气。语速较快,节奏感强。情绪热情积极,富有感染力。适合运动、游戏、娱乐等场景。",
|
||||
"text": "兄弟们,准备好了吗?今天我们要挑战全新的副本,冲冲冲!"
|
||||
},
|
||||
{
|
||||
"label": "权威专家",
|
||||
"instruct": "中年男性专家形象,音色沉稳权威,声音浑厚有力。语速适中,吐字清晰标准。情绪严肃专业,传递信任感和专业度。适合学术讲座、知识科普、正式场合。",
|
||||
"text": "从历史发展的角度来看,科技创新始终是推动社会进步的核心动力。"
|
||||
},
|
||||
{
|
||||
"label": "温柔妈妈",
|
||||
"instruct": "温柔慈爱的中年女性,音色柔和温暖,充满母性关怀。语速舒缓,音调平和安抚。情绪温暖体贴,给人安全感。适合儿童内容、情感陪伴、睡前故事。",
|
||||
"text": "宝贝,该睡觉了。妈妈给你讲个故事,从前有一只小兔子,它住在森林里..."
|
||||
},
|
||||
{
|
||||
"label": "播音主持",
|
||||
"instruct": "专业播音主持人声音,音色饱满圆润,标准普通话发音。音高适中,音域宽广。语速标准,节奏把控精准。情绪专业沉稳,字正腔圆。适合新闻播报、节目主持、正式朗读。",
|
||||
"text": "各位听众朋友大家好,欢迎收听今天的节目。接下来为您带来今日要闻。"
|
||||
},
|
||||
{
|
||||
"label": "俏皮少女",
|
||||
"instruct": "俏皮可爱的少女音色,声音轻快灵动,带有少女特有的活泼感。音调偏高且富于变化,语气中带有撒娇和卖萌的元素。语速时快时慢,吐字清晰但带有可爱的语气词。",
|
||||
"text": "哎呀,人家不是故意的嘛~你就原谅我一次好不好?拜托拜托啦~"
|
||||
}
|
||||
],
|
||||
"presetRefTexts": [
|
||||
{
|
||||
"label": "自然生活",
|
||||
"text": "在这个快节奏的世界里,我们总是在赶路,却忘了停下来听听内心的声音。其实,生活不仅仅是眼前的忙碌,还有远方的诗意和偶然发现的小确幸。希望这段录音,能像午后的微风一样,带给你一点点温柔和力量。无论未来如何变化,请记得保持对生活的热爱,去拥抱每一个灿烂的明天。"
|
||||
},
|
||||
{
|
||||
"label": "专业正式",
|
||||
"text": "科技的进步让我们能够跨越时空的界限,用数字化的方式延续情感与记忆。语音克隆不仅是精密的代码逻辑,更是连接人类与未来智能的纽带。通过深度学习与神经网络的不断演进,每一个细微的语调起伏,都能被精准地捕捉。让我们共同见证,技术如何赋予声音更具生命力的表达。"
|
||||
},
|
||||
{
|
||||
"label": "文学叙事",
|
||||
"text": "春天的风拂过柳梢,带着泥土的芬芳和花开的消息。你是否也曾期待过,在某个街角的转弯处,遇见那个久违的自己?无论是高亢的欢笑,还是低沉的呢喃,每一种声音都是独一无二的生命印记。让我们在此刻记录当下,让回忆在流淌的声音里,化作永恒的旋律。"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
qwen3-tts-frontend/src/locales/zh-CN/errors.json
Normal file
42
qwen3-tts-frontend/src/locales/zh-CN/errors.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"http": {
|
||||
"400": "请求参数错误",
|
||||
"401": "未授权,请登录",
|
||||
"403": "禁止访问",
|
||||
"404": "请求的资源不存在",
|
||||
"500": "服务器内部错误",
|
||||
"502": "网关错误",
|
||||
"503": "服务暂时不可用",
|
||||
"default": "请求失败,请稍后重试"
|
||||
},
|
||||
"fieldNames": {
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"text": "文本",
|
||||
"language": "语言",
|
||||
"speaker": "发言人",
|
||||
"instruct": "情感指令",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"audio_file": "音频文件",
|
||||
"reference_audio": "参考音频",
|
||||
"reference_text": "参考文本",
|
||||
"api_key": "API密钥",
|
||||
"backend": "后端服务"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{{field}}不能为空",
|
||||
"minLength": "{{field}}至少需要 {{min}} 个字符",
|
||||
"maxLength": "{{field}}不能超过 {{max}} 个字符",
|
||||
"invalid": "{{field}}格式不正确",
|
||||
"notFound": "未找到{{field}}",
|
||||
"alreadyExists": "{{field}}已存在",
|
||||
"uploadFailed": "{{field}}上传失败",
|
||||
"fileTooLarge": "文件大小不能超过 {{size}}MB",
|
||||
"invalidFileType": "不支持的文件类型"
|
||||
},
|
||||
"networkError": "网络连接失败,请检查网络",
|
||||
"unknownError": "未知错误",
|
||||
"operationFailed": "操作失败",
|
||||
"tryAgain": "请重试"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-CN/index.ts
Normal file
25
qwen3-tts-frontend/src/locales/zh-CN/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import common from './common.json'
|
||||
import nav from './nav.json'
|
||||
import auth from './auth.json'
|
||||
import tts from './tts.json'
|
||||
import voice from './voice.json'
|
||||
import job from './job.json'
|
||||
import settings from './settings.json'
|
||||
import user from './user.json'
|
||||
import errors from './errors.json'
|
||||
import constants from './constants.json'
|
||||
import onboarding from './onboarding.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
nav,
|
||||
auth,
|
||||
tts,
|
||||
voice,
|
||||
job,
|
||||
settings,
|
||||
user,
|
||||
errors,
|
||||
constants,
|
||||
onboarding,
|
||||
}
|
||||
55
qwen3-tts-frontend/src/locales/zh-CN/job.json
Normal file
55
qwen3-tts-frontend/src/locales/zh-CN/job.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"jobHistory": "任务历史",
|
||||
"jobList": "任务列表",
|
||||
"jobDetails": "任务详情",
|
||||
"jobId": "任务ID",
|
||||
"status": "状态",
|
||||
"createdAt": "创建时间",
|
||||
"completedAt": "完成时间",
|
||||
"duration": "耗时",
|
||||
"statusPending": "待处理",
|
||||
"statusProcessing": "进行中",
|
||||
"statusCompleted": "已完成",
|
||||
"statusFailed": "失败",
|
||||
"noJobs": "暂无任务",
|
||||
"viewJob": "查看任务",
|
||||
"deleteJob": "删除任务",
|
||||
"deleteJobConfirm": "确定要删除此任务吗?",
|
||||
"jobDeleted": "任务已删除",
|
||||
"refreshJobs": "刷新任务",
|
||||
"inputText": "输入文本",
|
||||
"parameters": "参数",
|
||||
"result": "结果",
|
||||
"errorMessage": "错误信息",
|
||||
"downloadResult": "下载结果",
|
||||
"retryJob": "重试",
|
||||
"cancelJob": "取消任务",
|
||||
"historyTitle": "历史记录",
|
||||
"historyCount": "共 {{count}} 条记录",
|
||||
"retry": "重试",
|
||||
"noHistory": "暂无历史记录",
|
||||
"historyDescription": "生成语音后,记录将会显示在这里",
|
||||
"detailsDescription": "查看任务的详细参数和生成结果",
|
||||
"basicInfo": "基本信息",
|
||||
"speaker": "发音人: ",
|
||||
"language": "语言: ",
|
||||
"autoDetect": "自动检测",
|
||||
"fastMode": "快速模式: ",
|
||||
"useCache": "使用缓存: ",
|
||||
"synthesisText": "合成文本",
|
||||
"notSet": "未设置",
|
||||
"voiceDescription": "音色描述",
|
||||
"emotionGuidance": "情绪指导",
|
||||
"referenceText": "参考文本",
|
||||
"notProvided": "未提供",
|
||||
"advancedParameters": "高级参数",
|
||||
"maxNewTokens": "最大生成长度: ",
|
||||
"temperature": "温度: ",
|
||||
"topK": "Top K: ",
|
||||
"topP": "Top P: ",
|
||||
"repetitionPenalty": "重复惩罚: ",
|
||||
"audioPlayback": "音频播放",
|
||||
"typeCustomVoice": "自定义音色",
|
||||
"typeVoiceDesign": "音色设计",
|
||||
"typeVoiceClone": "声音克隆"
|
||||
}
|
||||
12
qwen3-tts-frontend/src/locales/zh-CN/nav.json
Normal file
12
qwen3-tts-frontend/src/locales/zh-CN/nav.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "首页",
|
||||
"settings": "设置",
|
||||
"userManagement": "用户管理",
|
||||
"logout": "退出登录",
|
||||
"login": "登录",
|
||||
"toggleTheme": "切换主题",
|
||||
"changeLanguage": "切换语言",
|
||||
"customVoiceTab": "自定义",
|
||||
"voiceDesignTab": "设计",
|
||||
"voiceCloneTab": "克隆"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-CN/onboarding.json
Normal file
25
qwen3-tts-frontend/src/locales/zh-CN/onboarding.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"welcome": "欢迎使用 Qwen3 TTS",
|
||||
"configureApiKey": "配置阿里云 API 密钥",
|
||||
"selectBackendDescription": "请选择您的 TTS 后端模式,后续可在设置中修改",
|
||||
"enterApiKeyDescription": "请输入您的阿里云 API 密钥,系统将验证其有效性",
|
||||
"localModel": "本地模型",
|
||||
"localModelDescription": "免费使用本地 Qwen3-TTS 模型",
|
||||
"localModelNoPermission": "无本地模型权限,请联系管理员",
|
||||
"aliyunApi": "阿里云 API",
|
||||
"aliyunApiRecommended": "(推荐)",
|
||||
"aliyunApiDescription": "需要配置 API 密钥,按量计费",
|
||||
"skipConfig": "跳过配置",
|
||||
"nextStep": "下一步",
|
||||
"back": "返回",
|
||||
"verifying": "验证中...",
|
||||
"verifyAndComplete": "验证并完成",
|
||||
"apiKey": "API 密钥",
|
||||
"howToGetApiKey": "如何获取 API 密钥?",
|
||||
"skipSuccess": "已跳过配置,默认使用本地模式",
|
||||
"operationFailed": "操作失败,请重试",
|
||||
"configComplete": "配置完成,默认使用本地模式",
|
||||
"configCompleteAliyun": "配置完成,默认使用阿里云模式",
|
||||
"saveFailed": "保存配置失败,请重试",
|
||||
"verifyFailed": "API 密钥验证失败,请检查后重试"
|
||||
}
|
||||
61
qwen3-tts-frontend/src/locales/zh-CN/settings.json
Normal file
61
qwen3-tts-frontend/src/locales/zh-CN/settings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"settings": "设置",
|
||||
"generalSettings": "通用设置",
|
||||
"backendSettings": "后端设置",
|
||||
"apiSettings": "API设置",
|
||||
"preferencesSaved": "偏好设置已保存",
|
||||
"preferencesSaveFailed": "偏好设置保存失败",
|
||||
"defaultBackend": "默认后端",
|
||||
"local": "本地模型",
|
||||
"aliyun": "阿里云API",
|
||||
"aliyunApiKey": "阿里云 API 密钥",
|
||||
"apiKeyPlaceholder": "请输入 API 密钥",
|
||||
"apiKeyDescription": "用于阿里云 TTS 服务认证",
|
||||
"saveApiKey": "保存密钥",
|
||||
"apiKeySaved": "API 密钥已保存",
|
||||
"apiKeySaveFailed": "API 密钥保存失败",
|
||||
"showApiKey": "显示密钥",
|
||||
"hideApiKey": "隐藏密钥",
|
||||
"testConnection": "测试连接",
|
||||
"connectionSuccess": "连接成功",
|
||||
"connectionFailed": "连接失败",
|
||||
"language": "界面语言",
|
||||
"languageDescription": "选择界面显示语言",
|
||||
"theme": "主题",
|
||||
"themeLight": "浅色",
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "跟随系统",
|
||||
"title": "设置",
|
||||
"description": "管理您的账户设置和偏好",
|
||||
"backendPreference": "后端偏好",
|
||||
"backendPreferenceDescription": "选择默认的 TTS 后端模式",
|
||||
"localModel": "本地模型",
|
||||
"localModelDescription": "免费使用本地 Qwen3-TTS 模型",
|
||||
"localModelNoPermission": "请联系管理员开启使用本地模型权限",
|
||||
"aliyunApi": "阿里云 API",
|
||||
"aliyunApiDescription": "使用阿里云 TTS 服务",
|
||||
"switchedToLocal": "已切换到本地模式",
|
||||
"switchedToAliyun": "已切换到阿里云模式",
|
||||
"saveFailed": "保存失败,请重试",
|
||||
"apiKeyUpdated": "API 密钥已更新并验证成功",
|
||||
"apiKeyVerifyFailed": "API 密钥验证失败",
|
||||
"verifyFailed": "验证失败",
|
||||
"currentStatus": "当前状态:",
|
||||
"configured": "已配置并有效",
|
||||
"notConfigured": "未配置",
|
||||
"apiKey": "API 密钥",
|
||||
"updating": "更新中...",
|
||||
"addKey": "添加密钥",
|
||||
"updateKey": "更新密钥",
|
||||
"verifyKey": "验证密钥",
|
||||
"deleteKey": "删除密钥",
|
||||
"deleteKeyConfirm": "确定要删除阿里云 API 密钥吗?删除后将自动切换到本地模式。",
|
||||
"keyDeleted": "API 密钥已删除,已切换到本地模式",
|
||||
"deleteFailed": "删除失败",
|
||||
"accountInfo": "账户信息",
|
||||
"accountInfoDescription": "您的账户基本信息",
|
||||
"email": "邮箱",
|
||||
"changePassword": "修改密码",
|
||||
"passwordChangeSuccess": "密码修改成功",
|
||||
"passwordChangeFailed": "密码修改失败"
|
||||
}
|
||||
80
qwen3-tts-frontend/src/locales/zh-CN/tts.json
Normal file
80
qwen3-tts-frontend/src/locales/zh-CN/tts.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"customVoice": "自定义音色",
|
||||
"voiceDesign": "音色设计",
|
||||
"voiceClone": "音色克隆",
|
||||
"text": "文本",
|
||||
"textPlaceholder": "请输入要合成的文本...",
|
||||
"language": "语言",
|
||||
"speaker": "发言人",
|
||||
"instruct": "情感指令",
|
||||
"instructPlaceholder": "如:非常开心、有点难过...",
|
||||
"customSpeaker": "自定义发言人",
|
||||
"customSpeakerPlaceholder": "如:Vivian、Alice...",
|
||||
"presetInstructs": "预设情感",
|
||||
"generate": "生成语音",
|
||||
"generating": "生成中...",
|
||||
"generationSuccess": "语音生成成功",
|
||||
"generationFailed": "语音生成失败",
|
||||
"audioPlayer": "音频播放器",
|
||||
"noAudio": "暂无音频",
|
||||
"downloadAudio": "下载音频",
|
||||
"playAudio": "播放",
|
||||
"pauseAudio": "暂停",
|
||||
"backend": "后端服务",
|
||||
"localBackend": "本地模型",
|
||||
"aliyunBackend": "阿里云API",
|
||||
"backendSwitched": "后端已切换",
|
||||
"backendError": "后端服务错误",
|
||||
"languageLabel": "语言",
|
||||
"speakerLabel": "发音人",
|
||||
"speakerPlaceholder": "选择发音人",
|
||||
"textLabel": "合成文本",
|
||||
"instructLabel": "情绪指导(可选)",
|
||||
"instructPlaceholderDesign": "已使用音色设计的预设指导",
|
||||
"instructPlaceholderDefault": "例如:温柔体贴,语速平缓,充满关怀",
|
||||
"advancedOptions": "高级选项",
|
||||
"advancedOptionsTitle": "高级参数设置",
|
||||
"advancedOptionsDescription": "调整生成参数以控制音频质量和生成长度",
|
||||
"creating": "创建中...",
|
||||
"taskCreated": "任务已创建",
|
||||
"taskCreateFailed": "创建任务失败",
|
||||
"loadDataFailed": "加载数据失败",
|
||||
"myVoiceDesigns": "我的音色设计",
|
||||
"builtinSpeakers": "内置发音人",
|
||||
"designDescriptionLabel": "音色描述",
|
||||
"designDescriptionPlaceholder": "例如:成熟男性,低沉磁性,充满权威感",
|
||||
"saveDesignButton": "保存音色设计",
|
||||
"saveDesignTitle": "保存音色设计",
|
||||
"saveDesignDescription": "为当前音色设计命名并保存,以便后续快速使用",
|
||||
"designNameLabel": "设计名称",
|
||||
"designNamePlaceholder": "例如:磁性男声",
|
||||
"preparing": "准备中...",
|
||||
"designSaved": "音色设计已保存",
|
||||
"clonePrepared": "音色克隆准备完成",
|
||||
"clonePrepareFailed": "准备克隆失败,但设计已保存",
|
||||
"saveFailed": "保存失败",
|
||||
"fillDesignDescription": "请先填写音色描述",
|
||||
"fillDesignName": "请输入设计名称",
|
||||
"advancedParams": {
|
||||
"maxNewTokens": {
|
||||
"label": "最大生成长度",
|
||||
"description": "控制生成音频的最大长度。值越大,可生成的音频越长,但处理时间也会增加"
|
||||
},
|
||||
"temperature": {
|
||||
"label": "温度",
|
||||
"description": "控制生成的随机性。值越高生成越随机多样,值越低越稳定一致"
|
||||
},
|
||||
"topK": {
|
||||
"label": "Top K",
|
||||
"description": "采样时只考虑概率最高的 K 个候选。值越小生成越确定,越大越多样"
|
||||
},
|
||||
"topP": {
|
||||
"label": "Top P (核采样)",
|
||||
"description": "累积概率阈值,只从累积概率达到 P 的候选中采样。控制输出多样性"
|
||||
},
|
||||
"repetitionPenalty": {
|
||||
"label": "重复惩罚",
|
||||
"description": "惩罚重复内容的生成。值越大越避免重复,但过大可能影响自然度"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/zh-CN/user.json
Normal file
59
qwen3-tts-frontend/src/locales/zh-CN/user.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"userManagement": "用户管理",
|
||||
"userList": "用户列表",
|
||||
"userId": "用户ID",
|
||||
"username": "用户名",
|
||||
"role": "角色",
|
||||
"createdAt": "创建时间",
|
||||
"lastLogin": "最后登录",
|
||||
"actions": "操作",
|
||||
"addUser": "添加用户",
|
||||
"editUser": "编辑用户",
|
||||
"deleteUser": "删除用户",
|
||||
"deleteUserConfirm": "确定要删除用户 {{username}} 吗?",
|
||||
"userDeleted": "用户已删除",
|
||||
"userAdded": "用户已添加",
|
||||
"userUpdated": "用户已更新",
|
||||
"userOperationFailed": "用户操作失败",
|
||||
"roleAdmin": "管理员",
|
||||
"roleUser": "普通用户",
|
||||
"password": "密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"passwordMismatch": "两次输入的密码不一致",
|
||||
"changePassword": "修改密码",
|
||||
"passwordChanged": "密码已修改",
|
||||
"noUsers": "暂无用户",
|
||||
"searchUsers": "搜索用户",
|
||||
"filterByRole": "按角色筛选",
|
||||
"allRoles": "全部角色",
|
||||
"createUser": "创建用户",
|
||||
"loadUsersFailed": "加载用户列表失败",
|
||||
"userUpdateSuccess": "用户更新成功",
|
||||
"userCreateSuccess": "用户创建成功",
|
||||
"operationFailed": "操作失败",
|
||||
"userDeleteSuccess": "用户删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"createUserDialog": "创建用户",
|
||||
"editUserDialog": "编辑用户",
|
||||
"createUserDescription": "创建新用户并配置基本信息",
|
||||
"editUserDescription": "修改用户信息和权限设置",
|
||||
"email": "邮箱",
|
||||
"passwordOptional": "密码 (留空则不修改)",
|
||||
"isActive": "激活状态",
|
||||
"isSuperuser": "超级管理员",
|
||||
"canUseLocalModel": "本地模型权限",
|
||||
"canUseLocalModelDescription": "允许用户使用本地 TTS 模型",
|
||||
"saving": "保存中...",
|
||||
"active": "活跃",
|
||||
"inactive": "停用",
|
||||
"superuser": "超级管理员",
|
||||
"normalUser": "普通用户",
|
||||
"localModelPermission": "本地模型",
|
||||
"noPermission": "无",
|
||||
"validation": {
|
||||
"usernameMinLength": "用户名至少3个字符",
|
||||
"usernameMaxLength": "用户名最多20个字符",
|
||||
"emailInvalid": "请输入有效的邮箱地址"
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/zh-CN/voice.json
Normal file
59
qwen3-tts-frontend/src/locales/zh-CN/voice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"voiceDesign": "音色设计",
|
||||
"voiceClone": "音色克隆",
|
||||
"designName": "音色名称",
|
||||
"designNamePlaceholder": "请输入音色名称",
|
||||
"designDescription": "音色描述",
|
||||
"designDescriptionPlaceholder": "请描述音色特点...",
|
||||
"referenceAudio": "参考音频",
|
||||
"uploadReference": "上传参考音频",
|
||||
"referenceText": "参考文本",
|
||||
"referenceTextPlaceholder": "请输入参考音频的文本内容...",
|
||||
"cloneName": "克隆名称",
|
||||
"cloneNamePlaceholder": "请输入克隆音色名称",
|
||||
"cloneDescription": "克隆描述",
|
||||
"cloneDescriptionPlaceholder": "请描述克隆音色...",
|
||||
"uploadAudio": "上传音频",
|
||||
"audioFile": "音频文件",
|
||||
"audioText": "音频文本",
|
||||
"audioTextPlaceholder": "请输入音频对应的文本...",
|
||||
"saveVoice": "保存音色",
|
||||
"savingVoice": "保存中...",
|
||||
"voiceSaved": "音色已保存",
|
||||
"voiceSaveFailed": "音色保存失败",
|
||||
"deleteVoice": "删除音色",
|
||||
"deleteVoiceConfirm": "确定要删除此音色吗?",
|
||||
"voiceDeleted": "音色已删除",
|
||||
"voiceList": "音色列表",
|
||||
"noVoices": "暂无音色",
|
||||
"selectVoice": "选择音色",
|
||||
"voiceDetails": "音色详情",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"step1Title": "音频素材",
|
||||
"step2Title": "合成设置",
|
||||
"uploadTab": "上传音频",
|
||||
"recordTab": "在线录制",
|
||||
"refAudioLabel": "参考音频文件",
|
||||
"refTextLabel": "参考文稿(可选,提高准确率)",
|
||||
"refTextPlaceholder": "参考音频对应的文本内容...",
|
||||
"nextStep": "下一步",
|
||||
"prevStep": "上一步",
|
||||
"readPrompt": "请朗读以下任一段落:",
|
||||
"currentRefText": "当前参考文本",
|
||||
"currentRefTextPlaceholder": "选中的文本将显示在这里...",
|
||||
"languageOptional": "语言(可选)",
|
||||
"fastMode": "快速模式",
|
||||
"useCache": "使用缓存",
|
||||
"uploadAudioFile": "上传音频",
|
||||
"recordOnline": "在线录制",
|
||||
"validationFailed": "文件验证失败",
|
||||
"validating": "验证中...",
|
||||
"selectAudioFile": "选择音频文件",
|
||||
"seconds": "秒",
|
||||
"recordingValidationFailed": "录音验证失败",
|
||||
"browserNotSupported": "您的浏览器不支持录音功能",
|
||||
"recordingComplete": "录制完成",
|
||||
"releaseToFinish": "松开完成",
|
||||
"holdToRecord": "按住录音"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-TW/auth.json
Normal file
25
qwen3-tts-frontend/src/locales/zh-TW/auth.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"login": "登入",
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"loginButton": "登入",
|
||||
"loggingIn": "登入中...",
|
||||
"welcome": "歡迎使用 Qwen TTS",
|
||||
"loginPrompt": "請登入以繼續使用",
|
||||
"loginSuccess": "登入成功",
|
||||
"loginFailed": "登入失敗",
|
||||
"loginFailedCheckCredentials": "登入失敗,請檢查使用者名稱和密碼",
|
||||
"logoutSuccess": "已登出",
|
||||
"unauthorized": "未授權,請登入",
|
||||
"sessionExpired": "連線階段已過期,請重新登入",
|
||||
"noPermission": "您沒有權限執行此操作",
|
||||
"adminOnly": "此功能僅限管理員使用",
|
||||
"usernamePlaceholder": "請輸入使用者名稱",
|
||||
"passwordPlaceholder": "請輸入密碼",
|
||||
"validation": {
|
||||
"usernameMinLength": "使用者名稱至少需要 {{min}} 個字元",
|
||||
"usernameMaxLength": "使用者名稱不能超過 {{max}} 個字元",
|
||||
"passwordMinLength": "密碼至少需要 {{min}} 個字元",
|
||||
"apiKeyRequired": "請輸入 API 金鑰"
|
||||
}
|
||||
}
|
||||
40
qwen3-tts-frontend/src/locales/zh-TW/common.json
Normal file
40
qwen3-tts-frontend/src/locales/zh-TW/common.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"save": "儲存",
|
||||
"cancel": "取消",
|
||||
"confirm": "確認",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"add": "新增",
|
||||
"create": "建立",
|
||||
"update": "更新",
|
||||
"submit": "提交",
|
||||
"close": "關閉",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"search": "搜尋",
|
||||
"filter": "篩選",
|
||||
"clear": "清空",
|
||||
"reset": "重置",
|
||||
"loading": "載入中...",
|
||||
"noData": "暫無資料",
|
||||
"success": "成功",
|
||||
"error": "錯誤",
|
||||
"warning": "警告",
|
||||
"info": "資訊",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"ok": "確定",
|
||||
"download": "下載",
|
||||
"upload": "上傳",
|
||||
"copy": "複製",
|
||||
"copied": "已複製",
|
||||
"view": "檢視",
|
||||
"details": "詳情",
|
||||
"actions": "操作",
|
||||
"generatingAudio": "正在生成音訊,請稍候...",
|
||||
"generationTakingLong": "生成用時較長,請耐心等待...",
|
||||
"waitedSeconds": "已等待 {{seconds}} 秒",
|
||||
"loadingAudio": "載入中...",
|
||||
"failedToLoadAudio": "載入音訊失敗"
|
||||
}
|
||||
152
qwen3-tts-frontend/src/locales/zh-TW/constants.json
Normal file
152
qwen3-tts-frontend/src/locales/zh-TW/constants.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"languages": {
|
||||
"Auto": "自動偵測",
|
||||
"Chinese": "中文",
|
||||
"English": "英語",
|
||||
"Japanese": "日語",
|
||||
"Korean": "韓語",
|
||||
"German": "德語",
|
||||
"French": "法語",
|
||||
"Russian": "俄語",
|
||||
"Portuguese": "葡萄牙語",
|
||||
"Spanish": "西班牙語",
|
||||
"Italian": "義大利語",
|
||||
"Cantonese": "粵語",
|
||||
"zh": "中文",
|
||||
"en": "英語",
|
||||
"ja": "日語",
|
||||
"ko": "韓語",
|
||||
"yue": "粵語"
|
||||
},
|
||||
"speakers": {
|
||||
"Vivian": "女性,專業清晰",
|
||||
"Serena": "女性,溫柔溫暖",
|
||||
"Aria": "女性,活潑開朗",
|
||||
"Emma": "女性,成熟穩重",
|
||||
"Sophie": "女性,優雅知性",
|
||||
"Isabella": "女性,溫婉親和",
|
||||
"Ava": "女性,年輕時尚",
|
||||
"Oliver": "男性,磁性沉穩",
|
||||
"Lucas": "男性,陽光開朗",
|
||||
"Ethan": "男性,專業大氣",
|
||||
"Noah": "男性,溫和親切",
|
||||
"Liam": "男性,年輕活力"
|
||||
},
|
||||
"presetInstructs": [
|
||||
{
|
||||
"label": "Happy",
|
||||
"instruct": "very happy",
|
||||
"text": "The weather is so nice today, let's go to the park together!"
|
||||
},
|
||||
{
|
||||
"label": "Sad",
|
||||
"instruct": "very sad, with a crying tone",
|
||||
"text": "I'm sorry, I really tried my best, but I still let you down."
|
||||
},
|
||||
{
|
||||
"label": "Angry",
|
||||
"instruct": "very angry, with intense tone",
|
||||
"text": "How could you do this! This is absolutely unacceptable!"
|
||||
},
|
||||
{
|
||||
"label": "Gentle Care",
|
||||
"instruct": "gentle and caring, slow pace, soft tone, full of care and comfort",
|
||||
"text": "Don't worry, everything will be fine. I'll always be here with you."
|
||||
},
|
||||
{
|
||||
"label": "Excited",
|
||||
"instruct": "very excited, faster pace, rising tone, full of energy and enthusiasm",
|
||||
"text": "Awesome! We finally made it! This is so exciting!"
|
||||
},
|
||||
{
|
||||
"label": "Anxious",
|
||||
"instruct": "anxious tone, slightly faster pace, unstable tone, with tension and worry",
|
||||
"text": "What should we do? We're running out of time, we won't make it, what can we do?"
|
||||
},
|
||||
{
|
||||
"label": "Professional Broadcaster",
|
||||
"instruct": "Professional news broadcaster. Pace: standard broadcasting speed, clear articulation. Emotion: calm and professional, without personal emotion. Tone: mostly flat with slight variations, emphasis on key words. Character: rigorous, objective, authoritative.",
|
||||
"text": "According to Reuters, our space program has achieved a major breakthrough, with the successful completion of manned space missions."
|
||||
},
|
||||
{
|
||||
"label": "Warm Mentor",
|
||||
"instruct": "Warm mentor. Pace: unhurried, speaking slowly. Tone: stable with encouraging rises. Emotion: caring, patient, encouraging. Character: understanding, guiding, full of positive energy.",
|
||||
"text": "Everyone has their own pace, don't rush. Take your time, you will definitely find your own path."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Full of energy. Pace: fast, crisp articulation. Emotion: cheerful and optimistic, energetic. Tone: strong sense of rhythm, cadence. Character: outgoing, confident, enthusiastic, full of youthful spirit.",
|
||||
"text": "Wow, this game is so cool! Let's team up and play together, I promise I'll carry you!"
|
||||
}
|
||||
],
|
||||
"presetVoiceDesigns": [
|
||||
{
|
||||
"label": "Sweet Girl",
|
||||
"instruct": "Young female, sweet and bright voice, with a touch of girlish charm. High pitch, lively and varied intonation. Moderate pace, clear articulation. Cheerful and relaxed emotion, full of youthful energy. Suitable for: customer service, voice assistant, entertainment content.",
|
||||
"text": "Hello, I'm happy to help you! How may I assist you today?"
|
||||
},
|
||||
{
|
||||
"label": "Mature Woman",
|
||||
"instruct": "Mature and intellectual female voice, warm and full tone, with professional woman's capable temperament. Medium pitch, stable range. Moderate to fast pace, clear and organized. Calm and confident emotion, conveying professionalism and reliability.",
|
||||
"text": "According to the latest market analysis report, this quarter's performance shows steady growth, with all indicators meeting expected targets."
|
||||
},
|
||||
{
|
||||
"label": "Magnetic Male",
|
||||
"instruct": "Mid-low male voice, deep and magnetic tone, very appealing. Slow pace, steady rhythm. Moderate volume, thick and powerful voice. Suitable for emotional content, storytelling, brand promotion.",
|
||||
"text": "Night falls, yet the city lights remain brilliant. Under each light, there's a story about dreams."
|
||||
},
|
||||
{
|
||||
"label": "Energetic Youth",
|
||||
"instruct": "Energetic young male, bright and clear tone, with youthful vigor. Fast pace, strong sense of rhythm. Enthusiastic and positive emotion, very appealing. Suitable for sports, gaming, entertainment.",
|
||||
"text": "Brothers, are you ready? Today we're going to challenge the new dungeon, let's go!"
|
||||
},
|
||||
{
|
||||
"label": "Authority Expert",
|
||||
"instruct": "Middle-aged male expert image, calm and authoritative tone, thick and powerful voice. Moderate pace, clear and standard articulation. Serious and professional emotion, conveying trust and expertise. Suitable for academic lectures, knowledge popularization, formal occasions.",
|
||||
"text": "From a historical development perspective, technological innovation has always been the core driving force for social progress."
|
||||
},
|
||||
{
|
||||
"label": "Gentle Mother",
|
||||
"instruct": "Gentle and loving middle-aged female, soft and warm tone, full of maternal care. Slow pace, calm and soothing tone. Warm and caring emotion, giving a sense of security. Suitable for children's content, emotional companionship, bedtime stories.",
|
||||
"text": "Sweetie, it's time to sleep. Mom will tell you a story. Once upon a time, there was a little rabbit who lived in the forest..."
|
||||
},
|
||||
{
|
||||
"label": "Broadcasting Host",
|
||||
"instruct": "Professional broadcasting host voice, full and round tone, standard pronunciation. Medium pitch, wide range. Standard pace, precise rhythm control. Professional and calm emotion, clear articulation. Suitable for news broadcasting, program hosting, formal reading.",
|
||||
"text": "Hello dear listeners, welcome to today's program. Next, we bring you today's news."
|
||||
},
|
||||
{
|
||||
"label": "Playful Girl",
|
||||
"instruct": "Playful and cute girl voice, light and lively sound, with unique girlish liveliness. High and varied tone, with coquettish and cute elements. Varying pace, clear articulation with cute interjections.",
|
||||
"text": "Oh no, I didn't mean to~ Can you forgive me this time? Please please~"
|
||||
}
|
||||
],
|
||||
"presetRefTexts": [
|
||||
{
|
||||
"label": "Natural Life",
|
||||
"text": "In this fast-paced world, we're always rushing forward, forgetting to pause and listen to our inner voice. Life is not just about the busyness before us, but also the poetry in the distance and the little moments of happiness we discover. May this recording bring you a touch of gentleness and strength, like an afternoon breeze. No matter how the future changes, remember to keep your love for life and embrace every bright tomorrow."
|
||||
},
|
||||
{
|
||||
"label": "Professional Formal",
|
||||
"text": "Technological progress allows us to transcend the boundaries of time and space, continuing emotions and memories through digitalization. Voice cloning is not only precise code logic, but also a bridge connecting humanity with future intelligence. Through the continuous evolution of deep learning and neural networks, every subtle intonation can be accurately captured. Let us witness together how technology gives voice a more vital expression."
|
||||
},
|
||||
{
|
||||
"label": "Literary Narrative",
|
||||
"text": "The spring breeze brushes the willow tips, carrying the fragrance of earth and news of blooming flowers. Have you ever anticipated meeting your long-lost self at some street corner? Whether it's hearty laughter or low whispers, each voice is a unique mark of life. Let us record this moment, let memories flow in the sound, becoming an eternal melody."
|
||||
}
|
||||
],
|
||||
"uiLanguages": {
|
||||
"zh-CN": "Simplified Chinese",
|
||||
"zh-TW": "Traditional Chinese",
|
||||
"en-US": "English",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean"
|
||||
},
|
||||
"uiLanguagesShort": {
|
||||
"zh-CN": "ZH-CN",
|
||||
"zh-TW": "ZH-TW",
|
||||
"en-US": "EN",
|
||||
"ja-JP": "JA",
|
||||
"ko-KR": "KO"
|
||||
}
|
||||
}
|
||||
42
qwen3-tts-frontend/src/locales/zh-TW/errors.json
Normal file
42
qwen3-tts-frontend/src/locales/zh-TW/errors.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"http": {
|
||||
"400": "請求參數錯誤",
|
||||
"401": "未授權,請登入",
|
||||
"403": "禁止存取",
|
||||
"404": "請求的資源不存在",
|
||||
"500": "伺服器內部錯誤",
|
||||
"502": "閘道錯誤",
|
||||
"503": "服務暫時不可用",
|
||||
"default": "請求失敗,請稍後重試"
|
||||
},
|
||||
"fieldNames": {
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"text": "文字",
|
||||
"language": "語言",
|
||||
"speaker": "發言人",
|
||||
"instruct": "情感指令",
|
||||
"name": "名稱",
|
||||
"description": "描述",
|
||||
"audio_file": "音訊檔案",
|
||||
"reference_audio": "參考音訊",
|
||||
"reference_text": "參考文字",
|
||||
"api_key": "API金鑰",
|
||||
"backend": "後端服務"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{{field}}不能為空",
|
||||
"minLength": "{{field}}至少需要 {{min}} 個字元",
|
||||
"maxLength": "{{field}}不能超過 {{max}} 個字元",
|
||||
"invalid": "{{field}}格式不正確",
|
||||
"notFound": "未找到{{field}}",
|
||||
"alreadyExists": "{{field}}已存在",
|
||||
"uploadFailed": "{{field}}上傳失敗",
|
||||
"fileTooLarge": "檔案大小不能超過 {{size}}MB",
|
||||
"invalidFileType": "不支援的檔案類型"
|
||||
},
|
||||
"networkError": "網路連線失敗,請檢查網路",
|
||||
"unknownError": "未知錯誤",
|
||||
"operationFailed": "操作失敗",
|
||||
"tryAgain": "請重試"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-TW/index.ts
Normal file
25
qwen3-tts-frontend/src/locales/zh-TW/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import common from './common.json'
|
||||
import nav from './nav.json'
|
||||
import auth from './auth.json'
|
||||
import tts from './tts.json'
|
||||
import voice from './voice.json'
|
||||
import job from './job.json'
|
||||
import settings from './settings.json'
|
||||
import user from './user.json'
|
||||
import errors from './errors.json'
|
||||
import constants from './constants.json'
|
||||
import onboarding from './onboarding.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
nav,
|
||||
auth,
|
||||
tts,
|
||||
voice,
|
||||
job,
|
||||
settings,
|
||||
user,
|
||||
errors,
|
||||
constants,
|
||||
onboarding,
|
||||
}
|
||||
55
qwen3-tts-frontend/src/locales/zh-TW/job.json
Normal file
55
qwen3-tts-frontend/src/locales/zh-TW/job.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"jobHistory": "任務歷史",
|
||||
"jobList": "任務清單",
|
||||
"jobDetails": "任務詳情",
|
||||
"jobId": "任務ID",
|
||||
"status": "狀態",
|
||||
"createdAt": "建立時間",
|
||||
"completedAt": "完成時間",
|
||||
"duration": "耗時",
|
||||
"statusPending": "待處理",
|
||||
"statusProcessing": "進行中",
|
||||
"statusCompleted": "已完成",
|
||||
"statusFailed": "失敗",
|
||||
"noJobs": "暫無任務",
|
||||
"viewJob": "檢視任務",
|
||||
"deleteJob": "刪除任務",
|
||||
"deleteJobConfirm": "確定要刪除此任務嗎?",
|
||||
"jobDeleted": "任務已刪除",
|
||||
"refreshJobs": "重新整理任務",
|
||||
"inputText": "輸入文字",
|
||||
"parameters": "參數",
|
||||
"result": "結果",
|
||||
"errorMessage": "錯誤訊息",
|
||||
"downloadResult": "下載結果",
|
||||
"retryJob": "重試",
|
||||
"cancelJob": "取消任務",
|
||||
"historyTitle": "歷史記錄",
|
||||
"historyCount": "共 {{count}} 條記錄",
|
||||
"retry": "重試",
|
||||
"noHistory": "暫無歷史記錄",
|
||||
"historyDescription": "生成語音後,記錄將會顯示在這裡",
|
||||
"detailsDescription": "檢視任務的詳細參數和生成結果",
|
||||
"basicInfo": "基本資訊",
|
||||
"speaker": "發音人: ",
|
||||
"language": "語言: ",
|
||||
"autoDetect": "自動偵測",
|
||||
"fastMode": "快速模式: ",
|
||||
"useCache": "使用快取: ",
|
||||
"synthesisText": "合成文字",
|
||||
"notSet": "未設定",
|
||||
"voiceDescription": "音色描述",
|
||||
"emotionGuidance": "情緒指導",
|
||||
"referenceText": "參考文字",
|
||||
"notProvided": "未提供",
|
||||
"advancedParameters": "進階參數",
|
||||
"maxNewTokens": "最大生成長度: ",
|
||||
"temperature": "溫度: ",
|
||||
"topK": "Top K: ",
|
||||
"topP": "Top P: ",
|
||||
"repetitionPenalty": "重複懲罰: ",
|
||||
"audioPlayback": "音訊播放",
|
||||
"typeCustomVoice": "自訂音色",
|
||||
"typeVoiceDesign": "音色設計",
|
||||
"typeVoiceClone": "聲音複製"
|
||||
}
|
||||
12
qwen3-tts-frontend/src/locales/zh-TW/nav.json
Normal file
12
qwen3-tts-frontend/src/locales/zh-TW/nav.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"home": "首頁",
|
||||
"settings": "設定",
|
||||
"userManagement": "使用者管理",
|
||||
"logout": "登出",
|
||||
"login": "登入",
|
||||
"toggleTheme": "切換主題",
|
||||
"changeLanguage": "切換語言",
|
||||
"customVoiceTab": "自訂",
|
||||
"voiceDesignTab": "設計",
|
||||
"voiceCloneTab": "複製"
|
||||
}
|
||||
25
qwen3-tts-frontend/src/locales/zh-TW/onboarding.json
Normal file
25
qwen3-tts-frontend/src/locales/zh-TW/onboarding.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"welcome": "歡迎使用 Qwen3 TTS",
|
||||
"configureApiKey": "設定阿里雲 API 金鑰",
|
||||
"selectBackendDescription": "請選擇您的 TTS 後端模式,後續可在設定中修改",
|
||||
"enterApiKeyDescription": "請輸入您的阿里雲 API 金鑰,系統將驗證其有效性",
|
||||
"localModel": "本機模型",
|
||||
"localModelDescription": "免費使用本機 Qwen3-TTS 模型",
|
||||
"localModelNoPermission": "無本機模型權限,請聯絡管理員",
|
||||
"aliyunApi": "阿里雲 API",
|
||||
"aliyunApiRecommended": "(推薦)",
|
||||
"aliyunApiDescription": "需要設定 API 金鑰,按量計費",
|
||||
"skipConfig": "跳過設定",
|
||||
"nextStep": "下一步",
|
||||
"back": "返回",
|
||||
"verifying": "驗證中...",
|
||||
"verifyAndComplete": "驗證並完成",
|
||||
"apiKey": "API 金鑰",
|
||||
"howToGetApiKey": "如何取得 API 金鑰?",
|
||||
"skipSuccess": "已跳過設定,預設使用本機模式",
|
||||
"operationFailed": "操作失敗,請重試",
|
||||
"configComplete": "設定完成,預設使用本機模式",
|
||||
"configCompleteAliyun": "設定完成,預設使用阿里雲模式",
|
||||
"saveFailed": "儲存設定失敗,請重試",
|
||||
"verifyFailed": "API 金鑰驗證失敗,請檢查後重試"
|
||||
}
|
||||
61
qwen3-tts-frontend/src/locales/zh-TW/settings.json
Normal file
61
qwen3-tts-frontend/src/locales/zh-TW/settings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"settings": "設定",
|
||||
"generalSettings": "一般設定",
|
||||
"backendSettings": "後端設定",
|
||||
"apiSettings": "API設定",
|
||||
"preferencesSaved": "偏好設定已儲存",
|
||||
"preferencesSaveFailed": "偏好設定儲存失敗",
|
||||
"defaultBackend": "預設後端",
|
||||
"local": "本機模型",
|
||||
"aliyun": "阿里雲API",
|
||||
"aliyunApiKey": "阿里雲 API 金鑰",
|
||||
"apiKeyPlaceholder": "請輸入 API 金鑰",
|
||||
"apiKeyDescription": "用於阿里雲 TTS 服務認證",
|
||||
"saveApiKey": "儲存金鑰",
|
||||
"apiKeySaved": "API 金鑰已儲存",
|
||||
"apiKeySaveFailed": "API 金鑰儲存失敗",
|
||||
"showApiKey": "顯示金鑰",
|
||||
"hideApiKey": "隱藏金鑰",
|
||||
"testConnection": "測試連線",
|
||||
"connectionSuccess": "連線成功",
|
||||
"connectionFailed": "連線失敗",
|
||||
"language": "介面語言",
|
||||
"languageDescription": "選擇介面顯示語言",
|
||||
"theme": "主題",
|
||||
"themeLight": "淺色",
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "跟隨系統",
|
||||
"title": "設定",
|
||||
"description": "管理您的帳戶設定和偏好",
|
||||
"backendPreference": "後端偏好",
|
||||
"backendPreferenceDescription": "選擇預設的 TTS 後端模式",
|
||||
"localModel": "本機模型",
|
||||
"localModelDescription": "免費使用本機 Qwen3-TTS 模型",
|
||||
"localModelNoPermission": "請聯絡管理員開啟使用本機模型權限",
|
||||
"aliyunApi": "阿里雲 API",
|
||||
"aliyunApiDescription": "使用阿里雲 TTS 服務",
|
||||
"switchedToLocal": "已切換到本機模式",
|
||||
"switchedToAliyun": "已切換到阿里雲模式",
|
||||
"saveFailed": "儲存失敗,請重試",
|
||||
"apiKeyUpdated": "API 金鑰已更新並驗證成功",
|
||||
"apiKeyVerifyFailed": "API 金鑰驗證失敗",
|
||||
"verifyFailed": "驗證失敗",
|
||||
"currentStatus": "目前狀態:",
|
||||
"configured": "已設定且有效",
|
||||
"notConfigured": "未設定",
|
||||
"apiKey": "API 金鑰",
|
||||
"updating": "更新中...",
|
||||
"addKey": "新增金鑰",
|
||||
"updateKey": "更新金鑰",
|
||||
"verifyKey": "驗證金鑰",
|
||||
"deleteKey": "刪除金鑰",
|
||||
"deleteKeyConfirm": "確定要刪除阿里雲 API 金鑰嗎?刪除後將自動切換到本機模式。",
|
||||
"keyDeleted": "API 金鑰已刪除,已切換到本機模式",
|
||||
"deleteFailed": "刪除失敗",
|
||||
"accountInfo": "帳戶資訊",
|
||||
"accountInfoDescription": "您的帳戶基本資訊",
|
||||
"email": "電子郵件",
|
||||
"changePassword": "修改密碼",
|
||||
"passwordChangeSuccess": "密碼修改成功",
|
||||
"passwordChangeFailed": "密碼修改失敗"
|
||||
}
|
||||
80
qwen3-tts-frontend/src/locales/zh-TW/tts.json
Normal file
80
qwen3-tts-frontend/src/locales/zh-TW/tts.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"customVoice": "自訂音色",
|
||||
"voiceDesign": "音色設計",
|
||||
"voiceClone": "音色複製",
|
||||
"text": "文字",
|
||||
"textPlaceholder": "請輸入要合成的文字...",
|
||||
"language": "語言",
|
||||
"speaker": "發言人",
|
||||
"instruct": "情感指令",
|
||||
"instructPlaceholder": "例如:非常開心、有點難過...",
|
||||
"customSpeaker": "自訂發言人",
|
||||
"customSpeakerPlaceholder": "例如:Vivian、Alice...",
|
||||
"presetInstructs": "預設情感",
|
||||
"generate": "生成語音",
|
||||
"generating": "生成中...",
|
||||
"generationSuccess": "語音生成成功",
|
||||
"generationFailed": "語音生成失敗",
|
||||
"audioPlayer": "音訊播放器",
|
||||
"noAudio": "暫無音訊",
|
||||
"downloadAudio": "下載音訊",
|
||||
"playAudio": "播放",
|
||||
"pauseAudio": "暫停",
|
||||
"backend": "後端服務",
|
||||
"localBackend": "本機模型",
|
||||
"aliyunBackend": "阿里雲API",
|
||||
"backendSwitched": "後端已切換",
|
||||
"backendError": "後端服務錯誤",
|
||||
"languageLabel": "語言",
|
||||
"speakerLabel": "發音人",
|
||||
"speakerPlaceholder": "選擇發音人",
|
||||
"textLabel": "合成文字",
|
||||
"instructLabel": "情緒指導(可選)",
|
||||
"instructPlaceholderDesign": "已使用音色設計的預設指導",
|
||||
"instructPlaceholderDefault": "例如:溫柔體貼,語速平緩,充滿關懷",
|
||||
"advancedOptions": "進階選項",
|
||||
"advancedOptionsTitle": "進階參數設定",
|
||||
"advancedOptionsDescription": "調整生成參數以控制音訊品質和生成長度",
|
||||
"creating": "建立中...",
|
||||
"taskCreated": "任務已建立",
|
||||
"taskCreateFailed": "建立任務失敗",
|
||||
"loadDataFailed": "載入資料失敗",
|
||||
"myVoiceDesigns": "我的音色設計",
|
||||
"builtinSpeakers": "內建發音人",
|
||||
"designDescriptionLabel": "音色描述",
|
||||
"designDescriptionPlaceholder": "例如:成熟男性,低沉磁性,充滿權威感",
|
||||
"saveDesignButton": "儲存音色設計",
|
||||
"saveDesignTitle": "儲存音色設計",
|
||||
"saveDesignDescription": "為目前音色設計命名並儲存,以便後續快速使用",
|
||||
"designNameLabel": "設計名稱",
|
||||
"designNamePlaceholder": "例如:磁性男聲",
|
||||
"preparing": "準備中...",
|
||||
"designSaved": "音色設計已儲存",
|
||||
"clonePrepared": "音色複製準備完成",
|
||||
"clonePrepareFailed": "準備複製失敗,但設計已儲存",
|
||||
"saveFailed": "儲存失敗",
|
||||
"fillDesignDescription": "請先填寫音色描述",
|
||||
"fillDesignName": "請輸入設計名稱",
|
||||
"advancedParams": {
|
||||
"maxNewTokens": {
|
||||
"label": "最大生成長度",
|
||||
"description": "控制生成音訊的最大長度。值越大,可生成的音訊越長,但處理時間也會增加"
|
||||
},
|
||||
"temperature": {
|
||||
"label": "溫度",
|
||||
"description": "控制生成的隨機性。值越高生成越隨機多樣,值越低越穩定一致"
|
||||
},
|
||||
"topK": {
|
||||
"label": "Top K",
|
||||
"description": "採樣時只考慮概率最高的 K 個候選。值越小生成越確定,越大越多樣"
|
||||
},
|
||||
"topP": {
|
||||
"label": "Top P (核採樣)",
|
||||
"description": "累積概率閾值,只從累積概率達到 P 的候選中採樣。控制輸出多樣性"
|
||||
},
|
||||
"repetitionPenalty": {
|
||||
"label": "重複懲罰",
|
||||
"description": "懲罰重複內容的生成。值越大越避免重複,但過大可能影響自然度"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/zh-TW/user.json
Normal file
59
qwen3-tts-frontend/src/locales/zh-TW/user.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"userManagement": "使用者管理",
|
||||
"userList": "使用者清單",
|
||||
"userId": "使用者ID",
|
||||
"username": "使用者名稱",
|
||||
"role": "角色",
|
||||
"createdAt": "建立時間",
|
||||
"lastLogin": "最後登入",
|
||||
"actions": "操作",
|
||||
"addUser": "新增使用者",
|
||||
"editUser": "編輯使用者",
|
||||
"deleteUser": "刪除使用者",
|
||||
"deleteUserConfirm": "確定要刪除使用者 {{username}} 嗎?",
|
||||
"userDeleted": "使用者已刪除",
|
||||
"userAdded": "使用者已新增",
|
||||
"userUpdated": "使用者已更新",
|
||||
"userOperationFailed": "使用者操作失敗",
|
||||
"roleAdmin": "管理員",
|
||||
"roleUser": "一般使用者",
|
||||
"password": "密碼",
|
||||
"newPassword": "新密碼",
|
||||
"confirmPassword": "確認密碼",
|
||||
"passwordMismatch": "兩次輸入的密碼不一致",
|
||||
"changePassword": "修改密碼",
|
||||
"passwordChanged": "密碼已修改",
|
||||
"noUsers": "暫無使用者",
|
||||
"searchUsers": "搜尋使用者",
|
||||
"filterByRole": "按角色篩選",
|
||||
"allRoles": "全部角色",
|
||||
"createUser": "建立使用者",
|
||||
"loadUsersFailed": "載入使用者清單失敗",
|
||||
"userUpdateSuccess": "使用者更新成功",
|
||||
"userCreateSuccess": "使用者建立成功",
|
||||
"operationFailed": "操作失敗",
|
||||
"userDeleteSuccess": "使用者刪除成功",
|
||||
"deleteFailed": "刪除失敗",
|
||||
"createUserDialog": "建立使用者",
|
||||
"editUserDialog": "編輯使用者",
|
||||
"createUserDescription": "建立新使用者並設定基本資訊",
|
||||
"editUserDescription": "修改使用者資訊和權限設定",
|
||||
"email": "電子郵件",
|
||||
"passwordOptional": "密碼 (留空則不修改)",
|
||||
"isActive": "啟用狀態",
|
||||
"isSuperuser": "超級管理員",
|
||||
"canUseLocalModel": "本機模型權限",
|
||||
"canUseLocalModelDescription": "允許使用者使用本機 TTS 模型",
|
||||
"saving": "儲存中...",
|
||||
"active": "活躍",
|
||||
"inactive": "停用",
|
||||
"superuser": "超級管理員",
|
||||
"normalUser": "一般使用者",
|
||||
"localModelPermission": "本機模型",
|
||||
"noPermission": "無",
|
||||
"validation": {
|
||||
"usernameMinLength": "使用者名稱至少3個字元",
|
||||
"usernameMaxLength": "使用者名稱最多20個字元",
|
||||
"emailInvalid": "請輸入有效的電子郵件地址"
|
||||
}
|
||||
}
|
||||
59
qwen3-tts-frontend/src/locales/zh-TW/voice.json
Normal file
59
qwen3-tts-frontend/src/locales/zh-TW/voice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"voiceDesign": "音色設計",
|
||||
"voiceClone": "音色複製",
|
||||
"designName": "音色名稱",
|
||||
"designNamePlaceholder": "請輸入音色名稱",
|
||||
"designDescription": "音色描述",
|
||||
"designDescriptionPlaceholder": "請描述音色特點...",
|
||||
"referenceAudio": "參考音訊",
|
||||
"uploadReference": "上傳參考音訊",
|
||||
"referenceText": "參考文字",
|
||||
"referenceTextPlaceholder": "請輸入參考音訊的文字內容...",
|
||||
"cloneName": "複製名稱",
|
||||
"cloneNamePlaceholder": "請輸入複製音色名稱",
|
||||
"cloneDescription": "複製描述",
|
||||
"cloneDescriptionPlaceholder": "請描述複製音色...",
|
||||
"uploadAudio": "上傳音訊",
|
||||
"audioFile": "音訊檔案",
|
||||
"audioText": "音訊文字",
|
||||
"audioTextPlaceholder": "請輸入音訊對應的文字...",
|
||||
"saveVoice": "儲存音色",
|
||||
"savingVoice": "儲存中...",
|
||||
"voiceSaved": "音色已儲存",
|
||||
"voiceSaveFailed": "音色儲存失敗",
|
||||
"deleteVoice": "刪除音色",
|
||||
"deleteVoiceConfirm": "確定要刪除此音色嗎?",
|
||||
"voiceDeleted": "音色已刪除",
|
||||
"voiceList": "音色清單",
|
||||
"noVoices": "暫無音色",
|
||||
"selectVoice": "選擇音色",
|
||||
"voiceDetails": "音色詳情",
|
||||
"createdAt": "建立時間",
|
||||
"updatedAt": "更新時間",
|
||||
"step1Title": "音訊素材",
|
||||
"step2Title": "合成設定",
|
||||
"uploadTab": "上傳音訊",
|
||||
"recordTab": "線上錄製",
|
||||
"refAudioLabel": "參考音訊檔案",
|
||||
"refTextLabel": "參考文稿(可選,提高準確率)",
|
||||
"refTextPlaceholder": "參考音訊對應的文字內容...",
|
||||
"nextStep": "下一步",
|
||||
"prevStep": "上一步",
|
||||
"readPrompt": "請朗讀以下任一段落:",
|
||||
"currentRefText": "目前參考文字",
|
||||
"currentRefTextPlaceholder": "選中的文字將顯示在這裡...",
|
||||
"languageOptional": "語言(可選)",
|
||||
"fastMode": "快速模式",
|
||||
"useCache": "使用快取",
|
||||
"uploadAudioFile": "上傳音訊",
|
||||
"recordOnline": "線上錄製",
|
||||
"validationFailed": "檔案驗證失敗",
|
||||
"validating": "驗證中...",
|
||||
"selectAudioFile": "選擇音訊檔案",
|
||||
"seconds": "秒",
|
||||
"recordingValidationFailed": "錄音驗證失敗",
|
||||
"browserNotSupported": "您的瀏覽器不支援錄音功能",
|
||||
"recordingComplete": "錄製完成",
|
||||
"releaseToFinish": "放開完成",
|
||||
"holdToRecord": "按住錄音"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './locales'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, lazy, Suspense, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Navbar } from '@/components/Navbar'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
@@ -15,6 +16,7 @@ const VoiceDesignForm = lazy(() => import('@/components/tts/VoiceDesignForm'))
|
||||
const VoiceCloneForm = lazy(() => import('@/components/tts/VoiceCloneForm'))
|
||||
|
||||
function Home() {
|
||||
const { t } = useTranslation('nav')
|
||||
const [currentTab, setCurrentTab] = useState('custom-voice')
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
@@ -52,15 +54,15 @@ function Home() {
|
||||
<TabsList className="grid w-full grid-cols-3 h-9">
|
||||
<TabsTrigger value="custom-voice" variant="default">
|
||||
<User className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">自定义</span>
|
||||
<span className="hidden md:inline">{t('customVoiceTab')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="voice-design" variant="secondary">
|
||||
<Palette className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">设计</span>
|
||||
<span className="hidden md:inline">{t('voiceDesignTab')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="voice-clone" variant="outline">
|
||||
<Copy className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">克隆</span>
|
||||
<span className="hidden md:inline">{t('voiceCloneTab')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useForm, type ControllerRenderProps } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as z from 'zod'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -21,20 +22,24 @@ import {
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useState } from 'react'
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(3, '用户名至少需要 3 个字符')
|
||||
.max(20, '用户名不能超过 20 个字符'),
|
||||
password: z.string().min(8, '密码至少需要 8 个字符'),
|
||||
})
|
||||
|
||||
type LoginFormValues = z.infer<typeof loginSchema>
|
||||
type LoginFormValues = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
function Login() {
|
||||
const { login } = useAuth()
|
||||
const { t } = useTranslation('auth')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(3, t('validation.usernameMinLength', { min: 3 }))
|
||||
.max(20, t('validation.usernameMaxLength', { max: 20 })),
|
||||
password: z.string().min(8, t('validation.passwordMinLength', { min: 8 })),
|
||||
})
|
||||
|
||||
const form = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
@@ -58,8 +63,8 @@ function Login() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>登录</CardTitle>
|
||||
<CardDescription>登录到 Qwen3-TTS-WebUI 系统</CardDescription>
|
||||
<CardTitle>{t('login')}</CardTitle>
|
||||
<CardDescription>{t('loginPrompt')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
@@ -69,9 +74,9 @@ function Login() {
|
||||
name="username"
|
||||
render={({ field }: { field: ControllerRenderProps<LoginFormValues, 'username'> }) => (
|
||||
<FormItem>
|
||||
<FormLabel>用户名</FormLabel>
|
||||
<FormLabel>{t('username')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入用户名" {...field} />
|
||||
<Input placeholder={t('usernamePlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -82,11 +87,11 @@ function Login() {
|
||||
name="password"
|
||||
render={({ field }: { field: ControllerRenderProps<LoginFormValues, 'password'> }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -95,7 +100,7 @@ function Login() {
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? '登录中...' : '登录'}
|
||||
{isSubmitting ? t('loggingIn') : t('loginButton')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Eye, EyeOff, Trash2, Check, X } from 'lucide-react'
|
||||
import { Navbar } from '@/components/Navbar'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -24,13 +25,12 @@ import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { authApi } from '@/lib/api'
|
||||
import type { PasswordChangeRequest } from '@/types/auth'
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
api_key: z.string().min(1, '请输入 API 密钥'),
|
||||
const createApiKeySchema = (t: (key: string) => string) => z.object({
|
||||
api_key: z.string().min(1, t('auth:validation.apiKeyRequired')),
|
||||
})
|
||||
|
||||
type ApiKeyFormValues = z.infer<typeof apiKeySchema>
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation(['settings', 'auth', 'user', 'common'])
|
||||
const { user } = useAuth()
|
||||
const { preferences, hasAliyunKey, updatePreferences, refetchPreferences, isBackendAvailable } = useUserPreferences()
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
@@ -38,6 +38,9 @@ export default function Settings() {
|
||||
const [showPasswordDialog, setShowPasswordDialog] = useState(false)
|
||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false)
|
||||
|
||||
const apiKeySchema = createApiKeySchema(t)
|
||||
type ApiKeyFormValues = z.infer<typeof apiKeySchema>
|
||||
|
||||
const form = useForm<ApiKeyFormValues>({
|
||||
resolver: zodResolver(apiKeySchema),
|
||||
defaultValues: {
|
||||
@@ -48,9 +51,9 @@ export default function Settings() {
|
||||
const handleBackendChange = async (value: string) => {
|
||||
try {
|
||||
await updatePreferences({ default_backend: value as 'local' | 'aliyun' })
|
||||
toast.success(`已切换到${value === 'local' ? '本地' : '阿里云'}模式`)
|
||||
toast.success(value === 'local' ? t('settings:switchedToLocal') : t('settings:switchedToAliyun'))
|
||||
} catch (error) {
|
||||
toast.error('保存失败,请重试')
|
||||
toast.error(t('settings:saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +63,9 @@ export default function Settings() {
|
||||
await authApi.setAliyunKey(data.api_key)
|
||||
await refetchPreferences()
|
||||
form.reset()
|
||||
toast.success('API 密钥已更新并验证成功')
|
||||
toast.success(t('settings:apiKeyUpdated'))
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'API 密钥验证失败')
|
||||
toast.error(error.message || t('settings:apiKeyVerifyFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -73,20 +76,20 @@ export default function Settings() {
|
||||
setIsLoading(true)
|
||||
const result = await authApi.verifyAliyunKey()
|
||||
if (result.valid) {
|
||||
toast.success('API 密钥验证成功')
|
||||
toast.success(t('settings:apiKeySaved'))
|
||||
} else {
|
||||
toast.error(result.message || 'API 密钥无效')
|
||||
toast.error(result.message || t('settings:apiKeyVerifyFailed'))
|
||||
}
|
||||
await refetchPreferences()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '验证失败')
|
||||
toast.error(error.message || t('settings:verifyFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
if (!confirm('确定要删除阿里云 API 密钥吗?删除后将自动切换到本地模式。')) {
|
||||
if (!confirm(t('settings:deleteKeyConfirm'))) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,9 +97,9 @@ export default function Settings() {
|
||||
setIsLoading(true)
|
||||
await authApi.deleteAliyunKey()
|
||||
await refetchPreferences()
|
||||
toast.success('API 密钥已删除,已切换到本地模式')
|
||||
toast.success(t('settings:keyDeleted'))
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '删除失败')
|
||||
toast.error(error.message || t('settings:deleteFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -106,10 +109,10 @@ export default function Settings() {
|
||||
try {
|
||||
setIsPasswordLoading(true)
|
||||
await authApi.changePassword(data)
|
||||
toast.success('密码修改成功')
|
||||
toast.success(t('settings:passwordChangeSuccess'))
|
||||
setShowPasswordDialog(false)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '密码修改失败')
|
||||
toast.error(error.message || t('settings:passwordChangeFailed'))
|
||||
throw error
|
||||
} finally {
|
||||
setIsPasswordLoading(false)
|
||||
@@ -127,14 +130,14 @@ export default function Settings() {
|
||||
<main className="flex-1 overflow-y-auto container mx-auto p-3 sm:p-6 max-w-[800px]">
|
||||
<div className="space-y-3 sm:space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">设置</h1>
|
||||
<p className="text-sm sm:text-base text-muted-foreground mt-1 sm:mt-2">管理您的账户设置和偏好</p>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">{t('settings:title')}</h1>
|
||||
<p className="text-sm sm:text-base text-muted-foreground mt-1 sm:mt-2">{t('settings:description')}</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 sm:p-6">
|
||||
<CardTitle className="text-lg sm:text-xl">后端偏好</CardTitle>
|
||||
<CardDescription className="text-sm">选择默认的 TTS 后端模式</CardDescription>
|
||||
<CardTitle className="text-lg sm:text-xl">{t('settings:backendPreference')}</CardTitle>
|
||||
<CardDescription className="text-sm">{t('settings:backendPreferenceDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 sm:p-6">
|
||||
<RadioGroup
|
||||
@@ -150,11 +153,11 @@ export default function Settings() {
|
||||
disabled={!isBackendAvailable('local')}
|
||||
/>
|
||||
<Label htmlFor="backend-local" className="flex-1 cursor-pointer">
|
||||
<div className="font-medium text-sm sm:text-base">本地模型</div>
|
||||
<div className="font-medium text-sm sm:text-base">{t('settings:localModel')}</div>
|
||||
<div className="text-xs sm:text-sm text-muted-foreground">
|
||||
{!isBackendAvailable('local')
|
||||
? '请联系管理员开启使用本地模型权限'
|
||||
: '免费使用本地 Qwen3-TTS 模型'
|
||||
? t('settings:localModelNoPermission')
|
||||
: t('settings:localModelDescription')
|
||||
}
|
||||
</div>
|
||||
</Label>
|
||||
@@ -162,8 +165,8 @@ export default function Settings() {
|
||||
<div className="flex items-center space-x-2 sm:space-x-3 border rounded-lg p-3 sm:p-4 hover:bg-accent/50 cursor-pointer">
|
||||
<RadioGroupItem value="aliyun" id="backend-aliyun" />
|
||||
<Label htmlFor="backend-aliyun" className="flex-1 cursor-pointer">
|
||||
<div className="font-medium text-sm sm:text-base">阿里云 API</div>
|
||||
<div className="text-xs sm:text-sm text-muted-foreground">使用阿里云 TTS 服务</div>
|
||||
<div className="font-medium text-sm sm:text-base">{t('settings:aliyunApi')}</div>
|
||||
<div className="text-xs sm:text-sm text-muted-foreground">{t('settings:aliyunApiDescription')}</div>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
@@ -172,21 +175,21 @@ export default function Settings() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 sm:p-6">
|
||||
<CardTitle className="text-lg sm:text-xl">阿里云 API 密钥</CardTitle>
|
||||
<CardDescription className="text-sm">管理您的阿里云 API 密钥配置</CardDescription>
|
||||
<CardTitle className="text-lg sm:text-xl">{t('settings:aliyunApiKey')}</CardTitle>
|
||||
<CardDescription className="text-sm">{t('settings:apiKeyDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 sm:space-y-4 p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
<span className="text-muted-foreground">当前状态:</span>
|
||||
<span className="text-muted-foreground">{t('settings:currentStatus')}</span>
|
||||
{hasAliyunKey ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<Check className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
已配置并有效
|
||||
{t('settings:configured')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
未配置
|
||||
{t('settings:notConfigured')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -198,7 +201,7 @@ export default function Settings() {
|
||||
name="api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm sm:text-base">API 密钥</FormLabel>
|
||||
<FormLabel className="text-sm sm:text-base">{t('settings:apiKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
@@ -231,7 +234,7 @@ export default function Settings() {
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="submit" disabled={isLoading} className="flex-1 sm:flex-initial">
|
||||
{isLoading ? '更新中...' : hasAliyunKey ? '更新密钥' : '添加密钥'}
|
||||
{isLoading ? t('settings:updating') : hasAliyunKey ? t('settings:updateKey') : t('settings:addKey')}
|
||||
</Button>
|
||||
{hasAliyunKey && (
|
||||
<>
|
||||
@@ -242,7 +245,7 @@ export default function Settings() {
|
||||
disabled={isLoading}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
验证密钥
|
||||
{t('settings:verifyKey')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -253,7 +256,7 @@ export default function Settings() {
|
||||
className="sm:w-auto sm:px-4"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline sm:ml-2">删除密钥</span>
|
||||
<span className="hidden sm:inline sm:ml-2">{t('settings:deleteKey')}</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -265,20 +268,20 @@ export default function Settings() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 sm:p-6">
|
||||
<CardTitle className="text-lg sm:text-xl">账户信息</CardTitle>
|
||||
<CardDescription className="text-sm">您的账户基本信息</CardDescription>
|
||||
<CardTitle className="text-lg sm:text-xl">{t('settings:accountInfo')}</CardTitle>
|
||||
<CardDescription className="text-sm">{t('settings:accountInfoDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 sm:space-y-4 p-4 sm:p-6">
|
||||
<div className="grid gap-1.5 sm:gap-2">
|
||||
<Label className="text-sm sm:text-base">用户名</Label>
|
||||
<Label className="text-sm sm:text-base">{t('user:username')}</Label>
|
||||
<Input value={user.username} disabled />
|
||||
</div>
|
||||
<div className="grid gap-1.5 sm:gap-2">
|
||||
<Label className="text-sm sm:text-base">邮箱</Label>
|
||||
<Label className="text-sm sm:text-base">{t('settings:email')}</Label>
|
||||
<Input value={user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => setShowPasswordDialog(true)} className="w-full sm:w-auto">修改密码</Button>
|
||||
<Button onClick={() => setShowPasswordDialog(true)} className="w-full sm:w-auto">{t('settings:changePassword')}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Navbar } from '@/components/Navbar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -12,6 +13,7 @@ import type { User } from '@/types/auth'
|
||||
import type { UserCreateRequest, UserUpdateRequest } from '@/types/user'
|
||||
|
||||
export default function UserManagement() {
|
||||
const { t } = useTranslation(['user', 'common'])
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -26,7 +28,7 @@ export default function UserManagement() {
|
||||
const response = await userApi.listUsers()
|
||||
setUsers(response.users)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '加载用户列表失败')
|
||||
toast.error(error.message || t('user:loadUsersFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -60,15 +62,15 @@ export default function UserManagement() {
|
||||
delete updateData.password
|
||||
}
|
||||
await userApi.updateUser(selectedUser.id, updateData)
|
||||
toast.success('用户更新成功')
|
||||
toast.success(t('user:userUpdateSuccess'))
|
||||
} else {
|
||||
await userApi.createUser(data as UserCreateRequest)
|
||||
toast.success('用户创建成功')
|
||||
toast.success(t('user:userCreateSuccess'))
|
||||
}
|
||||
setUserDialogOpen(false)
|
||||
await loadUsers()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '操作失败')
|
||||
toast.error(error.message || t('user:operationFailed'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -80,11 +82,11 @@ export default function UserManagement() {
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
await userApi.deleteUser(selectedUser.id)
|
||||
toast.success('用户删除成功')
|
||||
toast.success(t('user:userDeleteSuccess'))
|
||||
setDeleteDialogOpen(false)
|
||||
await loadUsers()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '删除失败')
|
||||
toast.error(error.message || t('user:deleteFailed'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -96,10 +98,10 @@ export default function UserManagement() {
|
||||
<div className="container mx-auto p-4 sm:p-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<CardTitle>用户管理</CardTitle>
|
||||
<CardTitle>{t('user:userManagement')}</CardTitle>
|
||||
<Button onClick={handleCreateUser} className="w-full sm:w-auto">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建用户
|
||||
{t('user:createUser')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -35,4 +35,5 @@ export interface UserPreferences {
|
||||
default_backend: 'local' | 'aliyun'
|
||||
onboarding_completed: boolean
|
||||
available_backends?: string[]
|
||||
language?: 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user