From 76749549ff851030b690c095d6e02ee6386caa8d Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Mon, 2 Feb 2026 23:32:39 +0800 Subject: [PATCH] ... --- src/app/(features)/translator/page.tsx | 22 +- src/lib/bigmodel/translator/index.ts | 2 + src/lib/bigmodel/translator/orchestrator.ts | 189 ++++++++++++++++++ src/lib/bigmodel/translator/types.ts | 13 ++ src/lib/browser/localStorageOperators.ts | 44 ++++ src/lib/interfaces.ts | 19 ++ src/lib/logger.ts | 25 +++ src/lib/theme/colors.ts | 6 + src/modules/translator/index.ts | 2 +- .../translator/translator-action-dto.ts | 59 +++--- src/modules/translator/translator-action.ts | 102 ++++++++++ .../translator/translator-repository-dto.ts | 23 +++ .../translator/translator-repository.ts | 62 +++--- .../translator/translator-service-dto.ts | 11 + src/modules/translator/translator-service.ts | 69 +++++++ src/shared/constant.ts | 5 +- src/shared/index.ts | 3 +- src/shared/translator-type.ts | 8 + 18 files changed, 590 insertions(+), 74 deletions(-) create mode 100644 src/lib/bigmodel/translator/index.ts create mode 100644 src/lib/bigmodel/translator/orchestrator.ts create mode 100644 src/lib/bigmodel/translator/types.ts create mode 100644 src/lib/browser/localStorageOperators.ts create mode 100644 src/lib/interfaces.ts create mode 100644 src/lib/logger.ts create mode 100644 src/lib/theme/colors.ts create mode 100644 src/shared/translator-type.ts diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx index 3dd1047..0f90065 100644 --- a/src/app/(features)/translator/page.tsx +++ b/src/app/(features)/translator/page.tsx @@ -6,17 +6,17 @@ import IMAGES from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; -import { translateText } from "@/modules/translator"; +import { actionTranslateText } from "@/modules/translator"; import { toast } from "sonner"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; -import { TranslateTextOutput } from "@/modules/translator"; +import { TSharedTranslationResult } from "@/shared"; export default function TranslatorPage() { const t = useTranslations("translator"); const taref = useRef(null); const [targetLanguage, setTargetLanguage] = useState("Chinese"); - const [translationResult, setTranslationResult] = useState(null); + const [translationResult, setTranslationResult] = useState(null); const [needIpa, setNeedIpa] = useState(true); const [processing, setProcessing] = useState(false); const [lastTranslation, setLastTranslation] = useState<{ @@ -70,18 +70,22 @@ export default function TranslatorPage() { lastTranslation?.targetLanguage === targetLanguage; try { - const result = await translateText({ + const result = await actionTranslateText({ sourceText, targetLanguage, forceRetranslate, needIpa, }); - setTranslationResult(result); - setLastTranslation({ - sourceText, - targetLanguage, - }); + if (result.success && result.data) { + setTranslationResult(result.data); + setLastTranslation({ + sourceText, + targetLanguage, + }); + } else { + toast.error(result.message || "翻译失败,请重试"); + } } catch (error) { toast.error("翻译失败,请重试"); console.error("翻译错误:", error); diff --git a/src/lib/bigmodel/translator/index.ts b/src/lib/bigmodel/translator/index.ts new file mode 100644 index 0000000..5599900 --- /dev/null +++ b/src/lib/bigmodel/translator/index.ts @@ -0,0 +1,2 @@ +export { executeTranslation } from "./orchestrator"; +export * from "./types"; diff --git a/src/lib/bigmodel/translator/orchestrator.ts b/src/lib/bigmodel/translator/orchestrator.ts new file mode 100644 index 0000000..1fbec5f --- /dev/null +++ b/src/lib/bigmodel/translator/orchestrator.ts @@ -0,0 +1,189 @@ +import { getAnswer } from "../zhipu"; +import { parseAIGeneratedJSON } from "@/utils/json"; +import { LanguageDetectionResult, TranslationLLMResponse } from "./types"; + +async function detectLanguage(text: string): Promise { + const prompt = ` +你是一个语言识别专家。分析用户输入并返回 JSON 结果。 + +用户输入位于 标签内: +${text} + +你的任务是: +1. 识别输入文本的语言 +2. 评估识别置信度 + +返回 JSON 格式: +{ + "sourceLanguage": "检测到的语言名称(如 English、中文、日本語、Français、Deutsch等)", + "confidence": "high/medium/low" +} + +只返回 JSON,不要任何其他文字。 +`.trim(); + + try { + const result = await getAnswer([ + { + role: "system", + content: "你是一个语言识别专家,只返回 JSON 格式的分析结果。", + }, + { + role: "user", + content: prompt, + }, + ]).then(parseAIGeneratedJSON); + + if (typeof result.sourceLanguage !== "string" || !result.sourceLanguage) { + throw new Error("Invalid source language detected"); + } + + return result; + } catch (error) { + console.error("Language detection failed:", error); + throw new Error("Failed to detect source language"); + } +} + +async function performTranslation( + sourceText: string, + sourceLanguage: string, + targetLanguage: string +): Promise { + const prompt = ` +你是一个专业翻译。将文本翻译成目标语言。 + +源文本位于 标签内: +${sourceText} + +源语言:${sourceLanguage} +目标语言:${targetLanguage} + +要求: +1. 保持原意准确 +2. 符合目标语言的表达习惯 +3. 如果是成语、俗语或文化特定表达,在目标语言中寻找对应表达 +4. 只返回翻译结果,不要任何解释或说明 + +请直接返回翻译结果: +`.trim(); + + try { + const result = await getAnswer([ + { + role: "system", + content: "你是一个专业翻译,只返回翻译结果。", + }, + { + role: "user", + content: prompt, + }, + ]); + + return result.trim(); + } catch (error) { + console.error("Translation failed:", error); + throw new Error("Translation failed"); + } +} + +async function generateIPA( + text: string, + language: string +): Promise { + const prompt = ` +你是一个语音学专家。为文本生成国际音标(IPA)标注。 + +文本位于 标签内: +${text} + +语言:${language} + +要求: +1. 生成准确的国际音标(IPA)标注 +2. 使用标准的 IPA 符号 +3. 只返回 IPA 标注,不要任何其他文字 + +请直接返回 IPA 标注: +`.trim(); + + try { + const result = await getAnswer([ + { + role: "system", + content: "你是一个语音学专家,只返回 IPA 标注。", + }, + { + role: "user", + content: prompt, + }, + ]); + + return result.trim(); + } catch (error) { + console.error("IPA generation failed:", error); + return ""; + } +} + +export async function executeTranslation( + sourceText: string, + targetLanguage: string, + needIpa: boolean +): Promise { + try { + console.log("[翻译] 开始翻译流程..."); + console.log("[翻译] 源文本:", sourceText); + console.log("[翻译] 目标语言:", targetLanguage); + console.log("[翻译] 需要 IPA:", needIpa); + + // Stage 1: Detect source language + console.log("[阶段1] 检测源语言..."); + const detectionResult = await detectLanguage(sourceText); + console.log("[阶段1] 检测结果:", detectionResult); + + // Stage 2: Perform translation + console.log("[阶段2] 执行翻译..."); + const translatedText = await performTranslation( + sourceText, + detectionResult.sourceLanguage, + targetLanguage + ); + console.log("[阶段2] 翻译完成:", translatedText); + + // Validate translation result + if (!translatedText) { + throw new Error("Translation result is empty"); + } + + // Stage 3 (Optional): Generate IPA + let sourceIpa: string | undefined; + let targetIpa: string | undefined; + + if (needIpa) { + console.log("[阶段3] 生成 IPA..."); + sourceIpa = await generateIPA(sourceText, detectionResult.sourceLanguage); + console.log("[阶段3] 源文本 IPA:", sourceIpa); + + targetIpa = await generateIPA(translatedText, targetLanguage); + console.log("[阶段3] 目标文本 IPA:", targetIpa); + } + + // Assemble final result + const finalResult: TranslationLLMResponse = { + sourceText, + translatedText, + sourceLanguage: detectionResult.sourceLanguage, + targetLanguage, + sourceIpa, + targetIpa, + }; + + console.log("[完成] 翻译流程成功"); + return finalResult; + } catch (error) { + console.error("[错误] 翻译失败:", error); + const errorMessage = error instanceof Error ? error.message : "未知错误"; + throw new Error(errorMessage); + } +} diff --git a/src/lib/bigmodel/translator/types.ts b/src/lib/bigmodel/translator/types.ts new file mode 100644 index 0000000..4d5d286 --- /dev/null +++ b/src/lib/bigmodel/translator/types.ts @@ -0,0 +1,13 @@ +export interface LanguageDetectionResult { + sourceLanguage: string; + confidence: "high" | "medium" | "low"; +} + +export interface TranslationLLMResponse { + sourceText: string; + translatedText: string; + sourceLanguage: string; + targetLanguage: string; + sourceIpa?: string; + targetIpa?: string; +} diff --git a/src/lib/browser/localStorageOperators.ts b/src/lib/browser/localStorageOperators.ts new file mode 100644 index 0000000..6139cd5 --- /dev/null +++ b/src/lib/browser/localStorageOperators.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +interface LocalStorageOperator { + get: () => T; + set: (value: T) => void; +} + +export function getLocalStorageOperator( + key: string, + schema: T +): LocalStorageOperator> { + const get = (): z.infer => { + if (typeof window === "undefined") { + return [] as unknown as z.infer; + } + + try { + const item = localStorage.getItem(key); + if (item === null) { + return [] as unknown as z.infer; + } + + const parsed = JSON.parse(item); + return schema.parse(parsed); + } catch (error) { + console.error(`Error reading from localStorage key "${key}":`, error); + return [] as unknown as z.infer; + } + }; + + const set = (value: z.infer): void => { + if (typeof window === "undefined") { + return; + } + + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error(`Error writing to localStorage key "${key}":`, error); + } + }; + + return { get, set }; +} diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts new file mode 100644 index 0000000..7b29528 --- /dev/null +++ b/src/lib/interfaces.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +// Text Speaker types +export const TextSpeakerItemSchema = z.object({ + text: z.string(), + language: z.string(), + ipa: z.string().optional(), +}); + +export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema); + +// Alphabet types +export type SupportedAlphabets = "japanese" | "english" | "uyghur" | "esperanto"; + +export interface Letter { + letter: string; + letter_sound_ipa: string; + roman_letter?: string; +} diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..982f794 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,25 @@ +class Logger { + error(message: string, error?: unknown): void { + if (error instanceof Error) { + console.error(`[ERROR] ${message}:`, error.message, error.stack); + } else { + console.error(`[ERROR] ${message}:`, error); + } + } + + warn(message: string, ...args: unknown[]): void { + console.warn(`[WARN] ${message}`, ...args); + } + + info(message: string, ...args: unknown[]): void { + console.info(`[INFO] ${message}`, ...args); + } + + debug(message: string, ...args: unknown[]): void { + if (process.env.NODE_ENV === "development") { + console.debug(`[DEBUG] ${message}`, ...args); + } + } +} + +export const logger = new Logger(); diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts new file mode 100644 index 0000000..5dd45cc --- /dev/null +++ b/src/lib/theme/colors.ts @@ -0,0 +1,6 @@ +export const COLORS = { + primary: "#35786f", + secondary: "#2d5f58", + ghost: "#f3f4f6", + icon: "#6b7280", +} as const; diff --git a/src/modules/translator/index.ts b/src/modules/translator/index.ts index f3e4f38..dc9d4dd 100644 --- a/src/modules/translator/index.ts +++ b/src/modules/translator/index.ts @@ -1,2 +1,2 @@ export * from './translator-action'; -export * from './translator-action-dto'; \ No newline at end of file +export * from './translator-action-dto'; diff --git a/src/modules/translator/translator-action-dto.ts b/src/modules/translator/translator-action-dto.ts index 8184407..0bdfcc1 100644 --- a/src/modules/translator/translator-action-dto.ts +++ b/src/modules/translator/translator-action-dto.ts @@ -1,40 +1,27 @@ +import { TSharedTranslationResult } from "@/shared"; +import { + LENGTH_MAX_LANGUAGE, + LENGTH_MIN_LANGUAGE, + LENGTH_MAX_TRANSLATOR_TEXT, + LENGTH_MIN_TRANSLATOR_TEXT, +} from "@/shared/constant"; +import { generateValidator } from "@/utils/validate"; +import z from "zod"; -export interface CreateTranslationHistoryInput { - userId?: string; - sourceText: string; - sourceLanguage: string; - targetLanguage: string; - translatedText: string; - sourceIpa?: string; - targetIpa?: string; -} +const schemaActionInputTranslateText = z.object({ + sourceText: z.string().min(LENGTH_MIN_TRANSLATOR_TEXT).max(LENGTH_MAX_TRANSLATOR_TEXT), + targetLanguage: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE), + forceRetranslate: z.boolean().optional().default(false), + needIpa: z.boolean().optional().default(true), + userId: z.string().optional(), +}); -export interface TranslationHistoryQuery { - sourceText: string; - targetLanguage: string; -} +export type ActionInputTranslateText = z.infer; -export interface TranslateTextInput { - sourceText: string; - targetLanguage: string; - forceRetranslate?: boolean; // 默认 false - needIpa?: boolean; // 默认 true - userId?: string; // 可选用户 ID -} +export const validateActionInputTranslateText = generateValidator(schemaActionInputTranslateText); -export interface TranslateTextOutput { - sourceText: string; - translatedText: string; - sourceLanguage: string; - targetLanguage: string; - sourceIpa: string; // 如果 needIpa=false,返回空字符串 - targetIpa: string; // 如果 needIpa=false,返回空字符串 -} - -export interface TranslationLLMResponse { - translatedText: string; - sourceLanguage: string; - targetLanguage: string; - sourceIpa?: string; // 可选,根据 needIpa 决定 - targetIpa?: string; // 可选,根据 needIpa 决定 -} +export type ActionOutputTranslateText = { + message: string; + success: boolean; + data?: TSharedTranslationResult; +}; diff --git a/src/modules/translator/translator-action.ts b/src/modules/translator/translator-action.ts index 908fe79..7989c2b 100644 --- a/src/modules/translator/translator-action.ts +++ b/src/modules/translator/translator-action.ts @@ -1 +1,103 @@ "use server"; + +import { + ActionInputTranslateText, + ActionOutputTranslateText, + validateActionInputTranslateText, +} from "./translator-action-dto"; +import { ValidateError } from "@/lib/errors"; +import { serviceTranslateText } from "./translator-service"; +import { getAnswer } from "@/lib/bigmodel/zhipu"; + +export const actionTranslateText = async ( + dto: ActionInputTranslateText +): Promise => { + try { + return { + message: "success", + success: true, + data: await serviceTranslateText(validateActionInputTranslateText(dto)), + }; + } catch (e) { + if (e instanceof ValidateError) { + return { + success: false, + message: e.message, + }; + } + console.log(e); + return { + success: false, + message: "Unknown error occurred.", + }; + } +}; + +/** + * @deprecated 保留此函数以支持旧代码(text-speaker 功能) + */ +export const genIPA = async (text: string) => { + return ( + "[" + + ( + await getAnswer( + ` +${text} + +请生成以上文本的严式国际音标 +然后直接发给我 +不要附带任何说明 +不要擅自增减符号 +不许用"/"或者"[]"包裹 +`.trim(), + ) + ) + .replaceAll("[", "") + .replaceAll("]", "") + + "]" + ); +}; + +/** + * @deprecated 保留此函数以支持旧代码(text-speaker 功能) + */ +export const genLanguage = async (text: string) => { + const language = await getAnswer([ + { + role: "system", + content: ` +你是一个语言检测工具。请识别文本的语言并返回语言名称。 + +返回语言的标准英文名称,例如: +- 中文: Chinese +- 英语: English +- 日语: Japanese +- 韩语: Korean +- 法语: French +- 德语: German +- 意大利语: Italian +- 葡萄牙语: Portuguese +- 西班牙语: Spanish +- 俄语: Russian +- 阿拉伯语: Arabic +- 印地语: Hindi +- 泰语: Thai +- 越南语: Vietnamese +- 等等... + +如果无法识别语言,返回 "Unknown" + +规则: +1. 只返回语言的标准英文名称 +2. 首字母大写,其余小写 +3. 不要附带任何说明 +4. 不要擅自增减符号 + `.trim() + }, + { + role: "user", + content: `${text}` + } + ]); + return language.trim(); +}; diff --git a/src/modules/translator/translator-repository-dto.ts b/src/modules/translator/translator-repository-dto.ts index e69de29..44b5f79 100644 --- a/src/modules/translator/translator-repository-dto.ts +++ b/src/modules/translator/translator-repository-dto.ts @@ -0,0 +1,23 @@ +export type RepoInputSelectLatestTranslation = { + sourceText: string; + targetLanguage: string; +}; + +export type RepoOutputSelectLatestTranslation = { + id: number; + translatedText: string; + sourceLanguage: string; + targetLanguage: string; + sourceIpa: string | null; + targetIpa: string | null; +} | null; + +export type RepoInputCreateTranslationHistory = { + userId?: string; + sourceText: string; + sourceLanguage: string; + targetLanguage: string; + translatedText: string; + sourceIpa?: string; + targetIpa?: string; +}; diff --git a/src/modules/translator/translator-repository.ts b/src/modules/translator/translator-repository.ts index 5d12bd8..a1a251a 100644 --- a/src/modules/translator/translator-repository.ts +++ b/src/modules/translator/translator-repository.ts @@ -1,31 +1,41 @@ -"use server"; - -import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./translator-action-dto"; +import { + RepoInputCreateTranslationHistory, + RepoInputSelectLatestTranslation, + RepoOutputSelectLatestTranslation, +} from "./translator-repository-dto"; import prisma from "@/lib/db"; -/** - * 创建翻译历史记录 - */ -export async function repoCreateTranslationHistory(data: CreateTranslationHistoryInput) { - return prisma.translationHistory.create({ - data: data, - }); +export async function repoSelectLatestTranslation( + dto: RepoInputSelectLatestTranslation +): Promise { + const result = await prisma.translationHistory.findFirst({ + where: { + sourceText: dto.sourceText, + targetLanguage: dto.targetLanguage, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (!result) { + return null; + } + + return { + id: result.id, + translatedText: result.translatedText, + sourceLanguage: result.sourceLanguage, + targetLanguage: result.targetLanguage, + sourceIpa: result.sourceIpa, + targetIpa: result.targetIpa, + }; } -/** - * 查询最新的翻译记录 - * @param sourceText 源文本 - * @param targetLanguage 目标语言 - * @returns 最新的翻译记录,如果不存在则返回 null - */ -export async function repoSelectLatestTranslation(query: TranslationHistoryQuery) { - return prisma.translationHistory.findFirst({ - where: { - sourceText: query.sourceText, - targetLanguage: query.targetLanguage, - }, - orderBy: { - createdAt: 'desc', - }, - }); +export async function repoCreateTranslationHistory( + data: RepoInputCreateTranslationHistory +) { + return await prisma.translationHistory.create({ + data: data, + }); } diff --git a/src/modules/translator/translator-service-dto.ts b/src/modules/translator/translator-service-dto.ts index e69de29..2cf29e3 100644 --- a/src/modules/translator/translator-service-dto.ts +++ b/src/modules/translator/translator-service-dto.ts @@ -0,0 +1,11 @@ +import { TSharedTranslationResult } from "@/shared"; + +export type ServiceInputTranslateText = { + sourceText: string; + targetLanguage: string; + forceRetranslate: boolean; + needIpa: boolean; + userId?: string; +}; + +export type ServiceOutputTranslateText = TSharedTranslationResult; diff --git a/src/modules/translator/translator-service.ts b/src/modules/translator/translator-service.ts index e69de29..e603903 100644 --- a/src/modules/translator/translator-service.ts +++ b/src/modules/translator/translator-service.ts @@ -0,0 +1,69 @@ +import { executeTranslation } from "@/lib/bigmodel/translator"; +import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository"; +import { ServiceInputTranslateText, ServiceOutputTranslateText } from "./translator-service-dto"; + +export const serviceTranslateText = async ( + dto: ServiceInputTranslateText +): Promise => { + const { sourceText, targetLanguage, forceRetranslate, needIpa, userId } = dto; + + // Check for existing translation + const lastTranslation = await repoSelectLatestTranslation({ + sourceText, + targetLanguage, + }); + + if (forceRetranslate || !lastTranslation) { + // Call AI for translation + const response = await executeTranslation( + sourceText, + targetLanguage, + needIpa + ); + + // Save translation history asynchronously (don't block response) + repoCreateTranslationHistory({ + userId, + sourceText, + sourceLanguage: response.sourceLanguage, + targetLanguage: response.targetLanguage, + translatedText: response.translatedText, + sourceIpa: needIpa ? response.sourceIpa : undefined, + targetIpa: needIpa ? response.targetIpa : undefined, + }).catch((error) => { + console.error("Failed to save translation data:", error); + }); + + return { + sourceText: response.sourceText, + translatedText: response.translatedText, + sourceLanguage: response.sourceLanguage, + targetLanguage: response.targetLanguage, + sourceIpa: response.sourceIpa || "", + targetIpa: response.targetIpa || "", + }; + } else { + // Return cached translation + // Still save a history record for analytics + repoCreateTranslationHistory({ + userId, + sourceText, + sourceLanguage: lastTranslation.sourceLanguage, + targetLanguage: lastTranslation.targetLanguage, + translatedText: lastTranslation.translatedText, + sourceIpa: lastTranslation.sourceIpa || undefined, + targetIpa: lastTranslation.targetIpa || undefined, + }).catch((error) => { + console.error("Failed to save translation data:", error); + }); + + return { + sourceText, + translatedText: lastTranslation.translatedText, + sourceLanguage: lastTranslation.sourceLanguage, + targetLanguage: lastTranslation.targetLanguage, + sourceIpa: lastTranslation.sourceIpa || "", + targetIpa: lastTranslation.targetIpa || "", + }; + } +}; diff --git a/src/shared/constant.ts b/src/shared/constant.ts index a081c2d..90d0fc2 100644 --- a/src/shared/constant.ts +++ b/src/shared/constant.ts @@ -11,4 +11,7 @@ export const LENGTH_MAX_IPA = 150; export const LENGTH_MIN_IPA = 1; export const LENGTH_MAX_FOLDER_NAME = 20; -export const LENGTH_MIN_FOLDER_NAME = 1; \ No newline at end of file +export const LENGTH_MIN_FOLDER_NAME = 1; + +export const LENGTH_MAX_TRANSLATOR_TEXT = 1000; +export const LENGTH_MIN_TRANSLATOR_TEXT = 1; \ No newline at end of file diff --git a/src/shared/index.ts b/src/shared/index.ts index 7c3ea2a..26b3e49 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1 +1,2 @@ -export * from './dictionary-type'; \ No newline at end of file +export * from './dictionary-type'; +export * from './translator-type'; \ No newline at end of file diff --git a/src/shared/translator-type.ts b/src/shared/translator-type.ts new file mode 100644 index 0000000..cd57e86 --- /dev/null +++ b/src/shared/translator-type.ts @@ -0,0 +1,8 @@ +export type TSharedTranslationResult = { + sourceText: string; + translatedText: string; + sourceLanguage: string; + targetLanguage: string; + sourceIpa: string; + targetIpa: string; +};