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.
Files changed (2) hide show
  1. package/cli.cjs +110 -50
  2. 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.5; // seconds - consecutive changes within this are grouped
43
- const SCENE_THRESHOLD = options.sceneThreshold || 0.005; // Lower default threshold (was 0.01) to catch more subtle changes
44
- const MIN_INTERVAL = 1.0; // Minimum interval for additional keyframes (seconds)
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
- // If we have very few thumbnails for a long video, add interval-based ones
155
- if (videoDuration > 3 && allThumbnails.length < 3) {
156
- // Extract frames at regular intervals
157
- const intervalCount = Math.min(5, Math.floor(videoDuration / MIN_INTERVAL));
158
- for (let i = 1; i < intervalCount; i++) {
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 hasNearby = allThumbnails.some(t => Math.abs(t.time - time) < STABILIZATION_THRESHOLD);
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 ![](path/to/file)`);
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 ![](path/to/file)`
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>サムネイル数の調整</h4>
4524
- <p class="video-settings-desc">重要シーンの見逃しを防ぐため、サムネイル数を調整できます</p>
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.05" data-stab="1.0">少なめ</button>
4527
- <button data-scene="0.02" data-stab="0.8">やや少</button>
4528
- <button data-scene="0.005" data-stab="0.5" class="selected">標準</button>
4529
- <button data-scene="0.002" data-stab="0.3">やや多</button>
4530
- <button data-scene="0.001" data-stab="0.2">多め</button>
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
- <div class="video-settings-actions">
4533
- <button class="regenerate-btn" id="video-settings-regenerate">この設定で再生成</button>
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.005;
6802
- const DEFAULT_STABILIZATION_THRESHOLD = 0.5;
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 regenerateBtn = document.getElementById('video-settings-regenerate');
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 5-level button clicks
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
- // Regenerate timeline with selected settings
7272
- if (regenerateBtn) {
7273
- regenerateBtn.addEventListener('click', () => {
7274
- if (!currentVideoPath || !currentVideo) return;
7275
-
7276
- // Get selected button's values
7277
- const selectedBtn = sceneButtons?.querySelector('button.selected');
7278
- if (selectedBtn) {
7279
- currentSceneThreshold = parseFloat(selectedBtn.dataset.scene) || DEFAULT_SCENE_THRESHOLD;
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.005;
8615
- const stabilizationThreshold = parseFloat(urlParams.searchParams.get("stabilization")) || 0.5;
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" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {