惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
C
Check Point Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Last Week in AI
Last Week in AI
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
B
Blog
T
The Blog of Author Tim Ferriss
H
Help Net Security
云风的 BLOG
云风的 BLOG

CodeSky 代码之空

如何让 AI 成为 24 小时牛马:我的个人项目实践 - CodeSky 代码之空 AI 浪潮下真实诉求的想象空间—同志仍需努力 - CodeSky 代码之空 AI 浪潮下的新时代开发模式—AI Coding 的快速成长 - CodeSky 代码之空 体验豆包手机的 24 小时 - CodeSky 代码之空 Agent Platform Self-Host 开源项目对比 - Coze Studio - CodeSky 代码之空 Chrome 端到端网络排查战记 - CodeSky 代码之空 瞎逼逼:2025 年还没有升级研发工具的你落伍了吗 - CodeSky 代码之空 Project. 博客重建计划 - CodeSky 代码之空 群晖 Gitea + Gitea Actions 安装与部署服务教程 - CodeSky 代码之空 群晖通过 Active Backup for Business 实现服务器快照 - CodeSky 代码之空 Windows WSL 启用 NVIDIA CUDA 配置教程 - CodeSky 代码之空 HTTP Auth From BasicAuth to WebAuthn - CodeSky 代码之空 Redis 分布式锁的实现 - CodeSky 代码之空 限流与常见实现 - CodeSky 代码之空 Redis 大 key、热 key 判别和解决方案 - CodeSky 代码之空 缓存:高并发读的救世主 - CodeSky 代码之空 如何解决服务中的事务问题 - CodeSky 代码之空 聊聊 MySQL 索引与索引设计 - CodeSky 代码之空 Redis 中使用键空间监听 key 过期消息 - CodeSky 代码之空 AI:Make 死宅 Great Again - CodeSky 代码之空
Python 音频去广告+字幕提取 - CodeSky 代码之空
敖天羽 · 2025-01-28 · via CodeSky 代码之空

积压稿件 +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 编写,可以说过去让它写的代码更多的是提效用,我姑且还算一知半解,但是涉及到音频和数学知识的本功能我是真的一无所知,但它却能帮我做出一个非常完美的效果,真的是科技改变生活了。