すくらっぷ あんど びるどー(したい)

日々やった事のメモとかまとめ。

デイリーポータルZの疑似ライブ配信記事が面白かったのでPyQt6とWeb技術で作る透過デスクトップHUDを作った

デイリーポータルZの「AIがコメントしてくれる擬似ライブ配信をすると作業が楽しくなる」を読んでAIと一緒にゲームがやりたくなった。

dailyportalz.jp

僕がやりたかったのは配信ではなく、リスナー側である。(異常者)
ようはゲームをするときにAIと一緒にゲームできねーかなと思っていたのである。これは結構前から思っていて、実際にGeminiにコメントしながらやってたりしたことあるんだけど、なかなかめんどくさい。
それもあってどうにかコメントを作成と反応するようなのがやりたかった。それにこれがきたので、AIに作らせたらいけるんじゃないか……!? と思ったのでやったのである。ジョバンニが一晩でやって……くれませんでした!!!!

デバックの方が長かった。

1. プロジェクトの技術スタック

  • バックエンド / ウィンドウ制御: Python 3.9, PyQt6 (QWebEngineView)
  • フロントエンド (UI描画): HTML5, CSS3 (Glassmorphism), Vanilla JavaScript
  • AIエンジン / 通信: Ollama (Local LLM), FastAPI, WebSockets
  • OS環境: Windows 11

2. 構想:透明なHUDをドラッグで動かす

HTML側に「ドラッグ用ハンドル」となる要素(div)を配置し、以下のフローでウィンドウを動かす設計。

  1. JS側: ユーザーがHTML要素のハンドルをドラッグした際の移動量(差分ピクセル: dx, dy)を計算する。
  2. 通信: JSからPythonへ、その移動度合い(dx, dy)をリアルタイムに送信する。
  3. Python側: 受け取った差分をもとに、PyQtの move() メソッドで実際のウィンドウ座標を更新する。

これの前はそもそも移動させないようにしていたが、それだと面白みがないので追加してもらった。

3. トラブル1:QWebChannelの厳密すぎる型チェック

JavaScriptから送った移動座標の数値が、Python側に届かずに「無音で消失」してしまう事象が多発したのです。

【原因】 QWebChannel は内部の型チェックが非常に厳格に行われます。JavaScriptの数値は基本的にすべて「浮動小数点数(Float)」として扱われますが、Python側のスロット関数(受信口)が int 型を要求していたり、OSのスケーリングによって小数点以下のピクセルが発生したりすると、型の不一致によりメソッド呼び出しが静かに破棄されてしまいます。

【解決策:カスタムURLインターセプト方式への移行】 型の押し付け合いを避け、より原始的で確実な通信手段に切り替えました。それは「URLのインターセプト」です。

JS側に非表示の iframe を用意し、移動イベントが発生するたびに独自スキーム(例: laso://drag?dx=-5&dy=10)のURLへ遷移させようとします。

/* JavaScript側の実装 (script_v3.js) */
const dragIframe = document.createElement('iframe');
dragIframe.style.display = 'none';
document.body.appendChild(dragIframe);

dragHandle.addEventListener('pointermove', (e) => {
    if (!isMoving) return;
    const dx = e.screenX - startX;
    const dy = e.screenY - startY;
    if (dx !== 0 || dy !== 0) {
        // カスタムURLへ遷移を試みる
        dragIframe.src = `laso://drag?dx=${dx}&dy=${dy}`;
        startX = e.screenX;
        startY = e.screenY;
    }
});

そしてPython側では、QWebEnginePageacceptNavigationRequest メソッドをオーバーライドし、この独自スキームのURLへの遷移要求を傍受(インターセプト)します。

/* Python側の実装 (overlay.py) */
from PyQt6.QtWebEngineCore import QWebEnginePage

class WebPage(QWebEnginePage):
    def __init__(self, parent_window):
        super().__init__()
        self.window = parent_window

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        url_str = url.toString()
        if url_str.startswith("laso://drag"):
            # URLからクエリパラメータ (dx, dy) をパース
            query = QUrlQuery(url)
            try:
                dx = float(query.queryItemValue("dx"))
                dy = float(query.queryItemValue("dy"))
                # ウィンドウの現在位置に加算して移動
                current_pos = self.window.pos()
                self.window.move(int(current_pos.x() + dx), int(current_pos.y() + dy))
            except ValueError:
                pass
            return False # 実際のページ遷移はブロックする
        return super().acceptNavigationRequest(url, _type, isMainFrame)

この手法はすべてのデータがURL文字列として伝達されるため、型の不一致によるロストが起きず、非常に高速かつ安定したドラッグ移動を実現できました。

まるで他の事では失敗していないかのようにいっているが、その前にPython側で失敗が多かった。

  1. ログを全て保存する形式だったのでそれによってメモリが圧迫されてメモリが不足していますみたいなことになる。 
  2. そもそも初めはOBSのブラウザソースを使用してゲームをのせてコメントするといった形にしようとしていた。
  3. ポートが通らない。
  4. ポートを取らない方法にしたとたんに、コメントが反映されなくなる。
  5. 仕方なしにポート番号を変更して対処した途端、今度はAIのコメントが反映されない。
  6. OBS方式をやめてコメントだけに集中した。

これで初めてこうなったのである。

4. トラブル2:Flexboxレイアウトによる要素の消失

通信が安定した後、別の問題が発生しました。チャットのメッセージが増えると、画面上部に配置していたはずの「ドラッグハンドル」が画面外(ウィンドウのさらに上)へ押し出され、掴めなくなってしまったのです。

初めはそもそもドラッグハンドルが別枠だったんだけどデバックしている内に変更された。結構こういうことがある。

【原因】 CSSのFlexboxを用いて、チャットメッセージが下から上へ積み上がるように justify-content: flex-end; を設定していました。ドラッグハンドルも同じコンテナ内にあったため、下からメッセージが供給されるたびに、ハンドル自身も上へ上へと追いやられていました。

【解決策:絶対位置(absolute)による固定】 ドラッグハンドルは、コンテンツの量に関わらず常にウィンドウの同一座標に存在すべきです。したがって、関係するコンテナから要素を分離し、position: absolute; を用いてウィンドウの最上部に固定しました。

/* CSS側の実装 (style.css) */
#drag-handle {
    position: absolute;
    top: 20px;
    left: 20px;
    right: 20px;
    z-index: 9999;
    background: rgba(255, 255, 255, 0.1);
    /* その他装飾... */
}

これにより、どれだけ大量のチャットが流れてきても、ドラッグハンドルは透過ウィンドウの最上部に固定され続け、いつでも確実にHUDを移動させることができるようになりました。

まるですんなりいったようだが実際はちがくて……。
UIの外見とかは直ぐにできたんですけど、それを毎回デバックごとにいじるからおかしなことになる、みたいなことが発生していた。それはPythonの調査しているときの動作だったんだが、UIの作り込み時にもそれが発生してこうなったのである。触らないでくれ、とかでも偶に自動で流れ作業みたいにやることがある。なのでタスクを順番にやってくれといってからマシにはなった。

5. 実際の使用感

AIの感性というか、画面をみて判断するようになっているが、状況説明になりがちである。
仕様なのか言葉の使い方が怪しい。あと初めは画面をすべて把握するようにしていたから、状況説明をするときに外で使った場合はまずい情報をコメントするみたいなことがあった。それもあって選択できるように変更したが、それでも変な事をいう。昔やったゲームのキャラクターみたいだ。これもOllamaの味かもしれない。

1.わちゃわちゃする  

ものすごいわちゃわちゃしだす。

ユーザー側もコメントを打てるようにしてある。これで疑似配信を楽しめるような感覚である。
僕は配信をしたいのではなく、コメントをしあいながらわちゃわちゃするのがやりたいんだ……!

ヒフミーーーーーー!!!!!(異常者)

6. まとめ

AIによって異常者のレベルのがあった!

デイリーポータルZ……! どうしてくれるんだ……!

dailyportalz.jp

責任取れ……!!!(自業自得である)