Show HN: Easy Webpage Summarizer – Quickly Summarize Webpages and YouTube Videos

https://github.com/cobanov/easy-web-summarizer

websum
Summarise a web page or a YouTube video, with whichever model you want behind it.

PyPI · changelog · contributing

pypi python tests ci licence


Summarising a page is a few lines of LangChain right up until you want the same call to also take a YouTube link, chunk a transcript that will not fit in a context window, run against a local Ollama on a laptop with no network, and then move to OpenAI without any of the calling code changing. That is the part this packages.

One object, Summarizer, takes a URL and works out whether it is a page or a video. The model behind it is a constructor argument, so swapping Ollama for OpenAI, or for something you wrote yourself, is a single line.

from websum import Summarizer, OllamaBackend
s = Summarizer(backend=OllamaBackend(model="llama3:instruct"))
print(s.summarize("https://www.youtube.com/watch?v=4pOpQwiUVXc"))
  • Pages and YouTube through the same call. summarize(url) detects which it has. summarize_web and summarize_youtube are there when you already know.
  • The backend is a Protocol, not a base class to inherit. Anything with a .build() returning a LangChain chat model satisfies it.
  • Local by default. Ollama is a first-class backend, so nothing has to leave the machine, and no key is required to try it.
  • Long inputs are chunked, so a two-hour transcript does not have to fit anywhere in one piece.
  • A CLI, a library and a Gradio UI over the same code, and a py.typed marker so the API type-checks downstream.

Install

pip install 'websum[ollama]'     # library + CLI, local models
pip install 'websum[openai]'     # OpenAI backend
pip install 'websum[ui,ollama]'  # with the Gradio web UI
pip install 'websum[all]'        # everything

Using uv:

Python 3.10 to 3.13. The base install carries no model client at all, which is why the backend is an extra: installing websum alone should not drag in an SDK you are not going to call.

Use

As a library

from websum import Summarizer, OllamaBackend
s = Summarizer(backend=OllamaBackend(model="llama3:instruct"))
print(s.summarize("https://cobanov.dev/haftalik-bulten/hafta-13"))
print(s.summarize("https://www.youtube.com/watch?v=4pOpQwiUVXc"))
print(s.translate("Hello world", target_language="Turkish"))

Swap the backend without touching anything else:

from websum import Summarizer, OpenAIBackend
s = Summarizer(backend=OpenAIBackend(model="gpt-4o-mini"))

From the shell

websum summarize https://example.com
websum summarize https://example.com --backend openai --model gpt-4o-mini
websum translate "Hello world" --target-language Turkish
websum ui --port 7860

websum --help has the full reference.

In a browser

The Gradio UI with a URL box and the summary below it

The API

Object Purpose
Summarizer The high-level API. summarize(url), summarize_web(url), summarize_youtube(url), translate(text)
SummarizerConfig Chunking and language settings
OllamaBackend, OpenAIBackend The built-in backends. Frozen dataclasses with .build()
LLMBackend (Protocol) Implement this to plug in anything else
BackendRegistry Maps string names to backend classes, which is how the CLI resolves --backend

Every public name is re-exported from the top-level websum package and listed in __all__.

Writing a backend

LLMBackend is a Protocol, so there is nothing to subclass and nothing to register. A class with a build() method already satisfies it:

from dataclasses import dataclass
from websum import LLMBackend, Summarizer
@dataclass
class MyBackend:
    def build(self):
        from langchain_anthropic import ChatAnthropic
        return ChatAnthropic(model="claude-sonnet-5")
assert isinstance(MyBackend(), LLMBackend)  # runtime Protocol check
s = Summarizer(backend=MyBackend())

The import lives inside build() on purpose: a backend that is never constructed should not cost an import of an SDK that may not be installed.

Docker

docker build -t websum .
docker run -p 7860:7860 websum
# when ollama is running on the host
docker run --network host -p 7860:7860 websum

The image starts websum ui by default.

Development

git clone https://github.com/cobanov/websum
cd websum
uv sync --all-extras
uv run pre-commit install
uv run pytest
uv run ruff check .
uv run mypy src/websum

CI runs the 19 tests on 3.10, 3.11, 3.12 and 3.13, with ruff, ruff format --check and mypy on top. pytest-randomly shuffles the order every run, so a test that only passes because another one ran first fails here rather than later. CONTRIBUTING.md has the full guide.

Upgrading from 0.1.x

The 0.1.x scripts under app/ are gone. Everything moved into the websum package behind a typed, importable API.

Before After
python app/summarizer.py -u URL websum summarize URL
python app/webui.py websum ui
from summarizer import setup_summarization_chain from websum import Summarizer
Hardcoded ChatOllama OllamaBackend, OpenAIBackend, or your own LLMBackend
pip install -r requirements.txt pip install 'websum[ollama]'

Licence

MIT. See LICENSE.

{
"by": "cobanov",
"descendants": 0,
"id": 40226459,
"score": 11,
"text": "I&#x27;m excited to share a project. It&#x27;s a Python script that utilizes the LangChain framework and the ChatOllama model to generate concise summaries from webpages and YouTube videos. For those preferring a graphical interface, it includes a Gradio app that runs in the browser to use the summarizer interactively. Easily containerize and deploy the summarizer with Docker.<p>The tool is perfect for anyone needing quick insights without reading through the entire content&#x2F; It&#x27;s open for contributions, so if you&#x27;re interested in improving or extending its functionalities, feel free to dive in!",
"time": 1714584709,
"title": "Show HN: Easy Webpage Summarizer – Quickly Summarize Webpages and YouTube Videos",
"type": "story",
"url": "https://github.com/cobanov/easy-web-summarizer"
}
{
"author": "cobanov",
"date": null,
"description": "Summarize web pages and YouTube videos with pluggable LLM backends (Ollama, OpenAI). CLI, library, and Gradio UI. - cobanov/websum",
"image": "https://opengraph.githubassets.com/9e28227b33ab4f0819be863989f5254434e4e947c33fc76d82b2510e12f96bb6/cobanov/websum",
"logo": null,
"publisher": "GitHub",
"title": "GitHub - cobanov/websum: Summarize web pages and YouTube videos with pluggable LLM backends (Ollama, OpenAI). CLI, library, and Gradio UI.",
"url": "https://github.com/cobanov/websum"
}
{
"url": "https://github.com/cobanov/easy-web-summarizer",
"title": "GitHub - cobanov/websum: Summarize web pages and YouTube videos with pluggable LLM backends (Ollama, OpenAI). CLI, library, and Gradio UI.",
"description": "websum Summarise a web page or a YouTube video, with whichever model you want behind it. PyPI · changelog · contributing Summarising a page is a few lines of LangChain right up...",
"links": [
"https://github.com/cobanov/websum",
"https://github.com/cobanov/easy-web-summarizer"
],
"image": "https://opengraph.githubassets.com/9e28227b33ab4f0819be863989f5254434e4e947c33fc76d82b2510e12f96bb6/cobanov/websum",
"content": "<div><article><p>\n <strong>websum</strong><br />\n Summarise a web page or a YouTube video, with whichever model you want behind it.\n</p>\n<p>\n <a target=\"_blank\" href=\"https://pypi.org/project/websum/\">PyPI</a> ·\n <a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/CHANGELOG.md\">changelog</a> ·\n <a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/CONTRIBUTING.md\">contributing</a>\n</p>\n<p>\n <a target=\"_blank\" href=\"https://pypi.org/project/websum/\"><img alt=\"pypi\" src=\"https://camo.githubusercontent.com/e8f5c2cae666546d4f6e39e04ae4b04ae29aa2e14a355915639d3a099167d557/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f77656273756d3f636f6c6f723d356238646566266c6162656c436f6c6f723d316131613161\" /></a>\n <a target=\"_blank\" href=\"https://pypi.org/project/websum/\"><img alt=\"python\" src=\"https://camo.githubusercontent.com/8309e0ea5d2dcbd8cd2604a3d2a57fb938c4d2bec66e3d0fe0b5928d1e5892c3/68747470733a2f2f696d672e736869656c64732e696f2f707970692f707976657273696f6e732f77656273756d3f636f6c6f723d356238646566266c6162656c436f6c6f723d316131613161\" /></a>\n <a target=\"_blank\" href=\"https://camo.githubusercontent.com/52df65ca5cf31e525e7ecd693b9595bbd5a1da613a4dbb55db37e983bfbbacc2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d31392d3562386465663f6c6162656c436f6c6f723d316131613161\"><img alt=\"tests\" src=\"https://camo.githubusercontent.com/52df65ca5cf31e525e7ecd693b9595bbd5a1da613a4dbb55db37e983bfbbacc2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d31392d3562386465663f6c6162656c436f6c6f723d316131613161\" /></a>\n <a target=\"_blank\" href=\"https://github.com/cobanov/websum/actions/workflows/ci.yml\"><img alt=\"ci\" src=\"https://github.com/cobanov/websum/actions/workflows/ci.yml/badge.svg\" /></a>\n <a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/LICENSE\"><img alt=\"licence\" src=\"https://camo.githubusercontent.com/76adf36d6e44e963235bf32af0864eb7eae3ee4358ac62be3714379e47ee81c2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e63652d4d49542d3562386465663f6c6162656c436f6c6f723d316131613161\" /></a>\n</p>\n<hr />\n<p>Summarising a page is a few lines of LangChain right up until you want the same call\nto also take a YouTube link, chunk a transcript that will not fit in a context\nwindow, run against a local Ollama on a laptop with no network, and then move to\nOpenAI without any of the calling code changing. That is the part this packages.</p>\n<p>One object, <code>Summarizer</code>, takes a URL and works out whether it is a page or a video.\nThe model behind it is a constructor argument, so swapping Ollama for OpenAI, or for\nsomething you wrote yourself, is a single line.</p>\n<div><pre><span>from</span> <span>websum</span> <span>import</span> <span>Summarizer</span>, <span>OllamaBackend</span>\n<span>s</span> <span>=</span> <span>Summarizer</span>(<span>backend</span><span>=</span><span>OllamaBackend</span>(<span>model</span><span>=</span><span>\"llama3:instruct\"</span>))\n<span>print</span>(<span>s</span>.<span>summarize</span>(<span>\"https://www.youtube.com/watch?v=4pOpQwiUVXc\"</span>))</pre></div>\n<ul>\n<li><strong>Pages and YouTube through the same call.</strong> <code>summarize(url)</code> detects which it has.\n<code>summarize_web</code> and <code>summarize_youtube</code> are there when you already know.</li>\n<li><strong>The backend is a Protocol</strong>, not a base class to inherit. Anything with a\n<code>.build()</code> returning a LangChain chat model satisfies it.</li>\n<li><strong>Local by default.</strong> Ollama is a first-class backend, so nothing has to leave the\nmachine, and no key is required to try it.</li>\n<li><strong>Long inputs are chunked</strong>, so a two-hour transcript does not have to fit anywhere\nin one piece.</li>\n<li><strong>A CLI, a library and a Gradio UI</strong> over the same code, and a <code>py.typed</code> marker so\nthe API type-checks downstream.</li>\n</ul>\n<p></p><h2>Install</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#install\"></a><p></p>\n<div><pre>pip install <span><span>'</span>websum[ollama]<span>'</span></span> <span><span>#</span> library + CLI, local models</span>\npip install <span><span>'</span>websum[openai]<span>'</span></span> <span><span>#</span> OpenAI backend</span>\npip install <span><span>'</span>websum[ui,ollama]<span>'</span></span> <span><span>#</span> with the Gradio web UI</span>\npip install <span><span>'</span>websum[all]<span>'</span></span> <span><span>#</span> everything</span></pre></div>\n<p>Using <code>uv</code>:</p>\n<p>Python 3.10 to 3.13. The base install carries no model client at all, which is why\nthe backend is an extra: installing <code>websum</code> alone should not drag in an SDK you are\nnot going to call.</p>\n<p></p><h2>Use</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#use\"></a><p></p>\n<p><strong>As a library</strong></p>\n<div><pre><span>from</span> <span>websum</span> <span>import</span> <span>Summarizer</span>, <span>OllamaBackend</span>\n<span>s</span> <span>=</span> <span>Summarizer</span>(<span>backend</span><span>=</span><span>OllamaBackend</span>(<span>model</span><span>=</span><span>\"llama3:instruct\"</span>))\n<span>print</span>(<span>s</span>.<span>summarize</span>(<span>\"https://cobanov.dev/haftalik-bulten/hafta-13\"</span>))\n<span>print</span>(<span>s</span>.<span>summarize</span>(<span>\"https://www.youtube.com/watch?v=4pOpQwiUVXc\"</span>))\n<span>print</span>(<span>s</span>.<span>translate</span>(<span>\"Hello world\"</span>, <span>target_language</span><span>=</span><span>\"Turkish\"</span>))</pre></div>\n<p>Swap the backend without touching anything else:</p>\n<div><pre><span>from</span> <span>websum</span> <span>import</span> <span>Summarizer</span>, <span>OpenAIBackend</span>\n<span>s</span> <span>=</span> <span>Summarizer</span>(<span>backend</span><span>=</span><span>OpenAIBackend</span>(<span>model</span><span>=</span><span>\"gpt-4o-mini\"</span>))</pre></div>\n<p><strong>From the shell</strong></p>\n<div><pre>websum summarize https://example.com\nwebsum summarize https://example.com --backend openai --model gpt-4o-mini\nwebsum translate <span><span>\"</span>Hello world<span>\"</span></span> --target-language Turkish\nwebsum ui --port 7860</pre></div>\n<p><code>websum --help</code> has the full reference.</p>\n<p><strong>In a browser</strong></p>\n<p>\n <a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/assets/gradio.png\"><img src=\"https://github.com/cobanov/websum/raw/main/assets/gradio.png\" alt=\"The Gradio UI with a URL box and the summary below it\" /></a>\n</p>\n<p></p><h2>The API</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#the-api\"></a><p></p>\n<table>\n<thead>\n<tr>\n<th>Object</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Summarizer</code></td>\n<td>The high-level API. <code>summarize(url)</code>, <code>summarize_web(url)</code>, <code>summarize_youtube(url)</code>, <code>translate(text)</code></td>\n</tr>\n<tr>\n<td><code>SummarizerConfig</code></td>\n<td>Chunking and language settings</td>\n</tr>\n<tr>\n<td><code>OllamaBackend</code>, <code>OpenAIBackend</code></td>\n<td>The built-in backends. Frozen dataclasses with <code>.build()</code></td>\n</tr>\n<tr>\n<td><code>LLMBackend</code> (Protocol)</td>\n<td>Implement this to plug in anything else</td>\n</tr>\n<tr>\n<td><code>BackendRegistry</code></td>\n<td>Maps string names to backend classes, which is how the CLI resolves <code>--backend</code></td>\n</tr>\n</tbody>\n</table>\n<p>Every public name is re-exported from the top-level <code>websum</code> package and listed in\n<code>__all__</code>.</p>\n<p></p><h3>Writing a backend</h3><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#writing-a-backend\"></a><p></p>\n<p><code>LLMBackend</code> is a <code>Protocol</code>, so there is nothing to subclass and nothing to\nregister. A class with a <code>build()</code> method already satisfies it:</p>\n<div><pre><span>from</span> <span>dataclasses</span> <span>import</span> <span>dataclass</span>\n<span>from</span> <span>websum</span> <span>import</span> <span>LLMBackend</span>, <span>Summarizer</span>\n<span>@<span>dataclass</span></span>\n<span>class</span> <span>MyBackend</span>:\n <span>def</span> <span>build</span>(<span>self</span>):\n <span>from</span> <span>langchain_anthropic</span> <span>import</span> <span>ChatAnthropic</span>\n <span>return</span> <span>ChatAnthropic</span>(<span>model</span><span>=</span><span>\"claude-sonnet-5\"</span>)\n<span>assert</span> <span>isinstance</span>(<span>MyBackend</span>(), <span>LLMBackend</span>) <span># runtime Protocol check</span>\n<span>s</span> <span>=</span> <span>Summarizer</span>(<span>backend</span><span>=</span><span>MyBackend</span>())</pre></div>\n<p>The import lives inside <code>build()</code> on purpose: a backend that is never constructed\nshould not cost an import of an SDK that may not be installed.</p>\n<p></p><h2>Docker</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#docker\"></a><p></p>\n<div><pre>docker build -t websum <span>.</span>\ndocker run -p 7860:7860 websum\n<span><span>#</span> when ollama is running on the host</span>\ndocker run --network host -p 7860:7860 websum</pre></div>\n<p>The image starts <code>websum ui</code> by default.</p>\n<p></p><h2>Development</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#development\"></a><p></p>\n<div><pre>git clone https://github.com/cobanov/websum\n<span>cd</span> websum\nuv sync --all-extras\nuv run pre-commit install\nuv run pytest\nuv run ruff check <span>.</span>\nuv run mypy src/websum</pre></div>\n<p>CI runs the 19 tests on 3.10, 3.11, 3.12 and 3.13, with <code>ruff</code>, <code>ruff format --check</code>\nand <code>mypy</code> on top. <code>pytest-randomly</code> shuffles the order every run, so a test that\nonly passes because another one ran first fails here rather than later.\n<a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/CONTRIBUTING.md\">CONTRIBUTING.md</a> has the full guide.</p>\n<p></p><h2>Upgrading from 0.1.x</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#upgrading-from-01x\"></a><p></p>\n<p>The 0.1.x scripts under <code>app/</code> are gone. Everything moved into the <code>websum</code> package\nbehind a typed, importable API.</p>\n<table>\n<thead>\n<tr>\n<th>Before</th>\n<th>After</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>python app/summarizer.py -u URL</code></td>\n<td><code>websum summarize URL</code></td>\n</tr>\n<tr>\n<td><code>python app/webui.py</code></td>\n<td><code>websum ui</code></td>\n</tr>\n<tr>\n<td><code>from summarizer import setup_summarization_chain</code></td>\n<td><code>from websum import Summarizer</code></td>\n</tr>\n<tr>\n<td>Hardcoded <code>ChatOllama</code></td>\n<td><code>OllamaBackend</code>, <code>OpenAIBackend</code>, or your own <code>LLMBackend</code></td>\n</tr>\n<tr>\n<td><code>pip install -r requirements.txt</code></td>\n<td><code>pip install 'websum[ollama]'</code></td>\n</tr>\n</tbody>\n</table>\n<p></p><h2>Licence</h2><a target=\"_blank\" href=\"https://github.com/cobanov/easy-web-summarizer#licence\"></a><p></p>\n<p>MIT. See <a target=\"_blank\" href=\"https://github.com/cobanov/websum/blob/main/LICENSE\">LICENSE</a>.</p>\n</article></div>",
"author": "",
"favicon": "https://github.githubassets.com/favicons/favicon.svg",
"source": "github.com",
"published": "",
"ttr": 136,
"type": "object"
}