未分類

me

==========================================

【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())

コメント