<commit_before>module BawAudioTools
  class AudioFfmpeg

    WARN_ESTIMATE_DURATION = 'Estimating duration from bitrate, this may be inaccurate'

    # @param [string] ffmpeg_executable
    # @param [string] ffprobe_executable
    # @param [string] temp_dir
    def initialize(ffmpeg_executable, ffprobe_executable, temp_dir)
      @ffmpeg_executable = ffmpeg_executable
      @ffprobe_executable = ffprobe_executable
      @temp_dir = temp_dir
    end

    public

    def info_command(source)
      "#{@ffprobe_executable} -sexagesimal -print_format default -show_error -show_streams -show_format \"#{source}\""
    end

    def modify_command(source, source_info, target, start_offset = nil, end_offset = nil, channel = nil, sample_rate = nil)
      fail Exceptions::FileNotFoundError, "Source does not exist: #{source}" unless File.exists? source
      fail Exceptions::FileAlreadyExistsError, "Target exists: #{target}" if File.exists? target
      fail ArgumentError, "Source and Target are the same file: #{target}" unless source != target

      cmd_offsets = arg_offsets(start_offset, end_offset)
      cmd_sample_rate = arg_sample_rate(sample_rate)

      if sample_rate.blank? && source_info.include?(:sample_rate)
        cmd_sample_rate = arg_sample_rate(source_info[:sample_rate])
      else
        cmd_sample_rate = arg_sample_rate(sample_rate)
      end

      cmd_channel = arg_channel(channel)
      codec_info = codec_calc(target)

      audio_cmd = "#{@ffmpeg_executable} -i \"#{source}\" #{cmd_offsets} #{cmd_sample_rate} #{cmd_channel} #{codec_info.codec} \"#{codec_info.target}\""
      cmd = ''

      if codec_info[:target] == codec_info[:old_target]
        cmd = audio_cmd
      else
        partial_cmd = "\"#{codec_info[:target]}\" \"#{codec_info[:old_target]}\""
        separator_move = OS.windows? ? '&& move' : '; mv'
        cmd = "#{audio_cmd} #{separator_move} #{partial_cmd}"
      end

      cmd
    end

    def check_for_errors(stdout, stderr)

      unless stderr.blank?

        ffmpeg_warning_tag = '\[[^ ]+ @ [^ ]+\] '

        mod_stderr = stderr
        if stderr.include?(WARN_ESTIMATE_DURATION)
          mod_stderr = mod_stderr.gsub(/#{ffmpeg_warning_tag}#{WARN_ESTIMATE_DURATION}/, '')
        end
        if !mod_stderr.blank? && mod_stderr.match(/#{ffmpeg_warning_tag}/)
          fail Exceptions::FileCorruptError, "Ffmpeg output contained warning.\n\t Standard output: #{stdout}\n\t Standard Error: #{mod_stderr}"
        end
      end
    end

    # returns the duration in seconds (and fractions if present)
    def parse_duration(duration_string)
      duration_match = /(?<hour>\d+):(?<minute>\d+):(?<second>[\d+\.]+)/i.match(duration_string)
      duration = 0
      if !duration_match.nil? && duration_match.size == 4
        duration = (duration_match[:hour].to_f * 60 * 60) + (duration_match[:minute].to_f * 60) + duration_match[:second].to_f
      end
      duration
    end

    def parse_ffprobe_output(source, raw)
      # ffprobe std err contains info (separate on first equals(=))
      result = {}
      ffprobe_current_block_name = ''
      raw.strip.split(/\r?\n|\r/).each do |line|
        line.strip!
        if line[0] == '['
          # this chomp reverse stuff is due to the lack of a proper 'trim'
          ffprobe_current_block_name = line.chomp(']').reverse.chomp('[').reverse
        else
          current_key = line[0, line.index('=')].strip
          current_value = line[line.index('=')+1, line.length].strip
          result[ffprobe_current_block_name + ' ' + current_key] = current_value
        end
      end

      fail Exceptions::AudioFileNotFoundError, "Could not locate #{source}" unless File.exists?(source)
      fail Exceptions::NotAnAudioFileError, "Not an audio file #{source} (#{result['STREAM codec_type']} is not 'audio'): #{result.to_json}" if result['STREAM codec_type'] != 'audio'

      result
    end

    def arg_channel(channel)
      cmd_arg = ''
      unless channel.blank?
        channel_number = channel.to_i
        if channel_number < 1
          # mix down to mono
          cmd_arg = ' -ac 1 '
        else
          # select the channel (0 index based)
          cmd_arg = " -map_channel 0.0.#{channel_number - 1} "
        end
      end
      cmd_arg
    end

    def arg_sample_rate(sample_rate)
      cmd_arg = ''
      unless sample_rate.blank?<commit_msg>Add more information to errors #1<commit_after>module BawAudioTools
  class AudioFfmpeg

    WARN_ESTIMATE_DURATION = 'Estimating duration from bitrate, this may be inaccurate'

    # @param [string] ffmpeg_executable
    # @param [string] ffprobe_executable
    # @param [string] temp_dir
    def initialize(ffmpeg_executable, ffprobe_executable, temp_dir)
      @ffmpeg_executable = ffmpeg_executable
      @ffprobe_executable = ffprobe_executable
      @temp_dir = temp_dir
    end

    public

    def info_command(source)
      "#{@ffprobe_executable} -sexagesimal -print_format default -show_error -show_streams -show_format \"#{source}\""
    end

    def modify_command(source, source_info, target, start_offset = nil, end_offset = nil, channel = nil, sample_rate = nil)
      fail Exceptions::FileNotFoundError, "Source does not exist: #{source}" unless File.exists? source
      fail Exceptions::FileAlreadyExistsError, "Target exists: #{target}" if File.exists? target
      fail ArgumentError, "Source and Target are the same file: #{target}" unless source != target

      cmd_offsets = arg_offsets(start_offset, end_offset)
      cmd_sample_rate = arg_sample_rate(sample_rate)

      if sample_rate.blank? && source_info.include?(:sample_rate)
        cmd_sample_rate = arg_sample_rate(source_info[:sample_rate])
      else
        cmd_sample_rate = arg_sample_rate(sample_rate)
      end

      cmd_channel = arg_channel(channel)
      codec_info = codec_calc(target)

      audio_cmd = "#{@ffmpeg_executable} -i \"#{source}\" #{cmd_offsets} #{cmd_sample_rate} #{cmd_channel} #{codec_info.codec} \"#{codec_info.target}\""
      cmd = ''

      if codec_info[:target] == codec_info[:old_target]
        cmd = audio_cmd
      else
        partial_cmd = "\"#{codec_info[:target]}\" \"#{codec_info[:old_target]}\""
        separator_move = OS.windows? ? '&& move' : '; mv'
        cmd = "#{audio_cmd} #{separator_move} #{partial_cmd}"
      end

      cmd
    end

    def check_for_errors(stdout, stderr)

      unless stderr.blank?

        ffmpeg_warning_tag = '\[[^ ]+ @ [^ ]+\] '

        mod_stderr = stderr
        if stderr.include?(WARN_ESTIMATE_DURATION)
          mod_stderr = mod_stderr.gsub(/#{ffmpeg_warning_tag}#{WARN_ESTIMATE_DURATION}/, '')
        end
        if !mod_stderr.blank? && mod_stderr.match(/#{ffmpeg_warning_tag}/)
          fail Exceptions::FileCorruptError, "Ffmpeg output contained warning.\n\t Standard output: #{stdout}\n\t Standard Error: #{mod_stderr}"
        end
      end
    end

    # returns the duration in seconds (and fractions if present)
    def parse_duration(duration_string)
      duration_match = /(?<hour>\d+):(?<minute>\d+):(?<second>[\d+\.]+)/i.match(duration_string)
      duration = 0
      if !duration_match.nil? && duration_match.size == 4
        duration = (duration_match[:hour].to_f * 60 * 60) + (duration_match[:minute].to_f * 60) + duration_match[:second].to_f
      end
      duration
    end

    def parse_ffprobe_output(source, raw)
      # ffprobe std err contains info (separate on first equals(=))
      result = {}
      ffprobe_current_block_name = ''
      raw.strip.split(/\r?\n|\r/).each do |line|
        line.strip!
        if line[0] == '['
          # this chomp reverse stuff is due to the lack of a proper 'trim'
          ffprobe_current_block_name = line.chomp(']').reverse.chomp('[').reverse
        else
          current_key = line[0, line.index('=')].strip
          current_value = line[line.index('=')+1, line.length].strip
          result[ffprobe_current_block_name + ' ' + current_key] = current_value
        end
      end

      unless File.exists?(source)
        fail Exceptions::AudioFileNotFoundError, "Could not locate #{source}\n\tStandard Output: #{raw}"
      end

      if result['STREAM codec_type'] != 'audio'
        msg = "Not an audio file #{source} ('#{result['STREAM codec_type']}' is not 'audio'): #{result.to_json}\n\tStandard Output: #{raw}"
        fail Exceptions::NotAnAudioFileError, msg
      end

      result
    end

    def arg_channel(channel)
      cmd_arg = ''
      unless channel.blank?
        channel_number = channel.to_i
        if channel_number < 1
          # mix down to mono
          cmd_arg = ' -ac 1 '
        else
          # select the channel (0 index based)
          cmd_arg = " -map_channel 0.0.#{channel_number - 1} "
        end
      end
      cmd_arg
    end

    def arg_sample_rate(sample_rate)
      cmd_arg = ''
      unless sample_rate.blank?
{'fraction_non_alphanumeric': 0.12014849814377321, 'fraction_numerical': 0.0039374507818652265, 'mean_word_length': 2.933628318584071, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2630057', 'n_tokens_mistral': 2873, 'n_tokens_neox': 2776, 'n_words': 626}