Яндекс.Метрика
    Поиск по тегу

    voice recognition


    Найдено: 1 запись

    Звуки музыки

    Создание задач 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

    Ссылки


    Бинарники
    Исходники