itak 2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05d3ad0b0b7a48d1053aa421e23a4d720f8069e3b767d52ffe305eb52266ea4c
4
- data.tar.gz: 1ca65afc391b6d3b5b80c79478e8317ec87198785a3376e34feafe9b20aa86b7
3
+ metadata.gz: 3f5c18bc3e593097a8a4efac8f78432ccf2db6b7771ba740dae975f12b0f12f3
4
+ data.tar.gz: b8e1a1c9e26a8427116515243703e889171b6a5f49d4c42285a89b19b63749e9
5
5
  SHA512:
6
- metadata.gz: 682271f0f3c32b88b489f3465f40df61bfc20588f611fedd41cdea75b2935ed14259166993f8039d7833ffc8abd7e22af3cf0c41d91bad33f15068e22c9143a3
7
- data.tar.gz: 07e5d6f02e6127ebc74835561ee44c9c0aaa57094ef6fd110f980b08838b698f244cabe64d798b8acd9abf698db2296a6e6d54fded5a19eeb887759a30e10d6e
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
+ [![Gem Version](https://badge.fury.io/rb/itak.svg)](https://badge.fury.io/rb/itak)
7
+
6
8
  It does:
7
9
 
10
+ * normalize loudness,
8
11
  * remove noise, and
9
- * remove non-active period
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/output.wav
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
- Audio file must be mono WAV with 16kHz sampling rate and 16-bit per sample.
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
- output = Itak.new.run(src, options[:output])
11
+ outputs = Itak.new(src, options[:output]).run
10
12
  $stderr.puts "Output written to"
11
- $stderr.puts output
13
+ outputs.each do |output|
14
+ $stderr.puts "* #{output}"
15
+ end
12
16
  rescue => err
13
- abort err.message
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 = "2"
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"
@@ -15,12 +15,12 @@ Gem::Specification.new do |s|
15
15
  s.add_runtime_dependency "whispercpp", ">= 1.3.6"
16
16
  s.add_runtime_dependency "torchaudio", ">= 0.5.0"
17
17
  s.add_runtime_dependency "torchcodec"
18
- s.add_runtime_dependency "red-arrow"
19
- s.add_runtime_dependency "numo-narray-alt"
20
- s.add_runtime_dependency "red-arrow-numo-narray"
21
18
  s.add_runtime_dependency "torch-rb"
22
19
  s.add_runtime_dependency "optparse-pathname"
23
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"
24
24
 
25
25
  s.add_development_dependency "rake"
26
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,13 @@
1
+ class Itak
2
+ module Process
3
+ class DownMix
4
+ prepend Processible
5
+
6
+ def run(audio)
7
+ return if audio.waveform.length == 1
8
+
9
+ audio.waveform = audio.waveform.mean(0)
10
+ end
11
+ end
12
+ end
13
+ 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,15 @@
1
+ class Itak
2
+ module Process
3
+ class Save
4
+ prepend Processible
5
+
6
+ def initialize(to:)
7
+ @to = to
8
+ end
9
+
10
+ def run(audio)
11
+ audio.save @to
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -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,35 +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/denoiser"
5
- require "itak/vad"
8
+ require "itak/audio"
9
+ require "itak/process"
6
10
 
7
11
  class Itak
12
+ include NDAV::Converter
13
+ include Process
14
+
8
15
  SAMPLE_RATE = 16_000
9
16
 
10
- def run(src, dest=nil)
11
- src = Pathname(src)
17
+ def initialize(src, dest=nil)
18
+ @src = Pathname(src)
12
19
  if dest
13
- dest = Pathname(dest)
14
- if dest.directory?
15
- dest = dest/src.basename.sub_ext(".denoised-vad" + src.extname)
20
+ @dest = Pathname(dest)
21
+ if @dest.directory?
22
+ @dest = @dest/@src.basename.sub_ext(".denoised-vad" + @src.extname)
16
23
  end
17
24
  else
18
- dest = src.sub_ext(".denoised-vad" + src.extname)
25
+ @dest = src.sub_ext(".denoised-vad" + src.extname)
19
26
  end
20
- dest.dirname.mkpath
21
-
22
- waveform, sample_rate = TorchAudio.load(src.to_path)
23
- raise "Currently only #{SAMPLE_RATE}Hz audio is supported" unless sample_rate == SAMPLE_RATE
27
+ @transcription_dest = @dest.sub_ext(".txt")
28
+ end
24
29
 
25
- $stderr.puts "Denoising..."
26
- denoised = Denoiser.new.denoise(waveform)
30
+ def run
31
+ @dest.dirname.mkpath
27
32
 
28
- $stderr.puts "VAD..."
29
- vadded = VAD.new.run(denoised)
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
+ .<<
30
42
 
31
- TorchAudio.save(dest.to_path, vadded.unsqueeze(0), SAMPLE_RATE)
32
- $stderr.puts "Saved to #{dest}"
33
- dest
43
+ [@dest, @transcription_dest]
34
44
  end
35
45
  end
data/test/test_itak.rb CHANGED
@@ -9,6 +9,7 @@ class TestItak < TestBase
9
9
  output = Itak.new.run(@src)
10
10
  assert_equal Pathname("test/fixtures/mix.denoised-vad.wav").expand_path, output
11
11
  assert_path_exist output
12
+ assert_path_exist output.sub_ext(".txt")
12
13
 
13
14
  src_reader = WaveFile::Reader.new(@src)
14
15
  src_reader.close
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: '2'
4
+ version: '4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kitaiti Makoto
@@ -66,7 +66,7 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: red-arrow
69
+ name: torch-rb
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
@@ -80,7 +80,7 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: numo-narray-alt
83
+ name: optparse-pathname
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
@@ -94,7 +94,7 @@ dependencies:
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
96
  - !ruby/object:Gem::Dependency
97
- name: red-arrow-numo-narray
97
+ name: ndav-torch-tensor
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
@@ -108,7 +108,7 @@ dependencies:
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
110
  - !ruby/object:Gem::Dependency
111
- name: torch-rb
111
+ name: ebur128_stream
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - ">="
@@ -122,7 +122,7 @@ dependencies:
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  - !ruby/object:Gem::Dependency
125
- name: optparse-pathname
125
+ name: awaaz
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - ">="
@@ -136,7 +136,7 @@ dependencies:
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  - !ruby/object:Gem::Dependency
139
- name: ndav-torch-tensor
139
+ name: ndav-numo-narray
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - ">="
@@ -264,8 +264,19 @@ files:
264
264
  - bin/itak
265
265
  - itak.gemspec
266
266
  - lib/itak.rb
267
- - lib/itak/denoiser.rb
268
- - lib/itak/vad.rb
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
269
280
  - test/fixtures/.gitkeep
270
281
  - test/helper.rb
271
282
  - test/test_itak.rb
@@ -287,7 +298,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
298
  - !ruby/object:Gem::Version
288
299
  version: '0'
289
300
  requirements: []
290
- rubygems_version: 4.0.6
301
+ rubygems_version: 4.0.16
291
302
  specification_version: 4
292
303
  summary: Audio editing tool for podcasters
293
304
  test_files: []
data/lib/itak/denoiser.rb DELETED
@@ -1,13 +0,0 @@
1
- require "gtcrn"
2
-
3
- class Itak
4
- class Denoiser
5
- def initialize
6
- @gtcrn = GTCRN.new
7
- end
8
-
9
- def denoise(src)
10
- @gtcrn.enhance_speech_waveform(src)
11
- end
12
- end
13
- end
data/lib/itak/vad.rb DELETED
@@ -1,27 +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
- @vad = Whisper::VAD::Context.new(model)
13
- end
14
-
15
- def run(src, params: PARAMS)
16
- src = src[0] if src.ndim == 2 && src.shape[0] == 1
17
- chunks = []
18
- @vad.segments_from_samples(params, src.to_ndav).each do |segment|
19
- segment => {start_time:, end_time:}
20
- st = start_time * SAMPLE_RATE / 1000
21
- en = end_time * SAMPLE_RATE / 1000
22
- chunks << src[st..en]
23
- end
24
- Torch.cat(chunks)
25
- end
26
- end
27
- end