# [メタ情報] # 識別子: ギターコードアプリ改良版_exe # 補足: 開発はAI Studio利用 / 利用サイト https://www.aki-f.com/revchord/ # [/メタ情報] 要約: 「マイギターコード」は、ギターコードの管理と自動化を行うPython製Macアプリです。ローカル上のExcel DBと連携し、画像選択時に指位置データやコードネームを自動表示します。Playwrightをヘッドレスで利用し、バックグラウンドでWebからコード情報を取得・自動同期。MacのGUIバグ回避のため`tk.Entry`を採用しています。ローカルMIDI機能が充実しており、`mido`仮想ポート経由でGarageBand/Cubase等のDAWへ和音や単音を発音。アルペジオ再生とMIDIファイル生成、Finderでのハイライト表示も可能です。オートセーブ、Excel行と画像ファイルの物理削除に対応。画像パスを引数に受け取り、自動モード切替・DB読込・MIDI発音を可能にする外部連携エンジンを搭載。Stream DeckやAutomatorのクイックアクションからの起動に対応し、クイックアクションでは既存アプリを終了しGarageBand/Cubase起動後に指定画像でアプリが立ち上がります。 /Users/XXXXXX/python_scripts/my_guitar_chord_exe.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 🎸 マイギターコード (my_guitar_chord.py) - 全自動同期オートメーション搭載モデル(Macウィジェット不具合解消版) - ExcelDBの保存先を Dropbox 内の指定フォルダへ完全固定 - Playwrightのブラウザプロセスを開いたまま保持し、十分な試聴時間と確認時間を確保 - 【修正】Macのttk.Entryのロックバグを回避するため、標準の安定した tk.Entry へ完全変更 - 画像ファイルを選択した瞬間に、B列・C列のデータが100%確実に画面に自動表示される永久稼働モデル - 【ローカルMIDI対応】mido仮想ポートを利用し、GarageBand等のDAWでローカル音源を直接鳴らす発音機能を追加 - 【ローカルMIDI専念】外部サイト側の試聴音をクリックする処理を停止し、GarageBandの高音質なMIDI音源のみが鳴るように最適化 - 【完全バックグラウンド動作】Playwrightをheadless=True(非表示モード)で起動し、ブラウザ画面を手前に一切出現させずに裏で静かにB情報を取得 - 【バグ修正】「変更」モードでの保存時(C列の反映)に、画面に表示されている最新のB列(サイト提供コード)もExcel DBへ同時に強制同期保存するよう改修 - 【オートセーブ完全搭載】手動の保存ボタンを押し忘れても、別画像選択時・タブ切替時・指板変更時・アプリ終了時に、画面上のB列・C列データをExcelに裏で100%自動保存する安心機構を確立 - 【MIDI出力Finder表示対応】現在のコード音をアルペジオで再生しつつ、そのMIDIノートファイル(.mid)を自動生成してFinderでハイライト表示(Cubase等へ直接ドラッグ&ドロップできます) - 【完全物理削除対応】削除時にExcel行とローカル画像を完全物理削除。削除完了時に画面の全データを完全消去してオートセーブの誤復元を根本防止 - 【外部起動連携追加】起動時に引数(画像パス)を受け取ることで、自動的に「変更」モード切替・画像ロード・DB読込・MIDI発音を実行するエンジンを追加 """ import os import sys import re import time import subprocess import webbrowser import tkinter as tk from tkinter import ttk, messagebox, filedialog import openpyxl # 外部依存ライブラリのインポート確認 try: from playwright.sync_api import sync_playwright except ImportError: print("\n⚠️ 必須ライブラリ 'playwright' がインストールされていません。") sys.exit(1) try: from PIL import Image, ImageTk, ImageDraw except ImportError: print("\n⚠️ 必須ライブラリ 'Pillow' (PIL) がインストールされていません。") sys.exit(1) # MIDI送信用ライブラリの読み込み try: import mido except ImportError: print("\n⚠️ 必須ライブラリ 'mido' または 'python-rtmidi' がインストールされていません。MIDI発音機能はスキップされます。") mido = None # ========================================== # ギター指板 物理パラメータ設定 (固定値) # ========================================== STRING_BASES = { 1: 29, # 1弦開放 = 29 2: 24, # 2弦開放 = 24 3: 20, # 3弦開放 = 20 4: 15, # 4弦開放 = 15 5: 10, # 5弦開放 = 10 6: 5 # 6弦開放 = 5 } # 📝 秘匿対策済みの Dropbox 保存先パス(実稼働時は手作業にて本番パスに書き換えてください) EXCEL_FILE_PATH = "/Volumes/NO3_SSD/Dropbox/dropbox_1/ギターコード作成/my_fretboard.xlsx" IMAGE_DIR_PATH = "/Volumes/NO3_SSD/Dropbox/dropbox_1/ギターコード作成/fretboard_image/" SHEET_NAME = "chordname" # 📝 共有GemのURL(実稼働時は手作業にて本番URLに書き換えてください) GEMINI_RESEARCH_URL = "https://gemini.google.com/gem/1Gjdll9O0GMRMeko3QxLKjkYtotJG90Fd?usp=sharing" class GuitarChordApp: def __init__(self, root, file_path=None): self.root = root self.root.title("マイギターコード Workbench (全自動同期モデル)") self.root.geometry("1250x780") # プレビュー領域追加に伴い横幅を拡張 # データ初期化 self.current_mode = tk.StringVar(value="追加") self.fret_positions = {i: "n" for i in range(1, 7)} self.matrix_buttons = {} self.tk_preview_img = None # ImageTkオブジェクト保持用 # Playwright の状態保持用 self.playwright_manager = None self.playwright_browser = None self.playwright_context = None self.playwright_page = None # 🚀 常時開放型 MIDI ポートのインスタンス初期化 self.midi_port = None self.init_midi_port() # ウィンドウの「×」ボタンを押して終了した際にブラウザのゾンビ化を防ぐ self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # Excelファイルの初期化確認 self.ensure_excel_db() # UIコンポーネント作成 self.create_widgets() self.update_fretboard_ui_state() # 🚀 起動時に引数(ファイルパス)が指定されている場合、GUI構築後に自動処理を開始 if file_path: self.root.after(100, lambda: self.auto_start_from_file(file_path)) def init_midi_port(self): """🎵 [MIDIポート初期化] 仮想MIDIポートを起動時に常時オープン保持する""" if mido is None: return try: self.midi_port = mido.open_output('MyGuitarApp Port', virtual=True) print("🎵 [MIDI] 仮想MIDIポート 'MyGuitarApp Port' を常時開放モードで起動しました。") except Exception as e: print(f"⚠️ [MIDI初期化エラー] 仮想ポートの常時開放に失敗しました: {e}") self.midi_port = None def on_closing(self): """アプリ終了時に自動保存を走らせ、ブラウザやMIDIポートも安全に閉じる""" self.trigger_auto_save_if_needed() # 🚀 アプリ終了前の自動セーブ self.close_playwright() # 🚀 常時開放していたMIDIポートを安全に閉鎖 if self.midi_port: try: self.midi_port.close() print("🎵 [MIDI] 常時開放MIDIポートを安全にクローズしました。") except Exception: pass self.midi_port = None self.root.destroy() def close_playwright(self): """保持しているPlaywrightの全オブジェクトをメモリ上から安全に完全破棄する""" if self.playwright_page: try: self.playwright_page.close() except Exception: pass self.playwright_page = None if self.playwright_context: try: self.playwright_context.close() except Exception: pass self.playwright_context = None if self.playwright_browser: try: self.playwright_browser.close() except Exception: pass self.playwright_browser = None if self.playwright_manager: try: self.playwright_manager.stop() except Exception: pass self.playwright_manager = None # プロセスの完全終了と解放を確実にするため、ごくわずかにスリープ time.sleep(0.5) # ========================================== # 1. Excel DB (openpyxl) 制御ロジック # ========================================== def ensure_excel_db(self): try: os.makedirs(os.path.dirname(EXCEL_FILE_PATH), exist_ok=True) if not os.path.exists(EXCEL_FILE_PATH): wb = openpyxl.Workbook() ws = wb.active ws.title = SHEET_NAME ws.append(["指位置文字列", "サイト提供コードネーム", "自分調査コードネーム"]) wb.save(EXCEL_FILE_PATH) else: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) if SHEET_NAME not in wb.sheetnames: ws = wb.create_sheet(title=SHEET_NAME) ws.append(["指位置文字列", "サイト提供コードネーム", "自分調査コードネーム"]) wb.save(EXCEL_FILE_PATH) except Exception as e: messagebox.showerror("Excel初期化エラー", f"Dropboxへのファイルアクセスに失敗しました:\n{e}") def save_or_update_excel_automatically(self, param_str, site_chord): """ 🚀 [新規・全自動同期エンジン] サイト確認(Playwright)完了時に、自動的にExcelへ新規追加、または既存レコードのB列を上書き更新する """ try: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) ws = wb[SHEET_NAME] target = str(param_str).strip().replace('"', '').replace("'", "") found_row = None # 重複検索 for row in range(2, ws.max_row + 1): db_val = str(ws.cell(row=row, column=1).value or "").strip().replace('"', '').replace("'", "") if db_val == target: found_row = row break if found_row: # 既に登録済みの場合は、B列(サイト提供)のみを最新情報で上書き更新(C列は以前の記述を維持) ws.cell(row=found_row, column=2).value = site_chord print(f"🔄 [自動同期] Excel内の指位置「{param_str}」のB列(サイト提供コード)を最新上書き更新しました。") else: # 新規の場合は、末尾に新規行として自動登録(C列は白紙) ws.append([param_str, site_chord, ""]) print(f"📝 [自動同期] 新規の指位置「{param_str}」をExcel DBに自動登録しました。") wb.save(EXCEL_FILE_PATH) return True except Exception as e: print(f"⚠️ [自動同期失敗] Excelへの自動書き込み中にエラーが発生しました: {e}") return False def save_to_excel(self, param_str, site_chord, user_chord): """Excel DBへ手動で新規レコードを登録・保存する""" try: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) ws = wb[SHEET_NAME] target = str(param_str).strip().replace('"', '').replace("'", "") found = False for row in range(2, ws.max_row + 1): db_val = str(ws.cell(row=row, column=1).value or "").strip().replace('"', '').replace("'", "") if db_val == target: # 既に同じ指位置データがある場合は二重登録を防ぎ上書き ws.cell(row=row, column=2).value = site_chord ws.cell(row=row, column=3).value = user_chord found = True break if not found: ws.append([param_str, site_chord, user_chord]) wb.save(EXCEL_FILE_PATH) return True except Exception as e: messagebox.showerror("保存エラー", f"Dropboxへの書き込みに失敗しました:\n{e}") return False def update_in_excel(self, param_str, site_chord, user_chord): """🚀 [バグ修正版] B列(サイト提供)とC列(自分調査コードネーム)を同時に上書き保存する""" try: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) ws = wb[SHEET_NAME] found = False target_val = str(param_str).strip().replace('"', '').replace("'", "") for row in range(2, ws.max_row + 1): db_val = str(ws.cell(row=row, column=1).value or "").strip().replace('"', '').replace("'", "") if db_val == target_val: # B列(サイト提供)・C列(自分調査)の双方を画面表示の内容に書き換えて永続保存 ws.cell(row=row, column=2).value = site_chord ws.cell(row=row, column=3).value = user_chord found = True break if found: wb.save(EXCEL_FILE_PATH) return True else: # 万が一データがなかった場合に備え、新規行としてA,B,Cを登録する安全設計 ws.append([param_str, site_chord, user_chord]) wb.save(EXCEL_FILE_PATH) return True except Exception as e: messagebox.showerror("更新エラー", f"Dropboxへの書き込みに失敗しました:\n{e}") return False def trigger_auto_save_if_needed(self): """ 💾 [オートセーブエンジン] 現在が「変更」モードかつ、画面上に有効な指位置情報が表示されている場合、 手動保存ボタンが押されなくても裏側で音もなく自動同期保存を完了させます。 """ if self.current_mode.get() == "変更": param_str = self.entry_param.get().strip() site_chord = self.entry_site_chord.get().strip() user_chord = self.entry_user_chord.get().strip() # デフォルト状態以外の有効な指位置キーが入っているか確認 if param_str and param_str != "n-n-n-n-n-n": self.update_in_excel(param_str, site_chord, user_chord) print(f"💾 [バックグラウンド同期] 指位置「{param_str}」の最新B・C情報をExcelへ自動上書き保存しました。") def delete_from_excel(self, param_str): """🚀 [完全削除仕様] Excelから対象レコード(行全体)を完全に物理削除する""" try: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) ws = wb[SHEET_NAME] found_row = None target_val = str(param_str).strip().replace('"', '').replace("'", "") for row in range(2, ws.max_row + 1): db_val = str(ws.cell(row=row, column=1).value or "").strip().replace('"', '').replace("'", "") if db_val == target_val: found_row = row break if found_row: ws.delete_rows(found_row, 1) # 1行丸ごと物理消去 wb.save(EXCEL_FILE_PATH) return True else: messagebox.showerror("エラー", f"指位置「{param_str}」が見つかりません。") return False except Exception as e: messagebox.showerror("削除エラー", f"Dropboxデータ削除に失敗しました:\n{e}") return False def clear_all_ui_data(self): """ 🧹 [完全初期化] 削除後に画面やメモリに残骸データが残らないよう、指板・Entryフィールドを完全に空の状態にクリーンアップします。 これによって、オートセーブが旧データを誤って復元保存する不具合を100%根本から遮断します。 """ self.fret_positions = {i: "n" for i in range(1, 7)} for s in [1, 2, 3, 4, 5, 6]: self.set_matrix_highlight(s, 0) self.entry_param.config(state="normal") self.entry_param.delete(0, tk.END) self.entry_param.insert(0, "n-n-n-n-n-n") if self.current_mode.get() != "追加": self.entry_param.config(state="readonly") self.entry_site_chord.config(state="normal") self.entry_site_chord.delete(0, tk.END) self.entry_user_chord.config(state="normal") self.entry_user_chord.delete(0, tk.END) self.apply_field_states_by_mode() self.clear_image_preview("削除完了") def read_from_excel(self, param_str): """ Excel DB(A列)からレコードを高度なクレンジングを施して検索する """ try: wb = openpyxl.load_workbook(EXCEL_FILE_PATH) ws = wb[SHEET_NAME] target = str(param_str).strip().replace('"', '').replace("'", "") print(f"\n📊 [Excel全行走査デバッグ] -----------------------------") print(f" 🔍 ターゲット(探しているキー): '{target}'") print(f" 📂 Excelファイルパス: {EXCEL_FILE_PATH}") print(f" 📝 登録レコード件数: {ws.max_row - 1} 件") matched_data = None for row in range(2, ws.max_row + 1): cell_val = ws.cell(row=row, column=1).value if cell_val is not None: db_val = str(cell_val).strip().replace('"', '').replace("'", "") print(f" - 行 {row:03d} A列の生データ: '{cell_val}' ➔ クレンジング後: '{db_val}'") if db_val == target: matched_data = { "site_chord": ws.cell(row=row, column=2).value or "", "user_chord": ws.cell(row=row, column=3).value or "" } print(f" 👉 ➔ 一致しました! (行 {row:03d})") print(f"📊 ----------------------------------------------------") return matched_data except Exception as e: print(f"⚠️ [内部ログ] Excelデータの読込判定中にエラーが発生しました: {e}") return None # ========================================== # 2. GUI 画面描画 & インタラクション # ========================================== def create_widgets(self): main_paned_window = tk.Frame(self.root) main_paned_window.pack(fill="both", expand=True, padx=10, pady=10) left_area = tk.Frame(main_paned_window) left_area.pack(side="left", fill="both", expand=True) right_area = tk.LabelFrame(main_paned_window, text="指板画像プレビュー", padx=10, pady=10, font=("Arial", 10, "bold")) right_area.pack(side="right", fill="both", padx=10) # ----------------- 左側エリア構成 ----------------- # モード切替 mode_frame = tk.LabelFrame(left_area, text="モード切替タブ", padx=10, font=("Arial", 10, "bold")) mode_frame.pack(fill="x", pady=5) modes = ["追加", "変更", "削除"] for m in modes: rb = ttk.Radiobutton(mode_frame, text=m, value=m, variable=self.current_mode, command=self.on_mode_changed) rb.pack(side="left", padx=20, pady=5) # 指板マトリックス self.fret_frame = tk.LabelFrame(left_area, text="指板マトリックス (6弦[下]〜1弦[上])", padx=10, pady=10, font=("Arial", 10, "bold")) self.fret_frame.pack(fill="both", expand=True, pady=5) tk.Label(self.fret_frame, text="弦 / フレット", font=("Arial", 10, "bold")).grid(row=0, column=0, padx=5, pady=2) tk.Label(self.fret_frame, text="ミュート", font=("Arial", 9)).grid(row=0, column=1, padx=2, pady=2) tk.Label(self.fret_frame, text="開放(0)", font=("Arial", 9)).grid(row=0, column=2, padx=2, pady=2) for f in range(1, 13): tk.Label(self.fret_frame, text=f"{f}f", font=("Arial", 9)).grid(row=0, column=f+2, padx=2, pady=2) for string_idx in [1, 2, 3, 4, 5, 6]: tk.Label(self.fret_frame, text=f"{string_idx}弦", font=("Arial", 10, "bold")).grid(row=string_idx, column=0, padx=5, pady=2) for fret_view_idx in range(14): if fret_view_idx == 0: text_label = "n" elif fret_view_idx == 1: text_label = "0" else: text_label = str(fret_view_idx - 1) btn = tk.Button( self.fret_frame, text=text_label, width=4, height=1, relief="raised" ) # 🚀 指板クリックのイベントバインド btn.bind("", lambda e, s=string_idx, f=fret_view_idx: self.on_fret_clicked(e, s, f, False)) btn.bind("", lambda e, s=string_idx, f=fret_view_idx: self.on_fret_clicked(e, s, f, True)) btn.grid(row=string_idx, column=fret_view_idx+1, padx=1, pady=2) self.matrix_buttons[(string_idx, fret_view_idx)] = btn self.set_matrix_highlight(string_idx, 0) # データ入出力 io_frame = tk.LabelFrame(left_area, text="データ入出力フィールド", padx=10, pady=10, font=("Arial", 10, "bold")) io_frame.pack(fill="x", pady=10) # 🚀 [Mac状態ロックバグ回避] Mac環境でのttk.Entryの状態切替バグを防ぐため、標準の tk.Entry へ完全移行 tk.Label(io_frame, text="A: 指位置文字列:").grid(row=0, column=0, sticky="w", pady=5) self.entry_param = tk.Entry(io_frame, width=30, font=("Courier", 11, "bold"), relief="solid", bd=1) self.entry_param.grid(row=0, column=1, sticky="w", padx=5, pady=5) self.update_param_entry_text() tk.Label(io_frame, text="B: サイト提供コードネーム:").grid(row=1, column=0, sticky="w", pady=5) self.entry_site_chord = tk.Entry(io_frame, width=55, relief="solid", bd=1) self.entry_site_chord.grid(row=1, column=1, sticky="w", padx=5, pady=5) tk.Label(io_frame, text="C: 自分調査コードネーム:").grid(row=2, column=0, sticky="w", pady=5) self.entry_user_chord = tk.Entry(io_frame, width=55, relief="solid", bd=1) self.entry_user_chord.grid(row=2, column=1, sticky="w", padx=5, pady=5) # アクションボタンエリア action_frame = tk.Frame(left_area, pady=5) action_frame.pack(fill="x") self.btn_action = tk.Button( action_frame, text="指位置確認 (Playwright走らせる窓口)", font=("Arial", 12, "bold"), bg="#ccffcc", padx=20, pady=5, command=self.execute_main_action ) self.btn_action.pack(side="left", padx=5) self.btn_db_save = tk.Button( action_frame, text="Dropbox Excelへ保存(同時にサイトを閉じる)", font=("Arial", 12, "bold"), bg="#cce6ff", padx=20, pady=5, command=self.execute_db_save ) self.btn_db_save.pack(side="left", padx=10) # 【独立】調査ボタン(他アクションから明確に離して設置) self.btn_research = tk.Button( action_frame, text="🔍 コードネーム作成Gemを開く", font=("Arial", 11, "bold"), bg="#ffeb3b", fg="#000000", padx=15, pady=5, command=self.open_gemini_research ) self.btn_research.pack(side="right", padx=10) # ----------------- 右側エリア(プレビュー部)構成 ----------------- self.preview_canvas = tk.Label( right_area, text="[ 画像未選択 / 未登録 ]\n追加保存時、または変更モードでの\nファイル選択時にここにプレビュー表示されます。", relief="solid", bd=1, width=40, height=18, bg="#fafafa", fg="#777777" ) self.preview_canvas.pack(fill="both", expand=True, pady=10) self.btn_select_image = tk.Button( right_area, text="🖼️ 画像ファイル選択 (Mac Finder)", font=("Arial", 11, "bold"), bg="#e0e0e0", pady=5, command=self.select_image_file, state="disabled" ) self.btn_select_image.pack(fill="x", pady=5) self.btn_play_selected = tk.Button( right_area, text="▶️ 選択した画像の音を再生 (Playwright)", font=("Arial", 11, "bold"), bg="#ffe0b2", pady=5, command=self.play_sound_from_image, state="disabled" ) self.btn_play_selected.pack(fill="x", pady=5) # 🚀 【新ボタン追加】 self.btn_play_and_export_midi = tk.Button( right_area, text="🎵 MIDIをドラッグ&ドロップ(Finderが開きます)", font=("Arial", 11, "bold"), bg="#a3e4d7", pady=5, command=self.play_and_export_midi, state="disabled" ) self.btn_play_and_export_midi.pack(fill="x", pady=5) # [デザイン大改修] 背景をダークグレー(#2b2b2b)にして、文字を「真っ白」にし、級数を一回り大きくリデザイン hint_frame = tk.Frame(right_area, pady=12, padx=12, bg="#2b2b2b", relief="solid", bd=1) hint_frame.pack(fill="x", pady=10) hint_title = tk.Label( hint_frame, text="💡 Finderで画像を選ぶときのMac豆知識", font=("Arial", 12, "bold"), fg="#ffffff", bg="#2b2b2b" # 12pt・真っ白文字 ) hint_title.pack(anchor="w", pady=(0, 5)) hint_text = ( "・【最推奨】画像選択中に 「スペースキー」 を押すと、\n" " 巨大なプレビュー窓(Quick Look)がその場で開きます。\n\n" "・キーボード of 「Command + 3」 で右側にプレビューを常時表示。\n\n" "・アイコン表示中に 「Command + [+]」 で画像自体を拡大表示できます。" ) hint_desc = tk.Label( hint_frame, text=hint_text, justify="left", font=("Arial", 11), fg="#ffffff", bg="#2b2b2b", # 11pt・真っ白文字 wraplength=340 ) hint_desc.pack(anchor="w", pady=2) # ========================================== # 3. GUI イベント・状態ハンドラ # ========================================== def on_fret_clicked(self, event, string_idx, fret_view_idx, shift_pressed): """ 🎸 指板マトリックスボタン押下時のイベントハンドラ 通常左クリック:押下された弦の単音をGarageBandなどのDAWへ送る。 Shift+左クリック:その時点で設定されている和音(ストローク)全体を再生する。 """ if self.current_mode.get() == "削除": return # 🚀 フレットを変更して画面上の指位置データが切り替わる前に、現在の状態を自動保存 self.trigger_auto_save_if_needed() # 🚀 ミュート(n)以外であれば、対応するMIDIノートナンバーをあらかじめ計算保持する single_note = None if fret_view_idx != 0: fret_num = fret_view_idx - 1 single_note = STRING_BASES[string_idx] + fret_num + 35 if fret_view_idx == 0: self.fret_positions[string_idx] = "n" else: fret_num = fret_view_idx - 1 absolute_value = STRING_BASES[string_idx] + fret_num self.fret_positions[string_idx] = str(absolute_value) self.set_matrix_highlight(string_idx, fret_view_idx) self.update_param_entry_text() param_str = self.entry_param.get().strip() self.auto_load_excel_data(param_str) # 🚀 音声発音処理 (Playwright等は行わず、常時接続MIDI処理へ安全に引き継ぎ) if shift_pressed: self.play_midi_chord() elif single_note is not None: self.play_single_midi_note(single_note) def set_matrix_highlight(self, string_idx, active_fret_view_idx): for f_idx in range(14): btn = self.matrix_buttons[(string_idx, f_idx)] if f_idx == active_fret_view_idx: btn.config(bg="#ff9999", activebackground="#ff9999", highlightbackground="#ff9999", relief="sunken") else: btn.config(bg="#f0f0f0", activebackground="#e0e0e0", highlightbackground="#f0f0f0", relief="raised") def update_param_entry_text(self): param_str = "-".join([self.fret_positions[i] for i in [1, 2, 3, 4, 5, 6]]) self.entry_param.config(state="normal") self.entry_param.delete(0, tk.END) self.entry_param.insert(0, param_str) if self.current_mode.get() != "追加": self.entry_param.config(state="readonly") def parse_param_str_to_ui(self, param_str): parts = param_str.strip().split("-") if len(parts) != 6: return for i, val in enumerate(parts, 1): self.fret_positions[i] = val if val == "n": self.set_matrix_highlight(i, 0) else: try: abs_val = int(val) fret_num = abs_val - STRING_BASES[i] fret_view_idx = fret_num + 1 if 0 <= fret_view_idx < 14: self.set_matrix_highlight(i, fret_view_idx) except ValueError: self.set_matrix_highlight(i, 0) def auto_load_excel_data(self, param_str): """Excelからの一致判定による自動読込処理""" res = self.read_from_excel(param_str) # 🚀 tk.Entryに変更したため、どんな時でも100%確実に書き換え・挿入が成功します self.entry_site_chord.config(state="normal") self.entry_user_chord.config(state="normal") self.entry_site_chord.delete(0, tk.END) self.entry_user_chord.delete(0, tk.END) if res: print(f"🎯 [Excel同期成功] A={param_str} ➔ B={res['site_chord']}, C={res['user_chord']}") self.entry_site_chord.insert(0, res["site_chord"]) self.entry_user_chord.insert(0, res["user_chord"]) # 対応画像があればプレビューも自動同期 img_filename = f"guitar_fretboard_{param_str}.jpg" full_img_path = os.path.join(IMAGE_DIR_PATH, img_filename) if os.path.exists(full_img_path): self.load_image_preview(full_img_path) else: self.clear_image_preview("画像ファイル未検出") else: print(f"⚠️ [Excel同期スキップ] 指位置「{param_str}」は、現在Excel DB内に見つかりませんでした。") self.clear_image_preview("Excelデータ未登録") # 🚀 データをロードし直した瞬間は、未保存データはないため、保存ボタンを通常状態(薄青)にリセット if self.current_mode.get() == "変更": self.btn_db_save.config( text="上書き保存 (BとC列を反映)", bg="#cce6ff", fg="#000000", activebackground="#cce6ff" ) self.apply_field_states_by_mode() def apply_field_states_by_mode(self): """各モード別の入力フィールド制御""" mode = self.current_mode.get() if mode == "追加": self.entry_param.config(state="normal") self.entry_site_chord.config(state="normal") self.entry_user_chord.config(state="normal") elif mode == "変更": self.entry_param.config(state="readonly") self.entry_site_chord.config(state="readonly") self.entry_user_chord.config(state="normal") elif mode == "削除": self.entry_param.config(state="readonly") self.entry_site_chord.config(state="readonly") self.entry_user_chord.config(state="readonly") def on_mode_changed(self): # 🚀 別のモード(タブ)に切り替える直前に、現在の画面情報を自動保存 self.trigger_auto_save_if_needed() mode = self.current_mode.get() self.update_fretboard_ui_state() self.entry_site_chord.config(state="normal") self.entry_user_chord.config(state="normal") self.entry_site_chord.delete(0, tk.END) self.entry_user_chord.delete(0, tk.END) if mode == "追加": self.btn_action.config(text="指位置確認 (Playwright走らせる窓口)", state="normal") self.btn_db_save.config(text="Dropbox Excelへ保存(同時にサイトを閉じる)", state="normal", bg="#cce6ff", fg="#000000") self.btn_select_image.config(state="disabled") self.btn_play_selected.config(state="disabled") self.btn_play_and_export_midi.config(state="disabled") elif mode == "変更": self.btn_action.config(text="(Excelデータ自動読込中)", state="disabled") # 🚀 安定の薄青色(#cce6ff)を維持 self.btn_db_save.config(text="上書き保存 (BとC列を反映)", state="normal", bg="#cce6ff", fg="#000000") self.btn_select_image.config(state="normal") self.btn_play_selected.config(state="normal") self.btn_play_and_export_midi.config(state="normal") elif mode == "削除": self.btn_action.config(text="(Excelデータ自動読込中)", state="disabled") self.btn_db_save.config(text="このレコードを削除", state="normal", bg="#f0f0f0", fg="#000000") self.btn_select_image.config(state="disabled") self.btn_play_selected.config(state="disabled") self.btn_play_and_export_midi.config(state="disabled") self.update_param_entry_text() self.apply_field_states_by_mode() param_str = self.entry_param.get().strip() if param_str: self.auto_load_excel_data(param_str) def update_fretboard_ui_state(self): state_mode = "normal" if self.current_mode.get() in ["追加", "変更"] else "disabled" for (s, f), btn in self.matrix_buttons.items(): btn.config(state=state_mode) # ========================================== # 4. 画像プレビュー & Finder 連携ロジック # ========================================== def auto_start_from_file(self, file_path): """ 📁 [自動起動・外部連携エンジン] Finder等から画像パスを引数として受け取り、自動的に「変更」モードへ移行し、 画像ロード、ExcelDBからの読み込み、指板同期、MIDI発音を完全自動実行します。 """ if not file_path or not os.path.exists(file_path): print(f"⚠️ [自動起動スキップ] 指定されたファイルパスが存在しません: {file_path}") return filename = os.path.basename(file_path) match = re.search(r"([0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2})", filename) if not match: print(f"⚠️ [自動起動エラー] ファイル名から指位置文字列を抽出できませんでした: {filename}") return extracted_param = match.group(1).lower() print(f"🚀 [自動外部連携起動] 抽出指位置: {extracted_param}") # 1. アプリのモードを「変更」モードへ自動切り替え self.current_mode.set("変更") self.on_mode_changed() # 2. 該当画像をプレビュー領域にロード self.load_image_preview(file_path) # 3. 指板GUIとパラメータを抽出し同期 self.parse_param_str_to_ui(extracted_param) self.entry_param.config(state="normal") self.entry_param.delete(0, tk.END) self.entry_param.insert(0, extracted_param) self.entry_param.config(state="readonly") # 4. Excelデータベースから自動読み込みを行って画面に反映 self.auto_load_excel_data(extracted_param) # 5. 自動的にローカルMIDI発音を実行し、音を鳴らす self.play_midi_chord() # 画面の再描画を強制 self.root.update_idletasks() def load_image_preview(self, file_path): """Pillowを使用してプレビューを生成""" try: img = Image.open(file_path) img.thumbnail((320, 240)) self.tk_preview_img = ImageTk.PhotoImage(img) self.preview_canvas.config(image=self.tk_preview_img, text="") except Exception as e: self.clear_image_preview(f"プレビュー失敗\n{e}") def clear_image_preview(self, message): self.tk_preview_img = None self.preview_canvas.config(image="", text=f"[ {message} ]") def select_image_file(self): os.makedirs(IMAGE_DIR_PATH, exist_ok=True) # 🚀 1. 現在画面に表示されている指位置から、対象のファイル名を推測する param_str = self.entry_param.get().strip() initial_file = f"guitar_fretboard_{param_str}.jpg" if param_str and param_str != "n-n-n-n-n-n" else "" # 🚀 2. initialfile オプションを追加して、ダイアログを開く selected_file = filedialog.askopenfilename( initialdir=IMAGE_DIR_PATH, initialfile=initial_file, # ← これを追加することでハイライトされます title="指板画像ファイルを選択してください", filetypes=[("JPEG Images", "*.jpg"), ("PNG Images", "*.png"), ("All Files", "*.*")] ) if selected_file: # 🚀 新しい画像が読み込まれて画面情報が上書きされる前に、現在の表示データを自動保存 self.trigger_auto_save_if_needed() self.load_image_preview(selected_file) filename = os.path.basename(selected_file) # 🚀 ファイル名に「数字-数字-n...」という指位置さえ含まれていれば100%ぶち抜く万能正規表現 match = re.search(r"([0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2}-[0-9nN]{1,2})", filename) if match: extracted_param = match.group(1).lower() # 大文字Nを小文字nに統一 # 🚀 1. 抽出した指位置をTkinterの画面に反映 self.parse_param_str_to_ui(extracted_param) self.entry_param.config(state="normal") self.entry_param.delete(0, tk.END) self.entry_param.insert(0, extracted_param) self.entry_param.config(state="readonly") # 🚀 2. 自動的にExcelから該当コードネームを読み込んでTkinter画面に反映させる self.auto_load_excel_data(extracted_param) # 🚀 3. Tkinterの画面描画を強制アップデートして瞬時に画面に反映 self.root.update_idletasks() else: messagebox.showwarning("ファイル名不一致", "選択されたファイル名の中に指位置データ(例: 33-28-24-19-14-9)が検出できませんでした。") def play_sound_from_image(self): param_str = self.entry_param.get().strip() if not param_str: messagebox.showwarning("警告", "指位置文字列が確定していません。") return # 🚀 外部サイト確認と並行して、まずMacのローカルMIDIを発音 self.play_midi_chord() self.root.config(cursor="watch") self.root.update() result_chords = self.run_playwright_backend(param_str, download_image=False) self.root.config(cursor="") self.entry_site_chord.config(state="normal") self.entry_site_chord.delete(0, tk.END) self.entry_site_chord.insert(0, result_chords) self.apply_field_states_by_mode() # ========================================== # 🎨 5. ローカル指板画像・自己生成エンジン (Pillow・精密黄金レイアウト描画版) # ========================================== def generate_fretboard_image_locally(self, param_str, output_path): """ Pillow(PIL)を駆使して、マトリックス配列から極めて高精度な ギターのコードダイアグラム(JPG)を1から自己描画(レンダリング)する - 余白を精密設計:左右上下の切れを100%排除 """ try: parts = param_str.strip().split("-") if len(parts) != 6: return False # 各弦のフレット情報を特定 frets = {} for i, val in enumerate(parts, 1): if val == "n": frets[i] = "X" else: try: abs_val = int(val) fret_num = abs_val - STRING_BASES[i] frets[i] = fret_num except ValueError: frets[i] = "X" # 高画質なキャンバスを作成 (幅400px x 高さ300px, 背景白) img = Image.new("RGB", (400, 300), "white") draw = ImageDraw.Draw(img) # 🚀 左右上下マージンの安全領域設計 margin_left = 85 margin_top = 45 width = 240 height = 180 num_frets = 12 fret_w = width / num_frets string_h = height / 5 # 1. 縦のフレット線を描画 for f in range(num_frets + 1): x = margin_left + f * fret_w line_w = 4 if f == 0 else 1 draw.line([(x, margin_top), (x, margin_top + height)], fill="black", width=line_w) if f > 0: draw.text((x - fret_w / 2 - 3, margin_top + height + 5), str(f), fill="gray") # 2. 横の弦を描画(上から1弦〜下から6弦) for s in range(6): y = margin_top + s * string_h draw.line([(margin_left, y), (margin_left + width, y)], fill="black", width=2) labels = ["1st", "2nd", "3rd", "4th", "5th", "6th"] draw.text((margin_left - 55, y - 6), labels[s], fill="black") # 3. 押弦ポジション、開放弦、ミュートのマークを描画 for string_idx in range(1, 7): f = frets[string_idx] y = margin_top + (string_idx - 1) * string_h cx_left = margin_left - 25 if f == "X": r = 6 draw.line([(cx_left - r, y - r), (cx_left + r, y + r)], fill="red", width=2) draw.line([(cx_left - r, y + r), (cx_left + r, y - r)], fill="red", width=2) elif f == 0: r = 6 draw.ellipse([(cx_left - r, y - r), (cx_left + r, y + r)], outline="green", width=2) else: f_num = int(f) if 1 <= f_num <= 12: cx = margin_left + (f_num - 0.5) * fret_w cy = y r = 7 draw.ellipse([(cx - r, cy - r), (cx + r, cy + r)], fill="blue", outline="black") # 安全にディレクトリを生成して保存 os.makedirs(os.path.dirname(output_path), exist_ok=True) img.save(output_path, "JPEG", quality=95) print(f"🎨 [自己描画成功] Pillowによりコードダイアグラム(精密黄金余白版)をローカル生成しました: {output_path}") return True except Exception as e: print(f"⚠️ [自己描画失敗] Pillowレンダラー内で例外が発生しました: {e}") return False # ========================================== # 🎹 6. ローカルMIDI発音エンジン # ========================================== def play_midi_chord(self): """ 🎵 [MIDI発音エンジン - 和音] 現在の指位置データ(例: n-30-27-20-16-n)を解析し、 常時開放された仮想MIDIポートから低音弦(6弦)から高音弦(1弦)に向かってストローク発音させる。 """ if self.midi_port is None: self.init_midi_port() if self.midi_port is None: print("⚠️ MIDI再生不可: MIDIポートが初期化されていません。") return param_str = self.entry_param.get().strip() parts = param_str.split("-") if len(parts) != 6: return # 6弦(インデックス5)from1弦(インデックス0)に向かって順にノートを収集 midi_notes = [] for string_idx in range(6, 0, -1): val = parts[string_idx - 1] if val != "n": try: abs_val = int(val) # STRING_BASES基準値に35を加算して正確なMIDIノートナンバーに変換 midi_note = abs_val + 35 midi_notes.append(midi_note) except ValueError: continue if not midi_notes: return try: active_notes = [] # 6弦側から順にストローク(時間差)をつけて発音 for note in midi_notes: msg_on = mido.Message('note_on', note=note, velocity=85, time=0) self.midi_port.send(msg_on) active_notes.append(note) time.sleep(0.08) # ストローク(ジャランという時間差: 80ms) # 残響時間 time.sleep(1.2) # 全てのノートをオフにする for note in active_notes: msg_off = mido.Message('note_off', note=note, velocity=64, time=0) self.midi_port.send(msg_off) except Exception as e: print(f"⚠️ [MIDI再生エラー] 仮想ポート出力送信に失敗しました: {e}") self.midi_port = None def play_single_midi_note(self, note): """ 🎵 [単音MIDI発音機能] 常時開放された仮想MIDIポートを利用し、遅延なく単音を非同期・非ブロッキング発音。 """ if self.midi_port is None: self.init_midi_port() if self.midi_port is None: return try: msg_on = mido.Message('note_on', note=note, velocity=85, time=0) self.midi_port.send(msg_on) # 500ms後に非同期でノートオフを実行 self.root.after(500, lambda: self._turn_off_single_note(note)) except Exception as e: print(f"⚠️ [単音MIDI発音エラー] {e}") self.midi_port = None def _turn_off_single_note(self, note): """ 🎵 [非同期MIDIノートオフ] """ if self.midi_port is None: return try: msg_off = mido.Message('note_off', note=note, velocity=64, time=0) self.midi_port.send(msg_off) except Exception as e: print(f"⚠️ [単音MIDIノートオフ失敗] {e}") # ========================================== # 💾 7. MIDI file生成・Finder表示 # ========================================== def generate_midi_file(self, param_str, output_path): """ midoを使用して、現在のギターコードの押弦アルペジオ(80msずらし)を再現した 単発のコードMIDIファイル(.mid)を1から生成し保存します。 """ try: parts = param_str.strip().split("-") if len(parts) != 6: return False # 6弦から1弦方向(低音から高音)に向けてノートを計算 midi_notes = [] for string_idx in range(6, 0, -1): val = parts[string_idx - 1] if val != "n": try: abs_val = int(val) midi_note = abs_val + 35 midi_notes.append(midi_note) except ValueError: continue if not midi_notes: return False # MIDIファイルオブジェクト作成 (480 ticks per beat) mid = mido.MidiFile(ticks_per_beat=480) track = mido.MidiTrack() mid.tracks.append(track) # テンポ120の場合、1拍=500ms=480ticks。1ms = 0.96ticks。 # アルペジオの間隔 80ms = 77 ticks tick_offset = 77 # 1. 各弦 of 音を時間差で note_on for i, note in enumerate(midi_notes): delta = 0 if i == 0 else tick_offset track.append(mido.Message('note_on', note=note, velocity=85, time=delta)) # 2. 響かせる時間(約1拍 = 480 ticks) # 最後の音が鳴ってから 480 ticks 経ったのち、最初の音を note_off track.append(mido.Message('note_off', note=midi_notes[0], velocity=64, time=480)) # 3. 残りの音も 80ms(77ticks) 間隔で順次 note_off してアルペジオを完結させる for note in midi_notes[1:]: track.append(mido.Message('note_off', note=note, velocity=64, time=77)) os.makedirs(os.path.dirname(output_path), exist_ok=True) mid.save(output_path) return True except Exception as e: print(f"⚠️ [MIDIファイル自動生成エラー] {e}") return False def reveal_file_in_finder(self, file_path): """ subprocess.run(["open", "-R", file_path], check=True) を用いて、 指定されたファイルをFinder上で選択状態(ハイライト)にして表示し、 さらにFinderを最前面(アクティブ)にします。 """ try: # 1. ファイルを選択状態にしてFinderで表示 subprocess.run(["open", "-R", file_path], check=True) # 2. 🚀 Finderを確実に最前面に呼び出す(AppleScriptを実行) subprocess.run(["osascript", "-e", 'tell application "Finder" to activate'], check=True) return True except Exception as e: print(f"⚠️ [Finder表示失敗] {e}") return False def play_and_export_midi(self): """ 🎵 [音を再生 + MIDI Finder表示アクション] 現在のコード音をローカルDAWへ発音させると同時に、 一時MIDIファイルを自動生成し、Finderで自動ハイライト表示させてドラッグ&ドロップを促します。 """ param_str = self.entry_param.get().strip() if not param_str or param_str == "n-n-n-n-n-n": messagebox.showwarning("警告", "有効な指位置文字列が確定していません。") return # 1. まずローカルの仮想MIDIポートへ発音(DAWでの試聴) self.play_midi_chord() # 2. 一時フォルダにコードMIDIファイルを生成 temp_midi_path = os.path.join(IMAGE_DIR_PATH, "temp_chord.mid") if self.generate_midi_file(param_str, temp_midi_path): # 3. 生成したMIDIファイルをFinderで自動ハイライト表示 # 成功時のOKダイアログを廃止し、失敗時のみエラーを出す仕様に変更 if not self.reveal_file_in_finder(temp_midi_path): messagebox.showerror("エラー", "FinderでのMIDIファイル表示に失敗しました。") else: messagebox.showerror("エラー", "一時MIDIファイルの書き出しに失敗しました。") # ========================================== # 8. Playwright 持続型スクレイピングロジック # ========================================== def run_playwright_backend(self, s_param, download_image=False): self.close_playwright() target_url = f"https://www.aki-f.com/revchord/f-search.php?s={s_param}" chord_list = [] try: self.playwright_manager = sync_playwright().start() # 🚀 【ポップアップ根本対策】 # headless=True に変更し、ブラウザのGUIウィンドウ自体を最初から画面上に一切構築しない設定に切り替えました。 # これにより、OSのフォーカス強制移動が発生せず、Tkinter画面が1ミリも隠れることなく背後で静かにB情報をスクレイピング処理できます。 self.playwright_browser = self.playwright_manager.chromium.launch( headless=True, args=[ "--autoplay-policy=no-user-gesture-required", "--no-sandbox" ] ) self.playwright_context = self.playwright_browser.new_context(bypass_csp=True) self.playwright_context.add_init_script(""" (function() { window.addEventListener('DOMContentLoaded', () => { const ctxs = document.querySelectorAll('audio, video'); ctxs.forEach(m => { m.muted = false; m.play().catch(()=>{}); }); if (window.AudioContext) { new AudioContext().resume(); } }); })(); """) self.playwright_page = self.playwright_context.new_page() self.playwright_page.goto(target_url, timeout=15000) self.playwright_page.wait_for_load_state("domcontentloaded", timeout=10000) # 【自己描画画像自動保存】 if download_image: dest_img_path = os.path.join(IMAGE_DIR_PATH, f"guitar_fretboard_{s_param}.jpg") if self.generate_fretboard_image_locally(s_param, dest_img_path): self.root.after(0, lambda: self.load_image_preview(dest_img_path)) time.sleep(1.5) try: items = self.playwright_page.locator("li").all() for item in items: text = item.inner_text().strip() if "%" in text and "(" in text: chord_list.append(" ".join(text.split())) except Exception: pass # 念のためTkinterを手前にキープ self.root.lift() except Exception as e: self.close_playwright() return f"通信エラー ({str(e)[:30]})" return ", ".join(chord_list) if chord_list else "一致するコードネーム候補なし" # ========================================== # 9. アクション統合・独立調査窓口 # ========================================== def open_gemini_research(self): """【独立要件】別ウインドウで共有GemのURLを開く""" try: webbrowser.open(GEMINI_RESEARCH_URL) except Exception as e: messagebox.showerror("エラー", f"ブラウザ起動に失敗しました:\n{e}") def execute_main_action(self): mode = self.current_mode.get() param_str = self.entry_param.get().strip() if mode == "追加": # 🚀 外部サイト確認と並行して、まずMacのローカルMIDIを発音 self.play_midi_chord() self.root.config(cursor="watch") self.root.update() # 🚀 1. Playwrightを実行してコード情報を取得 result_chords = self.run_playwright_backend(param_str, download_image=True) # 🚀 2. 【最重要】取得した情報をその場で自動Excel保存&自動上書き更新 self.save_or_update_excel_automatically(param_str, result_chords) self.root.config(cursor="") self.entry_site_chord.config(state="normal") self.entry_site_chord.delete(0, tk.END) self.entry_site_chord.insert(0, result_chords) self.apply_field_states_by_mode() def execute_db_save(self): mode = self.current_mode.get() param_str = self.entry_param.get().strip() site_chord = self.entry_site_chord.get().strip() or "未解析" user_chord = self.entry_user_chord.get().strip() if not param_str: messagebox.showerror("エラー", "指位置文字列が空です。") return if mode == "追加": # 追加モードでの手動保存は念のため残しつつ、自動保存との二重バックアップ if self.save_to_excel(param_str, site_chord, user_chord): messagebox.showinfo("成功", f"Dropbox Excel DBへ指位置「{param_str}」を新規追加保存しました。") self.close_playwright() elif mode == "変更": # 🚀 引数にsite_chordを追加し、B列(サイト提供)とC列(自分調査)をExcelに同時同期保存するよう修正 if self.update_in_excel(param_str, site_chord, user_chord): messagebox.showinfo("成功", f"Excel DB of 指位置「{param_str}」を最新情報(B列: サイト提供, C列: 自分調査)で上書き更新しました。") # 🚀 [リセット] 保存成功に伴い、ボタンを通常の薄青色に戻します self.btn_db_save.config( text="上書き保存 (BとC列を反映)", bg="#cce6ff", fg="#000000", activebackground="#cce6ff" ) elif mode == "削除": if messagebox.askyesno("削除確認", f"本当に指位置「{param_str}」のレコードをExcelから完全に削除し、画像ファイルも削除しますか?"): # 🚀 1. Excelレコードの物理完全消去 if self.delete_from_excel(param_str): # 🚀 2. 画像ファイルのディスク上からの完全削除 img_filename = f"guitar_fretboard_{param_str}.jpg" full_img_path = os.path.join(IMAGE_DIR_PATH, img_filename) if os.path.exists(full_img_path): try: os.remove(full_img_path) print(f"🗑️ [物理画像消去] 画像ファイルを削除しました: {full_img_path}") except Exception as e: print(f"⚠️ [物理画像消去失敗] ファイルの削除に失敗しました: {e}") messagebox.showinfo("成功", f"指位置「{param_str}」のレコードおよび画像ファイルを完全に削除しました。") # 🚀 3. 【最重要】画面残骸データによるオートセーブ誤復元を防ぐため、UIとパラメータを完全初期化 self.clear_all_ui_data() if __name__ == "__main__": root = tk.Tk() # 🚀 コマンドライン引数を安全に解析。引数が1つ以上ある場合は画像ファイルパスとして認識 file_path = sys.argv[1] if len(sys.argv) > 1 else None app = GuitarChordApp(root, file_path=file_path) root.mainloop() ``` Stream Deck型 Automator アプリケーション シェルスクリプトを実行 /bin/zsh stdinへ ``` /Library/Frameworks/Python.framework/Versions/3.12/bin/python3 /Users/XXXXXX/python_scripts/my_guitar_chord_exe.py > /dev/null 2>&1 & ``` クイックアクション型 Automator アプリケーション シェルスクリプトを実行 /bin/zsh 引数として ``` #!/bin/zsh # 右クリックされた画像ファイルの絶対パスを取得 filepath="$1" # 空白チェック、および該当ファイルの存在確認 if [ -z "$filepath" ] || [ ! -f "$filepath" ]; then exit 0 fi # 🚀 【絶対排他キル・自爆回避モデル】 # 自身のシェルPIDを除外し、"my_guitar_chord" を含むPythonプロセスのみを安全・確実に全滅させます。 ps -ef | grep "my_guitar_chord" | grep -v "grep" | grep -v "$$" | awk '{print $2}' | xargs kill -9 > /dev/null 2>&1 # 1. GarageBandのプロジェクトファイルを open コマンドで安全に開く GB_PROJECT="/Users/XXXXXX/Music/GarageBand/ギターコードアプリ用.band" if [ -d "$GB_PROJECT" ] || [ -f "$GB_PROJECT" ]; then open "$GB_PROJECT" fi # 2. GarageBandが起動してポートが有効化されるのを待つため5秒スリープ sleep 5 # 3. Python3 の検出優先順位を「Tkinter同梱版(公式・システム標準)」に最適化 if [ -x "/usr/local/bin/python3" ]; then PYTHON_BIN="/usr/local/bin/python3" elif [ -x "/usr/bin/python3" ]; then PYTHON_BIN="/usr/bin/python3" elif [ -x "/opt/homebrew/bin/python3" ]; then PYTHON_BIN="/opt/homebrew/bin/python3" else PYTHON_BIN="python3" fi # 🚀 【Shigeyasu様 運用習慣・完全自動追従検知エンジン】 # exe.py が存在すれば本番起動、存在しなければ検証用 wip.py を自動選択して実行します。 if [ -f "/Users/XXXXXX/python_scripts/my_guitar_chord_exe.py" ]; then PYTHON_SCRIPT="/Users/XXXXXX/python_scripts/my_guitar_chord_exe.py" elif [ -f "/Users/XXXXXX/python_scripts/my_guitar_chord_wip.py" ]; then PYTHON_SCRIPT="/Users/XXXXXX/python_scripts/my_guitar_chord_wip.py" else # 最終フォールバック PYTHON_SCRIPT="/Users/XXXXXX/python_scripts/my_guitar_chord_exe.py" fi # 4. 判別されたスクリプトファイルをバックグラウンドで安全に起動 if [ -f "$PYTHON_SCRIPT" ]; then $PYTHON_BIN "$PYTHON_SCRIPT" "$filepath" > /dev/null 2>&1 & fi ```