Correct Audio Duration Discrepancies with Multi-Tool Validation and Transcoding

In this guide, you’ll learn how to check the audio duration of a file using three different tools: ffprobe, SoX, and MediaInfo. This guide was created in response to customer feedback about transcription results showing incorrect audio durations. The issue was traced to audio files with corrupted metadata or problematic headers, leading to inaccurate duration data. If these tools report differing durations for the same file, transcription inconsistencies can arise. We will programmatically detect any duration mismatches and transcode the file to resolve them, typically resulting in a more accurate transcription.

Quickstart

1import assemblyai as aai
2import subprocess
3
4aai.settings.api_key = "YOUR_API_KEY"
5transcriber = aai.Transcriber()
6
7def get_duration_ffprobe(file_path):
8 command = [
9 'ffprobe', '-v', 'error', '-show_entries',
10 'format=duration', '-of',
11 'default=noprint_wrappers=1:nokey=1', file_path
12 ]
13 try:
14 duration = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
15 return float(duration.stdout.strip())
16 except ValueError:
17 print("Error: Unable to parse duration from ffprobe output.")
18 return None
19
20def get_duration_sox(file_path):
21 command = ['soxi', '-D', file_path]
22 try:
23 duration = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
24 return float(duration.stdout.strip())
25 except ValueError:
26 print("Error: Unable to parse duration from SoX output.")
27 return None
28
29def get_duration_mediainfo(file_path):
30 command = ['mediainfo', '--Output=General;%Duration%', file_path]
31 try:
32 duration_ms = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
33 duration_str = duration_ms.stdout.strip()
34 # Check if the output is empty or not a valid number
35 if duration_str:
36 return float(duration_str) / 1000
37 else:
38 print("Error: MediaInfo returned empty or invalid duration")
39 return None
40 except ValueError:
41 print("Error: Unable to parse duration from MediaInfo output.")
42 return None
43
44def check_audio_durations(file_path):
45 #Check if audio durations differ among the three tools.
46 ffprobe_duration = get_duration_ffprobe(file_path)
47 sox_duration = get_duration_sox(file_path)
48 mediainfo_duration = get_duration_mediainfo(file_path)
49
50 # Print all retrieved durations
51 print(f"ffprobe duration: {ffprobe_duration:.6f} seconds" if ffprobe_duration is not None else "ffprobe duration: Error retrieving duration")
52 print(f"SoX duration: {sox_duration:.6f} seconds" if sox_duration is not None else "SoX duration: Error retrieving duration")
53 print(f"MediaInfo duration: {mediainfo_duration:.6f} seconds" if mediainfo_duration is not None else "MediaInfo duration: Error retrieving duration")
54
55 # Return durations for further checks
56 return (ffprobe_duration, sox_duration, mediainfo_duration)
57
58def transcribe(file):
59 print("Executing transcription as audio durations are consistent.")
60 transcript = transcriber.transcribe(file)
61 print(transcript.text)
62
63def transcode(input_file, output_file):
64 #Transcode audio file to a 16kHz WAV file.
65 print(f"Transcoding file {input_file} to {output_file}...")
66 command = [
67 'ffmpeg', '-i', input_file, '-ar', '16000', '-ac', '1', output_file
68 ]
69 try:
70 subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
71 if os.path.exists(output_file):
72 print(f"Transcoding complete. Output file: {output_file}")
73 else:
74 print("Error: Transcoding failed.")
75 except subprocess.CalledProcessError as e:
76 print("Warnings from ffmpeg")
77 """Print errors or warnings from ffmpeg"""
78 #print(e.stderr.decode())
79
80def durations_are_consistent(durations, tolerance=0.01):
81 #Check if durations are consistent within a given tolerance of 0.01 seconds.
82 if None in durations:
83 return False
84 min_duration = min(durations)
85 max_duration = max(durations)
86 return (max_duration - min_duration) <= tolerance
87
88def main(file_path):
89 durations = check_audio_durations(file_path)
90
91 if durations:
92 if None in durations:
93 print("Error: One or more duration values could not be retrieved.")
94 transcoded_file = file_path.rsplit('.', 1)[0] + '_transcoded.wav'
95 transcode(file_path, transcoded_file)
96 new_durations = check_audio_durations(transcoded_file)
97 if new_durations and durations_are_consistent(new_durations):
98 transcribe(transcoded_file)
99 else:
100 print("Warning: The audio durations still differ or an error occurred with the transcoded file.")
101 elif not durations_are_consistent(durations):
102 print("Warning: The audio durations differ between tools.")
103 transcoded_file = file_path.rsplit('.', 1)[0] + '_transcoded.wav'
104 transcode(file_path, transcoded_file)
105 new_durations = check_audio_durations(transcoded_file)
106 if new_durations and durations_are_consistent(new_durations):
107 transcribe(transcoded_file)
108 else:
109 print("Warning: The audio durations still differ or an error occurred with the transcoded file.")
110 else:
111 print("The audio durations are consistent.")
112 transcribe(file_path)
113
114audio_file="./audio.mp4"
115
116if __name__ == "__main__":
117 file_path = f"{audio_file}"
118 main(file_path)

Get Started

Before we begin, make sure you have an AssemblyAI account and an API key. You can sign up for an AssemblyAI account and get your API key from your dashboard.

Step-by-Step Instructions

Install the SDK:

$pip install assemblyai

Import the assemblyai package along with subprocess, set your AssemblyAI API key, and initiate the transcriber.

1import assemblyai as aai
2import subprocess
3
4aai.settings.api_key = "YOUR_API_KEY"
5transcriber = aai.Transcriber()

For this cookbook you will need ffmpeg, sox, and MediaInfo. We will use these tools to pull the duration from the audio. Matching audio duration is crucial because discrepancies may indicate issues with the audio file’s metadata or headers. Such inconsistencies can lead to inaccurate transcription results, playback issues, or unexpected behaviour in media applications. By verifying that the duration is consistent across all three tools, we can detect potential problems early and correct any corrupted metadata or faulty headers before processing the audio further.

First, we will get the audio duration using ffprobe.

1def get_duration_ffprobe(file_path):
2 command = [
3 'ffprobe', '-v', 'error', '-show_entries',
4 'format=duration', '-of',
5 'default=noprint_wrappers=1:nokey=1', file_path
6 ]
7 try:
8 duration = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
9 return float(duration.stdout.strip())
10 except ValueError:
11 print("Error: Unable to parse duration from ffprobe output.")
12 return None

Next, we will get the audio duration for the same file using sox.

1def get_duration_sox(file_path):
2 command = ['soxi', '-D', file_path]
3 try:
4 duration = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
5 return float(duration.stdout.strip())
6 except ValueError:
7 print("Error: Unable to parse duration from SoX output.")
8 return None

Finally, we will get the audio duration for the same file using MediaInfo.

1def get_duration_mediainfo(file_path):
2 command = ['mediainfo', '--Output=General;%Duration%', file_path]
3 try:
4 duration_ms = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
5 duration_str = duration_ms.stdout.strip()
6 # Check if the output is empty or not a valid number
7 if duration_str:
8 return float(duration_str) / 1000
9 else:
10 print("Error: MediaInfo returned empty or invalid duration")
11 return None
12 except ValueError:
13 print("Error: Unable to parse duration from MediaInfo output.")
14 return None

The following function will return the durations from the three tools and convert them to the same format.

1def check_audio_durations(file_path):
2 #Check if audio durations differ among the three tools.
3 ffprobe_duration = get_duration_ffprobe(file_path)
4 sox_duration = get_duration_sox(file_path)
5 mediainfo_duration = get_duration_mediainfo(file_path)
6
7 # Print all retrieved durations
8 print(f"ffprobe duration: {ffprobe_duration:.6f} seconds" if ffprobe_duration is not None else "ffprobe duration: Error retrieving duration")
9 print(f"SoX duration: {sox_duration:.6f} seconds" if sox_duration is not None else "SoX duration: Error retrieving duration")
10 print(f"MediaInfo duration: {mediainfo_duration:.6f} seconds" if mediainfo_duration is not None else "MediaInfo duration: Error retrieving duration")
11
12 # Return durations for further checks
13 return (ffprobe_duration, sox_duration, mediainfo_duration)

Define the transcribe function. This will run only when the duration is consistent among the three tools.

1def transcribe(file):
2 print("Executing transcription as audio durations are consistent.")
3 transcript = transcriber.transcribe(file)
4 print(transcript.text)

Define the transcode function. We will run this if one or more durations differ. The output file will be a 16kHz WAV file as that is the format AssemblyAI models are trained on. When running the ffmpeg command, the transcode may fail or return warnings if there are issues with the input file’s format, corrupted metadata, or unsupported codecs. These warnings tend to be verbose but you can print them for troubleshooting.

1def transcode(input_file, output_file):
2 #Transcode audio file to a 16kHz WAV file.
3 print(f"Transcoding file {input_file} to {output_file}...")
4 command = [
5 'ffmpeg', '-i', input_file, '-ar', '16000', '-ac', '1', output_file
6 ]
7 try:
8 subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
9 if os.path.exists(output_file):
10 print(f"Transcoding complete. Output file: {output_file}")
11 else:
12 print("Error: Transcoding failed.")
13 except subprocess.CalledProcessError as e:
14 print("Warnings from ffmpeg")
15 """Print errors or warnings from ffmpeg"""
16 #print(e.stderr.decode())

Define a function that will check if the durations are consistent. There may be small differences so it’s best to allow a small tolerance. In this example the tolerance value will be 0.01 seconds.

1def durations_are_consistent(durations, tolerance=0.01):
2 #Check if durations are consistent within a given tolerance of 0.01 seconds.
3 if None in durations:
4 return False
5 min_duration = min(durations)
6 max_duration = max(durations)
7 return (max_duration - min_duration) <= tolerance

Finally, here is the order of operations for this program. This program will first check the duration of an audio file across different tools to ensure consistency. If any tool fails to retrieve a duration or if the durations differ, it transcodes the audio to a new 16kHz WAV file and checks the duration of the WAV file. If the durations are consistent in the transcoded file, the program proceeds to transcribe it. If inconsistencies remain after transcoding, it logs a warning to highlight the issue and will not transcribe the file.

1def main(file_path):
2 durations = check_audio_durations(file_path)
3
4 if durations:
5 if None in durations:
6 print("Error: One or more duration values could not be retrieved.")
7 transcoded_file = file_path.rsplit('.', 1)[0] + '_transcoded.wav'
8 transcode(file_path, transcoded_file)
9 new_durations = check_audio_durations(transcoded_file)
10 if new_durations and durations_are_consistent(new_durations):
11 transcribe(transcoded_file)
12 else:
13 print("Warning: The audio durations still differ or an error occurred with the transcoded file.")
14 elif not durations_are_consistent(durations):
15 print("Warning: The audio durations differ between tools.")
16 transcoded_file = file_path.rsplit('.', 1)[0] + '_transcoded.wav'
17 transcode(file_path, transcoded_file)
18 new_durations = check_audio_durations(transcoded_file)
19 if new_durations and durations_are_consistent(new_durations):
20 transcribe(transcoded_file)
21 else:
22 print("Warning: The audio durations still differ or an error occurred with the transcoded file.")
23 else:
24 print("The audio durations are consistent.")
25 transcribe(file_path)
26
27audio_file="./audio/8950.mp4"
28
29if __name__ == "__main__":
30 file_path = f"{audio_file}"
31 main(file_path)

If you continue to experience unexpected behaviour with your file, please contact our support team at support@assemblyai.com for assistance in diagnosing the issue.