# [メタ情報] # 識別子: 字幕かんたんエディタappの作成_exe # 補足: # [/メタ情報] 要約: 「字幕かんたんエディタ」は、macOS向けのVTT字幕作成・編集アプリケーションです。Python (`subtitle_easy_editor.py`, `vtt_parser_logic.py`) とHTML/JavaScript (`html/index.html`) で構成され、PyInstallerとcreate-dmgスクリプト (`build_dmg.sh`, `subtitle_easy_editor.spec`) を用いて`SubtitleEasyEditor.dmg`としてビルドされます。ビルド後、DMGと`readme.txt`をZIPにまとめ配布します。 本アプリは、ユーザーが選択した動画・音声ファイルと連携し、WaveSurfer.jsによる波形表示を介して字幕の開始・終了時刻やテキストを直感的に編集・追加できます。既存VTTの読み込みや新規作成に対応し、保存時には字幕一覧表示用のHTMLファイル(`_listJ.txt`)を自動生成する機能も持ちます。 初回起動時はmacOSのセキュリティにより「右クリック」→「開く」が必要です。以降はFinderから動画・音声ファイルをアプリで開くことで、エディタ画面が起動し、字幕作成・編集作業を開始できます。 subtitle_pkgフォルダの中身: subtitle_easy_editor.py readme.txt build_dmg.sh html/index.html subtitle_easy_editor.spec vtt_parser_logic.py ターミナルで以下を入力して、アプリのビルドをする pkill -f "subtitle_easy_editor" cd ~/Desktop/subtitle_pkg source venv/bin/activate bash build_dmg.sh ビルドが成功すると、 subtitle_pkgフォルダの中に、 SubtitleEasyEditor.dmg が生成される。 SubtitleEasyEditor_distフォルダを作り SubtitleEasyEditor.dmg readme.txt を入れて、 SubtitleEasyEditor_dist.zip を作り配布する。 subtitle_easy_editor.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import threading import socket import bottle import webview from vtt_parser_logic import generate_list_j def get_resource_path(relative_path): try: base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) class SubtitleAPI: def __init__(self): self.window = None self.current_media_path = None self.current_vtt_path = None self.media_port = None def get_media_url(self): if self.current_media_path and os.path.exists(self.current_media_path): return f"http://127.0.0.1:{self.media_port}/stream" return "" def get_vtt_content(self): if self.current_vtt_path and os.path.exists(self.current_vtt_path): try: with open(self.current_vtt_path, 'r', encoding='utf-8') as f: return f.read() except Exception as e: return str(e) return "" def select_existing_vtt(self): try: file_types = ('VTT Files (*.vtt;*.txt)', 'All Files (*.*)') result = self.window.create_file_dialog( webview.OPEN_DIALOG, allow_multiple=False, file_types=file_types ) if result and len(result) > 0: file_path = result[0] self.current_vtt_path = file_path # ★追加対策:_listJ.txt が存在しない初期状態の場合、開いた瞬間に自動生成する dir_name = os.path.dirname(file_path) base_name = os.path.splitext(os.path.basename(file_path))[0] list_j_path = os.path.join(dir_name, f"{base_name}_listJ.txt") if not os.path.exists(list_j_path): generate_list_j(file_path) return {"status": "success", "path": file_path, "filename": os.path.basename(file_path)} return {"status": "cancel"} except Exception as e: return {"status": "error", "message": str(e)} def create_new_vtt(self): try: file_types = ('VTT Files (*.vtt)', 'All Files (*.*)') result = self.window.create_file_dialog( webview.SAVE_DIALOG, save_filename='new_subtitle.vtt', file_types=file_types ) if result: file_path = result[0] if isinstance(result, (tuple, list)) else result if not file_path.lower().endswith('.vtt'): file_path += '.vtt' with open(file_path, 'w', encoding='utf-8') as f: f.write("WEBVTT\n\n") self.current_vtt_path = file_path # ★新規作成時も同様に空の _listJ.txt を用意しておく generate_list_j(file_path) return {"status": "success", "path": file_path, "filename": os.path.basename(file_path)} return {"status": "cancel"} except Exception as e: return {"status": "error", "message": str(e)} def save_subtitle_and_generate_list(self, vtt_content): if not self.current_vtt_path: return {"status": "error", "message": "VTTファイルが指定されていません。"} try: with open(self.current_vtt_path, 'w', encoding='utf-8') as f: f.write(vtt_content) success = generate_list_j(self.current_vtt_path) if success: return {"status": "success", "message": "VTT及び_listJ.txtを保存完了しました。"} else: return {"status": "partial_success", "message": "VTTは保存されましたが、リスト生成に失敗しました。"} except Exception as e: return {"status": "error", "message": str(e)} api = SubtitleAPI() media_app = bottle.Bottle() @media_app.hook('after_request') def enable_cors(): bottle.response.headers['Access-Control-Allow-Origin'] = '*' bottle.response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS, HEAD' bottle.response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token, Range' bottle.response.headers['Access-Control-Expose-Headers'] = 'Content-Range, Accept-Ranges, Content-Length' @media_app.route('/stream', method=['GET', 'OPTIONS']) def serve_stream(): if bottle.request.method == 'OPTIONS': return {} path = api.current_media_path if not path or not os.path.exists(path): return bottle.HTTPError(404, "File not found") return bottle.static_file(os.path.basename(path), root=os.path.dirname(path)) def get_free_port(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('127.0.0.1', 0)) return s.getsockname()[1] def start_media_server(port): bottle.run(app=media_app, host='127.0.0.1', port=port, quiet=True) def main(): media_path = sys.argv[1] if len(sys.argv) > 1 else None api.current_media_path = media_path port = get_free_port() api.media_port = port server_thread = threading.Thread(target=start_media_server, args=(port,), daemon=True) server_thread.start() html_path = get_resource_path(os.path.join("html", "index.html")) with open(html_path, 'r', encoding='utf-8') as f: html_content = f.read() window = webview.create_window( title="字幕かんたんエディタ", html=html_content, js_api=api, width=600, height=850, resizable=True ) api.window = window webview.start(debug=False) if __name__ == "__main__": main() ``` readme.txt ``` ====================================================== 【はじめにお読みください】字幕かんたんエディタ 導入ガイド ====================================================== この度は「字幕かんたんエディタ」をダウンロードいただきありがとうございます。 macOSの仕様上、初回起動時のみ簡単なセキュリティ確認が必要となります。 システム設定を変更することなく、以下の3ステップで簡単にご利用いただけます。 【導入手順(初回のみ)】 1. アプリのインストール このフォルダに入っている「字幕かんたんエディタ」のアイコンを、 右隣の「Applications」フォルダへドラッグ&ドロップしてください。 (これでインストールは完了です) 2. 魔法の「右クリック」起動 アプリケーションフォルダを開き、「字幕かんたんエディタ」のアイコンを 【右クリック(またはControlキーを押しながらクリック)】し、 メニューから「開く」を選択してください。 3. 「開く」ボタンを押す 「開発元を検証できないため開けません」という警告画面が出ますが、 その中にある「開く」ボタンをクリックしてください。 ※次回からは、通常通りダブルクリックで起動できるようになります! 【便利な使い方(日常の作業フロー)】 インストール後は、以下の手順でスムーズに字幕作成を開始できます。 1. MacのFinderで、編集したい動画や音声ファイルを「右クリック」します。 2. メニューの「このアプリケーションで開く」の中から、「字幕かんたんエディタ.app」を選びます。 (※Macの環境によっては「サービス」や「クイックアクション」の中に表示される場合もあります) 3. アプリが起動したら、メニュー画面から「既存のVTTファイルを指定」または「新規にVTTファイルを作成」を選択してください。 4. 選択した動画・音声ファイルが自動的に読み込まれた状態で、エディタ画面が開きます。 ====================================================== ``` build_dmg.sh ``` #!/bin/bash APP_NAME="字幕かんたんエディタ.app" APP_PATH="dist/${APP_NAME}" DMG_NAME="SubtitleEasyEditor.dmg" echo "PyInstallerでビルドしています..." pyinstaller subtitle_easy_editor.spec --clean if [ ! -d "$APP_PATH" ]; then echo "エラー: ${APP_PATH} のビルドに失敗しました。" exit 1 fi rm -f "$DMG_NAME" echo "DMGパッケージを作成しています..." create-dmg \ --volname "字幕かんたんエディタ インストーラ" \ --window-pos 200 120 \ --window-size 600 400 \ --icon-size 100 \ --icon "$APP_NAME" 150 190 \ --hide-extension "$APP_NAME" \ --app-drop-link 450 190 \ "$DMG_NAME" \ "dist/" if [ $? -eq 0 ]; then echo "✅ DMGの作成が完了しました: $DMG_NAME" else echo "❌ DMGの作成に失敗しました。" fi ``` html/index.html ``` 字幕かんたん作成 統合エディタ

字幕かんたん作成 統合エディタ

縮小 拡大
現在: 開始: 終了: (新規用) 継続:
``` subtitle_easy_editor.spec ``` # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( ['subtitle_easy_editor.py'], pathex=[], binaries=[], datas=[('html', 'html')], # htmlフォルダを_MEIPASSに同梱 hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name='subtitle_easy_editor', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=False, # macOSアプリとして起動(ターミナル非表示) disable_windowed_traceback=False, argv_emulation=True, # macOSでの引数受け取り(NSServices連携)に必須 target_arch=None, codesign_identity=None, entitlements_file=None, ) # Info.plist に NSServices を自動埋め込み info_plist = { 'CFBundleName': '字幕かんたんエディタ', 'CFBundleDisplayName': '字幕かんたんエディタ', 'CFBundleGetInfoString': 'Subtitle Easy Editor', 'CFBundleIdentifier': 'com.XXXXXX.subtitle-easy-editor', 'CFBundleVersion': '1.0.0', 'CFBundleShortVersionString': '1.0.0', 'NSServices': [ { 'NSMenuItem': { 'default': '字幕かんたんエディタ' }, 'NSMessage': 'openFile', 'NSPortName': '字幕かんたんエディタ', 'NSSendTypes': [ 'public.movie', 'public.audio' ], 'NSRequiredContext': {} } ], # Finderから動画を直接ドロップできるようにする定義 'CFBundleDocumentTypes': [ { 'CFBundleTypeName': 'Media Files', 'CFBundleTypeRole': 'Viewer', 'LSHandlerRank': 'Owner', 'LSItemContentTypes': [ 'public.movie', 'public.audio' ] } ] } coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='subtitle_easy_editor', ) app = BUNDLE( coll, name='字幕かんたんエディタ.app', icon=None, # アイコン(.icns)がある場合はここで指定 bundle_identifier='com.XXXXXX.subtitle-easy-editor', info_plist=info_plist ) ``` vtt_parser_logic.py ``` import os import re def time_to_seconds(time_str): """HH:MM:SS.mmm または MM:SS.mmm を秒数(Float)に変換""" parts = time_str.strip().split(':') if len(parts) == 3: h, m, s = parts return int(h) * 3600 + int(m) * 60 + float(s) elif len(parts) == 2: m, s = parts return int(m) * 60 + float(s) return 0.0 def sec_to_seek_tag(seconds): """秒数を Wavesurfer/HTML5シーク用の 'minutes:seconds' 形式タグに変換""" total_min = int(seconds // 60) sec = int(seconds % 60) return f"{total_min}:{sec:02d}" def generate_list_j(vtt_path): """ VTTファイルを解析し、同じフォルダに『VTTファイル名_listJ.txt』を生成する。 仕様: - 時刻リンクタグ付きのインタラクティブ・トランスクリプト形式 - Apple翻訳対策(translate="yes/no")を完全埋め込み """ try: dir_name = os.path.dirname(vtt_path) base_name = os.path.splitext(os.path.basename(vtt_path))[0] out_list_j_path = os.path.join(dir_name, f"{base_name}_listJ.txt") with open(vtt_path, 'r', encoding='utf-8') as f: lines = f.readlines() body_list_j = [] # 共通CSSテンプレート(既存の details スタイルに準拠) header_html = '
字幕一覧(クリック)

\n' footer_html = '

\n' # ▼ 両方の環境で共存できるハイブリッドスタイルに変更 style_html = """\n""" i = 0 max_i = len(lines) while i < max_i: line = lines[i].strip() if "-->" in line: # タイムスタンプ行の取得 parts = line.split("-->") start_str = parts[0].strip()[:12] # "HH:MM:SS.mmm" display_stamp = start_str[:8] # "HH:MM:SS" # シーク秒数の算出 seconds = time_to_seconds(start_str) seek_tag = sec_to_seek_tag(seconds) # 次の行以降から字幕本文を収集(空行で終了、または次のタイムスタンプまで) caption_text_raw = "" j = i + 1 while j < max_i: next_line = lines[j].strip() if next_line == "": break # 数字のみの連番行はスキップ if next_line.isdigit(): j += 1 continue if "-->" in next_line: break if caption_text_raw == "": caption_text_raw = next_line else: caption_text_raw += " " + next_line j += 1 # _listJ.txt用の整形(先頭に既存の括弧書きのタイムスタンプがあればトリム) caption_for_j = caption_text_raw if caption_for_j.startswith("("): # 例: "(00:01:23) 本文" ➔ "本文" caption_for_j = re.sub(r'^\(\d{1,2}:\d{2}(:\d{2})?\)\s*', '', caption_for_j) # リンク付きの1行を生成 (時刻は翻訳させない「translate="no"」仕様) one_j = f'({display_stamp}) {caption_for_j}
\n' body_list_j.append(one_j) i = j else: i += 1 # ファイル書き出し with open(out_list_j_path, 'w', encoding='utf-8') as out_f: out_f.write(header_html) out_f.writelines(body_list_j) out_f.write(footer_html) out_f.write(style_html) print(f"✅ 字幕一覧用HTMLの生成完了: {out_list_j_path}") return True except Exception as e: print(f"❌ リスト生成エラー: {e}") return False ```