コンテンツへスキップ
Video2Any
2026-08-22

アルゴリズムのせいに見えた8つのバグ

Video2Any は、ブラウザでフレームをデコードし、直前のフレームと比較して、十分に変化したものを残すことでスライドを見つけます。結果がおかしいとき、まず疑われるのはこの比較処理です。でも、たいていは違います。

一週間で追いかけた8件を並べます。検出器そのものが原因だったのは2件だけ。残りは、消し忘れたガード、嘘をつくテストスクリプト、仮想スクロールのリスト、API 呼び出しとの競合、メモリの上限、そして自分の一番役に立つ部分を捨ててしまうエラーログでした。

8件

1. 押しても何も起きないコントロール

短い動画で Dense を選んでも3枚しか出ず、Default と同じだという報告がありました。しきい値の計算式をしばらく眺めたあとで、いちばん基本的なことを確かめました。その設定、そもそも効いているのか?

効いていませんでした。最初のスイープはファイルごとに一度だけ走るよう ref で守られています。密度を変えるとコールバックは作り直されますが、ガードが effect をそのまま追い返してしまい、実際に再実行できるのは別の通知にあるボタンだけでした。切り替えたあとに見えていたのは前回の結果で、タイムスタンプまで同じです。修正後、同じクリップで Sparse / Default / Dense はそれぞれ 3枚 / 6枚 / 13枚。以前は3つとも6枚でした。

2. テストスクリプトに二度だまされた

おかしな結果を説明するために、Node でパイプラインをオフラインに再現して同じファイルを流しました。ところが本番と食い違い、しかも二度とも向きが逆でした。最初はスクリプトが1枚で本番が6枚、直したら今度はスクリプトが15枚で本番が6枚。

原因は2つ。スクリプトは ffmpeg でグレースケールのフレームを抜いていましたが、ブラウザがエンジンに渡すのは canvas の RGBA で、縮小フィルタの違いだけで較正後のしきい値が 0.618 から 0.650 に動きます。さらに、アクティビティマスクを使うかどうかを決める関数を飛ばしていたので、本番と同じ経路を通っていませんでした。アルゴリズムだけ再現してパイプラインを再現していないスクリプトは、堂々と間違った答えを返してきます。

3. DOM にあるのは画面に映っている分だけ

32分の動画で30分以降のスライドが出ることを確かめるテストがありました。サムネイルのタイムスタンプを全部読んで、最後が 27:00。まさに途中で打ち切られたように見えます。

あのグリッドは仮想スクロールで、実際の要素になっているのは表示中の行だけです。先に最下部までスクロールしてから読むと、本当の最後は 32:00 でした。このアサーションはずっとビューポートを測っていたわけです。

4. ワーカーがそれぞれ勝手に重複を消していた

長い動画は複数のチャンクに分けて並列にスイープします。各ワーカーは自分の担当区間の中で重複した画面を落とします。発表者が前のスライドに戻る、会議が同じ画へ切り替わる、といったものです。ただし互いの結果は見えません。

その結果、繰り返し出てくる画面は、登場したチャンクの数だけ残ってしまいます。29分の会議録画で、9枚のうち3枚が重複でした。さらに厄介なのは、これだと最終結果がチャンク数、つまり視聴者のマシンのコア数に左右されることです。重複判定はメインスレッドに移し、スライドが届いたその場で比較するようにしました。だから重複が一度表示されてから消える、ということも起きません。

5. 正しい設計のほうが結果は悪かった

各チャンクが自分の区間だけでしきい値を較正するのは、どう考えても筋が悪い。動画は1本、分布も1つ、しきい値も1つのはずです。そこでそう書き換えました。ワーカーが測った比率を送り、メインスレッドがまとめて一度だけ較正し、答えを返す。

4チャンクと8チャンクの差は3枚から1枚に縮みました。そして、結果は悪くなりました。同じ29分の会議で、ffmpeg 自身のシーン検出は本物のカットを7つ数えます。チャンクごとの較正+上の重複除去なら6枚、全体較正だと4枚。まとめると分布が広がり、median + 2·MAD が上がり、本物の切り替わりが落ちます。revert しました。設計として筋が通っていても、いま動いているものに負けることはある。そして、それを判断できるのは自分のコードの外に参照があるときだけです。

6. 答えが「いいえ」なのではなく、まだ訊いていなかった

有料プランの方が長い録画をアップロードして、最初の30分しか解析されませんでした。再サンプリングを押すと全部出る。だから検出の問題に見えたのです。

エディタは、相手が誰かを知る前に動き始めます。権限フラグの初期値は false ですが、それは答えが「いいえ」だからではなく、リクエストがまだ飛んでいる最中だからです。長さの上限はその false を読んで、無料プランの天井を当てていました。再サンプリングのころにはプラン情報は届いています。最初はそれを待つように直し、次に待つ必要そのものをなくしました。ページを返す時点でワーカーはセッションを持っているので、プランをドキュメントに書き込んでしまえば、最初のレンダーで分かります。

7. すべてのエラーが「Error」という名前だった

文字起こしは成功より失敗のほうが多く、5対6。そのすべてが「Error」という文字列で記録されていました。19件の抽出失敗も同じです。

コードが記録していたのは err.name で、人が手で作った Error はどれもこの値が「Error」になります。情報が入っている message のほうは、どこにも残っていませんでした。message を記録するようにしたら、文字起こしの失敗に理由が見えました。decodeAudioData はオーディオコンテキストのサンプルレートに合わせてリサンプルするので、ハードウェアの 48 kHz ステレオで55分の講義をデコードすると、まず約630MBの浮動小数点サンプルが確保されます。2時間なら 1.4 GB。最初から 16 kHz のコンテキストでデコードすれば、モデルが受け取る音声は同じままで、メモリはおよそ6分の1です。

8. 速いのに、壊れているように感じる

92分の会議録画は52秒でスイープが終わります。最初のスライドが出るのは、そのうちの50秒目です。

遅い処理はどこにもありません。しきい値はフレーム差分の分布全体から較正するので、動画を最後まで読み終えるまで1枚も選べないのです。そしてその50秒間、画面にあるのはパーセンテージだけでした。ほかに何もない状態で数字だけが動いていると、それは「止まっている」ように読めます。いまはどちらの工程が動いているか、動画のどこまで読んだかを表示します。同じ52秒が、苦情ではなくなりました。

一週間前の自分に言うとしたら

コードを推理する前に、入力がそのコードまで届いているかを確かめること。今回の半分は、設定が届いていない、答えがまだ返っていない、テストが別のものを測っている、のどれかでした。

そして、自分のパイプラインの外に参照を1つ持っておくこと。ffmpeg のシーン検出は我々が作っているものではないし、そうである必要もありません。独立してさえいればいい。あれがなければ、較正の書き直しをそのまま出していたはずです。設計としては良かったので。そして7枚のうち3枚の本物のスライドを、静かに落としていたでしょう。

動画を変換ブログ