I recently started checking my text against the IBM Watson Tone Analyzer. This can be helpful sometimes when you want to make sure that your text conveys a positive message. The last time I did this was with my self-evaluation, which I wrote in a hurry. After running the text through the analyzer, I realized I needed to make some changes to make the text more joyful.
Typically, I write most text (and code) in Emacs, so at some point I wrote an extension that allows me to hightlight a region of text and issue a command to have the text analyzed with the Watson Tone Analyzer.
Before we dive into the Emacs code, however, I thought I'd show you here how simple it is to use curl
to analyze your text on the command line.
First, create a JSON file with your text. You have to follow JSON rules, so replace carriage returns and new-line characters with \n
in your text, and make sure that there are no funny characters like tabs.
Here's what your JSON file should look like:
{
"text": "She drew a circle that shut me out"
}
If you save that file with the name /tmp/circle.json
, you can analyze it like this:
curl -sX POST -u 'apikey:{apikey}' \
--header 'Content-Type: application/json' \
--data-binary '@/tmp/circle.json'
'{tone-analyzer-endpoint}?version=2017-09-21&sentences=false'
You can get the {apikey}
and {tone-analyzer-endpoint}
values by registering at
IBM Watson Tone Analyzer.
The code I wrote for Emacs creates an interactive command called analyze-tone-in-region
that allows you to highlight a region of text, then run M-x analyze-tone-in-region
to get a dialog box with the results from the Watson Tone Analyzer.
The code uses a couple of functions that I wrote a long time ago and that I'm including here as well.
Here are the functions that I wrote specifically to provide the analyze-tone-in-region functionality.
(setq ibm-cloud-apikey "xxx")
(setq ibm-tone-analyzer-endpoint "https://xxx/xxx...")
(setq ibm-tone-analyzer-version "2017-09-21")
(defun analyze-tone-in-file (filename &optional debug)
(let ((command (format "curl -sX POST -u '%s:%s' %s %s '@%s' '%s?version=%s&sentences=false'"
"apikey" ibm-cloud-apikey
"--header 'Content-Type: application/json'"
"--data-binary" filename
ibm-tone-analyzer-endpoint
ibm-tone-analyzer-version)))
(if debug
command
(let ((json-result (shell-command-to-string command)))
(if (or (null json-result) (equal json-result ""))
(list nil json-result)
(let ((lisp (json-to-lisp :string json-result)))
(if (member :error lisp)
(list nil lisp)
(list t lisp))))))))
(defun analyze-tone-in-region (beg end)
(interactive "*r")
(let ((temp-file (make-temp-file "watson-tone-analyzer-" nil ".json"))
(text (map 'string 'identity
(mapcar (lambda (c) (if (or (> c 127) (< c 10)) 32 c))
(replace-regexps-in-string
(buffer-substring beg end)
"\"" "\\\\\"" "\n" "\\\\n")))))
(with-current-buffer (generate-new-buffer temp-file)
(insert (format "{\"text\":\"%s\"}" text))
(write-file temp-file)
(kill-buffer))
(let ((analysis (analyze-tone-in-file temp-file)))
(message-box
(if (car analysis)
(loop with h = (make-hash-table :test 'equal)
for tone across (pick (second analysis)
:path '(:document_tone :tones))
for tone-name = (getf tone :tone_name)
for tone-score = (getf tone :score)
do (puthash tone-name tone-score h)
finally
(return (mapconcat
(lambda (k) (format "%-20s %0.3f" k (gethash k h)))
(sort (hash-keys h)
(lambda (a b) (>= (gethash a h) (gethash b h))))
"\n")))
(format "%s\n%s" "API call failed" (second analysis)))))))
You'll need to replace the values for ibm-cloud-apikey
and ibm-tone-analyzer-endpoint
in the above code. As mentioned earlier, you can obtain those values by registering at IBM Watson Tone Analyzer.
The above code uses functions that I wrote a long time ago and that are not part of Emacs or any Emacs package. Those functions are the json-to-lisp
function and the pick
function. Here's the code for those functions:
(cl-defun json-to-lisp (&key buffer string path fields)
"This function converts the given JSON string into a Lisp data
structure and, optionally, returns selected data from that
data structure. You provide a string via the :buffer
or :string parameters, which are mutually exclusive (use one
or the other). The :buffer parameter accepts the name of a
buffer that contains the JSON string. The :string parameter
accepts the string itself.
By default, the function returns a data structure representing
the whole JSON string. However, you can ask for a specific
node of the data structure using the optional :path parameter.
The :path parameter expects the name of a top-level key or a
list of keys that defines the path to a deep node. If you've
provided the :path parameter, and you expect the selected node
to contain an array of similar elements, then you can provide
a value or a list of values to the :fields parameter to select
only the given fields from each element of the array."
(let ((lon (if buffer
(with-current-buffer buffer
(goto-char (point-min))
(re-search-forward "{\\|\\[")
(goto-char (point-at-bol))
(json-read))
(json-read-from-string string))))
(if path (pick lon :path path :fields fields) lon)))
(cl-defun pick (tree &key default path fields)
(let* ((key-list (if (atom path) (list path) path))
(field-list (if (and fields (atom fields))
(list fields)
fields))
(key (pop key-list))
(key-exists (and key (list-key-exists tree key)))
(result (cond ((and key key-exists key-list)
(pick (fetch-list-value tree key)
:default default
:path key-list
:fields field-list))
((and key key-exists)
(fetch-list-value tree key))
((null key) tree)
(t default))))
(if field-list
(if (or (arrayp (elt result 0)) (listp (elt result 0)))
(mapcar
(λ (x) (loop for field in field-list
appending (list field (getf x field))))
result)
(loop for field in field-list
appending (list field (getf result field))))
result)))
The json-to-lisp
function is similar to the json-read-from-string
function that comes with Emacs. It converts a JSON string into a nested Lisp data structure. The pick
function allows you to select a subtree or value from a nested Lisp data structure.
Here are example screenshots of me using the analyze-tone-in-region
function:
Happy Hacking!