Когда возникает необходимость превратить звуковой файл с речью в текст, первыми на ум приходят решения Гугла и Яндекса. Но, кроме Яндекса, есть ещё одна отечественная компания — «Стэл» (http://speech.stel.ru/), API которой поддерживает «over 9000» и даже «очень очень много» запросов в день, а пробные ключи Stel раздает бесплатно.

image



Разобраться с API не так уж сложно, но на момент написания этой статьи мануал с сайта Стэла устарел и не работает, посему здесь будет представлен мануал с примерами на Python и Java. Пример на Java особенно актуален, если звуковой файл у вас имеется не в виде файла, а в виде массива байтов. Сразу стоит отметить, что Стэл работает только с wav-файлами с частотой дискретизации 8 кГц, размером сэмпла 16 бит, моно (один канал).

Ближе к делу: на сайте Стэла (http://speech.stel.ru/api_description) подробно описано, что и как (хоть на данный момент и немного устарело), посему, приводим сразу работающий (опять же, на данный момент) пример на питоне:

coding: utf-8
import httplib, json, base64

HOST = 'api.stel.ru:7071'
APIKEY = '***' # Place your API key here
MODEL = 'rus_gsm_ext'

WAV = base64.b64encode(open('test.wav', 'rb').read()) # demo audio file (WAV, 8000 HZ, 16-bit, mono)
con = httplib.HTTPConnection(HOST)

#Speech recognition
data = json.dumps({'apikey' : APIKEY, 'model': MODEL , 'wav' : WAV})
headers = {'Content-Type' : 'application/json', 'Accept': 'application/json', 'Content-Length' : '{0}'.format(len(data))}
con.request('POST', '/kwfind', data, headers)
resp = con.getresponse()

if resp.status == 200:
  print json.loads(resp.read()) # UTF-8 string with recognized text
else:
  print resp.reason


Как видно, тут на распознавание отправляется test.wav из рабочей директории скрипта. Аналогичный код на Java, а также код работающий с массивами байтов, приведены ниже. Во-первых, класс, который массивы байтов (без разметки) и файлы превращает в массивы байтов, соответствующие wav-файлу в указанном формате (нам будут нужны 8000 герц, 2 байта, 1 канал):

package ru.habrahabr.stel.example;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

public class WaveFile
{
	private int INT_SIZE = 4;
	public final int NOT_SPECIFIED = -1;
	private int sampleSize = NOT_SPECIFIED;
	private long framesCount = NOT_SPECIFIED;
	private byte[] data = null;
	private AudioInputStream ais = null;
	private AudioFormat af = null;

	WaveFile(File file) throws UnsupportedAudioFileException, IOException
	{
		if(!file.exists())
		{
			throw new FileNotFoundException(file.getAbsolutePath());
		}
		
		ais = AudioSystem.getAudioInputStream(file); 
		af = ais.getFormat();
		framesCount = ais.getFrameLength();
		sampleSize = af.getSampleSizeInBits()/8;
		long dataLength = framesCount*af.getSampleSizeInBits()*af.getChannels()/8;
		data = new byte[(int) dataLength]; 
		ais.read(data);
	}
	
	WaveFile(int sampleSize, float sampleRate, int channels, int[] samples) throws Exception
	{
		if(sampleSize < INT_SIZE)
		{
			throw new Exception("sample size < int size");
		}
		
		this.sampleSize = sampleSize;
		this.af = new AudioFormat(sampleRate, sampleSize*8, channels, true, false);
		this.data = new byte[samples.length*sampleSize];
		
		for(int i=0; i < samples.length; i++)
		{
			setSampleInt(i, samples[i]);
		}
		
		framesCount = data.length / (sampleSize*af.getChannels());
		ais = new AudioInputStream(new ByteArrayInputStream(data), af, framesCount);
	}
	
	WaveFile(int sampleSize, float sampleRate, int channels, byte[] wave) throws Exception
	{
		this.sampleSize = sampleSize;
		this.af = new AudioFormat(sampleRate, sampleSize*8, channels, true, false);
		this.data = Arrays.copyOf(wave, wave.length);
		
		framesCount = data.length / (sampleSize*af.getChannels());
		ais = new AudioInputStream(new ByteArrayInputStream(data), af, framesCount);
	}
	
	public AudioFormat getAudioFormat()
	{
		return af;
	}
	
	public byte[] getData()
	{
		return Arrays.copyOf(data, data.length);
	}
	
	public byte[] getWave() throws Exception
	{
		ByteArrayOutputStream bts = new ByteArrayOutputStream();
		AudioSystem.write(new AudioInputStream(new ByteArrayInputStream(data), 
				af, framesCount), AudioFileFormat.Type.WAVE, bts);
		return bts.toByteArray();
	}
	
	public int getSampleSize()
	{
		return sampleSize;
	}
	
	public double getDurationTime()
	{
		return getFramesCount() / getAudioFormat().getFrameRate();
	}
	
	public long getFramesCount()
	{
		return framesCount;
	}
	
	public void saveFile(File file) throws IOException
	{
		AudioSystem.write( new AudioInputStream(new ByteArrayInputStream(data), 
				af, framesCount), AudioFileFormat.Type.WAVE, file);
	}
	
	public int getSampleInt(int sampleNumber)
	{
		
		if(sampleNumber < 0 || sampleNumber >= data.length/sampleSize)
		{
			throw new IllegalArgumentException(
					"sample number is can't be < 0 or >= data.length/"
							+ sampleSize);
		}
		
		byte[] sampleBytes = new byte[sampleSize];
		
		for(int i=0; i < sampleSize; i++)
		{
			sampleBytes[i] = data[sampleNumber * sampleSize + i];
		}
		
		int sample = ByteBuffer.wrap(sampleBytes)
				.order(ByteOrder.LITTLE_ENDIAN).getInt();
		
		return sample;
	}
	
	public void setSampleInt(int sampleNumber, int sampleValue)
	{
		byte[] sampleBytes = ByteBuffer.allocate(sampleSize).
				order(ByteOrder.LITTLE_ENDIAN).putInt(sampleValue).array();
		
		for(int i=0; i < sampleSize; i++)
		{
			data[sampleNumber * sampleSize + i] = sampleBytes[i];
		}
	}
}


Стоит отметить, что это немного дописанный класс, взятый с http://blog.eqlbin.ru/2011/02/wave-java.html. В общем-то все, что тут добавлено — это функция getWave(), возвращающая массив байтов, соответствующих файлу, построенному одним из конструкторов. А также конструктор, принимающий массив байтов обычного raw-файла. Отправлять Стэлу будем именно результат функции getWave(). Далее приведена функция, которая принимает WaveFile, открывает соединение со Стэлом, отправляет все что нужно, закрывает соединение и возвращает распознанную строку:

String getResponseOn(WaveFile wf)
{
	String res = new String();
	try
	{
		byte[] wav = wf.getWave();
		HttpConnection conn = new HttpConnection("api.stel.ru", 7071);
		conn.open();
		HttpState state = new HttpState();
		
		PostMethod post = new PostMethod();
		
		JSONObject data = new JSONObject();
		data.put("apikey", "***");	// Place your API key here
		data.put("model", "rus_gsm_ext");
		data.put("wav", new String(Base64.encodeBase64(wav)));
		
		post.setPath("/kwfind");
		post.setRequestHeader("Content-Type", "application/json");
		post.setRequestHeader("Accept", "application/json");
		post.setRequestHeader("Content-Length", ""+data.toJSONString().length());
		post.setRequestEntity(new StringRequestEntity(data.toJSONString(), "application/json", null));
		post.execute(state, conn);
		
		res = res + (String) ((JSONObject) new JSONParser().parse(post.getResponseBodyAsString())).get("text");
		conn.close();
	}
	catch(Exception e)
	{
		res = null;
	}
	return res;
}


Не забудьте, что надо заменить "***" на ваш ключ, а также, что WaveFile для getResponseOn создается с параметрами (2, (float) 8000, 1, (byte[]) raw), например:

String res1 = getResponseOn(new WaveFile(2, (float) 8000.0, 1, sound));
String res2 = getResponseOn(new WaveFile(new File("test.waw"))); //demo audio file (WAV, 8000 HZ, 16-bit, mono)


Кроме того, нужно отметить, что в getResponseOn(WaveFile wf) используется org.json.simple.JSONObject и org.json.simple.parser.JSONParser, которые зачастую приходится качать отдельно, например, отсюда: www.java2s.com/Code/Jar/j/Downloadjsonsimple111jar.htm

«Стэл» легко идут на контакт, так что, если вам нужны будут другие языки или языковые базы, с ними можно договориться.

Напомним, что наша команда занимается разработкой интеллектуального домашнего помощника Лекси. Лекси — настольное устройство с искусственным интеллектом и полностью голосовым интерфейсом для управления умным домом. Устройство может получать информацию в интернете, управлять бытовой техникой, сообщать новости из социальных сетей. Кстати, почитать об интересных размышлениях о будущем подобных домашних роботов вы можете в этой статье .

Технология распознавания речи, как вы уже могли догадаться, у нас от Стэла. При этом распознавание речи происходит полностью на борту устройства (обзор нашей собственной электроники можно посмотреть тут). Это дает нам ряд преимуществ по сравнению с конкурентными аналогами, например, увеличение скорости выдачи пользователю ответа, отсутствие активационной фразы и возможность работы без интернета.

Следите на нашим проектом в социальных сетях: Вконтакте и Фейсбуке.

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

Комментарии (13)


  1. ChiefPilot
    30.06.2015 14:19
    +2

    Зашёл посмотреть, что же там таки Бендер пытался сказать, а тут нету ничего! Вот как же так можно-то, а?! :)


    1. Timon_Omsk
      30.06.2015 14:56
      +2

      А ведь Паша всегда так внимательно расспрашивал о здоровье Ксении Федоровны,


    1. Anisotropic
      30.06.2015 17:04
      +2

      habrahabr.ru/company/speechpro/blog/210078
      Отсюда картинка взята, тут же и описано это.


      1. SedovArtem
        30.06.2015 17:16

        Картинка была взята с гуглапоиска по картинкам.


        1. Anisotropic
          01.07.2015 03:26

          Ок. Я вам ссылку дал откуда она взялась в гуглопоиске :)


  1. gluck59
    30.06.2015 15:15

    Сказал в микрофон «проверка связи». Результат:

    habrastorage.org/files/4bf/e90/da7/4bfe90da71964e1f9b2b1b22bf42af15.JPG

    Сорри, здесь не работает вставка картинок, поэтому только так.


    1. KvanTTT
      10.07.2015 16:59

      Работает, просто у вас карма отрицательная.


  1. zvyagaaa
    30.06.2015 17:11

    Было бы здорово соорудить на API сервис для транскрибирования записей. Потому что на русском языке транскрибирования практически нет. А сервис, думаю, был бы востребован.


  1. AndreWin
    30.06.2015 18:25

    А что робот на первой картинке говорит?


    1. SedovArtem
      30.06.2015 18:28

      А ведь Паша всегда так внимательно расспрашивал о здоровье Ксении Федоровны


  1. DemiurgeSerge
    09.07.2015 10:35

    Первые три строчки статьи вызывают недоумение. А как же распознавание речи от ЦРТ (Центр речевых технологий)? А есть еще и другие компании… тоже Российские.


    1. eeeeep
      09.07.2015 11:20

      Большинство разработчиков используют то, что хорошо документировано и доступно. Поэтому мы и упомянули самые стандартные решения.
      А кроме ЦРТ вы кого знаете?


      1. DemiurgeSerge
        09.07.2015 13:32

        Вы думаете решения ЦРТ не документированы? Вместе с VoiceNavigator'ом идет пакет из 19 документов — часть из них я сам писал, когда работал в ЦРТ. Это полностью коммерческое и промышленное решение.
        Что значит доступно? Можно купить или мало денег стоит? Кто хочет купить систему распознавания речи — тот покупает, это никогда не было проблемой.
        Если Вы называете такие компании как Google и Яндекс, то почему забыли Nuance — мирового гиганта?
        Российские компании тоже есть, не смог их сейчас загуглить за минуту, но еще 2-3 точно есть, я с ними общался, но названия из головы вылетели.