/tog

A hackable Emacs based data-tagging framework

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

tog

https://img.shields.io/travis/com/Vernacular-ai/tog/master.svg?style=flat-square

A hackable Emacs based data-tagging framework.

./screen-tagged.png

Installation and Usage

You will need Emacs (not tested on version below 26). Recommended way for installing Emacs Lisp dependencies is to install cask and do cask install here. Once done, open cask emacs -Q ./init.sample.el to check out sample workflows with few of the existing taggers in ./taggers directory.

Additionally, for working with audio, we depend on the following command line tools:

  1. mplayer for playing audios. This can be changed by changing tog-player-command variable.
  2. arecord and sox for recording audio. These are only needed if your tagger uses tog-input-audio functions.

Creating a tagger

Working with and creating a tog tagger needs the user to define:

  1. Data loader for loading and saving data
  2. A tog-item class and few of it’s methods

Data loader

A loader takes care of keeping and navigating around the data items being tagged or to be tagged. It also defines mechanism for saving and loading tags.

At the moment, we have a simple JSON based loader (check out tog-io-json-loader) which assumes the data items are kept as list of dicts (with mandatory id keys) in a JSON file. It dumps tags in a sibling file (adding a .tog to the source file name) as a dictionary mapping item id to tag.

There are plans for supporting better loaders and serialization formats. A closer to completion one is where we stream data bidirectionally for active learning. These will be implemented by extending the classes in tog-io package.

Tog item

An item to tag is defined by creating a class that inherits from tog-item class. The default slots there are id, which uniquely identifies the item, and tag which keeps certain representation of tag for the item, or nil if this item is untagged as of now.

Let’s assume we are tagging intent (multiple intents are allowed) for a given text. The list of possible intents is the following:

  • greeting
  • goodbye

Our item can look like this:

(defclass tog-text-item (tog-item)
  ((text :initarg :text)))

Also let’s setup the list of intents somewhere

(defcustom tog-text-intents '("greeting" "goodbye") "Example intents")

Next, the following functions/methods need to be defined:

Factory function

A factory function creates an object of the above class based on an input data structure decided by the data loader.

For a simple JSON loader, the factory function takes a hash-table and returns the newly created object. For this example, we assume that our data is kept like this in a json:

cat ./test/resources/text-intent-data.json
[
  {"id": 1, "text": "hello world"},
  {"id": 2, "text": "goodbye"}
]

Since the JSON loader reads a list of dictionaries from file as Emacs Lisp hashtables, our factory function can be the following:

(defun make-tog-text-item (table)
  (tog-text-item :id (gethash "id" table)
                 :text (gethash "text" table)))

tog-add-tag

This adds a tag to an item. Here is an example for the current intent tagging case. Assuming that the tag is just a string and each item can get multiple such tags, we can do something like this:

(cl-defmethod tog-add-tag ((obj tog-text-item) tag)
  ;; You might want to handle duplicates here
  (oset obj :tag (cons tag (oref obj :tag))))

tog-render

This defines how the item is going to be rendered in the tog-mode buffer. Since tog-mode inherits from org-mode, you can use various org mode functions. For our simple case, we will just show the item id, text and current tag in separate lines.

;; Note that we are provided a clean buffer with tog mode on
(cl-defmethod tog-render ((obj tog-text-item))
  (insert "id  : " (number-to-string (oref obj :id)) "\n")
  (insert "text: " (oref obj :text) "\n")
  ;; Also showing the currently applied tags
  (insert "tags: " (string-join (oref obj :tag) ", ")))

tog-annotate

This method defines what happens when we start annotating the currently displayed item. In out case, we will just ask for an intent from the user and add to the current item:

(cl-defmethod tog-annotate ((obj tog-text-item))
  (let ((intent (tog-input-choice tog-text-intents)))
    (tog-add-tag obj intent)))

Now we can create a loader from our data file and start tagging:

(setq tog-loader (make-tog-io-json-loader "./test/resources/text-intent-data.json" #'make-tog-text-item))
(tog)

After (wrong) tagging, the tags are saved here:

cat ./test/resources/text-intent-data.json.tog
{"2":["goodbye","greeting"],"1":["greeting"]}

Hooks

  1. tog-nav-hook is called whenever we navigate to any item. This can be useful for setting up things like auto key presses for tagging speed up.
  2. tog-annotate-hook is called after every annotation act.

Keybindings

Important general commands are listed and bound to sensible defaults in ./init.sample.el. Check the file for more details.