A Keyboard Maestro macro to edit Anki sound file

Often when I import a pronunciation file into Anki, from Forvo for example, the volume isn’t quite right or there’s a lot of background noise; and I want to edit the sound file. How?

The solution for me, as it often the case is a Keyboard Maestro macro.

Prerequisites

  • Keyboard Maestro - if you are a macOS power user and don’t have KM, then your missing on a lot.
  • Audacity - the multi-platform FOSS audio editor

Outline of the approach

Since Keyboard Maestro won’t know the path to our file in Anki’s collection.media directory, we have to find it. But the first task is to extract the filename. In the Anki note field, it’s going to have this format:

[sound:forvo-e21a80cf-285b8575-3972ebd2-24eaa712-d8e5cc26.mp3]

To extract the filename forvo-e21a80cf-285b8575-3972ebd2-24eaa712-d8e5cc26.mp3 we can just use sed:

sed -E 's/\[.*:(.*)\]/\1/g'

And to find the file in the macOS file system:

mdfind -name $fn

But we want to restrict our search to a collection.media directory because the file might be cached somewhere else. For example Awesome TTS caches a copy of generated or downloaded files. Here, we can pipe our mdfind results to grep:

mdfind -name $fn | grep -E 'collection[^\0]media'

Putting it all together, this is script we’ll use in the KM macro:

#!/bin/bash

fn=$(pbpaste | sed -E 's/\[.*:(.*)\]/\1/g')
# open with Audacity but only if file is found in 
# a path with collection.media
open -a /Applications/Audacity.app \
     "$(mdfind -name $fn | grep -E 'collection[^\0]media')"

Now we just just have to make sure that the field contents are on the clipboard for pbpaste to work. In KM, we will just add Select All and Copy actions (⌘A and ⌘C.)

Querying the Anki database when the application is running

When the Anki application is open on the desktop, it places a lock on the sqlite3 database such that it can’t be queried by another process. One workaround is to try to open the database and if it fails, then make a temporary copy and query that. Of course, this only works with read-only queries. Here’s the basic strategy:

#!/usr/local/bin/python3
# -*- coding: utf-8 -*-

# requires python >= 3.8 to run because of anki module

from anki import Collection, errors

if __name__ == "__main__":
    try:
        col = Collection(path_to_anki_db)
    except (errors.DBError:
        # anki is open, copy to temp file
        import tempfile
        import shutil
        import os

        with tempfile.TemporaryDirectory() as tmpdir:
            dst = os.path.join(tmpdir, 'collectiontemp.anki2')
            shutil.copy(COLLECTION_PATH, dst)
            col = Collection(dst)
            # do something with Anki db

Note that the tempfile context manager will discard the database, if there are actions on the collection that are common to the Anki-is-open and Anki-is-not-open paths then those should be abstracted to separate function.