CodeSky 代码之空

随手记录自己的学习过程

Python 音频去广告+字幕提取

2025-01-28 15:55分类: Python评论: 0

积压稿件 +1

生存提示:不鼓励使用盗版资源,因此也不提供盗版资源,仅供学习交流。

之前下了一些音频课,但是存在一些音频中间插入广告,更万恶的是,它根本不分是不是整句,只要时间差不多了就插入。

要去掉广告我们分为以下步骤依次执行:

  1. 分析规律(就是前面找规律)
  2. 广告提取
  3. 识别广告
  4. 重新拼接

对于字幕提取,之前其实我们在 AI 相关的文章中也介绍过对应模型,直接转换并处理就可以了,后面再介绍。

分析规律

和写爬虫一样,第一点就是要找规律:用一张草稿纸记录每个广告的起始时间和结束时间,再分析它和整段音频的关系。

遗憾的是,在插入时或许是为了避免裁剪,逐秒计算(也叫做)后,我得出了一个结论:它是在固定时间(end_time - 3min) + random_offset 值,因为了 offset 值的介入,整个就变的玄学了起来。

还好很快我又有了一些新的想法:利用一些识别的手段把广告词裁掉就可以了。

还好广告词是固定的,而要处理的音频却多,这样计算下来 ROI 还是划算的。

广告提取

这一步是所有步骤里最耗费时间的,对于整句来说,切割分离是一个高敏感性的操作,稍微多留白几百毫秒,你听起来可能就很难受。只有原始数据切割的恰到好处,才能达到完美还原。

因此我们需要更精细化,精细到毫秒的裁剪手段。

Windows 下也不知道用啥,搜了下就选了 Audacity:

以毫秒控制选区,然后切割后如果听感是无缝的,那么就相当于抽离了,如果觉得怪怪的就再调整毫秒重新裁,如此反复直到无缝衔接。

依赖列表

下文完整的 import依赖(因为懒得在文末贴完整代码了):

1import glob
2import os
3from concurrent.futures import ThreadPoolExecutor, as_completed
4
5import numpy as np
6import librosa
7import torch
8import whisper
9from pydub import AudioSegment
10import soundfile as sf
11import torch.nn.functional as F
12import shutil
13

识别广告

接下来我们得到了两个片段,一个是完整版的音频,另一个是纯广告音频,将对应波形的相似度进行比对,找到相似的段,再进行切割。

当然,由于整段二三十分钟,相对的来说计算量会很大,由于我们知道了总是在一个音频快结束了插入广告,因此可以先裁剪缩小对比规模,然后再进行比对,减少计算量。

其中有一些非常抽象的音频和数学知识,只能说谢谢 GPT 老师(我也没学会)

1# 已知的广告片段文件
2AD_SNIPPET_FILE  = "./testcase/test2.wav"
3
4# 待处理的音频文件目录
5audio_dir = "./testcase"
6TAIL_SECONDS = 300  # 只截取最后5分钟处理
7SIMILARITY_THRESHOLD = 0.8  # 相似度阈值(0~1之间, 需根据实际情况调整)
8SUCCESS_DIR = "./success"
9device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
10
11def load_audio_segment(file_path, sr=16000, tail_seconds=None):
12    info = sf.info(file_path)
13    total_duration = info.duration
14    if tail_seconds is not None and tail_seconds < total_duration:
15        start_time = total_duration - tail_seconds
16        audio, _ = librosa.load(file_path, sr=sr, mono=True, offset=start_time, duration=tail_seconds)
17        return audio, start_time
18
19    else:
20        audio, _ = librosa.load(file_path, sr=sr, mono=True)
21        return audio, 0.0
22
23def find_audio_snippet(main_audio_path, snippet_audio, snippet_norm, sr=16000, tail_seconds=300):
24    """
25    在 main_audio_path 中寻找 snippet_audio 音频片段的出现位置。
26    snippet_audio 为事先加载好的 numpy 数组,snippet_norm 为 snippet 的二范数,用于相似度计算。
27    返回 (ad_start_time, ad_end_time, similarity)
28    若未找到则返回 (None, None, None)
29    """
30    main_audio, main_start = load_audio_segment(main_audio_path, sr=sr, tail_seconds=tail_seconds)
31    if len(snippet_audio) > len(main_audio):
32        return None, None, None
33    # 转换到 GPU 张量
34    main_audio_t = torch.from_numpy(main_audio).float().to(device).unsqueeze(0).unsqueeze(0)  # [1,1,M]
35    snippet_audio_t = torch.from_numpy(snippet_audio).float().to(device).unsqueeze(0).unsqueeze(0)  # [1,1,S]
36    # 使用 conv1d 来进行类似相似度搜索 (无 snippet 翻转)
37    correlation = F.conv1d(main_audio_t, snippet_audio_t)
38    correlation = correlation[0, 0].cpu().numpy()
39    best_index = np.argmax(correlation)
40    best_value = correlation[best_index]
41    # 相似度计算:归一化
42    similarity = best_value / snippet_norm if snippet_norm > 0 else 0
43    ad_start_time = main_start + best_index / sr
44    ad_end_time = ad_start_time + len(snippet_audio) / s
45
46    return ad_start_time, ad_end_time, similarity
47

重新拼接

找到广告后我们将广告段落减去,然后再重新拼接生成新的音频文件即可。

1
2def process_file(filename, snippet_audio, snippet_norm, sr=16000, tail_seconds=300, similarity_threshold=0.8):
3    """
4    处理单个文件,找到广告并移除。
5    """
6    ad_start, ad_end, similarity = find_audio_snippet(filename, snippet_audio, snippet_norm, sr=sr,
7                                                      tail_seconds=tail_seconds)
8
9    if ad_start is not None and similarity > similarity_threshold:
10        # 去除广告段落
11        audio = AudioSegment.from_file(filename)
12        part1 = audio[:ad_start * 1000]
13        part2 = audio[ad_end * 1000:]
14        cleaned = part1 + part2
15        cleaned_file = f"output/{os.path.basename(filename)}"
16        cleaned.export(cleaned_file, format="mp3")
17        shutil.move(filename, SUCCESS_DIR)
18        return f"{filename} 已移除广告,生成 {cleaned_file},相似度:{similarity}"
19    else:
20        return f"{filename} 中未高相似度检测到广告或相似度过低({similarity})"
21
22
23def remove_ads():
24    sr = 16000
25    # 预先加载广告片段
26    snippet_audio, _ = librosa.load(AD_SNIPPET_FILE , sr=sr, mono=True)
27    # 计算snippet的范数,用于相似度归一化
28    snippet_norm = np.dot(snippet_audio, snippet_audio)
29
30    file_list = [os.path.join(audio_dir, f) for f in os.listdir(audio_dir) if
31                 f.endswith(".mp3")]
32
33    # 使用多线程加速处理
34    # 线程数可根据您的机器资源调整
35    max_workers = 20
36    results = []
37    with ThreadPoolExecutor(max_workers=max_workers) as executor:
38        futures = {
39            executor.submit(process_file, file, snippet_audio, snippet_norm, sr, TAIL_SECONDS,
40                            SIMILARITY_THRESHOLD): file
41            for file in file_list
42        }
43
44        for future in as_completed(futures):
45            file = futures[future]
46            try:
47                res = future.result()
48                print(res)
49            except Exception as e:
50                print(f"{file} 处理时出错: {e}")
51

字幕提取

下一个问题是,音频是提取好了,但是音频的字幕和总结能力其实也是一个亮点,这个也是我们想要有的能力,而好多都是付费的,百度网盘虽然会员免费,但是实际听音频的过程中遇到了 Bug,让我不得不另谋高就。

要使用这个能力,核心还是使用 whisper这个模型的能力。

我考虑用 Emby 来当音频播放器,字幕可以和歌词字幕一样,因此就需要生成 lrc 格式的标准文件。

也就是:

  1. 提取字幕
  2. 给每段字幕和时间轴进行格式转换,转为 lrc 标准格式

而跑 AI 模型的时候,务必保证 GPU 加速(否则你会卡的痛不欲生)。

模型请根据自己的内存和实际情况决定,不一定是越大越好的,可以先跑一段音频试试效果。

如果本地没有找到对应的模型,whisper 先尝试下载,也可以使用本地准备好的模型。

1def format_lrc_timestamp(seconds: float) -> str:
2    """将秒数转换为 LRC 格式时间戳 [mm:ss.xx]"""
3    total_seconds = int(seconds)
4    m = total_seconds // 60
5    s = total_seconds % 60
6    # 毫秒取两位小数
7    ms = (seconds - total_seconds) * 100
8    return f"[{m:02d}:{s:02d}.{int(ms):02d}]"
9
10def trans_files():
11    # 请将此路径替换为你的音频目录路径
12    audio_directory = "./audio_files"
13    # 可根据需要选择模型大小,如 "small"、"medium"、"large"
14    trans_text(audio_directory, model_name="medium", language="zh")
15
16def transcribe_to_lrc(audio_path: str, lrc_path: str, model, language: str = "zh"):
17    """
18    使用已加载的 whisper model 对 audio_path 进行转录,
19    并将结果保存为 lrc_path 文件。
20    """
21    result = model.transcribe(audio_path, language=language)
22    segments = result.get("segments", [])
23
24    with open(lrc_path, "w", encoding="utf-8") as f:
25        # 可根据需要添加标签信息,如标题、歌手、专辑
26        f.write("[ti:未知标题]\n")
27        f.write("[ar:未知作者]\n")
28        f.write("[al:未知专辑]\n\n")
29
30        for seg in segments:
31            start_time = format_lrc_timestamp(seg['start'])
32            text = seg['text'].strip()
33            f.write(f"{start_time}{text}\n")
34
35
36def trans_text(audio_dir: str, model_name: str = "medium", language: str = "zh"):
37    # 尝试使用 GPU
38    device = "cuda:0" if torch.cuda.is_available() else "cpu"
39    print(f"使用设备: {device}")
40
41    # 加载模型到指定设备
42    # 模型大小可根据资源调整,如:tiny, base, small, medium, large
43    print(f"加载 Whisper 模型 ({model_name}),请稍候...")
44    model = whisper.load_model(model_name, device=device)
45    print("模型加载完成。")
46
47    # 遍历指定目录下所有 mp3
48    audio_files = glob.glob(os.path.join(audio_dir, "*.mp3"))
49    if not audio_files:
50        print("指定目录中未找到 MP3 文件。")
51        return
52
53    for audio_path in audio_files:
54        base_name = os.path.splitext(audio_path)[0]
55        lrc_path = base_name + ".lrc"
56
57        print(f"处理文件: {audio_path} -> {lrc_path}")
58        transcribe_to_lrc(audio_path, lrc_path, model=model, language=language)
59        print(f"完成: {lrc_path}")
60
61    print("所有文件处理完成!")
62
63def trans_files():
64    # 请将此路径替换为你的音频目录路径
65    audio_directory = "./audio_files"
66    # 可根据需要选择模型大小,如 "small"、"medium"、"large"
67    trans_text(audio_directory, model_name="medium", language="zh")
68

目前我还没有做总结功能(主要是普通播放器也没地方显示总结),但是有了全量文本,相信对于各位来说并不是难事。

总结

本文的所有代码均由 AI 编写,可以说过去让它写的代码更多的是提效用,我姑且还算一知半解,但是涉及到音频和数学知识的本功能我是真的一无所知,但它却能帮我做出一个非常完美的效果,真的是科技改变生活了。

评论 (0)