jagirl / utils /logger.py
kechiro's picture
Initial commit: Jagirl UI application
a2e2ce2
"""
統一ログ機能モジュール
画像生成の全パラメータと結果を包括的に記録する統一ログシステム
設計原則:
- 生成に使用された全パラメータの記録
- 生成画像との確実な紐づけ
- JSON形式での構造化データ保存
- 検索・分析しやすい形式
- パフォーマンス情報の詳細記録
"""
import json
import os
import time
from datetime import datetime
from typing import Dict, Any, Optional
import torch
from PIL import Image
class UnifiedLogger:
"""統一ログ機能クラス"""
def __init__(self, log_dir: str = "logs"):
"""
ログ機能の初期化
Args:
log_dir: ログファイルを保存するディレクトリ
"""
self.log_dir = log_dir
self.json_log_file = os.path.join(log_dir, "generation_history.json")
# ログディレクトリ作成
os.makedirs(log_dir, exist_ok=True)
# 既存ログの読み込み
self._load_existing_logs()
def _load_existing_logs(self):
"""既存のログデータを読み込み"""
if os.path.exists(self.json_log_file):
try:
with open(self.json_log_file, 'r', encoding='utf-8') as f:
self.log_data = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
self.log_data = {"metadata": self._create_metadata(), "generations": []}
else:
self.log_data = {"metadata": self._create_metadata(), "generations": []}
def _create_metadata(self) -> Dict[str, Any]:
"""ログファイルのメタデータを作成"""
return {
"format_version": "2.0",
"created_at": datetime.now().isoformat(),
"last_updated": datetime.now().isoformat(),
"description": "Unified generation history for jagirl UI project",
"model_info": {
"model_name": "aipicasso/jagirl",
"model_type": "StableDiffusionXL",
"specialized_for": "Japanese female faces"
},
"log_schema": {
"timestamp": "ISO format timestamp",
"generation_id": "Unique identifier for each generation",
"prompts": "All text prompts used",
"parameters": "Complete parameter set used for generation",
"output": "Generated image information",
"performance": "Execution metrics",
"system_info": "Hardware and software environment"
}
}
def _get_image_info(self, filepath: str) -> Dict[str, Any]:
"""画像ファイルの詳細情報を取得"""
if not os.path.exists(filepath):
return {"error": "File not found"}
try:
# ファイルサイズ
file_size_bytes = os.path.getsize(filepath)
file_size_mb = round(file_size_bytes / (1024 * 1024), 3)
# 画像情報
with Image.open(filepath) as img:
width, height = img.size
mode = img.mode
format_type = img.format
return {
"filepath": os.path.abspath(filepath),
"file_url": f"file:///{os.path.abspath(filepath).replace(os.sep, '/')}",
"filename": os.path.basename(filepath),
"file_size_bytes": file_size_bytes,
"file_size_mb": file_size_mb,
"image_width": width,
"image_height": height,
"image_mode": mode,
"image_format": format_type,
"created_at": datetime.fromtimestamp(os.path.getctime(filepath)).isoformat()
}
except Exception as e:
return {"error": f"Failed to get image info: {str(e)}"}
def _get_system_info(self) -> Dict[str, Any]:
"""システム情報を取得"""
system_info = {
"python_version": None,
"torch_version": None,
"cuda_available": False,
"cuda_version": None,
"gpu_name": None,
"vram_total_gb": 0,
"vram_allocated_gb": 0
}
try:
import sys
system_info["python_version"] = sys.version.split()[0]
system_info["torch_version"] = torch.__version__
system_info["cuda_available"] = torch.cuda.is_available()
if torch.cuda.is_available():
system_info["cuda_version"] = torch.version.cuda
system_info["gpu_name"] = torch.cuda.get_device_name(0)
system_info["vram_total_gb"] = round(
torch.cuda.get_device_properties(0).total_memory / (1024**3), 2
)
system_info["vram_allocated_gb"] = round(
torch.cuda.memory_allocated(0) / (1024**3), 2
)
except Exception as e:
system_info["error"] = f"Failed to get system info: {str(e)}"
return system_info
def log_generation(
self,
prompt: str,
negative_prompt: str = "",
parameters: Dict[str, Any] = None,
output_filepath: str = "",
execution_time: float = 0.0,
additional_info: Dict[str, Any] = None
) -> str:
"""
画像生成の完全なログを記録
Args:
prompt: メインプロンプト
negative_prompt: ネガティブプロンプト
parameters: 生成に使用された全パラメータ
output_filepath: 生成された画像ファイルパス
execution_time: 実行時間(秒)
additional_info: 追加情報
Returns:
generation_id: 生成された記録のユニークID
"""
# ユニークIDの生成
timestamp = datetime.now()
generation_id = f"gen_{timestamp.strftime('%Y%m%d_%H%M%S')}_{int(time.time() * 1000) % 100000}"
# デフォルトパラメータの設定
if parameters is None:
parameters = {}
# 完全なパラメータセットの作成
complete_parameters = {
# 基本パラメータ
"num_inference_steps": parameters.get("num_inference_steps", 20),
"guidance_scale": parameters.get("guidance_scale", 7.5),
"width": parameters.get("width", 1024),
"height": parameters.get("height", 1024),
"seed": parameters.get("seed", None),
# スケジューラー関連
"scheduler_type": parameters.get("scheduler_type", "default"),
"eta": parameters.get("eta", 0.0),
# 画像生成関連
"num_images": parameters.get("num_images", 1),
"batch_size": parameters.get("batch_size", 1),
# モデル関連
"torch_dtype": str(parameters.get("torch_dtype", "float16")),
"enable_xformers": parameters.get("enable_xformers", False),
"enable_cpu_offload": parameters.get("enable_cpu_offload", False),
# その他のパラメータ
**{k: v for k, v in parameters.items() if k not in [
"num_inference_steps", "guidance_scale", "width", "height",
"seed", "scheduler_type", "eta", "num_images", "batch_size",
"torch_dtype", "enable_xformers", "enable_cpu_offload"
]}
}
# ログエントリの作成
log_entry = {
"generation_id": generation_id,
"timestamp": timestamp.isoformat(),
"prompts": {
"main_prompt": prompt,
"negative_prompt": negative_prompt,
"prompt_length": len(prompt),
"negative_prompt_length": len(negative_prompt)
},
"parameters": complete_parameters,
"output": self._get_image_info(output_filepath) if output_filepath else {},
"performance": {
"execution_time_seconds": round(execution_time, 3),
"estimated_speed_sec_per_step": round(
execution_time / max(complete_parameters.get("num_inference_steps", 1), 1), 3
) if execution_time > 0 else 0
},
"system_info": self._get_system_info(),
"additional_info": additional_info or {}
}
# ログに追加
self.log_data["generations"].append(log_entry)
self.log_data["metadata"]["last_updated"] = timestamp.isoformat()
# ファイルに保存
self._save_logs()
return generation_id
def _save_logs(self):
"""ログをファイルに保存"""
try:
with open(self.json_log_file, 'w', encoding='utf-8') as f:
json.dump(self.log_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"ログ保存エラー: {e}")
def get_generation_by_id(self, generation_id: str) -> Optional[Dict[str, Any]]:
"""generation_idで特定の生成記録を取得"""
for generation in self.log_data["generations"]:
if generation["generation_id"] == generation_id:
return generation
return None
def get_recent_generations(self, count: int = 10) -> list:
"""最近の生成記録を取得"""
return self.log_data["generations"][-count:] if self.log_data["generations"] else []
def search_by_prompt(self, search_term: str, case_sensitive: bool = False) -> list:
"""プロンプトで検索"""
results = []
search_term = search_term if case_sensitive else search_term.lower()
for generation in self.log_data["generations"]:
main_prompt = generation["prompts"]["main_prompt"]
if not case_sensitive:
main_prompt = main_prompt.lower()
if search_term in main_prompt:
results.append(generation)
return results
def get_statistics(self) -> Dict[str, Any]:
"""生成統計を取得"""
generations = self.log_data["generations"]
if not generations:
return {"total_generations": 0}
total_time = sum(g["performance"]["execution_time_seconds"] for g in generations)
avg_time = total_time / len(generations)
schedulers = {}
for g in generations:
scheduler = g["parameters"].get("scheduler_type", "unknown")
schedulers[scheduler] = schedulers.get(scheduler, 0) + 1
return {
"total_generations": len(generations),
"total_execution_time_hours": round(total_time / 3600, 2),
"average_execution_time_seconds": round(avg_time, 2),
"scheduler_usage": schedulers,
"date_range": {
"first": generations[0]["timestamp"],
"last": generations[-1]["timestamp"]
}
}
def cleanup_old_logs(self, keep_days: int = 30):
"""古いログエントリを削除"""
cutoff_date = datetime.now().timestamp() - (keep_days * 24 * 3600)
original_count = len(self.log_data["generations"])
self.log_data["generations"] = [
g for g in self.log_data["generations"]
if datetime.fromisoformat(g["timestamp"]).timestamp() > cutoff_date
]
removed_count = original_count - len(self.log_data["generations"])
if removed_count > 0:
self._save_logs()
print(f"古いログエントリ {removed_count} 件を削除しました")
return removed_count
# グローバルロガーインスタンス
_global_logger = None
def get_logger(log_dir: str = "logs") -> UnifiedLogger:
"""グローバルロガーインスタンスを取得"""
global _global_logger
if _global_logger is None:
_global_logger = UnifiedLogger(log_dir)
return _global_logger
def log_generation(**kwargs) -> str:
"""グローバルロガーを使用して生成をログ"""
logger = get_logger()
return logger.log_generation(**kwargs)
def get_statistics() -> Dict[str, Any]:
"""生成統計を取得"""
logger = get_logger()
return logger.get_statistics()