Categories: sharing » writing » blogging

RSS - Atom - Subscribe via email

Thinking about how to reinvest the Google Open Source Peer Bonus

| blogging

I received a Google Open Source Peer Bonus for my contributions to GNU Emacs, which was a pleasant surprise. Thanks!

I'm thinking of ways to reinvest the ~USD 250 award into Emacs and the community to see what a little money earmarked for that could do. People have already donated enough to EmacsConf to cover hosting costs, so that's all sorted out. People have also already sent me more than enough to cover my hosting costs using my ancient pay-what-you-want resources. I wonder how I could use the money to help me make more blog posts and videos.

Speech recognition: Paying for cloud usage will let me do tiny experiments without upgrading my X230T1 for now. I could start with speech recognition as a way of fleshing out ideas and getting them into text faster. Deepgram charges USD 0.0048/min for batch-processing with Whisper Large and USD 0.0059/min for streaming with their Nova-2 model, so that's… umm… ~860+ hours I could process. Over the past couple of weeks of experimenting with this idea, I've recorded about 1-2 hours of audio braindumps a day, so that's still well over a year of being able to play around with this. (And actually I still have USD ~187 of free trial credits with them, so…)

AI: I might also be able to use AI for outlining/summarizing/cleaning up my audio braindumps. I just have to figure out the right prompts for ChatGPT. Here's one I've been experimenting with so that I can get things into roughly an Org Mode format while still letting me easily look things up in the transcript:

Outlining prompt
Reorganize this rough transcript into an outline of ideas.

Format it like this:

- item 1
  - details
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
  - details
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
  - details
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
    - more details
      - verbatim quote from transcript
    - more details
- item 2 ...

Drawing: The kiddo uses the iPad a lot for reading, but maybe I can squeeze in some time to tinker around with different apps for drawing and animation.

Video editing: Maybe I can learn more about video editing or figure out what gear makes sense to add to my setup.

If you have other suggestions for low-cost experiments that might pay off in terms of making more useful blog posts or videos, I'd love to hear them!

Footnotes:

1

The X230T is a lovely computer. This particular one is a donation from Matthew Darling, and it has an i5-3320M. I occasionally get tempted to upgrade to maybe a desktop with a GPU so that I can do more experiments with Whisper, ffmpeg, or local AI models, but since I still only have a tiny sliver of computing time each day before the kiddo wakes up, it doesn't make sense to buy a powerful computer that will sit idle most of the time. He also gave me a Surface Book with an i7-6600U, and I can probably run stuff on it. It has a 1GB NVIDIA GPU, even, so maybe I should figure out how I can ssh into it since it runs Windows at the moment. There's a W530 with an i7-3820QM around here with a 2GB NVIDIA GPU that also tends to be idle. That one dualboots between Windows and Linux, but it tends to be in Windows because the kiddo uses it to play Minecraft Bedrock. I've just set up SSH access to WSL on both of them, so that should be promising. I'm surrounded by excess compute resources that I could use for making videos either through interactive applications like Kdenlive or through text-based workflows using my Emacs Lisp functions. Besides, it makes sense to focus on very short videos for now (or even blog posts with more screenshots and animated GIFs). Maybe I just need to spend some time this winter break to figure out some workflows. Hmm…

Fixing my old ambiguous sketch references

| blogging, 11ty, emacs

At some point during the conversion of my blog from Wordpress to 11ty, I wanted to change my sketch links to use a custom shortcode instead of referring to the sketch in my old wp-uploads directory. Because Wordpress changed the filenames a little, I used the ID at the start of the filename. I forgot that many of my filenames from 2013 to 2015 just had the date without a uniquely identifying letter or number suffix, so many old references were ambiguous and my static site generator just linked to the first matching file. When I was listening to my old monthly reviews as part of my upcoming 10-year review, I noticed the repeated links. So I wrote these functions to help me find and replace markup of the form sketchLink "2013-10-06" with sketchLink "2013-10-06 Daily drawing - thinking on paper #drawing", replacing references to the same date with the next sketch in the list. I figured that would be enough to get the basic use case sorted out (usually a list of sketches in my monthly/weekly reviews), taking advantage of the my-list-sketches function I defined in my Emacs config.

(defun my-replace-duplicate-sketch-list-references ()
  (interactive)
  (goto-char (point-min))
  (let (seen)
    (while (re-search-forward "sketchLink \\\"\\([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\\)\\\""
                              nil t)
      (if (assoc (match-string 1) seen)
          (setcdr (assoc (match-string 1) seen) (1+ (assoc-default (match-string 1) seen)))
        (setq seen (cons (cons (match-string 1) 1) seen))))
    (mapc (lambda (entry)
            (goto-char (point-min))
            (mapc (lambda (sketch)
                    (if (re-search-forward (format "sketchLink \\\"\\(%s\\)\\\""
                                                   (regexp-quote (car entry))) nil t)
                        (replace-match (save-match-data (file-name-sans-extension sketch))
                                       nil t nil 1)
                      (message "Skipping %s possible ref to %s"
                               (buffer-file-name)
                               sketch)))
                  (my-list-sketches (concat "^" (regexp-quote (car entry))) nil '("~/sync/sketches"))))
          seen)))

Sometimes I needed to delete the whole list and start again:

(defun my-insert-sketch-list-between (start-date end-date)
  (insert
   (mapconcat
    (lambda (f)
      (format "<li>%s sketchLink \"%s\" %s</li>\n"
              (concat "{" "%")  ; avoid confusing 11ty when I export this
              (file-name-sans-extension f)
              (concat "%" "}")))
    (sort (seq-filter
           (lambda (f) (and (string< f end-date) (not (string< f start-date))))
           (my-list-sketches nil nil '("~/sync/sketches")))
          'string<)
    "")))

I used find-grep-dired to search for sketchLink \"[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\" and then I just used a keyboard macro to process each file.

Anyway, really old monthly reviews like this one for October 2013 should mostly make sense again. I could probably pull out the correct references from the Wordpress database backup, but what I've got is probably okay. I would probably have gotten much grumpier trying to do this without Emacs Lisp. Yay Emacs!

View org source for this post

Moving my Org post subtree to the 11ty directory

| 11ty, org, emacs, blogging

I sometimes want to move the Org source for my blog posts to the same directory as the 11ty-exported HTML. This should make it easier to update and reexport blog posts in the future. The following code copies or moves the subtree to the 11ty export directory.

(defun my-org-11ty-copy-subtree (&optional do-cut)
  "Copy the subtree for the current post to the 11ty export directory.
With prefix arg, move the subtree."
  (interactive (list current-prefix-arg))
  (let* ((file-properties
          (org-element-map
              (org-element-parse-buffer)
              'keyword
            (lambda (el)
              (when (string-match "ELEVENTY" (org-element-property :key el))
                (list
                 (org-element-property :key el)
                 (org-element-property :value el)
                 (buffer-substring-no-properties
                  (org-element-property :begin el)
                  (org-element-property :end el)))))))
         (entry-properties (org-entry-properties))
         (filename (expand-file-name
                    "index.org"
                    (expand-file-name
                     (assoc-default "EXPORT_ELEVENTY_FILE_NAME" entry-properties)
                     (car (assoc-default "ELEVENTY_BASE_DIR" file-properties))))))
    (unless (file-directory-p (file-name-directory filename))
      (make-directory (file-name-directory filename) t))
    ;; find the heading that sets the current EXPORT_ELEVENTY_FILE_NAME
    (goto-char
     (org-find-property "EXPORT_ELEVENTY_FILE_NAME" (org-entry-get-with-inheritance "EXPORT_ELEVENTY_FILE_NAME")))
    (org-copy-subtree 1 (if do-cut 'cut))
    (with-temp-file filename
      (org-mode)
      (insert (or
               (mapconcat (lambda (file-prop) (elt file-prop 2))
                          file-properties
                          "")
               "")
              "\n")
      (org-yank))
    (find-file filename)
    (goto-char (point-min))))

Then this adds a link to it:

(defun my-org-export-filter-body-add-index-link (string backend info)
  (if (and
       (member backend '(11ty html))
       (plist-get info :file-name)
       (plist-get info :base-dir)
       (file-exists-p (expand-file-name
                       "index.org"
                       (expand-file-name
                        (plist-get info :file-name)
                        (plist-get info :base-dir)))))
      (concat string
              (format "<div><a href=\"%sindex.org\">View org source for this post</a></div>"
                      (plist-get info :permalink)))
    string))

(with-eval-after-load 'ox
  (add-to-list 'org-export-filter-body-functions #'my-org-export-filter-body-add-index-link))

Then I want to wrap the whole thing up in an export function:

(defun my-org-11ty-export (&optional async subtreep visible-only body-only ext-plist)
  (let* ((info (org-11ty--get-info subtreep visible-only))
         (file (org-11ty--base-file-name subtreep visible-only)))
    (unless (string= (plist-get info :input-file)
                     (expand-file-name
                      "index.org"
                      (expand-file-name
                       (plist-get info :file-name)
                       (plist-get info :base-dir))))
      (save-window-excursion
        (my-org-11ty-copy-subtree)))
    (org-11ty-export-to-11tydata-and-html async subtreep visible-only body-only ext-plist)
    (my-org-11ty-find-file)))

Now to figure out how to override the export menu. Totally messy hack!

(with-eval-after-load 'ox-11ty
  (map-put (caddr (org-export-backend-menu (org-export-get-backend '11ty)))
           ?o (list "To Org, 11tydata.json, HTML" 'my-org-11ty-export)))
View org source for this post
This is part of my Emacs configuration.

Tweaking my writing workflow using SuperNote's new handwriting recognition

| blogging, supernote

Both Google Cloud Vision and SuperNote's new handwriting recognition handle my print fine. Neither handle columns the way I'd like, but to be fair, I'm not really sure how I want columns and wrapping handled anyway. I can always experiment with the standard use-case: one column of text, to export as text (with perhaps the occasional sketch, which I can crop and include).

If I can get the hang of writing my thoughts, then it turns some of those bedtime hours into writing hours. Writing by hand feels slow and linear, but it's better than nothing, and thinking takes most of the time anyway. While speech recognition feels like it might be faster in short bursts, I don't have a lot of "talking to myself" time (aside from sleepy brain dumps), and my workflow for processing audio is still slow and disjointed. I can't type on my phone because then A- will want to be on a screen too. I'm glad e-ink devices are different enough not to trigger her sense of unfairness, although sometimes she does ask if she can do mazes or connect-the-dots. Then I switch to knitting until it's really really time to go to bed.

I'm slowly figuring out my workflows for experimenting with and writing about code. Naturally, that's a little more challenging to write about by hand, but I could draft the context. I can think through life stuff too, and maybe look into saving more notes in my Org files.

I've experimented with handwritten blog posts before. Now that I have a little more time to tweak my workflow and think thoughts, maybe I'll get the hang of them!


It looks like the Supernote's real-time recognition is pretty accurate for my handwriting, getting the text out of multiple pages is pretty straightforward.

Here's the raw TXT output from the Supernote.

Here's what it took to edit it into the first part of this post - just adding line-breaks and fixing up some words:

"A screen recording showing editing"
Figure 1: My editing process - just added line breaks and fixed some words
Source images

[["The first page of my handwritten post"

"The second page of my handwritten post"
Figure 2: Second page

If I add more lines between paragraphs when writing, I might be able to skip adding them in the text export.

For comparison, here's the text output from Google Cloud Vision.

Tweaking my handwriting workflow
Both Google Cloud Vision and Super Note's new
handwriting recognition handle my print fine. Neither
handle columns the way I'd like, but to be fair,
I'm not really sure how I want columns and wrapping
handled anyway I can always experiment with the
standard use-case
use-case: One column of text, to export
as Text (with perhaps the occasional sketch, which
can crop and include).
If I can get the hang of writing my thoughts,
then it turns some of those bedtime hours into writi
writing
hours. Writing by hand feels slow and linear, but it's
better than nothing, and thinking takes most of the time
anyway while speech recognition feels like it might be
faster in short bursts, don't have a lot of "talking to
myself" time (aside from sleepy braindumps), and my workflow
for processing audio is still slow and disjointed. I can't
type on my phone because then A- will want to be on

I'm glad e-ink devices are different enough
not to trigger her sense of unfairness, although sometimes
she does ask if she can do mazes or connect-the-dots
a screen too
Then I switch to Knitting until it's really really time to
go to bed.
I'm slowly figuring out my workflows for experimenting
with and writing about code. Naturally, that's a little
more challenging to write about by hand, but I could
draft the context. I can think through life stuff too, and
maybe look into saving more notes in my org files
I've experimented with handwritten blog posts before
Now that I have a little more time to tweak my workflow
and think thoughts, maybe I'll get the hang of them!

I'm leaning towards SuperNote's recognition results for long text, although I don't get access to the confidence data so I'll probably just have to delete the misrecognized text if I include sketches.

Using Javascript to add a "Copy code" link to source code blocks in my blog posts

| css, js, blogging

I'd like to write about code more often. It's easier for people to try out ideas if they can copy the code without fiddling with selecting the text, especially on mobile browsers, so "Copy code" buttons on source code blocks would be nice. I used this tutorial for adding code buttons as a basis for the following CSS and JS code.

First, let's add the buttons with Javascript. I want the buttons to be visible in the summary line if I'm using the <details /> element. If not, they can go in the div with the org-src-container class.

/* Start of copy code */
// based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
const copyLabel = 'Copy code';

async function copyCode(block, button) {
  let code = block.querySelector('pre.src');
  let text = code.innerText;
  await navigator.clipboard.writeText(text);
  button.innerText = 'Copied';
  setTimeout(() => {
    button.innerText = copyLabel;
  }, 500);
}

function addCopyCodeButtons() {
  if (!navigator.clipboard) return;
  let blocks = document.querySelectorAll('.org-src-container');
  blocks.forEach((block) => {
    let button = document.createElement('button');
    button.innerText = copyLabel;
    button.classList.add('copy-code');
    let details = block.closest('details');
    let summary = details && details.querySelector('summary');
    if (summary) {
      summary.appendChild(button);
    } else {
      block.appendChild(button);
    }
    button.addEventListener('click', async() => {
      await copyCode(block, button);
    });
    block.setAttribute('tabindex', 0);
  });
}
document.addEventListener("DOMContentLoaded", function(event) { 
  addCopyCodeButtons();
});
/* End of copy code */

Then we style it:

/* Start of copy code */
pre.src { margin: 0 }
.org-src-container {
    position: relative;
    margin: 0 0;
    padding: 1.75rem 0 1.75rem 1rem;
}
summary { position: relative; }
summary .org-src-container { padding: 0 }
summary .org-src-container pre.src { margin: 0 }
.org-src-container button.copy-code, summary button.copy-code {
    position: absolute;
    top: 0px;
    right: 0px;
}
/* End of copy code */

Someday I'll figure out how to make it easier to tangle things to the post's directory and make the file available for download. In the meantime, this might be a good start.

Compiling selected blog posts into HTML and EPUB so I can annotate them

| blogging, 11ty, nodejs, supernote

[2023-01-04 Wed] Added a screenshot showing annotation.

I was thinking about how to prepare for my next 10-year review, since I'll turn 40 this year. I've been writing yearly reviews with some regularity and monthly reviews sporadically, and I figured it would be nice to have those posts in an EPUB so that I can read them on my e-reader and annotate them as I do my review.

I use the 11ty static site generator to publish my blog as HTML files, since I currently can't keep more than Emacs Lisp, Javascript, and Python in my brain. (No Hugo or Jekyll for me at the moment.) I briefly thought about getting 11ty to create that archive for me, but I realized it might be easier to just write it as an external script instead of trying to figure out how to get 11ty to export one thing conditionally.

One of the things I've configured 11ty to make is a JSON file that includes all of my posts with dates, titles, permalinks, and categories. It was easy to then parse this list and filter it to get the posts I wanted. I parsed the HTML out of the _site directory that 11ty produces instead of fetching the pages from my webserver. I got the images from my webserver, though, and I made a local cache and rewrote the URLs. That way, the EPUB conversion could include the images.

Download blog.js

blog.js
const blog = require('/home/sacha/proj/static-blog/_site/blog/all/index.json');
const cheerio = require('cheerio');
const base = '/home/sacha/proj/static-blog/_site';
const fs = require('fs');
const path = require('path');

function slugify(p) {
  return p.permalink.replace('/blog', 'post-').replace(/\//g, '-');
}

async function processPost(p) {
  console.log('Processing '+ p.permalink);
  let $ = cheerio.load(fs.readFileSync(base + p.permalink + 'index.html'));
  $('#comment').remove();
  let images = $('article img');
  await Promise.all(images.map((i, e) => {
    let url = $(e).attr('src');
    const outputFileName = 'images/' + path.basename(url).replace(/ |%20|%23/g, '-');
    $(e).attr('src', outputFileName);
    $(e).attr('style', 'max-height: 100%; max-width: 100%; ' + ($(e).attr('style') || ''));
    $(e).attr('srcset', null);
    $(e).attr('sizes', null);
    $(e).attr('width', null);
    $(e).attr('height', null);
    if (!fs.existsSync(outputFileName)) {
      console.log('fetch', outputFileName);
      return fetch(url).then(res => res.arrayBuffer()).then(data => {
        const buffer = Buffer.from(data);
        return fs.createWriteStream(outputFileName).write(buffer);
      });
    } else {
      console.log(outputFileName, 'exists');
      return null;
    }
  }));
  console.log('Done ' + p.permalink);
  let slug = slugify(p);
  $('article h2').attr('id', slug);
  let header = $('article header').html();
  let entry = $('article .entry').html();
  return `<article>${header}${entry}</article>`;
}

let last10 = blog.filter((p) => p.date >= '2013-08-01');
let posts = last10.filter((p) => p.categories.indexOf('yearly') >= 0)
    .concat(blog.filter((p) => p.title == 'Turning 30: A review of the last decade'))
    .concat(last10.filter((p) => p.categories.indexOf('monthly') >= 0));

let toc = '<h1>Table of Contents</h1><ul>' + posts.map((p) => {
  return `<li><a href="#${slugify(p)}">${p.title}</a></li>\n`;
}).join('') + '</ul>';

let content = posts.reduce(async (prev, val) => {
    return await prev + await processPost(val);
  }, '');
content.then((data) => {
  fs.writeFileSync('archive.html',
                   `<html><body>${toc}${data}</body></html>`);

});

This created an archive.html with my posts, using the images/ directory for the images. Then I used my shell script for converting and copying files to convert it to EPUB and copy it over.

On the SuperNote, I can highlight text by drawing square brackets around it. If I tap that text, I can write or draw underneath it. Here's what that looks like:

20230104_090739.png
Figure 1: Writing an annotation

These notes are collected into a "Digest" view, and I can export things from there. (Example: archive.pdf)

2023-01-04_09-23-57.png
Figure 2: Here's what that digest is like when exported.

(Hmm, maybe I should ask them about hiding the pencil icon…)

Anyway, I think that might be a good starting point for my review.

Writing my blog posts by hand

| blogging, supernote

A- complains if I get screentime when she doesn't get screentime, so it's hard to find time to write on my laptop or on my phone. I've experimented with dictation before, since Google Recorder can make a half-decent transcript. I'm not used to talking things out, though. I keep correcting false starts, stutters, and mis-recognized words.

Fortunately, I can write on my A5X while waiting for A-. I get more space than I do when writing on my phone, so it's easier for me to think. I can export pages as PNGs, Dropbox, share each page, sync with with Google Photos, and then use Lens to copy the text. I can then paste it into Orgzly, which automatically syncs with Syncthing so that I can edit it on my laptop with Emacs. It needs a little cleanup (capitalization, stray punctuation, missed words, things in the wrong order), but editing it feels easier than dealing with the output of speech recognition, so it seems to be worth the extra time and effort. Besides, it feels less embarrassing to write at the sandbox than it is to talk to myself.

I can edit the text directly on my phone, but I still need my laptop to publish my blog because I haven't set up my static site generator on my server. Some day! In the meantime, this might be a good workflow for getting thoughts out there.

What if I want to refer to sketches while I write? Flipping between pages on the A5X can be challenging if they're not next to each other, but I can keep my current writing page next to my sketch. I could also view the sketch on my phone and balance it on the A5X, or use layers to keep a small version of the sketch as a handy reference. Lots of ideas to play around with…