Звуки музыки →
Создание задач Redmine голосом
Как-то недавно с коллегами обсуждали возможность быстрого добавления задач в redmine. Среди немногих, а точнее единственным предложенным вариантом стало определение текста задачи по голосу. А мне стало интересно – смогу я склеить эту ‘балалайку’ за выходные?
Далее опишу основные моменты и что в итоге получилось.
Записываем во Flac
И так, мне потребовалось: Dot NET, Google Speech-Api, кодек Flac, Redmine API .NET.
Использование speech-api освещается в интернетах в т.ч. и на хабре, для этого необходимо просто отправить POST запрос со звуковым файлом, в ответ получите JSON объект:
Строка запроса:
http://www.google.com/speech-api/v1/recognize?lang=ru&client=chromium
Ответ:
{status:int, id:string, hypotheses : [{utterance : string, confidence : double}]}
Сложность заключается в записи файла такого формата, а именно – Flac, 16kHz, 16bit, mono. Получением pcm данных c wave-in интерфейса занимается (WaveLib господина Ianier Munoz) — полученные данные с буферов очереди отправляются в указанный callback, где записываются в кольцевой буфер (ring buffer), перед созданием обработчика записи создаётся тред для извлечения данных из кольцевого буфера и их отправки в кодек. Кодек я обернул в C++/CLI библиотеке, в чём помог пример, прилагающийся к libflac:
// Обратите внимание что сэмплы для flac имеют размер в 32bit
// Создаётся Енкодер
static
bool InitialiseEncoder(char* filepath, FILE** _file, FLAC__StreamEncoder** _encoder, FLAC__StreamMetadata** _metadata1, FLAC__StreamMetadata** _metadata2)
{
FLAC__bool ok = true;
FLAC__StreamEncoder *encoder = 0;
FLAC__StreamEncoderInitStatus init_status;
FLAC__StreamMetadata *metadata[2];
unsigned sample_rate = 16000;
unsigned channels = 1;
unsigned bps = 16;
/* allocate the encoder */
if((encoder = FLAC__stream_encoder_new()) == NULL) {
return false;
}
ok &= FLAC__stream_encoder_set_verify(encoder, true);
ok &= FLAC__stream_encoder_set_compression_level(encoder, 5);
ok &= FLAC__stream_encoder_set_channels(encoder, channels);
ok &= FLAC__stream_encoder_set_bits_per_sample(encoder, bps);
ok &= FLAC__stream_encoder_set_sample_rate(encoder, sample_rate);
ok &= FLAC__stream_encoder_set_total_samples_estimate(encoder, 0);
// В этот файл будет записан выход Енкодера
FILE* flacfile = _wfopen((wchar_t*)filepath, L"wb");
// Создаём Енкодер
if(ok) {
init_status = FLAC__stream_encoder_init_FILE(encoder, flacfile, progress_callback, /*client_data=*/NULL);
if(init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) {
fprintf(stderr, "ERROR: initializing encoder: %s\n", FLAC__StreamEncoderInitStatusString[init_status]);
ok = false;
}
}
*_encoder = encoder;
*_file = flacfile;
return ok;
}
// Отправка pcm данных кодеку
static
bool ProcessEncoder(FLAC__StreamEncoder *encoder, FLAC__byte* _pcm, size_t need)
{
// С wave-in данные приходят в 16bit на семпл
FLAC__bool ok = true;
// На вход екодеру послупают 32bit сэмплы, здесь они просто копируются соблюдая последовательности
for(unsigned int i = 0; i < need*1; i++) {
pcm[i] = (FLAC__int32)(((FLAC__int16)(FLAC__int8)_pcm[2*i+1] << 8) | (FLAC__int16)_pcm[2*i]);
}
ok = FLAC__stream_encoder_process_interleaved(encoder, pcm, need);
return ok;
}
Далее был создан класс Recorder. Помимо создания экземпляра WaveInRecorder(менеджер wave-in устройства) создаётся тред для отправки pcm данных в Flac кодек из колцевого буфера:
private unsafe void DataArrived(IntPtr data, int size)
{
// Записываем pcm в кольцевой буфер
cb.Upload(data.ToPointer(), size);
}
WaveLib.WaveInRecorder m_Recorder;
VorbisEnc.FlacEncoder ve;
VorbisEnc.CircleBuffer cb;
IntPtr filepath;
public unsafe Recorder(string tempfilepath)
{
cb = new VorbisEnc.CircleBuffer();
ve = new VorbisEnc.FlacEncoder();
ve.Initialise((sbyte*)System.Runtime.InteropServices.Marshal.StringToHGlobalUni(tempfilepath).ToPointer());
// Тред для кодировщика
System.Threading.Thread th = new System.Threading.Thread(EncodeData);
WaveLib.WaveFormat fmt = new WaveLib.WaveFormat(16000, 16, 1);
m_Recorder = new WaveLib.WaveInRecorder(-1, fmt, 4096, 4, new WaveLib.BufferDoneEventHandler(DataArrived));
th.Start();
}
bool StopThread;
public bool AllDone = false;
public void Stop()
{
m_Recorder.Dispose();
StopThread = true;
cb.Dispose();
}
unsafe void EncodeData()
{
IntPtr datax = System.Runtime.InteropServices.Marshal.AllocHGlobal(4096);
sbyte* data = (sbyte*)datax.ToPointer();
while (!StopThread)
{
System.Threading.Thread.Sleep(10);
// Извлекаем из кольца данные, если курсор записи приблизился к курсору
// чтения на 4 итерации
while (cb.getNeedForUpdate() < 4096 * 4)
{
cb.Download(data, 4096);
ve.Encode(data, 4096);
}
}
System.Runtime.InteropServices.Marshal.FreeHGlobal(datax);
ve.Close();
// Данные кодированы и записаны, файл закрыт
AllDone = true;
}
Важность кольцевого буфера сложно переоценить, ведь он компенсирует задержки кодека и запись в файл, без него в данном случае при недостаточной производительности происходили потери данных: задержки на кодирование и запись в файл, не давали вовремя передать следующий буфер в очередь записи.
Аудиофайл записывается одной функцией:
string file = Path.GetTempFileName();
Recorder rec = new Recorder(file);
// Просто ждём пока записывается аудиофайл
System.Threading.Thread.Sleep(seconds * 1000);
// Останавливаем запись, закрываем файл
rec.Stop();
Далее передаётся на отправку в google:
string result = WebUpload.UploadFileEx(flacpath, "http://www.google.com/speech-api/v1/recognize?lang=ru&client=chromium",
"file", "audio/x-flac; rate=16000", parameters, null);
Функция отправки файла:
Uri uri = new Uri(url);
FileStream fileStream = new FileStream(uploadfile,
FileMode.Open, FileAccess.Read);
HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(uri);
if (cookies != null)
webrequest.CookieContainer = cookies;
// Лишние заголовки необходимо убрать
webrequest.Headers.Clear();
webrequest.ContentLength = fileStream.Length;
webrequest.ContentType = contenttype;
webrequest.Method = "POST";
Stream requestStream = webrequest.GetRequestStream();
byte[] buffer = new Byte[checked((uint)Math.Min(4096,
(int)fileStream.Length))];
// Записываем файл в поток Http запроса
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
requestStream.Write(buffer, 0, bytesRead);
fileStream.Close();
WebResponse response = webrequest.GetResponse();
Stream s = response.GetResponseStream();
StreamReader sr = new StreamReader(s);
string resps = sr.ReadToEnd();
response.Close();
return resps;
Redmine API
В redmine есть Rest api через который без сложностей, используя логин – пароль добавляется задача
RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
Configuration.RedmineUser, Configuration.RedminePassword);
// Задача
var newIssue = new Issue
{
Subject = Title,
Description = Description,
Project = new IdentifiableName() { Id = ProjectId },
Tracker = new IdentifiableName() { Id = TrackerId }
};
// Находим id текущего пользователя
User thisuser = (from u in manager.GetObjectList<User>(new System.Collections.Specialized.NameValueCollection())
where u.Login == Configuration.RedmineUser
select u).FirstOrDefault();
if (thisuser != null)
newIssue.AssignedTo = new IdentifiableName() { Id = thisuser.Id };
manager.CreateObject(newIssue);
Получение списка проектов и трекеров:
public static Dictionary<string, int> GetProjects()
{
RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
Configuration.RedmineUser, Configuration.RedminePassword);
Dictionary<string, int> Projects = new Dictionary<string, int>();
foreach (Project proj in manager.GetObjectList<Project>(new NameValueCollection()))
{
Projects.Add(proj.Name, proj.Id);
}
return Projects;
}
public static Dictionary<string, int> GetTrackers()
{
RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
Configuration.RedmineUser, Configuration.RedminePassword);
Dictionary<string, int> Trackers = new Dictionary<string, int>();
foreach (Tracker track in manager.GetObjectList<Tracker>(new NameValueCollection()))
{
Trackers.Add(track.Name, track.Id);
}
return Trackers;
}
Кстати, всё то актуально для версий моложе 1.3 (появился список трекеров в REST API)
Итог или заключение
В итоге получилась форма с двумя полями для определения по голосу: название задачи, описание задачи. На время записи и распознавания все поля закрываются панелью. Запись производится с устройства, выставленного в системе для записи по умолчанию в течение 4 секунд.
Конечно, за выходные я управился и это плюс, однако получил не совсем то, что ожидал. Да, можно доработать и добавить выделение фраз для повышения точности распознавания, сделать интерфейс удобнее (много удобнее), но вряд ли это поможет избавить от рутины добавления задач.
Источники
Flac
Redmine API
WaveLib
Ссылки
Бинарники
Исходники
01.03.2012 23:21+0400