Newer
Older
TIASjudge / src / tiasjudge / __main__.py
"""エントリポイント(`python -m tiasjudge`).

`QApplication` を生成してメイン画面(SPEC_01)を表示する.
ログの出力先設定はエントリポイント側の責務とする(error-handling.md「ログ」).

起動オプション:

- `--check`: 画面を生成して即終了する(起動可能性の確認用.イベントループを回さない)
- `--preview-result`: 結果画面(SPEC_01「結果画面」)だけをダミー結果で開く(デバッグ用)
- `--failed`: `--preview-result` と併用し,診断失敗パターンで開く
- `--english`: `--preview-result` と併用し,英語表示で開く(結果画面は表示専用のため,
  プレビューでは画面内で言語を切り替えられない)
"""

from __future__ import annotations

import argparse
import logging
import sys
from collections.abc import Sequence
from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:  # pragma: no cover - 型チェック時のみ
    from numpy.typing import NDArray
    from PySide6.QtWidgets import QApplication

    from tiasjudge.core.svm import DiagnosisResult

logger = logging.getLogger(__name__)

#: `--check`(イベントループを回さずに終了する)オプション
CHECK_OPTION = "--check"

#: ログの書式(診断の入出力は `core` が INFO で記録する)
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"

# --- `--preview-result` 用のダミーデータ(デバッグ表示専用.診断は実行しない)---

#: プレビューに使うダミー画像の一辺(結果画面は 1/2 に縮小して表示する)
PREVIEW_IMAGE_SIZE = 400
#: プレビューに使うダミー画像の色(舌色に近い RGB)
PREVIEW_IMAGE_RGB = (198, 120, 120)
#: プレビューのダミー L*a*b*(学習データの範囲内にある代表値)
PREVIEW_LAB = (58.0, 24.0, 12.0)
#: 成功パターンのダミースコア(%)
PREVIEW_PERCENTAGES: dict[str, int] = {
    "purple": 5,
    "dark_red": 10,
    "crimson": 25,
    "pink": 55,
    "pale": 5,
}
#: 成功パターンの上位 2 色(`PREVIEW_PERCENTAGES` を降順に並べた先頭 2 件)
PREVIEW_TOP_LABELS = ("pink", "crimson")
#: 失敗パターンの上位 2 色(`LEGACY_FAILURE_PERCENTAGES` を降順に並べた先頭 2 件)
PREVIEW_FAILED_TOP_LABELS = ("purple", "pink")


def _build_parser() -> argparse.ArgumentParser:
    """コマンドライン引数のパーサを作る."""
    parser = argparse.ArgumentParser(
        prog="tiasjudge",
        description="舌診支援システム(TIAS)を起動する.",
    )
    parser.add_argument(
        CHECK_OPTION,
        action="store_true",
        help="画面を生成して即終了する(起動可能性の確認用)",
    )
    parser.add_argument(
        "--preview-result",
        action="store_true",
        help="結果画面だけをダミー結果で開く(デバッグ用.診断は実行しない)",
    )
    parser.add_argument(
        "--failed",
        action="store_true",
        help="--preview-result と併用し,診断失敗パターンで開く",
    )
    parser.add_argument(
        "--english",
        action="store_true",
        help="--preview-result と併用し,英語表示で開く",
    )
    return parser


def _preview_image() -> NDArray[np.uint8]:
    """プレビュー用の単色ダミー画像(BGR)を作る."""
    red, green, blue = PREVIEW_IMAGE_RGB
    image = np.empty((PREVIEW_IMAGE_SIZE, PREVIEW_IMAGE_SIZE, 3), dtype=np.uint8)
    # OpenCV と同じ BGR 並びで埋める
    image[:, :] = (blue, green, red)
    return image


def _preview_result(*, failed: bool) -> DiagnosisResult:
    """プレビュー用のダミー診断結果を組み立てる(SVM 学習は行わない)."""
    from tiasjudge.core.color import Lab
    from tiasjudge.core.csv_io import EVALUATION_COLUMN_COUNT
    from tiasjudge.core.svm import (
        LEGACY_FAILURE_PERCENTAGES,
        DiagnosisResult,
        FailureReason,
    )

    percentages = dict(LEGACY_FAILURE_PERCENTAGES) if failed else dict(PREVIEW_PERCENTAGES)
    top_labels = PREVIEW_FAILED_TOP_LABELS if failed else PREVIEW_TOP_LABELS
    return DiagnosisResult(
        lab=Lab(*PREVIEW_LAB),
        scores={key: value / 100 for key, value in percentages.items()},
        percentages=percentages,
        top_labels=top_labels,
        predictions=np.zeros(EVALUATION_COLUMN_COUNT, dtype=np.float32),
        failed=failed,
        failure_reason=FailureReason.OUT_OF_DOMAIN if failed else None,
    )


def _run_preview(app: QApplication, *, failed: bool, check: bool, english: bool = False) -> int:
    """結果画面だけをダミー結果で開く(`--preview-result`).

    結果画面は表示専用(言語切替ボタンを持たない)ため,プレビューの言語は
    `--english` オプションで指定する.
    """
    # 関数内 import は起動時のみ GUI を読み込むため(`core` は GUI 非依存)
    from tiasjudge.gui.result_window import Language, ResultWindow, show_on_secondary_screen

    language = Language.ENGLISH if english else Language.JAPANESE
    logger.info("結果画面のプレビューを開きます(failed=%s・language=%s)", failed, language.value)
    window = ResultWindow(_preview_result(failed=failed), _preview_image(), language)
    if check:
        window.close()
        return 0

    # プレビューにはメイン画面が無いため,プライマリモニタを基準に別モニタを選ぶ
    show_on_secondary_screen(window, app.screens(), app.primaryScreen())
    return int(app.exec())


def main(argv: Sequence[str] | None = None) -> int:
    """アプリを起動する.戻り値はプロセスの終了コード.

    Args:
        argv: コマンドライン引数(プログラム名を含まない).省略時は `sys.argv[1:]`.

    Returns:
        プロセスの終了コード(0 が正常終了).
    """
    arguments = _build_parser().parse_args(list(sys.argv[1:] if argv is None else argv))
    logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)

    # GUI ツールキットの import は起動時のみに限定する(`core` は GUI 非依存)
    from PySide6.QtWidgets import QApplication

    app = QApplication.instance() or QApplication(sys.argv[:1])

    if arguments.preview_result:
        return _run_preview(
            app,
            failed=arguments.failed,
            english=arguments.english,
            check=arguments.check,
        )

    from tiasjudge.gui.main_window import MainWindow

    window = MainWindow()
    if arguments.check:
        # 画面を構築できることだけを確認して終了する(ヘッドレス環境での確認用)
        window.close()
        return 0

    window.show()
    return int(app.exec())


if __name__ == "__main__":
    raise SystemExit(main())