Skip to content

Commit 7ca7f55

Browse files
committed
Initial commit of v2.1.0
0 parents  commit 7ca7f55

File tree

12 files changed

+3277
-0
lines changed

12 files changed

+3277
-0
lines changed

.gitignore

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Python bytecode and compiled files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# Distribution / packaging
7+
build/
8+
develop-eggs/
9+
dist/
10+
eggs/
11+
*.egg-info/
12+
*.egg
13+
wheels/
14+
*.spec # PyInstaller spec file (もし将来的に実行ファイル作成に使う場合)
15+
16+
# Virtual environment folders
17+
.venv/
18+
venv/
19+
ENV/
20+
env/
21+
virtualenv/
22+
# Windows specific virtualenv folders (often created by IDEs)
23+
Include/
24+
Lib/
25+
Scripts/
26+
# macOS/Linux specific virtualenv folders (often created by IDEs)
27+
# bin/ # bin/はffmpeg実行ファイルなどを置く可能性があるのでコメントアウト、必要なら有効化
28+
# lib/ # 同上
29+
# include/ # 同上
30+
31+
# IDE / Editor specific files
32+
.idea/ # IntelliJ IDEA, PyCharm
33+
.vscode/ # Visual Studio Code
34+
*.sublime-project
35+
*.sublime-workspace
36+
nbproject/ # NetBeans
37+
*.project # Eclipse (PyDev)
38+
.project # Eclipse
39+
.settings/ # Eclipse
40+
*.tmproj # TextMate
41+
*.komodoproject # Komodo Edit
42+
*.bak # Backup files often created by editors
43+
*~ # Backup files often created by editors (e.g., Gedit, Kate)
44+
*.swp # Vim swap files
45+
*.swo # Vim swap files
46+
47+
# OS generated files
48+
.DS_Store # macOS
49+
Thumbs.db # Windows
50+
ehthumbs.db # Windows
51+
Desktop.ini # Windows
52+
53+
# Log files (もしアプリケーションがログファイルを生成する場合)
54+
# *.log
55+
# logs/
56+
57+
# Test output (もしテストフレームワークを使用する場合)
58+
# .pytest_cache/
59+
# .tox/
60+
# htmlcov/
61+
# .coverage
62+
# nosetests.xml
63+
# coverage.xml
64+
65+
# Local settings files (もしユーザー固有の設定ファイルがある場合)
66+
# local_settings.py
67+
# .env
68+
69+
# Jupyter Notebook checkpoints (もし使用する場合)
70+
.ipynb_checkpoints
71+
72+
# mypy cache (もし型チェックにmypyを使用する場合)
73+
.mypy_cache/
74+
75+
# Ruff cache (もしリンター/フォーマッッターにRuffを使用する場合)
76+
.ruff_cache/
77+
78+
# pytest cache (もしテストにpytestを使用する場合)
79+
.pytest_cache/

LICENSE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright (c) 2025 stechdrive
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.

advanced_yaw_selector.py

Lines changed: 993 additions & 0 deletions
Large diffs are not rendered by default.

constants.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# constants.py
2+
# アプリケーション全体で使用する定数
3+
4+
APP_NAME = "Insta360Convert GUI"
5+
6+
# --- バージョン情報 ---
7+
# リリース日 (ユーザー提供の値を維持、必要に応じて更新)
8+
APP_RELEASE_DATE = "2025-06-04"
9+
10+
# アプリケーションのセマンティックバージョン番号
11+
# これを v2.1.0 に更新する場合
12+
APP_VERSION_MAJOR = 2
13+
APP_VERSION_MINOR = 1
14+
APP_VERSION_PATCH = 0
15+
# 文字列としてのバージョン (例: "v2.1.0" や "2.1.0")
16+
# 'v'プレフィックスを付けるかどうかは一貫性を持たせます。
17+
# GitHubのタグ名と合わせるなら 'v' を付けるのが一般的。
18+
APP_VERSION_STRING_SEMVER = f"v{APP_VERSION_MAJOR}.{APP_VERSION_MINOR}.{APP_VERSION_PATCH}"
19+
20+
# 開発バージョン文字列 (以前のAPP_DEV_VERSIONの役割。必要なら残すか、上記に統合)
21+
# もし APP_VERSION_STRING_SEMVER で十分なら、この行は不要かもしれません。
22+
# APP_DEV_LABEL = "dev" # 例えば開発版なら "dev", "beta", "rc1" など
23+
# APP_FULL_VERSION_STRING = f"{APP_VERSION_STRING_SEMVER}-{APP_DEV_LABEL}" if APP_DEV_LABEL else APP_VERSION_STRING_SEMVER
24+
25+
# GUIのタイトルバーやバージョン情報ダイアログで表示する文字列
26+
# (以前のAPP_VERSION_STRINGの役割。日付とバージョンを組み合わせるか、バージョンのみにするか選択)
27+
# 例1: 日付とセマンティックバージョン
28+
APP_DISPLAY_VERSION = f"{APP_VERSION_STRING_SEMVER} ({APP_RELEASE_DATE})"
29+
# 例2: セマンティックバージョンのみ (より一般的かも)
30+
# APP_DISPLAY_VERSION = APP_VERSION_STRING_SEMVER
31+
32+
# --- ここまでバージョン情報 ---
33+
34+
35+
# FFmpeg関連定数
36+
FFMPEG_PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"]
37+
DEFAULT_PRESET = "medium"
38+
DEFAULT_RESOLUTION_WIDTH = 1920
39+
HIGH_RESOLUTION_THRESHOLD = 4096 # この解像度を超える入力は高解像度とみなす
40+
41+
# --- GitHub関連定数 (アップデートチェック用) ---
42+
GITHUB_REPO_OWNER = "stechdrive"
43+
GITHUB_REPO_NAME = "Insta360Convert-GUI"
44+
GITHUB_API_URL_LATEST_RELEASE = f"https://api.github.com/repos/{GITHUB_REPO_OWNER}/{GITHUB_REPO_NAME}/releases/latest"
45+
GITHUB_RELEASES_PAGE_URL = f"https://github.com/{GITHUB_REPO_OWNER}/{GITHUB_REPO_NAME}/releases"

ffmpeg_worker.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# ffmpeg_worker.py
2+
# FFmpegのワーカープロセスと関連ヘルパー関数
3+
4+
import subprocess
5+
import os
6+
import time
7+
# import json # configは呼び出し側で準備され、worker内では直接jsonをパースしない
8+
# import queue # キューは引数として渡される
9+
# import multiprocessing # イベントは引数として渡される
10+
import traceback # 例外発生時のスタックトレース取得用
11+
12+
def check_for_cuda_fallback_error(ffmpeg_output_str):
13+
"""
14+
FFmpegの出力文字列を解析し、CUDA関連のエラーや
15+
CPUへのフォールバックを示唆する可能性のあるパターンを検出します。
16+
17+
Args:
18+
ffmpeg_output_str (str): FFmpegの標準エラー出力または標準出力の文字列。
19+
20+
Returns:
21+
bool: CUDAフォールバックが必要と判断されるエラーが含まれていればTrue、そうでなければFalse。
22+
"""
23+
output_lower = ffmpeg_output_str.lower()
24+
# CUDA初期化エラー
25+
if "hwaccel initialisation returned error" in output_lower or \
26+
"failed setup for format cuda" in output_lower:
27+
return True
28+
# 一般的なCUDAエラー
29+
if "cuda_error_" in output_lower: # Generic CUDA error string
30+
return True
31+
# 解像度関連のCUDAエラー (特定のキーワードの組み合わせ)
32+
if "not within range" in output_lower and \
33+
("width" in output_lower or "height" in output_lower) and \
34+
("cuda" in output_lower or "nvdec" in output_lower or "cuvid" in output_lower): # Resolution issues with CUDA
35+
return True
36+
# フォーマット変換関連のCUDAエラー
37+
if "impossible to convert between the formats supported by the filter" in output_lower and \
38+
("cuda" in output_lower or "hwdownload" in output_lower or "hwupload" in output_lower): # Format conversion issues with CUDA pipeline
39+
return True
40+
# フィルタ記述パースエラー (CUDA利用時によく見られる)
41+
if "error parsing a filter description" in output_lower or \
42+
"error parsing filterchain" in output_lower: # Often seen with CUDA issues if filters are complex
43+
return True # This might be too broad, but often indicates a problem in CUDA pipeline setup
44+
return False
45+
46+
def ffmpeg_worker_process(viewpoint_idx, viewpoint_data, config, log_queue_mp, progress_queue_mp, cancel_event_mp):
47+
"""
48+
個別の視点に対するFFmpeg変換処理をサブプロセスとして実行します。
49+
マルチプロセッシングプールから呼び出されることを想定しています。
50+
51+
Args:
52+
viewpoint_idx (int): 処理中の視点のインデックス。
53+
viewpoint_data (dict): 視点情報 (fov, pitch, yaw)。
54+
config (dict): 変換設定 (ffmpeg_path, input_file, output_folderなど)。
55+
log_queue_mp (multiprocessing.Queue): ログメッセージをGUIプロセスに送るためのキュー。
56+
progress_queue_mp (multiprocessing.Queue): 進捗情報をGUIプロセスに送るためのキュー。
57+
cancel_event_mp (multiprocessing.Event): キャンセル指示を検知するためのイベント。
58+
"""
59+
process_start_time = time.time() # 処理開始時間
60+
# configから必要な設定値を取得
61+
ffmpeg_path = config["ffmpeg_path"]
62+
input_file = config["input_file"]
63+
output_folder = config["output_folder"]
64+
output_width, output_height = config["output_resolution"]
65+
interp = config["interp"]
66+
threads_ffmpeg = config["threads_ffmpeg"] # CPU処理時のスレッド数
67+
use_cuda = config["use_cuda"]
68+
output_format = config["output_format"]
69+
frame_interval_val = config["frame_interval"] # PNG/JPEG時のフレーム抽出間隔
70+
video_preset = config["video_preset"] # 動画出力時のプリセット
71+
video_cq = config["video_cq"] # 動画出力時の品質値 (CQ/CRF)
72+
png_pred_option = config.get("png_pred_option", "3") # PNG予測オプション
73+
jpeg_quality = config.get("jpeg_quality", 90) # JPEG品質
74+
75+
# 視点データを展開
76+
fov, pitch, yaw = viewpoint_data["fov"], viewpoint_data["pitch"], viewpoint_data["yaw"]
77+
# FFmpegのyawは -180 から 180 の範囲であるため調整
78+
ffmpeg_yaw = yaw
79+
if ffmpeg_yaw > 180:
80+
ffmpeg_yaw -= 360
81+
82+
# v360フィルタのパラメータ文字列を生成
83+
v360_filter_params = (
84+
f"e:flat:yaw={ffmpeg_yaw:.2f}:pitch={pitch:.2f}:h_fov={fov:.2f}:v_fov={fov:.2f}"
85+
f":w={output_width}:h={output_height}:interp={interp}"
86+
)
87+
command = [ffmpeg_path, "-y"] # -y: 出力ファイルを無条件に上書き
88+
filter_complex_parts = [] # -vf または -filter_complex に渡すフィルタのリスト
89+
90+
# CUDAを使用する場合のオプション追加
91+
if use_cuda:
92+
command.extend(["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"])
93+
command.extend(["-i", input_file]) # 入力ファイル指定
94+
95+
# 静止画シーケンスでフレームレート指定がある場合
96+
if output_format in ["png", "jpeg"] and frame_interval_val > 0:
97+
filter_complex_parts.append(f"fps=fps=1/{frame_interval_val:.3f}")
98+
99+
# フィルタチェーンの構築
100+
if use_cuda:
101+
# CUDAパイプライン: hwdownloadでGPUメモリからシステムメモリへ -> v360 -> hwuploadでシステムメモリからGPUメモリへ
102+
filter_complex_parts.extend(["hwdownload", "format=nv12", f"v360={v360_filter_params}"])
103+
if output_format == "png":
104+
filter_complex_parts.append("format=rgb24") # PNGはrgb24が必要
105+
elif output_format == "jpeg":
106+
filter_complex_parts.append("format=yuvj420p") # JPEGはyuvj420p
107+
elif output_format == "video": # 動画(hevc_nvenc)
108+
filter_complex_parts.extend(["format=nv12", "hwupload_cuda"]) # hevc_nvencはnv12入力を期待
109+
else: # CPU処理
110+
filter_complex_parts.append(f"v360={v360_filter_params}")
111+
if output_format == "png":
112+
filter_complex_parts.append("format=rgb24")
113+
elif output_format == "jpeg":
114+
filter_complex_parts.append("format=yuvj420p")
115+
elif output_format == "video": # 動画(libx265)
116+
filter_complex_parts.append("format=yuv420p") # libx265はyuv420p
117+
118+
119+
# 出力ファイル名/フォルダ名の生成
120+
base_input_name = os.path.splitext(os.path.basename(input_file))[0]
121+
pitch_folder_str = f"{int(pitch):03d}".replace("-", "m") # ピッチ角をフォルダ名用に整形 (例: -30 -> m030)
122+
123+
if output_format in ["png", "jpeg"]:
124+
# 静止画シーケンスの場合、視点ごとにフォルダを作成
125+
view_folder_name = f"{base_input_name}_p{pitch_folder_str}_y{int(yaw):03d}{'_jpeg' if output_format == 'jpeg' else ''}"
126+
output_dir_for_viewpoint = os.path.join(output_folder, view_folder_name)
127+
try:
128+
os.makedirs(output_dir_for_viewpoint, exist_ok=True)
129+
except OSError as e:
130+
log_queue_mp.put({"type": "log", "level": "ERROR", "message": f"出力フォルダ作成失敗({view_folder_name}): {e}"})
131+
progress_queue_mp.put({
132+
"type": "task_result", "viewpoint_index": viewpoint_idx, "success": False,
133+
"error_message": f"出力フォルダ作成失敗: {e}", "duration": time.time() - process_start_time
134+
})
135+
return
136+
file_ext = "jpg" if output_format == "jpeg" else "png"
137+
output_filename_pattern = os.path.join(output_dir_for_viewpoint, f"{view_folder_name}_%05d.{file_ext}")
138+
command.extend(["-vf", ",".join(filter_complex_parts)])
139+
if not use_cuda: # CPU処理時のみスレッド数を指定
140+
command.extend(["-threads", str(threads_ffmpeg)])
141+
if output_format == "png":
142+
command.extend(["-pred", png_pred_option]) # PNG予測オプション
143+
elif output_format == "jpeg":
144+
# JPEG品質をFFmpegのqscale:vに変換 (1-100 -> 1-31, 1が高い)
145+
q_val = max(1, min(31, int(round(1 + (100 - jpeg_quality) * 30 / 99))))
146+
command.extend(["-qscale:v", str(q_val)])
147+
command.append(output_filename_pattern)
148+
elif output_format == "video":
149+
command.extend(["-vf", ",".join(filter_complex_parts)])
150+
if use_cuda:
151+
command.extend(["-c:v", "hevc_nvenc"]) # CUDAエンコーダ
152+
else:
153+
command.extend(["-c:v", "libx265", "-threads", str(threads_ffmpeg)]) # CPUエンコーダ
154+
view_file_name = f"{base_input_name}_p{pitch_folder_str}_y{int(yaw):03d}.mp4"
155+
output_file = os.path.join(output_folder, view_file_name)
156+
command.extend(["-preset", video_preset, "-cq" if use_cuda else "-crf", str(video_cq), "-an", output_file]) # -an: 音声なし
157+
158+
# FFmpegコマンドをログに出力 (デバッグ用)
159+
log_queue_mp.put({"type": "log", "level": "DEBUG", "message": f"Worker {viewpoint_idx + 1} (CUDA: {use_cuda}) command: {' '.join(command)}"})
160+
ffmpeg_process = None
161+
try:
162+
# Windowsでコンソールウィンドウを非表示にするための設定
163+
startupinfo = None
164+
if os.name == 'nt':
165+
startupinfo = subprocess.STARTUPINFO()
166+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
167+
startupinfo.wShowWindow = subprocess.SW_HIDE
168+
169+
# FFmpegプロセスを開始
170+
ffmpeg_process = subprocess.Popen(
171+
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # 標準出力と標準エラーをパイプで取得
172+
universal_newlines=True, startupinfo=startupinfo,
173+
encoding='utf-8', errors='replace' # エンコーディング指定
174+
)
175+
176+
# FFmpegの出力をリアルタイムで読み取り、ログキューに送信
177+
if ffmpeg_process.stdout: # stdoutがNoneでないことを確認
178+
for line in iter(ffmpeg_process.stdout.readline, ''):
179+
if cancel_event_mp.is_set(): # キャンセルが要求されたかチェック
180+
log_queue_mp.put({"type": "log", "level": "INFO", "message": f"Worker {viewpoint_idx + 1} (P{pitch:.1f} Y{yaw:.1f}) cancelled."})
181+
ffmpeg_process.terminate() # プロセスを終了
182+
try:
183+
ffmpeg_process.wait(timeout=2) # 終了待機 (タイムアウト付き)
184+
except subprocess.TimeoutExpired:
185+
ffmpeg_process.kill() # 強制終了
186+
break
187+
log_queue_mp.put({"type": "ffmpeg_raw", "line": line.strip(), "viewpoint_index": viewpoint_idx})
188+
ffmpeg_process.wait() # プロセスの終了を待つ
189+
190+
if cancel_event_mp.is_set():
191+
progress_queue_mp.put({
192+
"type": "task_result", "viewpoint_index": viewpoint_idx, "success": False,
193+
"cancelled": True, "duration": time.time() - process_start_time
194+
})
195+
return
196+
197+
# FFmpegの終了コードをチェック
198+
if ffmpeg_process.returncode == 0:
199+
progress_queue_mp.put({
200+
"type": "task_result", "viewpoint_index": viewpoint_idx, "success": True,
201+
"duration": time.time() - process_start_time
202+
})
203+
else:
204+
log_queue_mp.put({"type": "log", "level": "ERROR", "message": f"FFmpeg error (Worker {viewpoint_idx + 1}, P{pitch:.1f} Y{yaw:.1f}): Code {ffmpeg_process.returncode}"})
205+
progress_queue_mp.put({
206+
"type": "task_result", "viewpoint_index": viewpoint_idx, "success": False,
207+
"error_message": f"FFmpeg failed (code {ffmpeg_process.returncode})",
208+
"duration": time.time() - process_start_time
209+
})
210+
except Exception as e:
211+
log_queue_mp.put({"type": "log", "level": "CRITICAL", "message": f"Worker {viewpoint_idx + 1} (P{pitch:.1f} Y{yaw:.1f}) exception: {e}"})
212+
log_queue_mp.put({"type": "log", "level": "DEBUG", "message": traceback.format_exc()})
213+
progress_queue_mp.put({
214+
"type": "task_result", "viewpoint_index": viewpoint_idx, "success": False,
215+
"error_message": str(e), "duration": time.time() - process_start_time
216+
})
217+
finally:
218+
# プロセスがまだ実行中であれば強制終了
219+
if ffmpeg_process and ffmpeg_process.poll() is None:
220+
ffmpeg_process.kill()
221+
if ffmpeg_process.stdout:
222+
ffmpeg_process.stdout.close()
223+
# stderrも同様にクローズ (Popenでstdout=PIPE, stderr=STDOUTにしているのでstdoutのクローズで十分なはず)
224+
ffmpeg_process.wait()

0 commit comments

Comments
 (0)