@xiping/node-utils 1.0.70 → 1.0.77

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.
package/README.md CHANGED
@@ -6,6 +6,21 @@ Node.js 通用工具库,提供目录树、路径、SRT→VTT 字幕转换、FF
6
6
  npm install @xiping/node-utils
7
7
  ```
8
8
 
9
+ ## 命令行
10
+
11
+ 包内提供可执行命令(需本机已安装 Node;部分命令依赖 FFmpeg,见各模块说明):
12
+
13
+ | 命令 | 说明 |
14
+ |------|------|
15
+ | `video-thumbnail` | 为视频生成多帧合成缩略图(见 [src/ffmpeg/README.md](./src/ffmpeg/README.md) 中「缩略图生成」与「命令行」) |
16
+ | `translate-srt` | 字幕翻译教学相关 CLI(见 [src/subtitle-translation-teaching/README.md](./src/subtitle-translation-teaching/README.md)) |
17
+
18
+ **示例(临时执行、未安装依赖):**
19
+
20
+ ```bash
21
+ npx -p @xiping/node-utils@latest video-thumbnail --input /path/to/video.mp4
22
+ ```
23
+
9
24
  ## 使用案例
10
25
 
11
26
  ```typescript
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../lib/src/ffmpeg/cli-thumbnail.js";
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";
@@ -140,6 +140,27 @@ console.log(result.outputPath);
140
140
  console.log(result.metadata);
141
141
  ```
142
142
 
143
+ ### 命令行(`video-thumbnail`)
144
+
145
+ 安装 `@xiping/node-utils` 后可用 `video-thumbnail`,与上方 API 选项对应(见 `--help`)。
146
+
147
+ **未在项目中安装依赖、临时在一台机器上执行:**
148
+
149
+ ```bash
150
+ npx -p @xiping/node-utils@latest video-thumbnail --input /path/to/video.mp4
151
+ ```
152
+
153
+ (包名是 `@xiping/node-utils`,命令名是 `video-thumbnail`,临时拉包时需用 `-p` 指定包名。)
154
+
155
+ **已全局安装**(之后可省略 `npx` 与 `-p`):
156
+
157
+ ```bash
158
+ npm i -g @xiping/node-utils
159
+ video-thumbnail --input /path/to/video.mp4
160
+ ```
161
+
162
+ **当前项目已依赖本包** 时,可直接 `npx video-thumbnail --input ...`,无需 `-p`。
163
+
143
164
  ### 进度回调
144
165
 
145
166
  ```typescript
@@ -0,0 +1 @@
1
+ export declare function runCli(args: string[]): Promise<void>;
@@ -0,0 +1,130 @@
1
+ import path from "node:path";
2
+ import { getThumbnail } from "./getThumbnail.js";
3
+ function printHelp() {
4
+ console.log(`Usage: video-thumbnail --input <video> [options]
5
+
6
+ Options:
7
+ -i, --input <path> Video file path (required)
8
+ --frames <n> Frame count for composition (default: 60)
9
+ --output-width <n> Output width in pixels (default: 3840)
10
+ --columns <n> Grid columns (default: 4)
11
+ --output-name <name> Output filename, e.g. thumbnail.avif (default: thumbnail.avif)
12
+ --quality <1-100> Output quality (default: 80)
13
+ --format <fmt> avif | webp | jpeg | png (default: avif)
14
+ --batch-size <n> Batch size (default: 10)
15
+ --max-concurrency <n> Max concurrency (default: 4)
16
+ --temp-dir <path> Custom temp directory
17
+ -h, --help Show this help
18
+ `);
19
+ }
20
+ function parseArgs(args) {
21
+ const options = { input: "" };
22
+ for (let i = 0; i < args.length; i++) {
23
+ const arg = args[i];
24
+ if (arg === "--input" || arg === "-i") {
25
+ options.input = args[++i] ?? "";
26
+ }
27
+ else if (arg === "--frames") {
28
+ options.frames = parseInt(args[++i] ?? "", 10);
29
+ }
30
+ else if (arg === "--output-width") {
31
+ options.outputWidth = parseInt(args[++i] ?? "", 10);
32
+ }
33
+ else if (arg === "--columns") {
34
+ options.columns = parseInt(args[++i] ?? "", 10);
35
+ }
36
+ else if (arg === "--output-name") {
37
+ options.outputFileName = args[++i] ?? "";
38
+ }
39
+ else if (arg === "--quality") {
40
+ options.quality = parseInt(args[++i] ?? "", 10);
41
+ }
42
+ else if (arg === "--format") {
43
+ const f = args[++i] ?? "";
44
+ if (f === "avif" || f === "webp" || f === "jpeg" || f === "png") {
45
+ options.format = f;
46
+ }
47
+ else {
48
+ console.error(`Error: --format must be avif, webp, jpeg, or png, got: ${f}`);
49
+ process.exit(1);
50
+ }
51
+ }
52
+ else if (arg === "--batch-size") {
53
+ options.batchSize = parseInt(args[++i] ?? "", 10);
54
+ }
55
+ else if (arg === "--max-concurrency") {
56
+ options.maxConcurrency = parseInt(args[++i] ?? "", 10);
57
+ }
58
+ else if (arg === "--temp-dir") {
59
+ options.tempDir = args[++i] ?? "";
60
+ }
61
+ else if (arg === "--help" || arg === "-h") {
62
+ options.help = true;
63
+ }
64
+ }
65
+ return options;
66
+ }
67
+ function buildThumbnailOptions(opts) {
68
+ const out = {
69
+ onProgress: (p) => {
70
+ const line = `[${p.phase}] ${p.percent}%${p.message ? ` ${p.message}` : ""}`;
71
+ console.error(line);
72
+ },
73
+ };
74
+ if (opts.frames !== undefined && !Number.isNaN(opts.frames)) {
75
+ out.frames = opts.frames;
76
+ }
77
+ if (opts.outputWidth !== undefined && !Number.isNaN(opts.outputWidth)) {
78
+ out.outputWidth = opts.outputWidth;
79
+ }
80
+ if (opts.columns !== undefined && !Number.isNaN(opts.columns)) {
81
+ out.columns = opts.columns;
82
+ }
83
+ if (opts.outputFileName) {
84
+ out.outputFileName = opts.outputFileName;
85
+ }
86
+ if (opts.quality !== undefined && !Number.isNaN(opts.quality)) {
87
+ out.quality = opts.quality;
88
+ }
89
+ if (opts.format) {
90
+ out.format = opts.format;
91
+ }
92
+ if (opts.batchSize !== undefined && !Number.isNaN(opts.batchSize)) {
93
+ out.batchSize = opts.batchSize;
94
+ }
95
+ if (opts.maxConcurrency !== undefined && !Number.isNaN(opts.maxConcurrency)) {
96
+ out.maxConcurrency = opts.maxConcurrency;
97
+ }
98
+ if (opts.tempDir) {
99
+ out.tempDir = path.resolve(opts.tempDir);
100
+ }
101
+ return out;
102
+ }
103
+ export async function runCli(args) {
104
+ const opts = parseArgs(args);
105
+ if (opts.help) {
106
+ printHelp();
107
+ return;
108
+ }
109
+ if (!opts.input) {
110
+ console.error("Error: --input is required");
111
+ printHelp();
112
+ process.exit(1);
113
+ }
114
+ const videoPath = path.resolve(process.cwd(), opts.input);
115
+ const thumbnailOpts = buildThumbnailOptions(opts);
116
+ try {
117
+ const result = await getThumbnail(videoPath, thumbnailOpts);
118
+ console.log(result.outputPath);
119
+ }
120
+ catch (e) {
121
+ const msg = e instanceof Error ? e.message : String(e);
122
+ console.error(`Error: ${msg}`);
123
+ process.exit(1);
124
+ }
125
+ }
126
+ const argv = process.argv.slice(2);
127
+ runCli(argv).catch((e) => {
128
+ console.error(e instanceof Error ? e.message : String(e));
129
+ process.exit(1);
130
+ });
@@ -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,18 +1,23 @@
1
1
  {
2
2
  "name": "@xiping/node-utils",
3
- "version": "1.0.70",
3
+ "version": "1.0.77",
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
+ "video-thumbnail": "./bin/video-thumbnail"
13
+ },
10
14
  "directories": {
11
15
  "lib": "lib",
12
16
  "test": "__tests__"
13
17
  },
14
18
  "files": [
15
- "lib"
19
+ "lib",
20
+ "bin"
16
21
  ],
17
22
  "repository": {
18
23
  "type": "git",
@@ -25,7 +30,7 @@
25
30
  "bugs": {
26
31
  "url": "https://github.com/The-End-Hero/xiping/issues"
27
32
  },
28
- "gitHead": "61f56a865103626696a6b73c0527c2cb5fc859c1",
33
+ "gitHead": "53cce3c7a028d237fa19f768d9a392bea054d49a",
29
34
  "publishConfig": {
30
35
  "access": "public",
31
36
  "registry": "https://registry.npmjs.org/"
@@ -33,6 +38,7 @@
33
38
  "dependencies": {
34
39
  "@xiping/subtitle": "1.0.52",
35
40
  "chalk": "^5.6.2",
41
+ "openai": "^6.33.0",
36
42
  "sharp": "^0.34.5",
37
43
  "srt-parser-2": "^1.2.3"
38
44
  },