itak 3 → 4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +221 -3
- data/bin/itak +18 -3
- data/itak.gemspec +4 -1
- data/lib/itak/audio.rb +36 -0
- data/lib/itak/awaaz_patch.rb +8 -0
- data/lib/itak/callable.rb +22 -0
- data/lib/itak/debuggable.rb +32 -0
- data/lib/itak/process/denoise.rb +17 -0
- data/lib/itak/process/down_mix.rb +13 -0
- data/lib/itak/process/normalize_loudness.rb +40 -0
- data/lib/itak/process/resample.rb +18 -0
- data/lib/itak/process/save.rb +15 -0
- data/lib/itak/process/transcribe.rb +47 -0
- data/lib/itak/process/vad.rb +33 -0
- data/lib/itak/process.rb +8 -0
- data/lib/itak/processible.rb +34 -0
- data/lib/itak.rb +29 -31
- metadata +57 -5
- data/lib/itak/denoiser.rb +0 -13
- data/lib/itak/transcriber.rb +0 -39
- data/lib/itak/vad.rb +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f5c18bc3e593097a8a4efac8f78432ccf2db6b7771ba740dae975f12b0f12f3
|
|
4
|
+
data.tar.gz: b8e1a1c9e26a8427116515243703e889171b6a5f49d4c42285a89b19b63749e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8fc23e257aa870f7337ef05e6f930f96713402881c2f3290dececdbf2792522a1d8f9f4394e1e00f43108bd13c5498ee4e141409cee65f470dfcd28d9c01f03
|
|
7
|
+
data.tar.gz: 45e7516e05441624c7871c900097f12472e450bf0d557e9a81e7ef0c0f8512f9cc5d59dad0a88d4090db3f31f36b3ba5fef1d32c53e8fffce4cb2cdd35b0c9ce
|
data/README.md
CHANGED
|
@@ -3,21 +3,239 @@ Itak
|
|
|
3
3
|
|
|
4
4
|
Audio editing tool for podcasters.
|
|
5
5
|
|
|
6
|
+
[](https://badge.fury.io/rb/itak)
|
|
7
|
+
|
|
6
8
|
It does:
|
|
7
9
|
|
|
10
|
+
* normalize loudness,
|
|
8
11
|
* remove noise, and
|
|
9
|
-
* remove non-
|
|
12
|
+
* remove non-speech period
|
|
10
13
|
|
|
11
14
|
from the audio file.
|
|
12
15
|
|
|
13
16
|
SYNOPSIS
|
|
14
17
|
--------
|
|
15
18
|
|
|
16
|
-
% itak path/to/audio.wav --output=path/to/
|
|
19
|
+
% itak path/to/audio.wav --output=path/to/dir
|
|
20
|
+
Output written to
|
|
21
|
+
* path/to/dir/audio.denoised-vad.wav
|
|
22
|
+
* path/to/dir/audio.denoised-vad.txt
|
|
23
|
+
|
|
24
|
+
INSTALLATION
|
|
25
|
+
------------
|
|
26
|
+
|
|
27
|
+
% gem install itak
|
|
28
|
+
|
|
29
|
+
Or,
|
|
30
|
+
|
|
31
|
+
% bundle add itak
|
|
32
|
+
|
|
33
|
+
CLI
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
The `itak` command accepts an audio path and create an edited audio file.
|
|
37
|
+
|
|
38
|
+
By default, output path is created by inserting `.vad-denoised` before the file extension: `path/to/audio.mp3` -> `path/to/audio.vad-denoised.mp3`. You can specify the output path by `--output` (`-o` shortly) option:
|
|
39
|
+
|
|
40
|
+
# Specifies directory
|
|
41
|
+
itak path/to/audio --output=./
|
|
42
|
+
# Specifies file
|
|
43
|
+
itak path/to/audio.wav --output=./output.wav
|
|
44
|
+
|
|
45
|
+
LIBRARY
|
|
46
|
+
-------
|
|
47
|
+
|
|
48
|
+
Though Itak is intended to be used as CLI, it provides some useful classes for editing podcast audio.
|
|
49
|
+
|
|
50
|
+
*Note* that these classes and APIs might change frequently.
|
|
51
|
+
|
|
52
|
+
### `Audio` ###
|
|
53
|
+
|
|
54
|
+
`Itak::Audio` is a model of audio data. Attributes are:
|
|
55
|
+
|
|
56
|
+
* `waveform`: Multi-dimensional array containing the waveform. Currently, it is a `Torch::Tensor`
|
|
57
|
+
* `sample_rate`: Sample rate
|
|
58
|
+
* `layout`: Audio layout. `:interleaved` or `:planar`
|
|
59
|
+
* `channels`: The number of channels
|
|
60
|
+
|
|
61
|
+
*Note* that attributes are *not* kept consistent automatically. You are responsible for updating them when modifying the audio data:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
audio.waveform = ...
|
|
65
|
+
audio.sample_rate = ...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Also, it has methods to load and save audio:
|
|
69
|
+
|
|
70
|
+
* `Audio.load(path)` loads audio from `path` and returns `Itak::Audio` instance.
|
|
71
|
+
* `Audio#save(path)` saves audio to `path`.
|
|
72
|
+
|
|
73
|
+
It uses [TorchAudio Ruby][] internally, so supports formats which TorchAudio supports.
|
|
74
|
+
|
|
75
|
+
`Itak::Audio` is a base of processing audio. It chains processes like this:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
include Itak::Process
|
|
79
|
+
|
|
80
|
+
Itak::Audio.load("path/to/audio")
|
|
81
|
+
.(&NormalizeLoudness)
|
|
82
|
+
.(&DownMix)
|
|
83
|
+
.(&Resample[to: 16_000])
|
|
84
|
+
.(&Denoise)
|
|
85
|
+
.(&VAD)
|
|
86
|
+
.>>(&Save[to: "path/to/output"])
|
|
87
|
+
.>>(&Transcribe[dest: "path/to/transcription.txt"])
|
|
88
|
+
.<<
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
*Note* that process classes below mutate `Itak::Audio` object.
|
|
92
|
+
|
|
93
|
+
Processes below are under the `Itak::Process` namespace. So, `include`ing it before use is useful.
|
|
94
|
+
|
|
95
|
+
### `NormalizeLoudness` ###
|
|
96
|
+
|
|
97
|
+
`Itak::Process::NormalizeLoudness` normalizes audio loudness using EBU R128.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
include Itak::Process
|
|
101
|
+
|
|
102
|
+
audio = Itak::Audio.load("path/to/audio")
|
|
103
|
+
audio.(&NormalizeLoudness) # => Normalized audio
|
|
104
|
+
# Or
|
|
105
|
+
NormalizeLoudness.new
|
|
106
|
+
.run(audio)
|
|
107
|
+
audio # => Normalized audio
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can configure target loudness (in LUFS) and the true peak ceiling (in dBTP):
|
|
17
111
|
|
|
18
|
-
|
|
112
|
+
```ruby
|
|
113
|
+
audio.(&NormalizeLoudness[target: -16.0, true_peak_ceiling: -1.0])
|
|
114
|
+
# Or
|
|
115
|
+
NormalizeLoudness.new(target: -16.0, true_peak_ceiling: -1.0)
|
|
116
|
+
.run(audio)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `DownMix` ###
|
|
120
|
+
|
|
121
|
+
`Itak::Process::DownMix` down-mixes audio to mono.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
audio.(&DownMix) # => Mono audio
|
|
125
|
+
# Or
|
|
126
|
+
DownMix.new
|
|
127
|
+
.run(audio)
|
|
128
|
+
audio # => Mono audio
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `Resample` ###
|
|
132
|
+
|
|
133
|
+
`Itak::Process::Resample` resamples audio.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
audio.(&Resample[to: 16_000]) # => Audio with 16,000Hz sample rate
|
|
137
|
+
# Or
|
|
138
|
+
Resample.new(to: 16_000)
|
|
139
|
+
.run(audio)
|
|
140
|
+
audio # => Resampled audio
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `Denoise` ###
|
|
144
|
+
|
|
145
|
+
`Itak::Process::Denoise` denoises from audio.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
audio.(&Denoise) # => Denoised audio
|
|
149
|
+
# Or
|
|
150
|
+
Denoise.new
|
|
151
|
+
.run(audio)
|
|
152
|
+
audio # => Denoised audio
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `VAD` ###
|
|
156
|
+
|
|
157
|
+
`Itak::Process::VAD` detects voice activity and removes non-speech periods from the audio.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
audio.(&VAD) # => Audio without silent periods
|
|
161
|
+
# Or
|
|
162
|
+
VAD.new
|
|
163
|
+
.run(audio)
|
|
164
|
+
audio # => Audio without silent periods
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`Itak::Process::VAD` uses [Silero VAD][] via [whispercpp][] internally. You can specify the model and configure params:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
|
|
171
|
+
params = Whisper::VAD::Params.new(
|
|
172
|
+
threshold: 0.1, # Threshold to consider a frame as speech
|
|
173
|
+
min_silence_duration_ms: 3000 # Considers a segment is silent when silence continues 3 seconds.
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
audio.(&VAD[model: "silero-v5.1.2", params: params])
|
|
177
|
+
# Or
|
|
178
|
+
VAD.new(model: "silero-v5.1.2", params: params)
|
|
179
|
+
.run(audio)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
See `Whisper::VAD::Params` in the [signature file][whisper-sig] for available params.
|
|
183
|
+
|
|
184
|
+
### `Save` ###
|
|
185
|
+
|
|
186
|
+
`Itak::Process::Save` saves audio to a file.
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
audio.(&Save[to: "path/to/output"])
|
|
190
|
+
# Or
|
|
191
|
+
Save.new(to: "path/to/output")
|
|
192
|
+
.run(audio)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This is equivalent to:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
audio.save "path/to/output"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `Transcribe` ###
|
|
202
|
+
|
|
203
|
+
`Itak::Process::Transcribe` transcribes from audio and output to a file if needed.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
audio.(&Transcribe[dest: "path/to/output.txt"]) # => Audio, not transcription
|
|
207
|
+
# Or
|
|
208
|
+
transcription = Transcribe.new(dest: "path/to/output.txt")
|
|
209
|
+
.run(audio)
|
|
210
|
+
puts transcription
|
|
211
|
+
# [00:00:00 --> 00:00:30] Transcription text here
|
|
212
|
+
#
|
|
213
|
+
# [00:00:30 --> 00:00:60] Transcription text here
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Blank lines are inserted between transcription segments to make the output file easier to import into Notion.
|
|
217
|
+
|
|
218
|
+
`Itak::Process::Transcribe` uses [whispercpp][] internally. You can specify the model and configure params:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
params = {
|
|
222
|
+
language: "de",
|
|
223
|
+
temperature: 0.5
|
|
224
|
+
}
|
|
225
|
+
audio.(&Transcribe[model: "tiny", params: params, dest: "path/to/output.txt"])
|
|
226
|
+
# Or
|
|
227
|
+
Transcribe.new(model: "tiny", params: params, dest: "path/to/output.txt")
|
|
228
|
+
.run(audio)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
See `Whisper::Params` in the [signature file][whisper-sig] for available params.
|
|
19
232
|
|
|
20
233
|
LICENSE
|
|
21
234
|
-------
|
|
22
235
|
|
|
23
236
|
AGPL 3.0 or later. See the file agpl-3.0.txt.
|
|
237
|
+
|
|
238
|
+
[TorchAudio Ruby]: https://github.com/ankane/torchaudio-ruby
|
|
239
|
+
[Silero VAD]: https://github.com/snakers4/silero-vad
|
|
240
|
+
[whispercpp]: https://github.com/ggml-org/whisper.cpp/tree/master/bindings/ruby
|
|
241
|
+
[whisper-sig]: https://github.com/ggml-org/whisper.cpp/blob/master/bindings/ruby/sig/whisper.rbs
|
data/bin/itak
CHANGED
|
@@ -2,15 +2,23 @@ require "optparse"
|
|
|
2
2
|
require "optparse/pathname"
|
|
3
3
|
require "itak"
|
|
4
4
|
|
|
5
|
+
Version = Gem::Specification.find_by_name("itak").version
|
|
6
|
+
|
|
5
7
|
def main(argv)
|
|
6
8
|
options = parse_options(argv)
|
|
7
9
|
src = argv.shift
|
|
8
10
|
abort "Specify input file" unless src
|
|
9
|
-
|
|
11
|
+
outputs = Itak.new(src, options[:output]).run
|
|
10
12
|
$stderr.puts "Output written to"
|
|
11
|
-
|
|
13
|
+
outputs.each do |output|
|
|
14
|
+
$stderr.puts "* #{output}"
|
|
15
|
+
end
|
|
12
16
|
rescue => err
|
|
13
|
-
|
|
17
|
+
if $DEBUG
|
|
18
|
+
raise
|
|
19
|
+
else
|
|
20
|
+
abort err.message
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def parse_options(argv)
|
|
@@ -21,6 +29,13 @@ def parse_options(argv)
|
|
|
21
29
|
|
|
22
30
|
Usage: itak [options] INPUT
|
|
23
31
|
EOB
|
|
32
|
+
opt.on "-d", "--debug", "Set debugging flag" do
|
|
33
|
+
$DEBUG = true
|
|
34
|
+
$VERBOSE = true
|
|
35
|
+
end
|
|
36
|
+
opt.on "--verbose", "Set verbose flag" do
|
|
37
|
+
$VERBOSE = true
|
|
38
|
+
end
|
|
24
39
|
opt.on "-o", "--output=PATH", "Specify output file or directory", Pathname do |path|
|
|
25
40
|
options[:output] = path
|
|
26
41
|
end
|
data/itak.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = "itak"
|
|
3
|
-
s.version = "
|
|
3
|
+
s.version = "4"
|
|
4
4
|
s.authors = ["Kitaiti Makoto"]
|
|
5
5
|
s.email = ["KitaitiMaokto@gmail.com"]
|
|
6
6
|
s.summary = "Audio editing tool for podcasters"
|
|
@@ -18,6 +18,9 @@ Gem::Specification.new do |s|
|
|
|
18
18
|
s.add_runtime_dependency "torch-rb"
|
|
19
19
|
s.add_runtime_dependency "optparse-pathname"
|
|
20
20
|
s.add_runtime_dependency "ndav-torch-tensor"
|
|
21
|
+
s.add_runtime_dependency "ebur128_stream"
|
|
22
|
+
s.add_runtime_dependency "awaaz"
|
|
23
|
+
s.add_runtime_dependency "ndav-numo-narray"
|
|
21
24
|
|
|
22
25
|
s.add_development_dependency "rake"
|
|
23
26
|
s.add_development_dependency "rubygems-tasks"
|
data/lib/itak/audio.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "itak/callable"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
class Audio
|
|
5
|
+
prepend Callable
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def load(path, **)
|
|
9
|
+
waveform, sample_rate = TorchAudio.load(path.to_path)
|
|
10
|
+
new(waveform, sample_rate:, layout: :planar, **)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_accessor :waveform, :sample_rate, :layout
|
|
15
|
+
attr_reader :debug_info
|
|
16
|
+
|
|
17
|
+
def initialize(waveform, sample_rate:, layout:, debug_info: nil)
|
|
18
|
+
@waveform = waveform
|
|
19
|
+
@sample_rate = sample_rate
|
|
20
|
+
@layout = layout
|
|
21
|
+
@debug_info = debug_info
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def channels
|
|
25
|
+
shape = @waveform.shape
|
|
26
|
+
return 1 if shape.length == 1
|
|
27
|
+
|
|
28
|
+
layout == :interleaved ? shape[-1] : shape[0]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save(path)
|
|
32
|
+
TorchAudio.save path.to_path, waveform, sample_rate
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module AwaazResampeFix
|
|
2
|
+
def read_and_resample(input_samples, input_rate, output_rate, channels, sampling_option: :sinc_fastest)
|
|
3
|
+
samples = super
|
|
4
|
+
samples.reshape!(channels, samples.shape[0] / channels)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Awaaz::Utils::Resample.singleton_class.prepend self
|
|
8
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Itak
|
|
2
|
+
module Callable
|
|
3
|
+
def initialize(*, **, &)
|
|
4
|
+
super
|
|
5
|
+
@threads = []
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
alias call tap
|
|
9
|
+
|
|
10
|
+
def >>(&blk)
|
|
11
|
+
@threads << Thread.new(blk) {call(&blk)}
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def <<
|
|
16
|
+
while thread = @threads.shift
|
|
17
|
+
thread.join
|
|
18
|
+
end
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "pp"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Debuggable
|
|
5
|
+
def debug_log_start
|
|
6
|
+
return unless $VERBOSE
|
|
7
|
+
|
|
8
|
+
$stderr.puts "[#{self.class}] Started..."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def debug_inspect(obj)
|
|
12
|
+
return unless $VERBOSE
|
|
13
|
+
|
|
14
|
+
$stderr.puts "[#{self.class}]"
|
|
15
|
+
PP.pp obj, $stderr
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def debug_save(audio)
|
|
19
|
+
return unless $DEBUG
|
|
20
|
+
return unless audio.debug_info
|
|
21
|
+
src = audio.debug_info[:src]
|
|
22
|
+
dest = audio.debug_info[:dest]
|
|
23
|
+
return if src.nil? || dest.nil?
|
|
24
|
+
|
|
25
|
+
id = self.class.to_s.split("::").last.downcase
|
|
26
|
+
|
|
27
|
+
path = dest.dirname/src.basename.sub_ext(".#{id}" + src.extname)
|
|
28
|
+
audio.save path
|
|
29
|
+
$stderr.puts "[#{self.class}] Saved to #{path}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "gtcrn"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Process
|
|
5
|
+
class Denoise
|
|
6
|
+
prepend Processible
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@gtcrn = GTCRN.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(audio)
|
|
13
|
+
audio.waveform = @gtcrn.enhance_speech_waveform(audio.waveform)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "ebur128_stream"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Process
|
|
5
|
+
class NormalizeLoudness
|
|
6
|
+
prepend Processible
|
|
7
|
+
|
|
8
|
+
def initialize(target: -14.0, true_peak_ceiling: -0.1)
|
|
9
|
+
@target = target
|
|
10
|
+
@true_peak_ceiling= true_peak_ceiling
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(audio)
|
|
14
|
+
channels = case audio.channels
|
|
15
|
+
in 1
|
|
16
|
+
[:center]
|
|
17
|
+
in 2
|
|
18
|
+
[:left, :right]
|
|
19
|
+
end
|
|
20
|
+
normalizer = EBUR128Stream::Normalizer.new(
|
|
21
|
+
sample_rate: audio.sample_rate,
|
|
22
|
+
channels:,
|
|
23
|
+
target_lufs: @target,
|
|
24
|
+
true_peak_ceiling_dbtp: @true_peak_ceiling
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
samples = audio.waveform.transpose(1, 0)
|
|
28
|
+
shape = samples.shape
|
|
29
|
+
samples = samples.reshape([shape.reduce(:*)])
|
|
30
|
+
report = normalizer.normalize_in_place(samples)
|
|
31
|
+
samples = samples.reshape(shape).transpose(0, 1)
|
|
32
|
+
audio.waveform = samples
|
|
33
|
+
|
|
34
|
+
debug_inspect report
|
|
35
|
+
|
|
36
|
+
report
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class Itak
|
|
2
|
+
module Process
|
|
3
|
+
class Resample
|
|
4
|
+
include NDAV::Converter
|
|
5
|
+
prepend Processible
|
|
6
|
+
|
|
7
|
+
def initialize(to:)
|
|
8
|
+
@to = to
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(audio)
|
|
12
|
+
samples = NumoNArray(audio.waveform)
|
|
13
|
+
audio.waveform = Awaaz::Utils::Resample.read_and_resample(samples, audio.sample_rate, @to, audio.channels).to_ndav.to_torch_tensor
|
|
14
|
+
audio.sample_rate = @to
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "whisper"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Process
|
|
5
|
+
class Transcribe
|
|
6
|
+
prepend Processible
|
|
7
|
+
|
|
8
|
+
PARAMS = {
|
|
9
|
+
language: "ja",
|
|
10
|
+
temperature: 1.0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def initialize(model: "large-v3-turbo-q8_0", dest:, params: {})
|
|
14
|
+
Whisper.log_set proc {}, nil
|
|
15
|
+
@whisper = Whisper::Context.new(model)
|
|
16
|
+
@params = Whisper::Params.new(**PARAMS.merge(params))
|
|
17
|
+
@dest = dest
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run(audio)
|
|
21
|
+
src = audio.waveform
|
|
22
|
+
src = src[0] if src.ndim == 2 && src.shape[0] == 1
|
|
23
|
+
transcription = @whisper
|
|
24
|
+
.full(@params, src.to_ndav)
|
|
25
|
+
.each_segment
|
|
26
|
+
.collect {|segment|
|
|
27
|
+
"[%<start_time>s --> %<end_time>s]%<text>s" % {
|
|
28
|
+
start_time: format_time(segment.start_time),
|
|
29
|
+
end_time: format_time(segment.end_time),
|
|
30
|
+
text: segment.text
|
|
31
|
+
}
|
|
32
|
+
}.join("\n\n")
|
|
33
|
+
@dest.write transcription
|
|
34
|
+
transcription
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_time(time_ms)
|
|
40
|
+
sec, decimal_part = time_ms.divmod(1000)
|
|
41
|
+
min, sec = sec.divmod(60)
|
|
42
|
+
hour, min = min.divmod(60)
|
|
43
|
+
"%02d:%02d:%02d" % [hour, min, sec]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "whisper"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Process
|
|
5
|
+
class VAD
|
|
6
|
+
prepend Processible
|
|
7
|
+
|
|
8
|
+
PARAMS = Whisper::VAD::Params.new(
|
|
9
|
+
threshold: 0.7,
|
|
10
|
+
min_silence_duration_ms: 1500
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def initialize(model: "silero-v6.2.0", params: PARAMS)
|
|
14
|
+
Whisper.log_set proc {}, nil
|
|
15
|
+
@vad = Whisper::VAD::Context.new(model)
|
|
16
|
+
@params = params
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(audio)
|
|
20
|
+
src = audio.waveform
|
|
21
|
+
src = src[0] if src.ndim == 2 && src.shape[0] == 1
|
|
22
|
+
chunks = []
|
|
23
|
+
@vad.segments_from_samples(@params, src).each do |segment|
|
|
24
|
+
segment => {start_time:, end_time:}
|
|
25
|
+
st = start_time * SAMPLE_RATE / 1000
|
|
26
|
+
en = end_time * SAMPLE_RATE / 1000
|
|
27
|
+
chunks << src[st..en]
|
|
28
|
+
end
|
|
29
|
+
chunks.empty? ? src : Torch.cat(chunks)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/itak/process.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require "itak/processible"
|
|
2
|
+
require "itak/process/normalize_loudness"
|
|
3
|
+
require "itak/process/down_mix"
|
|
4
|
+
require "itak/process/resample"
|
|
5
|
+
require "itak/process/denoise"
|
|
6
|
+
require "itak/process/vad"
|
|
7
|
+
require "itak/process/transcribe"
|
|
8
|
+
require "itak/process/save"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "itak/debuggable"
|
|
2
|
+
|
|
3
|
+
class Itak
|
|
4
|
+
module Processible
|
|
5
|
+
include Debuggable
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def prepended(mod)
|
|
9
|
+
mod.extend ClassMethods
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(audio, *, **, &)
|
|
14
|
+
debug_log_start
|
|
15
|
+
rv = super
|
|
16
|
+
debug_save audio
|
|
17
|
+
rv
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_proc
|
|
21
|
+
->(audio) {run audio}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
def to_proc
|
|
26
|
+
new.to_proc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def [](*, **, &)
|
|
30
|
+
new(*, **, &)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/itak.rb
CHANGED
|
@@ -1,47 +1,45 @@
|
|
|
1
1
|
require "pathname"
|
|
2
2
|
require "torchaudio"
|
|
3
|
+
require "ndav/numo/narray"
|
|
4
|
+
require "ndav/torch/tensor"
|
|
5
|
+
require "awaaz"
|
|
6
|
+
require "itak/awaaz_patch"
|
|
3
7
|
|
|
4
|
-
require "itak/
|
|
5
|
-
require "itak/
|
|
6
|
-
require "itak/transcriber"
|
|
8
|
+
require "itak/audio"
|
|
9
|
+
require "itak/process"
|
|
7
10
|
|
|
8
11
|
class Itak
|
|
12
|
+
include NDAV::Converter
|
|
13
|
+
include Process
|
|
14
|
+
|
|
9
15
|
SAMPLE_RATE = 16_000
|
|
10
16
|
|
|
11
|
-
def
|
|
12
|
-
src = Pathname(src)
|
|
17
|
+
def initialize(src, dest=nil)
|
|
18
|
+
@src = Pathname(src)
|
|
13
19
|
if dest
|
|
14
|
-
dest = Pathname(dest)
|
|
15
|
-
if dest.directory?
|
|
16
|
-
dest = dest
|
|
20
|
+
@dest = Pathname(dest)
|
|
21
|
+
if @dest.directory?
|
|
22
|
+
@dest = @dest/@src.basename.sub_ext(".denoised-vad" + @src.extname)
|
|
17
23
|
end
|
|
18
24
|
else
|
|
19
|
-
dest = src.sub_ext(".denoised-vad" + src.extname)
|
|
25
|
+
@dest = src.sub_ext(".denoised-vad" + src.extname)
|
|
20
26
|
end
|
|
21
|
-
dest.
|
|
22
|
-
|
|
23
|
-
waveform, sample_rate = TorchAudio.load(src.to_path)
|
|
24
|
-
raise "Currently only #{SAMPLE_RATE}Hz audio is supported" unless sample_rate == SAMPLE_RATE
|
|
27
|
+
@transcription_dest = @dest.sub_ext(".txt")
|
|
28
|
+
end
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
def run
|
|
31
|
+
@dest.dirname.mkpath
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
Audio.load(@src, debug_info: {src: @src, dest: @dest})
|
|
34
|
+
.(&NormalizeLoudness)
|
|
35
|
+
.(&DownMix)
|
|
36
|
+
.(&Resample[to: SAMPLE_RATE])
|
|
37
|
+
.(&Denoise)
|
|
38
|
+
.(&VAD)
|
|
39
|
+
.>>(&Save[to: @dest])
|
|
40
|
+
.>>(&Transcribe[dest: @transcription_dest])
|
|
41
|
+
.<<
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
TorchAudio.save(dest.to_path, vadded.unsqueeze(0), SAMPLE_RATE)
|
|
34
|
-
$stderr.puts "Saved to #{dest}"
|
|
35
|
-
}
|
|
36
|
-
transcribing = Thread.new {
|
|
37
|
-
$stderr.puts "Transcribing..."
|
|
38
|
-
transcription = Transcriber.new.run(vadded)
|
|
39
|
-
transcription_path = dest.sub_ext(".txt")
|
|
40
|
-
transcription_path.write transcription
|
|
41
|
-
$stderr.puts "Transcription saved to #{transcription_path}"
|
|
42
|
-
}
|
|
43
|
-
saving.join
|
|
44
|
-
transcribing.join
|
|
45
|
-
dest
|
|
43
|
+
[@dest, @transcription_dest]
|
|
46
44
|
end
|
|
47
45
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: itak
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '
|
|
4
|
+
version: '4'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kitaiti Makoto
|
|
@@ -107,6 +107,48 @@ dependencies:
|
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: ebur128_stream
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: awaaz
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: ndav-numo-narray
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
110
152
|
- !ruby/object:Gem::Dependency
|
|
111
153
|
name: rake
|
|
112
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -222,9 +264,19 @@ files:
|
|
|
222
264
|
- bin/itak
|
|
223
265
|
- itak.gemspec
|
|
224
266
|
- lib/itak.rb
|
|
225
|
-
- lib/itak/
|
|
226
|
-
- lib/itak/
|
|
227
|
-
- lib/itak/
|
|
267
|
+
- lib/itak/audio.rb
|
|
268
|
+
- lib/itak/awaaz_patch.rb
|
|
269
|
+
- lib/itak/callable.rb
|
|
270
|
+
- lib/itak/debuggable.rb
|
|
271
|
+
- lib/itak/process.rb
|
|
272
|
+
- lib/itak/process/denoise.rb
|
|
273
|
+
- lib/itak/process/down_mix.rb
|
|
274
|
+
- lib/itak/process/normalize_loudness.rb
|
|
275
|
+
- lib/itak/process/resample.rb
|
|
276
|
+
- lib/itak/process/save.rb
|
|
277
|
+
- lib/itak/process/transcribe.rb
|
|
278
|
+
- lib/itak/process/vad.rb
|
|
279
|
+
- lib/itak/processible.rb
|
|
228
280
|
- test/fixtures/.gitkeep
|
|
229
281
|
- test/helper.rb
|
|
230
282
|
- test/test_itak.rb
|
|
@@ -246,7 +298,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
246
298
|
- !ruby/object:Gem::Version
|
|
247
299
|
version: '0'
|
|
248
300
|
requirements: []
|
|
249
|
-
rubygems_version: 4.0.
|
|
301
|
+
rubygems_version: 4.0.16
|
|
250
302
|
specification_version: 4
|
|
251
303
|
summary: Audio editing tool for podcasters
|
|
252
304
|
test_files: []
|
data/lib/itak/denoiser.rb
DELETED
data/lib/itak/transcriber.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
require "whisper"
|
|
2
|
-
|
|
3
|
-
class Itak
|
|
4
|
-
class Transcriber
|
|
5
|
-
PARAMS = {
|
|
6
|
-
language: "ja",
|
|
7
|
-
temperature: 1.0
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
def initialize(model: "large-v3-turbo-q8_0")
|
|
11
|
-
Whisper.log_set proc {}, nil
|
|
12
|
-
@whisper = Whisper::Context.new(model)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def run(src, params: {})
|
|
16
|
-
params = Whisper::Params.new(**PARAMS.merge(params))
|
|
17
|
-
src = src[0] if src.ndim == 2 && src.shape[0] == 1
|
|
18
|
-
@whisper
|
|
19
|
-
.full(params, src.to_ndav)
|
|
20
|
-
.each_segment
|
|
21
|
-
.collect {|segment|
|
|
22
|
-
"[%<start_time>s --> %<end_time>s]%<text>s" % {
|
|
23
|
-
start_time: format_time(segment.start_time),
|
|
24
|
-
end_time: format_time(segment.end_time),
|
|
25
|
-
text: segment.text
|
|
26
|
-
}
|
|
27
|
-
}.join("\n\n")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def format_time(time_ms)
|
|
33
|
-
sec, decimal_part = time_ms.divmod(1000)
|
|
34
|
-
min, sec = sec.divmod(60)
|
|
35
|
-
hour, min = min.divmod(60)
|
|
36
|
-
"%02d:%02d:%02d" % [hour, min, sec]
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/itak/vad.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
require "whisper"
|
|
2
|
-
require "ndav/torch/tensor"
|
|
3
|
-
|
|
4
|
-
class Itak
|
|
5
|
-
class VAD
|
|
6
|
-
PARAMS = Whisper::VAD::Params.new(
|
|
7
|
-
threshold: 0.7,
|
|
8
|
-
min_silence_duration_ms: 1500
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
def initialize(model: "silero-v6.2.0")
|
|
12
|
-
Whisper.log_set proc {}, nil
|
|
13
|
-
@vad = Whisper::VAD::Context.new(model)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def run(src, params: PARAMS)
|
|
17
|
-
src = src[0] if src.ndim == 2 && src.shape[0] == 1
|
|
18
|
-
chunks = []
|
|
19
|
-
@vad.segments_from_samples(params, src.to_ndav).each do |segment|
|
|
20
|
-
segment => {start_time:, end_time:}
|
|
21
|
-
st = start_time * SAMPLE_RATE / 1000
|
|
22
|
-
en = end_time * SAMPLE_RATE / 1000
|
|
23
|
-
chunks << src[st..en]
|
|
24
|
-
end
|
|
25
|
-
Torch.cat(chunks)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|