reviw 0.19.0 → 0.20.0
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/cli.cjs +110 -50
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -39,9 +39,21 @@ const timelineTempDirs = new Set();
|
|
|
39
39
|
// Uses "stabilization filter" - groups consecutive similar frames and takes the last frame of each group
|
|
40
40
|
function extractVideoTimeline(videoPath, tmpDir, res, onComplete, options = {}) {
|
|
41
41
|
// Use provided thresholds or defaults
|
|
42
|
-
const STABILIZATION_THRESHOLD = options.stabilizationThreshold || 0.
|
|
43
|
-
const SCENE_THRESHOLD = options.sceneThreshold || 0.
|
|
44
|
-
|
|
42
|
+
const STABILIZATION_THRESHOLD = options.stabilizationThreshold || 0.1; // seconds - consecutive changes within this are grouped
|
|
43
|
+
const SCENE_THRESHOLD = options.sceneThreshold || 0.01; // Default matches "標準" button
|
|
44
|
+
// Determine target thumbnail count based on BOTH scene sensitivity and stabilization
|
|
45
|
+
// Scene: lower threshold = more detail = more thumbnails
|
|
46
|
+
const SCENE_BASE = SCENE_THRESHOLD >= 0.1 ? 3
|
|
47
|
+
: SCENE_THRESHOLD >= 0.03 ? 5
|
|
48
|
+
: SCENE_THRESHOLD >= 0.01 ? 8
|
|
49
|
+
: SCENE_THRESHOLD >= 0.003 ? 12
|
|
50
|
+
: 20;
|
|
51
|
+
// Stab: lower threshold = finer granularity = scale up target
|
|
52
|
+
const STAB_FACTOR = STABILIZATION_THRESHOLD >= 0.3 ? 0.6
|
|
53
|
+
: STABILIZATION_THRESHOLD >= 0.1 ? 1.0
|
|
54
|
+
: STABILIZATION_THRESHOLD >= 0.05 ? 1.4
|
|
55
|
+
: 1.8;
|
|
56
|
+
const TARGET_THUMBNAILS = Math.round(SCENE_BASE * STAB_FACTOR);
|
|
45
57
|
|
|
46
58
|
// First, get video duration using ffprobe
|
|
47
59
|
let videoDuration = 0;
|
|
@@ -151,14 +163,16 @@ function extractVideoTimeline(videoPath, tmpDir, res, onComplete, options = {})
|
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
166
|
+
// Fill up to target thumbnail count with interval-based extraction
|
|
167
|
+
// This ensures scene sensitivity actually affects output even for gradient videos
|
|
168
|
+
if (videoDuration > 1 && allThumbnails.length < TARGET_THUMBNAILS) {
|
|
169
|
+
const needed = TARGET_THUMBNAILS - allThumbnails.length;
|
|
170
|
+
const intervalCount = needed + 1;
|
|
171
|
+
for (let i = 1; i <= needed; i++) {
|
|
159
172
|
const time = (videoDuration / intervalCount) * i;
|
|
160
173
|
// Check if we already have a thumbnail close to this time
|
|
161
|
-
const
|
|
174
|
+
const dedupThreshold = Math.min(STABILIZATION_THRESHOLD, 0.1);
|
|
175
|
+
const hasNearby = allThumbnails.some(t => Math.abs(t.time - time) < dedupThreshold);
|
|
162
176
|
if (!hasNearby) {
|
|
163
177
|
const intervalPath = `${tmpDir}/interval_${i}.jpg`;
|
|
164
178
|
const result = spawnSync('ffmpeg', [
|
|
@@ -182,7 +196,7 @@ function extractVideoTimeline(videoPath, tmpDir, res, onComplete, options = {})
|
|
|
182
196
|
// Remove duplicates (same time within threshold)
|
|
183
197
|
const uniqueThumbnails = [];
|
|
184
198
|
for (const thumb of allThumbnails) {
|
|
185
|
-
const isDuplicate = uniqueThumbnails.some(t => Math.abs(t.time - thumb.time) < 0.1);
|
|
199
|
+
const isDuplicate = uniqueThumbnails.some(t => Math.abs(t.time - thumb.time) < Math.min(STABILIZATION_THRESHOLD, 0.1));
|
|
186
200
|
if (!isDuplicate) {
|
|
187
201
|
uniqueThumbnails.push(thumb);
|
|
188
202
|
}
|
|
@@ -539,6 +553,24 @@ function parseCliArgs(argv) {
|
|
|
539
553
|
}
|
|
540
554
|
|
|
541
555
|
// ===== ファイルパス検証・解決関数(require.main時のみ呼ばれる) =====
|
|
556
|
+
// バイナリファイル拡張子(レビュー対象外)
|
|
557
|
+
const BINARY_EXTENSIONS = new Set([
|
|
558
|
+
// 動画
|
|
559
|
+
'.mp4', '.mov', '.webm', '.avi', '.mkv', '.m4v', '.ogv', '.wmv', '.flv',
|
|
560
|
+
// 画像
|
|
561
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.tiff', '.tif',
|
|
562
|
+
// 音声
|
|
563
|
+
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
|
|
564
|
+
// アーカイブ
|
|
565
|
+
'.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar',
|
|
566
|
+
// バイナリ実行
|
|
567
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a',
|
|
568
|
+
// その他バイナリ
|
|
569
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
570
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
571
|
+
'.sqlite', '.db'
|
|
572
|
+
]);
|
|
573
|
+
|
|
542
574
|
function validateAndResolvePaths(filePaths) {
|
|
543
575
|
const resolved = [];
|
|
544
576
|
for (const fp of filePaths) {
|
|
@@ -554,6 +586,13 @@ function validateAndResolvePaths(filePaths) {
|
|
|
554
586
|
console.error(`Please specify a file, not a directory.`);
|
|
555
587
|
process.exit(1);
|
|
556
588
|
}
|
|
589
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
590
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
591
|
+
console.error(`Error: Binary file cannot be reviewed: ${path.basename(resolvedPath)} (${ext})`);
|
|
592
|
+
console.error(`reviw supports: .csv, .tsv, .md, .diff, .patch, and text files.`);
|
|
593
|
+
console.error(`Tip: To review videos/images, embed them in a Markdown file using `);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
557
596
|
resolved.push(resolvedPath);
|
|
558
597
|
}
|
|
559
598
|
return resolved;
|
|
@@ -1076,6 +1115,16 @@ function loadData(filePath) {
|
|
|
1076
1115
|
);
|
|
1077
1116
|
}
|
|
1078
1117
|
const ext = path.extname(filePath).toLowerCase();
|
|
1118
|
+
|
|
1119
|
+
// バイナリファイルはレビュー対象外 - エラーで拒否(validateAndResolvePathsでも弾くが二重チェック)
|
|
1120
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`Binary file cannot be reviewed: ${path.basename(filePath)} (${ext})\n` +
|
|
1123
|
+
`reviw supports: .csv, .tsv, .md, .diff, .patch, and text files.\n` +
|
|
1124
|
+
`Tip: To review videos/images, embed them in a Markdown file using `
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1079
1128
|
if (ext === ".csv" || ext === ".tsv") {
|
|
1080
1129
|
const data = loadCsv(filePath);
|
|
1081
1130
|
return { ...data, mode: "csv" };
|
|
@@ -3571,13 +3620,6 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3571
3620
|
cursor: pointer;
|
|
3572
3621
|
transition: background 120ms ease;
|
|
3573
3622
|
}
|
|
3574
|
-
.video-settings-actions .regenerate-btn {
|
|
3575
|
-
background: #3b82f6;
|
|
3576
|
-
color: #fff;
|
|
3577
|
-
}
|
|
3578
|
-
.video-settings-actions .regenerate-btn:hover {
|
|
3579
|
-
background: #2563eb;
|
|
3580
|
-
}
|
|
3581
3623
|
.video-settings-actions .reset-btn {
|
|
3582
3624
|
background: rgba(255, 255, 255, 0.15);
|
|
3583
3625
|
color: #fff;
|
|
@@ -4520,17 +4562,22 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4520
4562
|
<button class="video-close-btn" id="video-close" aria-label="Close video" title="Close (ESC)">✕</button>
|
|
4521
4563
|
<button class="video-settings-btn" id="video-settings-btn" aria-label="Timeline settings" title="Timeline settings">⚙</button>
|
|
4522
4564
|
<div class="video-settings-panel" id="video-settings-panel">
|
|
4523
|
-
<h4
|
|
4524
|
-
<p class="video-settings-desc"
|
|
4565
|
+
<h4>タイムライン設定</h4>
|
|
4566
|
+
<p class="video-settings-desc">シーン感度: 映像変化の検出しきい値</p>
|
|
4525
4567
|
<div class="video-settings-buttons" id="scene-buttons">
|
|
4526
|
-
<button data-scene="0.
|
|
4527
|
-
<button data-scene="0.
|
|
4528
|
-
<button data-scene="0.
|
|
4529
|
-
<button data-scene="0.
|
|
4530
|
-
<button data-scene="0.001"
|
|
4568
|
+
<button data-scene="0.3">少なめ</button>
|
|
4569
|
+
<button data-scene="0.1">やや少</button>
|
|
4570
|
+
<button data-scene="0.01" class="selected">標準</button>
|
|
4571
|
+
<button data-scene="0.003">やや多</button>
|
|
4572
|
+
<button data-scene="0.001">多め</button>
|
|
4531
4573
|
</div>
|
|
4532
|
-
<
|
|
4533
|
-
|
|
4574
|
+
<p class="video-settings-desc">グルーピング: 連続する変化をまとめる秒数</p>
|
|
4575
|
+
<div class="video-settings-buttons" id="stab-buttons">
|
|
4576
|
+
<button data-stab="0.5">0.5s</button>
|
|
4577
|
+
<button data-stab="0.3">0.3s</button>
|
|
4578
|
+
<button data-stab="0.1" class="selected">0.1s</button>
|
|
4579
|
+
<button data-stab="0.05">0.05s</button>
|
|
4580
|
+
<button data-stab="0.02">0.02s</button>
|
|
4534
4581
|
</div>
|
|
4535
4582
|
</div>
|
|
4536
4583
|
<div class="video-container" id="video-container"></div>
|
|
@@ -6798,8 +6845,8 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6798
6845
|
}
|
|
6799
6846
|
|
|
6800
6847
|
// Default threshold values
|
|
6801
|
-
const DEFAULT_SCENE_THRESHOLD = 0.
|
|
6802
|
-
const DEFAULT_STABILIZATION_THRESHOLD = 0.
|
|
6848
|
+
const DEFAULT_SCENE_THRESHOLD = 0.01;
|
|
6849
|
+
const DEFAULT_STABILIZATION_THRESHOLD = 0.1;
|
|
6803
6850
|
let currentSceneThreshold = DEFAULT_SCENE_THRESHOLD;
|
|
6804
6851
|
let currentStabilizationThreshold = DEFAULT_STABILIZATION_THRESHOLD;
|
|
6805
6852
|
let currentVideoPath = null;
|
|
@@ -7245,7 +7292,27 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7245
7292
|
const settingsBtn = document.getElementById('video-settings-btn');
|
|
7246
7293
|
const settingsPanel = document.getElementById('video-settings-panel');
|
|
7247
7294
|
const sceneButtons = document.getElementById('scene-buttons');
|
|
7248
|
-
const
|
|
7295
|
+
const stabButtons = document.getElementById('stab-buttons');
|
|
7296
|
+
|
|
7297
|
+
// Auto-regenerate function triggered by button clicks
|
|
7298
|
+
function triggerRegenerate() {
|
|
7299
|
+
if (!currentVideoPath || !currentVideo) return;
|
|
7300
|
+
|
|
7301
|
+
const selectedScene = sceneButtons?.querySelector('button.selected');
|
|
7302
|
+
if (selectedScene) {
|
|
7303
|
+
currentSceneThreshold = parseFloat(selectedScene.dataset.scene) || DEFAULT_SCENE_THRESHOLD;
|
|
7304
|
+
}
|
|
7305
|
+
|
|
7306
|
+
const selectedStab = stabButtons?.querySelector('button.selected');
|
|
7307
|
+
if (selectedStab) {
|
|
7308
|
+
currentStabilizationThreshold = parseFloat(selectedStab.dataset.stab) || DEFAULT_STABILIZATION_THRESHOLD;
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
loadVideoTimeline(currentVideoPath, currentVideo, {
|
|
7312
|
+
sceneThreshold: currentSceneThreshold,
|
|
7313
|
+
stabilizationThreshold: currentStabilizationThreshold
|
|
7314
|
+
});
|
|
7315
|
+
}
|
|
7249
7316
|
|
|
7250
7317
|
if (settingsBtn && settingsPanel) {
|
|
7251
7318
|
// Toggle settings panel
|
|
@@ -7257,36 +7324,29 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7257
7324
|
// Prevent panel clicks from closing overlay
|
|
7258
7325
|
settingsPanel.addEventListener('click', (e) => e.stopPropagation());
|
|
7259
7326
|
|
|
7260
|
-
// Handle
|
|
7327
|
+
// Handle scene button clicks (auto-regenerate)
|
|
7261
7328
|
if (sceneButtons) {
|
|
7262
7329
|
const buttons = sceneButtons.querySelectorAll('button');
|
|
7263
7330
|
buttons.forEach(btn => {
|
|
7264
7331
|
btn.addEventListener('click', () => {
|
|
7265
7332
|
buttons.forEach(b => b.classList.remove('selected'));
|
|
7266
7333
|
btn.classList.add('selected');
|
|
7334
|
+
// Auto-regenerate on click
|
|
7335
|
+
triggerRegenerate();
|
|
7267
7336
|
});
|
|
7268
7337
|
});
|
|
7269
7338
|
}
|
|
7270
7339
|
|
|
7271
|
-
//
|
|
7272
|
-
if (
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
currentStabilizationThreshold = parseFloat(selectedBtn.dataset.stab) || DEFAULT_STABILIZATION_THRESHOLD;
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
loadVideoTimeline(currentVideoPath, currentVideo, {
|
|
7284
|
-
sceneThreshold: currentSceneThreshold,
|
|
7285
|
-
stabilizationThreshold: currentStabilizationThreshold
|
|
7340
|
+
// Handle stab button clicks (auto-regenerate)
|
|
7341
|
+
if (stabButtons) {
|
|
7342
|
+
const buttons = stabButtons.querySelectorAll('button');
|
|
7343
|
+
buttons.forEach(btn => {
|
|
7344
|
+
btn.addEventListener('click', () => {
|
|
7345
|
+
buttons.forEach(b => b.classList.remove('selected'));
|
|
7346
|
+
btn.classList.add('selected');
|
|
7347
|
+
// Auto-regenerate on click
|
|
7348
|
+
triggerRegenerate();
|
|
7286
7349
|
});
|
|
7287
|
-
|
|
7288
|
-
// Hide panel after regenerating
|
|
7289
|
-
settingsPanel.classList.remove('visible');
|
|
7290
7350
|
});
|
|
7291
7351
|
}
|
|
7292
7352
|
|
|
@@ -8611,8 +8671,8 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
8611
8671
|
if (req.method === "GET" && req.url.startsWith("/video-timeline?")) {
|
|
8612
8672
|
const urlParams = new URL(req.url, `http://localhost`);
|
|
8613
8673
|
const videoPath = urlParams.searchParams.get("path");
|
|
8614
|
-
const sceneThreshold = parseFloat(urlParams.searchParams.get("scene")) || 0.
|
|
8615
|
-
const stabilizationThreshold = parseFloat(urlParams.searchParams.get("stabilization")) || 0.
|
|
8674
|
+
const sceneThreshold = parseFloat(urlParams.searchParams.get("scene")) || 0.01;
|
|
8675
|
+
const stabilizationThreshold = parseFloat(urlParams.searchParams.get("stabilization")) || 0.1;
|
|
8616
8676
|
|
|
8617
8677
|
if (!videoPath) {
|
|
8618
8678
|
res.writeHead(400, { "Content-Type": "text/plain" });
|