重构了tts
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-05 17:34:15 +08:00
parent bd7eca1bd0
commit be3eb17490
24 changed files with 170 additions and 1593 deletions

View File

@@ -10,3 +10,6 @@ GITHUB_CLIENT_SECRET=
// Database // Database
DATABASE_URL= DATABASE_URL=
// DashScore
DASHSCORE_API_KEY=

View File

@@ -93,6 +93,9 @@ GITHUB_CLIENT_SECRET=your-client-secret
# 数据库 # 数据库
DATABASE_URL=postgresql://username:password@localhost:5432/database_name DATABASE_URL=postgresql://username:password@localhost:5432/database_name
// DashScore
DASHSCORE_API_KEY=
``` ```
## 重要配置细节 ## 重要配置细节

View File

@@ -26,7 +26,7 @@
### 国际化与辅助功能 ### 国际化与辅助功能
- **next-intl** - 国际化解决方案 - **next-intl** - 国际化解决方案
- **edge-tts-universal** - 跨平台文本转语音 - **qwen3-tts-flash** - 通义千问语音合成
### 开发工具 ### 开发工具
- **ESLint** - 代码质量检查 - **ESLint** - 代码质量检查

View File

@@ -40,8 +40,8 @@
"update": "Aktualisieren", "update": "Aktualisieren",
"text1": "Text 1", "text1": "Text 1",
"text2": "Text 2", "text2": "Text 2",
"locale1": "Sprache 1", "language1": "Sprache 1",
"locale2": "Sprache 2", "language2": "Sprache 2",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"delete": "Löschen" "delete": "Löschen"
}, },

View File

@@ -40,8 +40,8 @@
"update": "Update", "update": "Update",
"text1": "Text 1", "text1": "Text 1",
"text2": "Text 2", "text2": "Text 2",
"locale1": "Locale 1", "language1": "Locale 1",
"locale2": "Locale 2", "language2": "Locale 2",
"edit": "Edit", "edit": "Edit",
"delete": "Delete" "delete": "Delete"
}, },

View File

@@ -40,8 +40,8 @@
"update": "Mettre à jour", "update": "Mettre à jour",
"text1": "Texte 1", "text1": "Texte 1",
"text2": "Texte 2", "text2": "Texte 2",
"locale1": "Langue 1", "language1": "Langue 1",
"locale2": "Langue 2", "language2": "Langue 2",
"edit": "Modifier", "edit": "Modifier",
"delete": "Supprimer" "delete": "Supprimer"
}, },

View File

@@ -40,8 +40,8 @@
"update": "Aggiorna", "update": "Aggiorna",
"text1": "Testo 1", "text1": "Testo 1",
"text2": "Testo 2", "text2": "Testo 2",
"locale1": "Lingua 1", "language1": "Lingua 1",
"locale2": "Lingua 2", "language2": "Lingua 2",
"edit": "Modifica", "edit": "Modifica",
"delete": "Elimina" "delete": "Elimina"
}, },

View File

@@ -40,8 +40,8 @@
"update": "更新", "update": "更新",
"text1": "テキスト1", "text1": "テキスト1",
"text2": "テキスト2", "text2": "テキスト2",
"locale1": "言語1", "language1": "言語1",
"locale2": "言語2", "language2": "言語2",
"edit": "編集", "edit": "編集",
"delete": "削除" "delete": "削除"
}, },

View File

@@ -40,8 +40,8 @@
"update": "업데이트", "update": "업데이트",
"text1": "텍스트 1", "text1": "텍스트 1",
"text2": "텍스트 2", "text2": "텍스트 2",
"locale1": "언어 1", "language1": "언어 1",
"locale2": "언어 2", "language2": "언어 2",
"edit": "편집", "edit": "편집",
"delete": "삭제" "delete": "삭제"
}, },

View File

@@ -40,8 +40,8 @@
"update": "يېڭىلاش", "update": "يېڭىلاش",
"text1": "تېكىست 1", "text1": "تېكىست 1",
"text2": "تېكىست 2", "text2": "تېكىست 2",
"locale1": "تىل 1", "language1": "تىل 1",
"locale2": "تىل 2", "language2": "تىل 2",
"edit": "تەھرىرلەش", "edit": "تەھرىرلەش",
"delete": "ئۆچۈرۈش" "delete": "ئۆچۈرۈش"
}, },

View File

@@ -40,8 +40,8 @@
"update": "更新", "update": "更新",
"text1": "文本1", "text1": "文本1",
"text2": "文本2", "text2": "文本2",
"locale1": "语言1", "language1": "语言1",
"locale2": "语言2", "language2": "语言2",
"edit": "编辑", "edit": "编辑",
"delete": "删除" "delete": "删除"
}, },

View File

@@ -16,7 +16,6 @@
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-auth": "^1.4.10", "better-auth": "^1.4.10",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"edge-tts-universal": "^1.3.3",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "16.1.1", "next": "16.1.1",
"next-intl": "^4.7.0", "next-intl": "^4.7.0",

186
pnpm-lock.yaml generated
View File

@@ -27,9 +27,6 @@ importers:
dotenv: dotenv:
specifier: ^17.2.3 specifier: ^17.2.3
version: 17.2.3 version: 17.2.3
edge-tts-universal:
specifier: ^1.3.3
version: 1.3.3
lucide-react: lucide-react:
specifier: ^0.562.0 specifier: ^0.562.0
version: 0.562.0(react@19.2.3) version: 0.562.0(react@19.2.3)
@@ -1238,10 +1235,6 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1299,9 +1292,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1314,9 +1304,6 @@ packages:
resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1503,10 +1490,6 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@12.1.0: commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1527,9 +1510,6 @@ packages:
cookie-es@1.2.2: cookie-es@1.2.2:
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
cross-fetch@4.1.0:
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1613,10 +1593,6 @@ packages:
defu@6.1.4: defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0: denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@@ -1738,10 +1714,6 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
edge-tts-universal@1.3.3:
resolution: {integrity: sha512-jlUvYGsJ+o93FRPWQDQa/E7jqYbuLJAKKq9qvylo0/yHE1vtp4HCSzSAamviBewmpaNHlEJm+eEMimJXfu98zw==}
engines: {node: ^18.17 || ^20.9 || >=22, npm: '>=9'}
effect@3.18.4: effect@3.18.4:
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
@@ -1976,15 +1948,6 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1993,10 +1956,6 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fs-constants@1.0.0: fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -2122,10 +2081,6 @@ packages:
http-status-codes@2.3.0: http-status-codes@2.3.0:
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.7.0: iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2291,11 +2246,6 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isomorphic-ws@5.0.0:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
ws: '*'
iterator.prototype@1.1.5: iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2484,14 +2434,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-response@3.1.0: mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2588,15 +2530,6 @@ packages:
node-fetch-native@1.6.7: node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-mock-http@1.0.3: node-mock-http@1.0.3:
resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==}
@@ -2821,9 +2754,6 @@ packages:
proper-lockfile@4.1.2: proper-lockfile@4.1.2:
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
pump@3.0.3: pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3120,9 +3050,6 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-api-utils@2.1.0: ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'} engines: {node: '>=18.12'}
@@ -3275,10 +3202,6 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
valibot@1.2.0: valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies: peerDependencies:
@@ -3287,12 +3210,6 @@ packages:
typescript: typescript:
optional: true optional: true
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-boxed-primitive@1.1.1: which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3321,25 +3238,10 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
wsl-utils@0.1.0: wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'} engines: {node: '>=18'}
xml-escape@1.1.0:
resolution: {integrity: sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==}
xtend@4.0.2: xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
@@ -4582,8 +4484,6 @@ snapshots:
acorn@8.15.0: {} acorn@8.15.0: {}
agent-base@7.1.4: {}
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -4675,8 +4575,6 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
@@ -4685,14 +4583,6 @@ snapshots:
axe-core@4.11.0: {} axe-core@4.11.0: {}
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
babel-plugin-react-compiler@1.0.0: babel-plugin-react-compiler@1.0.0:
@@ -4896,10 +4786,6 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@12.1.0: {} commander@12.1.0: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
@@ -4912,12 +4798,6 @@ snapshots:
cookie-es@1.2.2: {} cookie-es@1.2.2: {}
cross-fetch@4.1.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -4993,8 +4873,6 @@ snapshots:
defu@6.1.4: {} defu@6.1.4: {}
delayed-stream@1.0.0: {}
denque@2.1.0: {} denque@2.1.0: {}
destr@2.0.5: {} destr@2.0.5: {}
@@ -5031,22 +4909,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
edge-tts-universal@1.3.3:
dependencies:
axios: 1.13.2
cross-fetch: 4.1.0
https-proxy-agent: 7.0.6
isomorphic-ws: 5.0.0(ws@8.18.3)
uuid: 11.1.0
ws: 8.18.3
xml-escape: 1.1.0
transitivePeerDependencies:
- bufferutil
- debug
- encoding
- supports-color
- utf-8-validate
effect@3.18.4: effect@3.18.4:
dependencies: dependencies:
'@standard-schema/spec': 1.0.0 '@standard-schema/spec': 1.0.0
@@ -5427,8 +5289,6 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.11: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
@@ -5438,14 +5298,6 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fs-constants@1.0.0: {} fs-constants@1.0.0: {}
function-bind@1.1.2: {} function-bind@1.1.2: {}
@@ -5579,13 +5431,6 @@ snapshots:
http-status-codes@2.3.0: {} http-status-codes@2.3.0: {}
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
iconv-lite@0.7.0: iconv-lite@0.7.0:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@@ -5750,10 +5595,6 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
isomorphic-ws@5.0.0(ws@8.18.3):
dependencies:
ws: 8.18.3
iterator.prototype@1.1.5: iterator.prototype@1.1.5:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -5903,12 +5744,6 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-response@3.1.0: {} mimic-response@3.1.0: {}
minimatch@3.1.2: minimatch@3.1.2:
@@ -6004,10 +5839,6 @@ snapshots:
node-fetch-native@1.6.7: {} node-fetch-native@1.6.7: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-mock-http@1.0.3: {} node-mock-http@1.0.3: {}
node-releases@2.0.27: {} node-releases@2.0.27: {}
@@ -6252,8 +6083,6 @@ snapshots:
retry: 0.12.0 retry: 0.12.0
signal-exit: 3.0.7 signal-exit: 3.0.7
proxy-from-env@1.1.0: {}
pump@3.0.3: pump@3.0.3:
dependencies: dependencies:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
@@ -6608,8 +6437,6 @@ snapshots:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
tr46@0.0.3: {}
ts-api-utils@2.1.0(typescript@5.9.3): ts-api-utils@2.1.0(typescript@5.9.3):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
@@ -6750,19 +6577,10 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@11.1.0: {}
valibot@1.2.0(typescript@5.9.3): valibot@1.2.0(typescript@5.9.3):
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-boxed-primitive@1.1.1: which-boxed-primitive@1.1.1:
dependencies: dependencies:
is-bigint: 1.1.0 is-bigint: 1.1.0
@@ -6812,14 +6630,10 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.18.3: {}
wsl-utils@0.1.0: wsl-utils@0.1.0:
dependencies: dependencies:
is-wsl: 3.1.0 is-wsl: 3.1.0
xml-escape@1.1.0: {}
xtend@4.0.2: {} xtend@4.0.2: {}
yallist@3.1.1: {} yallist@3.1.1: {}

View File

@@ -2,8 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSAudioUrl } from "@/lib/browser/tts"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import localFont from "next/font/local"; import localFont from "next/font/local";
import { isNonNegativeInteger, SeededRandom } from "@/lib/utils"; import { isNonNegativeInteger, SeededRandom } from "@/lib/utils";
@@ -59,21 +58,33 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
if (show === "answer") { if (show === "answer") {
const newIndex = (index + 1) % getTextPairs().length; const newIndex = (index + 1) % getTextPairs().length;
setIndex(newIndex); setIndex(newIndex);
if (dictation) if (dictation) {
getTTSAudioUrl( const textPair = getTextPairs()[newIndex];
getTextPairs()[newIndex][reverse ? "text2" : "text1"], const language = textPair[reverse ? "language2" : "language1"];
VOICES.find( const text = textPair[reverse ? "text2" : "text1"];
(v) =>
v.locale === // 映射语言到 TTS 支持的格式
getTextPairs()[newIndex][ const languageMap: Record<string, TTS_SUPPORTED_LANGUAGES> = {
reverse ? "locale2" : "locale1" "chinese": "Chinese",
], "english": "English",
)!.short_name, "japanese": "Japanese",
).then((url) => { "korean": "Korean",
"french": "French",
"german": "German",
"italian": "Italian",
"portuguese": "Portuguese",
"spanish": "Spanish",
"russian": "Russian",
};
const ttsLanguage = languageMap[language?.toLowerCase()] || "Auto";
getTTSUrl(text, ttsLanguage).then((url) => {
load(url); load(url);
play(); play();
}); });
} }
}
setShow(show === "question" ? "answer" : "question"); setShow(show === "question" ? "answer" : "question");
}; };

View File

@@ -12,7 +12,6 @@ import { ChangeEvent, useEffect, useRef, useState } from "react";
import z from "zod"; import z from "zod";
import SaveList from "./SaveList"; import SaveList from "./SaveList";
import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
import { genIPA, genLanguage } from "@/lib/server/bigmodel/translatorActions"; import { genIPA, genLanguage } from "@/lib/server/bigmodel/translatorActions";
@@ -160,7 +159,7 @@ export default function TextSpeakerPage() {
const handleUseItem = (item: z.infer<typeof TextSpeakerItemSchema>) => { const handleUseItem = (item: z.infer<typeof TextSpeakerItemSchema>) => {
if (textareaRef.current) textareaRef.current.value = item.text; if (textareaRef.current) textareaRef.current.value = item.text;
textRef.current = item.text; textRef.current = item.text;
setLanguage(item.locale); setLanguage(item.language);
setIPA(item.ipa || ""); setIPA(item.ipa || "");
if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current);
objurlRef.current = null; objurlRef.current = null;
@@ -202,12 +201,12 @@ export default function TextSpeakerPage() {
} else if (theIPA.length === 0) { } else if (theIPA.length === 0) {
save.push({ save.push({
text: textRef.current, text: textRef.current,
locale: theLanguage as string, language: theLanguage as string,
}); });
} else { } else {
save.push({ save.push({
text: textRef.current, text: textRef.current,
locale: theLanguage as string, language: theLanguage as string,
ipa: theIPA, ipa: theIPA,
}); });
} }

View File

@@ -57,8 +57,8 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
createPair({ createPair({
text1: item.text1, text1: item.text1,
text2: item.text2, text2: item.text2,
locale1: item.locale1, language1: item.language1,
locale2: item.locale2, language2: item.language2,
folder: { folder: {
connect: { connect: {
id: folder.id, id: folder.id,

View File

@@ -3,7 +3,6 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import IMAGES from "@/config/images";
import { VOICES } from "@/config/locales";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { TranslationHistorySchema } from "@/lib/interfaces"; import { TranslationHistorySchema } from "@/lib/interfaces";
import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators"; import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators";
@@ -23,7 +22,7 @@ import FolderSelector from "./FolderSelector";
import { createPair } from "@/lib/server/services/pairService"; import { createPair } from "@/lib/server/services/pairService";
import { shallowEqual } from "@/lib/utils"; import { shallowEqual } from "@/lib/utils";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { getTTSUrl } from "@/lib/server/bigmodel/tts"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
export default function TranslatorPage() { export default function TranslatorPage() {
const t = useTranslations("translator"); const t = useTranslations("translator");
@@ -51,7 +50,20 @@ export default function TranslatorPage() {
const tts = async (text: string, locale: string) => { const tts = async (text: string, locale: string) => {
if (lastTTS.current.text !== text) { if (lastTTS.current.text !== text) {
try { try {
const url = await getTTSUrl(text, locale); // Map language name to TTS format
let theLanguage = locale.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase());
// Check if language is in TTS supported list
const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [
"Auto", "Chinese", "English", "German", "Italian", "Portuguese",
"Spanish", "Japanese", "Korean", "French", "Russian"
];
if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) {
theLanguage = "Auto";
}
const url = await getTTSUrl(text, theLanguage as TTS_SUPPORTED_LANGUAGES);
await load(url); await load(url);
lastTTS.current.text = text; lastTTS.current.text = text;
lastTTS.current.url = url; lastTTS.current.url = url;
@@ -74,15 +86,15 @@ export default function TranslatorPage() {
const llmres: { const llmres: {
text1: string | null; text1: string | null;
text2: string | null; text2: string | null;
locale1: string | null; language1: string | null;
locale2: string | null; language2: string | null;
ipa1: string | null; ipa1: string | null;
ipa2: string | null; ipa2: string | null;
} = { } = {
text1: text1, text1: text1,
text2: null, text2: null,
locale1: null, language1: null,
locale2: null, language2: null,
ipa1: null, ipa1: null,
ipa2: null, ipa2: null,
}; };
@@ -92,21 +104,21 @@ export default function TranslatorPage() {
// 检查更新历史记录 // 检查更新历史记录
const checkUpdateLocalStorage = () => { const checkUpdateLocalStorage = () => {
if (historyUpdated) return; if (historyUpdated) return;
if (llmres.text1 && llmres.text2 && llmres.locale1 && llmres.locale2) { if (llmres.text1 && llmres.text2 && llmres.language1 && llmres.language2) {
setHistory( setHistory(
tlsoPush({ tlsoPush({
text1: llmres.text1, text1: llmres.text1,
text2: llmres.text2, text2: llmres.text2,
locale1: llmres.locale1, language1: llmres.language1,
locale2: llmres.locale2, language2: llmres.language2,
}), }),
); );
if (autoSave && autoSaveFolderId) { if (autoSave && autoSaveFolderId) {
createPair({ createPair({
text1: llmres.text1, text1: llmres.text1,
text2: llmres.text2, text2: llmres.text2,
locale1: llmres.locale1, language1: llmres.language1,
locale2: llmres.locale2, language2: llmres.language2,
folder: { folder: {
connect: { connect: {
id: autoSaveFolderId, id: autoSaveFolderId,
@@ -143,10 +155,10 @@ export default function TranslatorPage() {
setTresult(text2); setTresult(text2);
// 生成两个locale // 生成两个locale
genLocale(text1).then((locale) => { genLocale(text1).then((locale) => {
updateState("locale1", locale); updateState("language1", locale);
}); });
genLocale(text2).then((locale) => { genLocale(text2).then((locale) => {
updateState("locale2", locale); updateState("language2", locale);
}); });
// 生成俩IPA // 生成俩IPA
if (genIpa) { if (genIpa) {
@@ -202,7 +214,7 @@ export default function TranslatorPage() {
onClick={() => { onClick={() => {
const t = taref.current?.value; const t = taref.current?.value;
if (!t) return; if (!t) return;
tts(t, tlso.get().find((v) => v.text1 === t)?.locale1 || ""); tts(t, tlso.get().find((v) => v.text1 === t)?.language1 || "");
}} }}
></IconClick> ></IconClick>
</div> </div>
@@ -240,7 +252,7 @@ export default function TranslatorPage() {
onClick={() => { onClick={() => {
tts( tts(
tresult, tresult,
tlso.get().find((v) => v.text2 === tresult)?.locale2 || "", tlso.get().find((v) => v.text2 === tresult)?.language2 || "",
); );
}} }}
></IconClick> ></IconClick>

View File

@@ -11,8 +11,8 @@ interface AddTextPairModalProps {
onAdd: ( onAdd: (
text1: string, text1: string,
text2: string, text2: string,
locale1: string, language1: string,
locale2: string, language2: string,
) => void; ) => void;
} }
@@ -24,8 +24,8 @@ export default function AddTextPairModal({
const t = useTranslations("folder_id"); const t = useTranslations("folder_id");
const input1Ref = useRef<HTMLInputElement>(null); const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null); const input2Ref = useRef<HTMLInputElement>(null);
const [locale1, setLocale1] = useState("en-US"); const [language1, setLanguage1] = useState("english");
const [locale2, setLocale2] = useState("zh-CN"); const [language2, setLanguage2] = useState("chinese");
if (!isOpen) return null; if (!isOpen) return null;
@@ -33,8 +33,8 @@ export default function AddTextPairModal({
if ( if (
!input1Ref.current?.value || !input1Ref.current?.value ||
!input2Ref.current?.value || !input2Ref.current?.value ||
!locale1 || !language1 ||
!locale2 !language2
) )
return; return;
@@ -44,14 +44,14 @@ export default function AddTextPairModal({
if ( if (
typeof text1 === "string" && typeof text1 === "string" &&
typeof text2 === "string" && typeof text2 === "string" &&
typeof locale1 === "string" && typeof language1 === "string" &&
typeof locale2 === "string" && typeof language2 === "string" &&
text1.trim() !== "" && text1.trim() !== "" &&
text2.trim() !== "" && text2.trim() !== "" &&
locale1.trim() !== "" && language1.trim() !== "" &&
locale2.trim() !== "" language2.trim() !== ""
) { ) {
onAdd(text1, text2, locale1, locale2); onAdd(text1, text2, language1, language2);
input1Ref.current.value = ""; input1Ref.current.value = "";
input2Ref.current.value = ""; input2Ref.current.value = "";
} }
@@ -84,12 +84,12 @@ export default function AddTextPairModal({
<Input ref={input2Ref} className="w-full"></Input> <Input ref={input2Ref} className="w-full"></Input>
</div> </div>
<div> <div>
{t("locale1")} {t("language1")}
<LocaleSelector value={locale1} onChange={setLocale1} /> <LocaleSelector value={language1} onChange={setLanguage1} />
</div> </div>
<div> <div>
{t("locale2")} {t("language2")}
<LocaleSelector value={locale2} onChange={setLocale2} /> <LocaleSelector value={language2} onChange={setLanguage2} />
</div> </div>
</div> </div>
<LightButton onClick={handleAdd}>{t("add")}</LightButton> <LightButton onClick={handleAdd}>{t("add")}</LightButton>

View File

@@ -140,14 +140,14 @@ export default function InFolder({ folderId }: { folderId: number }) {
onAdd={async ( onAdd={async (
text1: string, text1: string,
text2: string, text2: string,
locale1: string, language1: string,
locale2: string, language2: string,
) => { ) => {
await createPair({ await createPair({
text1: text1, text1: text1,
text2: text2, text2: text2,
language1: locale1, language1: language1,
language2: locale2, language2: language2,
folder: { folder: {
connect: { connect: {
id: folderId, id: folderId,

View File

@@ -23,8 +23,8 @@ export default function UpdateTextPairModal({
const t = useTranslations("folder_id"); const t = useTranslations("folder_id");
const input1Ref = useRef<HTMLInputElement>(null); const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null); const input2Ref = useRef<HTMLInputElement>(null);
const [locale1, setLocale1] = useState(textPair.language1); const [language1, setLanguage1] = useState(textPair.language1);
const [locale2, setLocale2] = useState(textPair.language2); const [language2, setLanguage2] = useState(textPair.language2);
if (!isOpen) return null; if (!isOpen) return null;
@@ -32,8 +32,8 @@ export default function UpdateTextPairModal({
if ( if (
!input1Ref.current?.value || !input1Ref.current?.value ||
!input2Ref.current?.value || !input2Ref.current?.value ||
!locale1 || !language1 ||
!locale2 !language2
) )
return; return;
@@ -43,14 +43,14 @@ export default function UpdateTextPairModal({
if ( if (
typeof text1 === "string" && typeof text1 === "string" &&
typeof text2 === "string" && typeof text2 === "string" &&
typeof locale1 === "string" && typeof language1 === "string" &&
typeof locale2 === "string" && typeof language2 === "string" &&
text1.trim() !== "" && text1.trim() !== "" &&
text2.trim() !== "" && text2.trim() !== "" &&
locale1.trim() !== "" && language1.trim() !== "" &&
locale2.trim() !== "" language2.trim() !== ""
) { ) {
onUpdate(textPair.id, { text1, text2, locale1, locale2 }); onUpdate(textPair.id, { text1, text2, language1, language2 });
} }
}; };
return ( return (
@@ -88,12 +88,12 @@ export default function UpdateTextPairModal({
></Input> ></Input>
</div> </div>
<div> <div>
{t("locale1")} {t("language1")}
<LocaleSelector value={locale1} onChange={setLocale1} /> <LocaleSelector value={language1} onChange={setLanguage1} />
</div> </div>
<div> <div>
{t("locale2")} {t("language2")}
<LocaleSelector value={locale2} onChange={setLocale2} /> <LocaleSelector value={language2} onChange={setLanguage2} />
</div> </div>
</div> </div>
<LightButton onClick={handleUpdate}>{t("update")}</LightButton> <LightButton onClick={handleUpdate}>{t("update")}</LightButton>

View File

@@ -1,10 +1,16 @@
import { LOCALES } from "@/config/locales"; import { useState } from "react";
const COMMON_LOCALES = [ const COMMON_LANGUAGES = [
{ label: "中文", value: "zh-CN" }, { label: "中文", value: "chinese" },
{ label: "英文", value: "en-US" }, { label: "英文", value: "english" },
{ label: "意大利语", value: "it-IT" }, { label: "意大利语", value: "italian" },
{ label: "日语", value: "ja-JP" }, { label: "日语", value: "japanese" },
{ label: "韩语", value: "korean" },
{ label: "法语", value: "french" },
{ label: "德语", value: "german" },
{ label: "西班牙语", value: "spanish" },
{ label: "葡萄牙语", value: "portuguese" },
{ label: "俄语", value: "russian" },
{ label: "其他", value: "other" }, { label: "其他", value: "other" },
]; ];
@@ -14,34 +20,50 @@ interface LocaleSelectorProps {
} }
export function LocaleSelector({ value, onChange }: LocaleSelectorProps) { export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other"); const [customInput, setCustomInput] = useState("");
const showFullList = value === "other" || !isCommonLocale; const isCommonLanguage = COMMON_LANGUAGES.some((l) => l.value === value && l.value !== "other");
const showCustomInput = value === "other" || !isCommonLanguage;
// 计算输入框的值:如果是"other"使用自定义输入,否则使用外部传入的值
const inputValue = value === "other" ? customInput : value;
// 处理自定义输入
const handleCustomInputChange = (inputValue: string) => {
setCustomInput(inputValue);
onChange(inputValue);
};
// 当选择常见语言或"其他"时
const handleSelectChange = (selectedValue: string) => {
if (selectedValue === "other") {
setCustomInput("");
onChange("other");
} else {
onChange(selectedValue);
}
};
return ( return (
<div> <div>
<select <select
value={isCommonLocale ? value : "other"} value={isCommonLanguage ? value : "other"}
onChange={(e) => onChange(e.target.value)} onChange={(e) => handleSelectChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]" className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
> >
{COMMON_LOCALES.map((locale) => ( {COMMON_LANGUAGES.map((lang) => (
<option key={locale.value} value={locale.value}> <option key={lang.value} value={lang.value}>
{locale.label} {lang.label}
</option> </option>
))} ))}
</select> </select>
{showFullList && ( {showCustomInput && (
<select <input
value={value === "other" ? LOCALES[0] : value} type="text"
onChange={(e) => onChange(e.target.value)} value={inputValue}
onChange={(e) => handleCustomInputChange(e.target.value)}
placeholder="请输入语言名称"
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2" className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
> />
{LOCALES.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
)} )}
</div> </div>
); );

File diff suppressed because it is too large Load Diff

View File

@@ -19,15 +19,15 @@ export type SupportedAlphabets =
export const TextSpeakerItemSchema = z.object({ export const TextSpeakerItemSchema = z.object({
text: z.string(), text: z.string(),
ipa: z.string().optional(), ipa: z.string().optional(),
locale: z.string(), language: z.string(),
}); });
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema); export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
export const WordDataSchema = z.object({ export const WordDataSchema = z.object({
locales: z languages: z
.tuple([z.string(), z.string()]) .tuple([z.string(), z.string()])
.refine(([first, second]) => first !== second, { .refine(([first, second]) => first !== second, {
message: "Locales must be different", message: "Languages must be different",
}), }),
wordPairs: z wordPairs: z
.array(z.tuple([z.string(), z.string()])) .array(z.tuple([z.string(), z.string()]))
@@ -47,8 +47,8 @@ export const WordDataSchema = z.object({
export const TranslationHistorySchema = z.object({ export const TranslationHistorySchema = z.object({
text1: z.string(), text1: z.string(),
text2: z.string(), text2: z.string(),
locale1: z.string(), language1: z.string(),
locale2: z.string(), language2: z.string(),
}); });
export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema); export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema);

View File

@@ -1,3 +1,6 @@
"use server";
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
/** /**
* 支持的语音合成模型 * 支持的语音合成模型
@@ -135,13 +138,12 @@ class QwenTTSService {
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
}); });
const data: TTSResponse = await response.json();
// 4. 错误处理 // 4. 错误处理
if (data.status_code !== 200) { if (response.status !== 200) {
throw new Error(`API错误: [${data.code}] ${data.message}`); throw new Error(`TTS API错误: [${response.status}] ${response.statusText}}`);
} }
const data: TTSResponse = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -149,74 +151,6 @@ class QwenTTSService {
throw error; throw error;
} }
} }
/**
* 流式合成语音边生成边输出Base64音频数据
*/
async synthesizeStream(
text: string,
options: {
voice?: string;
language?: SupportedLanguage;
model?: TTSModel;
onAudioChunk?: (chunk: string) => void; // 接收音频片段的回调
} = {}
): Promise<void> {
const {
voice = 'Cherry',
language = 'Auto',
model = 'qwen3-tts-flash',
onAudioChunk
} = options;
this.validateTextLength(text, model);
const requestBody: TTSRequest = {
model,
input: {
text,
voice,
language_type: language
},
parameters: {
stream: true // 启用流式输出
}
};
try {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'X-DashScope-SSE': 'enable' // 关键:启用服务器发送事件
},
body: JSON.stringify(requestBody),
});
if (!response.ok || !response.body) {
throw new Error(`流式请求失败: ${response.status}`);
}
// 处理流式响应此处为简化示例实际需解析SSE格式
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
if (onAudioChunk && chunk.trim()) {
onAudioChunk(chunk); // 处理音频数据片段
}
}
} catch (error) {
console.error('流式合成失败:', error);
throw error;
}
}
} }
export type TTS_SUPPORTED_LANGUAGES = 'Auto' | 'Chinese' | 'English' | 'German' | 'Italian' | 'Portuguese' | 'Spanish' | 'Japanese' | 'Korean' | 'French' | 'Russian'; export type TTS_SUPPORTED_LANGUAGES = 'Auto' | 'Chinese' | 'English' | 'German' | 'Italian' | 'Portuguese' | 'Spanish' | 'Japanese' | 'Korean' | 'French' | 'Russian';
@@ -231,7 +165,7 @@ export async function getTTSUrl(text: string, lang: TTS_SUPPORTED_LANGUAGES) {
throw "API Key设置错误"; throw "API Key设置错误";
} }
const ttsService = new QwenTTSService( const ttsService = new QwenTTSService(
process.env.DASHSCOPE_API_KEY || 'sk-xxx', process.env.DASHSCORE_API_KEY
); );
const result = await ttsService.synthesize( const result = await ttsService.synthesize(
text, text,