自動でテックブログ生成ツールのブログを自動テックブログ生成ツールに書かせてみた
はじめまして
仕事や趣味で簡単なコードを書くことがあり、テックブログを書いてみたいという気持ちはありましたが、「紹介するほどのものでもない」、「コードの説明を日本語で書くのは面倒」などと思って敬遠してました。
そこで、近年目覚ましい進化を遂げている生成AIに自動でテックブログを書いてもらえるツールを作って、LLMにテックブログを書いてみてもらおうと思い立ったち作ってみました。
ここから下は自動でテックブログを生成するツールに書かせた、自身のリポジトリについてのテックブログです。うまく正しい内容が書かれているでしょうか?
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の基本構造とメタデータ
<!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ドキュメントの基本的な構造を定義し、文字コード、タイトル、スタイルシート、広告スクリプトを設定します。これにより、ブラウザはコンテンツを正しく解釈し、適切なスタイルと機能を提供できます。広告スクリプトは収益化のために使用されます。
サイドバー:情報入力フォーム
<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送信されます。
メインコンテンツ:メッセージ表示と状態に応じた画面表示
<!-- メインコンテンツ -->
<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
の値に応じて、最終ブログ確認画面、アウトライン確認画面、ブログ生成ステータス画面、または初期状態の画面を表示します。各画面には、対応するフォームやボタンが含まれています。
進捗履歴の表示
{% if progress_log and viewType != "final" %}
<h3>進捗履歴</h3>
<pre id="progress_history">{{ progress_log }}</pre>
{% endif %}
progress_log
が存在し、かつviewType
がfinal
でない場合に、ブログ生成の進捗履歴を表示します。これにより、ユーザーはブログ生成の過程を追跡できます。
JavaScriptによるviewTypeの受け渡しと外部JavaScriptファイルの読み込み
</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
の詳しいコード解説は以下の通りです。
ベースイメージの選択とシステム依存関係のインストール
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のインストールと設定
# 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コマンドを使用できるようになります。
ワーキングディレクトリの設定と依存関係のコピー
# 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
ファイルをワーキングディレクトリにコピーします。これらのファイルは、アプリケーションの依存関係を定義するために使用されます。先にこれらのファイルをコピーすることで、アプリケーションのソースコードが変更されない限り、依存関係のインストールステップがキャッシュされるため、ビルド時間を短縮できます。
依存関係のインストール
# 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出力を無効にして、依存関係をインストールします。
ビルド依存関係の削除
# 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のインストール
# Install Git
RUN apt-get update && apt-get install -y git
このセクションでは、Gitをインストールします。これにより、コンテナ内でGitコマンドを使用できるようになります。
アプリケーションソースコードのコピー
# Copy the application source code
COPY . /app
このセクションでは、アプリケーションのソースコードをワーキングディレクトリにコピーします。これにより、コンテナ内でアプリケーションを実行できるようになります。
非rootユーザーの作成と権限設定
# 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
として実行するように指定します。
ポートの公開
# Expose the port used by the Flask application (5001 as defined in app.py)
EXPOSE 8080
このセクションでは、Flaskアプリケーションが使用するポート8080を公開します。これにより、コンテナ外部からアプリケーションにアクセスできるようになります。
Gunicornによるアプリケーションの実行
# 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プロジェクト設定
[tool.poetry]
name = "auto-blog"
version = "0.1.0"
description = ""
readme = "README.md"
このセクションでは、Poetryプロジェクトの基本的な設定を定義しています。プロジェクト名、バージョン、説明、作者、READMEファイルなどを指定します。
依存関係の定義
[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以上を意味します。^
記号は、互換性のある最新バージョンを許可します。
パッケージの指定
[[tool.poetry.packages]]
include = "const"
from = "." # ← プロジェクトルート("Auto-blog")から探す
このセクションでは、プロジェクトに含めるパッケージを指定しています。include = "const"
は、const
という名前のパッケージをプロジェクトに含めることを意味します。from = "."
は、プロジェクトのルートディレクトリからパッケージを探すように指示しています。
ビルドシステムの設定
[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
の詳しいコード解説は以下の通りです。
モジュールのインポート
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のモジュールが含まれます。
ロギング設定
###############################################################################
# Logging configuration
###############################################################################
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)
アプリケーションのログを設定します。ログレベルをINFOに設定し、ログメッセージのフォーマットを指定します。これにより、アプリケーションの実行中に発生するイベントを記録し、デバッグや監視に役立てることができます。
環境変数のロードとAPIキーの確認
###############################################################################
# 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アプリケーションの初期化
###############################################################################
# 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サブドメインへのリダイレクト
# 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へのアクセスはリダイレクトされません。
進捗管理
###############################################################################
# 進捗管理(履歴と最新状態)
###############################################################################
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
関数は、これらの辞書を更新するために使用されます。
バックグラウンド処理の結果保管
###############################################################################
# バックグラウンド処理用結果保管
###############################################################################
result_store = {}
バックグラウンド処理の結果を保管するための辞書result_store
を定義します。これにより、バックグラウンドで実行されるタスクの結果を、Flaskアプリケーションの他の部分からアクセスできるようになります。
フォームからの共通パラメータ取得
###############################################################################
# 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
を定義します。これにより、複数のビュー関数で同じパラメータを取得するコードを繰り返す必要がなくなります。
引数からの共通パラメータ取得
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オブジェクトの取得
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
を使用します。
プロジェクトファイルの読み込み
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
指定されたルートディレクトリからプロジェクトファイルを読み込み、ファイルの内容を結合した文字列を返します。許可されていない拡張子のファイルや、サイズが大きすぎるファイルはスキップされます。
ディレクトリツリーの取得
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フェンスの除去
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フェンスの除去
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
Tech Blog Generator
このセクションでは、HTMLドキュメントの基本的な構造を定義し、文字コード、タイトル、スタイルシート、広告スクリプトを設定します。これにより、ブラウザはコンテンツを正しく解釈し、適切なスタイルと機能を提供できます。広告スクリプトは収益化のために使用されます。
### 3-1-2: サイドバー:情報入力フォーム
ブログ生成に必要な情報を入力するためのフォームについて解説します。
html
情報入力
プロジェクトのフォルダ選択:
GithubリポジトリのURL:
ターゲット読者層:
ブログのトーン:
その他リクエスト:
モデル選択 Google: gemini-2.0-flash Google: gemini-1.5-pro OpenAI: gpt-4o OpenAI: o3-mini
テックブログを生成する
ブログ生成に必要な情報を入力するためのサイドバーを定義します。フォームは、プロジェクトフォルダの選択、GitHubリポジトリのURL、ターゲット読者層、ブログのトーン、その他リクエスト、モデル選択を提供します。これらの入力に基づいて、ブログの内容がカスタマイズされます。フォームデータはサーバーにPOST送信されます。
### 3-1-3: メインコンテンツ:メッセージ表示と状態に応じた画面表示
ブログ生成の状態に応じた画面表示の制御について解説します。
html
{% with messages = get_flashed_messages(category_filter=["error", "info", "warning"]) %}
{% if messages %}
- {{ message }}
{% 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" %}
進捗履歴
{{ progress_log }}
{% endif %}
`progress_log`が存在し、かつ`viewType`が`final`でない場合に、ブログ生成の進捗履歴を表示します。これにより、ユーザーはブログ生成の過程を追跡できます。
### 3-1-5: JavaScriptによるviewTypeの受け渡しと外部JavaScriptファイルの読み込み
Flaskから渡された変数をJavaScriptで使用し、外部JavaScriptファイルを読み込む方法について解説します。
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に関する質問や意見がありましたら、以下の連絡先までお気軽にお問い合わせください。
皆様からのフィードバックを参考に、より使いやすいツールを目指して開発を進めていきます。