Skip to content

Instantly share code, notes, and snippets.

@roflsunriz
Last active May 8, 2025 13:26
Show Gist options
  • Save roflsunriz/f8e6c9350594cc87546450cdc130c259 to your computer and use it in GitHub Desktop.
Save roflsunriz/f8e6c9350594cc87546450cdc130c259 to your computer and use it in GitHub Desktop.
training_app : 自分専用筋トレアプリ
import tkinter as tk
from tkinter import ttk, messagebox, Scale
from PIL import Image, ImageDraw, ImageFont, ImageTk
import threading
import time
import os
import subprocess
import sys
import json
import wave
import numpy as np
from scipy.io import wavfile
import importlib
import pkg_resources
import platform
import tempfile
from typing import Optional, List, Dict, Any, Union
# 必要なライブラリとバージョンの定義
required_packages = {
'PIL': 'pillow',
'pydub': 'pydub',
'ffmpeg': 'ffmpeg-python',
'playsound': 'playsound==1.2.2',
'scipy': 'scipy',
'numpy': 'numpy',
'requests': 'requests'
}
# アプリケーションの設定ディレクトリとファイルパス
CONFIG_DIR = os.path.join("misc", "config")
CONFIG_FILE = os.path.join(CONFIG_DIR, "training_app.json")
ASSETS_DIR = os.path.join("misc", "assets")
def load_config():
"""設定ファイルから設定を読み込む"""
default_config = {
"voice_volume": 1.0,
"sound_volume": 1.0,
"voice_style": 3 # ずんだもん(あまあま)をデフォルトに
}
# 設定ディレクトリがなければ作成
os.makedirs(CONFIG_DIR, exist_ok=True)
# 設定ファイルが存在する場合は読み込み
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
loaded_config = json.load(f)
# デフォルト設定に読み込んだ設定を上書き
default_config.update(loaded_config)
print(f"設定ファイルを読み込みました: {CONFIG_FILE}")
print(f"読み込まれた設定: {loaded_config}") # デバッグ用ログ
except Exception as e:
print(f"設定ファイルの読み込みエラー: {e}")
else:
print(f"設定ファイルが見つかりません。デフォルト設定を使用します: {CONFIG_FILE}")
return default_config
def save_config(config):
"""設定ファイルに設定を保存する"""
# 設定ディレクトリがなければ作成
os.makedirs(CONFIG_DIR, exist_ok=True)
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"設定ファイルを保存しました: {CONFIG_FILE}")
return True
except Exception as e:
print(f"設定ファイルの保存エラー: {e}")
return False
def check_and_install_packages():
"""必要なライブラリが揃っているかチェックし、なければインストールする"""
missing_packages = []
# インストール済みのパッケージを取得
installed_packages = {pkg.key: pkg.version for pkg in pkg_resources.working_set}
# 必要なパッケージを確認
for package, install_name in required_packages.items():
try:
# パッケージがインストールされているか確認
importlib.import_module(package)
print(f"✓ {package} はインストール済みです")
except ImportError:
# 見つからない場合はインストールリストに追加
missing_packages.append(install_name)
print(f"✗ {package} が見つかりません - インストールが必要です")
# 不足しているパッケージがあればインストール
if missing_packages:
print("必要なライブラリをインストールしています...")
for package in missing_packages:
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
print(f"✓ {package} をインストールしました")
except Exception as e:
print(f"✗ {package} のインストールに失敗しました: {e}")
return False
# playsoundモジュールを遅延インポート(インストール後に行う必要がある)
global playsound
try:
from playsound import playsound
except ImportError:
print("playsoundモジュールのインポートに失敗しました。手動でインストールしてください。")
return False
# requestsモジュールを遅延インポート
global requests
try:
import requests
except ImportError:
print("requestsモジュールのインポートに失敗しました。手動でインストールしてください。")
return False
# Windowsの場合はwinsoundをインポート
global winsound
if platform.system() == "Windows":
try:
import winsound
except ImportError:
print("Windowsのwinsoundモジュールがインポートできませんでした。")
return True
# メイン処理の前にライブラリチェックを実行
if not check_and_install_packages():
print("必要なライブラリがインストールできませんでした。セットアップを中断します。")
sys.exit(1)
# VoiceVox関連の設定
VOICEVOX_PATH: Optional[str] = None # 自動検出される
VOICEVOX_URL: str = "http://localhost:50021"
VOICEVOX_STARTUP_WAIT: int = 10 # 起動待機秒数
class VoiceVoxManager:
"""VoiceVoxの管理とインターフェース"""
def __init__(self):
self.process = None
self.is_running = False
self.autostart = True
self.api_ready = False
self.output_dir = os.path.join(ASSETS_DIR, "tts_output")
os.makedirs(self.output_dir, exist_ok=True)
# 既に起動しているか確認
if self.check_api_available():
print("VoiceVoxが既に起動しています")
self.api_ready = True
self.is_running = True
def find_voicevox_path(self):
"""システム内のVoiceVoxを探す"""
global VOICEVOX_PATH
# パスが既に設定されていればそれを使う
if VOICEVOX_PATH and os.path.exists(VOICEVOX_PATH):
print(f"VoiceVoxのパスが既に設定されています: {VOICEVOX_PATH}")
return VOICEVOX_PATH
# 環境変数から直接パスを取得(手動設定用)
env_path = os.environ.get("VOICEVOX_PATH", "")
if env_path and os.path.exists(env_path):
VOICEVOX_PATH = env_path
print(f"環境変数からVoiceVoxのパスを読み込みました: {VOICEVOX_PATH}")
return VOICEVOX_PATH
# WindowsでのVoiceVoxの一般的なインストール場所
if platform.system() == "Windows":
# ユーザープロファイルのパス
user_profile = os.environ.get("USERPROFILE", "")
# ユーザープロファイルディレクトリ内のVoiceVox
user_voicevox_path = os.path.join(user_profile, "AppData", "Local", "Programs", "VOICEVOX", "VOICEVOX.exe")
if os.path.exists(user_voicevox_path):
VOICEVOX_PATH = user_voicevox_path
print(f"VoiceVoxをユーザープロファイルで見つけました: {VOICEVOX_PATH}")
return VOICEVOX_PATH
# Program Filesのパス
program_files = [
os.environ.get("ProgramFiles", "C:\\Program Files"),
os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")
]
for pf in program_files:
voicevox_dir = os.path.join(pf, "VOICEVOX")
voicevox_exe = os.path.join(voicevox_dir, "VOICEVOX.exe")
if os.path.exists(voicevox_exe):
VOICEVOX_PATH = voicevox_exe
print(f"VoiceVoxをProgram Filesで見つけました: {VOICEVOX_PATH}")
return VOICEVOX_PATH
print("WindowsでVoiceVoxが見つかりませんでした。")
# Macでの一般的なインストール場所
elif platform.system() == "Darwin":
mac_paths = [
"/Applications/VOICEVOX.app/Contents/MacOS/VOICEVOX"
]
for path in mac_paths:
if os.path.exists(path):
VOICEVOX_PATH = path
print(f"VoiceVoxをMacで見つけました: {VOICEVOX_PATH}")
return VOICEVOX_PATH
print("MacでVoiceVoxが見つかりませんでした。")
# それ以外(Linux等)での一般的なインストール場所
else:
linux_paths = [
"/usr/bin/voicevox",
"/usr/local/bin/voicevox",
os.path.expanduser("~/.local/bin/voicevox")
]
for path in linux_paths:
if os.path.exists(path):
VOICEVOX_PATH = path
print(f"VoiceVoxをLinuxで見つけました: {VOICEVOX_PATH}")
return VOICEVOX_PATH
print("LinuxでVoiceVoxが見つかりませんでした。")
print("VoiceVoxが見つかりませんでした。手動で起動してください。")
return None
def start_voicevox(self):
"""VoiceVoxを起動する"""
if self.is_running:
return True
voicevox_path = self.find_voicevox_path()
if not voicevox_path:
print("VoiceVoxの実行ファイルが見つかりません。手動でVoiceVoxを起動してください。")
return False
try:
# 起動(非表示モード)
if platform.system() == "Windows":
self.process = subprocess.Popen([voicevox_path],
creationflags=subprocess.CREATE_NO_WINDOW)
else:
self.process = subprocess.Popen([voicevox_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
self.is_running = True
print(f"VoiceVoxを起動しました(PID: {self.process.pid})")
# APIが利用可能になるまで待機
for i in range(VOICEVOX_STARTUP_WAIT):
if self.check_api_available():
self.api_ready = True
print(f"VoiceVox API が利用可能になりました")
return True
time.sleep(1)
print(f"VoiceVox API を待機中... {i+1}/{VOICEVOX_STARTUP_WAIT}")
print("VoiceVoxは起動しましたが、APIが応答しません。")
return False
except Exception as e:
print(f"VoiceVoxの起動に失敗しました: {e}")
return False
def check_api_available(self):
"""VoiceVox APIが利用可能か確認"""
try:
response = requests.get(f"{VOICEVOX_URL}/version")
return response.status_code == 200
except:
return False
def stop_voicevox(self):
"""VoiceVoxを停止する"""
if self.process and self.is_running:
try:
self.process.terminate()
self.is_running = False
self.api_ready = False
print("VoiceVoxを停止しました")
return True
except Exception as e:
print(f"VoiceVoxの停止に失敗しました: {e}")
return False
return True
def generate_speech(self, text, speaker_id=3, volume=1.0):
"""テキストから音声を生成する"""
if not self.check_api_available():
return None
try:
# 読み方修正のための前処理
# 難しい漢字や専門用語にVoiceVoxで正しく読ませるための置換処理
replacements = {
"肩甲骨": "けんこうこつ",
"腹斜筋": "ふくしゃきん",
"バイシクルクランチ": "バイシクル クランチ"
}
for word, replacement in replacements.items():
text = text.replace(word, replacement)
print(f"読み方修正後のテキスト: {text}") # デバッグ用ログ
# 音声合成用のクエリを作成
query_payload = {"text": text, "speaker": speaker_id}
query_response = requests.post(f"{VOICEVOX_URL}/audio_query", params=query_payload)
if query_response.status_code != 200:
print(f"VoiceVox クエリエラー: {query_response.text}")
return None
# 音量を調整(VoiceVoxのパラメータを変更)
query_data = query_response.json()
query_data["volumeScale"] = max(0.0, min(2.0, volume)) # 音量を0.0から2.0の範囲に制限
print(f"音量設定: {query_data['volumeScale']}") # デバッグ用ログ
# 音声合成
synthesis_payload = {"speaker": speaker_id}
synthesis_response = requests.post(
f"{VOICEVOX_URL}/synthesis",
headers={"Content-Type": "application/json"},
params=synthesis_payload,
data=json.dumps(query_data)
)
if synthesis_response.status_code != 200:
print(f"VoiceVox 合成エラー: {synthesis_response.text}")
return None
# 一時ファイルに保存
wav_filename = f"{self.output_dir}/temp_{int(time.time())}.wav"
with open(wav_filename, "wb") as f:
f.write(synthesis_response.content)
return wav_filename
except Exception as e:
print(f"音声合成エラー: {e}")
return None
class SoundManager:
"""効果音の管理"""
def __init__(self):
self.sound_dir = os.path.join(ASSETS_DIR, "sounds")
os.makedirs(self.sound_dir, exist_ok=True)
self.volume = 1.0 # 効果音の音量(0.0 - 1.0)
self.use_winsound = False # エラー発生時にwinsoundを使用するかどうか
def set_volume(self, volume):
"""効果音の音量を設定"""
self.volume = max(0.0, min(1.0, volume))
def play_sound(self, sound_file):
"""効果音を再生"""
if not os.path.exists(sound_file):
print(f"音声ファイルが見つかりません: {sound_file}")
return False
# winsoundが優先の場合(Windowsで.wavファイルの場合のみ)
if self.use_winsound and platform.system() == "Windows" and sound_file.endswith('.wav'):
try:
import winsound
winsound.PlaySound(sound_file, winsound.SND_FILENAME | winsound.SND_ASYNC)
print(f"winsoundで音声再生しました: {sound_file}")
return True
except Exception as e:
print(f"winsound再生エラー: {e}")
# winsound失敗時は次の方法を試す
# playsoundを使用して非同期再生(別スレッドで実行)
def _play_async():
try:
from playsound import playsound
playsound(sound_file, block=False) # block=Falseで非同期再生
print(f"playsoundで音声再生しました: {sound_file}")
except Exception as e:
print(f"playsound再生エラー: {e}")
# playsound失敗時はpydubを試す
try:
from pydub import AudioSegment
from pydub.playback import play
sound = AudioSegment.from_mp3(sound_file) if sound_file.endswith('.mp3') else AudioSegment.from_wav(sound_file)
sound = sound - (60 * (1.0 - self.volume))
play(sound)
print(f"pydubで音声再生しました: {sound_file}")
except Exception as e:
print(f"pydub再生エラー: {e}")
self.use_winsound = True # 次回からwinsoundを試す
# 別スレッドで再生を開始
threading.Thread(target=_play_async, daemon=True).start()
return True
def create_beep_sound(self, filename, duration=1.0, frequency=440, volume=None):
"""シンプルなビープ音を作成する"""
# 指定されたボリュームがなければデフォルト値を使用
if volume is None:
volume = self.volume
# ディレクトリが存在しない場合は作成
os.makedirs(os.path.dirname(filename), exist_ok=True)
# サイン波を生成
sample_rate = 44100
t = np.linspace(0, duration, int(sample_rate * duration), False)
signal = np.sin(2 * np.pi * frequency * t) * volume
# フェードアウト効果(終わりの0.1秒)
fadeout_len = int(0.1 * sample_rate)
if len(signal) > fadeout_len:
fadeout = np.linspace(1.0, 0.0, fadeout_len)
signal[-fadeout_len:] *= fadeout
# 16ビット整数に変換
audio = (signal * 32767).astype(np.int16)
# WAVファイルとして保存
try:
wavfile.write(filename, sample_rate, audio)
return True
except Exception as e:
print(f"効果音作成エラー: {e}")
return False
def create_all_sounds(self):
"""必要な効果音をすべて作成"""
# 各効果音のパラメータ定義
sounds = [
{"name": "start.wav", "duration": 0.5, "frequency": 880, "volume": self.volume},
{"name": "rest.wav", "duration": 0.3, "frequency": 660, "volume": self.volume},
{"name": "complete.wav", "duration": 1.0, "frequency": 440, "volume": self.volume},
{"name": "countdown.wav", "duration": 0.3, "frequency": 1000, "volume": self.volume},
{"name": "beep.wav", "duration": 0.2, "frequency": 800, "volume": self.volume * 0.3}
]
# 各効果音を作成
created_count = 0
for sound in sounds:
filename = os.path.join(self.sound_dir, sound["name"])
if not os.path.exists(filename) or True: # 常に上書き作成
if self.create_beep_sound(filename, sound["duration"], sound["frequency"], sound["volume"]):
created_count += 1
return created_count
class ImageManager:
"""画像の管理"""
def __init__(self):
self.image_dir = os.path.join(ASSETS_DIR, "images")
os.makedirs(self.image_dir, exist_ok=True)
def create_all_images(self):
"""画像ディレクトリの確認と作成"""
# 各筋トレ種目の情報
exercises = [
"0. ハムストリングスストレッチ",
"1. クランチ",
"2. リバースクランチ",
"3. バイシクルクランチ",
"4. レッグレイズ",
"5. プランク"
]
# 画像ディレクトリが存在することを確認
print(f"画像ディレクトリを確認: {self.image_dir}")
if not os.path.exists(self.image_dir):
os.makedirs(self.image_dir, exist_ok=True)
print(f"画像ディレクトリを作成しました")
# 休憩画像が存在しない場合は作成(シンプルな画像を作成)
rest_image_path = os.path.join(self.image_dir, "rest.png")
if not os.path.exists(rest_image_path):
try:
# シンプルな休憩画像を作成
img = Image.new('RGB', (800, 600), color='#e0f0ff')
d = ImageDraw.Draw(img)
# フォントを設定(利用可能なフォントを使用)
try:
font = ImageFont.truetype("arial.ttf", 32)
small_font = ImageFont.truetype("arial.ttf", 24)
except:
# デフォルトフォントを使用
font = ImageFont.load_default()
small_font = ImageFont.load_default()
# テキスト描画
d.text((400, 200), "休憩時間", fill='#0066cc', anchor='mm', font=font)
d.text((400, 250), "深呼吸して、次のエクササイズに備えましょう", fill='#333333', anchor='mm', font=small_font)
# 画像を保存
img.save(rest_image_path)
print(f"休憩画像を作成しました: {rest_image_path}")
return 1 # 作成した画像の数を返す
except Exception as e:
print(f"休憩画像の作成に失敗しました: {e}")
return 0 # 作成した画像の数を返す(この場合は0)
# シングルトンインスタンス
voicevox_manager = VoiceVoxManager()
sound_manager = SoundManager()
image_manager = ImageManager()
class TrainingApp:
def __init__(self, root):
self.root = root
self.root.title("ものぐさでも出来る寝ながら集中腹筋鍛錬")
self.root.geometry("800x600")
self.root.resizable(False, False)
# 設定の読み込み
self.config = load_config()
# 初期セットアップ(ウィンドウ表示前)
self.setup_resources()
# VoiceVoxとサウンド設定
self.voicevox_speaker = self.config["voice_style"] # 設定から読み込み
self.voicevox_volume = self.config["voice_volume"] # 設定から読み込み
self.sound_volume = self.config["sound_volume"] # 設定から読み込み
# サウンドマネージャーに音量設定を適用
sound_manager.set_volume(self.sound_volume)
# 難易度とそれに対応する時間(秒)
self.difficulty_times = {
"軽め": 20,
"通常": 30,
"重め": 40
}
self.selected_difficulty = tk.StringVar(value="通常")
# トレーニングデータの設定
self.training_data = [
{
"name": "0. ハムストリングスストレッチ",
"description": "仰向けに寝て、片足をまっすぐ上に伸ばします。\n両手で足の裏または足首を持ち、足を頭の方向にゆっくり引き寄せます。\n反対の足も同様に行いましょう。各足20秒ずつキープします。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise0.png")
},
{
"name": "1. クランチ",
"description": "仰向けに寝て、両膝を曲げ、両手を耳の横に置きます。\n腹筋を意識しながら上半身を持ち上げ、肩甲骨が床から離れる程度まで上げます。\nゆっくりと元の位置に戻し、反動を使わないようにしましょう。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise1.png")
},
{
"name": "2. リバースクランチ",
"description": "仰向けに寝て、両膝を90度に曲げます。\n腹筋を使ってお尻を床から少し浮かし、膝を胸に近づけるようにします。\nゆっくりと元の位置に戻し、下腹部を意識して行いましょう。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise2.png")
},
{
"name": "3. バイシクルクランチ",
"description": "仰向けに寝て、両手を耳の横に置き、両膝を90度に曲げます。\n右肘を左膝に、左肘を右膝に交互に近づけるようにします。\n自転車をこぐようなリズムで、腹斜筋を意識しながら行いましょう。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise3.png")
},
{
"name": "4. レッグレイズ",
"description": "仰向けに寝て、両手を体の横に置きます。\n両足をそろえたまま、まっすぐ上に持ち上げ、床と垂直になるまで上げます。\nゆっくりと下ろす際も床につけず、下腹部を意識して行いましょう。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise4.png")
},
{
"name": "5. プランク",
"description": "うつ伏せになり、肘と前腕、つま先を床につけて体を支えます。\n体が一直線になるようにキープし、お腹を引き締めて腰が沈まないようにします。\n呼吸を止めず、体幹全体を意識して維持しましょう。",
"image_path": os.path.join(ASSETS_DIR, "images", "exercise5.png")
}
]
# 休憩時の画像
self.rest_image_path = os.path.join(ASSETS_DIR, "images", "rest.png")
# 現在のトレーニング状態
self.current_exercise_index = 0
self.is_training = False
self.is_rest = False
self.is_preparing = False
self.remaining_time = 0
self.timer_thread = None
self.is_countdown = False
self.countdown_played = False # カウントダウン音が既に再生されたかどうかを追跡
# 音声ファイルパス
self.sound_start = os.path.join(ASSETS_DIR, "sounds", "complete.mp3")
self.sound_rest = os.path.join(ASSETS_DIR, "sounds", "complete.mp3")
self.sound_complete = os.path.join(ASSETS_DIR, "sounds", "complete.mp3")
self.sound_countdown = os.path.join(ASSETS_DIR, "sounds", "countdown.mp3")
self.sound_beep = os.path.join(ASSETS_DIR, "sounds", "countdown.mp3")
# 画面の初期化
self.setup_start_screen()
# 終了時の処理を設定
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_resources(self):
"""必要なリソース(画像・音声)をセットアップ"""
# 進捗表示用のトップレベルウィンドウ
progress_window = tk.Toplevel(self.root)
progress_window.title("初期化中...")
progress_window.geometry("400x150")
progress_window.resizable(False, False)
progress_window.transient(self.root)
progress_window.grab_set()
# 進捗メッセージ
message_label = tk.Label(progress_window, text="セットアップを実行中です...", font=("Helvetica", 12))
message_label.pack(pady=20)
# 進捗バー
progress_bar = ttk.Progressbar(progress_window, mode='indeterminate', length=300)
progress_bar.pack(pady=10)
progress_bar.start(10)
# 詳細メッセージ
detail_label = tk.Label(progress_window, text="リソースを準備しています...", font=("Helvetica", 10))
detail_label.pack(pady=10)
# UIを更新
progress_window.update()
# 画像ディレクトリの確認
detail_label.config(text="画像ディレクトリを確認しています...")
progress_window.update()
image_dir_check = image_manager.create_all_images()
# 音声リソースのセットアップ
detail_label.config(text="効果音を準備しています...")
progress_window.update()
sounds_created = sound_manager.create_all_sounds()
# VoiceVoxのセットアップ(あれば)
if voicevox_manager.autostart:
detail_label.config(text="VoiceVoxの起動を試みています...")
progress_window.update()
voicevox_started = voicevox_manager.start_voicevox()
if voicevox_started:
detail_label.config(text="VoiceVoxが起動しました!")
else:
detail_label.config(text="VoiceVoxは利用できません(テキスト表示モードで動作します)")
# セットアップ完了
time.sleep(1) # 完了メッセージを少し表示
progress_window.destroy()
def set_voice_style(self, speaker_id):
"""音声スタイルを設定"""
self.voicevox_speaker = speaker_id
self.config["voice_style"] = speaker_id # 設定を更新
save_config(self.config) # 設定を即時保存
def setup_start_screen(self):
"""スタート画面のセットアップ"""
# 既存のウィジェットをクリア
for widget in self.root.winfo_children():
widget.destroy()
# タイトルラベル
title_label = tk.Label(self.root, text="ものぐさでも出来る寝ながら集中腹筋鍛錬", font=("Helvetica", 24, "bold"))
title_label.pack(pady=30)
# 難易度選択フレーム
difficulty_frame = tk.Frame(self.root)
difficulty_frame.pack(pady=15)
difficulty_label = tk.Label(difficulty_frame, text="難易度選択:", font=("Helvetica", 14))
difficulty_label.pack(side=tk.LEFT, padx=10)
for difficulty in ["軽め", "通常", "重め"]:
rb = tk.Radiobutton(difficulty_frame, text=difficulty, variable=self.selected_difficulty,
value=difficulty, font=("Helvetica", 12))
rb.pack(side=tk.LEFT, padx=10)
# 説明テキスト
description_text = f"軽め: 各種目 20秒\n通常: 各種目 30秒\n重め: 各種目 40秒\n\n全5種目、休憩は各10秒"
description_label = tk.Label(self.root, text=description_text, font=("Helvetica", 12), justify=tk.LEFT)
description_label.pack(pady=15)
# 音声エンジン選択
tts_frame = tk.Frame(self.root)
tts_frame.pack(pady=10)
tts_label = tk.Label(tts_frame, text="音声:", font=("Helvetica", 12))
tts_label.pack(side=tk.LEFT, padx=10)
# ずんだもんノーマル
zunda_normal_rb = tk.Radiobutton(tts_frame, text="ずんだもん(ノーマル)",
variable=self.voicevox_speaker, # 同じ変数を参照
value=1, # ノーマルの値
command=lambda: self.set_voice_style(1), # 変更時に設定を保存
font=("Helvetica", 12))
zunda_normal_rb.pack(side=tk.LEFT, padx=5)
zunda_normal_rb.select() # デフォルト選択
# ずんだもんあまあま
zunda_sweet_rb = tk.Radiobutton(tts_frame, text="ずんだもん(あまあま)",
variable=self.voicevox_speaker, # 同じ変数を参照
value=3, # あまあまの値
command=lambda: self.set_voice_style(3), # 変更時に設定を保存
font=("Helvetica", 12))
zunda_sweet_rb.pack(side=tk.LEFT, padx=5)
# 音量設定セクション
volume_frame = tk.Frame(self.root)
volume_frame.pack(pady=15)
# VoiceVox音量
voice_volume_label = tk.Label(volume_frame, text="音声ガイド音量:", font=("Helvetica", 10))
voice_volume_label.grid(row=0, column=0, padx=10)
voice_volume = tk.Scale(volume_frame, from_=0, to=100, orient=tk.HORIZONTAL,
command=self.set_voice_volume)
voice_volume.set(int(self.voicevox_volume * 100))
voice_volume.grid(row=0, column=1, padx=10)
# 効果音音量
sound_volume_label = tk.Label(volume_frame, text="効果音音量:", font=("Helvetica", 10))
sound_volume_label.grid(row=1, column=0, padx=10)
sound_volume = tk.Scale(volume_frame, from_=0, to=100, orient=tk.HORIZONTAL,
command=self.set_sound_volume)
sound_volume.set(int(self.sound_volume * 100))
sound_volume.grid(row=1, column=1, padx=10)
# スタートボタン
start_button = tk.Button(self.root, text="トレーニング開始", font=("Helvetica", 16, "bold"),
command=self.start_training, bg="#4CAF50", fg="white", padx=20, pady=10)
start_button.pack(pady=20)
# VoiceVox状態表示
self.voicevox_status_label = tk.Label(self.root, text="", font=("Helvetica", 10), fg="gray")
self.voicevox_status_label.pack()
# VoiceVoxの状態を確認して表示を更新
self.update_voicevox_status()
# 注意書き
note_text = "※「ずんだもん」ボイスを使用するには、VoiceVoxが必要です。"
note_label = tk.Label(self.root, text=note_text, font=("Helvetica", 8), fg="gray")
note_label.pack(pady=5)
def update_voicevox_status(self):
"""VoiceVoxの状態を確認して表示を更新"""
if voicevox_manager.check_api_available():
self.voicevox_status_label.config(text="VoiceVox: 接続済み✓", fg="green")
else:
self.voicevox_status_label.config(text="VoiceVox: 利用不可✗", fg="red")
def set_voice_volume(self, value):
"""VoiceVoxの音量を設定"""
self.voicevox_volume = float(value) / 100.0
# 設定を更新
self.config["voice_volume"] = self.voicevox_volume
save_config(self.config) # 設定を即時保存
def set_sound_volume(self, value):
"""効果音の音量を設定"""
self.sound_volume = float(value) / 100.0
sound_manager.set_volume(self.sound_volume)
# 設定を更新
self.config["sound_volume"] = self.sound_volume
save_config(self.config) # 設定を即時保存
def setup_training_screen(self):
"""トレーニング画面のセットアップ"""
# 既存のウィジェットをクリア
for widget in self.root.winfo_children():
widget.destroy()
# 現在のエクササイズデータ
exercise_data = self.training_data[self.current_exercise_index]
# 種目名
name_label = tk.Label(self.root, text=exercise_data["name"], font=("Helvetica", 20, "bold"))
name_label.pack(pady=20)
# 画像表示部分
try:
# 画像が存在するか確認
if os.path.exists(exercise_data["image_path"]):
img = Image.open(exercise_data["image_path"])
img = img.resize((300, 200), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
img_label = tk.Label(self.root, image=photo)
img_label.image = photo # 参照を保持
img_label.pack(pady=10)
else:
# 画像がない場合はテキストで代替
img_label = tk.Label(self.root, text=f"[{exercise_data['name']}のイメージ]",
font=("Helvetica", 12), width=40, height=10,
relief=tk.RIDGE, bg="#f0f0f0")
img_label.pack(pady=10)
print(f"画像ファイルが見つかりません: {exercise_data['image_path']}")
except Exception as e:
# エラー時はテキストで代替
img_label = tk.Label(self.root, text="[画像読み込みエラー]", font=("Helvetica", 12),
width=40, height=10, relief=tk.RIDGE, bg="#ffe0e0")
img_label.pack(pady=10)
print(f"画像読み込みエラー: {e}")
# 説明テキスト
desc_label = tk.Label(self.root, text=exercise_data["description"], font=("Helvetica", 12),
justify=tk.LEFT, wraplength=600)
desc_label.pack(pady=20)
# タイマーラベル
self.timer_label = tk.Label(self.root, text=f"残り時間: {self.remaining_time}秒", font=("Helvetica", 16))
self.timer_label.pack(pady=20)
# ステータスラベル (休憩中かどうか)
status_text = "カウントダウン中" if self.is_countdown else ("休憩中" if self.is_rest else "トレーニング中")
color = "orange" if self.is_countdown else ("blue" if self.is_rest else "green")
self.status_label = tk.Label(self.root, text=status_text, font=("Helvetica", 14), fg=color)
self.status_label.pack(pady=10)
# 中止ボタン
stop_button = tk.Button(self.root, text="中止", font=("Helvetica", 12),
command=self.stop_training, bg="#f44336", fg="white")
stop_button.pack(pady=20)
def setup_complete_screen(self):
"""終了画面のセットアップ"""
# 既存のウィジェットをクリア
for widget in self.root.winfo_children():
widget.destroy()
# 完了メッセージ
complete_label = tk.Label(self.root, text="トレーニング完了!", font=("Helvetica", 24, "bold"), fg="green")
complete_label.pack(pady=100)
message_label = tk.Label(self.root, text="お疲れ様でした!", font=("Helvetica", 16))
message_label.pack(pady=20)
# ホームに戻るボタン
home_button = tk.Button(self.root, text="ホームに戻る", font=("Helvetica", 14),
command=self.setup_start_screen, bg="#2196F3", fg="white", padx=10, pady=5)
home_button.pack(pady=50)
# 完了音を再生
try:
sound_manager.play_sound(self.sound_complete)
except Exception as e:
print(f"音声再生エラー: {e}")
# 音声アナウンス
self.speak("トレーニングが終了しました。お疲れ様でした!")
def start_training(self):
"""トレーニングを開始する"""
self.is_training = True
self.current_exercise_index = 0
self.is_rest = False
self.is_countdown = False
self.countdown_played = False
# 準備時間から開始
self.start_preparation()
def start_preparation(self):
"""準備時間を開始する"""
self.is_preparing = True
self.remaining_time = 30 # 準備時間は30秒固定
# 準備画面に切り替え
self.setup_preparation_screen()
# 開始音を再生
try:
sound_manager.play_sound(self.sound_start)
except Exception as e:
print(f"音声再生エラー: {e}")
# 準備開始をアナウンス
self.speak("トレーニングの準備をしましょう。30秒後に開始します。")
# タイマー開始
self.start_timer()
def setup_preparation_screen(self):
"""準備画面のセットアップ"""
# 既存のウィジェットをクリア
for widget in self.root.winfo_children():
widget.destroy()
# 準備表示
title_label = tk.Label(self.root, text="準備時間", font=("Helvetica", 24, "bold"), fg="purple")
title_label.pack(pady=20)
# 最初のエクササイズを表示
first_exercise = self.training_data[self.current_exercise_index]
next_text = f"最初のエクササイズ: {first_exercise['name']}"
next_label = tk.Label(self.root, text=next_text, font=("Helvetica", 16))
next_label.pack(pady=10)
# 説明
instruction_text = "これから腹筋トレーニングを始めます。\n姿勢を整え、水分補給をして準備してください。"
instruction_label = tk.Label(self.root, text=instruction_text, font=("Helvetica", 14), wraplength=600)
instruction_label.pack(pady=20)
# タイマーラベル
self.timer_label = tk.Label(self.root, text=f"残り時間: {self.remaining_time}秒", font=("Helvetica", 16))
self.timer_label.pack(pady=20)
# ステータスラベル
self.status_label = tk.Label(self.root, text="準備中", font=("Helvetica", 14), fg="purple")
self.status_label.pack(pady=10)
# 中止ボタン
stop_button = tk.Button(self.root, text="中止", font=("Helvetica", 12),
command=self.stop_training, bg="#f44336", fg="white")
stop_button.pack(pady=20)
# スキップボタン
skip_button = tk.Button(self.root, text="準備をスキップ", font=("Helvetica", 12),
command=self.skip_preparation, bg="#ff9800", fg="white")
skip_button.pack(pady=10)
def skip_preparation(self):
"""準備時間をスキップする"""
if messagebox.askyesno("確認", "準備時間をスキップしますか?"):
self.is_preparing = False
self.start_first_exercise()
def stop_training(self):
"""トレーニングを中止する"""
if messagebox.askyesno("確認", "トレーニングを中止しますか?"):
self.is_training = False
if self.timer_thread is not None and self.timer_thread.is_alive():
# タイマースレッドは自動的に終了する
pass
self.setup_start_screen()
def start_timer(self):
"""タイマーを開始する"""
if self.timer_thread is not None and self.timer_thread.is_alive():
return # すでに動作中なら何もしない
self.timer_thread = threading.Thread(target=self.timer_loop)
self.timer_thread.daemon = True
self.timer_thread.start()
def timer_loop(self):
"""タイマーのメインループ"""
try:
while self.is_training and self.remaining_time > 0:
try:
# カウントダウン表示が必要か確認
if self.remaining_time <= 5 and not self.is_countdown:
self.is_countdown = True
self.countdown_played = False # カウントダウン開始時にフラグをリセット
# UIの更新(この部分は安全のためafterを使用)
self.root.after(0, self.update_status_display)
# カウントダウン音を再生(5秒のタイミングで一度だけ再生)
if self.remaining_time == 5 and not self.countdown_played:
self.countdown_played = True # フラグを設定して再度再生されないようにする
# カウントダウン音を直接再生
try:
print(f"カウントダウン音を再生します... 残り時間: {self.remaining_time}秒")
if os.path.exists(self.sound_countdown):
result = sound_manager.play_sound(self.sound_countdown)
if result:
print(f"カウントダウン音の再生を開始しました")
else:
print(f"カウントダウン音の再生に失敗しました")
else:
print(f"カウントダウン音ファイルが見つかりません: {self.sound_countdown}")
except Exception as e:
print(f"カウントダウン音再生エラー: {e}")
# ビープ音を鳴らす(カウントダウン中の各秒、残り5,4,3,2,1秒のときだけ)
if self.remaining_time <= 5 and self.remaining_time >= 1:
# ビープ音を直接再生
try:
print(f"ビープ音を再生します... 残り時間: {self.remaining_time}秒")
if os.path.exists(self.sound_beep):
result = sound_manager.play_sound(self.sound_beep)
if result:
print(f"ビープ音の再生を開始しました")
else:
print(f"ビープ音の再生に失敗しました")
else:
print(f"ビープ音ファイルが見つかりません: {self.sound_beep}")
except Exception as e:
print(f"ビープ音再生エラー: {e}")
# UIの更新はメインスレッドで行う
self.root.after(0, self.update_timer_display)
except Exception as e:
print(f"タイマーループ内でエラーが発生しました: {e}")
# エラーが発生しても続行
# 1秒待機して残り時間を減らす
time.sleep(1)
self.remaining_time -= 1
# タイマー終了時の処理(必ずメインスレッドで実行)
if not self.is_training:
return
# 次のアクションの前にカウントダウン状態をリセット
self.is_countdown = False
self.countdown_played = False
# このタイマーループ終了後の次のアクションを決定
if self.is_preparing:
# 準備時間終了、最初のエクササイズへ
self.root.after(0, self.start_first_exercise)
elif self.is_rest:
# 休憩終了、次のエクササイズへ
self.root.after(0, self.after_rest_completed)
else:
# エクササイズ終了、休憩か完了へ
if self.current_exercise_index < len(self.training_data) - 1:
# まだエクササイズが残っている場合は休憩
self.root.after(0, self.start_rest)
else:
# すべてのエクササイズが終了
self.root.after(0, self.setup_complete_screen)
except Exception as e:
print(f"タイマーループで重大なエラーが発生しました: {e}")
# 重大なエラーの場合は安全に終了
self.root.after(0, self.stop_training)
def start_first_exercise(self):
"""最初のエクササイズを開始する"""
self.is_preparing = False
self.is_countdown = False
self.countdown_played = False
# 選択された難易度に応じた時間を設定
difficulty = self.selected_difficulty.get()
self.exercise_time = self.difficulty_times[difficulty]
self.remaining_time = self.exercise_time
# トレーニング画面に切り替え
self.setup_training_screen()
# 開始音を再生
try:
sound_manager.play_sound(self.sound_start)
except Exception as e:
print(f"音声再生エラー: {e}")
# 音声アナウンス - 名前と説明を読み上げる
exercise_data = self.training_data[self.current_exercise_index]
self.speak(f"{exercise_data['name']}を始めます。{exercise_data['description']}")
# タイマー開始
self.start_timer()
def after_rest_completed(self):
"""休憩終了後の処理"""
self.is_rest = False
self.is_countdown = False
self.countdown_played = False
self.current_exercise_index += 1
if self.current_exercise_index >= len(self.training_data):
# すべてのエクササイズが終了
self.setup_complete_screen()
return
# 次のエクササイズ時間をセット
self.remaining_time = self.exercise_time
self.setup_training_screen()
# 次のエクササイズをアナウンス - 名前と説明を読み上げる
exercise_data = self.training_data[self.current_exercise_index]
self.speak(f"{exercise_data['name']}を始めます。{exercise_data['description']}")
# 開始音を再生
try:
sound_manager.play_sound(self.sound_start)
except Exception as e:
print(f"音声再生エラー: {e}")
# 新しいタイマーを開始
self.start_timer()
def start_rest(self):
"""休憩時間開始"""
self.is_rest = True
self.is_countdown = False
self.countdown_played = False
self.remaining_time = 10 # 休憩時間は10秒固定
# 休憩画面に切り替え
self.setup_rest_screen()
# 休憩をアナウンス
self.speak("休憩時間です")
# 休憩音を再生
try:
sound_manager.play_sound(self.sound_rest)
except Exception as e:
print(f"音声再生エラー: {e}")
# 新しいタイマーを開始
self.start_timer()
def setup_rest_screen(self):
"""休憩画面のセットアップ"""
# 既存のウィジェットをクリア
for widget in self.root.winfo_children():
widget.destroy()
# 休憩表示
title_label = tk.Label(self.root, text="休憩時間", font=("Helvetica", 24, "bold"), fg="blue")
title_label.pack(pady=20)
# 次のエクササイズ名を表示(最後のエクササイズでなければ)
if self.current_exercise_index < len(self.training_data) - 1:
next_exercise = self.training_data[self.current_exercise_index + 1]
next_text = f"次は: {next_exercise['name']}"
next_label = tk.Label(self.root, text=next_text, font=("Helvetica", 16))
next_label.pack(pady=10)
# 休憩画像表示
try:
# 休憩画像が存在するか確認
if os.path.exists(self.rest_image_path):
img = Image.open(self.rest_image_path)
img = img.resize((300, 200), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
img_label = tk.Label(self.root, image=photo)
img_label.image = photo # 参照を保持
img_label.pack(pady=20)
else:
# 画像がない場合はテキストで代替
img_label = tk.Label(self.root, text="[休憩中]",
font=("Helvetica", 16), width=40, height=10,
relief=tk.RIDGE, bg="#e0f0ff")
img_label.pack(pady=20)
print(f"休憩画像ファイルが見つかりません: {self.rest_image_path}")
except Exception as e:
# エラー時はテキストで代替
img_label = tk.Label(self.root, text="[休憩中]",
font=("Helvetica", 16), width=40, height=10,
relief=tk.RIDGE, bg="#e0f0ff")
img_label.pack(pady=20)
print(f"休憩画像読み込みエラー: {e}")
# 休憩メッセージ
message = "深呼吸して、次のエクササイズに備えましょう"
message_label = tk.Label(self.root, text=message, font=("Helvetica", 14))
message_label.pack(pady=20)
# タイマーラベル
self.timer_label = tk.Label(self.root, text=f"残り時間: {self.remaining_time}秒", font=("Helvetica", 16))
self.timer_label.pack(pady=20)
# ステータスラベル
self.status_label = tk.Label(self.root, text="休憩中", font=("Helvetica", 14), fg="blue")
self.status_label.pack(pady=10)
# 中止ボタン
stop_button = tk.Button(self.root, text="中止", font=("Helvetica", 12),
command=self.stop_training, bg="#f44336", fg="white")
stop_button.pack(pady=20)
# ステータス表示を更新
self.update_status_display()
def update_timer_display(self):
"""タイマー表示を更新"""
if hasattr(self, 'timer_label'):
self.timer_label.config(text=f"残り時間: {self.remaining_time}秒")
def update_status_display(self):
"""ステータス表示を更新"""
if hasattr(self, 'status_label'):
status_text = "カウントダウン中" if self.is_countdown else ("休憩中" if self.is_rest else "準備中" if self.is_preparing else "トレーニング中")
color = "orange" if self.is_countdown else ("blue" if self.is_rest else "purple" if self.is_preparing else "green")
self.status_label.config(text=status_text, fg=color)
def speak(self, text):
"""テキストを音声で読み上げる(VoiceVox使用)"""
# 音声合成と再生のための別スレッドを起動
# 別スレッドでは先に音声ファイルを生成してからメインスレッドに通知する
def _speak():
# VoiceVoxが利用可能か確認
if not voicevox_manager.check_api_available():
# テキスト表示のみ
print(f"音声ガイド(テキストのみ): {text}")
return
# テキストとスピーカーIDからファイル名を生成(一意のIDとなるようにハッシュ化)
import hashlib
text_hash = hashlib.md5(f"{text}_{self.voicevox_speaker}".encode()).hexdigest()
cached_filename = f"{voicevox_manager.output_dir}/cache_{text_hash}.wav"
# キャッシュファイルが存在するか確認
if os.path.exists(cached_filename):
print(f"キャッシュから音声を使用: {text}")
# キャッシュを使用
self.root.after(10, lambda: self._play_speech(cached_filename, False))
return
# キャッシュがない場合は新規に音声合成
print(f"新規に音声合成: {text}")
wav_file = voicevox_manager.generate_speech(
text,
speaker_id=self.voicevox_speaker,
volume=self.voicevox_volume
)
if wav_file:
# 生成されたファイルをキャッシュとしてコピー
try:
import shutil
shutil.copy2(wav_file, cached_filename)
# 生成が完了したら、メインスレッドで再生を行う(UIをブロックしない)
self.root.after(10, lambda: self._play_speech(wav_file, True))
except Exception as e:
print(f"キャッシュ保存エラー: {e}")
# エラー時は元のファイルを再生
self.root.after(10, lambda: self._play_speech(wav_file, True))
# 音声合成は時間がかかるので別スレッドで実行
thread = threading.Thread(target=_speak)
thread.daemon = True
thread.start()
def _play_speech(self, wav_file, delete_after_play=True):
"""生成済みの音声ファイルを再生する"""
try:
# pydubを使用して音量調整付きで再生
from pydub import AudioSegment
from pydub.playback import play
# 音声ファイルを読み込み、音量調整
sound = AudioSegment.from_wav(wav_file)
sound = sound - (60 * (1.0 - self.voicevox_volume)) # 音量を調整(0.0 - 1.0の範囲)
# 変数をキャプチャして内部関数のスコープ問題を回避
file_to_delete = wav_file if delete_after_play else None
# 音声再生を別スレッドで実行してタイマーをブロックしないようにする
def play_sound_thread():
try:
# すでに読み込み済みのsoundオブジェクトを使用
play(sound)
# 一時ファイルを削除(キャッシュの場合は削除しない)
if file_to_delete:
try:
os.remove(file_to_delete)
except Exception as e:
print(f"ファイル削除エラー: {e}")
except Exception as e:
print(f"音声再生スレッドエラー: {e}")
# 別スレッドで音声再生
play_thread = threading.Thread(target=play_sound_thread)
play_thread.daemon = True
play_thread.start()
return True
except Exception as e:
print(f"音声再生エラー: {e}")
return False
def on_closing(self):
"""アプリ終了時の処理"""
if self.is_training:
if not messagebox.askyesno("確認", "トレーニング中ですが、終了しますか?"):
return
# 設定を保存
self.config["voice_style"] = self.voicevox_speaker
save_config(self.config)
# VoiceVoxを終了
voicevox_manager.stop_voicevox()
# アプリケーション終了
self.root.destroy()
def main():
"""アプリケーションのメイン関数"""
# アセットディレクトリの作成
os.makedirs(os.path.join(ASSETS_DIR, "sounds"), exist_ok=True)
os.makedirs(os.path.join(ASSETS_DIR, "images"), exist_ok=True)
os.makedirs(os.path.join(ASSETS_DIR, "tts_output"), exist_ok=True)
# 起動メッセージと環境情報
print("=" * 60)
print("ものぐさでも出来る寝ながら集中腹筋鍛錬")
print("=" * 60)
print(f"OS: {platform.system()} {platform.release()}")
print(f"Python: {platform.python_version()}")
print("-" * 60)
# VoiceVoxの情報表示
print("【VoiceVox設定】")
print("・自動検出: " + ("有効" if voicevox_manager.autostart else "無効"))
print("・手動設定方法: 環境変数 VOICEVOX_PATH にVoiceVoxの実行ファイルパスを設定")
print(" 例: set VOICEVOX_PATH=C:\\path\\to\\VOICEVOX.exe")
if voicevox_manager.check_api_available():
print("✓ VoiceVoxサーバーは既に起動しています")
else:
voicevox_path = voicevox_manager.find_voicevox_path()
if voicevox_path:
print(f"✓ VoiceVoxの実行ファイルが見つかりました: {voicevox_path}")
else:
print("✗ VoiceVoxの実行ファイルが見つかりませんでした")
print(" ・音声ガイドを使用するにはVoiceVoxをインストールしてください")
print(" ・https://voicevox.hiroshiba.jp/ からダウンロード可能です")
print("-" * 60)
# 設定情報の表示
config = load_config()
print(f"音声ボリューム: {int(config['voice_volume'] * 100)}%")
print(f"効果音ボリューム: {int(config['sound_volume'] * 100)}%")
print(f"音声スタイル: {config['voice_style']}")
print("-" * 60)
# アプリケーション起動
root = tk.Tk()
app = TrainingApp(root)
root.mainloop()
if __name__ == "__main__":
main()

自分専用 超簡単筋トレアプリ

寝たままできる、超初心者向け筋トレに特化した個人用アプリです。 下腹部・腹筋・背筋を対象に、5つの簡単な筋トレメニューを順番に行うことができます。

特徴

  • 寝たままできる5種類の筋トレメニュー
  • 難易度選択機能(軽め/通常/重め)
  • ずんだもん音声ガイドと効果音でサポート
  • 各種目・休憩終了時には5秒のカウントダウン
  • わかりやすいイラスト表示

セットアップ方法

  1. 「ずんだもん」音声を使用するには:

    • VoiceVoxをインストール・起動してください(詳細は VOICEVOX_SETUP.md を参照)
    • VoiceVoxが使用できない場合でも、テキスト表示のみで動作します
  2. アプリの実行:

python training_app.pyw

使い方

  1. スタート画面で難易度を選択(軽め/通常/重め)
  2. 音声スタイルを選択(ずんだもん ノーマル/あまあま)
  3. スタートボタンを押してトレーニング開始
  4. 表示される指示に従って筋トレを行う
  5. 種目終了5秒前にはカウントダウンとビープ音が鳴ります
  6. 全メニュー完了後、終了画面が表示される

新機能

  • ずんだもん音声: VoiceVox連携でずんだもんの声によるガイドが可能になりました
  • カウントダウン: 各種目と休憩の終了5秒前からカウントダウンとビープ音がなります
  • 安定した画面遷移: 休憩時間やカウントダウンがスムーズに動作するように改善

備考

  • 画像フォルダに筋トレのイラストを保存してください(exercise0.pngなどのダミー画像が初期設定)
  • 音声ガイドが不要な場合は、VoiceVoxを起動しないでください

ものぐさでも出来る寝ながら集中腹筋鍛錬 仕様書(正式版)

概要

寝たままできる、初心者向けだけど少し本格的な筋トレに特化した個人用アプリ。
対象部位:下腹部・腹筋・背筋。
PythonでシンプルなGUIアプリとして実装する。

使用技術

  • Python
  • GUIライブラリ(例:Tkinter または PyQt)
  • 音声合成(pyttsx3ライブラリなど)
  • 効果音再生(playsoundまたはpygameなど)
  • 画像表示(種目ごとのイラスト用)

機能要件

トレーニング機能

  • 5つの固定メニューを順番に表示
  • 軽め/通常/重め モード選択(秒数変化)
    • 軽め:20秒
    • 通常:30秒
    • 重め:40秒
  • 各種目の間に10秒休憩
  • メニュー切り替え時に効果音を鳴らす
  • 音声ガイドで次の種目をアナウンス
  • 種目ごとにイラスト表示+簡単な説明文を表示

画面構成

  • スタート画面
    • タイトル表示
    • 難易度選択(軽め/通常/重め)
    • スタートボタン
  • トレーニング画面
    • 現在の種目名
    • 種目イラスト
    • 種目説明テキスト
    • タイマー表示
    • 効果音・音声ガイド再生
  • 終了画面
    • 「トレーニング完了!」メッセージ
    • 効果音再生

筋トレメニュー詳細

1. クランチ

  • 説明文:
    仰向けに寝て、両膝を曲げ、両手を耳の横に置きます。
    腹筋を意識しながら上半身を持ち上げ、肩甲骨が床から離れる程度まで上げます。
    ゆっくりと元の位置に戻し、反動を使わないようにしましょう。

  • イラスト:
    仮イメージ:上半身を少し持ち上げた姿


2. リバースクランチ

  • 説明文:
    仰向けに寝て、両膝を90度に曲げます。
    腹筋を使ってお尻を床から少し浮かし、膝を胸に近づけるようにします。
    ゆっくりと元の位置に戻し、下腹部を意識して行いましょう。

  • イラスト:
    仮イメージ:膝を胸に引き寄せる姿


3. バイシクルクランチ

  • 説明文:
    仰向けに寝て、両手を耳の横に置き、両膝を90度に曲げます。
    右肘を左膝に、左肘を右膝に交互に近づけるようにします。
    自転車をこぐようなリズムで、腹斜筋を意識しながら行いましょう。

  • イラスト:
    仮イメージ:肘と反対側の膝を近づける姿


4. レッグレイズ

  • 説明文:
    仰向けに寝て、両手を体の横に置きます。
    両足をそろえたまま、まっすぐ上に持ち上げ、床と垂直になるまで上げます。
    ゆっくりと下ろす際も床につけず、下腹部を意識して行いましょう。

  • イラスト:
    仮イメージ:足をまっすぐ持ち上げている姿


5. プランク

  • 説明文:
    うつ伏せになり、肘と前腕、つま先を床につけて体を支えます。
    体が一直線になるようにキープし、お腹を引き締めて腰が沈まないようにします。
    呼吸を止めず、体幹全体を意識して維持しましょう。

  • イラスト:
    仮イメージ:前腕とつま先で体を支える姿


効果音・音声ガイド要件

  • 効果音:開始音/休憩開始音/終了音
  • 音声ガイド:次に行う種目名を読み上げ

備考

  • イラストは最初は仮画像(後から差し替え可能に設計する)
  • 音声ガイドが不要な場合、設定でOFFにできるように拡張できる

自分専用 超簡単筋トレアプリ セットアップ手順

前提条件

  • Python 3.7以上がインストールされていること
  • pipパッケージ管理ツールが利用可能なこと

セットアップ手順

アプリの実行

以下のコマンドでアプリを起動します:

python training_app.pyw

トラブルシューティング

画像が表示されない場合

  • misc/assets/imagesフォルダが存在するか確認
  • imagesフォルダ内にexercise0.pngexercise5.png,rest.pngが存在するか確認

音声が再生されない場合

  • misc/assets/soundsフォルダが存在するか確認
  • soundsフォルダ内にstart.wavrest.wavcomplete.wavbeep.wavcountdown.mp3complete.mp3が存在するか確認
  • 音声合成エンジン(VOICEVOX)が正しくインストールされているか確認

Tkinterエラーが出る場合

  • WindowsならPythonインストール時にTkinterが含まれているはず
  • Linux(Ubuntu)の場合:sudo apt-get install python3-tk
  • Macの場合:Homebrew経由でpythonをインストールした場合は同梱されています

カスタマイズ

画像のカスタマイズ

misc/assets/imagesフォルダ内の画像ファイルを置き換えてください。ファイル名は変更しないでください:

  • exercise0.png
  • exercise1.png
  • exercise2.png
  • exercise3.png
  • exercise4.png
  • exercise5.png
  • rest.png

音声のカスタマイズ

misc/assets/soundsフォルダ内の音声ファイルを置き換えてください。ファイル名は変更しないでください:

  • start.wav
  • rest.wav
  • complete.wav
  • beep.wav
  • countdown.mp3
  • complete.mp3

ずんだもん音声でのトレーニングアプリのセットアップ

VoiceVoxのセットアップ

筋トレアプリで「ずんだもん」の声を使用するには、VoiceVoxという音声合成ソフトウェアを別途インストールして実行する必要があります。

1. VoiceVoxのダウンロードとインストール

  1. VoiceVoxの公式サイトからソフトウェアをダウンロードします:

  2. ダウンロードしたインストーラーを実行し、指示に従ってインストールします。

2. VoiceVoxエンジンの起動

アプリを使用する前に、VoiceVoxを起動しておく必要があります:

  1. インストールしたVoiceVoxを起動します。
  2. VoiceVoxが起動したら、そのまま最小化しておいても構いません。

3. 筋トレアプリを使用する

  1. VoiceVoxを起動した状態で筋トレアプリを起動します。
  2. スタート画面で音声として「ずんだもん(ノーマル)」または「ずんだもん(あまあま)」を選択します。
  3. トレーニング開始ボタンを押すと、ずんだもんの声でガイドが始まります。

トラブルシューティング

VoiceVox連携がうまくいかない場合:

  1. VoiceVoxが正しく起動しているか確認してください。
  2. 複数回VoiceVoxを起動していないか確認してください(ポート競合の可能性)。
  3. ファイアウォールがVoiceVoxのAPIへのアクセスをブロックしていないか確認してください。

もし音声連携がうまくいかない場合でも、アプリは音声ガイドなしで動作します。

その他のVoiceVox音声について

VoiceVoxには「ずんだもん」以外にも多くの音声が含まれています。コードを修正することで、他の音声に変更することも可能です。

利用可能な音声IDの一部:

  • 1: ずんだもん(ノーマル)
  • 2: ずんだもん(あたたかい)
  • 3: ずんだもん(あまあま)
  • 8: 春日部つむぎ
  • 10: 波音リツ
  • など

詳細については、VoiceVoxの公式ドキュメントを参照してください。

@roflsunriz
Copy link
Author

音声ガイダンスを使用するにはVoiceVoxが必要です

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment