RSS収集・日本語要約・HTMLメール送信をローカルLLMで実現
はじめに ─ GPT-5でもできなかった「メール配信」
GPT-5が登場したとき、真っ先に試したのがエージェント機能でした。
スケジュール管理からファイル操作まで、やらせればやるほど器用にこなしてくれる。
しかし──どれだけ頑張ってもメールを送る機能だけは実装されていない。
「ならば、自分で作ってしまえばいい」
そう思い、ローカル推論に特化した新星gpt-ossでやることに。
LM Studioに載せればOpenAI互換APIとして動かせる。
これなら、外部サーバーに頼らず、手元のマシンだけでAI朝刊編集長を走らせることができる。
こうして生まれたのが、
RSSでニュースを収集 → AIで要約 → Gmailに送信
という、自分専用の朝刊メール配信システムです。
本記事では、その構成と実装方法を、
gpt-oss × LM Studio × Python という今まさに旬の組み合わせで解説していきます。
システム概要 ─ 自分専用AI朝刊の仕組み
今回の仕組みは、いたって単純です。
やっていることは大きく分けて4つのステップだけ。
- RSS収集
あらかじめ登録したTech系ニュースサイト(ITmedia、TechCrunch、Engadget、Gigazineなど)のRSSをまとめて取得。
新着記事だけをピックアップします。 - AI要約
英語記事は日本語に翻訳したうえで、3行要約+論点3つに整理。
ローカルで動くgpt-oss(LM Studio経由)が、出典URLつきで素早くまとめます。 - Markdown保存 → HTMLメール化
要約結果はMarkdownファイルとして保存。
それをHTMLカードデザインに変換し、メールで読みやすく整形します。 - メール送信(自分宛)
SMTP(ロリポップのサーバー)を叩き、Gmailに配信。
朝起きたら受信トレイに自分専用の「AI朝刊」が届く、というわけです。
シンプルですが、これだけで「AIが毎日ヘッドラインを選び、短くまとめて送ってくれる」
パーソナル編集長体験が手に入ります。
動作デモ ─ 実際の朝刊はこう届く
LM Studioでモデルロード完了の画面

まずはLM Studioでgpt-oss-20bをロード。OpenAI互換APIモードを有効化すれば、Pythonコードから直接叩けます。他のモデルを選択しても、きっと動いてくれるでしょう。
コンソール実行の様子

スクリプトを叩くと、RSS収集→AI要約→Markdown保存まで一気に処理。数分後には結果がファイルに保存されます。
Gmailで受信したHTML朝刊

保存したMarkdownはHTMLに変換して、そのままGmailに送信。出典リンクをクリックすれば元記事へアクセスできます。
環境構築 ─ LM Studio+Pythonで動かす
この朝刊システムは、外部サーバーや有料APIを使わず、すべて手元のPCで完結します。
必要なのは、LM StudioとPython環境だけです。ただし、、相応のGPUが必要です。GTX1060などVRAMの少ないモデルでやりたいなら、Gemmna-3n-4b などの軽量モデルを選択するのがいいでしょう。gpt-oss-20bを動かすには
1. LM Studioの準備
- LM Studio公式サイト からインストーラをダウンロードし、インストール。
- 起動後、モデルブラウザから
openai/gpt-oss-20bを検索してダウンロード。 - モデルをロードし、OpenAI互換APIモードを有効化(
http://localhost:1234/v1が表示されればOK)。 - この状態を保ったまま、後述のPythonスクリプトを実行します。
2. Pythonの準備
- Python 3.11系をインストール(Python公式サイト またはWindows Store経由)。
- ターミナル(またはコマンドプロンプト)で作業用フォルダを作成し、スクリプトを配置。
- 以下のコマンドで必要なパッケージをインストール:
pip install -r requirements.txt
requirements.txt の内容(記事用簡略版):
feedparser
requests
markdown
python-dotenv
3. .env ファイルの設定
SMTPサーバーやメールアドレスなどの情報は、スクリプト本体に直書きせず .env に記載します。
公開用サンプルは以下の通り(メール送信は最初はOFF推奨):
iniコピーする編集する# LM Studio
LMSTUDIO_BASE_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=openai/gpt-oss-20b
OPENAI_API_KEY=lm-studio
# 収集パラメータ
MAX_PER_FEED=3
TIME_WINDOW_HOURS=72
SIMILARITY_TH=0.82
# メール設定(まずはOFF)
EMAIL_ENABLED=false
SMTP_HOST=smtp.example.com
SMTP_PORT=465
SMTP_USER=user@example.com
SMTP_PASS=password
MAIL_FROM=user@example.com
MAIL_TO=user@example.com
MAIL_SUBJECT=自分専用AI朝刊
4. 動作確認
.env の設定後、まずはメール送信をOFFにして実行:
python morning_brief_public.py
brief_out/ フォルダに Markdown ファイルが生成されれば成功です。
内容を確認してから、EMAIL_ENABLED=true にして本番運用に移ります。
コード全文(公開用ダミー版)
# morning_brief_public.py
# 自分専用AI朝刊 - 公開用ダミー版
# 必要環境: Python 3.11系 + LM Studio (OpenAI互換APIモード)
# ライセンス: MIT (ただしRSS配信元の著作権は各社に帰属)
import os, re, json, time, datetime as dt
from pathlib import Path
from difflib import SequenceMatcher
from html import escape
import feedparser
import requests
import markdown
import smtplib, ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from dotenv import load_dotenv
# ==== 設定読み込み ====
load_dotenv()
# RSSフィード一覧(好みで追加可能)
FEEDS = [
"https://rss.itmedia.co.jp/rss/2.0/news_bursts.xml",
"https://jp.techcrunch.com/feed/",
"https://japanese.engadget.com/rss.xml",
"https://gigazine.net/news/rss_2.0/",
"https://www.theverge.com/rss/index.xml",
"https://feeds.arstechnica.com/arstechnica/technology-lab",
"https://feeds.reuters.com/reuters/technologyNews",
"http://feeds.bbci.co.uk/news/technology/rss.xml",
]
MAX_PER_FEED = int(os.getenv("MAX_PER_FEED", "3"))
TIME_WINDOW_HOURS = int(os.getenv("TIME_WINDOW_HOURS", "72"))
SIMILARITY_TH = float(os.getenv("SIMILARITY_TH", "0.82"))
ENDPOINT = os.getenv("LMSTUDIO_BASE_URL", "http://localhost:1234/v1")
MODEL = os.getenv("LMSTUDIO_MODEL", "openai/gpt-oss-20b")
API_KEY = os.getenv("OPENAI_API_KEY", "lm-studio")
OUT_DIR = Path("./brief_out"); OUT_DIR.mkdir(exist_ok=True)
EMAIL_ENABLED = os.getenv("EMAIL_ENABLED", "false").lower() == "true"
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.example.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_USER = os.getenv("SMTP_USER", "user@example.com")
SMTP_PASS = os.getenv("SMTP_PASS", "password")
MAIL_FROM = os.getenv("MAIL_FROM", "user@example.com")
MAIL_TO = [s.strip() for s in os.getenv("MAIL_TO", "user@example.com").split(",")]
MAIL_SUBJECT = os.getenv("MAIL_SUBJECT", "自分専用AI朝刊")
# ==== 要約プロンプト ====
SYS_PROMPT = (
"あなたはニュース編集者です。読者が一目で理解できる、正確で中立的な要約を作ります。"
"英語の記事は自然な日本語に翻訳してから要約してください。"
"出典URLは必ず記載してください。"
)
USER_TEMPLATE = """次の記事を要約してください。
タイトル: {title}
概要/本文: {body}
URL: {url}
出力フォーマット(厳守):
要約:
<1行目(80字以内)>
<2行目(80字以内)>
<3行目(80字以内)>
論点:
- <論点1>
- <論点2>
- <論点3>
出典:
{url}
"""
# ==== 共通関数 ====
def now_jst():
return dt.datetime.now(dt.timezone(dt.timedelta(hours=9)))
def is_recent(entry, hours=TIME_WINDOW_HOURS):
t = None
for key in ("published_parsed", "updated_parsed"):
val = getattr(entry, key, None) or entry.get(key)
if val:
t = val; break
if not t: return True
ts = dt.datetime.fromtimestamp(time.mktime(t), tz=dt.timezone.utc).astimezone(
dt.timezone(dt.timedelta(hours=9))
)
return (now_jst() - ts) <= dt.timedelta(hours=hours)
def normalize_title(s: str) -> str:
s = s.lower()
s = re.sub(r"[\s\u3000]+", " ", s)
return s.strip()
def is_similar(a: str, b: str, th: float = SIMILARITY_TH) -> bool:
return SequenceMatcher(None, normalize_title(a), normalize_title(b)).ratio() >= th
def pick_text(entry):
title = entry.get("title", "").strip()
body = (entry.get("summary") or entry.get("description") or
(entry.get("content") or [{}])[0].get("value", "") or "")
body = re.sub(r"<[^>]+>", " ", body)
return title, re.sub(r"\s+", " ", body).strip()
def chat_complete(messages, temperature=0.2, timeout=90):
url = f"{ENDPOINT}/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"}
payload = {"model": MODEL, "messages": messages, "temperature": temperature, "stream": False}
resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=timeout)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"].strip()
def summarize_item(title, body, url):
messages = [
{"role":"system","content":SYS_PROMPT},
{"role":"user","content":USER_TEMPLATE.format(title=title, body=body[:5000], url=url)}
]
try:
return chat_complete(messages)
except:
return f"要約:\n{title}\n論点:\n- 要約失敗\n出典:\n{url}"
def collect_items():
items, seen = [], []
for feed in FEEDS:
d = feedparser.parse(feed)
count = 0
for entry in d.entries:
if count >= MAX_PER_FEED: break
if not is_recent(entry): continue
title, body = pick_text(entry)
link = entry.get("link", "")
if not title or not link: continue
if any(is_similar(title, s["title"]) for s in seen): continue
items.append({"title": title, "body": body, "url": link})
seen.append({"title": title})
count += 1
return items
def md_to_html(md_text: str, subject: str) -> str:
html = markdown.markdown(md_text, extensions=["nl2br", "sane_lists"])
html = re.sub(r'(?P<u>https?://[^\s<]+)', r'<a href="\g<u>">\g<u></a>', html)
html = '<div class="card">' + html.replace("\n", "<br>") + "</div>"
return f"""<!doctype html><html lang="ja"><head>
<meta charset="UTF-8"><title>{escape(subject)}</title></head><body>{html}</body></html>"""
def send_mail(subject: str, text_body: str, html_body: str):
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = MAIL_FROM
msg["To"] = ", ".join(MAIL_TO)
msg.attach(MIMEText(text_body, "plain", "utf-8"))
msg.attach(MIMEText(html_body, "html", "utf-8"))
ctx = ssl.create_default_context()
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx) as server:
server.login(SMTP_USER, SMTP_PASS)
server.sendmail(MAIL_FROM, MAIL_TO, msg.as_string())
# ==== メイン処理 ====
def main():
items = collect_items()
if not items:
print("新着なし。")
return
today = now_jst().strftime("%Y-%m-%d")
md_lines = [f"# 自分専用AI朝刊({today})", ""]
for i, it in enumerate(items, 1):
md_lines += [f"## {i}. {it['title']}", summarize_item(it['title'], it['body'], it['url']), ""]
md_text = "\n".join(md_lines)
outfile = OUT_DIR / f"morning-brief-{now_jst().strftime('%Y%m%d')}.md"
outfile.write_text(md_text, encoding="utf-8")
print(f"Saved: {outfile.resolve()}")
if EMAIL_ENABLED:
html = md_to_html(md_text, f"{MAIL_SUBJECT}({today})")
send_mail(f"{MAIL_SUBJECT}({today})", md_text, html)
print("Mail sent.")
if __name__ == "__main__":
main()
このコードを .py ファイルとして保存し、前章の .env と合わせて使えば、そのまま動きます。
次の章では、このコードを実行してメール配信まで確認する手順に入ります。
実行してみる ─ 初回テストから配信確認まで
コードと .env の準備が整ったら、いよいよ初回テストです。
いきなりメール送信を有効化するとトラブル時に原因が分かりづらいので、まずはローカル保存のみで動かしてみます。
1. メール送信OFFでのテスト実行
ターミナルで以下を実行します。
python morning_brief_public.py
成功すれば、コンソールにこんな感じで表示されます。
Saved: C:\path\to\project\brief_out\morning-brief-20250809.md
brief_out フォルダを開くと、日付入りのMarkdownファイルができています。
中身は、AIが要約した記事と論点、出典リンクが並んだ朝刊です。
2. メール送信をONにする
内容に問題がなければ、.env の設定を変更します。
EMAIL_ENABLED=true
そのうえで再実行します。
python morning_brief_public.py
コンソールに Mail sent. と出れば送信成功です。
3. Gmailでの受信確認
受信トレイに、件名が日付入りの「自分専用AI朝刊」が届きます。
HTMLカードデザインで整形された要約が並び、リンクをクリックすると元記事へアクセスできます。
あとは、cronにでもタスクスケジューラにでも、お好きな時間に実行設定してください。
4. よくあるエラーと対策
- SMTP接続不可
→.envのSMTP_HOSTとSMTP_PORTが正しいか確認。SSL/465固定推奨。 - 認証エラー
→ ユーザー名とパスワード(またはアプリパスワード)が一致しているか確認。
※GmailやYahooなど一部サービスは「アプリパスワード」発行が必要な場合あり(2段階認証対策) - 文字化け
→ メール送信部分でutf-8が指定されているか確認(本コードは既に対応済み)。
他社情報の取り扱いと注意点
この「自分専用AI朝刊」は、あくまで自分宛にメールで受け取ることを前提に設計しています。
RSSフィードに含まれる記事は、配信元メディアが制作した著作物であり、著作権法によって保護されています。
安全かつ円滑に使うために、次の点に留意してください。
- 全文転載はしない
→ 記事本文をそのまま公開したり、ブログやSNSにコピペするのはNGです。 - 要約+出典リンクにとどめる
→ 本コードは自動的に出典URLを明記します。要約文だけにしておけば安全性が高まります。 - 翻訳も引用ルール内で
→ 英語記事の翻訳も、原文の意味を変えないよう配慮しましょう。 - 用途は自分用に限定
→ この仕組みは自分の情報収集効率化が目的であり、商用配信や第三者への提供は想定していません。
要するに──
「朝刊を作って読むのは自分だけ」という運用なら、著作権や契約上のリスクは最小限に抑えられます。
ニュースの出典は必ず明記し、配信元への敬意を忘れないことが、長く安心して使うための秘訣です。
まとめと今後の展望
今回作った「自分専用AI朝刊」は、
- RSSからニュースを収集
- ローカルLLM(gpt-oss)で要約・翻訳
- Markdown保存→HTMLメール化
- Gmail送信
という一連の流れを、すべて自分のPCだけで完結させるシステムです。
GPT-5のエージェント機能では実現できなかった「メール配信」を、
LM Studioとgpt-ossの組み合わせで力技的に突破しました。
パラメータ調整で自分好みに
スクリプト内や .env で、以下の値を変えるだけで挙動をカスタマイズできます。
FEEDS
取得するRSSフィードのリスト。自分の好きなニュースサイトやブログのRSSに差し替え可能。
RSSのURLはサイト内の「購読」や「RSS」アイコンから探せます。MAX_PER_FEED
1つのフィードから取得する記事数の上限。
例:3 → 新着3本だけ拾う。多くすると情報量は増えますが、要約処理に時間がかかります。TIME_WINDOW_HOURS
何時間以内の記事を対象にするか。
例:72 → 過去3日以内の記事だけ収集。短くすればより新鮮な朝刊になります。SIMILARITY_TH
タイトルの類似度判定しきい値。値を上げると似たニュースをまとめて除外できます。
今後の展望
次のアップデート候補として、
- RSSフィードをコマンドやWebUIから追加登録できる「フィード自動追加」機能
- 朝刊メールのレイアウトカスタマイズ(画像やカテゴリ別表示)
などを構想中です。
まずは、このシンプルな形で動かし、自分のニュースルーチンに組み込んでみてください。
毎朝トレイに届く「自分専用の編集長からの手紙」は、想像以上に快適です。

