From mboxrd@z Thu Jan 1 00:00:00 1970 From: Ross Patterson Subject: Re: feature request: a basic conversation manager Date: Sat, 29 Nov 2008 10:22:24 -0800 Message-ID: <873ahajstb.fsf@transitory.lefae.org> References: <20524da70811261844o3f47782ay3437fdfdc55bda95@mail.gmail.com> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Return-path: Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id 1L6USq-0002e2-TD for emacs-orgmode@gnu.org; Sat, 29 Nov 2008 13:22:44 -0500 Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id 1L6USq-0002dL-7c for emacs-orgmode@gnu.org; Sat, 29 Nov 2008 13:22:44 -0500 Received: from [199.232.76.173] (port=53128 helo=monty-python.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1L6USp-0002dH-QS for emacs-orgmode@gnu.org; Sat, 29 Nov 2008 13:22:43 -0500 Received: from main.gmane.org ([80.91.229.2]:50426 helo=ciao.gmane.org) by monty-python.gnu.org with esmtps (TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.60) (envelope-from ) id 1L6USp-0006Ua-14 for emacs-orgmode@gnu.org; Sat, 29 Nov 2008 13:22:43 -0500 Received: from list by ciao.gmane.org with local (Exim 4.43) id 1L6USi-0002ld-9l for emacs-orgmode@gnu.org; Sat, 29 Nov 2008 18:22:36 +0000 Received: from dsl093-183-048.sfo4.dsl.speakeasy.net ([66.93.183.48]) by main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Sat, 29 Nov 2008 18:22:36 +0000 Received: from me by dsl093-183-048.sfo4.dsl.speakeasy.net with local (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Sat, 29 Nov 2008 18:22:36 +0000 List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Sender: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org Errors-To: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org To: emacs-orgmode@gnu.org --=-=-= I've had this post flagged since it first appeared and I've been intending to read it and comment on it fully. Since I appear not to be getting around to it, I thought I'd make a brief post for now. I haven't read your proposal fully but I've implemented some code that might be relevant or a related cousin. I use my cell phone's voice memo capability as a part of my GTD inbox collection points. I record brief voice memos when something occurs to me, they are saved to my phone's microSD card. Later I export the voice memos to a special "in" folder on my laptop. Then I review the memos, refile them next to whatever org-mode file they apply to, create any new headlines/TODOs if appropriate, and transcribe them into special notes on the headline. To make all this very fast and efficient, I've written a library called transcribe.el. I've attached it. I start by populating the bongo playlist buffer with all the memo files from my in folder (i f "~/in/phone/sounds/*.qcp") and I play the first file. The transcribe.el library provides a global key binding to a command for moving the currently playing file. I use that keystroke once I've heard enough of the memo to refile it to an appropriate location. As such, the contents of the "in" folder continually represent the memos that have not yet been processed even if I'm interrupted. I create any appropriate headlines/TODOs for the memo. Then I use the org-add-transcription command bound to "C-c v z" to add a special kind of note to the headline/TODO. The note is pre-populated with a link to the memo file and a timestamp for the time the memo was taken if supported (see below). I transcribe the memo using a global key binding for bongo-seek, "C-c v s", as necessary. When I'm done, I save the note and use bong-seek again to advance to the next memo. Then I repeat this "move, add headlines, transcribe note" cycle until I'm done. With this approach, I can process my voice memos moving freely around my org-mode buffers as appropriate and without having to switch to any bongo buffers, and doing everything from key bindings. As such, the only context switches I have to do are directly related to the contexts of the voice memos themselves. I find it works quite well for me. The memo's are *.qcp files in Qualcomm's PureVoice format. The transcribe.el library includes a bongo backend to play the PureVoice filed using Qualcomm's pvconv converter: http://www.qctconnect.com/products/purevoice_downloads.html The backend converts the files to *.wav files next to the original *.qcp files and plays the *.wav files. The pvconv converter is pretty fast, but even so long *.qcp recordings can take a couple seconds to convert before bongo can start playing the file. If someone wants to work out how to convert the *.qcp file asynchronously so that bongo can start playing the *.wav before pvconv is finished, that would be great. The *.qcp files are so much smaller than the converted *.wav files, so the backend deletes the *.wav file once it stops playing the file. Phones using Qualcomm's PureVoice memos embed a timestamp into the filename of the memo. Currently transcribe.el can extract this timestamp for use in the transcription note. I'd be interested in contributions for extracting timestamps from voice memos that do it differently. I'd like to hear any thoughts on this, if this can/should be integrated with your concept of a conversation manager, or even independent of that. I also hope to review your proposal more thoroughly in the near future. Ross --=-=-= Content-Type: application/emacs-lisp Content-Disposition: attachment; filename=transcribe.el Content-Transfer-Encoding: quoted-printable (require 'bongo) (require 'org) ;;; customization (defgroup transcribe nil "Transcribe voice memo recordings" :group 'outlines :group 'hypermedia :group 'bongo :group 'calendar) ;;; QuallComm PureVoice (defcustom qcp-date-regexp (concat "^\\([[:digit:]]\\{2\\}\\)" ;; month "\\([[:digit:]]\\{2\\}\\)" ;; day "\\([[:digit:]]\\{2\\}\\)" ;; year "\\([[:digit:]]\\{2\\}\\)" ;; hour "\\([[:digit:]]\\{2\\}\\)" ;; minute "[[:ascii:]]?$") "Regexp used to identify and extract date and time stamp from the base name of a filename of a QualComm PureVoice memo." :type 'regexp :group 'transcribe) (defun qcp-date-to-time (file-name) (let ((name (file-name-sans-extension (file-name-nondirectory file-name)))) (if (string-match qcp-date-regexp name) (encode-time 0 (string-to-number (match-string 5 name)) (string-to-number (match-string 4 name)) (string-to-number (match-string 2 name)) (string-to-number (match-string 1 name)) (+ (* (/ (nth 5 (decode-time)) 1000) 1000) (string-to-number (match-string 3 name))))))) (defun qcp-current-track-time () (with-bongo-playlist-buffer (if (marker-position bongo-current-track-marker) (qcp-date-to-time (bongo-line-file-name bongo-current-track-marker))))) (defun qcp-org-current-time-stamp () (with-bongo-playlist-buffer (if (numberp (marker-position bongo-current-track-marker)) (format-time-string (org-time-stamp-format 'long 'inactive) (qcp-current-track-time))))) (defun qcp-insert-current-time-stamp () (interactive) (org-insert-time-stamp (qcp-current-track-time) t t)) (defun pvconv (infile) "Use pvconv to convert INFILE to a file next to infile. The absolute path to the converted file is returned." (call-process "pvconv" nil "*pvconv*" nil infile) (concat (file-name-sans-extension infile) (if (equal (file-name-extension infile t) ".qcp") ".wav" ".qcp"))) (define-bongo-backend qcp :pretty-name "PureVoice" :constructor 'bongo-start-qcp-player :matcher '(local-file "qcp")) (defun pvconv-cleanup-player (player) "Remove the file converted by pvconv. Intended for use as a bongo player hook such as bongo-player-stopped-functions." (if (bongo-player-get player 'pvconv-file-name) (let ((file-name (bongo-player-get player 'file-name))) (if (file-regular-p file-name) (delete-file file-name))))) (add-hook 'bongo-player-stopped-functions 'pvconv-cleanup-player) (defun bongo-start-qcp-player (file-name &optional extra-arguments) (let* ((wav-file-name (pvconv file-name)) (player (bongo-play-file wav-file-name))) (bongo-player-put player 'pvconv-file-name file-name) player)) ;;; Bongo (defun transcribe/bongo-current-file () (with-bongo-playlist-buffer (and (marker-position bongo-current-track-marker) (bongo-local-file-track-line-p bongo-current-track-marker) (bongo-line-file-name bongo-current-track-marker)))) (defun transcribe/bongo-rename-line () "Call bongo-rename-line on the line for the current track. Use read-file-name if the line is a local file for a better input interface. The track is replayed after moving." (interactive) (with-bongo-playlist-buffer (if (bongo-local-file-track-line-p) (let* ((file (bongo-line-file-name bongo-current-track-marker)) (dir (file-name-directory file)) (base (file-name-nondirectory file)) (new (read-file-name (format "Move %s to: " file) dir base)) (new (if (file-directory-p new) (expand-file-name base new) new))) (bongo-rename-local-file-track new bongo-current-track-marker)) (call-interactively 'bongo-rename-line)) (bongo-play-previous))) (global-set-key "\C-cvr" 'transcribe/bongo-rename-line) (global-set-key "\C-cvs" 'bongo-seek) ;;; Org-mode ;; Modify the org-log-note-headings customize variable to include our ;; values (defcustom org-log-note-headings '((done . "CLOSING NOTE %t") (state . "State %-12s %t") (note . "Note taken on %t") (clock-out . "") (transcribe . "[[file://%f][Recorded]] on %r")) "Headings for notes added to entries. The value is an alist, with the car being a symbol indicating the note context, and the cdr is the heading to be used. The heading may also be the empty string. %t in the heading will be replaced by a time stamp. %s will be replaced by the new TODO state, in double quotes. %u will be replaced by the user name. %U will be replaced by the full user name. %r will be replaced by the recording date. %f will be replaced by the recording file." :group 'org-todo :group 'org-progress :type '(list :greedy t (cons (const :tag "Heading when closing an item" done) string) (cons (const :tag "Heading when changing todo state (todo sequence only)" state) string) (cons (const :tag "Heading when just taking a note" note) string) (cons (const :tag "Heading when clocking out" clock-out) string) (cons (const :tag "Heading when transcribing" note) string))) ;; Add support for transcription notes to the org-add-log-note ;; function (defun org-add-log-note (&optional purpose) "Pop up a window for taking a note, and add this note later at point." (remove-hook 'post-command-hook 'org-add-log-note) (setq org-log-note-window-configuration (current-window-configuration)) (delete-other-windows) (move-marker org-log-note-return-to (point)) (switch-to-buffer (marker-buffer org-log-note-marker)) (goto-char org-log-note-marker) (org-switch-to-buffer-other-window "*Org Note*") (erase-buffer) (if (memq org-log-note-how '(time state)) (org-store-log-note) (let ((org-inhibit-startup t)) (org-mode)) (insert (format "# Insert note for %s. # Finish with C-c C-c, or cancel with C-c C-k.\n\n" (cond ((eq org-log-note-purpose 'clock-out) "stopped clock") ((eq org-log-note-purpose 'done) "closed todo item") ((eq org-log-note-purpose 'state) (format "state change to \"%s\"" org-log-note-state)) ((memq org-log-note-purpose '(note transcribe)) "this entry") (t (error "This should not happen"))))) (org-set-local 'org-finish-function 'org-store-log-note))) (defun org-store-log-note () "Finish taking a log note, and insert it to where it belongs." (let ((txt (buffer-string)) (note (cdr (assq org-log-note-purpose org-log-note-headings))) lines ind) (kill-buffer (current-buffer)) (while (string-match "\\`#.*\n[ \t\n]*" txt) (setq txt (replace-match "" t t txt))) (if (string-match "\\s-+\\'" txt) (setq txt (replace-match "" t t txt))) (setq lines (org-split-string txt "\n")) (when (and note (string-match "\\S-" note)) (setq note (org-replace-escapes note (list (cons "%u" (user-login-name)) (cons "%U" user-full-name) (cons "%t" (format-time-string (org-time-stamp-format 'long 'inactive) (current-time))) (cons "%s" (if org-log-note-state (concat "\"" org-log-note-state "\"") "")) (cons "%r" (qcp-org-current-time-stamp)) (cons "%f" (transcribe/bongo-current-file))))) (if lines (setq note (concat note " \\\\"))) (push note lines)) (when (or current-prefix-arg org-note-abort) (setq lines nil)) (when lines (save-excursion (set-buffer (marker-buffer org-log-note-marker)) (save-excursion (goto-char org-log-note-marker) (move-marker org-log-note-marker nil) (end-of-line 1) (if (not (bolp)) (let ((inhibit-read-only t)) (insert "\n"))) (indent-relative nil) (insert "- " (pop lines)) (org-indent-line-function) (beginning-of-line 1) (looking-at "[ \t]*") (setq ind (concat (match-string 0) " ")) (end-of-line 1) (while lines (insert "\n" ind (pop lines))))))) (set-window-configuration org-log-note-window-configuration) (with-current-buffer (marker-buffer org-log-note-return-to) (goto-char org-log-note-return-to)) (move-marker org-log-note-return-to nil) (and org-log-post-message (message "%s" org-log-post-message))) (defun org-add-transcription () "Add a transcription to the current entry. This is done in the same way as adding a state change note." (interactive) (org-add-log-setup 'transcribe nil t nil)) (global-set-key "\C-cvz" 'org-add-transcription) (defun transcribe-move-and-store-link (&optional file newname) "Move the file or the currently playing bongo file also storing a link to the new location." (interactive) (with-bongo-playlist-buffer (let* ((basename (file-name-nondirectory file)) (basename-wo-ext (file-name-sans-extension basename)) (desc (if (string-match transcribe-file-regexp basename-wo-ext) (format-time-string=20 (concat "["=20 (substring (cdr org-time-stamp-formats) 1 -1)=20 "]")=20 (org-read-date=20 nil t (format "%s-%s-%s %s:%s" (match-string 3 basename-wo-ext) (match-string 1 basename-wo-ext) (match-string 2 basename-wo-ext) (match-string 4 basename-wo-ext) (match-string 5 basename-wo-ext)))) basename-wo-ext))) (setq org-stored-links (cons (list (concat "file://" newname) desc) org-stored-links))))) ;;;; Finish up (provide 'transcribe) --=-=-= "Samuel Wales" writes: > This took months to write, but only to be specific in the > spirit of the "how you can help" discussion. The idea and > feature request are relatively simple. > > To skip the preamble, search for [[here is a solution]]. > > A =conversation manager= is focused on phone conversations, > transcripts, letters, journal entries, etc. A > =conversation= is one interaction or note. > > The idea is to keep a global record of conversations of a > certain kind (e.g. phone calls to insurance companies or > doctors) while also keeping that information easily > accessible in the various org places where it belongs. > > Some history: > > Before I started using org, I kept a record of all medical > conversations in a file. This provided a time-sorted place > to look for conversations. I'll call this a =journal=, > after Carsten's usage in the manual. > > I also had a todo file for data (e.g. phone numbers, people > to talk to about x), unfinished tasks (e.g. get insurance > company 1 to see reason, see doctor 1), etc. This was an > indented plain text file in emacs. I will call the org > equivalent =todo.org=. > > I copied back and forth. > > I want to do better than that with org, because org-mode is > powerful. > > Here are some problems with using todo.org to keep > conversations and notes together: > > 1. The journal doesn't have all conversations; some are > in todo.org unless I only use one consistently. > > 2. todo.org grows and extraneous information is in there. > > 3. The notes are scattered over todo.org. For example, I > might have a call to a doctor, and put that note under > the todo item to call that doctor. But that is bad > when I want all medical phone calls in order. > > 4. I want conversations accessible from more than one > place. For example, if the conversation is under > doctor 1, I also want it under the medical issue and > possibly elsewhere, without duplication. > > 5. The journal doesn't have its entries in order, because > I might add something else later that happened > earlier, if I copy to journal from todo. > > 6. The todo.org notes are out of time order (i.e. the > first conversation in the buffer is not necessarily > the first conversation). > > 7. Except for metadata, conversations should be out of > sight until they need to be looked up. > > Of the many solutions that come to mind, here are a few that > I believe will *not* work: > > 1. Using ordinary links is not a solution, because you > would have to click on each link to see only one > conversation. Also, you couldn't isearch all > conversations at the same time. > > 2. Advising org-log-note to copy the note to the journal > duplicates stuff. That means that grep will find > things in 2 places. Also, it doesn't handle the > question of notes that should be attached to more than > one item. Duplication is a disaster, IMO. > > 3. Keeping the notes scattered in todo.org precludes > access to the journal outside org (e.g. if your > computer crashes and you need to get the journal from > your backups on a computer that does not run emacs), > doesn't handle notes that should be attached to more > than one item, keeps unnecessary stuff there, and > increases the size of the org file. > > Here is a solution that I believe will work: > > - <>. If you are on the doctor 1 > headline in todo.org, you run a command that shows all > conversations with that doctor in a single buffer. > > The conversations are stored only in the journal. A > single place for all medical conversations that is still > accessible from todo.org. > > Here is a design using drawers. See below for a > different design using org-id's that I think will be > better. This one is to illustrate the concept. > > - <>: > > - Each todo.org heading that has conversations gets > a list that is like the CLOCK interval list, > except that it contains links to conversations > (I.e. journal entries). > > todo.org: > > * doctor 1 > :CONVERSATION: > CONVERSATION: [2007-10-27 Sat 13:55] medical-journal.org > CONVERSATION: [2008-12-01 Mon 16:10] medical-journal.org > \:END: > ** phone number is ... > * insurance company 1 > :CONVERSATION: > CONVERSATION: [2007-07-05 Thu 12:00] medical-journal.org > CONVERSATION: [2008-12-01 Mon 16:10] medical-journal.org > CONVERSATION: [2009-12-02 Wed 17:15] medical-journal.org > \:END: > ** talk to soandso > > (Perhaps the links would be actual links.) > > - A command (perhaps c-c c-c) gathers the > conversations into a buffer. > > - To start a new conversation, a command inserts a > link into todo.org and an entry in the journal. > > - The medical journal: > > * [2007-10-27 Sat 13:55] > called mary at doctor 1's office about our appointment. > ... > * [2007-11-05 Mon 16:05] > called doctor 2 about issue 1. nobody was in. > > - The links below the todo.org headline give you an > idea of when you have called doctor 1 without having > to keep the actual conversations in todo.org. > > Here is the other design: > > - A different design, the <>, is > arguably simpler, and I like it better: > > - Every journaled task gets a list of org-id's. > - Each id refers to a conversation in the journal > file. > - Then we gather conversations using org-id's. > > Here are my comments on the org-id design: > > - This is a more general solution. It will work for > more than just conversations. There are interesting > possibilities here. > - For backward links, we do the same in reverse, thus > gathering the todo.org tasks that are related to the > conversation. The code is the same. I'd recommend > doing this by default. > - This design does not show you the list of > timestamps. That is a drawback. > - This might be solved by putting the target > headline over the org-id as an overlay. For > conversations, the target headline is simply the > start timestamp. For future applications, it > can be anything. > > Here are some comments on the solution in general (either > of the designs). > > - Some comments: > - There appear to be no archiving or expiry issues > with this solution. > - Here are 3 possible ways to create the buffer that > contains the collected conversations. > > 1. Create a read-only buffer. > 2. Have it actually be the journal buffer via > folding or possibly via hiding. > 3. Pass editing to the journal buffer a la grep mode > or the org agenda. > > IMO, #2 would be ideal for the user. > - Automatic journaling to another task also > - It would be useful to specify that anything > journaled below a place in the outline hierarchy > should always also be journaled to that place. > - A simple tag, :journal:, would work nicely. > - In "/medical/doctors/doctor 1", talking to doctor > 1 also saves the conversation link to medical if > and only if you put that tag on medical. If you > do not have the tag on anything, then the > conversation will only be connected with doctor 1. > - It might be useful to specify under "doctor 1" that > it and anything below it should always also be > journaled to "insurance company 1". This might > require an org-id for the target, which is another > argument for org-id's, since we would do that anyway > to go backward. > - Of course, if you just want to use org-add-note to > store a regular note, you can. > > For me, this functionality would make org simpler to use. > > Comments? > > I have notes on future possibilities in case there is > interest. > > Thanks. --=-=-= Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ Emacs-orgmode mailing list Remember: use `Reply All' to send replies to the list. Emacs-orgmode@gnu.org http://lists.gnu.org/mailman/listinfo/emacs-orgmode --=-=-=--