<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ApplyFreeBoiler.uk]]></title><description><![CDATA[ApplyFreeBoiler.uk]]></description><link>https://applyfreeboiler.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 12:15:40 GMT</lastBuildDate><atom:link href="https://applyfreeboiler.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Working with YouTube Transcripts: A Practical Guide to Notes, Quotes and Search (SRT & TXT)]]></title><description><![CDATA[Conference talks, lectures, interviews, podcasts: a lot of useful knowledge now lives in YouTube videos. Video is great to watch and terrible to work with…]]></description><link>https://applyfreeboiler.hashnode.dev/working-with-youtube-transcripts-a-practical-guide-to-notes-quotes-and-search-srt-txt</link><guid isPermaLink="true">https://applyfreeboiler.hashnode.dev/working-with-youtube-transcripts-a-practical-guide-to-notes-quotes-and-search-srt-txt</guid><category><![CDATA[youtube]]></category><category><![CDATA[Python]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Faisal Ashfaq]]></dc:creator><pubDate>Fri, 23 Oct 2026 04:27:34 GMT</pubDate><content:encoded><![CDATA[<p>Conference talks, lectures, interviews, podcasts: a lot of useful knowledge now lives in YouTube videos. Video is great to watch and terrible to <em>work with</em>. You can't grep it, you can't easily quote it, and finding "that bit where she explained the cache invalidation trick" means scrubbing through a timeline.</p>
<p>Transcripts fix that. This guide covers a simple workflow I use for research and technical notes:</p>
<ol>
<li>Get the transcript as <strong>SRT</strong> (timed) and/or <strong>TXT</strong> (plain).</li>
<li>Turn it into <strong>notes</strong> you'll actually reuse.</li>
<li><strong>Quote</strong> accurately, with timestamp links back to the source.</li>
<li>Make a growing collection of transcripts <strong>searchable</strong>.</li>
</ol>
<p>Everything here uses plain files, Python's standard library and SQLite. No services required.</p>
<hr />
<h2>1. SRT vs TXT: pick the right format</h2>
<p>Most transcript tools give you two formats, and they're good at different things.</p>
<p><strong>SRT</strong> keeps timing. Each "cue" has a number, a start and end time, and the text:</p>
<pre><code>42
00:12:03,480 --&gt; 00:12:07,120
so the trick is to invalidate on write
not on read
</code></pre>
<p>Use SRT when <strong>time matters</strong>: quoting, linking to moments, aligning with slides, or building a search index that jumps to the right second.</p>
<p><strong>TXT</strong> is just the words. It's easier to read, paste into a doc, or feed into a summariser.</p>
<p>My rule: <strong>always keep the SRT</strong> as the source of truth, and generate TXT from it when you need reading copy. You can always throw timing away, but you can't get it back.</p>
<p>For getting the files: YouTube's own transcript panel (under the description, "Show transcript") is fine for reading. For downloads, a free web tool such as <a href="https://youtubescribe.com">YouTubeScribe</a> gives you TXT and SRT from the video's existing caption track. Developers can script it with <code>yt-dlp</code> or the <code>youtube-transcript-api</code> Python package. Whatever you use, remember that these all read the captions the video <em>already has</em>. No captions means no transcript.</p>
<hr />
<h2>2. Parse SRT in a few lines of Python</h2>
<p>You don't need a library for basic SRT parsing. This yields <code>(start_seconds, text)</code> for each cue:</p>
<pre><code class="language-python">import re
from pathlib import Path

CUE = re.compile(
    r"(\d+)\s*\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --&gt; [^\n]+\n(.*?)(?:\n\n|\Z)",
    re.S,
)

def parse_srt(path):
    text = Path(path).read_text(encoding="utf-8").replace("\r\n", "\n")
    for m in CUE.finditer(text):
        h, mnt, s = int(m[2]), int(m[3]), int(m[4])
        yield h * 3600 + mnt * 60 + s, " ".join(m[6].split())
</code></pre>
<p>(If your files get messy, the <code>srt</code> package on PyPI is a solid, tolerant parser.)</p>
<p>Converting to readable TXT is then trivial. Group cues into paragraphs every 60 seconds or so, since auto-captions often lack reliable sentence punctuation:</p>
<pre><code class="language-python">def to_paragraphs(cues, every=60):
    para, bucket = [], None
    for start, text in cues:
        if bucket is None or start - bucket &gt;= every:
            if para:
                yield bucket, " ".join(para)
            para, bucket = [], start
        para.append(text)
    if para:
        yield bucket, " ".join(para)
</code></pre>
<hr />
<h2>3. Notes you'll actually reuse</h2>
<p>A raw transcript isn't a note. It's source material. What's worked for me is one Markdown file per video, with three sections:</p>
<pre><code class="language-markdown"># Talk title — Speaker (Conference, 2025)
Source: https://www.youtube.com/watch?v=VIDEO_ID

## Summary (my words, 3–5 bullets)
- ...

## Key moments
- [12:03](https://www.youtube.com/watch?v=VIDEO_ID&amp;t=723s) Invalidate on write, not read
- [31:40](https://www.youtube.com/watch?v=VIDEO_ID&amp;t=1900s) Benchmark numbers

## Transcript
(paragraphed TXT, with a timestamp at the start of each paragraph)
</code></pre>
<p>A few habits make a big difference:</p>
<ul>
<li><strong>Write the summary yourself.</strong> AI summaries are fine for triage, but writing it in your own words is what makes you remember it. It also catches errors in the captions.</li>
<li><strong>Timestamp links are your citations.</strong> YouTube accepts <code>&amp;t=&lt;seconds&gt;s</code> on a watch URL, so every key moment is one click from the source.</li>
<li><strong>Keep the full transcript at the bottom</strong> so full-text search in Obsidian, Logseq, VS Code or plain <code>ripgrep</code> finds it later.</li>
</ul>
<p>Generating that timestamp link from a parsed cue is one line:</p>
<pre><code class="language-python">def ts_link(video_id, seconds):
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    label = f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
    return f"[{label}](https://www.youtube.com/watch?v={video_id}&amp;t={int(seconds)}s)"
</code></pre>
<hr />
<h2>4. Quoting accurately</h2>
<p>Transcripts make quoting easy, and also make it easy to quote <em>wrongly</em>. Auto-generated captions mishear names, technical terms and numbers ("Kubernetes" becomes "cooper Nettie's", "15%" becomes "50%").</p>
<p>Before a quote goes into a paper, blog post or slide:</p>
<ol>
<li><strong>Find it in the transcript</strong> (search is instant).</li>
<li><strong>Click the timestamp and listen</strong> to the original few seconds.</li>
<li><strong>Fix the wording</strong> to match what was actually said. Use <code>[brackets]</code> for anything you add, and an ellipsis for anything you cut.</li>
<li><strong>Cite the video and the time.</strong> For example, in APA style:</li>
</ol>
<blockquote>
<p>Speaker, A. (2025, March 4). <em>Title of the talk</em> [Video]. YouTube. <a href="https://www.youtube.com/watch?v=VIDEO_ID">https://www.youtube.com/watch?v=VIDEO_ID</a></p>
</blockquote>
<p>and give the timestamp in the in-text citation, e.g. (Speaker, 2025, 12:03).</p>
<p>If a video has human-uploaded captions, they're usually more reliable than auto-generated ones. Many tools (and <code>yt-dlp</code>'s <code>--write-subs</code>) let you prefer them.</p>
<hr />
<h2>5. Search across all your transcripts with SQLite FTS5</h2>
<p>Once you have more than a handful of videos, you want to ask "which talk mentioned <em>vector clocks</em>, and when?" SQLite's built-in full-text search (FTS5) does this with no server:</p>
<pre><code class="language-python">import sqlite3
from pathlib import Path

db = sqlite3.connect("transcripts.db")
db.execute(
    "CREATE VIRTUAL TABLE IF NOT EXISTS cues "
    "USING fts5(video_id, start UNINDEXED, text)"
)

# Index every SRT in a folder; name files &lt;video_id&gt;.srt
for srt in Path("transcripts").glob("*.srt"):
    rows = [(srt.stem, start, text) for start, text in parse_srt(srt)]
    db.executemany("INSERT INTO cues VALUES (?, ?, ?)", rows)
db.commit()

def search(q, limit=10):
    sql = ("SELECT video_id, start, snippet(cues, 2, '[', ']', '…', 12) "
           "FROM cues WHERE cues MATCH ? ORDER BY rank LIMIT ?")
    for vid, start, snip in db.execute(sql, (q, limit)):
        print(f"https://www.youtube.com/watch?v={vid}&amp;t={start}s  {snip}")

search('"vector clocks"')
search("cache NEAR(invalidate, 10)")
</code></pre>
<p>Every hit is a link that opens the video at the right second. FTS5 supports phrase queries, <code>NEAR</code>, prefix search (<code>invalid*</code>) and boolean operators, which is plenty for a personal research library.</p>
<p>One tip: because a single cue is only a few words long, phrases can straddle two cues. If you miss results, index <strong>overlapping windows</strong> instead (for example, each row holds cue <em>n</em> plus cue <em>n+1</em>).</p>
<hr />
<h2>6. Ethics and practicalities</h2>
<ul>
<li><strong>Respect the source.</strong> Transcripts for personal notes, study and research are one thing. Republishing someone's whole talk as your blog post is another. Quote, link and credit.</li>
<li><strong>Check the licence</strong> if you plan to share a transcript dataset.</li>
<li><strong>Mind rate limits</strong> if you script downloads. Fetch slowly and cache everything locally.</li>
<li><strong>No captions, no transcript.</strong> For videos without a caption track, you'd need speech-to-text (e.g. running Whisper locally on audio you have the right to use).</li>
</ul>
<hr />
<h2>Wrapping up</h2>
<p>The whole workflow fits in one small script: keep SRT as the source of truth, generate readable TXT and Markdown notes from it, link every quote back to its timestamp, and drop everything into an FTS5 index. After a few weeks you end up with a searchable personal library of every talk and lecture you've learned from, and every claim in it is one click from the original video.</p>
]]></content:encoded></item><item><title><![CDATA[Cleaning the DESNZ ECO council data: pitfalls and a reproducible CSV]]></title><description><![CDATA[The Department for Energy Security and Net Zero (DESNZ) publishes excellent statistics on the Energy Company Obligation (ECO), but the council-level tables…]]></description><link>https://applyfreeboiler.hashnode.dev/cleaning-the-desnz-eco-council-data-pitfalls-and-a-reproducible-csv</link><guid isPermaLink="true">https://applyfreeboiler.hashnode.dev/cleaning-the-desnz-eco-council-data-pitfalls-and-a-reproducible-csv</guid><dc:creator><![CDATA[Faisal Ashfaq]]></dc:creator><pubDate>Fri, 09 Oct 2026 20:57:41 GMT</pubDate><content:encoded><![CDATA[<p>The Department for Energy Security and Net Zero (DESNZ) publishes excellent statistics on the Energy Company Obligation (ECO), but the council-level tables are built for reading in Excel, not for analysis. Here is how the data was turned into a clean, machine-readable CSV of 350 Great Britain councils, and the traps to watch for.</p>
<h2>The source</h2>
<p>The data comes from DESNZ's Household Energy Efficiency Statistics headline release (June 2026): Table 3.4 (ECO measures by area), Table 3.5 (flexible eligibility measures) and Table 4.4 (households helped). Three tables, three slightly different layouts.</p>
<h2>Problems worth knowing about</h2>
<p><strong>1. Mixed hierarchy.</strong> The sheets contain councils, counties, Inner and Outer London sub-totals and regions in the same column. If you sum everything you double count. The fix was to keep only single-tier and district councils (by ONS code prefixes E06, E07, E08, E09, W06 and S12) and move the county into its own column.</p>
<p><strong>2. Bilingual names.</strong> Welsh councils appear as "Ceredigion / Ceredigion"-style strings. The published names are kept, so join on the ONS area code instead of the name.</p>
<p><strong>3. Suppressed values.</strong> DESNZ withholds small numbers: "#" means a value of 1 to 4 and "^" means 5 or more, withheld for privacy. Casting these to zero would silently bias the data, so they become blank cells with a separate flag column. In the file, 30 councils have a suppressed flex value.</p>
<p><strong>4. Different time windows.</strong> The main measures column covers January 2013 to March 2026 across every ECO phase. The flexible eligibility figures only start in April 2022. Mixing these in one chart is misleading, so each column is labelled with its window.</p>
<h2>A validation step worth copying</h2>
<p>After cleaning, check that the 350 council rows add up exactly to the published Great Britain totals: 4,453,004 measures and 2,595,218 households. If a council is dropped or a county subtotal sneaks in, the check fails immediately. It is cheap to write and catches most structural mistakes.</p>
<h2>A caution on interpretation</h2>
<p>Raw totals favour big cities. Birmingham has the most measures (195,919), but Bradford has the highest rate at 578.4 per 1,000 households. Use the per-household rate to compare areas. Also, DESNZ does not publish an ECO4-only total by council, so this data should not be read as "ECO4 only".</p>
<pre><code class="language-python">import pandas as pd
df = pd.read_csv("eco_measures_by_council_gb.csv")
top = df.sort_values("measures_per_1000_households", ascending=False).head(10)
print(top[["council", "measures_per_1000_households"]])
</code></pre>
<h2>Get the data</h2>
<p>The cleaned CSV, column dictionary and methodology are on the <a href="https://applyfreeboiler.uk/data/eco-measures-by-council">ECO measures by council data page</a>. It is released under the Open Government Licence v3.0, and the figures are provisional, as published by DESNZ. If you spot a problem, let us know and we will fix it.</p>
]]></content:encoded></item></channel></rss>