Commit e6d24765 authored by Eugene Popov's avatar Eugene Popov
Browse files

Add speech algorithm

parent 64c71b62
No related merge requests found
Showing with 1555 additions and 0 deletions
+1555 -0
File added
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# Master Class AI
Репозиторий проекта, написанного в рамках мастер-класса **«AI-технологии: создаем приложение под iOS с функцией распознавания речи»** от IT-компании MediaSoft и онлайн-университета Skillbox.
**MediaSoft** — это команда из 250 разработчиков по направлениям backend, frontend, iOS/Android, QA и аналитика. Разрабатываем сложные веб-системы, бэкенды, мобильные приложения и highload-проекты с 2014 года. Офисы находятся в Ульяновске, Санкт-Петербург, Самаре и Ростове-на-Дону (Firecode).
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
//
// AppDelegate.swift
//
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
//
// SceneDelegate.swift
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
//
// Constants.swift
//
import Foundation
enum Constants {
static let text: String = "Все счастливые семьи похожи друг на друга, каждая несчастливая семья несчастлива по-своему. Все смешалось в доме Облонских. Жена узнала, что муж был в связи с бывшею в их доме француженкою-гувернанткой, и объявила мужу, что не может жить с ним в одном доме. Положение это продолжалось уже третий день и мучительно чувствовалось и самими супругами, и всеми членами семьи, и домочадцами."
}
//
// String+TR.swift
//
import UIKit
extension String {
var attributed: NSAttributedString {
NSAttributedString(string: self, attributes: [.font: UIFont.systemFont(ofSize: 14)])
}
func splitToWords() -> [Word] {
var words: [Word] = []
enumerateSubstrings(in: startIndex ..< endIndex, options: .byWords) { substring, range, _, _ in
if let substring = substring {
let offset = range.upperBound.distance(in: self)
words.append(Word(text: substring, offset: offset - substring.count))
}
}
return words
}
func getDifference(with etalon: String) -> Int {
count - getIntersection(with: etalon).count
}
func getIntersection(with etalon: String) -> [Character] {
let wordChars = Array(self)
let etalonChars = Array(etalon)
return Array(Set(wordChars).intersection(Set(etalonChars)))
}
}
extension StringProtocol {
func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) }
func distance<S: StringProtocol>(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) }
}
extension String.Index {
func distance<S: StringProtocol>(in string: S) -> Int { string.distance(to: self) }
}
extension Collection {
func distance(to index: Index) -> Int { distance(from: startIndex, to: index) }
}
extension String {
func levenshteinScore(to string: String, ignoreCase: Bool = true, trimWhiteSpacesAndNewLines: Bool = true) -> Float {
var firstString = self
var secondString = string
if ignoreCase {
firstString = firstString.lowercased()
secondString = secondString.lowercased()
}
if trimWhiteSpacesAndNewLines {
firstString = firstString.trimmingCharacters(in: .whitespacesAndNewlines)
secondString = secondString.trimmingCharacters(in: .whitespacesAndNewlines)
}
let empty: [Int] = Array(repeating: 0, count: secondString.count)
var last: [Int] = Array(0...secondString.count)
for (i, tLett) in firstString.enumerated() {
var cur = [i + 1] + empty
for (j, sLett) in secondString.enumerated() {
cur[j + 1] = tLett == sLett ? last[j] : Swift.min(last[j], last[j + 1], cur[j]) + 1
}
last = cur
}
let lowestScore = max(firstString.count, secondString.count)
if let validDistance = last.last {
return 1 - (Float(validDistance) / Float(lowestScore))
}
return 0.0
}
}
infix operator =~
func =~ (string: String, otherString: String) -> Bool {
string.levenshteinScore(to: otherString) >= 0.85
}
//
// UIView+TR.swift
//
import UIKit
extension UIView {
func shadow(
radius: CGFloat = 8,
offset: CGSize = CGSize(width: 0, height: 7),
opacity: Float = 1.0,
color: UIColor = UIColor.black.withAlphaComponent(0.2)
) {
layer.shadowRadius = radius
layer.shadowOpacity = opacity
layer.shadowColor = color.cgColor
layer.shadowOffset = offset
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone Usage Description</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech Recognition Usage Description</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SpeechText" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="2Hu-qN-2IP">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="stN-y1-2R1" customClass="UIControl">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="4Eh-D6-OCn">
<rect key="frame" x="16" y="16" width="382" height="412"/>
<color key="backgroundColor" systemColor="systemGray6Color"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="lmB-wt-Dej">
<rect key="frame" x="16" y="452" width="382" height="290"/>
<color key="backgroundColor" systemColor="systemGray6Color"/>
<constraints>
<constraint firstAttribute="height" constant="290" id="HNa-7U-cLU"/>
</constraints>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="e3N-BW-zFN">
<rect key="frame" x="16" y="766" width="382" height="44"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JeW-q7-rrc">
<rect key="frame" x="0.0" y="0.0" width="187" height="44"/>
<color key="backgroundColor" red="0.31226042550169747" green="0.6079287338502628" blue="0.32286651655986065" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<state key="normal" title="Старт">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="recognizeButtonTap:" destination="BYZ-38-t0r" eventType="touchUpInside" id="B7W-wv-Uda"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3qC-la-apD">
<rect key="frame" x="195" y="0.0" width="187" height="44"/>
<color key="backgroundColor" systemColor="systemGray5Color"/>
<state key="normal" title="Сброс">
<color key="titleColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="resetButtonTap:" destination="BYZ-38-t0r" eventType="touchUpInside" id="bfS-MJ-Wji"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="44" id="Ble-5R-Lgx"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="4Eh-D6-OCn" secondAttribute="trailing" constant="16" id="5zN-Sl-XeC"/>
<constraint firstAttribute="trailing" secondItem="e3N-BW-zFN" secondAttribute="trailing" constant="16" id="ARw-Pa-e09"/>
<constraint firstItem="e3N-BW-zFN" firstAttribute="top" secondItem="lmB-wt-Dej" secondAttribute="bottom" constant="24" id="Kaj-lQ-YRv"/>
<constraint firstItem="4Eh-D6-OCn" firstAttribute="top" secondItem="stN-y1-2R1" secondAttribute="top" constant="16" id="MFa-sm-jbN"/>
<constraint firstAttribute="trailing" secondItem="lmB-wt-Dej" secondAttribute="trailing" constant="16" id="P8O-QZ-B2u"/>
<constraint firstItem="e3N-BW-zFN" firstAttribute="leading" secondItem="stN-y1-2R1" secondAttribute="leading" constant="16" id="ZgY-fD-nkw"/>
<constraint firstAttribute="bottom" secondItem="e3N-BW-zFN" secondAttribute="bottom" constant="8" id="dgN-g4-lga"/>
<constraint firstItem="4Eh-D6-OCn" firstAttribute="leading" secondItem="stN-y1-2R1" secondAttribute="leading" constant="16" id="jYq-My-Qth"/>
<constraint firstItem="lmB-wt-Dej" firstAttribute="leading" secondItem="stN-y1-2R1" secondAttribute="leading" constant="16" id="lxp-qv-mby"/>
<constraint firstItem="lmB-wt-Dej" firstAttribute="top" secondItem="4Eh-D6-OCn" secondAttribute="bottom" constant="24" id="rX6-Fx-yYQ"/>
</constraints>
<connections>
<action selector="onScreenTap:" destination="BYZ-38-t0r" eventType="touchUpInside" id="gEj-Xw-e3N"/>
</connections>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="eV9-bP-K1w"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="stN-y1-2R1" firstAttribute="top" secondItem="eV9-bP-K1w" secondAttribute="top" id="LMV-UX-1eL"/>
<constraint firstItem="eV9-bP-K1w" firstAttribute="bottom" secondItem="stN-y1-2R1" secondAttribute="bottom" id="MNp-PK-g2H"/>
<constraint firstItem="eV9-bP-K1w" firstAttribute="trailing" secondItem="stN-y1-2R1" secondAttribute="trailing" id="vWK-tY-3Jh"/>
<constraint firstItem="stN-y1-2R1" firstAttribute="leading" secondItem="eV9-bP-K1w" secondAttribute="leading" id="vkS-Tk-WP8"/>
</constraints>
</view>
<connections>
<outlet property="inputTextView" destination="4Eh-D6-OCn" id="XRj-tZ-5rK"/>
<outlet property="outputTextView" destination="lmB-wt-Dej" id="Y82-Qd-pjX"/>
<outlet property="recognizeButton" destination="JeW-q7-rrc" id="PrF-0X-Z8o"/>
<outlet property="resetButton" destination="3qC-la-apD" id="0Ox-dU-qEb"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="137.68115942028987" y="118.52678571428571"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGray5Color">
<color red="0.89803921568627454" green="0.89803921568627454" blue="0.91764705882352937" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemGray6Color">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
//
// ViewController.swift
//
import UIKit
import Speech
final class ViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var inputTextView: UITextView!
@IBOutlet weak var outputTextView: UITextView!
@IBOutlet weak var recognizeButton: UIButton!
@IBOutlet weak var resetButton: UIButton!
// MARK: - Properties
private let text: String = Constants.text
private var recognizer: SpeechRecognizable = SpeechRecognizer()
private var words: [Word] = []
private var isRecognitionStarted: Bool = false {
didSet {
if isRecognitionStarted {
recognizeButton.setTitle("Стоп", for: .normal)
recognizeButton.backgroundColor = #colorLiteral(red: 0.7220714092, green: 0, blue: 0.1236564294, alpha: 1)
recognizer.start()
} else {
recognizeButton.setTitle("Старт", for: .normal)
recognizeButton.backgroundColor = #colorLiteral(red: 0.3411764801, green: 0.6235294342, blue: 0.1686274558, alpha: 1)
recognizer.stop()
}
}
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureView()
parseText()
recognizer.delegate = self
recognizer.requestPermissions { [weak self] success in
self?.recognizeButton.isEnabled = success
}
}
// MARK: - Private methods
private func configureView() {
inputTextView.layer.cornerRadius = 5
inputTextView.autocorrectionType = .no
inputTextView.attributedText = text.attributed
outputTextView.layer.cornerRadius = 5
outputTextView.autocorrectionType = .no
outputTextView.text = ""
recognizeButton.layer.cornerRadius = 8
recognizeButton.shadow()
recognizeButton.isEnabled = false
resetButton.layer.cornerRadius = 8
}
private func parseText() {
words = text.splitToWords()
}
private func find(_ word: String, in words: [Word], threshold: Float = 0.80) -> [Word] {
var matchedWords: [Word] = []
for mainWord in words {
var result = mainWord
if word == mainWord.text {
result.score = 1.0
matchedWords.append(result)
} else {
let score = word.levenshteinScore(to: mainWord.text)
if score >= threshold {
result.score = score
matchedWords.append(result)
}
}
}
return matchedWords
}
private func highlight(words: [Word]) {
let attributed = NSMutableAttributedString(string: text)
words.forEach {
attributed.addAttribute(.backgroundColor, value: UIColor.green, range: NSRange(location: $0.offset, length: $0.text.count))
}
attributed.addAttribute(.font, value: UIFont.systemFont(ofSize: 14), range: NSRange(location: 0, length: text.count))
inputTextView.attributedText = attributed
}
// MARK: - IBActions
@IBAction func recognizeButtonTap(_ sender: Any) {
isRecognitionStarted.toggle()
}
@IBAction func resetButtonTap(_ sender: Any) {
inputTextView.attributedText = text.attributed
outputTextView.text = ""
parseText()
}
@IBAction func onScreenTap(_ sender: Any) {
view.endEditing(true)
}
}
extension ViewController: SpeechRecognizableDelegate {
func output(result: SFSpeechRecognitionResult, isFinal: Bool) {
guard !isFinal else {
print("All readed text:", result.bestTranscription.formattedString)
return
}
let words = result.bestTranscription.segments.compactMap({ $0.substring })
print("Readed words:", words.joined(separator: ", "))
DispatchQueue.main.async { [weak self] in
guard let self = self, let word = words.last else {
return
}
self.highlight(words: self.find(word, in: self.words))
}
}
}
//
// Word.swift
//
import Foundation
struct Word {
var text: String = ""
var offset: Int = 0
var score: Float = 0.0
}
//
// SpeechRecognizer.swift
//
import Foundation
import Speech
protocol SpeechRecognizable {
var delegate: SpeechRecognizableDelegate? { get set }
func requestPermissions(complition: ((Bool) -> Void)?)
func start()
func stop()
}
protocol SpeechRecognizableDelegate: AnyObject {
func output(result: SFSpeechRecognitionResult, isFinal: Bool)
}
final class SpeechRecognizer: NSObject, SpeechRecognizable {
// MARK: - Propreties
weak var delegate: SpeechRecognizableDelegate?
private let recognizer: SFSpeechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru-RU"))!
private var request: SFSpeechAudioBufferRecognitionRequest?
private var task: SFSpeechRecognitionTask?
private let engine = AVAudioEngine()
// MARK: - Public methods
func requestPermissions(complition: ((Bool) -> Void)?) {
requestMicrophonePermission { [weak self] success in
if success {
self?.requestSpeechPermission { success in
complition?(success)
}
} else {
complition?(success)
}
}
}
func start() {
guard !engine.isRunning else {
return
}
do {
try startRecording()
} catch {
print("Recording Not Available")
}
}
func stop() {
guard engine.isRunning else {
return
}
engine.inputNode.removeTap(onBus: 0)
engine.stop()
request?.endAudio()
}
// MARK: - Private methods
private func requestMicrophonePermission(complition: ((Bool) -> Void)?) {
AVAudioSession.sharedInstance().requestRecordPermission { success in
OperationQueue.main.addOperation {
complition?(success)
}
print("Microphone status permission:", success)
}
}
private func requestSpeechPermission(complition: ((Bool) -> Void)?) {
SFSpeechRecognizer.requestAuthorization { authStatus in
OperationQueue.main.addOperation {
complition?(authStatus == .authorized)
}
print("Speech status permission:", authStatus == .authorized)
}
}
private func startRecording() throws {
task?.cancel()
self.task = nil
let session = AVAudioSession.sharedInstance()
try session.setCategory(.record, mode: .measurement)
try session.setActive(true, options: .notifyOthersOnDeactivation)
let node = engine.inputNode
request = SFSpeechAudioBufferRecognitionRequest()
guard let recognitionRequest = request else {
print("SFSpeechAudioBufferRecognitionRequest: unable to create")
return
}
recognitionRequest.shouldReportPartialResults = true
recognitionRequest.requiresOnDeviceRecognition = false
task = recognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
guard let self = self else {
return
}
var isFinal = false
if let result = result {
isFinal = result.isFinal
self.delegate?.output(result: result, isFinal: isFinal)
}
if error != nil || isFinal {
self.engine.stop()
node.removeTap(onBus: 0)
self.request = nil
self.task = nil
}
}
let format = node.outputFormat(forBus: 0)
node.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer , _ in
self.request?.append(buffer)
}
engine.prepare()
try engine.start()
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment