@sns-parse/core 0.3.0-alpha.1 → 0.4.0-alpha.2
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/config.d.ts +5 -0
- package/lib/config.js +28 -0
- package/lib/engine/config-io.d.ts +44 -0
- package/lib/engine/config-io.js +155 -0
- package/lib/engine/fetcher.d.ts +21 -0
- package/lib/engine/fetcher.js +228 -0
- package/lib/engine/gateway.d.ts +21 -0
- package/lib/engine/gateway.js +19 -0
- package/lib/engine/parser.d.ts +2 -0
- package/lib/engine/parser.js +162 -0
- package/lib/engine/platform-config.d.ts +24 -0
- package/lib/engine/platform-config.js +76 -0
- package/lib/engine/runtime.d.ts +49 -0
- package/lib/engine/runtime.js +90 -0
- package/lib/engine/translate.d.ts +15 -0
- package/lib/engine/translate.js +107 -0
- package/lib/engine/twitter.d.ts +26 -0
- package/lib/engine/twitter.js +398 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +19 -0
- package/lib/registry.d.ts +20 -0
- package/lib/registry.js +77 -0
- package/lib/utils/cache.d.ts +9 -0
- package/lib/utils/cache.js +34 -0
- package/lib/utils/common.d.ts +7 -0
- package/lib/utils/common.js +58 -0
- package/lib/utils/concurrency.d.ts +8 -0
- package/lib/utils/concurrency.js +29 -0
- package/lib/utils/field-mapping.d.ts +2 -0
- package/lib/utils/field-mapping.js +29 -0
- package/lib/utils/format.d.ts +4 -0
- package/lib/utils/format.js +79 -0
- package/lib/utils/gif.d.ts +10 -0
- package/lib/utils/gif.js +104 -0
- package/lib/utils/image-size.d.ts +10 -0
- package/lib/utils/image-size.js +69 -0
- package/lib/utils/merge.d.ts +61 -0
- package/lib/utils/merge.js +388 -0
- package/lib/utils/tls-client.d.ts +30 -0
- package/lib/utils/tls-client.js +120 -0
- package/lib/utils/url.d.ts +10 -0
- package/lib/utils/url.js +89 -0
- package/package.json +16 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConcurrencyLimiter = void 0;
|
|
4
|
+
class ConcurrencyLimiter {
|
|
5
|
+
constructor(max) {
|
|
6
|
+
this.max = max;
|
|
7
|
+
this.running = 0;
|
|
8
|
+
this.queue = [];
|
|
9
|
+
}
|
|
10
|
+
async acquire() {
|
|
11
|
+
if (this.running < this.max) {
|
|
12
|
+
this.running++;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return new Promise(resolve => {
|
|
16
|
+
this.queue.push(() => {
|
|
17
|
+
this.running++;
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
release() {
|
|
23
|
+
this.running--;
|
|
24
|
+
const next = this.queue.shift();
|
|
25
|
+
if (next)
|
|
26
|
+
next();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.ConcurrencyLimiter = ConcurrencyLimiter;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNestedValue = getNestedValue;
|
|
4
|
+
exports.parseFieldMapping = parseFieldMapping;
|
|
5
|
+
function getNestedValue(obj, path) {
|
|
6
|
+
if (!path)
|
|
7
|
+
return obj;
|
|
8
|
+
const keys = path.split('.');
|
|
9
|
+
let current = obj;
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
if (current === null || current === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
current = current[key];
|
|
14
|
+
}
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
function parseFieldMapping(mappingStr) {
|
|
18
|
+
if (!mappingStr || mappingStr.trim() === '{}' || mappingStr.trim() === '')
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const obj = JSON.parse(mappingStr);
|
|
22
|
+
if (typeof obj === 'object' && !Array.isArray(obj))
|
|
23
|
+
return obj;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ParsedData } from '../types';
|
|
2
|
+
export declare function formatDuration(seconds: number): string;
|
|
3
|
+
export declare function formatPublishTime(ms: number): string;
|
|
4
|
+
export declare function generateFormattedText(p: ParsedData, format: string, index?: number, total?: number): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatDuration = formatDuration;
|
|
4
|
+
exports.formatPublishTime = formatPublishTime;
|
|
5
|
+
exports.generateFormattedText = generateFormattedText;
|
|
6
|
+
const translate_1 = require("../engine/translate");
|
|
7
|
+
const formatVarRegex = /\$\{([^}]+)\}/g;
|
|
8
|
+
function formatDuration(seconds) {
|
|
9
|
+
if (!seconds || seconds <= 0)
|
|
10
|
+
return '';
|
|
11
|
+
const h = Math.floor(seconds / 3600);
|
|
12
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
13
|
+
const s = Math.floor(seconds % 60);
|
|
14
|
+
if (h > 0)
|
|
15
|
+
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
16
|
+
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
17
|
+
}
|
|
18
|
+
function formatPublishTime(ms) {
|
|
19
|
+
if (!ms)
|
|
20
|
+
return '';
|
|
21
|
+
const d = new Date(ms);
|
|
22
|
+
const y = d.getFullYear();
|
|
23
|
+
const mo = (d.getMonth() + 1).toString().padStart(2, '0');
|
|
24
|
+
const day = d.getDate().toString().padStart(2, '0');
|
|
25
|
+
const H = d.getHours().toString().padStart(2, '0');
|
|
26
|
+
const i = d.getMinutes().toString().padStart(2, '0');
|
|
27
|
+
return `${y}年${mo}月${day}日 ${H}:${i}`;
|
|
28
|
+
}
|
|
29
|
+
function generateFormattedText(p, format, index, total) {
|
|
30
|
+
const imageCount = p.images.length || p.live_photo.length;
|
|
31
|
+
const vars = {
|
|
32
|
+
'标题': p.title,
|
|
33
|
+
'作者': p.author,
|
|
34
|
+
'简介': p.desc,
|
|
35
|
+
'视频时长': p.duration > 0 ? formatDuration(p.duration) : '',
|
|
36
|
+
'点赞数': String(p.like),
|
|
37
|
+
'收藏数': String(p.collect),
|
|
38
|
+
'转发数': String(p.share),
|
|
39
|
+
'播放数': String(p.play),
|
|
40
|
+
'评论数': String(p.comment),
|
|
41
|
+
'发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
|
|
42
|
+
'图片数量': String(imageCount),
|
|
43
|
+
'作者ID': p.uid,
|
|
44
|
+
'音乐标题': p.music.title || '',
|
|
45
|
+
'音乐作者': p.music.author || '',
|
|
46
|
+
'翻译': p.translation || '',
|
|
47
|
+
// 原文语言与提供方仅在有译文时展示(同一行变量全空则整行隐藏)
|
|
48
|
+
'原文语言': p.translation ? (0, translate_1.langName)(p.lang) : '',
|
|
49
|
+
'翻译提供方': p.translationProvider || '',
|
|
50
|
+
};
|
|
51
|
+
const lines = format.split('\n');
|
|
52
|
+
const resultLines = [];
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const varMatches = line.match(formatVarRegex);
|
|
55
|
+
if (varMatches && varMatches.length > 0) {
|
|
56
|
+
let allEmptyOrZero = true;
|
|
57
|
+
for (const match of varMatches) {
|
|
58
|
+
const varName = match.slice(2, -1);
|
|
59
|
+
const val = vars[varName];
|
|
60
|
+
if (val && val !== '0') {
|
|
61
|
+
allEmptyOrZero = false;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (allEmptyOrZero)
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let newLine = line;
|
|
69
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
70
|
+
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
71
|
+
}
|
|
72
|
+
resultLines.push(newLine);
|
|
73
|
+
}
|
|
74
|
+
let text = resultLines.join('\n').trim();
|
|
75
|
+
if (index !== undefined && total !== undefined && total > 1) {
|
|
76
|
+
text = `【${index}/${total}】\n${text}`;
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ParserRuntime } from '../engine/runtime';
|
|
2
|
+
import type { GifOptions } from '../extensions';
|
|
3
|
+
export type { GifOptions };
|
|
4
|
+
export declare function resolveFfmpeg(): string;
|
|
5
|
+
/** 生成 ffmpeg 滤镜串(导出供测试) */
|
|
6
|
+
export declare function gifFilter(width: number, fps: number): string;
|
|
7
|
+
/** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
|
|
8
|
+
export declare function gifDuration(durationSec: number, maxDurationSec: number): number;
|
|
9
|
+
/** 下载视频并转 GIF;任何失败返回 null */
|
|
10
|
+
export declare function mp4ToGif(rt: ParserRuntime, url: string, durationSec: number, opts: GifOptions): Promise<Buffer | null>;
|
package/lib/utils/gif.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveFfmpeg = resolveFfmpeg;
|
|
4
|
+
exports.gifFilter = gifFilter;
|
|
5
|
+
exports.gifDuration = gifDuration;
|
|
6
|
+
exports.mp4ToGif = mp4ToGif;
|
|
7
|
+
/**
|
|
8
|
+
* 视频转 GIF(推文动图用)。
|
|
9
|
+
*
|
|
10
|
+
* ffmpeg 管道两遍法(palettegen + paletteuse),不落盘。
|
|
11
|
+
* ffmpeg 不可用 / 转换失败 / 下载失败 → 返回 null,调用方回退发送原视频。
|
|
12
|
+
*/
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const logger_1 = require("../logger");
|
|
15
|
+
let ffmpegMissingWarned = false;
|
|
16
|
+
/**
|
|
17
|
+
* ffmpeg 路径解析:内置静态二进制(optionalDependency ffmpeg-static,随插件自动安装)
|
|
18
|
+
* → 系统 PATH。解析结果记忆化,来源记入 debug 日志。
|
|
19
|
+
*/
|
|
20
|
+
let resolvedFfmpeg = null;
|
|
21
|
+
function resolveFfmpeg() {
|
|
22
|
+
if (resolvedFfmpeg !== null)
|
|
23
|
+
return resolvedFfmpeg;
|
|
24
|
+
try {
|
|
25
|
+
const p = require('ffmpeg-static');
|
|
26
|
+
resolvedFfmpeg = typeof p === 'string' && p ? p : 'ffmpeg';
|
|
27
|
+
(0, logger_1.debugLog)(`ffmpeg 解析:内置静态二进制 ${resolvedFfmpeg}`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
resolvedFfmpeg = 'ffmpeg';
|
|
31
|
+
(0, logger_1.debugLog)('ffmpeg 解析:未安装 ffmpeg-static(--no-optional 安装?),回退系统 PATH');
|
|
32
|
+
}
|
|
33
|
+
return resolvedFfmpeg;
|
|
34
|
+
}
|
|
35
|
+
/** 生成 ffmpeg 滤镜串(导出供测试) */
|
|
36
|
+
function gifFilter(width, fps) {
|
|
37
|
+
const w = Math.max(120, Math.min(1080, Math.round(width)));
|
|
38
|
+
const f = Math.max(5, Math.min(30, Math.round(fps)));
|
|
39
|
+
return `fps=${f},scale=${w}:-2:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer`;
|
|
40
|
+
}
|
|
41
|
+
/** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
|
|
42
|
+
function gifDuration(durationSec, maxDurationSec) {
|
|
43
|
+
const max = Math.max(1, Math.min(60, maxDurationSec));
|
|
44
|
+
if (!durationSec || durationSec <= 0)
|
|
45
|
+
return max;
|
|
46
|
+
return Math.max(1, Math.min(max, Math.round(durationSec)));
|
|
47
|
+
}
|
|
48
|
+
function runFfmpeg(input, vf, durationSec) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const child = (0, child_process_1.spawn)(resolveFfmpeg(), [
|
|
51
|
+
'-hide_banner', '-loglevel', 'error',
|
|
52
|
+
'-i', 'pipe:0',
|
|
53
|
+
'-t', String(durationSec),
|
|
54
|
+
'-vf', vf,
|
|
55
|
+
'-f', 'gif', 'pipe:1',
|
|
56
|
+
]);
|
|
57
|
+
const out = [];
|
|
58
|
+
let err = '';
|
|
59
|
+
child.stdout.on('data', (d) => out.push(d));
|
|
60
|
+
child.stderr.on('data', (d) => { err += d.toString(); });
|
|
61
|
+
child.on('error', (e) => {
|
|
62
|
+
if (!ffmpegMissingWarned) {
|
|
63
|
+
ffmpegMissingWarned = true;
|
|
64
|
+
logger_1.logger.warn(`GIF 转换需要 ffmpeg,但二进制不可用(动图将回退为视频发送):${e.message}。请安装 optionalDependency ffmpeg-static,或在 PATH 中提供 ffmpeg。`);
|
|
65
|
+
}
|
|
66
|
+
resolve(null);
|
|
67
|
+
});
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
if (code === 0 && out.length) {
|
|
70
|
+
const gif = Buffer.concat(out);
|
|
71
|
+
logger_1.logger.info(`动图已转 GIF 发送:${Math.round(gif.length / 1024)}KB(源视频 ${Math.round(input.length / 1024)}KB)`);
|
|
72
|
+
resolve(gif);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger_1.logger.info(`动图转 GIF 失败,回退发送原视频(ffmpeg 退出码 ${code}${err ? ':' + err.slice(0, 120) : ''})`);
|
|
76
|
+
resolve(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
child.stdin.on('error', () => { });
|
|
80
|
+
child.stdin.end(input);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** 下载视频并转 GIF;任何失败返回 null */
|
|
84
|
+
async function mp4ToGif(rt, url, durationSec, opts) {
|
|
85
|
+
try {
|
|
86
|
+
// X 媒体 CDN (video.twimg.com) 需要浏览器级 UA + Referer 才不会 403(与 vault 下载一致)
|
|
87
|
+
const res = await rt.http.get(url, {
|
|
88
|
+
responseType: 'arraybuffer',
|
|
89
|
+
timeout: Math.min(rt.config.timeout || 60000, 120000),
|
|
90
|
+
headers: {
|
|
91
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
92
|
+
'Referer': 'https://twitter.com/',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const input = Buffer.from(res.data);
|
|
96
|
+
if (!input.length)
|
|
97
|
+
return null;
|
|
98
|
+
return await runFfmpeg(input, gifFilter(opts.maxWidth, opts.fps), gifDuration(durationSec, opts.maxDurationSec));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
logger_1.logger.info(`动图 GIF 转换跳过(源视频下载失败,回退发送原视频):${e?.message || e}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 图片尺寸探测(无依赖):PNG / GIF / JPEG / WebP 头部解析。
|
|
4
|
+
* 仅供同源切图识别用:只读宽高,不解码像素。
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.probeImageSize = probeImageSize;
|
|
8
|
+
/** 探测图片缓冲区尺寸;无法识别返回 null */
|
|
9
|
+
function probeImageSize(buf) {
|
|
10
|
+
if (!buf || buf.length < 12)
|
|
11
|
+
return null;
|
|
12
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A,IHDR 宽高在 16..24
|
|
13
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
14
|
+
if (buf.length < 24)
|
|
15
|
+
return null;
|
|
16
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
17
|
+
}
|
|
18
|
+
// GIF: GIF8
|
|
19
|
+
if (buf.toString('ascii', 0, 4) === 'GIF8') {
|
|
20
|
+
if (buf.length < 10)
|
|
21
|
+
return null;
|
|
22
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
23
|
+
}
|
|
24
|
+
// JPEG: FF D8,扫描 SOF0/1/2/...(C0-CF,除 C4/C8/CC)段
|
|
25
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
26
|
+
let off = 2;
|
|
27
|
+
while (off + 9 < buf.length) {
|
|
28
|
+
if (buf[off] !== 0xff) {
|
|
29
|
+
off++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const marker = buf[off + 1];
|
|
33
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
34
|
+
const height = buf.readUInt16BE(off + 5);
|
|
35
|
+
const width = buf.readUInt16BE(off + 7);
|
|
36
|
+
if (width && height)
|
|
37
|
+
return { width, height };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {
|
|
41
|
+
off += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const segLen = buf.readUInt16BE(off + 2);
|
|
45
|
+
if (segLen < 2)
|
|
46
|
+
return null;
|
|
47
|
+
off += 2 + segLen;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// WebP: RIFF....WEBP
|
|
52
|
+
if (buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
53
|
+
const fourcc = buf.toString('ascii', 12, 16);
|
|
54
|
+
if (fourcc === 'VP8 ' && buf.length >= 30) {
|
|
55
|
+
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
56
|
+
}
|
|
57
|
+
if (fourcc === 'VP8L' && buf.length >= 25) {
|
|
58
|
+
const b = buf.readUInt32LE(21);
|
|
59
|
+
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
60
|
+
}
|
|
61
|
+
if (fourcc === 'VP8X' && buf.length >= 30) {
|
|
62
|
+
const w = 1 + (buf[24] | (buf[25] << 8) | (buf[26] << 16));
|
|
63
|
+
const h = 1 + (buf[27] | (buf[28] << 8) | (buf[29] << 16));
|
|
64
|
+
return { width: w, height: h };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ParserRuntime } from '../engine/runtime';
|
|
2
|
+
import { type ImageSize } from './image-size';
|
|
3
|
+
export type MergeLayout = {
|
|
4
|
+
kind: 'grid';
|
|
5
|
+
cols: number;
|
|
6
|
+
rows: number;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'v';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'h';
|
|
11
|
+
};
|
|
12
|
+
/** 仅凭数量与尺寸给出全部可行布局(内容验证前) */
|
|
13
|
+
export declare function candidateLayouts(n: number, sizes: ImageSize[]): MergeLayout[];
|
|
14
|
+
export interface SeamVerdict {
|
|
15
|
+
/** false = 接缝不可验证(边界带平坦,无纹理可判)或不连续 */
|
|
16
|
+
ok: boolean;
|
|
17
|
+
score: number;
|
|
18
|
+
/** 证据明细(供 debug 日志) */
|
|
19
|
+
dir: 'V' | 'H';
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
/** 边界带高频纹理能量(两侧最大值;低于 TEXTURE_MIN 即不可验证) */
|
|
23
|
+
texture: number;
|
|
24
|
+
/** 平滑后接缝差 */
|
|
25
|
+
trendMAD: number;
|
|
26
|
+
/** 片内自然步进基线 */
|
|
27
|
+
baseline: number;
|
|
28
|
+
/** 结论文案 */
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
/** 内容验证:所有接缝(趋势+纹理)均通过才成立;返回最大接缝比值与逐缝证据 */
|
|
32
|
+
export declare function verifyLayout(layout: MergeLayout, grays: Buffer[]): {
|
|
33
|
+
pass: boolean;
|
|
34
|
+
score: number;
|
|
35
|
+
seams: SeamVerdict[];
|
|
36
|
+
};
|
|
37
|
+
/** 内容识别:候选布局逐一经接缝连续性验证,取通过者中比值最小的一个 */
|
|
38
|
+
export declare function detectMergeLayout(sizes: ImageSize[], grays: Buffer[]): MergeLayout | null;
|
|
39
|
+
/** 布局名(日志用) */
|
|
40
|
+
export declare function layoutName(layout: MergeLayout): string;
|
|
41
|
+
/**
|
|
42
|
+
* 候选布局逐一验证并产出证据链;返回通过者中证据最强(worst 接缝比值最小)的一个。
|
|
43
|
+
* 每次调用都会把逐候选、逐接缝的判定依据写入 debug 日志(含尺寸与灰度行数)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function pickMergeLayout(sizes: ImageSize[], grays: Buffer[]): {
|
|
46
|
+
layout: MergeLayout;
|
|
47
|
+
score: number;
|
|
48
|
+
seams: SeamVerdict[];
|
|
49
|
+
} | null;
|
|
50
|
+
/** xstack 均匀网格布局串(各片已统一缩放为 w×h,行优先) */
|
|
51
|
+
export declare function xstackLayout(cols: number, rows: number, w: number, h: number): string;
|
|
52
|
+
/** 生成 ffmpeg filter_complex(输出标签恒为 [out],导出供测试) */
|
|
53
|
+
export declare function buildMergeFilter(n: number, layout: MergeLayout, sizes: ImageSize[]): string;
|
|
54
|
+
/**
|
|
55
|
+
* 下载 → 灰度解码 → 内容识别接缝 → 合并。
|
|
56
|
+
* 不构成母图切片(接缝不连续)或任一环节失败 → null。
|
|
57
|
+
*/
|
|
58
|
+
export declare function mergeImages(rt: ParserRuntime, urls: string[]): Promise<{
|
|
59
|
+
buffer: Buffer;
|
|
60
|
+
layout: MergeLayout;
|
|
61
|
+
} | null>;
|