Zum Hauptinhalt springen

Sunday-Projects – Transkription von Videodateien

· 7 Minuten Lesezeit

'Sunday Projects' sind die kleinen, technischen Projekte und Spielereien mit denen ich in unregelmäßigen Abständen meine Zeit verbringe.

Als Reaktion auf Lucas Erstlingswerk für die „Sunday Projects“ habe ich sein Projekt direkt als Lösungsansatz wiederverwendet, um Folgendes, welches mich immer wieder auf der Arbeit wurmt, zu erledigen: das Verarbeiten von großen Informationsmengen und das Organisieren von Wissen.

Das Problem

Ich bin ein Datenmessie und wende deshalb einen beachtlichen Anteil meiner Zeit dafür auf, die entsprechenden Informationen für meine Arbeit zu suchen, zu organisieren und zu verteilen. Und dementsprechend viel lese ich. Es macht so einen beachtlichen Anteil aus, dass ich mich beim Schreiben dieser paar Zeilen gefragt habe, weshalb ich keine Karriere als Bibliothekar angestrebt habe. Das geht so weit, dass ich letztens von einem ehemaligen Kollegen angeschrieben wurde, um seinen Kollegen bei Wien Energie für ein Projekt einen Crashkurs zu einem spezifischen Thema zu (inkl. Folder mit Quellen). Und ja, ich war überrascht, dass ein Energieversorger keine interne Expertise zum Thema Energiespeicher hatte. Aber zurück zur Problemstellung: Auf meinem Arbeitsnotebook horte ich 65 GB an Publikationen, Büchern, Videos, E-Mails und Aufzeichnungen. Insgesamt handelt es sich um 28.585 Dateien, organisiert in 2834 Ordnern (es ist etwas ungenau, weil 183 Ordner durch Zotero angelegt wurden, um die dort gespeicherten Publikationen zu verwalten). Während nun die meisten PDFs eigentlich ganz einfach zu durchsuchen sind, wird es aber mit den ca. 660 Videodateien problematisch, weil keine einzige davon Untertitel hat.

Die Lösung?

Dank Lucas Post konnte ich mich recht großzügig beim Code bedienen und ihn entsprechend für meine Zwecke wiederverwenden (den Code habe ich einfach mittels ChatGPT kommentieren lassen). Im Endeffekt gehe ich durch meinen Literaturordner, suche nach Videodateien und schicke diese durch Whisper. Aus irgendeinem Grund war es anfangs nicht möglich, direkt die Videodateien zu verwenden, weshalb ich erstmal MP3s extrahiert habe, um anschließend alles durch Whisper zu schicken. Danach funktionierte es wieder direkt per Video, also vermute ich, dass da eine Datei das Problem verursacht hat. Aus Faulheit habe ich aber auch keine Fallback-Lösungen eingebaut (z. B. zuerst Video, dann Audio), da ich nicht beabsichtige, das Skript jeden Tag unbeaufsichtigt ausführen zu lassen (oder vielleicht doch irgendwann?).

import os
import subprocess
import glob
import torch
import whisper
from tqdm import tqdm

folder_path = 'WHATEVER'
# Load the Model
model = whisper.load_model("base")

def extract_audio(video_path, audio_path):
# Use ffmpeg to extract the audio from the video
subprocess.run(['ffmpeg', '-y', '-loglevel', 'panic', '-i', video_path, '-f', 'mp3', '-ab', '192000', '-vn', audio_path])

def transcribe_videos(folder_path):
# Initialize the progress bar
files_list = glob.glob(os.path.join(folder_path, '**/*.mp4'), recursive=True) + \
glob.glob(os.path.join(folder_path, '**/*.avi'), recursive=True) + \
glob.glob(os.path.join(folder_path, '**/*.mkv'), recursive=True)
total_files = len(files_list)
progress_bar = tqdm(files_list, total=total_files, desc='Processing')

# Iterate through all files in the folder (including subfolders)
for video_path in progress_bar:
audio_path = os.path.splitext(video_path)[0] + '.mp3'
transcript_path = os.path.splitext(video_path)[0] + '.txt'

# Check if transcript file already exists and has content
if os.path.exists(transcript_path) and os.path.getsize(transcript_path) > 0:
continue

# Extract audio from the video
extract_audio(video_path, audio_path)

# Transcribe the extracted audio using Whisper
transcript = model.transcribe(audio_path, fp16=False)

# Save the transcript to a text file next to the video file
file_name = os.path.splitext(video_path)[0]
with open(transcript_path, 'w', encoding="utf-8") as f:
f.write(transcript['text'])

# Delete the temporary audio file
os.remove(audio_path)

transcribe_videos(folder_path)

Aus Mangel an Rechenressourcen habe ich im Gegensatz zu Luca das Base- anstatt das Large-Modell von OpenAI verwendet. Das sah in den ersten Ergebnissen auch alles ganz okay aus. Hier ein Ausschnitt (insgesamt 118.290 Zeichen):

 So let's look at a typical silicon carbide or any vertical device for that matter. There is a thick layer, a drift layer that holds the high voltage. And if you want to make a device of a specific voltage, then the thickness is inversely proportional to the critical electric field. So if the critical electric field is 10 times higher, then the thickness of your device is 10 times smaller. If you look at the device resistance, the equation right below, it's inversely proportional to the third power now of the critical electric field. So if your critical electric field is 10 times higher, then the resistance of that layer will be 1000 times lower. …

Zusatzaufgabe: Erstmal Platz schaffen

Das funktionierte alles soweit auch echt super, bis ich auf ein kleines Detail gestoßen bin: Ich habe die Daten in OneDrive gespeichert und während also das Skript durchlief und brav die Videodateien beim Zugriff heruntergeladen hat, wurde allmählich mein freier Speicherplatz immer geringer, bis ich bei 0 Byte gelandet bin. Blöd. Aber dank FFmpeg und FFprobe ist das auch gar kein Problem (danke ChatGPT, das meinen Code refactored hat). FFmpeg ist eine Bibliothek zum Konvertieren von Videodateien und erlaubt alles Mögliche. Extrahieren von Audio-Dateien, Konvertieren, Filter usw. FFprobe ist davon Bestandteil und ermöglicht es herauszufinden, welchen Codec die Videodateien verwenden. Was ich also tue, ist somit Folgendes:

  1. Hey, erstell mir mal eine Liste aller Videodateien.
  2. Gehe durch alle Videodateien und prüfe, ob sie h.265 verwenden.
  3. Jetzt wäre es irgendwie Leiwand, wenn du sie mit der CPU* alle umwandelst.
  4. Lösch auch den alten Müll einfach.
  5. Voila, Videos sind nur noch ein Bruchteil so groß (teilweise 1/10).
  6. Leiwand.
  • CPU, da NVenc meiner Erfahrung nach sowohl bei der Videoqualität als auch beim Komprimieren deutlich schlechter ist.
import os
import subprocess
import shutil
import glob
from tqdm import tqdm

# Specify the folder where you want to search for video files
folder_path = 'WHATEVER'

def is_h265_encoded(file_path):
# Execute ffprobe command to gather video stream information
process = subprocess.Popen(["ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=codec_name", "-of", "default=nw=1:nk=1",
file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()

# Check if the output contains "hevc" indicating H.265 encoding
return output.strip().decode("utf-8") == "hevc"

def convert_to_mkv(file_path):
# Replace .mp4 extension with .mkv for the output file
output_path = file_path.replace('.mp4', '.mkv')

# Use ffmpeg to convert the video to MKV format with H.265 encoding
result = subprocess.call(['ffmpeg', '-i', file_path, '-c:v', 'libx265',
'-hide_banner', '-loglevel', 'panic',
'-crf', '28', '-c:a', 'copy', output_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# Return the path of the converted MKV file if conversion was successful, otherwise return None
if result == 0 and os.path.exists(output_path):
return output_path
else:
return None

def search_and_convert_videos(folder_path):
# Use glob to get a list of video files (with .mp4 extension) in the folder and its subfolders
video_files = glob.glob(os.path.join(folder_path, '**/*.mp4'), recursive=True) + \
glob.glob(os.path.join(folder_path, '**/*.avi'), recursive=True)

# Create a progress bar using tqdm
progress_bar = tqdm(video_files, unit='file')

for file_path in progress_bar:
# Check if the video is already encoded in H.265 format
if not is_h265_encoded(file_path):
# Construct the output path for the converted MKV file
output_path = file_path.replace('.mp4', '.mkv')

# Check if the output file already exists
if os.path.exists(output_path):
# Update the progress bar description
progress_bar.set_description(f"Skipped {os.path.basename(file_path)} (already converted)")
else:
# Convert the video to MKV format and get the output path
output_path = convert_to_mkv(file_path)

if output_path is not None:
# Remove the original video file
os.remove(file_path)

# Update the progress bar description
progress_bar.set_description(f"Converted {os.path.basename(file_path)} to {os.path.basename(output_path)}")
else:
progress_bar.set_description(f"Error converting {os.path.basename(file_path)}")
else:
progress_bar.set_description(f"{os.path.basename(file_path)} is already in H.265 format, skipping conversion")

# Call the function to search for video files and convert them to MKV
search_and_convert_videos(folder_path)

Ausblick

Was soll eigentlich der ganze Terz, wenn der Text immer noch nicht richtig leserlich ist? Nun ja, es gibt noch ein anderes Projekt, an dem ich nebenbei etwas arbeite: Mein eigener Chatbot mithilfe von PrivateGPT, um es als Suchmaschine zu verwenden. Das läuft auch soweit, wenn auch noch sehr langsam, und ich bin gerade dabei, es auf eine neuere Version zu aktualisieren. Danach kann ich mir dann Gedanken darüber machen, wie ich es für die Kollegen zugänglich machen kann. ;-)


Ciao Kakao, Adrian