==========================================
【App A】 GPV解析・キャッシュエンジン (Ver 96.5コア + 97.5スキャン統合版)
VERSION INFO: UI・抽出コアは96.5の堅牢性を完全復元 / ANAL・GUID・新旧GSMスキャン対応
==========================================
import sys, os, glob, re, subprocess, queue
from datetime import datetime
import multiprocessing as mp
os.environ[‘QT_API’] = ‘pyqt6’
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction
try: import cfgrib
except ImportError: sys.exit(“エラー: cfgribがインストールされていません。”)
APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, “gpv_cache_npz”)
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
WGRIB2_EXE = os.path.join(APP_DIR, “wgrib2_data”, “wgrib2.exe”)
os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)
def write_syslog(msg):
log_file = os.path.join(CURRENT_OUTPUT_DIR, “system_log.txt”)
try:
with open(log_file, “a”, encoding=”utf-8″) as f:
f.write(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}\n”)
except Exception: pass
def create_lightning_icon():
pixmap = QPixmap(64, 64); pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap); font = painter.font(); font.setPixelSize(50)
painter.setFont(font); painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, “⚡”)
painter.end(); return QIcon(pixmap)
==========================================
【裏方ワーカー】 96.5の「一切データを逃さない」最強抽出コアを完全復元
==========================================
def extract_gpv_worker(model, mode, cache, init, target_fts, f1, f2, wgrib2_path, q):
try:
import os, sys, subprocess, warnings
import numpy as np
import cfgrib
warnings.filterwarnings(“ignore”)
os.environ[“ECCODES_MAX_VALUES”] = “5000000”
if sys.platform == "win32":
conda_dir = os.path.dirname(sys.executable)
dll_paths = [os.path.join(conda_dir, "Library", "bin"), os.path.join(conda_dir, "bin")]
for p in dll_paths:
if os.path.exists(p):
os.environ["PATH"] = f"{p};{os.environ.get('PATH', '')}"
try: os.add_dll_directory(p)
except Exception: pass
def calculate_vorticity(u, v, lon, lat):
try:
R = 6371000.0; LON, LAT = np.meshgrid(lon, lat) if lon.ndim == 1 else (lon, lat)
rad_lat = np.deg2rad(LAT); rad_lon = np.deg2rad(LON)
dy = R * np.gradient(rad_lat, axis=0); dx = R * np.cos(rad_lat) * np.gradient(rad_lon, axis=1)
dx[dx == 0] = 1e-10; dy[dy == 0] = 1e-10
return ((np.gradient(v, axis=1) / dx) - (np.gradient(u, axis=0) / dy)) * 1e5
except Exception: return np.zeros_like(u)
def safe_max(arr):
return np.nanmax(arr) if not np.all(np.isnan(arr)) else 0.0
d_all = {ft: {} for ft in target_fts}
creationflags = 0x08000000 if sys.platform == "win32" else 0
for t_ft in target_fts:
d = d_all[t_ft]
def slice_with_wgrib2(fin, ft_val, suffix):
if fin == "NONE": return "NONE"
fout = os.path.join(cache, f"temp_{model}_{mode}_{init}_{ft_val}_{suffix}.bin")
d_str = f"{ft_val//24} day" if ft_val % 24 == 0 else "INVALID"
if mode == "ANAL":
match_str = ":(anl):"
elif ft_val == 0:
match_str = ":(anl|0 hour [^:]+|[0-9]+-0 hour [^:]+):"
else:
match_str = f":({ft_val} hour [^:]+|[0-9]+-{ft_val} hour [^:]+|{d_str} [^:]+|[0-9]+-{d_str} [^:]+):"
cmd = [wgrib2_path, fin, "-match", match_str, "-grib", fout]
try:
subprocess.run(
cmd, creationflags=creationflags, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
)
if os.path.exists(fout) and os.path.getsize(fout) > 0: return fout
except Exception: pass
return "NONE"
f1_mini = slice_with_wgrib2(f1, t_ft, "1")
t_ft_p = (t_ft // 3) * 3 if model == "MSM" else t_ft
f2_mini = slice_with_wgrib2(f2, t_ft_p, "2")
def process_file(filepath, is_pall):
if filepath == "NONE": return
try:
dss = []
try:
dss = cfgrib.open_datasets(filepath, backend_kwargs={'indexpath': ''})
except Exception as e:
import xarray as xr
for stype in ['instant', 'accum', 'avg']:
try:
ds_sub = xr.open_dataset(filepath, engine='cfgrib', backend_kwargs={'indexpath': '', 'filter_by_keys': {'stepType': stype}})
dss.append(ds_sub)
except: pass
if not dss:
try:
dss.append(xr.open_dataset(filepath, engine='cfgrib', backend_kwargs={'indexpath': ''}))
except: pass
for ds in dss:
lon = ds['longitude'].values if 'longitude' in ds.coords else (ds['lon'].values if 'lon' in ds.coords else None)
lat = ds['latitude'].values if 'latitude' in ds.coords else (ds['lat'].values if 'lat' in ds.coords else None)
for v in ds.data_vars:
da = ds[v]
is_ens = 'number' in da.dims
if not is_pall and 'lon_surf' not in d and lon is not None:
d['lon_surf'] = lon; d['lat_surf'] = lat; d['lon'] = lon; d['lat'] = lat
elif is_pall and 'lon_pall' not in d and lon is not None:
d['lon_pall'] = lon; d['lat_pall'] = lat
da_step = da.isel(step=0) if 'step' in da.dims else da
if is_ens and 'number' in da_step.coords: da_step = da_step.isel(number=0)
sName = str(da.attrs.get('GRIB_shortName', v)).lower()
name_str = str(da.attrs.get('GRIB_name', '')).lower()
attrs_str = str(da.attrs).lower()
# --- GUIDモード用の特別バイパス(次元削減を許容するのはGUIDのみ) ---
if mode == "GUID":
val_2d = da_step.values.copy()
while val_2d.ndim > 2: val_2d = val_2d[0]
if sName != 'unknown': d[sName] = val_2d
if sName in ['2t', 't2m']: d['t2m'] = val_2d
elif sName in ['tp', 'apcp', 'pr']: d['precip'] = val_2d
continue # 以下の複雑なGPV層解析をスキップ
# --- 以下、96.5の堅牢な多次元(上空)抽出処理 ---
level_dim_name = None
for coord_key in ['isobaricInhPa', 'isobaricInPa', 'level']:
if coord_key in da_step.dims:
level_dim_name = coord_key; break
if level_dim_name and da_step[level_dim_name].size > 1:
levels = da_step[level_dim_name].values
for i, lvl_raw in enumerate(levels):
lvl = float(lvl_raw)
if level_dim_name == 'isobaricInPa': lvl /= 100.0
lvl = int(lvl)
if lvl <= 0: continue
val_2d = da_step.isel(**{level_dim_name: i}).values.copy()
while val_2d.ndim > 2: val_2d = val_2d[0]
if is_pall:
if sName in ['u', 'u-component']: d[f'u{lvl}'] = val_2d
elif sName in ['v', 'v-component']: d[f'v{lvl}'] = val_2d
elif sName in ['t', 'temp']: d[f't{lvl}'] = val_2d - 273.15 if safe_max(val_2d) > 150 else val_2d
elif sName in ['r', 'rh']: d[f'rh{lvl}'] = val_2d
elif sName in ['gh', 'hgt']: d[f'gh{lvl}'] = val_2d
elif sName in ['w', 'vvel', 'dzdt']: d[f'w{lvl}'] = val_2d
elif sName in ['absv', 'vort']: d[f'vort{lvl}'] = val_2d
else:
level_val = -1.0
for coord_key in ['isobaricInhPa', 'isobaricInPa', 'level']:
if coord_key in da_step.coords:
import numpy as np
val_coord = np.atleast_1d(da_step.coords[coord_key].values)
if val_coord.size == 1:
level_val = float(val_coord[0])
if coord_key == 'isobaricInPa': level_val /= 100.0
break
if level_val < 0: level_val = float(da_step.attrs.get('GRIB_level', -1))
val_2d = da_step.values.copy()
while val_2d.ndim > 2:
valid_idx = 0
for i in range(val_2d.shape[0]):
import numpy as np
if not np.all(np.isnan(val_2d[i])):
valid_idx = i; break
val_2d = val_2d[valid_idx]
if not is_pall:
step_type_str = str(da_step.coords.get('stepType', da_step.attrs.get('GRIB_stepType', ''))).lower()
is_precip = False
if sName in ['tp', 'apcp', 'pr', 'precip', 'prate', 'tprat'] or 'precip' in name_str or 'rate' in name_str: is_precip = True
elif sName == 'unknown' and 'accum' in step_type_str: is_precip = True
if is_precip:
if 'precip_raw' not in d:
import numpy as np
val_p = np.where(val_2d >= 9990, 0.0, val_2d)
factor = 3600.0 if ('rate' in name_str or sName in ['prate', 'tprat']) else 1.0
raw_accum = np.nan_to_num(val_p * factor, nan=0.0)
d['precip_raw'] = raw_accum
final_precip = raw_accum.copy()
if model.startswith("GSM"):
interval = 6 if model == "GSM_GL" else 3
prev_ft = t_ft - interval; prev_accum = None
if prev_ft in d_all and 'precip_raw' in d_all[prev_ft]:
prev_accum = d_all[prev_ft]['precip_raw']
elif prev_ft >= 0:
prev_file = os.path.join(cache, f"{model}_{init}_FT{prev_ft:02d}.npz")
if os.path.exists(prev_file):
try:
with np.load(prev_file) as p_npz:
if 'precip_raw' in p_npz: prev_accum = p_npz['precip_raw']
except: pass
if prev_accum is not None and final_precip.shape == prev_accum.shape:
final_precip = np.maximum(final_precip - prev_accum, 0.0)
d['precip'] = final_precip
continue
if sName in ['prmsl', 'msl', 'mslet']:
if 'slp' not in d: d['slp'] = val_2d / 100.0 if safe_max(val_2d) > 2000 else val_2d
elif sName in ['pres', 'sp'] or 'pressure' in attrs_str:
if 'pres' not in d: d['pres'] = val_2d / 100.0 if safe_max(val_2d) > 2000 else val_2d
elif sName in ['10u', 'u10'] or ('u' in sName and '10' in attrs_str) or (sName == 'u' and level_val in [10, 0, -1]):
if 'u10' not in d: d['u10'] = val_2d
elif sName in ['10v', 'v10'] or ('v' in sName and '10' in attrs_str) or (sName == 'v' and level_val in [10, 0, -1]):
if 'v10' not in d: d['v10'] = val_2d
elif sName in ['2t', 't2m', 'tmp2m', 'temp2m'] or ('temperature' in attrs_str and '2' in attrs_str):
if 't2m' not in d: d['t2m'] = val_2d - 273.15 if safe_max(val_2d) > 150 else val_2d
elif sName in ['2r', 'r', 'rh2m', 'rh'] or ('humidity' in attrs_str):
if 'rh2m' not in d: d['rh2m'] = val_2d
elif sName in ['tcc', 'tcdc']: d['tcc'] = val_2d
elif sName in ['hcc', 'hcdc']: d['hcc'] = val_2d
elif sName in ['mcc', 'mcdc']: d['mcc'] = val_2d
elif sName in ['lcc', 'lcdc']: d['lcc'] = val_2d
else:
if level_val > 0:
lvl = int(level_val)
if sName in ['u', 'u-component']: d[f'u{lvl}'] = val_2d
elif sName in ['v', 'v-component']: d[f'v{lvl}'] = val_2d
elif sName in ['t', 'temp']: d[f't{lvl}'] = val_2d - 273.15 if safe_max(val_2d) > 150 else val_2d
elif sName in ['r', 'rh']: d[f'rh{lvl}'] = val_2d
elif sName in ['gh', 'hgt']: d[f'gh{lvl}'] = val_2d
elif sName in ['w', 'vvel', 'dzdt']: d[f'w{lvl}'] = val_2d
elif sName in ['absv', 'vort']: d[f'vort{lvl}'] = val_2d
for ds in dss: ds.close()
except Exception: pass
process_file(f1_mini, False)
process_file(f2_mini, True)
if 'tcc' not in d and 'hcc' in d and 'mcc' in d and 'lcc' in d:
d['tcc'] = np.maximum(np.maximum(d['hcc'], d['mcc']), d['lcc'])
if 'vort500' not in d and 'u500' in d and 'v500' in d:
lon_arr = d.get('lon_pall', d.get('lon_surf'))
lat_arr = d.get('lat_pall', d.get('lat_surf'))
if lon_arr is not None and lat_arr is not None:
d['vort500'] = calculate_vorticity(d['u500'], d['v500'], lon_arr, lat_arr)
pfx = "GUID_" if mode == "GUID" else ""
out_file = os.path.join(cache, f"{model}_{pfx}{init}_FT{t_ft:02d}.npz")
if len(d) > 3:
np.savez_compressed(out_file, **d)
q.put(("SUCCESS", f"{mode}_{t_ft}"))
try:
if f1_mini != "NONE" and os.path.exists(f1_mini): os.remove(f1_mini)
if f2_mini != "NONE" and os.path.exists(f2_mini): os.remove(f2_mini)
except Exception: pass
except Exception as e:
import traceback
q.put(("ERROR", str(e) + "\n" + traceback.format_exc()))
==========================================
スキャンスレッド (97.5の全フォーマット対応)
==========================================
class DataParserThread(QThread):
log_signal = pyqtSignal(str); progress_signal = pyqtSignal(int, str); finished_signal = pyqtSignal(bool)
def init(self, folder_paths, output_dir, last_reported):
super().init()
self.folder_paths = [path for path in folder_paths if path and os.path.exists(path)]
self.output_dir = output_dir
self.last_reported = last_reported; self.abort = False
def run(self):
if not self.folder_paths: self.finished_signal.emit(False); return
if not os.path.exists(WGRIB2_EXE):
self.log_signal.emit(f"⚠️ wgrib2が見つかりません。"); self.finished_signal.emit(False); return
has_new_data = False; scan_started = False; reported_new_file = False
for folder_path in self.folder_paths:
if self.abort: break
scan_rules = [
('*MSM*GPV*.bin', 'MSM', 'GPV', range(0, 80)),
('*GSM*GPV*Rjp*.bin', 'GSM_JP', 'GPV', range(0, 265, 3)),
('*GSM*GPV*.bin', 'GSM_JP', 'GPV', range(0, 265, 3)),
('*_Rgl*.bin', 'GSM_GL', 'GPV', range(0, 265, 6)),
('*ANAL_grib2*.bin', 'ANAL', 'ANAL', [0]),
('*GSM_GUID*.bin', 'GSM', 'GUID', range(0, 265, 3))
]
for pattern, model_id, mode, ft_range in scan_rules:
if self.abort: break
files = glob.glob(os.path.join(folder_path, '**', pattern), recursive=True)
if not files: continue
times = set(re.search(r'_(\d{14})_', os.path.basename(f)).group(1) for f in files if re.search(r'_(\d{14})_', f))
if not times: continue
latest_it = max(times)
latest_files = [f for f in files if latest_it in f]
if self.last_reported.get(f"{model_id}_{mode}") != latest_it:
self.log_signal.emit(f"✅ {model_id} ({mode}) 最新データ発見: {latest_it}")
self.last_reported[f"{model_id}_{mode}"] = latest_it; reported_new_file = True
tasks = []
pfx = "GUID_" if mode == "GUID" else ""
for ft in ft_range:
npz_file = os.path.join(self.output_dir, f"{model_id}_{pfx}{latest_it}_FT{ft:02d}.npz")
if not os.path.exists(npz_file): tasks.append(ft)
if not tasks: continue
if not scan_started:
self.log_signal.emit("🔍 新規データを抽出中..."); scan_started = True; has_new_data = True
file_groups = {}
for ft in tasks:
if model_id == "GSM_GL":
f1 = self._get_file_for_ft(latest_files, "", ft, model_id) or "NONE"
f2 = f1
else:
f1 = self._get_file_for_ft(latest_files, "" if mode in ["ANAL", "GUID"] else "Lsurf", ft, model_id) or "NONE"
if mode in ["ANAL", "GUID"]:
f2 = "NONE"
else:
ft_p = (ft // 3) * 3 if model_id == "MSM" else ft
f2 = self._get_file_for_ft(latest_files, "P-all" if any("P-all" in x for x in latest_files) else ("L-pall" if any("L-pall" in x for x in latest_files) else "P"), ft_p, model_id) or "NONE"
if f1 == "NONE" and f2 == "NONE":
f1 = self._get_file_for_ft(latest_files, "", ft, model_id) or "NONE"
f2 = f1
if f1 == "NONE" and f2 == "NONE": continue
key = (f1, f2)
if key not in file_groups: file_groups[key] = []
file_groups[key].append(ft)
if file_groups:
self.log_signal.emit(f"🚀 {model_id} ({mode}) 爆速抽出 (対象: {len(tasks)}件)...")
total_extracted = 0
ctx = mp.get_context('spawn')
for (f1, f2), fts in file_groups.items():
if self.abort: break
for i in range(0, len(fts), 5):
if self.abort: break
chunk_fts = fts[i:i + 5]
q = ctx.Queue()
p = ctx.Process(target=extract_gpv_worker, args=(model_id, mode, self.output_dir, latest_it, chunk_fts, f1, f2, WGRIB2_EXE, q))
p.start()
while p.is_alive() or not q.empty():
if self.abort: p.terminate(); break
try:
msg_type, msg_val = q.get(timeout=0.1)
if msg_type == "SUCCESS":
total_extracted += 1
self.progress_signal.emit(int((total_extracted / len(tasks)) * 100), f"⚡ wgrib2解析中 {model_id} {msg_val}...")
elif msg_type == "ERROR":
self.log_signal.emit(f"⚠️ {msg_val}")
except queue.Empty: pass
p.join()
if has_new_data or reported_new_file: self.log_signal.emit("✅ スキャン・抽出完了")
self.finished_signal.emit(has_new_data)
def _get_file_for_ft(self, files_list, keyword, ft, model_name):
if "GSM" in model_name:
for f in files_list:
if keyword and keyword not in f: continue
m_single = re.search(r'FD(\d{2})(\d{2})_grib2', os.path.basename(f))
if m_single:
file_ft = int(m_single.group(1)) * 24 + int(m_single.group(2))
if file_ft == ft: return f
m_range = re.search(r'FD(\d{2})(\d{2})-(\d{2})(\d{2})', os.path.basename(f))
if m_range:
ft_s = int(m_range.group(1)) * 24 + int(m_range.group(2))
ft_e = int(m_range.group(3)) * 24 + int(m_range.group(4))
if ft_s <= ft <= ft_e: return f
return next((f for f in files_list if (keyword in f or not keyword)), None)
for f in files_list:
if keyword and keyword not in f: continue
m = re.search(r'FH(\d{2})-(\d{2})', os.path.basename(f))
if m and int(m.group(1)) <= ft <= int(m.group(2)): return f
elif not m: return f
return None
==========================================
GUIクラス (96.5完全維持)
==========================================
class EngineStatusWindow(QWidget):
def init(self):
super().init()
self.setWindowTitle(“GPV 解析エンジン (Ver 97.5 復元・統合版)”)
self.setFixedSize(650, 580)
self.setStyleSheet(“background-color: #0A192F; color: #E0E0E0; font-family: ‘MS Gothic’;”)
layout = QVBoxLayout(self)
self.status_label = QLabel(“状態: 起動中…”); self.status_label.setStyleSheet(“font-size: 14pt; font-weight: bold; color: #64FFDA;”)
layout.addWidget(self.status_label)
control_layout = QHBoxLayout()
self.pause_btn = QPushButton("⏸ 一時停止"); self.resume_btn = QPushButton("▶ 手動強制スキャン"); self.stop_btn = QPushButton("⏹ 停止")
self.clear_btn = QPushButton("🧹 キャッシュ全削除")
for btn in [self.pause_btn, self.resume_btn, self.stop_btn]:
btn.setStyleSheet("background-color: #1D3557; padding: 4px; font-weight: bold; color: #F1FAEE;"); control_layout.addWidget(btn)
self.clear_btn.setStyleSheet("background-color: #E63946; padding: 4px; font-weight: bold; color: white;"); control_layout.addWidget(self.clear_btn)
layout.addLayout(control_layout)
out_layout = QHBoxLayout()
self.out_folder_btn = QPushButton("💾 出力先フォルダを設定")
self.out_folder_btn.setStyleSheet("background-color: #457B9D; padding: 4px; font-weight: bold; color: white;")
out_layout.addWidget(self.out_folder_btn)
self.out_folder_label = QLabel("未設定")
self.out_folder_label.setStyleSheet("background: #112240; padding: 4px; border: 1px solid #457B9D;")
out_layout.addWidget(self.out_folder_label, stretch=1)
layout.addLayout(out_layout)
self.log_list = QListWidget()
self.log_list.setStyleSheet("background: #112240; border: 1px solid #444; color: #E0E0E0;")
layout.addWidget(self.log_list)
self.copy_btn = QPushButton("📋 ログをコピー")
self.copy_btn.setStyleSheet("background-color: #1D3557; color: white;")
layout.addWidget(self.copy_btn)
folder_layout = QHBoxLayout()
self.folder_btns = [QPushButton(f"📁 読込元フォルダ {i+1}") for i in range(3)]
for b in self.folder_btns:
b.setStyleSheet("background-color: #1D3557; color: white;")
folder_layout.addWidget(b)
layout.addLayout(folder_layout)
self.exit_btn = QPushButton("システムを完全に終了"); self.exit_btn.setStyleSheet("background-color: #E63946; color: white; font-weight: bold; padding: 6px;")
layout.addWidget(self.exit_btn)
def log(self, message):
self.log_list.addItem(f"[{datetime.now().strftime('%H:%M:%S')}] {message}")
self.log_list.scrollToBottom(); write_syslog(message)
def closeEvent(self, event):
event.ignore(); self.hide()
if hasattr(self, 'tray_msg_callback'): self.tray_msg_callback()
class TrayApp(QApplication):
def init(self, sys_argv):
super().init(sys_argv)
self.setQuitOnLastWindowClosed(False)
self.settings = QSettings(“SapporoWeatherApp”, “GPVEngine”)
self.monitor_folders = [self.settings.value(f”watch_dir_{i+1}”, “”) for i in range(3)]
global CURRENT_OUTPUT_DIR
self.output_dir = self.settings.value("output_dir", DEFAULT_OUTPUT_DIR)
CURRENT_OUTPUT_DIR = self.output_dir
os.makedirs(self.output_dir, exist_ok=True)
self.last_reported = {}
self.window = EngineStatusWindow()
self.window.tray_msg_callback = self.show_tray_message
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(create_lightning_icon())
self.tray_icon.setToolTip("GPV抽出エンジン (稼働中)")
tray_menu = QMenu()
show_action = QAction("ウィンドウを表示"); show_action.triggered.connect(self.restore_window)
quit_action = QAction("完全に終了する"); quit_action.triggered.connect(self.quit)
tray_menu.addAction(show_action); tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_icon_activated); self.tray_icon.show()
with open(os.path.join(self.output_dir, "system_log.txt"), "w", encoding="utf-8") as f: pass
self.window.log("===============================")
self.window.log("システム起動: Ver 97.5 (堅牢コア復元 + 最新スキャン統合)")
self.window.log("===============================")
self.window.out_folder_label.setText(self.output_dir)
self.window.out_folder_btn.clicked.connect(self.select_output_folder)
folders_set = False
for i in range(3):
self.window.folder_btns[i].clicked.connect(lambda checked, idx=i: self.select_folder(idx))
self.update_folder_btn_text(i)
if self.monitor_folders[i]: folders_set = True
if not folders_set: self.window.status_label.setText("状態: 読込元フォルダ未設定")
self.window.copy_btn.clicked.connect(self.copy_logs); self.window.pause_btn.clicked.connect(self.pause_parsing)
self.window.resume_btn.clicked.connect(self.force_scan); self.window.stop_btn.clicked.connect(self.stop_parsing)
self.window.clear_btn.clicked.connect(self.clear_cache); self.window.exit_btn.clicked.connect(self.quit)
self.timer = QTimer(); self.timer.timeout.connect(self.run_parser); self.timer.start(10000)
self.is_processing = False; self.run_parser(); self.window.show()
def tray_icon_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick: self.restore_window()
def restore_window(self): self.window.showNormal(); self.window.activateWindow()
def show_tray_message(self):
self.tray_icon.showMessage("エンジン稼働中", "ウィンドウを隠しました。終了時は右クリックからどうぞ。", QIcon(self.tray_icon.icon()), 3000)
def select_output_folder(self):
global CURRENT_OUTPUT_DIR
folder = QFileDialog.getExistingDirectory(self.window, "出力先フォルダを選択", self.output_dir)
if folder:
self.output_dir = folder; CURRENT_OUTPUT_DIR = folder
self.settings.setValue("output_dir", folder); self.window.out_folder_label.setText(folder)
self.window.log(f"💾 出力先フォルダを更新しました: {folder}"); self.run_parser()
def clear_cache(self):
reply = QMessageBox.question(self.window, "確認", "バグで壊れたデータをすべて削除し、再抽出しますか?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.timer.stop(); self.window.log("🗑️ キャッシュを削除中...")
for f in glob.glob(os.path.join(self.output_dir, "*.npz")) + glob.glob(os.path.join(self.output_dir, "*.json")) + glob.glob(os.path.join(self.output_dir, "*.bin")):
try: os.remove(f)
except: pass
for d in self.monitor_folders:
if d:
for idx_f in glob.glob(os.path.join(d, '**', '*.idx'), recursive=True):
try: os.remove(idx_f)
except: pass
self.last_reported = {}; self.window.log(f"✨ クリーンな状態で再抽出を開始します!"); self.run_parser(); self.timer.start(10000)
def select_folder(self, index):
folder = QFileDialog.getExistingDirectory(self.window, "読込元フォルダ選択")
if folder:
self.monitor_folders[index] = folder; self.settings.setValue(f"watch_dir_{index+1}", folder)
self.update_folder_btn_text(index); self.window.log(f"フォルダ{index+1}を更新: {folder}"); self.run_parser()
def update_folder_btn_text(self, i):
f = self.monitor_folders[i]; self.window.folder_btns[i].setText(f"📁 {os.path.basename(f)}" if f else f"📁 未設定 {i+1}")
def copy_logs(self): QApplication.clipboard().setText("\n".join([self.window.log_list.item(i).text() for i in range(self.window.log_list.count())]))
def pause_parsing(self): self.timer.stop(); self.window.log("一時停止しました")
def force_scan(self): self.window.log("手動強制スキャン開始..."); self.timer.start(10000); self.run_parser()
def stop_parsing(self): self.timer.stop(); self.window.log("停止しました")
def run_parser(self):
if self.is_processing: return
if not any(self.monitor_folders): self.window.status_label.setText("状態: 読込元フォルダ未設定"); return
self.is_processing = True
self.worker = DataParserThread(self.monitor_folders, self.output_dir, self.last_reported)
self.worker.log_signal.connect(self.window.log); self.worker.progress_signal.connect(lambda v, t: self.window.status_label.setText(f"状態: {t}"))
self.worker.finished_signal.connect(self.on_parse_finished)
self.worker.start()
def on_parse_finished(self, has_new_data):
self.is_processing = False; self.window.status_label.setText("状態: 待機中 (自動スキャン継続中)")
if name == ‘main‘:
mp.freeze_support()
app = TrayApp(sys.argv)
sys.exit(app.exec())

コメント