@xiping/node-utils 1.0.70 → 1.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const rootDir = join(__dirname, '..');
9
+
10
+ spawn('npx', ['tsx', join(rootDir, 'src/subtitle-translation-teaching/src/cli.ts'), ...process.argv.slice(2)], {
11
+ stdio: 'inherit',
12
+ shell: true,
13
+ });
package/lib/index.d.ts CHANGED
@@ -4,3 +4,6 @@ export * from "./src/srt-to-vtt/index.js";
4
4
  export * from "./src/ffmpeg/index.js";
5
5
  export * from "./src/file/getFileConfig.js";
6
6
  export * from "./src/image/index.js";
7
+ export { translateSubtitle } from "./src/subtitle-translation-teaching/src/translator/translate.js";
8
+ export type { TranslateOptions } from "./src/subtitle-translation-teaching/src/translator/translate.js";
9
+ export type { TeachingResult, GrammarAnalysis, VocabularyItem, PhraseItem, WordFrequencyItem, RelatedPattern, UsageExample, } from "./src/subtitle-translation-teaching/src/types.js";
package/lib/index.js CHANGED
@@ -10,3 +10,5 @@ export * from "./src/ffmpeg/index.js";
10
10
  export * from "./src/file/getFileConfig.js";
11
11
  // 图片处理相关功能
12
12
  export * from "./src/image/index.js";
13
+ // 字幕翻译教学功能
14
+ export { translateSubtitle } from "./src/subtitle-translation-teaching/src/translator/translate.js";
@@ -0,0 +1,16 @@
1
+ export interface SubtitleBlock {
2
+ index: number;
3
+ startTime: string;
4
+ endTime: string;
5
+ text: string;
6
+ }
7
+ export interface ParsedSubtitle {
8
+ index: number;
9
+ startTime: string;
10
+ endTime: string;
11
+ text: string;
12
+ /** 原始文本行(保留换行) */
13
+ rawText: string;
14
+ }
15
+ export declare function parseSRT(content: string): ParsedSubtitle[];
16
+ export declare function formatSRTTime(seconds: number): string;
@@ -0,0 +1,27 @@
1
+ export function parseSRT(content) {
2
+ const blocks = [];
3
+ // 分割字幕块:数字 + 时间 + 文本
4
+ const regex = /(\d+)\s*\n(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*\n([\s\S]*?)(?=\n\n\d+\s*\n|$)/g;
5
+ let match;
6
+ while ((match = regex.exec(content + '\n\n')) !== null) {
7
+ const [, indexStr, startTime, endTime, text] = match;
8
+ blocks.push({
9
+ index: parseInt(indexStr, 10),
10
+ startTime,
11
+ endTime,
12
+ text: text.trim().replace(/\n/g, ' '),
13
+ rawText: text.trim(),
14
+ });
15
+ }
16
+ return blocks;
17
+ }
18
+ export function formatSRTTime(seconds) {
19
+ const h = Math.floor(seconds / 3600);
20
+ const m = Math.floor((seconds % 3600) / 60);
21
+ const s = Math.floor(seconds % 60);
22
+ const ms = Math.floor((seconds % 1) * 1000);
23
+ return `${pad(h)}:${pad(m)}:${pad(s)},${pad(ms, 3)}`;
24
+ }
25
+ function pad(n, len = 2) {
26
+ return n.toString().padStart(len, '0');
27
+ }
@@ -0,0 +1,18 @@
1
+ import type { VocabularyItem, PhraseItem, WordFrequencyItem, RelatedPattern, UsageExample } from '../types.js';
2
+ export interface LLMAdapter {
3
+ analyze(text: string): Promise<{
4
+ translation: string;
5
+ difficulty: string;
6
+ tone: string;
7
+ phonetics: string;
8
+ grammar: string;
9
+ vocabulary: VocabularyItem[];
10
+ phrases: PhraseItem[];
11
+ culture: string;
12
+ wordFrequency: WordFrequencyItem[];
13
+ relatedPatterns: RelatedPattern[];
14
+ usageExamples: UsageExample[];
15
+ mistakeWarnings: string[];
16
+ }>;
17
+ }
18
+ export declare function createMiMoAdapter(apiKey: string): LLMAdapter;
@@ -0,0 +1,103 @@
1
+ import OpenAI from 'openai';
2
+ export function createMiMoAdapter(apiKey) {
3
+ const client = new OpenAI({
4
+ apiKey,
5
+ baseURL: 'https://api.xiaomimimo.com/v1',
6
+ dangerouslyAllowBrowser: false,
7
+ });
8
+ return new MiMoAdapter(client);
9
+ }
10
+ class MiMoAdapter {
11
+ constructor(client) {
12
+ this.client = client;
13
+ this.model = 'mimo-v2-pro';
14
+ }
15
+ async analyze(text) {
16
+ const prompt = `你是一位专业的英语教师。请分析下面的英文句子,给出详细的教学分解。
17
+
18
+ 英文句子:${text}
19
+
20
+ 请以JSON格式输出,包含以下所有字段:
21
+ - translation: 中文翻译
22
+ - difficulty: CEFR难度等级 (A1/A2/B1/B2/C1/C2)
23
+ - tone: 语气情感 (friendly/neutral/formal/informal/aggressive 等)
24
+ - phonetics: 句子音标 (IPA格式,如 /həˈloʊ/)
25
+ - grammar: 语法分析(句子结构、时态、语态、从句等)
26
+ - vocabulary: 重点词汇列表,每项包含 word(单词)、phonetic(音标,可选)、partOfSpeech(词性)和 meaning(中文释义)
27
+ - phrases: 短语/习语列表,每项包含 phrase(短语)、meaning(中文释义)和 example(例句,可选)
28
+ - culture: 文化背景注释(如果没有可以为空字符串)
29
+ - wordFrequency: 词频信息,每项包含 word(单词)、level(CEFR等级)和 frequency(出现频率描述)
30
+ - relatedPatterns: 相关句型变换,每项包含 pattern(英文句型)和 translation(中文翻译)
31
+ - usageExamples: 用法例句,每项包含 sentence(英文例句)和 translation(中文翻译)
32
+ - mistakeWarnings: 中国人易错点提示(字符串数组,如果没有可以为空数组)
33
+
34
+ 只输出JSON,不要有其他文字。`;
35
+ const response = await this.client.chat.completions.create({
36
+ model: this.model,
37
+ messages: [
38
+ {
39
+ role: 'system',
40
+ content: 'You are a professional English teacher. Output ONLY a JSON object with these exact fields: translation, difficulty, tone, phonetics, grammar, vocabulary (array of {word, phonetic, partOfSpeech, meaning}), phrases (array of {phrase, meaning, example}), culture, wordFrequency (array of {word, level, frequency}), relatedPatterns (array of {pattern, translation}), usageExamples (array of {sentence, translation}), mistakeWarnings (array of strings). No other text.',
41
+ },
42
+ { role: 'user', content: prompt },
43
+ ],
44
+ max_completion_tokens: 4096,
45
+ temperature: 0.5,
46
+ });
47
+ const content = response.choices[0]?.message?.content?.trim() ?? '{}';
48
+ try {
49
+ const jsonStr = content.replace(/^```json\n?|```$/g, '');
50
+ const result = JSON.parse(jsonStr);
51
+ // 确保所有字段存在并有默认值
52
+ return {
53
+ translation: result.translation ?? '',
54
+ difficulty: result.difficulty ?? 'B1',
55
+ tone: result.tone ?? 'neutral',
56
+ phonetics: result.phonetics ?? '',
57
+ grammar: result.grammar ?? '解析失败',
58
+ vocabulary: (result.vocabulary ?? []).map((v) => ({
59
+ word: v.word ?? '',
60
+ phonetic: v.phonetic ?? '',
61
+ partOfSpeech: v.partOfSpeech ?? 'unknown',
62
+ meaning: v.meaning ?? '',
63
+ })),
64
+ phrases: (result.phrases ?? []).map((p) => ({
65
+ phrase: p.phrase ?? '',
66
+ meaning: p.meaning ?? '',
67
+ example: p.example ?? '',
68
+ })),
69
+ culture: result.culture ?? '',
70
+ wordFrequency: (result.wordFrequency ?? []).map((w) => ({
71
+ word: w.word ?? '',
72
+ level: w.level ?? 'B1',
73
+ frequency: w.frequency ?? 'common',
74
+ })),
75
+ relatedPatterns: (result.relatedPatterns ?? []).map((r) => ({
76
+ pattern: r.pattern ?? '',
77
+ translation: r.translation ?? '',
78
+ })),
79
+ usageExamples: (result.usageExamples ?? []).map((u) => ({
80
+ sentence: u.sentence ?? '',
81
+ translation: u.translation ?? '',
82
+ })),
83
+ mistakeWarnings: result.mistakeWarnings ?? [],
84
+ };
85
+ }
86
+ catch {
87
+ return {
88
+ translation: '',
89
+ difficulty: 'B1',
90
+ tone: 'neutral',
91
+ phonetics: '',
92
+ grammar: '解析失败',
93
+ vocabulary: [],
94
+ phrases: [],
95
+ culture: '',
96
+ wordFrequency: [],
97
+ relatedPatterns: [],
98
+ usageExamples: [],
99
+ mistakeWarnings: [],
100
+ };
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,7 @@
1
+ import { TranslateResult } from '../types.js';
2
+ export interface TranslateOptions {
3
+ apiKey: string;
4
+ batchSize?: number;
5
+ delayMs?: number;
6
+ }
7
+ export declare function translateSubtitle(filePath: string, options: TranslateOptions): Promise<TranslateResult>;
@@ -0,0 +1,88 @@
1
+ import { parseSRT } from '../parser/srt.js';
2
+ import { createMiMoAdapter } from './adapter.js';
3
+ import { readFile } from 'fs/promises';
4
+ export async function translateSubtitle(filePath, options) {
5
+ const content = await readFile(filePath, 'utf-8');
6
+ const subtitles = parseSRT(content);
7
+ const adapter = createMiMoAdapter(options.apiKey);
8
+ const results = [];
9
+ let errorCount = 0;
10
+ const batchSize = options.batchSize ?? 5;
11
+ const delayMs = options.delayMs ?? 500;
12
+ for (let i = 0; i < subtitles.length; i += batchSize) {
13
+ const batch = subtitles.slice(i, i + batchSize);
14
+ const batchResults = await Promise.all(batch.map((sub) => processSubtitle(sub, adapter)));
15
+ for (const result of batchResults) {
16
+ if (result) {
17
+ results.push(result);
18
+ }
19
+ else {
20
+ errorCount++;
21
+ }
22
+ }
23
+ if (i + batchSize < subtitles.length) {
24
+ await sleep(delayMs);
25
+ }
26
+ }
27
+ return {
28
+ subtitles: results,
29
+ model: 'mimo-v2-pro',
30
+ totalCount: subtitles.length,
31
+ errorCount,
32
+ };
33
+ }
34
+ async function processSubtitle(sub, adapter) {
35
+ try {
36
+ const analysis = await adapter.analyze(sub.text);
37
+ return {
38
+ index: sub.index,
39
+ startTime: sub.startTime,
40
+ endTime: sub.endTime,
41
+ original: sub.text,
42
+ translation: analysis.translation,
43
+ difficulty: analysis.difficulty,
44
+ tone: analysis.tone,
45
+ phonetics: analysis.phonetics,
46
+ grammar: {
47
+ structure: analysis.grammar,
48
+ tense: detectTense(sub.text),
49
+ },
50
+ vocabulary: analysis.vocabulary,
51
+ phrases: analysis.phrases,
52
+ culture: analysis.culture || undefined,
53
+ wordFrequency: analysis.wordFrequency,
54
+ relatedPatterns: analysis.relatedPatterns,
55
+ usageExamples: analysis.usageExamples,
56
+ mistakeWarnings: analysis.mistakeWarnings,
57
+ };
58
+ }
59
+ catch (err) {
60
+ console.error(`Failed to process subtitle ${sub.index}:`, err);
61
+ return null;
62
+ }
63
+ }
64
+ function detectTense(text) {
65
+ const lower = text.toLowerCase();
66
+ if (/\bwill\b/.test(lower))
67
+ return 'Future';
68
+ if (/\bwould\b/.test(lower))
69
+ return 'Conditional';
70
+ if (/\bcan\b/.test(lower))
71
+ return 'Modal';
72
+ if (/\bhave had\b/.test(lower))
73
+ return 'Past Perfect';
74
+ if (/\bhad\b/.test(lower))
75
+ return 'Past';
76
+ if (/\bis\b.*\b-ing\b|\bare\b.*\b-ing\b|\bwas\b.*\b-ing\b/.test(lower))
77
+ return 'Present Continuous';
78
+ if (/\bhave\b.*\b-ing\b|\bhas\b.*\b-ing\b/.test(lower))
79
+ return 'Present Perfect Continuous';
80
+ if (/\bhave\b|\bhas\b/.test(lower))
81
+ return 'Present Perfect';
82
+ if (/\bam\b.*\b-ing\b/.test(lower))
83
+ return 'Present Continuous';
84
+ return 'Simple Present';
85
+ }
86
+ function sleep(ms) {
87
+ return new Promise((resolve) => setTimeout(resolve, ms));
88
+ }
@@ -0,0 +1,61 @@
1
+ export interface TeachingResult {
2
+ index: number;
3
+ startTime: string;
4
+ endTime: string;
5
+ original: string;
6
+ translation: string;
7
+ /** CEFR 难度等级: A1, A2, B1, B2, C1, C2 */
8
+ difficulty: string;
9
+ /** 语气/情感: friendly, neutral, formal, informal, aggressive */
10
+ tone: string;
11
+ /** 句子音标 */
12
+ phonetics?: string;
13
+ grammar: GrammarAnalysis;
14
+ vocabulary: VocabularyItem[];
15
+ phrases: PhraseItem[];
16
+ culture?: string;
17
+ /** 词频信息 */
18
+ wordFrequency: WordFrequencyItem[];
19
+ /** 相关句型变换 */
20
+ relatedPatterns: RelatedPattern[];
21
+ /** 例句 */
22
+ usageExamples: UsageExample[];
23
+ /** 易错点提示 */
24
+ mistakeWarnings: string[];
25
+ }
26
+ export interface GrammarAnalysis {
27
+ structure: string;
28
+ tense: string;
29
+ voice?: string;
30
+ clauses?: string[];
31
+ }
32
+ export interface VocabularyItem {
33
+ word: string;
34
+ phonetic?: string;
35
+ partOfSpeech: string;
36
+ meaning: string;
37
+ }
38
+ export interface PhraseItem {
39
+ phrase: string;
40
+ meaning: string;
41
+ example?: string;
42
+ }
43
+ export interface WordFrequencyItem {
44
+ word: string;
45
+ level: string;
46
+ frequency: string;
47
+ }
48
+ export interface RelatedPattern {
49
+ pattern: string;
50
+ translation: string;
51
+ }
52
+ export interface UsageExample {
53
+ sentence: string;
54
+ translation: string;
55
+ }
56
+ export interface TranslateResult {
57
+ subtitles: TeachingResult[];
58
+ model: string;
59
+ totalCount: number;
60
+ errorCount: number;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@xiping/node-utils",
3
- "version": "1.0.70",
3
+ "version": "1.0.76",
4
4
  "description": "node-utils",
5
5
  "type": "module",
6
6
  "author": "The-End-Hero <527409987@qq.com>",
7
7
  "homepage": "https://github.com/The-End-Hero/xiping#readme",
8
8
  "license": "MIT",
9
9
  "main": "lib/index.js",
10
+ "bin": {
11
+ "translate-srt": "./bin/translate-srt"
12
+ },
10
13
  "directories": {
11
14
  "lib": "lib",
12
15
  "test": "__tests__"
@@ -25,7 +28,7 @@
25
28
  "bugs": {
26
29
  "url": "https://github.com/The-End-Hero/xiping/issues"
27
30
  },
28
- "gitHead": "61f56a865103626696a6b73c0527c2cb5fc859c1",
31
+ "gitHead": "24c2d65c542f30ed4b09d7c9473a004f5e08dd3c",
29
32
  "publishConfig": {
30
33
  "access": "public",
31
34
  "registry": "https://registry.npmjs.org/"
@@ -33,6 +36,7 @@
33
36
  "dependencies": {
34
37
  "@xiping/subtitle": "1.0.52",
35
38
  "chalk": "^5.6.2",
39
+ "openai": "^6.33.0",
36
40
  "sharp": "^0.34.5",
37
41
  "srt-parser-2": "^1.2.3"
38
42
  },