Talking penguin

On the vast expanses of the Internet there are a lot of articles about smart homes, augmented reality glasses, the newfangled “Internet of things” and other perspectives of an imaginable future. Whether it will be bright or in the “good” traditions of noir cyberpunk, time will tell, but for now we will take a small step towards it.

As you guessed by the title of the publication, we will focus on speech synthesis. I will tell you how I implemented it, what I came across. I will not be original, I used festival.

Start


Experimental:

  • HP Notebook PC
  • Sound alsa (later delivered by pulseaudio)

- Installation, no problems should arise. Example
- Script Examples

Battery Alert


The script recognizes the status and charge. It reports the charge if it is not connected to charging and the charge is <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

Declination

#!/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

The script generates wav with files in the dat. It remembers the current volume, sets the optimum. Plays and returns the volume back. Deletes the current wav file.

#!/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

Checking gmail


Found an almost complete gem gmail solution .

Pluses of this solution:
- Notification of the number of messages by label. First you need to create filters with labels on the site.
- Ability to read the message header and text (not implemented).


# 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

Problems


  • At the start of the system with tasks in crontab, the device was busy (in manual mode without problems);
  • Volume differences.

Rethinking


After some time, having tried many options, I left pulseaudio for flow control.

pcm.pulse {
 type pulse
}
ctl.pulse {
 type pulse
}
pcm.!default {
 type pulse
}
ctl.!default {
 type pulse
}

Rewrote bash scripts in ruby ​​by writing 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

Result


- Stable work in crontab
- Music plays in the background
- Smoothly lower the volume and return
- The message plays at the current volume (if between min and max)

Unpleasant moments:
- After watching movies, it is worth dropping the volume
- Sometimes the voice acting is echoed (rarely)
- No female voice

PS


All scripts except gem "fest", old versions. Rewritten scripts and demo

Thank you for your attention.

Also popular now: