Ускорить локальную диктовку стабильным префиксом
This commit is contained in:
@@ -6,6 +6,11 @@ struct RecordedChunk: Sendable {
|
||||
let continuesPhrase: Bool
|
||||
}
|
||||
|
||||
enum AudioChunking: Sendable {
|
||||
case stablePreviews
|
||||
case committed
|
||||
}
|
||||
|
||||
final class AudioRecorder: @unchecked Sendable {
|
||||
private var engine: AVAudioEngine?
|
||||
private let lock = NSLock()
|
||||
@@ -21,8 +26,10 @@ final class AudioRecorder: @unchecked Sendable {
|
||||
private var onChunk: (@Sendable (RecordedChunk) -> Void)?
|
||||
private var onSilence: (@Sendable () -> Void)?
|
||||
private var recordingFormat: AVAudioFormat?
|
||||
private var chunking = AudioChunking.committed
|
||||
|
||||
func start(
|
||||
chunking: AudioChunking,
|
||||
onChunk: @escaping @Sendable (RecordedChunk) -> Void,
|
||||
onSilence: @escaping @Sendable () -> Void
|
||||
) throws {
|
||||
@@ -33,6 +40,7 @@ final class AudioRecorder: @unchecked Sendable {
|
||||
let format = input.outputFormat(forBus: 0)
|
||||
self.onChunk = onChunk
|
||||
self.onSilence = onSilence
|
||||
self.chunking = chunking
|
||||
recordingFormat = format
|
||||
try beginChunk(format: format)
|
||||
input.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] buffer, _ in
|
||||
@@ -130,15 +138,22 @@ final class AudioRecorder: @unchecked Sendable {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep enough context for Whisper, but insert completed phrases while
|
||||
// the user is still dictating. Prefer a short natural pause; cap a
|
||||
// continuous phrase so the input never stays blank indefinitely.
|
||||
let minimumChunk = AVAudioFramePosition(format.sampleRate * 2.5)
|
||||
let pauseBoundary = AVAudioFramePosition(format.sampleRate * 1.0)
|
||||
let maximumChunk = AVAudioFramePosition(format.sampleRate * 4.0)
|
||||
let reachedPause =
|
||||
currentFrames >= minimumChunk && chunkSilentFrames >= pauseBoundary
|
||||
let reachedMaximum = currentFrames >= maximumChunk
|
||||
let reachedPause: Bool
|
||||
let reachedMaximum: Bool
|
||||
switch chunking {
|
||||
case .stablePreviews:
|
||||
reachedPause = false
|
||||
reachedMaximum = currentFrames >= AVAudioFramePosition(format.sampleRate * 1.25)
|
||||
case .committed:
|
||||
// Cloud dictation keeps the old larger chunks so it does not issue
|
||||
// overlapping paid requests.
|
||||
let minimumChunk = AVAudioFramePosition(format.sampleRate * 2.5)
|
||||
let pauseBoundary = AVAudioFramePosition(format.sampleRate * 1.0)
|
||||
let maximumChunk = AVAudioFramePosition(format.sampleRate * 4.0)
|
||||
reachedPause =
|
||||
currentFrames >= minimumChunk && chunkSilentFrames >= pauseBoundary
|
||||
reachedMaximum = currentFrames >= maximumChunk
|
||||
}
|
||||
let shouldRotate = currentChunkHasSpeech && (reachedPause || reachedMaximum)
|
||||
|
||||
if shouldRotate, let finishedURL = currentURL {
|
||||
@@ -174,6 +189,13 @@ final class AudioRecorder: @unchecked Sendable {
|
||||
|
||||
enum AudioConverter {
|
||||
static func toWhisperWAV(_ input: URL) throws -> URL {
|
||||
try toWhisperWAV([input])
|
||||
}
|
||||
|
||||
static func toWhisperWAV(_ inputs: [URL]) throws -> URL {
|
||||
guard !inputs.isEmpty else {
|
||||
throw WhisperKeyError.processFailed("Нет аудиофрагментов для распознавания.")
|
||||
}
|
||||
let output = FileManager.default.temporaryDirectory.appendingPathComponent("whisperkey-\(UUID().uuidString).wav")
|
||||
let ffmpegCandidates = ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"]
|
||||
guard let ffmpeg = ffmpegCandidates.first(where: FileManager.default.isExecutableFile(atPath:)) else {
|
||||
@@ -181,7 +203,26 @@ enum AudioConverter {
|
||||
}
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: ffmpeg)
|
||||
process.arguments = ["-hide_banner", "-loglevel", "error", "-y", "-i", input.path, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", output.path]
|
||||
var arguments = ["-hide_banner", "-loglevel", "error", "-y"]
|
||||
for input in inputs {
|
||||
arguments.append(contentsOf: ["-i", input.path])
|
||||
}
|
||||
if inputs.count > 1 {
|
||||
let streams = inputs.indices.map { "[\($0):a:0]" }.joined()
|
||||
arguments.append(contentsOf: [
|
||||
"-filter_complex",
|
||||
"\(streams)concat=n=\(inputs.count):v=0:a=1[out]",
|
||||
"-map",
|
||||
"[out]",
|
||||
])
|
||||
}
|
||||
arguments.append(contentsOf: [
|
||||
"-ar", "16000",
|
||||
"-ac", "1",
|
||||
"-c:a", "pcm_s16le",
|
||||
output.path,
|
||||
])
|
||||
process.arguments = arguments
|
||||
let stderr = Pipe()
|
||||
process.standardError = stderr
|
||||
try process.run()
|
||||
|
||||
Reference in New Issue
Block a user