@xiping/node-utils 1.0.21 → 1.0.36
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/lib/index.d.ts +4 -51
- package/lib/index.js +8 -85
- package/lib/src/directory-tree.d.ts +34 -0
- package/lib/src/directory-tree.js +58 -0
- package/lib/src/ffmpeg/check.d.ts +6 -0
- package/lib/src/ffmpeg/check.js +14 -0
- package/lib/src/ffmpeg/getThumbnail.d.ts +29 -0
- package/lib/src/ffmpeg/getThumbnail.js +226 -0
- package/lib/src/ffmpeg/index.d.ts +49 -0
- package/lib/src/ffmpeg/index.js +111 -0
- package/lib/src/path-utils.d.ts +17 -0
- package/lib/src/path-utils.js +25 -0
- package/lib/src/srt-to-vtt/index.d.ts +33 -0
- package/lib/src/srt-to-vtt/index.js +115 -0
- package/package.json +8 -2
package/lib/index.d.ts
CHANGED
|
@@ -1,51 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @property {string} label - Display name of the file or directory
|
|
6
|
-
* @property {Tree[]} [children] - Optional array of child nodes for directories
|
|
7
|
-
*/
|
|
8
|
-
export interface Tree {
|
|
9
|
-
id: string;
|
|
10
|
-
label: string;
|
|
11
|
-
children?: Tree[];
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Recursively builds a tree structure from a directory
|
|
15
|
-
* @param {string} dirPath - The absolute path to the directory to process
|
|
16
|
-
* @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
|
|
17
|
-
* @returns {Tree} A tree structure representing the directory hierarchy
|
|
18
|
-
* @throws Will throw an error if directory access fails
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Create a tree from a directory
|
|
22
|
-
* const tree = buildDirectoryTree('/path/to/directory');
|
|
23
|
-
*
|
|
24
|
-
* // Structure of returned tree:
|
|
25
|
-
* // {
|
|
26
|
-
* // id: '/path/to/directory',
|
|
27
|
-
* // label: 'directory',
|
|
28
|
-
* // children: [
|
|
29
|
-
* // { id: '/path/to/directory/file.txt', label: 'file.txt' },
|
|
30
|
-
* // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
|
|
31
|
-
* // ]
|
|
32
|
-
* // }
|
|
33
|
-
*/
|
|
34
|
-
export declare function buildDirectoryTree(dirPath: string, basePath?: string): Tree;
|
|
35
|
-
/**
|
|
36
|
-
* Gets the parent directory path of a given directory path
|
|
37
|
-
* Cross-platform compatible (Windows, macOS, Linux)
|
|
38
|
-
* @param {string} dirPath - The path to get the parent directory from
|
|
39
|
-
* @returns {string} The parent directory path
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* // On Unix-like systems (Mac/Linux)
|
|
43
|
-
* getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
|
|
44
|
-
*
|
|
45
|
-
* // On Windows
|
|
46
|
-
* getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
|
|
47
|
-
*
|
|
48
|
-
* // Works with both forward and backward slashes
|
|
49
|
-
* getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
|
|
50
|
-
*/
|
|
51
|
-
export declare function getParentDirectory(dirPath: string): string;
|
|
1
|
+
export * from "./src/directory-tree.js";
|
|
2
|
+
export * from "./src/path-utils.js";
|
|
3
|
+
export * from "./src/srt-to-vtt/index.js";
|
|
4
|
+
export * from "./src/ffmpeg/index.js";
|
package/lib/index.js
CHANGED
|
@@ -1,85 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* // Create a tree from a directory
|
|
12
|
-
* const tree = buildDirectoryTree('/path/to/directory');
|
|
13
|
-
*
|
|
14
|
-
* // Structure of returned tree:
|
|
15
|
-
* // {
|
|
16
|
-
* // id: '/path/to/directory',
|
|
17
|
-
* // label: 'directory',
|
|
18
|
-
* // children: [
|
|
19
|
-
* // { id: '/path/to/directory/file.txt', label: 'file.txt' },
|
|
20
|
-
* // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
|
|
21
|
-
* // ]
|
|
22
|
-
* // }
|
|
23
|
-
*/
|
|
24
|
-
export function buildDirectoryTree(dirPath, basePath = dirPath) {
|
|
25
|
-
const stats = statSync(dirPath);
|
|
26
|
-
const normalizedDirPath = dirPath.replace(/\\/g, '/');
|
|
27
|
-
const normalizedBasePath = basePath.replace(/\\/g, '/');
|
|
28
|
-
const relativePath = normalizedDirPath.replace(normalizedBasePath, "").replace(/^\//, "") || "/";
|
|
29
|
-
const fileName = normalizedDirPath.split("/").pop() || "/";
|
|
30
|
-
const tree = {
|
|
31
|
-
id: normalizedDirPath,
|
|
32
|
-
label: fileName,
|
|
33
|
-
};
|
|
34
|
-
if (stats.isDirectory()) {
|
|
35
|
-
try {
|
|
36
|
-
const items = readdirSync(dirPath);
|
|
37
|
-
if (items.length > 0) {
|
|
38
|
-
tree.children = items
|
|
39
|
-
.map((item) => {
|
|
40
|
-
const itemPath = join(dirPath, item);
|
|
41
|
-
try {
|
|
42
|
-
return buildDirectoryTree(itemPath, basePath);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
console.error(`Error processing ${itemPath}:`, error);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
.filter((item) => item !== null);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
console.error(`Error reading directory ${dirPath}:`, error);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
console.log("tree=", tree);
|
|
57
|
-
return tree;
|
|
58
|
-
}
|
|
59
|
-
// buildDirectoryTree(
|
|
60
|
-
// "/Users/xipingwang/Documents/EF-code/e1-ai-assistant-it-web/src",
|
|
61
|
-
// );
|
|
62
|
-
/**
|
|
63
|
-
* Gets the parent directory path of a given directory path
|
|
64
|
-
* Cross-platform compatible (Windows, macOS, Linux)
|
|
65
|
-
* @param {string} dirPath - The path to get the parent directory from
|
|
66
|
-
* @returns {string} The parent directory path
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* // On Unix-like systems (Mac/Linux)
|
|
70
|
-
* getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
|
|
71
|
-
*
|
|
72
|
-
* // On Windows
|
|
73
|
-
* getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
|
|
74
|
-
*
|
|
75
|
-
* // Works with both forward and backward slashes
|
|
76
|
-
* getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
|
|
77
|
-
*/
|
|
78
|
-
export function getParentDirectory(dirPath) {
|
|
79
|
-
// Normalize the path to handle different path formats
|
|
80
|
-
const normalizedPath = normalize(dirPath);
|
|
81
|
-
// Get the parent directory using dirname
|
|
82
|
-
const parentPath = dirname(normalizedPath);
|
|
83
|
-
// If we're already at the root, return the normalized root
|
|
84
|
-
return normalizedPath === parentPath ? parentPath : parentPath;
|
|
85
|
-
}
|
|
1
|
+
// 目录树相关功能
|
|
2
|
+
export * from "./src/directory-tree.js";
|
|
3
|
+
// 路径操作相关功能
|
|
4
|
+
export * from "./src/path-utils.js";
|
|
5
|
+
// SRT到VTT转换相关功能
|
|
6
|
+
export * from "./src/srt-to-vtt/index.js";
|
|
7
|
+
// FFmpeg相关功能
|
|
8
|
+
export * from "./src/ffmpeg/index.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a node in a directory tree structure
|
|
3
|
+
* @interface Tree
|
|
4
|
+
* @property {string} id - Unique identifier for the node (full path)
|
|
5
|
+
* @property {string} label - Display name of the file or directory
|
|
6
|
+
* @property {Tree[]} [children] - Optional array of child nodes for directories
|
|
7
|
+
*/
|
|
8
|
+
export interface Tree {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
children?: Tree[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Recursively builds a tree structure from a directory
|
|
15
|
+
* @param {string} dirPath - The absolute path to the directory to process
|
|
16
|
+
* @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
|
|
17
|
+
* @returns {Tree} A tree structure representing the directory hierarchy
|
|
18
|
+
* @throws Will throw an error if directory access fails
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Create a tree from a directory
|
|
22
|
+
* const tree = buildDirectoryTree('/path/to/directory');
|
|
23
|
+
*
|
|
24
|
+
* // Structure of returned tree:
|
|
25
|
+
* // {
|
|
26
|
+
* // id: '/path/to/directory',
|
|
27
|
+
* // label: 'directory',
|
|
28
|
+
* // children: [
|
|
29
|
+
* // { id: '/path/to/directory/file.txt', label: 'file.txt' },
|
|
30
|
+
* // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
|
|
31
|
+
* // ]
|
|
32
|
+
* // }
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildDirectoryTree(dirPath: string, basePath?: string): Tree;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Recursively builds a tree structure from a directory
|
|
5
|
+
* @param {string} dirPath - The absolute path to the directory to process
|
|
6
|
+
* @param {string} [basePath] - The base path for generating relative paths (defaults to dirPath)
|
|
7
|
+
* @returns {Tree} A tree structure representing the directory hierarchy
|
|
8
|
+
* @throws Will throw an error if directory access fails
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Create a tree from a directory
|
|
12
|
+
* const tree = buildDirectoryTree('/path/to/directory');
|
|
13
|
+
*
|
|
14
|
+
* // Structure of returned tree:
|
|
15
|
+
* // {
|
|
16
|
+
* // id: '/path/to/directory',
|
|
17
|
+
* // label: 'directory',
|
|
18
|
+
* // children: [
|
|
19
|
+
* // { id: '/path/to/directory/file.txt', label: 'file.txt' },
|
|
20
|
+
* // { id: '/path/to/directory/subdir', label: 'subdir', children: [...] }
|
|
21
|
+
* // ]
|
|
22
|
+
* // }
|
|
23
|
+
*/
|
|
24
|
+
export function buildDirectoryTree(dirPath, basePath = dirPath) {
|
|
25
|
+
const stats = statSync(dirPath);
|
|
26
|
+
const normalizedDirPath = dirPath.replace(/\\/g, "/");
|
|
27
|
+
const normalizedBasePath = basePath.replace(/\\/g, "/");
|
|
28
|
+
const relativePath = normalizedDirPath.replace(normalizedBasePath, "").replace(/^\//, "") || "/";
|
|
29
|
+
const fileName = normalizedDirPath.split("/").pop() || "/";
|
|
30
|
+
const tree = {
|
|
31
|
+
id: normalizedDirPath,
|
|
32
|
+
label: fileName,
|
|
33
|
+
};
|
|
34
|
+
if (stats.isDirectory()) {
|
|
35
|
+
try {
|
|
36
|
+
const items = readdirSync(dirPath);
|
|
37
|
+
if (items.length > 0) {
|
|
38
|
+
tree.children = items
|
|
39
|
+
.map((item) => {
|
|
40
|
+
const itemPath = join(dirPath, item);
|
|
41
|
+
try {
|
|
42
|
+
return buildDirectoryTree(itemPath, basePath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`Error processing ${itemPath}:`, error);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter((item) => item !== null);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error(`Error reading directory ${dirPath}:`, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
console.log("tree=", tree);
|
|
57
|
+
return tree;
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as shell from "shelljs";
|
|
2
|
+
// 检查 ffmpeg 可用性
|
|
3
|
+
export function checkFFmpegAvailability() {
|
|
4
|
+
const result = shell.exec('ffmpeg -version', { silent: true });
|
|
5
|
+
return result.code === 0;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 检查 ffprobe 是否可用
|
|
9
|
+
* @returns 是否可用
|
|
10
|
+
*/
|
|
11
|
+
export function isFfprobeAvailable() {
|
|
12
|
+
const result = shell.exec("ffprobe -version", { silent: true });
|
|
13
|
+
return result.code === 0;
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视频缩略图生成器
|
|
3
|
+
* 使用 ffmpeg 提取视频帧,使用 sharp 合成缩略图
|
|
4
|
+
*/
|
|
5
|
+
interface ThumbnailOptions {
|
|
6
|
+
frames?: number;
|
|
7
|
+
outputWidth?: number;
|
|
8
|
+
columns?: number;
|
|
9
|
+
outputFileName?: string;
|
|
10
|
+
quality?: number;
|
|
11
|
+
format?: 'avif' | 'webp' | 'jpeg' | 'png';
|
|
12
|
+
batchSize?: number;
|
|
13
|
+
maxConcurrency?: number;
|
|
14
|
+
tempDir?: string;
|
|
15
|
+
}
|
|
16
|
+
interface ProcessingResult {
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
outputPath: string;
|
|
19
|
+
metadata: {
|
|
20
|
+
frames: number;
|
|
21
|
+
duration: number;
|
|
22
|
+
outputSize: {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare const getThumbnail: (videoPath: string, options?: ThumbnailOptions) => Promise<ProcessingResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as shell from "shelljs";
|
|
2
|
+
import Sharp from "sharp";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import { checkFFmpegAvailability } from "./check.js";
|
|
7
|
+
// 日志函数
|
|
8
|
+
const log = (message, data) => {
|
|
9
|
+
console.log(`[ThumbnailGenerator] ${message}`, data ? JSON.stringify(data, null, 2) : '');
|
|
10
|
+
};
|
|
11
|
+
// 验证和清理路径
|
|
12
|
+
function validateAndSanitizePath(inputPath) {
|
|
13
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
14
|
+
throw new Error('Invalid path provided');
|
|
15
|
+
}
|
|
16
|
+
const resolvedPath = path.resolve(inputPath);
|
|
17
|
+
// 检查路径是否在允许的目录内
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
if (!resolvedPath.startsWith(cwd)) {
|
|
20
|
+
throw new Error('Path traversal attack detected');
|
|
21
|
+
}
|
|
22
|
+
return resolvedPath;
|
|
23
|
+
}
|
|
24
|
+
// 获取视频时长
|
|
25
|
+
function getDurationInSeconds(videoPath) {
|
|
26
|
+
log('Getting video duration', { videoPath });
|
|
27
|
+
const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
|
|
28
|
+
if (result.code !== 0) {
|
|
29
|
+
throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
|
|
30
|
+
}
|
|
31
|
+
const duration = parseFloat(result.stdout);
|
|
32
|
+
if (isNaN(duration) || duration <= 0) {
|
|
33
|
+
throw new Error('Invalid video duration');
|
|
34
|
+
}
|
|
35
|
+
return duration;
|
|
36
|
+
}
|
|
37
|
+
// 并行提取视频帧
|
|
38
|
+
async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrency = 4) {
|
|
39
|
+
log('Starting frame extraction', { frames, interval, maxConcurrency });
|
|
40
|
+
const framePromises = Array.from({ length: frames - 1 }, (_, i) => {
|
|
41
|
+
const t = interval * i;
|
|
42
|
+
const framePath = path.join(tempDir, `frame-${t}.jpg`);
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const result = shell.exec(`ffmpeg -ss ${t} -i "${videoPath}" -vframes 1 -q:v 10 -an -threads 4 "${framePath}"`, { silent: true });
|
|
45
|
+
if (result.code === 0) {
|
|
46
|
+
log(`Frame ${i + 1}/${frames - 1} extracted`, { time: t });
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
reject(new Error(`FFmpeg failed for frame ${i + 1}: ${result.stderr}`));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// 分批执行以控制并发数
|
|
55
|
+
for (let i = 0; i < framePromises.length; i += maxConcurrency) {
|
|
56
|
+
const batch = framePromises.slice(i, i + maxConcurrency);
|
|
57
|
+
await Promise.all(batch);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 创建缩略图
|
|
61
|
+
async function createThumbnail(frameFiles, tempDir, options) {
|
|
62
|
+
const { columns = 4, outputWidth = 3840, format = 'avif', quality = 80 } = options;
|
|
63
|
+
log('Creating thumbnail', { frameCount: frameFiles.length, columns, outputWidth });
|
|
64
|
+
// 计算尺寸
|
|
65
|
+
const rows = Math.ceil(frameFiles.length / columns);
|
|
66
|
+
const singleWidth = Math.floor(outputWidth / columns);
|
|
67
|
+
// 获取第一帧的元数据来计算宽高比
|
|
68
|
+
const firstFrame = await Sharp(path.join(tempDir, frameFiles[0])).metadata();
|
|
69
|
+
const aspectRatio = (firstFrame.width || 1) / (firstFrame.height || 1);
|
|
70
|
+
const singleHeight = Math.floor(singleWidth / aspectRatio);
|
|
71
|
+
log('Calculated dimensions', {
|
|
72
|
+
rows,
|
|
73
|
+
singleWidth,
|
|
74
|
+
singleHeight,
|
|
75
|
+
aspectRatio: aspectRatio.toFixed(2)
|
|
76
|
+
});
|
|
77
|
+
// 创建合成图像
|
|
78
|
+
const composite = Sharp({
|
|
79
|
+
create: {
|
|
80
|
+
width: singleWidth * columns,
|
|
81
|
+
height: singleHeight * rows,
|
|
82
|
+
channels: 3,
|
|
83
|
+
background: { r: 0, g: 0, b: 0 },
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
// 分批处理帧以避免内存问题
|
|
87
|
+
const batchSize = options.batchSize || 10;
|
|
88
|
+
const overlays = [];
|
|
89
|
+
for (let i = 0; i < frameFiles.length; i += batchSize) {
|
|
90
|
+
const batch = frameFiles.slice(i, i + batchSize);
|
|
91
|
+
const batchOverlays = await Promise.all(batch.map(async (file, batchIndex) => {
|
|
92
|
+
const index = i + batchIndex;
|
|
93
|
+
const row = Math.floor(index / columns);
|
|
94
|
+
const col = index % columns;
|
|
95
|
+
const resized = await Sharp(path.join(tempDir, file))
|
|
96
|
+
.resize(singleWidth, singleHeight, { fit: 'cover' })
|
|
97
|
+
.toBuffer();
|
|
98
|
+
return {
|
|
99
|
+
input: resized,
|
|
100
|
+
top: row * singleHeight,
|
|
101
|
+
left: col * singleWidth,
|
|
102
|
+
};
|
|
103
|
+
}));
|
|
104
|
+
overlays.push(...batchOverlays);
|
|
105
|
+
log(`Processed batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(frameFiles.length / batchSize)}`);
|
|
106
|
+
}
|
|
107
|
+
// 生成最终图像
|
|
108
|
+
let finalImage = composite.composite(overlays);
|
|
109
|
+
// 根据格式设置输出
|
|
110
|
+
switch (format) {
|
|
111
|
+
case 'avif':
|
|
112
|
+
finalImage = finalImage.avif({ quality });
|
|
113
|
+
break;
|
|
114
|
+
case 'webp':
|
|
115
|
+
finalImage = finalImage.webp({ quality });
|
|
116
|
+
break;
|
|
117
|
+
case 'jpeg':
|
|
118
|
+
finalImage = finalImage.jpeg({ quality });
|
|
119
|
+
break;
|
|
120
|
+
case 'png':
|
|
121
|
+
finalImage = finalImage.png({ quality });
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
finalImage = finalImage.avif({ quality });
|
|
125
|
+
}
|
|
126
|
+
return await finalImage.toBuffer();
|
|
127
|
+
}
|
|
128
|
+
// 临时目录管理
|
|
129
|
+
async function withTempDir(fn, customTempDir) {
|
|
130
|
+
const tempDir = customTempDir || fs.mkdtempSync(path.join(os.tmpdir(), "video-thumbnails-"));
|
|
131
|
+
try {
|
|
132
|
+
return await fn(tempDir);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (fs.existsSync(tempDir) && !customTempDir) {
|
|
136
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
137
|
+
log('Cleaned up temporary directory', { tempDir });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 主函数
|
|
142
|
+
export const getThumbnail = async (videoPath, options = {}) => {
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
try {
|
|
145
|
+
// 输入验证
|
|
146
|
+
const validatedPath = validateAndSanitizePath(videoPath);
|
|
147
|
+
if (!fs.existsSync(validatedPath)) {
|
|
148
|
+
throw new Error(`Video file not found: ${validatedPath}`);
|
|
149
|
+
}
|
|
150
|
+
// 检查 ffmpeg 可用性
|
|
151
|
+
if (!checkFFmpegAvailability()) {
|
|
152
|
+
throw new Error('FFmpeg is not available. Please install FFmpeg first.');
|
|
153
|
+
}
|
|
154
|
+
// 参数设置和验证
|
|
155
|
+
const { frames = 60, outputWidth = 3840, columns = 4, outputFileName = "thumbnail.avif", quality = 80, format = 'avif', batchSize = 10, maxConcurrency = 4, tempDir: customTempDir } = options;
|
|
156
|
+
// 参数验证
|
|
157
|
+
if (frames <= 0 || outputWidth <= 0 || columns <= 0) {
|
|
158
|
+
throw new Error('Invalid parameters: frames, outputWidth, and columns must be positive');
|
|
159
|
+
}
|
|
160
|
+
if (quality < 1 || quality > 100) {
|
|
161
|
+
throw new Error('Quality must be between 1 and 100');
|
|
162
|
+
}
|
|
163
|
+
log('Starting thumbnail generation', {
|
|
164
|
+
videoPath: validatedPath,
|
|
165
|
+
options: { frames, outputWidth, columns, format, quality }
|
|
166
|
+
});
|
|
167
|
+
// 获取视频信息
|
|
168
|
+
const durationInSeconds = getDurationInSeconds(validatedPath);
|
|
169
|
+
const interval = durationInSeconds / frames;
|
|
170
|
+
log('Video analysis complete', { durationInSeconds, interval });
|
|
171
|
+
// 准备输出路径
|
|
172
|
+
const videoDir = path.dirname(validatedPath);
|
|
173
|
+
const fileNameWithoutExt = path.basename(validatedPath, path.extname(validatedPath));
|
|
174
|
+
const outputPath = path.join(videoDir, `${fileNameWithoutExt}_${outputFileName}`);
|
|
175
|
+
// 使用临时目录处理
|
|
176
|
+
const result = await withTempDir(async (tempDir) => {
|
|
177
|
+
// 提取帧
|
|
178
|
+
await extractFrames(validatedPath, tempDir, frames, interval, maxConcurrency);
|
|
179
|
+
// 获取所有帧文件并排序
|
|
180
|
+
const frameFiles = fs
|
|
181
|
+
.readdirSync(tempDir)
|
|
182
|
+
.filter((file) => file.startsWith("frame-"))
|
|
183
|
+
.sort((a, b) => {
|
|
184
|
+
const numA = parseFloat(a.match(/\d+/)?.[0] || "0");
|
|
185
|
+
const numB = parseFloat(b.match(/\d+/)?.[0] || "0");
|
|
186
|
+
return numA - numB;
|
|
187
|
+
});
|
|
188
|
+
if (frameFiles.length === 0) {
|
|
189
|
+
throw new Error('No frames were extracted from the video');
|
|
190
|
+
}
|
|
191
|
+
log('Frame extraction complete', { extractedFrames: frameFiles.length });
|
|
192
|
+
// 创建缩略图
|
|
193
|
+
const buffer = await createThumbnail(frameFiles, tempDir, options);
|
|
194
|
+
return buffer;
|
|
195
|
+
}, customTempDir);
|
|
196
|
+
// 保存文件
|
|
197
|
+
fs.writeFileSync(outputPath, result);
|
|
198
|
+
const processingTime = Date.now() - startTime;
|
|
199
|
+
log('Thumbnail generation complete', {
|
|
200
|
+
outputPath,
|
|
201
|
+
processingTime: `${processingTime}ms`,
|
|
202
|
+
fileSize: `${(result.length / 1024).toFixed(2)}KB`
|
|
203
|
+
});
|
|
204
|
+
// 获取输出图像元数据
|
|
205
|
+
const metadata = await Sharp(result).metadata();
|
|
206
|
+
return {
|
|
207
|
+
buffer: result,
|
|
208
|
+
outputPath,
|
|
209
|
+
metadata: {
|
|
210
|
+
frames,
|
|
211
|
+
duration: durationInSeconds,
|
|
212
|
+
outputSize: {
|
|
213
|
+
width: metadata.width || 0,
|
|
214
|
+
height: metadata.height || 0
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
log('Thumbnail generation failed', {
|
|
221
|
+
error: error instanceof Error ? error.message : String(error),
|
|
222
|
+
videoPath
|
|
223
|
+
});
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export * from "./getThumbnail.js";
|
|
2
|
+
export * from "./check.js";
|
|
3
|
+
/**
|
|
4
|
+
* 获取视频信息的接口定义
|
|
5
|
+
*/
|
|
6
|
+
export interface VideoInfo {
|
|
7
|
+
/** 视频文件路径 */
|
|
8
|
+
path: string;
|
|
9
|
+
/** 视频时长(秒) */
|
|
10
|
+
duration: number;
|
|
11
|
+
/** 视频时长格式化字符串 */
|
|
12
|
+
durationFormatted: string;
|
|
13
|
+
/** 视频宽度 */
|
|
14
|
+
width: number;
|
|
15
|
+
/** 视频高度 */
|
|
16
|
+
height: number;
|
|
17
|
+
/** 视频编码格式 */
|
|
18
|
+
videoCodec: string;
|
|
19
|
+
/** 音频编码格式 */
|
|
20
|
+
audioCodec: string;
|
|
21
|
+
/** 视频比特率 */
|
|
22
|
+
bitrate: number;
|
|
23
|
+
/** 帧率 */
|
|
24
|
+
fps: number;
|
|
25
|
+
/** 文件大小(字节) */
|
|
26
|
+
fileSize: number;
|
|
27
|
+
/** 文件大小格式化字符串 */
|
|
28
|
+
fileSizeFormatted: string;
|
|
29
|
+
/** 原始 ffprobe 输出信息 */
|
|
30
|
+
rawInfo: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 获取视频信息(使用 ffprobe)
|
|
34
|
+
* @param videoPath 视频文件路径
|
|
35
|
+
* @returns 视频信息对象
|
|
36
|
+
*/
|
|
37
|
+
export declare function getVideoInfo(videoPath: string): VideoInfo;
|
|
38
|
+
/**
|
|
39
|
+
* 批量获取视频信息
|
|
40
|
+
* @param videoPaths 视频文件路径数组
|
|
41
|
+
* @returns 视频信息对象数组
|
|
42
|
+
*/
|
|
43
|
+
export declare function getMultipleVideoInfo(videoPaths: string[]): VideoInfo[];
|
|
44
|
+
/**
|
|
45
|
+
* 获取详细的视频信息(包含更多元数据)
|
|
46
|
+
* @param videoPath 视频文件路径
|
|
47
|
+
* @returns 详细的视频信息
|
|
48
|
+
*/
|
|
49
|
+
export declare function getDetailedVideoInfo(videoPath: string): any;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import shell from "shelljs";
|
|
2
|
+
export * from "./getThumbnail.js";
|
|
3
|
+
export * from "./check.js";
|
|
4
|
+
/**
|
|
5
|
+
* 获取视频信息(使用 ffprobe)
|
|
6
|
+
* @param videoPath 视频文件路径
|
|
7
|
+
* @returns 视频信息对象
|
|
8
|
+
*/
|
|
9
|
+
export function getVideoInfo(videoPath) {
|
|
10
|
+
// 检查文件是否存在
|
|
11
|
+
if (!shell.test("-f", videoPath)) {
|
|
12
|
+
throw new Error(`视频文件不存在: ${videoPath}`);
|
|
13
|
+
}
|
|
14
|
+
// 使用 ffprobe 获取 JSON 格式的视频信息
|
|
15
|
+
const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`, { silent: true });
|
|
16
|
+
if (result.code !== 0) {
|
|
17
|
+
throw new Error(`获取视频信息失败: ${result.stderr}`);
|
|
18
|
+
}
|
|
19
|
+
let probeData;
|
|
20
|
+
try {
|
|
21
|
+
probeData = JSON.parse(result.stdout);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new Error(`解析 ffprobe 输出失败: ${error}`);
|
|
25
|
+
}
|
|
26
|
+
const info = {
|
|
27
|
+
path: videoPath,
|
|
28
|
+
rawInfo: JSON.stringify(probeData, null, 2),
|
|
29
|
+
};
|
|
30
|
+
// 从 format 信息中获取时长和文件大小
|
|
31
|
+
if (probeData.format) {
|
|
32
|
+
if (probeData.format.duration) {
|
|
33
|
+
info.duration = parseFloat(probeData.format.duration);
|
|
34
|
+
info.durationFormatted = formatDuration(info.duration);
|
|
35
|
+
}
|
|
36
|
+
if (probeData.format.size) {
|
|
37
|
+
info.fileSize = parseInt(probeData.format.size);
|
|
38
|
+
info.fileSizeFormatted = formatFileSize(info.fileSize);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 从视频流中获取信息
|
|
42
|
+
const videoStream = probeData.streams?.find((stream) => stream.codec_type === "video");
|
|
43
|
+
if (videoStream) {
|
|
44
|
+
info.videoCodec = videoStream.codec_name || "unknown";
|
|
45
|
+
info.width = videoStream.width || 0;
|
|
46
|
+
info.height = videoStream.height || 0;
|
|
47
|
+
if (videoStream.r_frame_rate) {
|
|
48
|
+
const [num, den] = videoStream.r_frame_rate.split("/");
|
|
49
|
+
info.fps = parseInt(num) / parseInt(den);
|
|
50
|
+
}
|
|
51
|
+
if (videoStream.bit_rate) {
|
|
52
|
+
info.bitrate = parseInt(videoStream.bit_rate);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 从音频流中获取信息
|
|
56
|
+
const audioStream = probeData.streams?.find((stream) => stream.codec_type === "audio");
|
|
57
|
+
if (audioStream) {
|
|
58
|
+
info.audioCodec = audioStream.codec_name || "unknown";
|
|
59
|
+
}
|
|
60
|
+
return info;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 格式化文件大小
|
|
64
|
+
* @param bytes 字节数
|
|
65
|
+
* @returns 格式化后的文件大小字符串
|
|
66
|
+
*/
|
|
67
|
+
function formatFileSize(bytes) {
|
|
68
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
69
|
+
let size = bytes;
|
|
70
|
+
let unitIndex = 0;
|
|
71
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
72
|
+
size /= 1024;
|
|
73
|
+
unitIndex++;
|
|
74
|
+
}
|
|
75
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 格式化时长
|
|
79
|
+
* @param seconds 秒数
|
|
80
|
+
* @returns 格式化后的时长字符串
|
|
81
|
+
*/
|
|
82
|
+
function formatDuration(seconds) {
|
|
83
|
+
const hours = Math.floor(seconds / 3600);
|
|
84
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
85
|
+
const secs = Math.floor(seconds % 60);
|
|
86
|
+
const ms = Math.floor((seconds % 1) * 100);
|
|
87
|
+
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(2, "0")}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 批量获取视频信息
|
|
91
|
+
* @param videoPaths 视频文件路径数组
|
|
92
|
+
* @returns 视频信息对象数组
|
|
93
|
+
*/
|
|
94
|
+
export function getMultipleVideoInfo(videoPaths) {
|
|
95
|
+
return videoPaths.map((path) => getVideoInfo(path));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 获取详细的视频信息(包含更多元数据)
|
|
99
|
+
* @param videoPath 视频文件路径
|
|
100
|
+
* @returns 详细的视频信息
|
|
101
|
+
*/
|
|
102
|
+
export function getDetailedVideoInfo(videoPath) {
|
|
103
|
+
if (!shell.test("-f", videoPath)) {
|
|
104
|
+
throw new Error(`视频文件不存在: ${videoPath}`);
|
|
105
|
+
}
|
|
106
|
+
const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams -show_chapters -show_private_data "${videoPath}"`, { silent: true });
|
|
107
|
+
if (result.code !== 0) {
|
|
108
|
+
throw new Error(`获取详细视频信息失败: ${result.stderr}`);
|
|
109
|
+
}
|
|
110
|
+
return JSON.parse(result.stdout);
|
|
111
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the parent directory path of a given directory path
|
|
3
|
+
* Cross-platform compatible (Windows, macOS, Linux)
|
|
4
|
+
* @param {string} dirPath - The path to get the parent directory from
|
|
5
|
+
* @returns {string} The parent directory path
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // On Unix-like systems (Mac/Linux)
|
|
9
|
+
* getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
|
|
10
|
+
*
|
|
11
|
+
* // On Windows
|
|
12
|
+
* getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
|
|
13
|
+
*
|
|
14
|
+
* // Works with both forward and backward slashes
|
|
15
|
+
* getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
|
|
16
|
+
*/
|
|
17
|
+
export declare function getParentDirectory(dirPath: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dirname, normalize } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Gets the parent directory path of a given directory path
|
|
4
|
+
* Cross-platform compatible (Windows, macOS, Linux)
|
|
5
|
+
* @param {string} dirPath - The path to get the parent directory from
|
|
6
|
+
* @returns {string} The parent directory path
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // On Unix-like systems (Mac/Linux)
|
|
10
|
+
* getParentDirectory('/Users/documents/folder') // returns '/Users/documents'
|
|
11
|
+
*
|
|
12
|
+
* // On Windows
|
|
13
|
+
* getParentDirectory('C:\\Users\\Documents\\folder') // returns 'C:\\Users\\Documents'
|
|
14
|
+
*
|
|
15
|
+
* // Works with both forward and backward slashes
|
|
16
|
+
* getParentDirectory('C:/Users/Documents/folder') // returns 'C:/Users/Documents'
|
|
17
|
+
*/
|
|
18
|
+
export function getParentDirectory(dirPath) {
|
|
19
|
+
// Normalize the path to handle different path formats
|
|
20
|
+
const normalizedPath = normalize(dirPath);
|
|
21
|
+
// Get the parent directory using dirname
|
|
22
|
+
const parentPath = dirname(normalizedPath);
|
|
23
|
+
// If we're already at the root, return the normalized root
|
|
24
|
+
return normalizedPath === parentPath ? parentPath : parentPath;
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从文件读取SRT内容并转换为VTT格式
|
|
3
|
+
* @param srtFilePath SRT文件路径
|
|
4
|
+
* @returns 转换后的VTT内容字符串
|
|
5
|
+
*/
|
|
6
|
+
export declare function convertSrtFileToVtt(srtFilePath: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* 从文件读取SRT内容并转换为VTT格式,同时保存到文件
|
|
9
|
+
* @param srtFilePath SRT文件路径
|
|
10
|
+
* @param outputPath 输出VTT文件路径(可选,默认与SRT文件同目录)
|
|
11
|
+
* @returns 输出文件路径
|
|
12
|
+
*/
|
|
13
|
+
export declare function convertSrtFileToVttFile(srtFilePath: string, outputPath?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* 批量转换SRT文件到VTT格式
|
|
16
|
+
* @param srtFilePaths SRT文件路径数组
|
|
17
|
+
* @param outputDir 输出目录(可选)
|
|
18
|
+
* @returns 转换结果数组
|
|
19
|
+
*/
|
|
20
|
+
export declare function batchConvertSrtToVtt(srtFilePaths: string[], outputDir?: string): Array<{
|
|
21
|
+
input: string;
|
|
22
|
+
output: string;
|
|
23
|
+
success: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* 从字符串内容直接转换为VTT格式
|
|
28
|
+
* @param srtContent SRT内容字符串
|
|
29
|
+
* @returns VTT格式字符串
|
|
30
|
+
*/
|
|
31
|
+
export declare function convertSrtStringToVtt(srtContent: string): string;
|
|
32
|
+
export { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
|
|
33
|
+
export default convertSrtFileToVtt;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
|
+
import { join, extname, basename } from "path";
|
|
4
|
+
/**
|
|
5
|
+
* 从文件读取SRT内容并转换为VTT格式
|
|
6
|
+
* @param srtFilePath SRT文件路径
|
|
7
|
+
* @returns 转换后的VTT内容字符串
|
|
8
|
+
*/
|
|
9
|
+
export function convertSrtFileToVtt(srtFilePath) {
|
|
10
|
+
try {
|
|
11
|
+
// 检查文件是否存在
|
|
12
|
+
if (!existsSync(srtFilePath)) {
|
|
13
|
+
throw new Error(`SRT文件不存在: ${srtFilePath}`);
|
|
14
|
+
}
|
|
15
|
+
// 检查文件扩展名
|
|
16
|
+
const ext = extname(srtFilePath).toLowerCase();
|
|
17
|
+
if (ext !== ".srt") {
|
|
18
|
+
throw new Error(`文件扩展名必须是.srt,当前为: ${ext}`);
|
|
19
|
+
}
|
|
20
|
+
// 读取SRT文件内容
|
|
21
|
+
const srtContent = readFileSync(srtFilePath, "utf-8");
|
|
22
|
+
// 验证SRT格式
|
|
23
|
+
if (!validateSRT(srtContent)) {
|
|
24
|
+
throw new Error("SRT文件格式无效");
|
|
25
|
+
}
|
|
26
|
+
// 转换为VTT格式
|
|
27
|
+
const vttContent = srtToVtt(srtContent);
|
|
28
|
+
return vttContent;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`转换失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 从文件读取SRT内容并转换为VTT格式,同时保存到文件
|
|
36
|
+
* @param srtFilePath SRT文件路径
|
|
37
|
+
* @param outputPath 输出VTT文件路径(可选,默认与SRT文件同目录)
|
|
38
|
+
* @returns 输出文件路径
|
|
39
|
+
*/
|
|
40
|
+
export function convertSrtFileToVttFile(srtFilePath, outputPath) {
|
|
41
|
+
try {
|
|
42
|
+
// 转换SRT到VTT
|
|
43
|
+
const vttContent = convertSrtFileToVtt(srtFilePath);
|
|
44
|
+
// 确定输出路径
|
|
45
|
+
let vttFilePath;
|
|
46
|
+
if (outputPath) {
|
|
47
|
+
vttFilePath = outputPath;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// 默认输出到同目录,文件名相同但扩展名为.vtt
|
|
51
|
+
const dir = srtFilePath.substring(0, srtFilePath.lastIndexOf("/") + 1);
|
|
52
|
+
const baseName = basename(srtFilePath, ".srt");
|
|
53
|
+
vttFilePath = join(dir, `${baseName}.vtt`);
|
|
54
|
+
}
|
|
55
|
+
// 写入VTT文件
|
|
56
|
+
writeFileSync(vttFilePath, vttContent, "utf-8");
|
|
57
|
+
return vttFilePath;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new Error(`文件转换失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 批量转换SRT文件到VTT格式
|
|
65
|
+
* @param srtFilePaths SRT文件路径数组
|
|
66
|
+
* @param outputDir 输出目录(可选)
|
|
67
|
+
* @returns 转换结果数组
|
|
68
|
+
*/
|
|
69
|
+
export function batchConvertSrtToVtt(srtFilePaths, outputDir) {
|
|
70
|
+
const results = [];
|
|
71
|
+
for (const srtFilePath of srtFilePaths) {
|
|
72
|
+
try {
|
|
73
|
+
let outputPath;
|
|
74
|
+
if (outputDir) {
|
|
75
|
+
const baseName = basename(srtFilePath, ".srt");
|
|
76
|
+
outputPath = join(outputDir, `${baseName}.vtt`);
|
|
77
|
+
}
|
|
78
|
+
const vttFilePath = convertSrtFileToVttFile(srtFilePath, outputPath);
|
|
79
|
+
results.push({
|
|
80
|
+
input: srtFilePath,
|
|
81
|
+
output: vttFilePath,
|
|
82
|
+
success: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
results.push({
|
|
87
|
+
input: srtFilePath,
|
|
88
|
+
output: "",
|
|
89
|
+
success: false,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 从字符串内容直接转换为VTT格式
|
|
98
|
+
* @param srtContent SRT内容字符串
|
|
99
|
+
* @returns VTT格式字符串
|
|
100
|
+
*/
|
|
101
|
+
export function convertSrtStringToVtt(srtContent) {
|
|
102
|
+
try {
|
|
103
|
+
if (!validateSRT(srtContent)) {
|
|
104
|
+
throw new Error("SRT内容格式无效");
|
|
105
|
+
}
|
|
106
|
+
return srtToVtt(srtContent);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`转换失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 重新导出原始包的功能
|
|
113
|
+
export { srtToVtt, validateSRT } from "@xiping/srt-to-vtt";
|
|
114
|
+
// 默认导出主转换函数
|
|
115
|
+
export default convertSrtFileToVtt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiping/node-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
4
4
|
"description": "node-utils",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "The-End-Hero <527409987@qq.com>",
|
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/The-End-Hero/wang-ping/issues"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "79ba6360ab08693c43b9d0cc3dace1e7baba0421",
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public",
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
32
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@xiping/srt-to-vtt": "1.0.35",
|
|
35
|
+
"chalk": "^5.5.0",
|
|
36
|
+
"sharp": "^0.34.3",
|
|
37
|
+
"shelljs": "0.10.0"
|
|
38
|
+
},
|
|
33
39
|
"devDependencies": {
|
|
34
40
|
"@types/node": "^22.15.34",
|
|
35
41
|
"tslib": "^2.8.1",
|