Python實(shí)現(xiàn)音頻去廣告和字幕提取
之前下了一些音頻課,但是存在一些音頻中間插入廣告,更萬(wàn)惡的是,它根本不分是不是整句,只要時(shí)間差不多了就插入。
要去掉廣告我們分為以下步驟依次執(zhí)行:
- 分析規(guī)律(就是前面找規(guī)律)
- 廣告提取
- 識(shí)別廣告
- 重新拼接
對(duì)于字幕提取,之前其實(shí)我們?cè)?AI 相關(guān)的文章中也介紹過(guò)對(duì)應(yīng)模型,直接轉(zhuǎn)換并處理就可以了,后面再介紹。
分析規(guī)律
和寫(xiě)爬蟲(chóng)一樣,第一點(diǎn)就是要找規(guī)律:用一張草稿紙記錄每個(gè)廣告的起始時(shí)間和結(jié)束時(shí)間,再分析它和整段音頻的關(guān)系。
遺憾的是,在插入時(shí)或許是為了避免裁剪,逐秒計(jì)算(也叫做)后,我得出了一個(gè)結(jié)論:它是在固定時(shí)間(end_time - 3min) + random_offset 值,因?yàn)榱?offset 值的介入,整個(gè)就變的玄學(xué)了起來(lái)。
還好很快我又有了一些新的想法:利用一些識(shí)別的手段把廣告詞裁掉就可以了。
還好廣告詞是固定的,而要處理的音頻卻多,這樣計(jì)算下來(lái) ROI 還是劃算的。
廣告提取
這一步是所有步驟里最耗費(fèi)時(shí)間的,對(duì)于整句來(lái)說(shuō),切割分離是一個(gè)高敏感性的操作,稍微多留白幾百毫秒,你聽(tīng)起來(lái)可能就很難受。只有原始數(shù)據(jù)切割的恰到好處,才能達(dá)到完美還原。
因此我們需要更精細(xì)化,精細(xì)到毫秒的裁剪手段。
Windows 下也不知道用啥,搜了下就選了 Audacity:
以毫秒控制選區(qū),然后切割后如果聽(tīng)感是無(wú)縫的,那么就相當(dāng)于抽離了,如果覺(jué)得怪怪的就再調(diào)整毫秒重新裁,如此反復(fù)直到無(wú)縫銜接。
依賴(lài)列表
下文完整的 import
依賴(lài)(因?yàn)閼械迷谖哪┵N完整代碼了):
import glob import os from concurrent.futures import ThreadPoolExecutor, as_completed import numpy as np import librosa import torch import whisper from pydub import AudioSegment import soundfile as sf import torch.nn.functional as F import shutil
識(shí)別廣告
接下來(lái)我們得到了兩個(gè)片段,一個(gè)是完整版的音頻,另一個(gè)是純廣告音頻,將對(duì)應(yīng)波形的相似度進(jìn)行比對(duì),找到相似的段,再進(jìn)行切割。
當(dāng)然,由于整段二三十分鐘,相對(duì)的來(lái)說(shuō)計(jì)算量會(huì)很大,由于我們知道了總是在一個(gè)音頻快結(jié)束了插入廣告,因此可以先裁剪縮小對(duì)比規(guī)模,然后再進(jìn)行比對(duì),減少計(jì)算量。
其中有一些非常抽象的音頻和數(shù)學(xué)知識(shí),只能說(shuō)謝謝 GPT 老師(我也沒(méi)學(xué)會(huì))
# 已知的廣告片段文件 AD_SNIPPET_FILE = "./testcase/test2.wav" # 待處理的音頻文件目錄 audio_dir = "./testcase" TAIL_SECONDS = 300 # 只截取最后5分鐘處理 SIMILARITY_THRESHOLD = 0.8 # 相似度閾值(0~1之間, 需根據(jù)實(shí)際情況調(diào)整) SUCCESS_DIR = "./success" device = torch.device("cuda" if torch.cuda.is_available() else "cpu") def load_audio_segment(file_path, sr=16000, tail_seconds=None): info = sf.info(file_path) total_duration = info.duration if tail_seconds is not None and tail_seconds < total_duration: start_time = total_duration - tail_seconds audio, _ = librosa.load(file_path, sr=sr, mono=True, offset=start_time, duration=tail_seconds) return audio, start_time else: audio, _ = librosa.load(file_path, sr=sr, mono=True) return audio, 0.0 def find_audio_snippet(main_audio_path, snippet_audio, snippet_norm, sr=16000, tail_seconds=300): """ 在 main_audio_path 中尋找 snippet_audio 音頻片段的出現(xiàn)位置。 snippet_audio 為事先加載好的 numpy 數(shù)組,snippet_norm 為 snippet 的二范數(shù),用于相似度計(jì)算。 返回 (ad_start_time, ad_end_time, similarity) 若未找到則返回 (None, None, None) """ main_audio, main_start = load_audio_segment(main_audio_path, sr=sr, tail_seconds=tail_seconds) if len(snippet_audio) > len(main_audio): return None, None, None # 轉(zhuǎn)換到 GPU 張量 main_audio_t = torch.from_numpy(main_audio).float().to(device).unsqueeze(0).unsqueeze(0) # [1,1,M] snippet_audio_t = torch.from_numpy(snippet_audio).float().to(device).unsqueeze(0).unsqueeze(0) # [1,1,S] # 使用 conv1d 來(lái)進(jìn)行類(lèi)似相似度搜索 (無(wú) snippet 翻轉(zhuǎn)) correlation = F.conv1d(main_audio_t, snippet_audio_t) correlation = correlation[0, 0].cpu().numpy() best_index = np.argmax(correlation) best_value = correlation[best_index] # 相似度計(jì)算:歸一化 similarity = best_value / snippet_norm if snippet_norm > 0 else 0 ad_start_time = main_start + best_index / sr ad_end_time = ad_start_time + len(snippet_audio) / s return ad_start_time, ad_end_time, similarity
重新拼接
找到廣告后我們將廣告段落減去,然后再重新拼接生成新的音頻文件即可。
def process_file(filename, snippet_audio, snippet_norm, sr=16000, tail_seconds=300, similarity_threshold=0.8): """ 處理單個(gè)文件,找到廣告并移除。 """ ad_start, ad_end, similarity = find_audio_snippet(filename, snippet_audio, snippet_norm, sr=sr, tail_seconds=tail_seconds) if ad_start is not None and similarity > similarity_threshold: # 去除廣告段落 audio = AudioSegment.from_file(filename) part1 = audio[:ad_start * 1000] part2 = audio[ad_end * 1000:] cleaned = part1 + part2 cleaned_file = f"output/{os.path.basename(filename)}" cleaned.export(cleaned_file, format="mp3") shutil.move(filename, SUCCESS_DIR) return f"{filename} 已移除廣告,生成 {cleaned_file},相似度:{similarity}" else: return f"{filename} 中未高相似度檢測(cè)到廣告或相似度過(guò)低({similarity})" def remove_ads(): sr = 16000 # 預(yù)先加載廣告片段 snippet_audio, _ = librosa.load(AD_SNIPPET_FILE , sr=sr, mono=True) # 計(jì)算snippet的范數(shù),用于相似度歸一化 snippet_norm = np.dot(snippet_audio, snippet_audio) file_list = [os.path.join(audio_dir, f) for f in os.listdir(audio_dir) if f.endswith(".mp3")] # 使用多線(xiàn)程加速處理 # 線(xiàn)程數(shù)可根據(jù)您的機(jī)器資源調(diào)整 max_workers = 20 results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit(process_file, file, snippet_audio, snippet_norm, sr, TAIL_SECONDS, SIMILARITY_THRESHOLD): file for file in file_list } for future in as_completed(futures): file = futures[future] try: res = future.result() print(res) except Exception as e: print(f"{file} 處理時(shí)出錯(cuò): {e}")
字幕提取
下一個(gè)問(wèn)題是,音頻是提取好了,但是音頻的字幕和總結(jié)能力其實(shí)也是一個(gè)亮點(diǎn),這個(gè)也是我們想要有的能力,而好多都是付費(fèi)的,百度網(wǎng)盤(pán)雖然會(huì)員免費(fèi),但是實(shí)際聽(tīng)音頻的過(guò)程中遇到了 Bug,讓我不得不另謀高就。
要使用這個(gè)能力,核心還是使用 whisper這個(gè)模型的能力。
我考慮用 Emby 來(lái)當(dāng)音頻播放器,字幕可以和歌詞字幕一樣,因此就需要生成 lrc 格式的標(biāo)準(zhǔn)文件。
也就是:
- 提取字幕
- 給每段字幕和時(shí)間軸進(jìn)行格式轉(zhuǎn)換,轉(zhuǎn)為 lrc 標(biāo)準(zhǔn)格式
而跑 AI 模型的時(shí)候,務(wù)必保證 GPU 加速(否則你會(huì)卡的痛不欲生)。
模型請(qǐng)根據(jù)自己的內(nèi)存和實(shí)際情況決定,不一定是越大越好的,可以先跑一段音頻試試效果。
如果本地沒(méi)有找到對(duì)應(yīng)的模型,whisper 先嘗試下載,也可以使用本地準(zhǔn)備好的模型。
def format_lrc_timestamp(seconds: float) -> str: """將秒數(shù)轉(zhuǎn)換為 LRC 格式時(shí)間戳 [mm:ss.xx]""" total_seconds = int(seconds) m = total_seconds // 60 s = total_seconds % 60 # 毫秒取兩位小數(shù) ms = (seconds - total_seconds) * 100 return f"[{m:02d}:{s:02d}.{int(ms):02d}]" def trans_files(): # 請(qǐng)將此路徑替換為你的音頻目錄路徑 audio_directory = "./audio_files" # 可根據(jù)需要選擇模型大小,如 "small"、"medium"、"large" trans_text(audio_directory, model_name="medium", language="zh") def transcribe_to_lrc(audio_path: str, lrc_path: str, model, language: str = "zh"): """ 使用已加載的 whisper model 對(duì) audio_path 進(jìn)行轉(zhuǎn)錄, 并將結(jié)果保存為 lrc_path 文件。 """ result = model.transcribe(audio_path, language=language) segments = result.get("segments", []) with open(lrc_path, "w", encoding="utf-8") as f: # 可根據(jù)需要添加標(biāo)簽信息,如標(biāo)題、歌手、專(zhuān)輯 f.write("[ti:未知標(biāo)題]\n") f.write("[ar:未知作者]\n") f.write("[al:未知專(zhuān)輯]\n\n") for seg in segments: start_time = format_lrc_timestamp(seg['start']) text = seg['text'].strip() f.write(f"{start_time}{text}\n") def trans_text(audio_dir: str, model_name: str = "medium", language: str = "zh"): # 嘗試使用 GPU device = "cuda:0" if torch.cuda.is_available() else "cpu" print(f"使用設(shè)備: {device}") # 加載模型到指定設(shè)備 # 模型大小可根據(jù)資源調(diào)整,如:tiny, base, small, medium, large print(f"加載 Whisper 模型 ({model_name}),請(qǐng)稍候...") model = whisper.load_model(model_name, device=device) print("模型加載完成。") # 遍歷指定目錄下所有 mp3 audio_files = glob.glob(os.path.join(audio_dir, "*.mp3")) if not audio_files: print("指定目錄中未找到 MP3 文件。") return for audio_path in audio_files: base_name = os.path.splitext(audio_path)[0] lrc_path = base_name + ".lrc" print(f"處理文件: {audio_path} -> {lrc_path}") transcribe_to_lrc(audio_path, lrc_path, model=model, language=language) print(f"完成: {lrc_path}") print("所有文件處理完成!") def trans_files(): # 請(qǐng)將此路徑替換為你的音頻目錄路徑 audio_directory = "./audio_files" # 可根據(jù)需要選擇模型大小,如 "small"、"medium"、"large" trans_text(audio_directory, model_name="medium", language="zh")
目前我還沒(méi)有做總結(jié)功能(主要是普通播放器也沒(méi)地方顯示總結(jié)),但是有了全量文本,相信對(duì)于各位來(lái)說(shuō)并不是難事。
總結(jié)
本文的所有代碼均由 AI 編寫(xiě),可以說(shuō)過(guò)去讓它寫(xiě)的代碼更多的是提效用,我姑且還算一知半解,但是涉及到音頻和數(shù)學(xué)知識(shí)的本功能我是真的一無(wú)所知,但它卻能幫我做出一個(gè)非常完美的效果,真的是科技改變生活了。
以上就是Python實(shí)現(xiàn)音頻去廣告和字幕提取的詳細(xì)內(nèi)容,更多關(guān)于Python音頻去廣告和字幕提取的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
gethostbyaddr在Python3中引發(fā)UnicodeDecodeError
本文介紹了gethostbyaddr()在Python?3中引發(fā)UnicodeDecodeError的處理方法,對(duì)大家解決問(wèn)題具有一定的參考價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-05-05Python中逗號(hào)轉(zhuǎn)為空格的三種方法
本文介紹了Python中將逗號(hào)轉(zhuǎn)換為空格的三種方法,包含使用replace函數(shù)、使用split函數(shù)、使用正則表達(dá)式,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02Python實(shí)現(xiàn)批量合并多個(gè)txt文件并生成Excel文件
在數(shù)據(jù)處理中,有時(shí)會(huì)面臨合并多個(gè)文本文件的任務(wù),本文將詳細(xì)介紹如何使用Python批量合并多個(gè)txt文件,并將其生成為一個(gè)Excel文件,需要的可以參考下2023-12-12Python+PyQT5的子線(xiàn)程更新UI界面的實(shí)例
今天小編就為大家分享一篇Python+PyQT5的子線(xiàn)程更新UI界面的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-06-06Python?Pipeline處理數(shù)據(jù)工作原理探究
如果你是一個(gè)Python開(kāi)發(fā)者,你可能聽(tīng)過(guò)"pipeline"這個(gè)術(shù)語(yǔ),但?pipeline?到底是什么,它又有什么用呢?在這篇文章中,我們將探討?Python?中的?pipeline?概念,它們是如何工作的,以及它們?nèi)绾螏椭憔帉?xiě)更清晰、更高效的代碼2024-01-01Django values()和value_list()的使用
這篇文章主要介紹了Django values()和value_list()的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03Tensorflow?2.1完成對(duì)MPG回歸預(yù)測(cè)詳解
這篇文章主要為大家介紹了Tensorflow?2.1完成對(duì)MPG回歸預(yù)測(cè)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11python3使用urllib模塊制作網(wǎng)絡(luò)爬蟲(chóng)
本文給大家介紹的是利用urllib模塊通過(guò)指定的URL抓取網(wǎng)頁(yè)內(nèi)容 所謂網(wǎng)頁(yè)抓取,就是把URL地址中指定的網(wǎng)絡(luò)資源從網(wǎng)絡(luò)流中讀取出來(lái),保存到本地,有需要的小伙伴可以參考下2016-04-04python?open函數(shù)中newline參數(shù)實(shí)例詳解
newLine()方法可用于輸出一個(gè)換行字符"/n",下面這篇文章主要給大家介紹了關(guān)于python?open函數(shù)中newline參數(shù)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06