Технология распознавания речи и базы данных

Можно ли реализовать ввод информации в базу данных, применяя технологию распознавания речи? В статье описывается вариант такой программы. В этой программе речь отсылается на преобразование в строковое представление на сервер Apple. Полученная обратно от сервера строка состоит из слов (подстрок) разделенных пробелами как разделителями. Эти подстроки выделяются и заносятся в поля формы. Затем сформированная запись сохраняется в базе данных. Стандартные команды манипуляции данными в базе данных, такие как: Save, Find, Update, Delete так же могут быть введены с голоса и распознаны программой.
Программа реализована и работает на аппаратных и программных средствах компании Apple. В проекте в качестве операционной системы используется Apple iOS 10 с SDK Speech framework. В качестве базы данных применяется iCloud Storage c CloudKit Framework. Отмечу, что может быть использована и другая СУБД. Программа написана на языке Swift 3.
Хочу поблагодарность Smyth Neil за его книгу "iOS 10 App Development Essentials". Это очень хорошая книга. Часть описанных в книге тем по реализации отдельных вопросов использованы в статье.
Идея применения технологии распознавания речи для работы с базами данных и ее реализация в виде программы принадлежит автору статьи.
1. Работа с программой.
Давайте посмотрим, как выглядит работа с программой. Внешне все очень просто. После запуска, программа попросит согласие на использование микрофона. Cогласитесь и для начала процесса перевода голоса в текст нажмите кнопку "Start". Программа готова принимать данные. Вы можете произносить фразы и наблюдать их отображение в соответствующих полях формы. По завершении ввода вы можете произнести (или нажать на кнопку) команду "Сохранить" или "Записать" и программа послушно сохранит ваши данные в облаке Apple iCloud, известив вас выводом на экран соответствующего уведомления. Вот так выглядит интерфейс программы после выдачи команды "Стоп".
Рис. 1.
2. Какие команды распознает программа?
Для управления БД используются команды:
  • "Сохранить". Это команда записывает данные из полей формы в базу данных. По завершении сохранения информации формируется уведомление об успешности операции или, в случае не удачи, сообщение об ошибке.
  • "Найти". Это команда на выполнение поиска в базе данных. Поиск производится по полям: Фамилия + Имя + Отчество, все три поля не должны быть пустыми.
  • "Обновить". По этой команде выполняются две операции. Сначала выполняется поиск по содержимому полей ФИО, как поисковому образу, затем, если запись будет найдена, выполняется ее модификация с новыми данными из полей формы: "Дата рождения" и "Номер телефона". Если запись не найдена или произойдет ошибка, будет сформировано соответствующее сообщение.
  • "Удалить". По данной команде также выполняются две операции – сначала поиск записи, а затем удаление записи с соответствующими извещениями.
  • "Стоп". Это команда остановки процесса преобразования и работы программы.
Для управления вводом данных на форму используется всего две команды:
  • "Назад". По этой команде стирается содержимое текущего поля на форме. Указатель устанавливается в начало поля. Команда может быть выдана не однократно, тем самым вы будете стирать каждое предыдущее поле и подниматься вверх по полям формы.
  • "Стереть". Данная команда стирает содержимое всех полей. Указатель устанавливается в первое поле – это поле "Фамилия".
Для остановки преобразования звука в текстовое представление используется команда "Stop". Отмечу, что число команд может быть изменено, а любая из команд может быть модифицирована. Это никто и никак не ограничивает.
Все команды для работы с программой продублированы соответствующими кнопками, так что данные и команды можно вводить и обычным способом.
3. Инфраструктура для работы программы.
Программа на локальном устройстве (iPhone и т.д.) не занимается распознаванием речи и сохранением данных, для этого данные отсылаются на сервера Apple. Таким образом, мобильное устройство, на котором запускается приложение, находится внутри инфраструктуры. Как организована работа всей этой системы?
Звуковой сигнал воспринимается микрофоном на локальном устройстве и отсылается программой по каналу связи на распознавание. На сервере этот сигнал преобразуется в строку, разделенную пробелами, и отсылается обратно на локальное устройство. Программа на локальном устройстве принимает и анализирует полученную строку, разделяя данные и команду.
Распознав данные и команду из перечня, определенного выше, программа обращается к СУБД уже на другом сервере Apple, передавая информацию и команду на исполнение. Выполнив команду, процесс на сервере СУБД возвращает ответ, который принимается программой на локальном устройстве. Обратите внимание, насколько мы зависимы от инфраструктуры.

Примеры формата общения с программой.

Формат для ввода данных:
  • Иванов
  • Сидор
  • Петрович
  • Первое апреля 1990
  • 921 278 48 70
  • Сохранить
Формат для удаления записи:
  • Крючков
  • Валентин
  • Григорьевич
  • Удалить
Аналогично выполняются другие команды.
4. Структура программы.
Практически каждая строка исходного текста имеет комментарий поэтому, что-либо добавлять нет надобности. Остановлюсь на двух моментах структуры программы.

1. Операции удаления и обновления записей в базе данных реализованы как двухступенчатые, в том смысле, что этим операциям предшествует операция поиска записи. Действительно, ведь для того чтобы что-то изменить, сначала нужно найти запись. Если написать в тексте программы последовательно функцию поиска и затем сразу функцию удаления (обновления) записи, то последняя операция выполнена не будет. В чем причина? Дело в том, что все операции с базой данных асинхронны, программа запускает процесс поиска записи на сервере и затем ждет Callback. А ждать его можно очень долго по меркам длительности цикла процессора.
Поэтому возникает задача синхронизации двух асинхронных процессов. В программе она решена следующим образом. Операция удаления записи вызывается из Callback операции поиска, так эти два процесса синхронизируются по времени. Процесс удаления (обновления) начинается только после окончания процесса поиска. Обратите внимание, что и операция удаления (обновления) так же асинхронна и имеет свой Callback.

2. Несколько слов о методике распознавании речи на сервере Apple. В некоторых случаях при разборе возвращаемой сервером строки Вы будете удивлены. Например, при последовательном распознавании нескольких слов, можно обнаружить, что на очередном слове будет изменен формат предыдущих слов. Надо полагать, что так на сервере происходит процесс "переосмысления" контекста произносимой фразы.

3. Ограничение сессии распознавания речи одной минутой, существующее в настоящее время, на мой взгляд, несущественно и может быть легко устранено повторным перезапуском сессии, например, через таймер.
5. Выводы по результатам тестовой эксплуатации.
  • Программа создавалась с целью определения возможностей голосового ввода данных и команд в компьютер.
  • Надежность ввода информации. Первое, на что следует обращать внимание - это четкость при произношении фраз. Основная доля ошибок приходится именно на процесс распознавания речи.
  • Пропускная способность каналов связи и распознавания. Если оператор начинает очень быстро произносить фразы – процент ошибок возрастает.
  • Для ответственных приложений потребуется диалог человека с программой, уточняющий данные и команды. Например, так как это уже делается в Siri.
  • Голосовое управление компьютером становится реальностью, однако клавиатурный ввод данных, останется востребованным.
6. Исходный текст файла ViewController.swift.
ViewController.swift
//  ViewController.swift
//  DataInputVoice
//
//  Created by Evgeny Veresov on 02.07.2017.
//  Copyright © 2017 Evgeny Veresov. All rights reserved.
//
//  Программа потокового ввода информации в базу данных с голоса.
//
//  1. Операционная система на устройстве ввода данных - iOS 10.
//  1. Преобразование голоса в текст выполняется на серверах Apple.
//  2. В качестве базы данных используются iCloud от Apple.
//  3. Работа с базой данных и преобразование голоса в текст
//     написаны по мотивам книги Neil Smyth "iOS 10 App Development Essentials".
//     Спасибо автору книги.
//  4. Идея и реализация программы голосового ввода в базу данных прнинадлежит мне.
//  5. Программа предоставляется "Как есть".
 
import UIKit
import Speech
import CloudKit
import UserNotifications
 
class ViewController: UIViewController, UNUserNotificationCenterDelegate  {
    
    // Переменные для базы данных
    let containerDatabase = CKContainer.default
    var privateDatabase: CKDatabase?
    var zoneDatabase: CKRecordZone?
    
    // Переменные для распознавания голоса
    let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru-RU"))!
    var speechRecognitionRequest:  SFSpeechAudioBufferRecognitionRequest?
    var speechRecognitionTask: SFSpeechRecognitionTask?
    let engine = AVAudioEngine()
    
    //Таймер для отсчета времени
    var timer: Timer!
    // Время работы трансляции
    var interval = 0
    // Признак окончания преобразования
    var flagStop = true
    // Текущий номер поля на форме
    var currentField = 0
    // Последнее распознанное слово
    var lastWord = ""
    // Прежнее значение счетчика буфера
    var oldCountBuffer = 0
    // Result String
    var resultInput = ""
    // Массив для resultInput
    var arrayBuffer:[String] = []
    
    // Переменные для оповещения
    var titleNotification       = ""
    var subtitleNotification    = ""
    var bodyNotification        = ""
    
    
    //  Кнопка записи в базу данных
    @IBOutlet weak var saveDatabase: UIBarButtonItem!
    @IBAction func selectSaveDatabase(_ sender: Any) {
        saveDatabase.isEnabled = false
        saveRecord()
        saveDatabase.isEnabled = true
        }
    
    
    // Кнопка поиска в базе данных
    @IBOutlet weak var findDatabase: UIBarButtonItem!
    @IBAction func selectFindDatabase(_ sender: Any) {
        findDatabase.isEnabled = false
        findRecord()
        findDatabase.isEnabled = true
        }
    
    
    // Кнопка обновления в базе данных
    @IBOutlet weak var updateDatabase: UIBarButtonItem!
    @IBAction func selectUpdateDatabase(_ sender: Any) {
        updateDatabase.isEnabled = false
        updateRecord()
        updateDatabase.isEnabled = true
        }
    
    
    // Кнопка удалентия записи
    @IBOutlet weak var deleteDatabase: UIBarButtonItem!
    @IBAction func selectDeleteDatabase(_ sender: Any) {
        deleteDatabase.isEnabled = false
        deleteRecord()
        deleteDatabase.isEnabled = true
        }
    
    // Кнопка Start
    @IBOutlet weak var transcribeButton: UIBarButtonItem!
    @IBAction func selectTranscribleButton(_ sender: Any) { runTranslation()}
    
    // Кнопка Stop
    @IBOutlet weak var stopButton: UIBarButtonItem!
    @IBAction func selectStopButton(_ sender: Any) {endTranslation()}
    
    
    // Поле Фамилия
    @IBOutlet weak var fam: UITextField!
    // Поле Имя
    @IBOutlet weak var im: UITextField!
    // Поле Отчество
    @IBOutlet weak var otch: UITextField!
    // Поле отчество
    @IBOutlet weak var dateBirth: UITextField!
    //Телефон
    @IBOutlet weak var phone: UITextField!
    // Таймер
    @IBOutlet weak var time: UITextField!
    
    // По загрузке
    override func viewDidLoad() {
        super.viewDidLoad()
        // Авторизация пользователя
        authorizeSR()
        // Разрешение на оповещения
        authorizeNotification()
        // Создадим private Database
        privateDatabase = containerDatabase().privateCloudDatabase
        // Создадим зону
        zoneDatabase = CKRecordZone(zoneName: "DataInputVoice")
        // Сохраним базу данных
        privateDatabase?.save(zoneDatabase!, completionHandler: {(recordzone, error) in
            if (error != nil) {
                DispatchQueue.main.async {self.notifyUser("Ошибка зоны", message: "Ошибка при создании зоны")}}
            else { print("Создана зона")}
        })
        
        // Начальное значение в поле телефон
        phone.text = "+7"
        // Создадим таймер
        createTimer()
    }
    
    
    // Сессия преобразования голоса в текст
    func startSession () throws {
        // Если задача преобразования уже запущена - остановим ее
        if let recognitionTask = speechRecognitionTask {
            recognitionTask.cancel()
            self.speechRecognitionTask = nil
        }
        // Создадим сессию
        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(AVAudioSessionCategoryRecord)
        // Создадим объект запроса
        speechRecognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        // Если ошибка - выдадим сообщение
        guard let recognitionRequest = speechRecognitionRequest else {
            fatalError("Ошибка при создании SFSpeechAudioBufferRecognitionRequest") }
        // Создадим input node
        guard let inputNode = engine.inputNode else { fatalError("Не удалось создать input node") }
        // Установим признак выдавачи частичных результатов
        recognitionRequest.shouldReportPartialResults = true
        // Запустим задачу преобразования с обработчиком ответа преобразователя
        speechRecognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
            
            //Определим признак окончания времени
            var finished = false
            
            //  Считаем строку из преобразователя
            if  let result = result {
                self.resultInput = result.bestTranscription.formattedString
                // Считаем признак окончания работы
                finished = result.isFinal
            }
            // Если ошибка распознавания
            if error != nil {
                DispatchQueue.main.async {self.notifyUser("Распознавание речи", message: "Ошибка распознавания!")}
                self.flagStop = true
                
                self.engine.stop()
                inputNode.removeTap(onBus: 0)
                
                self.speechRecognitionRequest = nil
                self.speechRecognitionTask = nil
                
                self.transcribeButton.isEnabled = true
                self.stopButton.isEnabled = false
            }
            
            // Если истекло время перевода
            if finished {
                self.flagStop = true
                self.engine.stop()
                inputNode.removeTap(onBus: 0)
                
                self.speechRecognitionRequest = nil
                self.speechRecognitionTask = nil
                
                self.transcribeButton.isEnabled = true
                self.stopButton.isEnabled = false
            }
            
            //  Если все хорошо - начинаем анализ строки
            if (finished  != true && error == nil && self.flagStop == false) {
                // Переведем строку в массив
                self.arrayBuffer = self.resultInput.components(separatedBy: " ")
                // Перевод в нижний регистр последнего слова
                self.lastWord = self.arrayBuffer.last!.lowercased()
                
                // Если повторение слова
                if self.oldCountBuffer == self.arrayBuffer.count {
                    // Если команда - уходим
                    if (self.lastWord == "назад" || self.lastWord == "сохранить" || self.lastWord == "найти"
                        || self.lastWord == "обновить" || self.lastWord == "удалить") {return}
                    // Если слово для ввода - повторяем
                    if self.currentField > 0 {self.currentField -= 1}
                }
                
                // Разбор слова
                switch self.lastWord {
                // Команды работы с базой данных
                case "сохранить","записать"self.saveRecord()
                case "найти"    : self.findRecord()
                case "обновить" : self.updateRecord()
                case "удалить"  : self.deleteRecord()
                // Очистка всех полей
                case "стереть","обнулить","очистить"  : self.clearAll()
                // Очистка одного поля
                case "назад"    : self.clearField()
                // Команда окончания пееревода
                case "стоп"     : self.endTranslation()
                // Данные для ввода в поля
                default : self.translate(self.lastWord)
                    
                }
                // Запомним колличество слов в буффере
                self.oldCountBuffer = self.arrayBuffer.count
            }
        }
        // Определим формат преобразования
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.removeTap(onBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.speechRecognitionRequest?.append(buffer)
        }
        // Подготовим преобразователь
        engine.prepare()
        // Запустим
        try engine.start()
    }
    
    // Очистка текущего поля
    func clearField(){
        switch currentField {
        // Отмена однословных полей: фамилия, имя, отчество
        case 1 : fam.text   = ""
        case 2 : im.text    = ""
        case 3 : otch.text  = ""
        // Отмена даты рождения: число, месяц, год.
        case 4,5,6      : dateBirth.text = "";  currentField = 4
        case 7,8,9,10   : phone.text =   "+7 "; currentField = 7
        default : break
        }
        // Поднимаемся из стека в верх при удалении полей
        if currentField > 1 {currentField -= 1else {currentField = 0}
    }
    
    // Функция заполнения полей формы при вводе с голоса
    func translate (_ lastWord: String) {
        // Определим текущую позицию в буфере
        let currentPositionBuffer  = arrayBuffer.count
        
        // Внимание! self.lastWord не переведена в нижний регистр
        // это предназначено для полей ФИО
        self.lastWord  = arrayBuffer[currentPositionBuffer - 1]
        
        // Опускаемся по стеку вниз
        if (currentField < 10 ) {currentField += 1else {currentField = 1}
        switch currentField {
        case 1 : self.fam.text   = self.lastWord
        case 2 : self.im.text    = self.lastWord
        case 3 : self.otch.text  = self.lastWord;
        // Число
        case 4 : dateBirth.text  = lastWord
        // Месяц
        case 5 : dateBirth.text = arrayBuffer[currentPositionBuffer - 2] +  " " +
            arrayBuffer[currentPositionBuffer - 1]
        //Год
        case 6 : dateBirth.text  = arrayBuffer[currentPositionBuffer - 3] + "." +
            decoderMonth(arrayBuffer[currentPositionBuffer - 2]) + "." +
            String(arrayBuffer[currentPositionBuffer - 1].characters.prefix(4))
            
        // Проверимчто пришло числоИначе ничего не вводим и уменьшаем счетчик поля на форме
        case 7 : if Int(lastWord) != nil {
            phone.text = "+7 " + lastWord}
        else {self.currentField -= 1return}
            
        case 8 : if Int(lastWord) != nil{
            phone.text = "+7 " + arrayBuffer[currentPositionBuffer - 2] +  " " +
                arrayBuffer[currentPositionBuffer - 1]
        }
        else {self.currentField -= 1return}
            
        case 9 : if Int(lastWord) != nil {
            phone.text = "+7 " + arrayBuffer[currentPositionBuffer - 3] +  " " +
                arrayBuffer[currentPositionBuffer - 2] + " " +
                arrayBuffer[currentPositionBuffer - 1]
        }
        else {self.currentField -= 1return}
            
        case 10 : if Int(lastWord) != nil {
            phone.text = "+7 " + arrayBuffer[currentPositionBuffer - 4] +  " " +
                arrayBuffer[currentPositionBuffer - 3] + " " +
                arrayBuffer[currentPositionBuffer - 2] + " " +
                arrayBuffer[currentPositionBuffer - 1]
        }
        else {self.currentField -= 1return}
        default : break
        }
    }
    
    // Функция сохранения записи в базе данных
    func saveRecord() {
        //Остановим переобразование
        endTranslation()       
        // Проверим на заполняемость ФИО
        if !checkFullPredicate() {return}     
        // Определим имя записи
        let record = CKRecord(recordType: "DataInputFromVoice", zoneID: (zoneDatabase?.zoneID)!)
        // Присвоим значения полям записи (Field in Row)
        record.setObject(fam.text   as CKRecordValue?,  forKey: "fam")
        record.setObject(im.text    as CKRecordValue?,  forKey: "im")
        record.setObject(otch.text  as CKRecordValue?,  forKey: "otch")
        record.setObject(dateBirth.text    as CKRecordValue?,  forKey: "dateBirth")
        record.setObject(phone.text  as CKRecordValue?,  forKey: "phone")
        // Определим метод записи в базу данных
        let modifyRecords = CKModifyRecordsOperation (recordsToSave: [record],recordIDsToDelete: nil)
        // Определим timeout для операции записи
        modifyRecords.timeoutIntervalForRequest  = 10
        // Определим обработчик записи
        modifyRecords.modifyRecordsCompletionBlock =
            { records, recordIDs, error in
                if let err = error {
                    DispatchQueue.main.async {self.notifyUser("Ошибка записи", message: err.localizedDescription)}}
                else {
                    DispatchQueue.main.async {
                        self.titleNotification      =   "Save record"
                        self.subtitleNotification   =    self.fam.text! + " "  + self.im.text!
                        self.bodyNotification       =   "Запись сохранена"
                        self.clearAll()
                        self.sendNotificationSave()
                        self.selectTranscribleButton("")
                    }
                }
        }
        // Запустим операцию
        privateDatabase?.add(modifyRecords)
    }
    
    
    //функция поиска записи в базе данных
    func findRecord() {
        //Остановим переобразование
        endTranslation()
       // Проверим на заполняемость ФИО
        if !checkFullPredicate() {return}
 
        // Сформируем поисковы образ
        let predicate = NSPredicate(format: "fam = %@ AND im = %@ AND otch = %@" , self.fam.text!, self.im.text!, self.otch.text!)
        // Сформируем запрос к базе данных
        let query = CKQuery(recordType: "DataInputFromVoice", predicate: predicate)
        // Выполним зпрос
        privateDatabase?.perform(query, inZoneWith: zoneDatabase?.zoneID, completionHandler: ({results, error in
            // Проверка результатов запроса
            // Если ошибка - выведем сообщение пользователю
            if (error != nil) {DispatchQueue.main.async {self.notifyUser("Ошибка поиска",message: error!.localizedDescription)}}
            else {
                // Если нашли записи - выведем первую в поля фоормы
                if results!.count > 0 {
                    let record = results![0]
                    //  Выведем запись на форму
                    DispatchQueue.main.async {
                        self.fam.text   = record.object(forKey: "fam")    as? String
                        self.im.text    = record.object(forKey: "im")     as? String
                        self.otch.text  = record.object(forKey: "otch")   asString
                        self.dateBirth.text = record.object(forKey: "dateBirth")     as? String
                        self.phone.text = record.object(forKey: "phone")   as? String
                        self.notifyUser("Поиск записи", message: "Запись найдена!")
                        self.currentField = 10
                    }
                    // Если найденных записей нет - сообщим пользователю
                } else {DispatchQueue.main.async {self.notifyUser("Поиск записи", message: "Запись не найдена!")}}
            }
        }))
    }
    
    
    // Функция обновления записи
    func updateRecord(){
        //Остановим переобразование
        endTranslation()
        // Проверим на заполняемость ФИО
        if !checkFullPredicate() {return}
        // Сформируем поисковы образ
        let predicate = NSPredicate(format: "fam = %@ AND im = %@ AND otch = %@" , self.fam.text!, self.im.text!, self.otch.text!)
        // Сформируем запрос к базе данных
        let query = CKQuery(recordType: "DataInputFromVoice", predicate: predicate)
        // Выполним  поиск
        privateDatabase?.perform(query, inZoneWith: zoneDatabase?.zoneID, completionHandler:
            ({results, error in
                // Проверка результатов запроса
                // Если ошибка - выведем сообщение пользователю
                if (error != nil) {DispatchQueue.main.async {self.notifyUser("Ошибка поиска",message: error!.localizedDescription)}}
                else {
                    // Если нашли записи
                    if results!.count > 0 {
                        let record = results![0]
                        record.setObject(self.fam.text       as CKRecordValue?, forKey: "fam")
                        record.setObject(self.im.text        as CKRecordValue?, forKey: "im")
                        record.setObject(self.otch.text      as CKRecordValue?, forKey: "otch")
                        record.setObject(self.dateBirth.text as CKRecordValue?, forKey: "dateBirth")
                        record.setObject(self.phone.text     as CKRecordValue?, forKey: "phone")
                        // Запишем в базу данных
                        self.privateDatabase?.save(record, completionHandler:({returnRecord, error in
                            if let err = error {DispatchQueue.main.async {self.notifyUser("Ошибка обновления", message: err.localizedDescription)}
                            }
                            else {
                                 DispatchQueue.main.async {self.notifyUser("Обновление", message:"Запись обновлена")}
                                }
                            }))
 
                    } 
                    else {DispatchQueue.main.async {self.notifyUser("Обновление записи", message:"Запись не найдена")}}
                }
            }))
        }
 
    
    // Фукнция удаления записи
    func deleteRecord() {
        //Остановим переобразование
        endTranslation()
        // Проверим на заполняемость ФИО
        if !checkFullPredicate() {return}
        // Сформируем поисковы образ
        let predicate = NSPredicate(format: "fam = %@ AND im = %@ AND otch = %@" , self.fam.text!, self.im.text!, self.otch.text!)
        // Сформируем запрос к базе данных
        let query = CKQuery(recordType: "DataInputFromVoice", predicate: predicate)
        // Выполним  поиск
        privateDatabase?.perform(query, inZoneWith: zoneDatabase?.zoneID, completionHandler:
            ({results, error in
                // Проверка результатов запроса
                // Если ошибка - выведем сообщение пользователю
                if (error != nil) {DispatchQueue.main.async {self.notifyUser("Ошибка поиска",message: error!.localizedDescription)}}
                else {
                    // Если нашли записи
                    if results!.count > 0 {
                        let record = results![0]
                        // Удалим запись
                        self.privateDatabase?.delete(withRecordID: record.recordID, completionHandler: ({returnRecord, error in
                            if let err = error {DispatchQueue.main.async {self.notifyUser("Ошибка удаления", message: err.localizedDescription)}}
                            else {
                                DispatchQueue.main.async {
                                    self.notifyUser("Удаление записи", message: "Запись удалена")
                                    self.clearAll()
                                    }
                                }
                        }))
                       }
                    // Если найденных записей нет - сообщим пользователю
                    else {DispatchQueue.main.async {self.notifyUser("Удаление записи", message: "Запись не найдена!")}}
                }
        }))
    }
    
    // Создание таймера
    func createTimer(){
        timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: (#selector(ViewController.updateTimer)),
                                     userInfo: nil, repeats: true)
    }
    
    
    // Обновление таймера через секунду
    func updateTimer() {
        // Если флага останова трансляции нет - считаем
        if flagStop == false {
            if interval <= 60 {
                time.text = String(60 - interval)
                interval = interval + 1
            }
          //  print(String(format: "%02i", interval))
        }
    }
    
    // Запуск перевода
    func runTranslation() {
        print("Start")
        flagStop = false
        try! startSession()
        interval = 0
        transcribeButton.isEnabled = false
        stopButton.isEnabled = true
            }
    
    // Остановка преобразования
    func endTranslation() {
        // Установим флаг конца преобразования
        flagStop = true
        
        // Если преобразование запущено - остановим
        if engine.isRunning {
            print("Finished")
            engine.stop()
            speechRecognitionRequest?.endAudio()
        }
        
        // Установка видимости кнопок старт/стоп
        transcribeButton.isEnabled = true
        stopButton.isEnabled = false
        time.text = String(0)
    }
    
    // Функция декодирования месяца в цифры
    func decoderMonth(_ month:String) -> String {
        // Выделим три символа
        let predicate = String(month.characters.prefix(3)).lowercased()
       // print(predicate)
        switch predicate {
        case "янв"return "01"
        case "фев"return "02"
        case "мар"return "03"
        case "апр"return "04"
        case "мая","май"return "05"
        case "июн"return "06"
        case "июл"return "07"
        case "авг"return "08"
        case "сен"return "09"
        case "окт"return "10"
        case "ноя"return "11"
        case "дек"return "12"
        defaultreturn ""
        }
    }
    
    // Проверка заполняемости полей
    func checkFullPredicate() -> Bool {
        if  (fam.text! == "" || im.text! == "" || otch.text! == "" ) {
            notifyUser("Поисковый образ", message: "Заполните поля формы ФИО!")
            return false
        }
        else
        {return true}
    }

 
    // Очистка всех полей на форме
    func clearAll() {
        fam.text    = ""
        im.text     = ""
        otch.text   = ""
        dateBirth.text   = ""
        phone.text  = ""
        currentField = 0
    }
    
    // Функцмя закрытия клавиатуры по окончании редактирования
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        fam.endEditing(true)
        im.endEditing(true)
        otch.endEditing(true)
        dateBirth.endEditing(true)
        phone.endEditing(true)
    }
    
    // Функция освобождения ресурсов
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        timer.invalidate()
    }
    
    
    // Ооповещения пользователя с запуском трансляции
    func notifyUser(_ title: String, message: String) -> Void {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "OK", style: .cancel,
                                         handler: {(action) -> Void in self.selectTranscribleButton("")})
        alert.addAction(cancelAction)
        self.present(alert, animated: true, completion: nil)
    }
    
    // Функция авторизации
    func authorizeSR() {
        SFSpeechRecognizer.requestAuthorization { authStatus in
            OperationQueue.main.addOperation {
                switch authStatus {
                case .authorized:
                    self.transcribeButton.isEnabled = true
                case .denied:
                    self.transcribeButton.isEnabled = false
                case .restricted:
                    self.transcribeButton.isEnabled = false
                case .notDetermined:
                    self.transcribeButton.isEnabled = false
                }
            }
        }
    }
    
    // Авторизация на выдачу оповещений
    func authorizeNotification(){
        UNUserNotificationCenter.current().requestAuthorization(options:
            [[.alert, .sound, .badge]], completionHandler: { (granted, error) in  })
        UNUserNotificationCenter.current().delegate = self
    }
    
    // Функция оповещения
    func sendNotificationSave() {
        let content = UNMutableNotificationContent()
        content.title       = titleNotification
        content.subtitle    = subtitleNotification
        content.body        = bodyNotification
        content.badge = 1
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let requestIdentifier = "01/08/2017"
        let request = UNNotificationRequest(identifier: requestIdentifier,  content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request,  withCompletionHandler: { (error) in
            if error != nil { print ("error + \(error.debugDescription)")} })
    }
    
    // Вызов оповещения
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .sound])
    }
    
}


Текст файла ViewController.swift можно свободно загрузить.
Евгений Вересов.
10.07.2017 года.