143 lines
4.8 KiB
Swift
143 lines
4.8 KiB
Swift
//
|
||
// VehicleRecordService.swift
|
||
// AutoCatCore
|
||
//
|
||
// Created by Selim Mustafaev on 16.03.2025.
|
||
// Copyright © 2025 Selim Mustafaev. All rights reserved.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
public actor VehicleRecordService {
|
||
|
||
let recordService: AudioRecordServiceProtocol
|
||
let locationService: LocationServiceProtocol
|
||
let settingsService: SettingsServiceProtocol
|
||
|
||
let validLetters = Constants.pnLettersMap.keys.map(String.init)
|
||
|
||
var url: URL?
|
||
var date = Date()
|
||
var location: VehicleEventDto?
|
||
|
||
@AutoCancellable
|
||
var locationTask: Task<Void,Error>?
|
||
|
||
public init(
|
||
recordService: AudioRecordServiceProtocol,
|
||
locationService: LocationServiceProtocol,
|
||
settingsService: SettingsServiceProtocol
|
||
) {
|
||
self.recordService = recordService
|
||
self.locationService = locationService
|
||
self.settingsService = settingsService
|
||
}
|
||
|
||
func getPlateNumber(from recognizedText: String?) -> String? {
|
||
guard let recognizedText else {
|
||
return nil
|
||
}
|
||
|
||
let trimmed = recognizedText
|
||
.replacingOccurrences(of: " ", with: "")
|
||
.uppercased()
|
||
.replacingOccurrences(of: "Ф", with: "В")
|
||
.replacingOccurrences(of: "НОЛЬ", with: "0")
|
||
.replacingOccurrences(of: "Э", with: "")
|
||
|
||
var result = ""
|
||
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
|
||
result = String(trimmed[range])
|
||
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), settingsService.recognizeAlternativeOrder {
|
||
let n = String(trimmed[range])
|
||
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
|
||
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), settingsService.recognizeShortenedNumbers {
|
||
result = String(trimmed[range]) + settingsService.defaultRegion
|
||
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), settingsService.recognizeAlternativeOrder && settingsService.recognizeShortenedNumbers {
|
||
let n = String(trimmed[range])
|
||
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + settingsService.defaultRegion
|
||
}
|
||
|
||
if !result.isEmpty && valid(number: result) {
|
||
return result
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func valid(number: String) -> Bool {
|
||
guard number.count >= 8 else { return false }
|
||
|
||
let first = String(number.prefix(1))
|
||
let second = number.substring(with: 4..<5)
|
||
let third = number.substring(with: 5..<6)
|
||
let digits = Int(number.substring(with: 1..<4))
|
||
let region = Int(number.substring(from: 6))
|
||
|
||
return self.validLetters.contains(first)
|
||
&& self.validLetters.contains(second)
|
||
&& self.validLetters.contains(third)
|
||
&& digits != nil
|
||
&& region != nil
|
||
&& region! < 1000
|
||
}
|
||
}
|
||
|
||
extension VehicleRecordService: VehicleRecordServiceProtocol {
|
||
|
||
public func requestPermissionsIfNeeded() async {
|
||
|
||
await recordService.requestRecordPermissions()
|
||
await recordService.requestRecognitionAuthorization()
|
||
}
|
||
|
||
public func startRecording() async throws {
|
||
|
||
date = Date()
|
||
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
|
||
let url = try FileManager.default.url(for: fileName, in: "recordings")
|
||
self.url = url
|
||
|
||
try await recordService.startRecording(to: url)
|
||
locationTask = Task {
|
||
location = try await locationService.getRecentLocation()
|
||
}
|
||
}
|
||
|
||
public func stopRecording() async throws -> AudioRecordDto {
|
||
guard let url else {
|
||
await recordService.cancelRecording()
|
||
throw VehicleRecordError.emptyUrl
|
||
}
|
||
|
||
await recordService.stopRecording()
|
||
|
||
async let recognitionTask = recordService.recognizeText(from: url)
|
||
async let durationTask = recordService.getDuration(from: url)
|
||
|
||
let (text, duration) = await (recognitionTask, try? durationTask)
|
||
|
||
locationTask?.cancel()
|
||
locationTask = nil
|
||
self.url = nil
|
||
|
||
let record = AudioRecordDto(
|
||
path: url.lastPathComponent,
|
||
number: getPlateNumber(from: text),
|
||
raw: text ?? "",
|
||
duration: duration ?? 0,
|
||
event: location
|
||
)
|
||
|
||
return record
|
||
}
|
||
|
||
public func cancelRecording() async {
|
||
|
||
await recordService.cancelRecording()
|
||
locationTask?.cancel()
|
||
locationTask = nil
|
||
url = nil
|
||
}
|
||
}
|