1

I want to exactly recreate macOS's built-in Text To Speech keyboard shortcut feature with AppleScript. When I say "exactly," I mean "exactly."

The built-in option can be found in System Preferences → Dictation & Speech → Text to Speech:

screen shot

Here is the description of this feature:

Set a key combination to speak selected text.

Use this key combination to hear your computer speak selected text. If the computer is speaking, press the keys to stop.

The reason that I want to recreate this feature (instead of simply using it) is because it is buggy; sometimes it works, but, other times, I press the keyboard shortcut and nothing happens. If I code it manually in AppleScript, I hope that the process will be more reliable.


I understand how to start and stop Speech in AppleScript, as explained here.

But I would like to use the same keyboard shortcut, and thus the same .scpt file, to both start and stop the Speech, mirroring the functionality of the built-in Speech keyboard shortcut.

I am using FastScripts to run the .scpt file by a keyboard shortcut.

If the same .scpt file is in charge of both starting and stopping the Speech, the script requires an if statement at the top of the AppleScript, or something similar, to immediately check if Speech is currently being spoken or not, before the script can proceed. I do not know how to implement this check, or if it is even possible.

But, here's what I have:

if <This is where I need your help, Ask Different> then
    say "" with stopping current speech
    error number -128 -- quits the AppleScript
end if



-- Back up original clipboard contents:
set savedClipboard to my fetchStorableClipboard()

-- Copy selected text to clipboard:
tell application "System Events" to keystroke "c" using {command down}
delay 1 -- Without this, the clipboard may have stale data.

set theSelectedText to the clipboard

-- Restore original clipboard:
my putOnClipboard:savedClipboard

-- Speak the selected text:
say theSelectedText waiting until completion no





use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"


on fetchStorableClipboard()
    set aMutableArray to current application's NSMutableArray's array() -- used to store contents
    -- get the pasteboard and then its pasteboard items
    set thePasteboard to current application's NSPasteboard's generalPasteboard()
    -- loop through pasteboard items
    repeat with anItem in thePasteboard's pasteboardItems()
        -- make a new pasteboard item to store existing item's stuff
        set newPBItem to current application's NSPasteboardItem's alloc()'s init()
        -- get the types of data stored on the pasteboard item
        set theTypes to anItem's types()
        -- for each type, get the corresponding data and store it all in the new pasteboard item
        repeat with aType in theTypes
            set theData to (anItem's dataForType:aType)'s mutableCopy()
            if theData is not missing value then
                (newPBItem's setData:theData forType:aType)
            end if
        end repeat
        -- add new pasteboard item to array
        (aMutableArray's addObject:newPBItem)
    end repeat
    return aMutableArray
end fetchStorableClipboard


on putOnClipboard:theArray
    -- get pasteboard
    set thePasteboard to current application's NSPasteboard's generalPasteboard()
    -- clear it, then write new contents
    thePasteboard's clearContents()
    thePasteboard's writeObjects:theArray
end putOnClipboard:

(Originally, I had wanted the AppleScript to speak the clipboard, but then I realized that this was overwriting the original clipboard contents. So, I actually want the AppleScript to speak the contents of the theSelectedText variable, as demonstrated in the above code.)

3

It's possible with the say command in a shell, not with the AppleScript say command.

Info for the AppleScript say command:

  • you can stop the speech of say command from the same script until the script run, not after that the script exit.
  • Example:
say "I want to recreate macOS's built-in Text To Speech" waiting until completion no
delay 0.5
say "" with stopping current speech -- this stop the first say command of this script
delay 1
say "Hello"

This script use the say command in a shell to speak the contents of the pbpaste command (the clipboard), and it put the PID of the say command to a persistent property:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
property this_say_Pid : missing value -- the persistent property

if this_say_Pid is not missing value then -- check the pid of all 'say' commands, if exists then quit the unix process
    set allSayPid to {}
    try
        set allSayPid to words of (do shell script "pgrep -x 'say'")
    end try
    if this_say_Pid is in allSayPid then -- the PID = an item in the list
        do shell script "/bin/kill " & this_say_Pid -- quit this process to stop the speech
        error number -128 -- quits the AppleScript
    end if
end if

-- Back up original clipboard contents:
set savedClipboard to my fetchStorableClipboard()

-- Copy selected text to clipboard:
tell application "System Events" to keystroke "c" using {command down}
delay 1 -- Without this, the clipboard may have stale data.

-- Speak the clipboard:
--  pbpaste = the contents of the clipboard , this run the commands without waiting, and get the PID of the 'say' command 
set this_say_Pid to do shell script "LANG=en_US.UTF-8 pbpaste -Prefer txt | say > /dev/null 2>&1 & echo $!"

-- Restore original clipboard:
my putOnClipboard:savedClipboard

on fetchStorableClipboard()
    set aMutableArray to current application's NSMutableArray's array() -- used to store contents
    -- get the pasteboard and then its pasteboard items
    set thePasteboard to current application's NSPasteboard's generalPasteboard()
    -- loop through pasteboard items
    repeat with anItem in thePasteboard's pasteboardItems()
        -- make a new pasteboard item to store existing item's stuff
        set newPBItem to current application's NSPasteboardItem's alloc()'s init()
        -- get the types of data stored on the pasteboard item
        set theTypes to anItem's types()
        -- for each type, get the corresponding data and store it all in the new pasteboard item
        repeat with aType in theTypes
            set theData to (anItem's dataForType:aType)'s mutableCopy()
            if theData is not missing value then
                (newPBItem's setData:theData forType:aType)
            end if
        end repeat
        -- add new pasteboard item to array
        (aMutableArray's addObject:newPBItem)
    end repeat
    return aMutableArray
end fetchStorableClipboard


on putOnClipboard:theArray
    -- get pasteboard
    set thePasteboard to current application's NSPasteboard's generalPasteboard()
    -- clear it, then write new contents
    thePasteboard's clearContents()
    thePasteboard's writeObjects:theArray
end putOnClipboard:

It's possible that the first script will not work, if the value of this_say_Pid variable doesn't persist across runs, it depends how the script will be launched. In that case, you must write the PID to a file, so use this script:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

set tFile to POSIX path of (path to temporary items as text) & "_the_Pid_of_say_command_of_this_script.txt" -- the temp file
set this_say_Pid to missing value
try
    set this_say_Pid to paragraph 1 of (read tFile) -- get the pid of the last speech
end try

if this_say_Pid is not in {"", missing value} then -- check the pid of all 'say' commands, if exists then quit the unix process
    set allSayPid to {}
    try
        set allSayPid to words of (do shell script "pgrep -x 'say'")
    end try
    if this_say_Pid is in allSayPid then -- the PID = an item in the list
        do shell script "/bin/kill " & this_say_Pid -- quit this process to stop the speech
        error number -128 -- quits the AppleScript
    end if
end if

-- Back up original clipboard contents:
set savedClipboard to my fetchStorableClipboard()

-- Copy selected text to clipboard:
tell application "System Events" to keystroke "c" using {command down}
delay 1 -- Without this, the clipboard may have stale data.

-- Speak the clipboard:

--  pbpaste = the contents of the clipboard , this run the commands without waiting, and it write the PID of the 'say' command to the temp file
do shell script "LANG=en_US.UTF-8 pbpaste -Prefer txt | say > /dev/null 2>&1 & echo $! > " & quoted form of tFile

-- Restore original clipboard:
my putOnClipboard:savedClipboard

-- *** Important *** : This script is not complete,  you must add the 'putOnClipboard:' handler and the 'fetchStorableClipboard()' handler to this script.
  • Just wanted to add, there actually is a way to tell if the (Apple)Script Editor or an AppleScript app, etc. is running the AppleScript say command. Taking readings with ps and lsof before, during and after, I was able to isolate a pattern, using diff, of files that come into play that are linked to the calling process and could be coded to test for. However I'd probably opt for your method as it's just to easy use the method you've proposed or to use pgrep say to get its PID. – user3439894 Feb 11 '17 at 23:41
  • @jackjr300 I added some important details to my question and I realized that I actually do not want to use the clipboard directly as the speak-text source, since then the original clipboard contents may be lost; please see my edit. But anyway...your first solution did not halt the currently speaking text and instead simply triggered the Speech command again, so that they were overlapping; the two speeches happen simultaneously. The second solution behaves the same way, where, e.g., 5 speeches all speak at the same time if I run the .scpt file 5 times in a row. Does it work correctly for you? – rubik's sphere Feb 12 '17 at 6:52
  • @rubik's sphere, The script copied and pasted from this pages doesn't work because the result of the command do shell script "/bin/ps .... contains a space character before the pid, while the result of my saved script is the PID only. Anyway the script is changed according to your need. – jackjr300 Feb 12 '17 at 16:07
  • @user3439894 Yes, I will use the pgrep command now, thanks. – jackjr300 Feb 12 '17 at 16:08
  • @jackjr300 (1/2) I appreciate it. We're getting closer, but there's still a bug in your code. This is true of both the first and second solutions. When I first run your code (either the first or the second solution), the selected text is spoken. Great. Then, I immediately run the code for a second time and the speaking is stopped. Great. But, if I run the script a third time, instead of the selected text being spoken, the clipboard text is spoken (as in, the original clipboard contents before the selected text is copied to the clipboard). But, if I wait exactly 6 minutes before – rubik's sphere Feb 13 '17 at 1:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .