缩略图二分定位 + 邻域采样

B. 二分定位 + 邻域采样(我推荐)
把配置的语义从"这就是要生成的 10 档"改成"允许搜索的参数梯度"(比如分辨率梯度、帧率梯度、质量梯度三张表)。运行时:
1. 用每个组合只编 2 帧的方式粗估体积(很便宜);
2. 靠单调性做二分,几轮就找到"体积刚好不超过目标"的参数点;
3. 在这个点周围取 10 个组合(略高、略低各几个),完整生成。
成本大概是十几次估算编码 + 10 档完整编码,和现在基本持平甚至更省(估算只编 2 帧)。效果是候选真正围绕你输入的目标分布——比如你填 30 KB,出来的就是 22、26、29、31、35 KB 这种,而不是 5 KB 和 300 KB 各一个。
代价是你现在这套配置格式要推翻重做——从"精确档位表"变成"参数范围表"
This commit is contained in:
Yeomai committed 2026-09-20 00:39:38 +08:00
1 parent 5808e3dfce
commit e94dfb9e35
27 files changed
+710 -299

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
'use strict';
/**
* 动态缩略图的档位配置。
*
* 每项是 [细节分辨率, 帧率, 编码质量]:
* - 细节分辨率决定画面保留多少细节(输出画布始终 400×400,小分辨率会被放大填满);
* - 帧率决定每秒采样多少帧,只改采样密度不改时间轴,每档的帧延迟会自动重算;
* - 编码质量是 0–1 的 WebP 质量,APNG 是无损编码会忽略这一项。
* 数组顺序就是生成顺序,也是画廊里的展示顺序。
*
* 想调整压缩策略,直接改下面的数字即可;增删档位也支持,每档自带质量。
*/
const ANIMATED_PRIORITIES = {
// 分辨率优先:先把帧率压到底,再降分辨率。
resolution: [
[400, 18, 1.0], [400, 15, 0.8], [400, 12, 0.6], [400, 9, 0.4], [400, 6, 0.2],
[200, 18, 1.0], [200, 15, 0.8], [200, 12, 0.6], [200, 9, 0.4], [200, 6, 0.2],
],
// 帧率优先:先把分辨率压到底,再降帧率。
framerate: [
[400, 18, 1.0], [400, 18, 0.5], [350, 15, 1.0], [350, 15, 0.5], [300, 9, 1.0],
[300, 9, 0.5], [250, 6, 1.0], [250, 6, 0.5], [200, 9, 1.0], [200, 6, 0.5],
],
// 交替进行:分辨率和帧率交替下降。
balanced: [
[400, 15, 0.8], [400, 12, 0.6], [400, 9, 0.4], [400, 6, 0.2], [200, 15, 0.8],
[200, 12, 0.6], [200, 9, 0.4], [200, 6, 0.2], [100, 3, 0.5], [100, 3, 0.1],
],
};
const DEFAULT_PRIORITY = 'resolution';
function animatedProfiles(priority) {
const plan = ANIMATED_PRIORITIES[priority] || ANIMATED_PRIORITIES[DEFAULT_PRIORITY];
return plan.map(([size, fps, quality]) => ({ size, fps, quality }));
}
module.exports = { ANIMATED_PRIORITIES, DEFAULT_PRIORITY, animatedProfiles };
+1
View File
@@ -16,6 +16,7 @@ function chunk(type, payload) {
}
function frameData(webp) {
if (!webp) throw new Error('WebP 帧数据缺失');
const input = Buffer.from(webp);
if (input.length < 20 || input.toString('ascii', 0, 4) !== 'RIFF' || input.toString('ascii', 8, 12) !== 'WEBP') {
throw new Error('Invalid WebP frame');
+2
View File
@@ -22,6 +22,8 @@ function orderCurrentTargetLast(targets, currentSourcePath) {
*/
async function applyThumbnailTargets(options) {
const targets = options.targets || [];
// 渲染结果为空时立刻报清楚的原因,否则会在写临时文件时才抛出难懂的参数类型错误。
if (!options.rendered || !Buffer.isBuffer(options.rendered.buffer)) throw new Error('缩略图渲染结果为空');
const settleDelayMs = options.settleDelayMs === undefined ? 800 : options.settleDelayMs;
const cleanupDelayMs = options.cleanupDelayMs === undefined ? 1200 : options.cleanupDelayMs;
const retryAttempts = options.retryAttempts === undefined ? 3 : options.retryAttempts;