自動でテックブログ生成ツールのブログを自動テックブログ生成ツールに書かせてみた

はじめまして

仕事や趣味で簡単なコードを書くことがあり、テックブログを書いてみたいという気持ちはありましたが、「紹介するほどのものでもない」、「コードの説明を日本語で書くのは面倒」などと思って敬遠してました。

そこで、近年目覚ましい進化を遂げている生成AIに自動でテックブログを書いてもらえるツールを作って、LLMにテックブログを書いてみてもらおうと思い立たち作ってみました。

[Webアプリ(デモ)](http://www.techblog-generator.igtcons.com)
[GitHubリポジトリ](https://github.com/nibura2002/tech-blog-generator)

ここから下は自動でテックブログを生成するツールに書かせた、自身のリポジトリについてのテックブログです。うまく正しい内容が書かれているでしょうか？


# 1章: はじめに - Tech Blog Generatorとは

## 1-1: Tech Blog Generatorの概要

### 1-1-1: Tech Blog Generatorの紹介

Tech Blog Generatorは、ソフトウェアエンジニアが技術ブログを効率的に作成するためのツールです。プロジェクトのソースコード（GitHubリポジトリまたはローカルフォルダ）を解析し、その構造、各ファイルの役割、詳細なコード解説を自動生成します。これらの情報を基に、ブログ記事のアウトラインと最終的なMarkdown形式の記事を生成します。

### 1-1-2: ターゲットユーザー

このツールは、以下のようなエンジニアに特に役立ちます。

*   **時間がないエンジニア**: ドキュメント作成に時間をかけられないが、技術情報を共有したいエンジニア。
*   **ドキュメント作成が苦手なエンジニア**: コードは書けるが、文章化が苦手なエンジニア。
*   **技術ブログを始めたいエンジニア**: 何から書けば良いか分からないエンジニア。
*   **チームで知識を共有したいエンジニア**: プロジェクトの構造やコードを他のメンバーに伝えたいエンジニア。

## 1-2: 解決する課題

### 1-2-1: ドキュメント作成の課題

プロジェクトのドキュメント作成には、以下のような課題がつきものです。

*   **時間と労力**: ドキュメント作成には多くの時間と労力がかかります。
*   **属人化**: ドキュメントが特定の担当者に依存し、更新が滞る可能性があります。
*   **情報の陳腐化**: コードの変更にドキュメントの更新が追いつかず、情報が古くなることがあります。
*   **品質のばらつき**: ドキュメントの品質が担当者のスキルに依存し、ばらつきが生じることがあります。
*   **学習コスト**: 新しいプロジェクトに参加するメンバーが、コードを理解するのに時間がかかることがあります。

### 1-2-2: Tech Blog Generatorによる解決策

Tech Blog Generatorは、これらの課題に対して以下の解決策を提供します。

*   **自動化による効率化**: ソースコードの解析から記事の生成までを自動化し、ドキュメント作成にかかる時間と労力を大幅に削減します。
*   **標準化された形式**: 記事の構成やコード解説の形式を標準化し、品質のばらつきを抑えます。
*   **最新情報の維持**: ソースコードから直接情報を抽出するため、常に最新の状態を反映したドキュメントを作成できます。
*   **知識の共有促進**: プロジェクトの構造やコードを分かりやすく解説することで、チーム内での知識共有を促進します。
*   **ブログ作成の敷居を下げる**: 記事の雛形を自動生成することで、技術ブログを始める際のハードルを下げます。

Tech Blog Generatorを活用することで、エンジニアはより効率的に技術情報を共有し、プロジェクトの品質向上に貢献できます。
# 1章: はじめに - Tech Blog Generatorとは

## 1-1: Tech Blog Generatorの概要

### 1-1-1: Tech Blog Generatorの紹介

Tech Blog Generatorは、プロジェクトのソースコードから技術ブログの記事を自動生成するツールです。GitHubリポジトリまたはローカルのプロジェクトフォルダを解析し、その構造、各ファイルの役割、そしてコードの詳細な解説を基に、ブログ記事のアウトラインと本文を生成します。

### 1-1-2: ターゲットユーザー

このツールは、以下のようなエンジニアに役立ちます。

*   **技術情報を発信したいが、記事作成に時間が割けないエンジニア:** プロジェクトのコードを基に記事を自動生成するため、執筆時間を大幅に削減できます。
*   **プロジェクトのドキュメントを効率的に作成したいエンジニア:** ソースコードから詳細な解説を生成することで、ドキュメント作成の負担を軽減できます。
*   **技術ブログを始めたいが、何を書けば良いか分からないエンジニア:** プロジェクトの解析結果から記事のアイデアを得ることができます。

## 1-2: 解決する課題

### 1-2-1: ドキュメント作成の課題

プロジェクトのドキュメント作成には、以下のような課題が伴います。

*   **時間と労力:** ドキュメントの作成には、コードの理解、構成の検討、文章の執筆など、多くの時間と労力が必要です。
*   **情報の陳腐化:** コードが変更されると、ドキュメントも更新する必要がありますが、その作業が疎かになりがちです。
*   **属人化:** ドキュメントの作成が特定の担当者に依存し、知識の共有が不十分になることがあります。
*   **品質のばらつき:** ドキュメントの品質が担当者のスキルに依存し、一貫性が保たれないことがあります。

### 1-2-2: Tech Blog Generatorによる解決策

Tech Blog Generatorは、これらの課題に対し、以下のような解決策を提供します。

*   **自動生成による効率化:** ソースコードから自動的にブログ記事を生成することで、ドキュメント作成にかかる時間と労力を大幅に削減します。
*   **コードとの連携:** ソースコードの変更に合わせてブログ記事を更新することで、情報の陳腐化を防ぎます。
*   **知識の共有:** 生成されたブログ記事は、プロジェクトメンバー間で知識を共有するための貴重なリソースとなります。
*   **品質の標準化:** ツールによって生成される記事は、一定の品質を保つことができます。

Tech Blog Generatorを活用することで、エンジニアはより効率的に技術情報を発信し、プロジェクトのドキュメント作成を円滑に進めることができるようになります。

# 2章: プロジェクトの構造と各ファイルの役割

## 2-1: ディレクトリ構造の概要

### 2-1-1: ディレクトリ構造の可視化

プロジェクトのディレクトリ構造は、プロジェクトの全体像を把握する上で非常に重要です。以下に、このプロジェクトのディレクトリ構造をツリー形式で示します。

```
├── tmpl1e21yo5/
│   ├── index.html
│   ├── Dockerfile
│   ├── pyproject.toml
│   ├── prompt.cpython-312.pyc
│   ├── __init__.py
│   ├── README.md
│   ├── main.js
│   ├── style.css
│   ├── app.py
│   ├── pygments.css
│   ├── prompt.py
│   ├── const.cpython-312.pyc
│   ├── poetry.lock
│   ├── __init__.cpython-312.pyc
│   ├── const.py
```

この構造を理解することで、各ファイルがプロジェクト内でどのような役割を果たしているかを理解しやすくなります。

### 2-1-2: 主要ディレクトリの説明

このプロジェクトの主要なディレクトリとその役割について説明します。

*   **tmpl1e21yo5/**: これは一時的なディレクトリであり、プロジェクトのファイルが格納されています。名前は毎回変わる可能性があります。
*   **static/**: CSSやJavaScriptなどの静的ファイルが格納されています。
*   **templates/**: HTMLテンプレートファイルが格納されています。
*   **const/**: アプリケーションで使用される定数やプロンプトが格納されています。

これらのディレクトリを適切に管理することで、プロジェクトの保守性と拡張性を高めることができます。

## 2-2: 主要ファイルの役割

### 2-2-1: index.html

`index.html`は、ウェブページの構造とコンテンツを定義するHTMLファイルです。このファイルには、ページのタイトル、スタイルシートのリンク、JavaScriptファイルのリンク、そしてメインコンテンツが含まれています。

`index.html`の詳しいコード解説は以下の通りです。

#### HTMLの基本構造とメタデータ

```html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Tech Blog Generator</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/pygments.css') }}">
    <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6831610624805777"
        crossorigin="anonymous"></script>
</head>
```

このセクションでは、HTMLドキュメントの基本的な構造を定義し、文字コード、タイトル、スタイルシート、広告スクリプトを設定します。これにより、ブラウザはコンテンツを正しく解釈し、適切なスタイルと機能を提供できます。広告スクリプトは収益化のために使用されます。

#### サイドバー：情報入力フォーム

```html
<body>
    <div class="container">
        <!-- サイドバー（常に表示） -->
        <aside class="sidebar">
            <h2>情報入力</h2>
            <form id="projectForm" method="POST" enctype="multipart/form-data">
                <label>プロジェクトのフォルダ選択:<br>
                    <input type="file" name="project_folder" webkitdirectory directory multiple>
                </label>
                <br><br>
                <label>GithubリポジトリのURL:<br>
                    <input type="url" name="github_url" placeholder="https://github.com/your-repo">
                </label>
                <br><br>
                <label>ターゲット読者層:<br>
                    <input type="text" name="target_audience" value="エンジニア全般">
                </label>
                <br><br>
                <label>ブログのトーン:<br>
                    <input type="text" name="blog_tone" value="カジュアルだけど専門性を感じるトーン">
                </label>
                <br><br>
                <!-- 言語変更未対応のため、コメントアウト -->
                <!--
                <label>言語の選択:<br>
                    <select name="language">
                        <option value="ja" selected>日本語</option>
                        <option value="en">English</option>
                    </select>
                </label>
                <br><br>
                -->
                <label>その他リクエスト:<br>
                    <textarea name="additional_requirements" rows="3"></textarea>
                </label>
                <br><br>
                <label for="model">モデル選択</label>
                <select name="model" id="model" class="form-control">
                    <option value="gemini-2.0-flash">Google: gemini-2.0-flash</option>
                    <option value="gemini-1.5-pro">Google: gemini-1.5-pro</option>                    
                    <option value="gpt-4o">OpenAI: gpt-4o</option>
                    <option value="o3-mini">OpenAI: o3-mini</option>
                </select>
                <br><br>
                <button type="submit">テックブログを生成する</button>
            </form>
        </aside>
```

ブログ生成に必要な情報を入力するためのサイドバーを定義します。フォームは、プロジェクトフォルダの選択、GitHubリポジトリのURL、ターゲット読者層、ブログのトーン、その他リクエスト、モデル選択を提供します。これらの入力に基づいて、ブログの内容がカスタマイズされます。フォームデータはサーバーにPOST送信されます。

#### メインコンテンツ：メッセージ表示と状態に応じた画面表示

```html
        <!-- メインコンテンツ -->
        <main class="main-content">
            {% with messages = get_flashed_messages(category_filter=["error", "info", "warning"]) %}
            {% if messages %}
            <ul class="flashes">
                {% for message in messages %}
                <li>{{ message }}</li>
                {% endfor %}
            </ul>
            {% endif %}
            {% endwith %}

            {% if blog_markdown and viewType == "final" %}
            <h1>最終ブログ確認</h1>
            <div class="final-container">
                <!-- 編集パネル -->
                <div class="edit-panel">
                    <!-- アウトライン編集エリア -->
                    <div class="outline-section">
                        <h2>アウトライン (編集可能)</h2>
                        <form id="outlineForm">
                            <textarea name="edited_outline" rows="15" cols="100">{{ blog_outline|e }}</textarea>
                        </form>
                    </div>
                    <!-- 本文編集エリア -->
                    <div class="content-section">
                        <h2>本文 (編集可能)</h2>
                        <form id="blogForm" method="POST">
                            <textarea name="edited_markdown" rows="25" cols="100">{{ blog_markdown|e }}</textarea>
                        </form>
                    </div>
                    <!-- 再生成ボタン群（アウトライン・本文を隣接して配置） -->
                    <div class="regen-buttons">
                        <button type="button" id="regenerateOutlineButton"
                            onclick="submitOutline()">アウトライン・本文を再生成</button>
                        <button type="button" id="regenerateContentButton"
                            onclick="submitBlogGeneration()">本文を再生成</button>
                    </div>
                </div>
                <!-- プレビューセクション -->
                <div class="preview-section">
                    <h2>プレビュー (Markdown → HTML)</h2>
                    <form id="previewForm">
                        <button type="button" onclick="updatePreview()">Previewを更新</button>
                    </form>
                    <div id="preview-container">{{ converted_html|safe }}</div>
                    <br>
                    <a href="{{ url_for('download_markdown') }}">この内容でMarkdownをダウンロード</a>
                </div>
            </div>
            <!-- 「リセット」ボタンの追加 -->
            <br><br>
            <form action="{{ url_for('reset') }}" method="get">
                <button type="submit">すべての情報をリフレッシュして最初の画面に戻る</button>
            </form>
            <script>
                // 最終生成画面ではリロード用フラグをクリア
                sessionStorage.removeItem("reloadTriggered");
            </script>
            {% elif blog_markdown and viewType == "preview" %}
            {% elif blog_outline and viewType == "outline" %}
            <!-- アウトライン確認画面（既存） -->
            <h1>アウトライン確認</h1>
            <form id="outlineForm">
                <textarea name="edited_outline" rows="20" cols="100">{{ blog_outline|e }}</textarea>
                <br><br>
                <button type="button" id="generateButton" onclick="submitOutline()">このアウトラインで最終ブログを生成</button>
            </form>
            <!-- 「リセット」ボタンの追加 -->
            <br><br>
            <form action="{{ url_for('reset') }}" method="get">
                <button type="submit">すべての情報をリフレッシュして最初の画面に戻る</button>
            </form>
            {% elif viewType == "status" %}
            <!-- ブログ生成ステータス画面（既存） -->
            <h1>ブログ生成ステータス</h1>
            <p>現在、最終ブログ生成処理が進行中です。しばらくお待ちください。</p>
            <div id="progress" style="display:none;">進捗情報をここに表示します…</div>
            {% else %}
            <!-- 初期状態 -->
            <h1>Tech Blog Generator</h1>
            <!-- 使用方法の説明を整形して表示 -->
            <div class="box">
                <ul>
                    <li>このツールは、プロジェクトフォルダ/GitHub リポジトリからテックブログを生成するためのツールです。</li>
                    <li>サイドバーから「プロジェクトフォルダを選択」または「GitHub リポジトリの URL」を入力してください。</li>
                    <li>その他、ターゲット読者層、ブログのトーン、その他リクエストを入力することで、ブログの内容をカスタマイズできます。</li>
                    <li>「テックブログを生成する」ボタンをクリックすると、ブログ生成処理が開始されます。</li>
                    <li>ブログのアウトラインが生成されると、アウトライン確認画面が表示されます。</li>
                    <li>アウトライン確認画面で「このアウトラインで最終ブログを生成」ボタンをクリックすると、ブログが生成されます。</li>
                    <li>ブログ生成が完了すると、最終ブログ確認画面が表示されます。</li>
                </ul>
            </div>

            {% endif %}
            <!-- 進捗履歴表示 -->
            {% if progress_log and viewType != "final" %}
            <h3>進捗履歴</h3>
            <pre id="progress_history">{{ progress_log }}</pre>
            {% endif %}
        </main>
```

メインコンテンツ領域を定義します。`get_flashed_messages`を使用して、エラー、情報、警告メッセージを表示します。`blog_markdown`と`viewType`の値に応じて、最終ブログ確認画面、アウトライン確認画面、ブログ生成ステータス画面、または初期状態の画面を表示します。各画面には、対応するフォームやボタンが含まれています。

#### 進捗履歴の表示

```html
{% if progress_log and viewType != "final" %}
    <h3>進捗履歴</h3>
    <pre id="progress_history">{{ progress_log }}</pre>
{% endif %}
```

`progress_log`が存在し、かつ`viewType`が`final`でない場合に、ブログ生成の進捗履歴を表示します。これにより、ユーザーはブログ生成の過程を追跡できます。

#### JavaScriptによるviewTypeの受け渡しと外部JavaScriptファイルの読み込み

```html
    </div>
    <!-- viewType を JS 変数に出力 -->
    <script>
        var viewType = "{{ viewType }}";
        console.log("viewType:", viewType);
    </script>
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>

</html>
```

Flaskから渡された`viewType`変数をJavaScriptで使用できるようにします。また、外部JavaScriptファイル（`main.js`）を読み込み、クライアントサイドのロジックを実装します。

### 2-2-2: Dockerfile

`Dockerfile`は、アプリケーションをコンテナ化するための設定ファイルです。このファイルには、ベースイメージ、必要な依存関係、環境変数、そしてアプリケーションの起動コマンドが含まれています。

`Dockerfile`の詳しいコード解説は以下の通りです。

#### ベースイメージの選択とシステム依存関係のインストール

```dockerfile
FROM python:3.12-slim

# Install system dependencies required for building packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl build-essential && rm -rf /var/lib/apt/lists/*
```

このセクションでは、Python 3.12のslim版のDockerイメージをベースとして使用し、必要なシステム依存関係をインストールします。`apt-get update`でパッケージリストを更新し、`curl`と`build-essential`をインストールします。`--no-install-recommends`オプションを使用することで、推奨パッケージのインストールを避け、イメージサイズを削減します。最後に、パッケージリストを削除してイメージサイズをさらに削減します。

#### Poetryのインストールと設定

```dockerfile
# Install Poetry (Python dependency manager)
RUN curl -sSL https://install.python-poetry.org | python -
ENV PATH="/root/.local/bin:$PATH"
```

このセクションでは、Pythonの依存関係管理ツールであるPoetryをインストールします。Poetryのインストーラをダウンロードして実行し、Poetryの実行可能ファイルがPATH環境変数に追加されるように設定します。これにより、後続のステップでPoetryコマンドを使用できるようになります。

#### ワーキングディレクトリの設定と依存関係のコピー

```dockerfile
# Set the working directory
WORKDIR /app

# Copy dependency definition files first for caching purposes
COPY pyproject.toml poetry.lock* /app/
```

このセクションでは、コンテナ内のワーキングディレクトリを`/app`に設定します。次に、`pyproject.toml`と`poetry.lock`ファイルをワーキングディレクトリにコピーします。これらのファイルは、アプリケーションの依存関係を定義するために使用されます。先にこれらのファイルをコピーすることで、アプリケーションのソースコードが変更されない限り、依存関係のインストールステップがキャッシュされるため、ビルド時間を短縮できます。

#### 依存関係のインストール

```dockerfile
# Configure Poetry to install production dependencies (excluding dev dependencies) without creating a virtual environment
RUN poetry config virtualenvs.create false && \
    poetry install --no-root --no-interaction --no-ansi
```

このセクションでは、Poetryを使用してアプリケーションの依存関係をインストールします。`poetry config virtualenvs.create false`コマンドは、Poetryに仮想環境を作成しないように指示します。`poetry install --no-root --no-interaction --no-ansi`コマンドは、ルートプロジェクトをインストールせずに、対話モードを無効にし、ANSI出力を無効にして、依存関係をインストールします。

#### ビルド依存関係の削除

```dockerfile
# Remove build dependencies to reduce the final image size
RUN apt-get purge -y --auto-remove build-essential && rm -rf /var/lib/apt/lists/*
```

このセクションでは、ビルドに必要な依存関係を削除して、最終的なイメージサイズを削減します。`apt-get purge -y --auto-remove build-essential`コマンドは、`build-essential`パッケージとその依存関係を削除します。最後に、パッケージリストを削除してイメージサイズをさらに削減します。

#### Gitのインストール

```dockerfile
# Install Git
RUN apt-get update && apt-get install -y git
```

このセクションでは、Gitをインストールします。これにより、コンテナ内でGitコマンドを使用できるようになります。

#### アプリケーションソースコードのコピー

```dockerfile
# Copy the application source code
COPY . /app
```

このセクションでは、アプリケーションのソースコードをワーキングディレクトリにコピーします。これにより、コンテナ内でアプリケーションを実行できるようになります。

#### 非rootユーザーの作成と権限設定

```dockerfile
# Create a non-root user for security and change ownership of the app directory
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser:appuser /app
USER appuser
```

このセクションでは、セキュリティのために、非rootユーザー`appuser`を作成し、アプリケーションディレクトリの所有者を`appuser`に変更します。`adduser --disabled-password --gecos '' appuser`コマンドは、パスワードなしで、追加情報なしに`appuser`を作成します。`chown -R appuser:appuser /app`コマンドは、`/app`ディレクトリとその内容の所有者を`appuser`に変更します。`USER appuser`コマンドは、後続のコマンドを`appuser`として実行するように指定します。

#### ポートの公開

```dockerfile
# Expose the port used by the Flask application (5001 as defined in app.py)
EXPOSE 8080
```

このセクションでは、Flaskアプリケーションが使用するポート8080を公開します。これにより、コンテナ外部からアプリケーションにアクセスできるようになります。

#### Gunicornによるアプリケーションの実行

```dockerfile
# Use Gunicorn to run the application (assumes the Flask app object is defined in app:app)
CMD ["gunicorn", "-w", "1", "--worker-class", "gthread", "--threads", "4", "--timeout", "3600", "-b", "0.0.0.0:8080", "app:app"]
```

このセクションでは、Gunicornを使用してFlaskアプリケーションを実行します。`CMD`命令は、コンテナが起動したときに実行するコマンドを指定します。`gunicorn -w 1 --worker-class gthread --threads 4 --timeout 3600 -b 0.0.0.0:8080 app:app`コマンドは、Gunicornを起動し、1つのワーカープロセスを使用し、ワーカークラスとして`gthread`を使用し、4つのスレッドを使用し、タイムアウトを3600秒に設定し、すべてのインターフェースのポート8080でリッスンするように指示します。`app:app`は、Flaskアプリケーションオブジェクトが`app.py`ファイル内の`app`変数として定義されていることを示します。

### 2-2-3: pyproject.tomlとpoetry.lock

`pyproject.toml`と`poetry.lock`は、Pythonプロジェクトの依存関係を管理するために使用されるファイルです。`pyproject.toml`はプロジェクトの設定ファイルであり、依存関係やビルド設定などが記述されています。`poetry.lock`は、Poetryによって管理される依存関係のバージョンを固定するためのファイルです。

`pyproject.toml`の詳しいコード解説は以下の通りです。

#### Poetryプロジェクト設定

```toml
[tool.poetry]
name = "auto-blog"
version = "0.1.0"
description = ""
readme = "README.md"
```

このセクションでは、Poetryプロジェクトの基本的な設定を定義しています。プロジェクト名、バージョン、説明、作者、READMEファイルなどを指定します。

#### 依存関係の定義

```toml
[tool.poetry.dependencies]
python = "^3.12"
langchain = "^0.3.18"
langchain-community = "^0.3.17"
langchain-openai = "^0.3.5"
flask = "^3.1.0"
python-dotenv = "^1.0.1"
werkzeug = "^3.1.3"
markdown = "^3.7"
gunicorn = "^23.0.0"
autopep8 = "^2.3.2"
pre-commit = "^4.1.0"
langchain-google-genai = "^2.0.9"
```

このセクションでは、プロジェクトが依存するPythonパッケージとそのバージョンを指定しています。`python = "^3.12"`はPython 3.12以上を意味します。`^`記号は、互換性のある最新バージョンを許可します。

#### パッケージの指定

```toml
[[tool.poetry.packages]]
include = "const"
from = "."  # ← プロジェクトルート（"Auto-blog"）から探す
```

このセクションでは、プロジェクトに含めるパッケージを指定しています。`include = "const"`は、`const`という名前のパッケージをプロジェクトに含めることを意味します。`from = "."`は、プロジェクトのルートディレクトリからパッケージを探すように指示しています。

#### ビルドシステムの設定

```toml
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

このセクションでは、プロジェクトのビルドに使用するシステムを指定しています。`requires = ["poetry-core"]`は、ビルドに`poetry-core`が必要であることを意味します。`build-backend = "poetry.core.masonry.api"`は、PoetryのMasonry APIをビルドバックエンドとして使用することを指定しています。

### 2-2-4: app.py

`app.py`は、Flaskアプリケーションのメインロジックを記述するPythonファイルです。このファイルには、ルーティング、ビュー関数、そしてアプリケーションの起動処理が含まれています。

`app.py`の詳しいコード解説は以下の通りです。

#### モジュールのインポート

```python
import os
import re
import logging
import subprocess
import tempfile
import threading
import uuid
import markdown
import json
import time

from dotenv import load_dotenv
from flask import Flask, request, render_template, redirect, url_for, flash, send_file, session, jsonify, Response, stream_with_context
from werkzeug.utils import secure_filename

# LLM用プロンプトのインポート
from const.prompt import (
    file_role_prompt_template,
    code_detail_prompt_template,
    blog_outline_prompt_template,
    final_blog_prompt_template,
    context_blog_prompt_template,
    chapter_generation_prompt_template
)

# Disallowed file extensionsのインポート
from const.const import DISALLOWED_EXTENSIONS, MAX_FILE_SIZE, MAX_FILE_LENGTH, IGNORED_DIRECTORIES, DEFAULT_ENCODING

# LangChain & OpenAI imports
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
```

必要なPythonモジュールをインポートします。標準ライブラリのモジュールに加えて、`.env`ファイルから環境変数をロードするための`dotenv`、Flaskフレームワーク、LangChainとOpenAIのモジュールが含まれます。

#### ロギング設定

```python
###############################################################################
# Logging configuration
###############################################################################
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)
```

アプリケーションのログを設定します。ログレベルをINFOに設定し、ログメッセージのフォーマットを指定します。これにより、アプリケーションの実行中に発生するイベントを記録し、デバッグや監視に役立てることができます。

#### 環境変数のロードとAPIキーの確認

```python
###############################################################################
# Load environment variables & check for OpenAI key
###############################################################################
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
    logger.error("OPENAI_API_KEY is not set. Please set it in the .env file.")
    raise EnvironmentError(
        "OPENAI_API_KEY is not set. Please set it in the .env file.")
logger.info("OPENAI_API_KEY successfully loaded.")

google_api_key = os.getenv("GOOGLE_API_KEY")
if not google_api_key:
    raise EnvironmentError("GOOGLE_API_KEY が .env にセットされていません。")
```

`.env`ファイルから環境変数をロードし、OpenAI APIキーが設定されていることを確認します。APIキーが設定されていない場合は、エラーログを出力し、`EnvironmentError`を発生させます。これにより、アプリケーションがAPIキーなしで実行されるのを防ぎます。

#### Flaskアプリケーションの初期化

```python
###############################################################################
# Flask App Initialization
###############################################################################
app = Flask(__name__)
app.secret_key = os.getenv(
    "FLASK_SECRET_KEY",
    "replace_with_a_secure_random_key")
app.config['ENV'] = 'production'
app.config['DEBUG'] = False
app.config['TESTING'] = False

# Session configuration
app.config['SESSION_PERMANENT'] = False
```

Flaskアプリケーションを初期化し、シークレットキー、環境、デバッグモード、テストモードを設定します。シークレットキーはセッションのセキュリティのために使用されます。

#### wwwサブドメインへのリダイレクト

```python
# Redirect to www subdomain
@app.before_request
def redirect_to_www():
    host = request.headers.get("Host", "")
    if "localhost" in host or "127.0.0.1" in host:
        return None
    if not host.startswith("www."):
        target_url = request.url.replace(host, "www." + host, 1)
        return redirect(target_url, code=301)
```

`before_request`デコレータを使用して、すべてのリクエストを処理する前に実行される関数を定義します。この関数は、ホストが`www.`で始まらない場合に、`www.`サブドメインにリダイレクトします。localhostまたは127.0.0.1へのアクセスはリダイレクトされません。

#### 進捗管理

```python
###############################################################################
# 進捗管理（履歴と最新状態）
###############################################################################
progress_history = {}  # 全進捗履歴
progress_status = {}   # 最新の進捗状態

def update_progress(progress_id, message):
    """進捗履歴と最新状態を更新する"""
    global progress_history, progress_status
    if progress_id not in progress_history:
        progress_history[progress_id] = ""
    progress_history[progress_id] += message
    progress_status[progress_id] = message
```

`progress_history`と`progress_status`という2つの辞書を使用して、アプリケーションの進捗を管理します。`progress_history`はすべての進捗メッセージを保存し、`progress_status`は最新の進捗メッセージを保存します。`update_progress`関数は、これらの辞書を更新するために使用されます。

#### バックグラウンド処理の結果保管

```python
###############################################################################
# バックグラウンド処理用結果保管
###############################################################################
result_store = {}
```

バックグラウンド処理の結果を保管するための辞書`result_store`を定義します。これにより、バックグラウンドで実行されるタスクの結果を、Flaskアプリケーションの他の部分からアクセスできるようになります。

#### フォームからの共通パラメータ取得

```python
###############################################################################
# Helper Functions
###############################################################################

def get_common_params_from_form():
    return {
        "github_url": request.form.get("github_url", "").strip(),
        "target_audience": request.form.get("target_audience", "エンジニア全般").strip(),
        "blog_tone": request.form.get("blog_tone", "カジュアルだけど専門性を感じるトーン").strip(),
        "additional_requirements": request.form.get("additional_requirements", "").strip(),
        "language": request.form.get("language", "ja").strip(),
        "model": request.form.get("model", "gemini-2.0-flash").strip()  # 追加
    }
```

フォームから送信された共通のパラメータ（GitHub URL、対象読者、ブログのトーン、追加要件、言語）を取得するための関数`get_common_params_from_form`を定義します。これにより、複数のビュー関数で同じパラメータを取得するコードを繰り返す必要がなくなります。

#### 引数からの共通パラメータ取得

```python
def get_common_params_from_args():
    return {
        "github_url": request.args.get("github_url", ""),
        "target_audience": request.args.get("target_audience", "エンジニア全般"),
        "blog_tone": request.args.get("blog_tone", "カジュアルだけど専門性を感じるトーン"),
        "additional_requirements": request.args.get("additional_requirements", ""),
        "language": request.args.get("language", "ja")
    }
```

クエリパラメータから共通のパラメータ（GitHub URL、対象読者、ブログのトーン、追加要件、言語）を取得するための関数`get_common_params_from_args`を定義します。これにより、複数のビュー関数で同じパラメータを取得するコードを繰り返す必要がなくなります。

#### LLMオブジェクトの取得

```python
def get_llm(selected_model, openai_api_key):
    """
    選択されたモデルに応じて、適切なLLMオブジェクトを返します。
    - selected_model が "gemini-*" の場合は ChatGoogleGenerativeAI を利用
    - それ以外は ChatOpenAI を利用
    """
    if selected_model.startswith("gemini"):
        from langchain_google_genai import ChatGoogleGenerativeAI
        return ChatGoogleGenerativeAI(model=selected_model)
    else:
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(
            model_name=selected_model,
            openai_api_key=openai_api_key)
```

選択されたモデルに応じて、適切なLLMオブジェクトを返す関数`get_llm`を定義します。モデル名が`gemini-*`で始まる場合は`ChatGoogleGenerativeAI`を、それ以外の場合は`ChatOpenAI`を使用します。

#### プロジェクトファイルの読み込み

```python
def read_project_files(root_dir):
    logger.info("Reading project files from: %s", root_dir)
    all_text = []
    MAX_FILE_SIZE = 20 * 1024 * 1024  # 20MB
    MAX_FILE_LENGTH = 20000
    for dirpath, dirnames, filenames in os.walk(root_dir):
        dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRECTORIES]
        for file in filenames:
            file_path = os.path.join(dirpath, file)
            if file.lower().endswith(DISALLOWED_EXTENSIONS):
                logger.info("Skipping disallowed file: %s", file_path)
                continue
            if "__pycache__" in file_path:
                logger.info("Skipping __pycache__ file: %s", file_path)
                continue
            try:
                size = os.path.getsize(file_path)
                if size > MAX_FILE_SIZE:
                    logger.info(
                        "Skipping large file (>20MB): %s (size=%d bytes)",
                        file_path,
                        size)
                    continue
            except Exception as e:
                logger.warning(
                    "Could not determine file size for %s: %s", file_path, e)
                continue
            try:
                with open(file_path, "r", encoding=DEFAULT_ENCODING, errors="ignore") as f:
                    content = f.read()
                if len(content) > MAX_FILE_LENGTH:
                    logger.info(
                        "Skipping file due to excessive length (>20000 chars): %s (length=%d)",
                        file_path,
                        len(content))
                    continue
                relative_path = os.path.relpath(file_path, root_dir)
                header = f"\n\n### File: {relative_path}\n"
                all_text.append(header + content)
            except Exception as e:
                logger.warning("Could not read file %s: %s", file_path, e)
                continue
    combined_text = "\n".join(all_text)
    logger.info(
        "Completed reading project files. Total length: %d characters",
        len(combined_text))
    return combined_text
```

指定されたルートディレクトリからプロジェクトファイルを読み込み、ファイルの内容を結合した文字列を返します。許可されていない拡張子のファイルや、サイズが大きすぎるファイルはスキップされます。

#### ディレクトリツリーの取得

```python
def get_directory_tree(root_dir):
    tree_lines = []
    for dirpath, dirnames, filenames in os.walk(root_dir):
        level = dirpath.replace(root_dir, '').count(os.sep)
        indent = "│   " * level
        tree_lines.append(f"{indent}├── {os.path.basename(dirpath)}/")
        for f in filenames:
            tree_lines.append(f"{indent}│   ├── {f}")
    return "\n".join(tree_lines)
```

指定されたルートディレクトリのディレクトリツリー構造を文字列として取得します。`os.walk`を使用してディレクトリを走査し、各ディレクトリとファイルの相対パスをツリー構造で表現します。

#### Markdownフェンスの除去

```python
def remove_outer_markdown_fence(text: str) -> str:
    """
    テキスト全体が
        ```markdown
         ... (任意のテキスト) ...
        ```
    の形式で丸ごと囲われている場合のみ、
    その外側の "```markdown" と "```" を取り除いて返す。

    - 途中にある他のコードブロックは削除しない
    - 先頭と末尾にあるフェンス記号を取り除くだけ
    """
    # 前後の余白を除去したうえで判定する
    trimmed = text.strip()

    # DOTALLオプションで改行を含めてマッチする
    # ^```markdown\s*(.*?)\s*```$ という正規表現で
    # テキスト全体が1つのフェンスにくるまれているかチェック
    pattern = re.compile(r'^```markdown\s*(.*?)\s*```$', re.DOTALL)

    m = pattern.match(trimmed)
    if m:
        # グループ1に包まれていた中身が入っているので、それを返す
        return m.group(1).strip("\n\r")

    # 全体が包まれていない場合は何も変更しない
    return text
```

テキスト全体がMarkdownのフェンスで囲まれている場合、そのフェンスを取り除く関数`remove_outer_markdown_fence`を定義します。これにより、LLMからの出力を整形する際に、不要なフェンスを除去できます。

#### JSONフェンスの除去

```python
def remove_outer_json_fence(text: str) -> str:
    """
    テキスト全体が
        ```json
         ... (任意のテキスト) ...
        ```
    の形式で丸ごと囲われている場合のみ、
    その外側の "```json" と "```" を取り除いて返す。

    - 途中にある他のコードブロックは削除しない
    - 先頭と末尾にあるフェンス記号を取り除くだけ
    """
    # 前後の余白を除去したうえで判定する
    trimmed = text.strip()

    # DOTALLオプションで改行を含めてマッチする
    # ^```json\s*(.*?)\s*```$ という正規表現で
    # テキスト全体が1つのフェンスにくるまれているかチェック
    pattern = re.compile(r'^```json\s*(.*?)\s*```$', re.DOTALL)

    m = pattern.match(trimmed)
    if m:
        

# 3章: 主要機能の詳細なコード解説

## 3-1: index.html - ユーザーインターフェース

### 3-1-1: HTMLの基本構造とメタデータ

HTMLドキュメントの基本構造とメタデータの設定について解説します。

```html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Tech Blog Generator</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/pygments.css') }}">
    <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6831610624805777"
        crossorigin="anonymous"></script>
</head>
```

このセクションでは、HTMLドキュメントの基本的な構造を定義し、文字コード、タイトル、スタイルシート、広告スクリプトを設定します。これにより、ブラウザはコンテンツを正しく解釈し、適切なスタイルと機能を提供できます。広告スクリプトは収益化のために使用されます。

### 3-1-2: サイドバー：情報入力フォーム

ブログ生成に必要な情報を入力するためのフォームについて解説します。

```html
<body>
    <div class="container">
        <!-- サイドバー（常に表示） -->
        <aside class="sidebar">
            <h2>情報入力</h2>
            <form id="projectForm" method="POST" enctype="multipart/form-data">
                <label>プロジェクトのフォルダ選択:<br>
                    <input type="file" name="project_folder" webkitdirectory directory multiple>
                </label>
                <br><br>
                <label>GithubリポジトリのURL:<br>
                    <input type="url" name="github_url" placeholder="https://github.com/your-repo">
                </label>
                <br><br>
                <label>ターゲット読者層:<br>
                    <input type="text" name="target_audience" value="エンジニア全般">
                </label>
                <br><br>
                <label>ブログのトーン:<br>
                    <input type="text" name="blog_tone" value="カジュアルだけど専門性を感じるトーン">
                </label>
                <br><br>
                <!-- 言語変更未対応のため、コメントアウト -->
                <!--
                <label>言語の選択:<br>
                    <select name="language">
                        <option value="ja" selected>日本語</option>
                        <option value="en">English</option>
                    </select>
                </label>
                <br><br>
                -->
                <label>その他リクエスト:<br>
                    <textarea name="additional_requirements" rows="3"></textarea>
                </label>
                <br><br>
                <label for="model">モデル選択</label>
                <select name="model" id="model" class="form-control">
                    <option value="gemini-2.0-flash">Google: gemini-2.0-flash</option>
                    <option value="gemini-1.5-pro">Google: gemini-1.5-pro</option>                    
                    <option value="gpt-4o">OpenAI: gpt-4o</option>
                    <option value="o3-mini">OpenAI: o3-mini</option>
                </select>
                <br><br>
                <button type="submit">テックブログを生成する</button>
            </form>
        </aside>
```

ブログ生成に必要な情報を入力するためのサイドバーを定義します。フォームは、プロジェクトフォルダの選択、GitHubリポジトリのURL、ターゲット読者層、ブログのトーン、その他リクエスト、モデル選択を提供します。これらの入力に基づいて、ブログの内容がカスタマイズされます。フォームデータはサーバーにPOST送信されます。

### 3-1-3: メインコンテンツ：メッセージ表示と状態に応じた画面表示

ブログ生成の状態に応じた画面表示の制御について解説します。

```html
        <!-- メインコンテンツ -->
        <main class="main-content">
            {% with messages = get_flashed_messages(category_filter=["error", "info", "warning"]) %}
            {% if messages %}
            <ul class="flashes">
                {% for message in messages %}
                <li>{{ message }}</li>
                {% endfor %}
            </ul>
            {% endif %}
            {% endwith %}

            {% if blog_markdown and viewType == "final" %}
            <h1>最終ブログ確認</h1>
            <div class="final-container">
                <!-- 編集パネル -->
                <div class="edit-panel">
                    <!-- アウトライン編集エリア -->
                    <div class="outline-section">
                        <h2>アウトライン (編集可能)</h2>
                        <form id="outlineForm">
                            <textarea name="edited_outline" rows="15" cols="100">{{ blog_outline|e }}</textarea>
                        </form>
                    </div>
                    <!-- 本文編集エリア -->
                    <div class="content-section">
                        <h2>本文 (編集可能)</h2>
                        <form id="blogForm" method="POST">
                            <textarea name="edited_markdown" rows="25" cols="100">{{ blog_markdown|e }}</textarea>
                        </form>
                    </div>
                    <!-- 再生成ボタン群（アウトライン・本文を隣接して配置） -->
                    <div class="regen-buttons">
                        <button type="button" id="regenerateOutlineButton"
                            onclick="submitOutline()">アウトライン・本文を再生成</button>
                        <button type="button" id="regenerateContentButton"
                            onclick="submitBlogGeneration()">本文を再生成</button>
                    </div>
                </div>
                <!-- プレビューセクション -->
                <div class="preview-section">
                    <h2>プレビュー (Markdown → HTML)</h2>
                    <form id="previewForm">
                        <button type="button" onclick="updatePreview()">Previewを更新</button>
                    </form>
                    <div id="preview-container">{{ converted_html|safe }}</div>
                    <br>
                    <a href="{{ url_for('download_markdown') }}">この内容でMarkdownをダウンロード</a>
                </div>
            </div>
            <!-- 「リセット」ボタンの追加 -->
            <br><br>
            <form action="{{ url_for('reset') }}" method="get">
                <button type="submit">すべての情報をリフレッシュして最初の画面に戻る</button>
            </form>
            <script>
                // 最終生成画面ではリロード用フラグをクリア
                sessionStorage.removeItem("reloadTriggered");
            </script>
            {% elif blog_markdown and viewType == "preview" %}
            {% elif blog_outline and viewType == "outline" %}
            <!-- アウトライン確認画面（既存） -->
            <h1>アウトライン確認</h1>
            <form id="outlineForm">
                <textarea name="edited_outline" rows="20" cols="100">{{ blog_outline|e }}</textarea>
                <br><br>
                <button type="button" id="generateButton" onclick="submitOutline()">このアウトラインで最終ブログを生成</button>
            </form>
            <!-- 「リセット」ボタンの追加 -->
            <br><br>
            <form action="{{ url_for('reset') }}" method="get">
                <button type="submit">すべての情報をリフレッシュして最初の画面に戻る</button>
            </form>
            {% elif viewType == "status" %}
            <!-- ブログ生成ステータス画面（既存） -->
            <h1>ブログ生成ステータス</h1>
            <p>現在、最終ブログ生成処理が進行中です。しばらくお待ちください。</p>
            <div id="progress" style="display:none;">進捗情報をここに表示します…</div>
            {% else %}
            <!-- 初期状態 -->
            <h1>Tech Blog Generator</h1>
            <!-- 使用方法の説明を整形して表示 -->
            <div class="box">
                <ul>
                    <li>このツールは、プロジェクトフォルダ/GitHub リポジトリからテックブログを生成するためのツールです。</li>
                    <li>サイドバーから「プロジェクトフォルダを選択」または「GitHub リポジトリの URL」を入力してください。</li>
                    <li>その他、ターゲット読者層、ブログのトーン、その他リクエストを入力することで、ブログの内容をカスタマイズできます。</li>
                    <li>「テックブログを生成する」ボタンをクリックすると、ブログ生成処理が開始されます。</li>
                    <li>ブログのアウトラインが生成されると、アウトライン確認画面が表示されます。</li>
                    <li>アウトライン確認画面で「このアウトラインで最終ブログを生成」ボタンをクリックすると、ブログが生成されます。</li>
                    <li>ブログ生成が完了すると、最終ブログ確認画面が表示されます。</li>
                </ul>
            </div>

            {% endif %}
            <!-- 進捗履歴表示 -->
            {% if progress_log and viewType != "final" %}
            <h3>進捗履歴</h3>
            <pre id="progress_history">{{ progress_log }}</pre>
            {% endif %}
        </main>
```

メインコンテンツ領域を定義します。`get_flashed_messages`を使用して、エラー、情報、警告メッセージを表示します。`blog_markdown`と`viewType`の値に応じて、最終ブログ確認画面、アウトライン確認画面、ブログ生成ステータス画面、または初期状態の画面を表示します。各画面には、対応するフォームやボタンが含まれています。

### 3-1-4: 進捗履歴の表示

ブログ生成の進捗履歴を表示する方法について解説します。

```html
{% if progress_log and viewType != "final" %}
    <h3>進捗履歴</h3>
    <pre id="progress_history">{{ progress_log }}</pre>
{% endif %}
```

`progress_log`が存在し、かつ`viewType`が`final`でない場合に、ブログ生成の進捗履歴を表示します。これにより、ユーザーはブログ生成の過程を追跡できます。

### 3-1-5: JavaScriptによるviewTypeの受け渡しと外部JavaScriptファイルの読み込み

Flaskから渡された変数をJavaScriptで使用し、外部JavaScriptファイルを読み込む方法について解説します。

```html
    </div>
    <!-- viewType を JS 変数に出力 -->
    <script>
        var viewType = "{{ viewType }}";
        console.log("viewType:", viewType);
    </script>
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>

</html>
```

Flaskから渡された`viewType`変数をJavaScriptで使用できるようにします。また、外部JavaScriptファイル（`main.js`）を読み込み、クライアントサイドのロジックを実装します。

## 3-2: Dockerfile - 環境構築

### 3-2-1: ベースイメージの選択とシステム依存関係のインストール

Dockerイメージのベースと必要なパッケージのインストールについて解説します。

```dockerfile
FROM python:3.12-slim

# Install system dependencies required for building packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl build-essential && rm -rf /var/lib/apt/lists/*
```

このセクションでは、Python 3.12のslim版のDockerイメージをベースとして使用し、必要なシステム依存関係をインストールします。`apt-get update`でパッケージリストを更新し、`curl`と`build-essential`をインストールします。`--no-install-recommends`オプションを使用することで、推奨パッケージのインストールを避け、イメージサイズを削減します。最後に、パッケージリストを削除してイメージサイズをさらに削減します。

### 3-2-2: Poetryのインストールと設定

Pythonの依存関係管理ツールPoetryのインストールについて解説します。

```dockerfile
# Install Poetry (Python dependency manager)
RUN curl -sSL https://install.python-poetry.org | python -
ENV PATH="/root/.local/bin:$PATH"
```

このセクションでは、Pythonの依存関係管理ツールであるPoetryをインストールします。Poetryのインストーラをダウンロードして実行し、Poetryの実行可能ファイルがPATH環境変数に追加されるように設定します。これにより、後続のステップでPoetryコマンドを使用できるようになります。

### 3-2-3: ワーキングディレクトリの設定と依存関係のコピー

コンテナ内のワーキングディレクトリの設定と依存関係のコピーについて解説します。

```dockerfile
# Set the working directory
WORKDIR /app

# Copy dependency definition files first for caching purposes
COPY pyproject.toml poetry.lock* /app/
```

このセクションでは、コンテナ内のワーキングディレクトリを`/app`に設定します。次に、`pyproject.toml`と`poetry.lock`ファイルをワーキングディレクトリにコピーします。これらのファイルは、アプリケーションの依存関係を定義するために使用されます。先にこれらのファイルをコピーすることで、アプリケーションのソースコードが変更されない限り、依存関係のインストールステップがキャッシュされるため、ビルド時間を短縮できます。

### 3-2-4: 依存関係のインストール

Poetryを使用した依存関係のインストールについて解説します。

```dockerfile
# Configure Poetry to install production dependencies (excluding dev dependencies) without creating a virtual environment
RUN poetry config virtualenvs.create false && \
    poetry install --no-root --no-interaction --no-ansi
```

このセクションでは、Poetryを使用してアプリケーションの依存関係をインストールします。`poetry config virtualenvs.create false`コマンドは、Poetryに仮想環境を作成しないように指示します。`poetry install --no-root --no-interaction --no-ansi`コマンドは、ルートプロジェクトをインストールせずに、対話モードを無効にし、ANSI出力を無効にして、依存関係をインストールします。

### 3-2-5: ビルド依存関係の削除

ビルドに必要な依存関係の削除によるイメージサイズの削減について解説します。

```dockerfile
# Remove build dependencies to reduce the final image size
RUN apt-get purge -y --auto-remove build-essential && rm -rf /var/lib/apt/lists/*
```

このセクションでは、ビルドに必要な依存関係を削除して、最終的なイメージサイズを削減します。`apt-get purge -y --auto-remove build-essential`コマンドは、`build-essential`パッケージとその依存関係を削除します。最後に、パッケージリストを削除してイメージサイズをさらに削減します。

### 3-2-6: Gitのインストール

Gitのインストールについて解説します。

```dockerfile
# Install Git
RUN apt-get update && apt-get install -y git
```

このセクションでは、Gitをインストールします。これにより、コンテナ内でGitコマンドを使用できるようになります。

### 3-2-7: アプリケーションソースコードのコピー

アプリケーションソースコードのコピーについて解説します。

```dockerfile
# Copy the application source code
COPY . /app
```

このセクションでは、アプリケーションのソースコードをワーキングディレクトリにコピーします。これにより、コンテナ内でアプリケーションを実行できるようになります。

### 3-2-8: 非rootユーザーの作成と権限設定

セキュリティのための非rootユーザーの作成について解説します。

```dockerfile
# Create a non-root user for security and change ownership of the app directory
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser:appuser /app
USER appuser
```

このセクションでは、セキュリティのために、非rootユーザー`appuser`を作成し、アプリケーションディレクトリの所有者を`appuser`に変更します。`adduser --disabled-password --gecos '' appuser`コマンドは、パスワードなしで、追加情報なしに`appuser`を作成します。`chown -R appuser:appuser /app`コマンドは、`/app`ディレクトリとその内容の所有者を`appuser`に変更します。`USER appuser`コマンドは、後続のコマンドを`appuser`として実行するように指定します。

### 3-2-9: ポートの公開

Flaskアプリケーションのポート公開について解説します。

```dockerfile
# Expose the port used by the Flask application (5001 as defined in app.py)
EXPOSE 8080
```

このセクションでは、Flaskアプリケーションが使用するポート8080を公開します。これにより、コンテナ外部からアプリケーションにアクセスできるようになります。

### 3-2-10: Gunicornによるアプリケーションの実行

Gunicornを使用したFlaskアプリケーションの実行について解説します。

```dockerfile
# Use Gunicorn to run the application (assumes the Flask app object is defined in app:app)
CMD ["gunicorn", "-w", "1", "--worker-class", "gthread", "--threads", "4", "--timeout", "3600", "-b", "0.0.0.0:8080", "app:app"]
```

このセクションでは、Gunicornを使用してFlaskアプリケーションを実行します。`CMD`命令は、コンテナが起動したときに実行するコマンドを指定します。`gunicorn -w 1 --worker-class gthread --threads 4 --timeout 3600 -b 0.0.0.0:8080 app:app`コマンドは、Gunicornを起動し、1つのワーカープロセスを使用し、ワーカークラスとして`gthread`を使用し、4つのスレッドを使用し、タイムアウトを3600秒に設定し、すべてのインターフェースのポート8080でリッスンするように指示します。`app:app`は、Flaskアプリケーションオブジェクトが`app.py`ファイル内の`app`変数として定義されていることを示します。

## 3-3: pyproject.toml - 依存関係の管理

### 3-3-1: Poetryプロジェクト設定

Poetryプロジェクトの基本的な設定について解説します。

```toml
[tool.poetry]
name = "auto-blog"
version = "0.1.0"
description = ""
readme = "README.md"
```

このセクションでは、Poetryプロジェクトの基本的な設定を定義しています。プロジェクト名、バージョン、説明、作者、READMEファイルなどを指定します。

### 3-3-2: 依存関係の定義

プロジェクトが依存するPythonパッケージとそのバージョンについて解説します。

```toml
[tool.poetry.dependencies]
python = "^3.12"
langchain = "^0.3.18"
langchain-community = "^0.3.17"
langchain-openai = "^0.3.5"
flask = "^3.1.0"
python-dotenv = "^1.0.1"
werkzeug = "^3.1.3"
markdown = "^3.7"
gunicorn = "^23.0.0"
autopep8 = "^2.3.2"
pre-commit = "^4.1.0"
langchain-google-genai = "^2.0.9"
```

このセクションでは、プロジェクトが依存するPythonパッケージとそのバージョンを指定しています。`python = "^3.12"`はPython 3.12以上を意味します。`^`記号は、互換性のある最新バージョンを許可します。

### 3-3-3: パッケージの指定

プロジェクトに含めるパッケージの指定について解説します。

```toml
[[tool.poetry.packages]]
include = "const"
from = "."  # ← プロジェクトルート（"Auto-blog"）から探す
```

このセクションでは、プロジェクトに含めるパッケージを指定しています。`include = "const"`は、`const`という名前のパッケージをプロジェクトに含めることを意味します。`from = "."`は、プロジェクトのルートディレクトリからパッケージを探すように指示しています。

### 3-3-4: ビルドシステムの設定

プロジェクトのビルドに使用するシステムの設定について解説します。

```toml
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

このセクションでは、プロジェクトのビルドに使用するシステムを指定しています。`requires = ["poetry-core"]`は、ビルドに`poetry-core`が必要であることを意味します。`build-backend = "poetry.core.masonry.api"`は、PoetryのMasonry APIをビルドバックエンドとして使用することを指定しています。

## 3-4: app.py - アプリケーションロジック

### 3-4-1: モジュールのインポート

必要なPythonモジュールのインポートについて解説します。

```python
import os
import re
import logging
import subprocess
import tempfile
import threading
import uuid
import markdown
import json
import time

from dotenv import load_dotenv
from flask import Flask, request, render_template, redirect, url_for, flash, send_file, session, jsonify, Response, stream_with_context
from werkzeug.utils import secure_filename

# LLM用プロンプトのインポート
from const.prompt import (
    file_role_prompt_template,
    code_detail_prompt_template,
    blog_outline_prompt_template,
    final_blog_prompt_template,
    context_blog_prompt_template,
    chapter_generation_prompt_template
)

# Disallowed file extensionsのインポート
from const.const import DISALLOWED_EXTENSIONS, MAX_FILE_SIZE, MAX_FILE_LENGTH, IGNORED_DIRECTORIES, DEFAULT_ENCODING

# LangChain & OpenAI imports
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
```

必要なPythonモジュールをインポートします。標準ライブラリのモジュールに加えて、`.env`ファイルから環境変数をロードするための`dotenv`、Flaskフレームワーク、LangChainとOpenAIのモジュールが含まれます。

### 3-4-2: ロギング設定

アプリケーションのログを設定する方法について解説します。

```python
###############################################################################
# Logging configuration
###############################################################################
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)
```

アプリケーションのログを設定します。ログレベルをINFOに設定し、ログメッセージのフォーマットを指定します。これにより、アプリケーションの実行中に発生するイベントを記録し、デバッグや監視に役立てることができます。

### 3-4-3: 環境変数のロードとAPIキーの確認

.envファイルから環境変数をロードし、APIキーを確認する方法について解説します。

```python
###############################################################################
# Load environment variables & check for OpenAI key
###############################################################################
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
    logger.error("OPENAI_API_KEY is not set. Please set it in the .env file.")
    raise EnvironmentError(
        "OPENAI_API_KEY is not set. Please set it in the .env file.")
logger.info("OPENAI_API_KEY successfully loaded.")

google_api_key = os.getenv("GOOGLE_API_KEY")
if not google_api_key:
    raise EnvironmentError("GOOGLE_API_KEY が .env にセットされていません。")
```

`.env`ファイルから環境変数をロードし、OpenAI APIキーが設定されていることを確認します。APIキーが設定されていない場合は、エラーログを出力し、`EnvironmentError`を発生させます。これにより、アプリケーションがAPIキーなしで実行されるのを防ぎます。

### 3-4-4: Flaskアプリケーションの初期化

Flaskアプリケーションの初期化について解説します。

```python
###############################################################################
# Flask App Initialization
###############################################################################
app = Flask(__name__)
app.secret_key = os.getenv(
    "FLASK_SECRET_KEY",
    "replace_with_a_secure_random_key")
app.config['ENV'] = 'production'
app.config['DEBUG'] = False
app.config['TESTING'] = False

# Session configuration
app.config['SESSION_PERMANENT'] = False
```

Flaskアプリケーションを初期化し、シークレットキー、環境、デバッグモード、テストモードを設定します。シークレットキーはセッションのセキュリティのために使用されます。

### 3-4-5: wwwサブドメインへのリダイレクト

wwwサブドメインへのリダイレクトについて解説します。

```python
# Redirect to www subdomain
@app.before_request
def redirect_to_www():
    host = request.headers.get("Host", "")
    if "localhost" in host or "127.0.0.1" in host:
        return None
    if not host.startswith("www."):
        target_url = request.url.replace(host, "www." + host, 1)
        return redirect(target_url, code=301)
```

`before_request`デコレータを使用して、すべてのリクエストを処理する前に実行される関数を定義します。この関数は、ホストが`www.`で始まらない場合に、`www.`サブドメインにリダイレクトします。localhostまたは127.0.0.1へのアクセスはリダイレクトされません。

### 3-4-6: 進捗管理

進捗管理について解説します。

```python
###############################################################################
# 進捗管理（履歴と最新状態）
###############################################################################
progress_history = {}  # 全進捗履歴
progress_status = {}   # 最新の進捗状態

def update_progress(progress_id, message):
    """進捗履歴と最新状態を更新する"""
    global progress_history, progress_status
    if progress_id not in progress_history:
        progress_history[progress_id] = ""
    progress_history[progress_id] += message
    progress_status[progress_id] = message
```

`progress_history`と`progress_status`という2つの辞書を使用して、アプリケーションの進捗を管理します。`progress_history`はすべての進捗メッセージを保存し、`progress_status`は最新の進捗メッセージを保存します。`update_progress`関数は、これらの辞書を更新するために使用されます。

### 3-4-7: バックグラウンド処理の結果保管

バックグラウンド処理の結果保管について解説します。

```python
###############################################################################
# バックグラウンド処理用結果保管
###############################################################################
result_store = {}
```

バックグラウンド処理の結果を保管するための辞書`result_store`を定義します。これにより、バックグラウンドで実行されるタスクの結果を、Flaskアプリケーションの他の部分からアクセスできるようになります。

### 3-4-8: フォームからの共通パラメータ取得

フォームからの共通パラメータ取得について解説します。

```python
###############################################################################
# Helper Functions
###############################################################################

def get_common_params_from_form():
    return {
        "github_url": request.form.get("github_url", "").strip(),
        "target_audience": request.form.get("target_audience", "エンジニア全般").strip(),
        "blog_tone": request.form.get("blog_tone", "カジュアルだけど専門性を感じるトーン").strip(),
        "additional_requirements": request.form.get("additional_requirements", "").strip(),
        "language": request.form.get("language", "ja").strip(),
        "model": request.form.get("model", "gemini-2.0-flash").strip()  # 追加
    }
```

フォームから送信された共通のパラメータ（GitHub URL、対象読者、ブログのトーン、追加要件、言語）を取得するための関数`get_common_params_from_form`を定義します。これにより、複数のビュー関数で同じパラメータを取得するコードを繰り返す必要がなくなります。

### 3-4-9: 引数からの共通パラメータ取得

引数からの共通パラメータ取得について解説します。

```python
def get_common_params_from_args():
    return {
        "github_url": request.args.get("github_url", ""),
        "target_audience": request.args.get("target_audience", "エンジニア全般"),
        "blog_tone": request.args.get("blog_tone", "カジュアルだけど専門性を感じるトーン"),
        "additional_requirements": request.args.get("additional_requirements", ""),
        "language": request.args.get("language", "ja")
    }
```

クエリパラメータから共通のパラメータ（GitHub URL、対象読者、ブログのトーン、追加要件、言語）を取得するための関数`get_common_params_from_args`を定義します。これにより、複数のビュー関数で同じパラメータを取得するコードを繰り返す必要がなくなります。

### 3-4-10: LLMオブジェクトの取得

LLMオブジェクトの取得について解説します。

```python
def get_llm(selected_model, openai_api_key):
    """
    選択されたモデルに応じて、適切なLLMオブジェクトを返します。
    - selected_model が "gemini-*" の場合は ChatGoogleGenerativeAI を利用
    - それ以外は ChatOpenAI を利用
    """
    if selected_model.startswith("gemini"):
        from langchain_google_genai import ChatGoogleGenerativeAI
        return ChatGoogleGenerativeAI(model=selected_model)
    else:
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(
            model_name=selected_model,
            openai_api_key=openai_api_key)
```

選択されたモデルに応じて、適切なLLMオブジェクトを返す関数`get_llm`を定義します。モデル名が`gemini-*`で始まる場合は`ChatGoogleGenerativeAI`を、それ以外の場合は`ChatOpenAI`を使用します。

### 3-4-11: プロジェクトファイルの読み込み

プロジェクトファイルの読み込みについて解説します。

```python
def read_project_files(root_dir):
    logger.info("Reading project files from: %s", root_dir)
    all_text = []
    MAX_FILE_SIZE = 20 * 1024 * 1024  # 20MB
    MAX_FILE_LENGTH = 20000
    for dirpath, dirnames, filenames in os.walk(root_dir):
        dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRECTORIES]
        for file in filenames:
            file_path = os.path.join(dirpath, file)
            if file.lower().endswith(DISALLOWED_EXTENSIONS):
                logger.info("Skipping disallowed file: %s", file_path)
                continue
            if "__pycache__" in file_path:
                logger.info("Skipping __pycache__ file: %s", file_path)
                continue
            try:
                size = os.path.getsize(file_path)
                if size > MAX_FILE_SIZE:
                    logger.info(
                        "Skipping large file (>20MB): %s (size=%d bytes)",
                        file_path,
                        size)
                    continue
            except Exception as e:
                logger.warning(
                    "Could not determine file size for %s: %s", file_path, e)
                continue
            try:
                with open(file_path, "r", encoding=DEFAULT_ENCODING, errors="ignore") as f:
                    content = f.read()
                if len(content) > MAX_FILE_LENGTH:
                    logger.info(
                        "Skipping file due to excessive length (>20000 chars): %s (length=%d)",
                        file_path,
                        len(content))
                    continue
                relative_path = os.path.relpath(file_path, root_dir)
                header = f"\n\n### File: {relative_path}\n"
                all_text.append(header + content)
            except Exception as e:
                logger.warning("Could not read file %s: %s", file_path, e)
                continue
    combined_text = "\n".join(all_text)
    logger.info(
        "Completed reading project files. Total length: %d characters",
        len(combined_text))
    return combined_text
```

指定されたルートディレクトリからプロジェクトファイルを読み込み、ファイルの内容を結合した文字列を返します。許可されていない拡張子のファイルや、サイズが大きすぎるファイルはスキップされます。

### 3-4-12: ディレクトリツリーの取得

ディレクトリツリーの取得について解説します。

```python
def get_directory_tree(root_dir):
    tree_lines = []
    for dirpath, dirnames, filenames in os.walk(root_dir):
        level = dirpath.replace(root_dir, '').count(os.sep)
        indent = "│   " * level
        tree_lines.append(f"{indent}├── {os.path.basename(dirpath)}/")
        for f in filenames:
            tree_lines.append(f"{indent}│   ├── {f}")
    return "\n".join(tree_lines)
```

指定されたルートディレクトリのディレクトリツリー構造を文字列として取得します。`os.walk`を使用してディレクトリを走査し、各ディレクトリとファイルの相対パスをツリー構造で表現します。

### 3-4-13: Markdownフェンスの除去

Markdownフェンスの除去について解説します。

```python
def remove_outer_markdown_fence(text: str) -> str:
    """
    テキスト全体が
        ```markdown
         ... (任意のテキスト) ...
        ```
    の形式で丸ごと囲われている場合のみ、
    その外側の "```markdown" と "```" を取り除いて返す。

    - 途中にある他のコードブロックは削除しない
    - 先頭と末尾にあるフェンス記号を取り除くだけ
    """
    # 前後の余白を除去したうえで判定する
    trimmed = text.strip()

    # DOTALLオプションで改行を含めてマッチする
    # ^```markdown\s*(.*?)\s*```$ という正規表現で
    # テキスト全体が1つのフェンスにくるまれているかチェック
    pattern = re.compile(r'^```markdown\s*(.*?)\s*```$', re.DOTALL)

    m = pattern.match(trimmed)
    if m:
        # グループ1に包まれていた中身が入っているので、それを返す
        return m.group(1).strip("\n\r")

    # 全体が包まれていない場合は何も変更しない
    return text
```

テキスト全体がMarkdownのフェンスで囲まれている場合、そのフェンスを取り除く関数`remove_outer_markdown_fence`を定義します。これにより、LLMからの出力を整形する際に、不要なフェンスを除去できます。

### 3-4-14: JSONフェンスの除去

JSONフェンスの除去について解説します。

```python
def remove_outer_json_fence(text: str) -> str:
    """
    テキスト全体が
        ```json
         ... (任意のテキスト) ...
        ```
    の形式で丸ごと囲われている場合のみ、
    その外側の "```json" と "```" を取り除いて返す。

    - 途中にある他のコードブロックは削除しない
    - 先頭と末尾にあるフェンス記号を取り除くだけ
    """
    # 前後の余白を除去したうえで判定する
    trimmed = text.strip()

    # DOTALLオプションで改行を含めてマッチする
    # ^```json\s*(.*?)\s*```$ という正規表現で
    # テキスト全体が1つのフェンスにくるまれているかチェック
    pattern = re.compile(r'^```json\s*(.*?)\s
```


# 4章: まとめと今後の展望

## 4-1: まとめ

### 4-1-1: Tech Blog Generatorの利点

このツールを使用する利点を改めて確認しましょう。Tech Blog Generatorは、以下の点でエンジニアの皆様のブログ作成を強力にサポートします。

*   **時間短縮**: ソースコード解析からブログ記事の生成まで自動化することで、執筆にかかる時間を大幅に削減します。
*   **高品質な記事**: LLMを活用することで、技術的な正確性と読みやすさを両立した記事を作成できます。
*   **知識共有の促進**: プロジェクトの構造やコードを分かりやすく解説することで、チーム内での知識共有を促進します。
*   **ブログ作成の敷居を下げる**: 記事の雛形を自動生成することで、技術ブログを始める際のハードルを下げます。

### 4-1-2: 今後の開発計画

Tech Blog Generatorはまだ発展途上のツールであり、今後も様々な機能拡張や改善を予定しています。

*   **対応言語の拡充**: 現在はPythonに特化していますが、他のプログラミング言語（Java, JavaScript, Goなど）への対応を検討しています。
*   **より詳細なコード解析**: 静的解析ツールとの連携により、コードの品質や潜在的なバグに関する情報を記事に含めることを検討しています。
*   **多言語対応**: 英語だけでなく、多言語でのブログ記事生成をサポートします。
*   **デザインの改善**: ユーザーインターフェースをより直感的で使いやすいものに改善します。
*   **アウトプット形式の拡充**: Markdownだけでなく、HTMLやPDFなど、様々な形式での出力に対応します。
*   **LLMの選択肢の拡充**: OpenAIのGPTシリーズだけでなく、GoogleのGeminiシリーズなど、様々なLLMを選択できるようにします。

## 4-2: 貢献のお願い

### 4-2-1: コミュニティへの参加

Tech Blog Generatorは、オープンソースプロジェクトとして開発を進めています。バグ報告や機能提案など、コミュニティへの参加を歓迎します。

*   **バグ報告**: ツールを使用中に問題を発見した場合は、GitHubのIssue Trackerにご報告ください。
*   **機能提案**: 新しい機能や改善案がありましたら、GitHubのIssue Trackerにご提案ください。
*   **Pull Request**: 積極的にコードを書いて貢献したい方は、Pull Requestをお待ちしています。

### 4-2-2: お問い合わせ

Tech Blog Generatorに関する質問や意見がありましたら、以下の連絡先までお気軽にお問い合わせください。

*   **GitHub**: <https://github.com/nibura2002/tech-blog-generator>

皆様からのフィードバックを参考に、より使いやすいツールを目指して開発を進めていきます。