
iRacingを音声操作して最強になろう
OpenAI APIを利用して、iRacingのピットコマンドを音声で操作できるPythonプログラムを作成したので共有します。
こんな感じで動作します。
う、うおぉぉぉぉぉぉ pic.twitter.com/0lixGgFifr
— ellettie (@lh_runner) January 4, 2025
0. はじめに
プログラムを作成するにあたっていくつかの留意事項があります。
OpenAI APIは従量課金制です。利用量に応じて料金を支払う必要があります。(多分行っても月100円くらい)
今回はプッシュトゥトークで音声を送信する仕組みにしています。(google gemini 2.0 flashとかだと、リアルタイムで会話できる方法があるみたいなので、それ使っても面白いかも)
PythonからiRacingにピットコマンドを送信するのに、pyirsdkを使用します。
1. 環境構築
Pythonコードを動かすために、環境構築が必要なので、さらっと紹介します。
PCにPythonをインストールする。https://www.python.org/
※仮想環境を使用することをお勧めします。https://xtech.nikkei.com/atcl/nxt/column/18/02615/102300003/VScode等IDEをインストールする。https://code.visualstudio.com/
ライブラリをインストールする。 https://note.nkmk.me/python-pip-usage/
pip install pyirsdk openai SpeechRecognition pygame
OpenAIでAPIキーを取得する。https://qiita.com/kurata04/items/a10bdc44cc0d1e62dad3
2. 音声を文字起こしする機能を作ろう
音声を文字起こしするのに、SpeechRecognitionというライブラリを使用します。プログラムの先頭でインポートしましょう。
import speech_recognition as sr
次に音声を文字起こしする関数を作成します。
recognizer.recognize_googleの引数languageに日本語を指定します。
def speech2text():
recognizer = sr.Recognizer()
with sr.Microphone() as source:
print("話してください")
audio = recognizer.listen(source)
print("認識中...")
text = recognizer.recognize_google(audio, language="ja-JP") #日本語を指定
print(text)
関数を実行しましょう。
speech2text()
このようになれば成功です。
話してください
認識中...
こんにちは
3. コントローラの設定をしよう
pythonでコントローラを扱うために、pygameを使用します。
使用するコントローラの名前とボタン番号を知っておく必要があるので、チェック用のプログラムを作成しましょう。
import pygame
import time
import json
def check_controller_name():
pygame.init()
pygame.joystick.init()
for i in range(pygame.joystick.get_count()):
joystick = pygame.joystick.Joystick(i)
joystick.init()
print(f"コントローラ : {joystick.get_id()} 名前 : {joystick.get_name()}")
check_controller_name()
JOYSTICK_NAME = input("コントローラ名 : ")
def chack_button_number():
controller_config = {}
pygame.init()
pygame.joystick.init()
for i in range(pygame.joystick.get_count()):
joystick = pygame.joystick.Joystick(i)
joystick.init()
if joystick.get_name() == JOYSTICK_NAME:
joystick_id = i
break
if joystick_id is None:
print(f"コントローラ : {JOYSTICK_NAME} が見つかりません")
return
joystick = pygame.joystick.Joystick(joystick_id)
joystick.init()
print("ボタンを押してください")
while True:
for event in pygame.event.get():
if event.type == pygame.JOYBUTTONDOWN:
print(f"コントローラ : {joystick.get_id()} ボタン : {event.button} が押されました")
controller_config["joystick_name"] = joystick.get_name()
controller_config["button_number"] = event.button
return controller_config
time.sleep(0.01)
controller_config = chack_button_number()
print(controller_config)
with open("controller_config.json", "w") as f:
json.dump(controller_config, f)
上記のコードを実行すると、接続されているコントローラが表示されます。
使用したいコントローラの名前をコピーしてインプットに貼り付けましょう。
次に使用したいボタンをクリックすると、設定がjsonに保存されます。
4. PushtoTalk機能を作ろう
PushtoTalk機能を作っていく前に、録音開始、終了、中止のタイミングが分かるように、効果音を追加します。
この効果音を追加し、PushtoTalkを実装したのが次のコードになります。
import asyncio
import speech_recognition as sr
import pygame
import json
with open("controller_config.json", "r") as f:
controller_config = json.load(f)
JOYSTICK_NAME = controller_config["joystick_name"]
BUTTON_NUMBER = controller_config["button_number"]
class PushToTalk:
def __init__(self):
pygame.init()
pygame.joystick.init()
pygame.mixer.init()
self.joystick_id = None
if pygame.joystick.get_count() == 0:
print("コントローラが接続されていません")
return
else:
for i in range(pygame.joystick.get_count()):
joystick = pygame.joystick.Joystick(i)
joystick.init()
if joystick.get_name() == JOYSTICK_NAME:
self.joystick_id = i
break
if self.joystick_id is None:
print(f"コントローラ : {JOYSTICK_NAME} が見つかりません")
return
self.joystick = pygame.joystick.Joystick(self.joystick_id)
self.joystick.init()
self.is_button_pushed = False
self.start_sound = pygame.mixer.Sound("start_sound.wav")
self.stop_sound = pygame.mixer.Sound("stop_sound.wav")
self.error_sound = pygame.mixer.Sound("error_sound.wav")
async def monitor_controller(self):
while True:
for event in pygame.event.get():
if event.type == pygame.JOYBUTTONDOWN:
if event.button == BUTTON_NUMBER and not self.is_button_pushed:
self.is_button_pushed = True
print("ボタンが押されました")
asyncio.create_task(self.speech2text())
elif event.type == pygame.JOYBUTTONUP:
if event.button == BUTTON_NUMBER and self.is_button_pushed:
self.is_button_pushed = False
print("ボタンが離されました")
await asyncio.sleep(0.01)
async def speech2text(self):
try:
recognizer = sr.Recognizer()
await asyncio.sleep(0.2) # すぐ離されたら録音しない
if not self.is_button_pushed:
self.error_sound.play()
return
with sr.Microphone() as source:
self.start_sound.play()
print("話してください")
audio = recognizer.listen(source, timeout=1.5) # 1.5秒何も入力がなかったら認識を中止
await asyncio.sleep(0.01)
if not self.is_button_pushed:
self.error_sound.play()
return # 途中でボタンが離されたら認識を中止
self.stop_sound.play()
text = recognizer.recognize_google(audio, language="ja-JP")
print(text)
except Exception as e:
self.error_sound.play()
if self.is_button_pushed:
print(f"エラーが発生しました: {e}")
async def run(self):
async with asyncio.TaskGroup() as tg:
tg.create_task(self.monitor_controller())
if __name__ == "__main__":
main = PushToTalk()
try:
asyncio.run(main.run())
except KeyboardInterrupt:
print("終了します")
except Exception as e:
print(f"エラーが発生しました: {e}")
print("終了します")
終了音が鳴るまでボタンを押し続け、話した言葉が表示されれば成功です。
ボタンが押されました
話してください
こんにちは
ボタンが離されました
5. 入力した音声をChatGPTに送信しよう
先ほど作成したコードに、追加でopenaiモジュールをインポートします。
import openai
ChatGPTクラスを新しく作成し、音声を認識したら、send2aiメソッドを実行するようにします。
class ChatGPT:
def __init__(self):
self.client = openai.OpenAI(api_key="ここにAPIキーを入力")
async def send2ai(self, message):
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "あなたは優秀なAIアシスタントです。"},
{"role": "user", "content": message},
],
)
input_tokens = response.usage.prompt_tokens
output_tokens = response.usage.completion_tokens
print(f"インプットトークン: {input_tokens}, アウトプットトークン: {output_tokens}")
print(response.choices[0].message.content)
class PushToTalk:
def __init__(self):
self.ai = ChatGPT():
~~~~~~~~~~省略~~~~~~~~~~
async def speech2text(self):
~~~~~~~~~~省略~~~~~~~~~~
text = recognizer.recognize_google(audio, language="ja-JP")
print(text)
asyncio.create_task(self.ai.send2ai(text)) # AIに文字起こししたテキストを送信
ChatGPTからの回答がコンソールに出力されれば成功です。
ボタンが押されました
話してください
こんにちは
インプットトークン: 24, アウトプットトークン: 16
こんにちは!何かお手伝いできることはありますか?
ボタンが離されました
終了します
6. iRacingにピットコマンドを送信しよう
いよいよiRacingを音声認識で操作する部分を作っていきます。
iRacing側の設定
iRacingのチャットコマンドの11から15番目のスロットを以下のように編集してください。

追加するモジュールをインポート
import time
import irsdk
from irsdk import PitCommandMode
FunctionCallingでiRacingにコマンドを送信
FunctionCallingとは、プロンプトに応じてAIが関数を呼び出してくれる機能です。
これを利用し、自分がやりたい操作を関数にして、それをChatGPTに呼び出してもらうようにします。
以下のようにChatGPTクラスを変更します。
SLEEP = 0.1 # 定数を宣言
class ChatGPT:
functions = [
{"name": "check_all_tires", "description": "check_all_tires"},
{"name": "check_front_tires", "description": "check_front_tires"},
{"name": "check_rear_tires", "description": "check_rear_tires"},
{"name": "check_left_tires", "description": "check_left_tires"},
{"name": "check_right_tires", "description": "check_right_tires"},
{"name": "check_tire", "description": "check_tire_with_pressure, tireはタイヤの位置(lf, rf, lr, rr)、pressureはタイヤの圧力", "parameters": {
"type": "object", "properties": {"tire": {"type": "string"}, "pressure": {"type": "number"}}}, "required": ["tire"]},
{"name": "check_fuel", "description": "check_fuel", "parameters": {
"type": "object", "properties": {"fuel": {"type": "number"}}}},
{"name": "check_ws", "description": "check_ws"},
{"name": "check_fr", "description": "check_fr ファストリペアにチェックをつける"},
{"name": "clear_all_tires", "description": "clear_all_tires_check"},
{"name": "clear_ws", "description": "clear_ws_check"},
{"name": "clear_fr", "description": "clear_fr_check"},
{"name": "clear_fuel", "description": "clear_fuel_check 次回給油しない"},
{"name": "clear_all", "description": "clear_all_check"},
{"name": "clear_other_than_fr", "description": "clear_other_than_fr"},
{"name": "change_tire_compound", "description": "change_tire_compound dry or wet", "parameters": {
"type": "object", "properties": {"compound": {"type": "string"}}}, "required": ["compound"]},
{"name": "toggle_autofuel",
"description": "toggle_autofuel 「切り替えて」「オンにして」「オフにして」など"},
{"name": "set_fuel_margin", "description": "set_fuel_margin 「増やして」「1周にして」のときはTrue、「減らして」「0周にして」のときはFalse",
"parameters": {"type": "object", "properties": {"margin": {"type": "boolean"}}}, "required": ["margin"]},
]
def __init__(self):
self.client = openai.OpenAI(
api_key="ここにAPIキーを入力")
self.ir = irsdk.IRSDK()
self.ir.startup()
async def send2ai(self, message):
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "あなたは優秀なピットアシスタントです。"},
{"role": "user", "content": message},
],
functions=self.functions,
function_call="auto",
)
input_tokens = response.usage.prompt_tokens
output_tokens = response.usage.completion_tokens
print(f"インプットトークン: {input_tokens}, アウトプットトークン: {output_tokens}")
print(response.choices[0].message.content)
if response.choices[0].message.function_call is not None:
answer = self.execute_function(response.choices[0].message)
print(answer)
else:
print("サポートされていないコマンドです")
def execute_function(self, message):
function_name = message.function_call.name
arguments = json.loads(message.function_call.arguments)
if function_name == "check_all_tires":
return self.check_all_tires()
elif function_name == "check_front_tires":
return self.check_front_tires()
elif function_name == "check_rear_tires":
return self.check_rear_tires()
elif function_name == "check_left_tires":
return self.check_left_tires()
elif function_name == "check_right_tires":
return self.check_right_tires()
elif function_name == "check_tire":
pressure = arguments.get("pressure", 0)
return self.check_tire(arguments["tire"], pressure)
elif function_name == "check_fuel":
fuel = arguments.get("fuel", 0)
return self.check_fuel(fuel)
elif function_name == "check_ws":
return self.check_ws()
elif function_name == "check_fr":
return self.check_fr()
elif function_name == "clear_all_tires":
return self.clear_all_tires()
elif function_name == "clear_ws":
return self.clear_ws()
elif function_name == "clear_fr":
return self.clear_fr()
elif function_name == "clear_fuel":
return self.clear_fuel()
elif function_name == "clear_all":
return self.clear_all()
elif function_name == "clear_other_than_fr":
return self.clear_other_than_fr()
elif function_name == "change_tire_compound":
return self.change_tire_compound(arguments["compound"])
elif function_name == "toggle_autofuel":
return self.toggle_autofuel()
elif function_name == "set_fuel_margin":
return self.set_fuel_margin(arguments["margin"])
else:
return "サポートされていない関数です"
def check_all_tires(self):
self.ir.pit_command(PitCommandMode.lf, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rf, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.lr, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rr, 0)
return "全てのタイヤにチェックをつけました"
def check_front_tires(self):
self.ir.pit_command(PitCommandMode.clear_tires, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.lf, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rf, 0)
return "フロントタイヤにチェックをつけました"
def check_rear_tires(self):
self.ir.pit_command(PitCommandMode.clear_tires, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.lr, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rr, 0)
return "リアタイヤにチェックをつけました"
def check_left_tires(self):
self.ir.pit_command(PitCommandMode.clear_tires, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.lf, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.lr, 0)
return "左のタイヤにチェックをつけました"
def check_right_tires(self):
self.ir.pit_command(PitCommandMode.clear_tires, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rf, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.rr, 0)
return "右のタイヤにチェックをつけました"
def check_tire(self, tire: str, pressure: float = 0):
tires = {"lf": PitCommandMode.lf, "rf": PitCommandMode.rf,
"lr": PitCommandMode.lr, "rr": PitCommandMode.rr}
self.ir.pit_command(tires[tire], int(pressure))
return f"{tire}のタイヤにチェックをつけました"
def check_fuel(self, fuel: float = 0):
self.ir.pit_command(PitCommandMode.fuel, int(fuel))
return f"燃料を{fuel}L追加しました"
def check_ws(self):
self.ir.pit_command(PitCommandMode.ws, 0)
return "ウインドシールドにチェックをつけました"
def check_fr(self):
self.ir.pit_command(PitCommandMode.fr, 0)
return "ファストリペアにチェックをつけました"
def clear_all_tires(self):
self.ir.pit_command(PitCommandMode.clear_tires, 0)
return "全てのタイヤのチェックを外しました"
def clear_ws(self):
self.ir.pit_command(PitCommandMode.clear_ws, 0)
return "ウインドシールドのチェックを外しました"
def clear_fr(self):
self.ir.pit_command(PitCommandMode.clear_fr, 0)
return "ファストリペアのチェックを外しました"
def clear_fuel(self):
self.ir.pit_command(PitCommandMode.clear_fuel, 0)
return "燃料のチェックを外しました"
def clear_all(self):
self.ir.pit_command(PitCommandMode.clear, 0)
return "全てのチェックを外しました"
def clear_other_than_fr(self):
self.ir.pit_command(PitCommandMode.clear, 0)
time.sleep(SLEEP)
self.ir.pit_command(PitCommandMode.fr, 0)
return "ファストリペア以外のチェックを外しました"
def change_tire_compound(self, compound: str):
tires = {"dry": 10, "wet": 11}
self.ir.chat_command_macro(macro_num=tires[compound])
return f"{compound}タイヤに変更しました"
def toggle_autofuel(self):
self.ir.chat_command_macro(macro_num=12)
return "オートフューエルを切り替えました"
def set_fuel_margin(self, margin: bool):
margins = {True: [13, '1周'], False: [14, '0周']}
self.ir.chat_command_macro(macro_num=margins[margin][0])
return f"オートフューエルのマージンを{margins[margin][1]}にしました"
基本的なピットコマンドは、IRSDK.pit_command()で操作できます。
タイヤのコンパウンド切り替え(ここではドライ⇔ウェットを想定)やオートフューエルの切り替えなどは、このメソッドで操作できません。
なので、先ほど設定したチャットコマンドを呼び出せるよう、IRSDK.chat_command_macro()メソッドを使用しています。
7. さいごに
以上が、iRacingを音声認識で操作できるプログラムの作り方になります。
プロンプトの書き方や、FunctionCallingの指定方法など、改善できる部分が沢山あると思います。
また、ピットコマンドに限らず、リプレイカメラの操作などがirsdkからの入力に対応しているので、実装してみても面白いかもしれません。