From e42e761dca58557799084ceafdf2088d85fe40c5 Mon Sep 17 00:00:00 2001 From: Sacha Chua Date: Tue, 25 Oct 2022 11:13:23 -0400 Subject: Caption daemon --- README.org | 2 + group_vars/all.yml | 1 + inventory.yml | 1 + roles/caption/defaults/main.yml | 2 + roles/caption/tasks/main.yml | 60 ++++++++++++++--- roles/caption/templates/captions.init.d | 78 ++++++++++++++++++++++ .../caption/templates/inotify-process-captions.sh | 15 +++++ roles/caption/templates/process-captions.py | 46 +++++++------ 8 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 roles/caption/defaults/main.yml create mode 100755 roles/caption/templates/captions.init.d create mode 100755 roles/caption/templates/inotify-process-captions.sh diff --git a/README.org b/README.org index 9b90e7b..d5df01d 100644 --- a/README.org +++ b/README.org @@ -163,3 +163,5 @@ ansible-playbook -i inventory.yml prod-playbook.yml --tags test-stream-pattern - Set up whisper ansible-playbook -i inventory.yml prod-playbook.yml --tags caption + +ffmpeg -y -i handwritten/reencode.webm -t 60 -vcodec copy -acodec copy test.webm diff --git a/group_vars/all.yml b/group_vars/all.yml index bc8f39a..8f86f46 100644 --- a/group_vars/all.yml +++ b/group_vars/all.yml @@ -1,4 +1,5 @@ docker: false +emacsconf_user: orga emacsconf_tracks: - name: General id: gen diff --git a/inventory.yml b/inventory.yml index 6608342..997e01f 100644 --- a/inventory.yml +++ b/inventory.yml @@ -4,6 +4,7 @@ prod: ansible_host: res.emacsconf.org ansible_python_interpreter: /usr/bin/python3 ansible_become: true + emacsconf_group: org front: ansible_host: front0.emacsconf.org remote_user: orga diff --git a/roles/caption/defaults/main.yml b/roles/caption/defaults/main.yml new file mode 100644 index 0000000..c158118 --- /dev/null +++ b/roles/caption/defaults/main.yml @@ -0,0 +1,2 @@ +emacsconf_caption_dir: /data/emacsconf/{{ emacsconf_year }} + diff --git a/roles/caption/tasks/main.yml b/roles/caption/tasks/main.yml index 6396339..7fe1570 100644 --- a/roles/caption/tasks/main.yml +++ b/roles/caption/tasks/main.yml @@ -5,10 +5,11 @@ - ffmpeg - cmake - jq + - inotify-tools - name: Install whisper ansible.builtin.pip: name: git+https://github.com/openai/whisper.git -- name: Install lhotse +- name: Install Python packages ansible.builtin.pip: name: - lhotse @@ -16,18 +17,57 @@ - tqdm - torchaudio - num2words -- name: Copy the shell script - tags: process-captions +- name: Create group + group: + name: "{{ emacsconf_group }}" + state: present +- name: Create user + user: + name: "{{ emacsconf_user }}" + group: "{{ emacsconf_group }}" + state: present +- name: Ensure the directory exists + file: + path: "{{ emacsconf_caption_dir }}" + state: directory +- name: Copy the script for processing the files + tags: process-captions, wip template: src: process-captions.py - dest: /data/emacsconf/{{ emacsconf_year }}/process-captions.py - mode: 0755 - owner: sachac - group: org + dest: "{{ emacsconf_caption_dir }}/process-captions.py" + mode: 0775 +- name: Copy the inotify script + tags: process-captions + template: + src: inotify-process-captions.sh + dest: "{{ emacsconf_caption_dir }}/inotify-process-captions.sh" + mode: 0775 - name: Copy talks.json tags: talks-json template: src: talks.json - dest: /data/emacsconf/{{ emacsconf_year }}/talks.json - owner: sachac - group: org + dest: "{{ emacsconf_caption_dir }}/talks.json" + mode: 0664 +- name: Install init.d configuration + tags: system + become: true + template: + src: captions.init.d + dest: /etc/init.d/captions + owner: root + group: root + mode: 0755 +- name: Change the group for all the files + tags: wip + file: + dest: "{{ emacsconf_caption_dir }}" + group: "{{ emacsconf_group }}" + mode: "g+rwX" + recurse: true +- name: Restart caption monitoring service + become: true + tags: wip + service: + name: captions + enabled: true + state: started diff --git a/roles/caption/templates/captions.init.d b/roles/caption/templates/captions.init.d new file mode 100755 index 0000000..5e1c9f2 --- /dev/null +++ b/roles/caption/templates/captions.init.d @@ -0,0 +1,78 @@ +#!/bin/sh +# {{ ansible_managed }} + +### BEGIN INIT INFO +# Provides: captions +# Required-Start: $local_fs $remote_fs $network $syslog +# Required-Stop: $local_fs $remote_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: starts captioning monitor +# Description: starts captioning monitor using start-stop-daemon +### END INIT INFO + +PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin" +USER="{{ emacsconf_user }}" +GROUP="{{ emacsconf_group }}" +DESC="EmacsConf captions" +NAME="captions" +WORKDIR="{{ emacsconf_caption_dir }}" + +set -e + +. /lib/lsb/init-functions + +start() { + echo "Starting $DESC... " + + start-stop-daemon --start --chuid "$USER:$GROUP" -d $WORKDIR --background --make-pidfile --pidfile /var/run/$NAME.pid --exec "$WORKDIR/inotify-process-captions.sh > /var/log/$NAME.log" || true + echo "done" +} + +#We need this function to ensure the whole process tree will be killed +killtree() { + local _pid=$1 + local _sig=${2-TERM} + for _child in $(ps -o pid --no-headers --ppid ${_pid}); do + killtree ${_child} ${_sig} + done + kill -${_sig} ${_pid} +} + +stop() { + echo "Stopping $DESC... " + if test -f /var/run/$NAME.pid; then + while test -d /proc/$(cat /var/run/$NAME.pid); do + killtree $(cat /var/run/$NAME.pid) 15 + sleep 0.5 + done + rm /var/run/$NAME.pid + fi + echo "done" +} + +status() { + status_of_proc -p /var/run/$NAME.pid "" "$NAME" && exit 0 || exit $? +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + status) + status + ;; + *) + echo "Usage: $NAME {start|stop|restart|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/roles/caption/templates/inotify-process-captions.sh b/roles/caption/templates/inotify-process-captions.sh new file mode 100755 index 0000000..ce5f416 --- /dev/null +++ b/roles/caption/templates/inotify-process-captions.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# {{ ansible_managed }} + +# This script waits for new webm and mov files +# in the current directory (recursively) and then calls +# process-captions to process all the new files that need work. +inotifywait -r -m "{{ emacsconf_caption_dir }}" -e create -e moved_to | + while read directory action file; do + if [[ "$file" =~ .*(webm|mov)$ ]]; then + "{{ emacsconf_caption_dir }}"/process-captions.py | tee -a "{{ emacsconf_caption_dir }}/captions.log" + elif [[ "$file" =~ .*(vtt|srv2|ogg|opus)$ ]]; then + # Copy to backstage area + rsync --ignore-existing $directory/$file orga@media.emacsconf.org:/var/www/media.emacsconf.org/{{ emacsconf_year }}/backstage/ + fi + done diff --git a/roles/caption/templates/process-captions.py b/roles/caption/templates/process-captions.py index 6ad890a..72e9ad2 100755 --- a/roles/caption/templates/process-captions.py +++ b/roles/caption/templates/process-captions.py @@ -46,7 +46,8 @@ AUDIO_REGEXP = '\.(ogg|opus)$' ALWAYS = False TRIM_AUDIO = False MODEL = os.environ.get('MODEL', 'large') # Set to tiny for testing -JSON_FILE = '/data/emacsconf/2022/talks.json' +WORK_DIR = "{{ emacsconf_caption_dir }}" +JSON_FILE = os.path.join(WORK_DIR, 'talks.json') def get_slug_from_filename(filename): m = re.search('emacsconf-[0-9]+-([a-z]+)--', filename) @@ -100,14 +101,14 @@ def get_files_to_work_on(directory): return needs_work def extract_audio(work): - output = subprocess.check_output(['ffprobe', video_file], stderr=subprocess.STDOUT) + output = subprocess.check_output(['ffprobe', work['video']], stderr=subprocess.STDOUT) extension = 'opus' if 'Audio: vorbis' in output.decode(): extension = 'ogg' - new_file = os.path.join(os.path.dirname(video_file), base_name(video_file) + '.' + extension) - acodec = 'copy' if re.search('webm$', video_file) else 'libopus' - log("Extracting audio from %s acodec %s" % (video_file, acodec)) - output = subprocess.check_output(['ffmpeg', '-y', '-i', video_file, '-acodec', acodec, '-vn', new_file], stderr=subprocess.STDOUT) + new_file = work['base'] + '.' + extension + acodec = 'copy' if re.search('webm$', work['video']) else 'libopus' + log("Extracting audio from %s acodec %s" % (work['video'], acodec)) + output = subprocess.check_output(['ffmpeg', '-y', '-i', work['video'], '-acodec', acodec, '-vn', new_file], stderr=subprocess.STDOUT) work['audio'] = new_file return work @@ -213,19 +214,22 @@ def base_name(s): # assert(base_name('/home/sachac/current/sqlite/emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--normalized.webm.vtt') == 'emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--main') log(f"MODEL {MODEL} ALWAYS {ALWAYS} TRIM_AUDIO {TRIM_AUDIO}") -directory = sys.argv[1] if len(sys.argv) > 1 else "~/current" +directory = sys.argv[1] if len(sys.argv) > 1 else WORK_DIR + needs_work = get_files_to_work_on(directory) -if THREADS > 0: - torch.set_num_threads(THREADS) -for work in needs_work: - log("Started processing %s" % work['base']) - if work['audio']: - if ALWAYS or not 'vtt' in work: - work = generate_captions(work) - if ALWAYS or not 'srv2' in work: - work = generate_srv2(work) -# print("Aligning words", audio_file, datetime.datetime.now()) -# word_cuts = align_words(cuts) -# convert_cuts_to_word_timing(audio_file, word_cuts) - log("Done %s" % str(work['base'])) - +if len(needs_work) > 0: + if THREADS > 0: + torch.set_num_threads(THREADS) + for work in needs_work: + log("Started processing %s" % work['base']) + if work['audio']: + if ALWAYS or not 'vtt' in work: + work = generate_captions(work) + if ALWAYS or not 'srv2' in work: + work = generate_srv2(work) + # print("Aligning words", audio_file, datetime.datetime.now()) + # word_cuts = align_words(cuts) + # convert_cuts_to_word_timing(audio_file, word_cuts) + log("Done %s" % str(work['base'])) +else: + log("No work needed.") -- cgit v1.2.3