IT-WEB -- MCタイチ

前の記事では、PCキーボードのUp(上矢印)/Down(下矢印)キーを音声で操作する方法を紹介しましたが。今回は、音声でマウスポインターを任意の場所に移動させクリックする方法を紹介します。

上図のように、Up/Downキーでプリセットを切り替えられない音源の場合、プリセット名の右にある極小の上下矢印をマウスでクリックする必要があります。これはUp/Downキーを押すより遥かに面倒な操作です。

そこで「次」と唱えるだけで、下矢印の上にマウスポインタを移動させクリックもしようという話です。しかも、プラグインのウィンドが何処にあっても、その中のターゲットに確実にヒットしなければなりません。そんな事が出来るのか?と思いましたが、結果的には出来ましたw

マウスポインタの座標を計測する

このアクロバティックな機能を実装する為には、先ずプラグイン画面の左上の隅からクリックしたい位置までの相対座標を計測する必要があります。やり方は、マウスポインタの現在位置を表示する下のスクリプトを実行します。

Python
import pyautogui
import time

print("マウスを移動させてください。5秒後に現在の座標を表示します。")
time.sleep(5)
# マウスカーソルの現在のX, Y座標を表示する
print(pyautogui.displayMousePosition()) 
Python

そして、プラグインウィンド(タイトルが書かれた白い帯を含む)の左上隅にマウスポインタを持っていき、その時Thonnyに表示された座標(下図の黄色いマーカー部分)を書き留めます。次に、プリセット名の左にある矢印(とりあえず下矢印)にポインタを合わせてその時の座標を書き留めます。するとこの2つの座標の差が、ウィンド上の下矢印の座標という訳です。

音声でマウスポインタを飛ばす

次はいよいよ音声でマウスポインタを移動させます。以下は完成したコードですが、先ず冒頭で読み込まれているライブラリをThonnyで予めインストールします。

そして27行目付近の”OFFSET_X = 585 OFFSET_Y = 50”の部分に先程計測したX,Y座標の相対値を入れます(Y座標は数値が増える程下に移動します)。尚、このスクリプトではその下にReaktor用のオフセット座標(但しアンサンブルによって全然違う)も設定してあるので、この値とプラグイン名を変えれば好きなプラグインを操作できます。

つまりスクリプトがウィンドのタイトル(OberhausenなりReaktorなり)を探し、そのプラグインの指定された座標にマウスポインタを飛ばすわけです。もし何れのタイトルも見当たらない場合は、PCキーボードのUp/Downキーを押すという流れです。

Python
import json
import queue
import sounddevice as sd
from vosk import Model, KaldiRecognizer
import pyautogui
import time
import pygetwindow as gw

# マウス暴走時のフェイルセーフ
pyautogui.FAILSAFE = True

# --- 設定 ---
model = Model("model") 
device_info = sd.query_devices(None, 'input')
samplerate = int(device_info['default_samplerate'])
q = queue.Queue()

def callback(indata, frames, time, status):
    q.put(bytes(indata))

words_limit = '["次", "前", "[unk]"]'

INTERVAL = 1.0 
last_action_time = 0

# Oberhausen用のオフセット座標
OFFSET_X = 585
OFFSET_Y = 50
PREV_OFFSET_X = 585
PREV_OFFSET_Y = 40

# Reaktor用のオフセット座標
ReOFFSET_X = 752
ReOFFSET_Y = 55
RePREV_OFFSET_X = 752
RePREV_OFFSET_Y = 45

def click_on_window(window_title, off_x, off_y):
    """指定されたタイトルのウィンドウがあれば座標をクリック"""
    target_windows = gw.getWindowsWithTitle(window_title)
    if target_windows:
        win = target_windows[0]
        click_x = win.left + off_x
        click_y = win.top + off_y
        pyautogui.click(click_x, click_y)
        return True
    return False

# --- メイン実行ループ ---
with sd.RawInputStream(samplerate=samplerate, blocksize=8000, device=None,
                        dtype='int16', channels=1, callback=callback):
    
    rec = KaldiRecognizer(model, samplerate, words_limit)
    print("音声操作実行中(ハイブリッド・上下キーモード)...")

    while True:
        data = q.get()
        current_time = time.time()

        if rec.AcceptWaveform(data):
            rec.Result() 
        else:
            partial = json.loads(rec.PartialResult())
            p_text = partial.get("partial", "").replace(" ", "")
            
            if ("次" in p_text or "前" in p_text) and (current_time - last_action_time > INTERVAL):
                
                if "次" in p_text:
                    if click_on_window('oberhausen', OFFSET_X, OFFSET_Y):
                        print("Oberhausen: 次の音色をクリック")
                    elif click_on_window('Reaktor', ReOFFSET_X, ReOFFSET_Y):
                        print("Reaktor: 次の音色をクリック")
                    else:
                        # --- キーボード操作を [下] に変更 ---
                        pyautogui.press('down')
                        print("キーボード操作: [下] (Next) を送信")
                
                elif "前" in p_text:
                    if click_on_window('oberhausen', PREV_OFFSET_X, PREV_OFFSET_Y):
                        print("Oberhausen: 前の音色をクリック")
                    elif click_on_window('Reaktor', RePREV_OFFSET_X, RePREV_OFFSET_Y):
                        print("Reaktor: 前の音色をクリック")
                    else:
                        # --- キーボード操作を [上] に変更 ---
                        pyautogui.press('up')
                        print("キーボード操作: [上] (Prev) を送信")

                last_action_time = current_time
                time.sleep(0.1)
                rec.Reset()
Python

では、指定したプラグインを表示させた状態で、上のスクリプトを実行してみてください。「次」の声でポインタは目的の場所に飛んだでしょうか?…多分、最初は多少ズレると思いますが、そこまで来たらしめたもの。後はピクセル単位でオフセット値を微調整していけば良いのです。そうして下矢印にヒットしたら次は上矢印。X座標(PREV_OFFSET_X)は同じなので、Y座標(PREV_OFFSET_Y)の値を少し引いていけばそのうちヒットするでしょうw

ソフト音源の膨大なプリセットをチェックするときに、MIDIキーボードから手を放さずに音声で順送りしたいと思ったことはないでしょうか?私はあります。そこでAIの助けを借りてやったら、意外と簡単にできたので紹介します。勿論、ソフト音源以外のアプリ(メーラーや表計算など)でも使えます。

尚、本記事で解説するのはPCキーボードのUp(上矢印)/Down(下矢印)キーを音声で操作する方法です。これとは別に、音声でマウスを操作し、画面内の上下矢印キーを操作する方法については、別の記事で紹介します。

Pythonスクリプト

もし、Pythonを使ったことが無いという人は、ThonyというIDEを使うのがお勧めです。ここからダウンロードしてインストール(Pythonプロブラム本体も同時にインストールされる)。エディット画面に下のコードをコピペして実行するだけ。おっと、ライブラリのインストールを忘れてました。Thonyウインド上部のメニューのツール>パッケージを管理 で子画面を出して、コード内で”import ○○”と書かれているライブラリを検索して全てインストールしてください。

今やプログラミングには欠かせなくなったAI(ここではGemini)ですが、漠然と願いを伝えるだけだと、時々思わぬ迷宮に案内してくれるので、今回はPCベースのPythonスクリプトを指定しました。それでも最初は、クラウドベースの音声ライブラリーを勧められ、動いたものの案の定モッサリです。そこでもっと早い方法はないかと尋ねたところ、Voskというプリロードタイプのライブラリを勧められました。

もっとも僕としては、予め録音した自分の声と照合した方が早いと思っていたので、それを提案したところ「それはとても合理的な発想です」としながらも、やはり既存の音声ライブラリの方が早いとの事。理由は丁寧に説明してくれたものの納得出来ず暫く食い下がりましたが、ここで時間を食うよりもまずはAIのおすすめをやってみる事にしました。

で結論ですが、以下のコードで遅延も感じられず、ちゃんと動きました。「次(つぎ)」という音声に対してDownキー、「前(まえ)」という音声に対して、Upを押す設定です。

尚、当初のコードでは2段飛びが起きたので、少しチューニングしてあります。「まーえー」と音を伸ばしたりせず、短めに話した方がうまく行くと思います。もっとも、使う人の声や使用環境、マイクやマイキングによっては、”INTERVAL”や”last_action_time”の値を変更した方がうまく行くかもしれません。

Python
import json
import queue
import sounddevice as sd
from vosk import Model, KaldiRecognizer
import pyautogui
import time

# --- 設定 ---
# 1. Voskのモデルディレクトリを指定(既存の"model"フォルダを利用)
model = Model(r"C:\<path>\model") 

# 2. マイク設定
device_info = sd.query_devices(None, 'input')
samplerate = int(device_info['default_samplerate'])
q = queue.Queue()

def callback(indata, frames, time, status):
    q.put(bytes(indata))

# 3. 認識する言葉を最小限に絞って精度と速度を向上
words_limit = '["次", "前", "[unk]"]'
INTERVAL = 0.8  # 操作の間隔(0.5秒)
last_action_time = 0

# --- メイン処理 ---
with sd.RawInputStream(samplerate=samplerate, blocksize=8000, device=None,
                        dtype='int16', channels=1, callback=callback):
    
    rec = KaldiRecognizer(model, samplerate, words_limit)
    print(">>> 閲覧モード起動中(声でスクロール)")
    print(">>> 「次」で下へ、「前」で上へ移動します。")

    while True:
        data = q.get()
        current_time = time.time()

        if rec.AcceptWaveform(data):
            # 確定結果(今回は不使用)
            rec.Result() 
        else:
            # 認識途中の結果を取得(反応速度を優先)
            partial = json.loads(rec.PartialResult())
            p_text = partial.get("partial", "").replace(" ", "")
            
            if (current_time - last_action_time > INTERVAL):
                if "次" in p_text:
                    pyautogui.press('down')
                    print("Action: [下] キー送信")
                    last_action_time = current_time
                    rec.Reset() # 連続反応を防ぐためリセット

                elif "前" in p_text:
                    pyautogui.press('up')
                    print("Action: [上] キー送信")
                    last_action_time = current_time
                    rec.Reset()
Python

これに加えて、スペースキーを操作するコードが下記の通りです。命令文は「スペース」です。

Python
import json
import queue
import sounddevice as sd
from vosk import Model, KaldiRecognizer
import pyautogui
import time

# --- 設定 ---
model = Model(r"C:\<path>\model") 

device_info = sd.query_devices(None, 'input')
samplerate = int(device_info['default_samplerate'])
q = queue.Queue()

def callback(indata, frames, time, status):
    q.put(bytes(indata))

# 3. 認識対象に「スペース」を追加
words_limit = '["次", "前", "スペース", "[unk]"]'
INTERVAL = 0.8  
last_action_time = 0

with sd.RawInputStream(samplerate=samplerate, blocksize=8000, device=None,
                        dtype='int16', channels=1, callback=callback):
    
    rec = KaldiRecognizer(model, samplerate, words_limit)
    print(">>> 閲覧モード起動中")
    print(">>> 「次」:下 / 「前」:上 / 「スペース」:スペースキー")

    while True:
        data = q.get()
        current_time = time.time()

        if rec.AcceptWaveform(data):
            rec.Result() 
        else:
            partial = json.loads(rec.PartialResult())
            p_text = partial.get("partial", "").replace(" ", "")
            
            if (current_time - last_action_time > INTERVAL):
                if "次" in p_text:
                    pyautogui.press('down')
                    print("Action: [下] キー")
                    last_action_time = current_time
                    rec.Reset()

                elif "前" in p_text:
                    pyautogui.press('up')
                    print("Action: [上] キー")
                    last_action_time = current_time
                    rec.Reset()

                # --- 「スペース」という言葉に反応 ---
                elif "スペース" in p_text:
                    pyautogui.press('space')
                    print("Action: [スペース] キー")
                    last_action_time = current_time
                    rec.Reset()
Python

この「スペース」命令を追加したのは、寝そべった状態でYoutube動画を再生/停止したかったからです。しかし実際にやってみると、何も言ってないのに時々勝手に停止/再生します。これは多分、動画内の会話に反応してるのだろうと思ったので、取りあえず会話音が出るアプリでの使用はやめました。気が向いたらまたAIに相談して、対策するかもしれません。