AppleScript: ¿Es posible verificar si Speech se está ejecutando actualmente?

Quiero recrear exactamente la función de método abreviado de teclado Text To Speech integrada de macOS con AppleScript. Cuando digo "exactamente", quiero decir "exactamente".

La opción integrada se puede encontrar en Preferencias del sistema → Dictado y voz → Texto a voz:

captura de pantalla

Aquí está la descripción de esta función:

Establece una combinación de teclas para pronunciar el texto seleccionado.

Use esta combinación de teclas para escuchar a su computadora pronunciar el texto seleccionado. Si la computadora está hablando, presione las teclas para detener.

La razón por la que quiero recrear esta función (en lugar de simplemente usarla) es porque tiene errores; a veces funciona, pero, otras veces, presiono el atajo de teclado y no pasa nada. Si lo codifico manualmente en AppleScript, espero que el proceso sea más confiable.


Entiendo cómo iniciar y detener Speech en AppleScript, como se explica aquí .

Pero me gustaría usar el mismo método abreviado de teclado y, por lo tanto, el mismo archivo .scpt, tanto para iniciar como para detener Speech, reflejando la funcionalidad del método abreviado de teclado de Speech integrado.

Estoy usando FastScripts para ejecutar el archivo .scpt mediante un método abreviado de teclado.

Si el mismo archivo .scpt está a cargo de iniciar y detener Speech, la secuencia de comandos requiere una declaración if en la parte superior de AppleScript, o algo similar, para verificar de inmediato si Speech se está hablando actualmente o no, antes de que la secuencia de comandos pueda. proceder. No sé cómo implementar este control, o incluso si es posible.

Pero, esto es lo que tengo:

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:

(Originalmente, quería que AppleScript hablara the clipboard, pero luego me di cuenta de que esto estaba sobrescribiendo el contenido original del portapapeles. Entonces, en realidad quiero que AppleScript hable el contenido de la theSelectedTextvariable, como se demuestra en el código anterior).

Respuestas (1)

Es posible con el saycomando en un shell, no con el saycomando AppleScript.

Información para el comando say de AppleScript:

  • puede detener el discurso del comando decir desde el mismo script hasta que se ejecute, no después de que el script salga.
  • Ejemplo:
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"

Este script usa el saycomando en un shell para pronunciar el contenido del pbpastecomando (el portapapeles) y coloca el PID del saycomando en una propiedad persistente:

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:

Es posible que el primer script no funcione, si el valor de la variable this_say_Pid no persiste en las ejecuciones, depende de cómo se inicie el script. En ese caso, debe escribir el PID en un archivo, así que use este 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.
Solo quería agregar que, en realidad, hay una manera de saber si el editor de secuencias de comandos (Apple) o una aplicación AppleScript, etc. está ejecutando el say comando AppleScript . Tomando lecturas con psy lsofantes, durante y después, pude aislar un patrón, usando diff, de archivos que entran en juego que están vinculados al proceso de llamada y podrían codificarse para probar. Sin embargo, probablemente optaría por su método, ya que es solo para usar fácilmente el método que ha propuesto o para usar pgrep saypara obtener su archivo PID.
@ jackjr300 Agregué algunos detalles importantes a mi pregunta y me di cuenta de que en realidad no quiero usar el portapapeles directamente como fuente de texto hablado, ya que entonces el contenido original del portapapeles puede perderse; por favor vea mi edición. Pero de todos modos... su primera solución no detuvo el texto que se estaba hablando en ese momento y en su lugar simplemente activó el comando Voz nuevamente, para que se superpusieran; los dos discursos suceden simultáneamente. La segunda solución se comporta de la misma manera, donde, por ejemplo, 5 discursos hablan todos al mismo tiempo si ejecuto el archivo .scpt 5 veces seguidas. ¿Te funciona correctamente?
Esfera de @rubik, el script copiado y pegado de estas páginas no funciona porque el resultado del comando do shell script "/bin/ps ....contiene un carácter de espacio antes del pid, mientras que el resultado de mi script guardado es solo el PID. De todos modos, el guión se cambia de acuerdo a su necesidad.
@user3439894 Sí, usaré el pgrepcomando ahora, gracias.
@ jackjr300 (1/2) Lo aprecio. Nos estamos acercando, pero todavía hay un error en su código. Esto es cierto tanto para la primera como para la segunda solución. Cuando ejecuto su código por primera vez (ya sea la primera o la segunda solución), se pronuncia el texto seleccionado. Excelente. Luego, inmediatamente ejecuto el código por segunda vez y se detiene el habla. Excelente. Pero, si ejecuto el script por tercera vez, en lugar de que se pronuncie el texto seleccionado, se pronunciará el texto del portapapeles (como en el contenido original del portapapeles antes de que el texto seleccionado se copie en el portapapeles). Pero, si espero exactamente 6 minutos antes
(2/2) ejecutando el código nuevamente, el script funciona correctamente para una ejecución (es decir, el script habla el texto seleccionado en lugar del texto del portapapeles), pero luego el ciclo se repite, donde tengo que esperar otros 6 minutos si desea que la secuencia de comandos hable el texto seleccionado en lugar del texto original del portapapeles. Además, no sé si se trata de un problema aparte o si está relacionado con el error mencionado anteriormente, pero ocasionalmente tengo que ejecutar el script dos veces para detener un discurso actual; ejecutar el script durante un discurso no hace nada, así que lo ejecuto de nuevo y eso detiene el discurso.
Probé con la utilidad 'FastScripts' (utilizo un atajo de teclado para ejecutar el script), y no tengo este problema en mi máquina (incluso en una gran selección). Asegúrese de que el acceso directo keystroke "c" using {command down}funcione desde la aplicación más frontal, si escucha un pitido (la pulsación de tecla no funcionó). Intenta aumentar el tiempo en segundos del delaycomando. Intente agregar un delaycomando antes de esta línea my putOnClipboard:savedClipboard. Intente agregar un delaycomando antes de esta línea tell application "System Events" to keystroke "c" using {command down}.
@ jackjr300 Ahora, solo para simplificar el proceso de depuración para mí, ¿se refiere a su primer bloque de código o a su segundo bloque de código cuando dice que funciona correctamente para usted con un atajo de teclado asignado a la secuencia de comandos en FastScripts?
Esfera de @rubik, uso mi primer bloque de código en macOS Sierra V10.12.3
@ jackjr300 (1/2) He encontrado el problema. Pido disculpas por fijar el error en su código. Como puede notar en mi publicación original, he estado usando la tecla F8 para activar el script. Dado que F8 es una tecla de función de la fila superior (configurada como un control fijo de reproducción/pausa para iTunes), descargué un software de terceros titulado FunctionFlip para liberar mi tecla F8. Bueno, FunctionFlip ha sido responsable de todos los problemas de capacidad de respuesta que describí. Dejé de usar FunctionFlip y ahora uso Karabiner para liberar teclas . Estoy usando tu primer bloque
(2/2) de código y funciona perfectamente. El atajo de teclado y el script son 100% confiables. Incluso cambié tu delay 1línea a delay 0.1, y todavía funciona perfectamente. Luego comparé el tiempo de respuesta de su secuencia de comandos con el tiempo de respuesta del método abreviado de teclado de texto a voz incorporado, y su secuencia de comandos es en realidad un poco más rápida, tanto en términos de iniciar el discurso como de detenerlo. ¡Gracias por tu ayuda! Solo agregaré una verificación a su código para que, si el texto seleccionado está vacío cuando se ejecuta el script, el código no diga el texto del portapapeles, sino que cierre el script.