Говорящий пингвин

в 16:51, , рубрики: crontab, festival speech, ruby, Настройка Linux, метки:

На бескрайних просторах интернета немало статей про умные дома, очки дополненной реальности, новомодный «интернет вещей» и другие преспективы вообразимого будущего. Будет ли оно светлым или в «добрых» традициях нуарного киберпанка, время покажет, а пока мы сделаем маленький шаг навстречу ему.

Как вы догадались по заголовку публикации, речь пойдёт о синтезе речи. Я расскажу, как реализовал, c чем столкнулся. Буду не оригинальным, использовал festival.

Начало

Подопытный:

  • Ноутбук HP
  • Звук alsa (позднее поставил pulseaudio)

— Установка, никаких проблем не должно возникать. Пример
Примеры скриптов

Оповещение заряда батареи

Скрипт узнает статус и заряд. Сообщает о заряде, если не подключен к зарядке и заряд < 50%.

#!/bin/bash

scripts="$HOME/.bin/festival/"
charge=$(cat /sys/class/power_supply/BAT0/capacity)
procent=$(${scripts}pluralform.sh $charge процент процента процентов)
status=$(cat /sys/class/power_supply/BAT0/status)
text=$(echo "Заряд батареи $charge $procent")
critical="Критический заряд батареи."

if [ "$status" == 'Discharging' ] && [ $charge -gt 10 -a $charge -lt 50 ];
then
    ${scripts}say.sh "$text"
elif [ "$status" == 'Discharging' ] && [ $charge -lt 10 ];
then
    ${scripts}say.sh "$critical"
fi
exit

Склонение

#!/usr/bin/env bash

n=$(($1 % 100))
n1=$(($n % 10))

if [ $n -gt 10 -a $n -lt 20 ]; then echo $4;
elif [ $n1 -gt 1 -a $n1 -lt 5 ]; then echo $3;
elif [ $n1 -eq 1 ]; then echo $2;
else echo $4
fi

Скрипт генерирует wav c учётом файлов в dat`е. Запоминает текущую громкость, ставит оптимальную. Проигрывает и возвращает громкость обратно. Удаляет текущий wav-файл.

#!/bin/bash

data="$HOME/.bin/festival/data"
ru="(voice_msu_ru_nsh_clunits)"
volume=$(amixer | grep -o [0-9]* | sed "5 ! d")
i=$(ls -r $data | grep -o [0-9]* | sed "1 ! d")

if test -z "$i";
then i=1;
elif test -f "$data/saytext_$i.wav";
then i=$(($i+1));
fi

echo "$1" | text2wave -o $data/saytext_$i.wav -eval $ru > /dev/null 2>&1
amixer set Master 67% > /dev/null 2>&1
aplay $data/saytext_$i.wav > /dev/null 2>&1

aplay=$(ps -el | grep aplay | wc -l)
if [ $aplay -eq 0 ];
then amixer set Master $volume% > /dev/null 2>&1;
fi

rm -f $data/saytext_$i.wav

Проверка почты gmail

Нашёл почти готовое решение gem gmail.

Плюсы этого решения:
— Оповещение о кол-ве сообщений по лейблам. Предварительно нужно создать фильтры с лейблами на сайте.
— Возможность прочитать заголовок сообщения и текст (не реализовывал).

# coding: utf-8
require 'gmail'
require 'yaml'

Gmail.new("login", "password") do |gmail|

  @festival = "$HOME/.bin/festival/"
  @labels = {"INBOX" => "Входящие", "Search job" => "Поиск работы",
             "Music" => "Музыка", "Advertising" => "Реклама",
             "Education" => "Обучение", "Interesting" => "Интересное"}

  def check_letters(gmail)
    counts = {}
    @labels.each do |k,v|
      h = {k => gmail.mailbox(k).count(:unread)}
      counts.merge!(h)
    end
    return counts
  end

  def read_counts_letters
    unless File.exist?(".gmail.yml")
      return {}
    end
    old_counts = YAML::load(File.open(".gmail.yml"))
    return old_counts
  end

  def save_counts_letters(counts)
    system("touch .gmail.yml") if File.exist?(".gmail.yml")
    File.open(".gmail.yml", "w") do |f|
      f.write counts.to_yaml
    end
  end

  def check_new_counts_letters(counts, old_counts)
    counts.each do |k, v|
      if old_counts.empty? || v < old_counts[k]
        count = v
      else
        count = v - old_counts[k]
      end
      say_new_counts(k, count)
    end
  end

  def say_new_counts(k, count)
    unless count == 0
      text = pluralform(count)
      count = "Одн+о" if count == 1
      all = "У вас #{count} #{text}"
      part = "#{count} #{text} в разделе #{@labels[k]}"

      if k == "INBOX"
        system("#{@festival}say.sh '#{all}'")
      else
        system("#{@festival}say.sh '#{part}'")
      end
    end
  end

  def pluralform(count)
    n = count % 100
    m = n % 10
    msg = ['сообщение',
           'сообщения',
           'сообщений']

    if n > 10 && n < 20
      return msg[2]
    elsif m > 1 && m < 5
      return msg[1]
    elsif m == 1
      return msg[0]
    else
      return msg[2]
    end
  end

  counts = check_letters(gmail)
  old_counts = read_counts_letters
  check_new_counts_letters(counts, old_counts)
  save_counts_letters(counts)
  gmail.logout

end

Возникшие проблемы

  • При старте системы с задачами в crontab устройство было занято (в ручном режиме без проблем);
  • Перепады громкости.

Переосмысление

Спустя некоторое время, испробовав многие варианты, оставил pulseaudio для управления потоками.

pcm.pulse {
 type pulse
}

ctl.pulse {
 type pulse
}

pcm.!default {
 type pulse
}

ctl.!default {
 type pulse
}

Переписал bash script`ы на ruby, написав gem fest.

# coding: utf-8
#
class Fest
  def say(string, params = {})
    init(params)
    check_conditions
    make_wav(string)
    expect_if_paplay_now
    check_optimal_volume
    play_wav
  end

  def init(params)  # получаем все параметры
    @params = params
    @path = @params[:path] || '/tmp'
    @current_volume = `amixer | grep -o '[0-9]*' | sed "5 ! d"`.to_i
    @index = `ls -r #{@path} | grep -o '[0-9]*' | sed "1 ! d"`.to_i
    @min_volume = @params[:down_volume[0]] || 20
    @max_volume = @params[:down_volume[1]] || 60
    @step = @params[:down_volume[2]] || 4
  end

  def check_optimal_volume  # получаем громкость пониженную
    @volume = @current_volume - @current_volume / 10 * @step
    optimize_min_and_max_volume
  end

  def optimize_min_and_max_volume
    @optimize_volume = (
    if @current_volume > @max_volume
      @max_volume
    elsif @current_volume < @min_volume
      @min_volume
    else
      @current_volume
    end
    )
  end

  def check_conditions # проверка условий
    check_light
    check_home_theater
    check_say_wav
  end

  def check_light # молчим, если экран потушен
    exit if @params[:backlight].nil? && `xbacklight`.to_i == 0
  end

  def check_home_theater # при просмотре фильмов тоже разумеется
    xbmc = `ps -el | grep xbmc | wc -l`.to_i
    vlc = `ps -el | grep vlc | wc -l`.to_i
    kodi = `ps -el | grep kodi | wc -l`.to_i
    exit if xbmc > 0 || vlc > 0 || kodi > 0
  end

  def check_say_wav # index wav file
    @index > 0 ? @index += 1 : @index = 1
  end

  def make_wav(string) # создаём wav (c mp3 возникли проблемы)
    system("echo '#{string}' | text2wave -o #{@path}/say_#{@index}.wav 
      -eval '(#{@params[:language] || 'voice_msu_ru_nsh_clunits'})' 
      > /dev/null 2>&1")
  end

  def change_volume(volume) # ставим громкость 
    system("amixer set Master #{volume}% > /dev/null 2>&1")
  end

  def expect_if_paplay_now # ожидаем конца сообщения
    loop do
      break if `ps -el | grep paplay | wc -l`.to_i == 0
      sleep 1
    end
  end

  # получаем текущие источники
  # пошагово понижаем громкость
  def turn_down_volume
    @inputs = `pactl list sink-inputs | grep № | grep -o '[0-9]*'`.split("n")
    @inputs.each do |input|
      volume = @current_volume
      loop do
        system("pactl set-sink-input-volume #{input} '#{volume * 655}'")
        volume -= @step
        break if volume < @volume
      end
    end
  end

  def play_wav # воспроизводим wav
    turn_down_volume
    system("paplay #{@path}/say_#{@index}.wav 
      --volume='#{@optimize_volume * 655}' > /dev/null 2>&1")
    return_current_volume
    delete_wav
  end

  # пошагово возвращаем громкость 
  def return_current_volume
    @inputs.each do |input|
      volume = @volume
      loop do
        system("pactl set-sink-input-volume #{input} '#{volume * 655}'")
        volume += @step
        break if volume > @current_volume
      end
    end
  end

  def delete_wav # удаляем wav 
    system("rm -f #{@path}/say_#{@index}.wav")
  end

  def pluralform(number, array) # склонение
    n = number % 100
    m = n % 10

    if n > 10 && n < 20
      array[2]
    elsif m > 1 && m < 5
      array[1]
    elsif m == 1
      array[0]
    else
      array[2]
    end
  end
end

Crontab

PATH=$(echo $PATH)
SHELL=/bin/bash
GEM_HOME=$(gem environment gemdir)
GEM_PATH=$(gem environment gemdir)
DISPLAY=:0
*/10 * * * * ~/.bin/festival/charge
*/20 * * * * ~/.bin/festival/gmail
*/30 * * * * ~/.bin/festival/quotes

Результат

— Стабильная работа в crontab
— Музыка играет на заднем фоне
— Плавное понижение громкости и возврат
— Сообщение играет с текущей громкостью (если в пределах min и max)

Неприятные моменты:
— После просмотра фильмов стоит сбрасывать громкость
— Иногда озвучка искажается эхом (редко)
— Нет женского голоса

P.S.

Все скрипты, кроме gem «fest», старых версий. Переписанные скрипты

Спасибо за внимание.

Автор: AfsmNGhr

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js