这个bug的主要问题是:在视频慢速处理模式下,最后一条字幕的理论时长(基于计算)比实际物理探测的时长长很多,导致音频和视频不同步。
修复方案如下:
def _execute_video_processing(self):
"""
[修复] 视频处理阶段 - 特别处理最后一条字幕的时长问题
"""
tools.set_process(
text="[4/5] 处理视频并探测真实时长..." if config.defaulelang == 'zh' else "[4/5] Processing video & probing real durations...",
uuid=self.uuid)
config.logger.info("================== [阶段 4/5] 执行视频处理并探测真实时长 ==================")
if not self.shoud_videorate or not self.novoice_mp4_original or not tools.vail_file(self.novoice_mp4_original):
config.logger.warning("视频处理被跳过,因为未启用或无声视频文件不存在。")
for it in self.queue_tts:
it['final_video_duration_real'] = it['final_video_duration_theoretical']
return None
clip_meta_list = self._create_clip_meta()
# [修复] 先探测原始视频的实际总时长
original_video_duration = self._get_video_duration_safe(self.novoice_mp4_original)
config.logger.info(f"原始视频实际总时长: {original_video_duration}ms, 配置的总时长: {self.raw_total_time}ms")
for task in clip_meta_list:
if config.exit_soft: return None
# [修复] 特别处理最后一条字幕相关的片段
if task['type'] == 'sub' and task['index'] == len(self.queue_tts) - 1:
# 最后一条字幕,检查to时间是否超出视频实际长度
if task['to'] > original_video_duration:
config.logger.warning(f"最后一条字幕的结束时间 {task['to']}ms 超出视频实际长度 {original_video_duration}ms,将进行调整")
# 调整结束时间为视频实际长度
task['to'] = original_video_duration
# 重新计算PTS比率
actual_source_duration = task['to'] - task['ss']
if actual_source_duration > 0:
sub_item = self.queue_tts[task['index']]
pts_val = sub_item['final_video_duration_theoretical'] / actual_source_duration
task['pts'] = min(pts_val, self.max_video_pts_rate) # 限制最大PTS
config.logger.info(f"最后一条字幕调整后: ss={task['ss']}, to={task['to']}, pts={task['pts']}")
# PTS > 1.01 才应用,避免浮点数误差导致不必要的处理
pts_param = str(task['pts']) if task.get('pts', 1.0) > 1.01 else None
self._cut_to_intermediate(ss=task['ss'], to=task['to'], source=self.novoice_mp4_original, pts=pts_param,
out=task['out'])
real_duration_ms = 0
if Path(task['out']).exists() and Path(task['out']).stat().st_size > 1024:
real_duration_ms = self._get_video_duration_safe(task['out'])
task['real_duration_ms'] = real_duration_ms
if task['type'] == 'sub':
sub_item = self.queue_tts[task['index']]
sub_item['final_video_duration_real'] = real_duration_ms
# [修复] 如果最后一条字幕的物理时长明显小于理论时长,进行警告和调整
if task['index'] == len(self.queue_tts) - 1:
theoretical_duration = sub_item['final_video_duration_theoretical']
if theoretical_duration > 0 and real_duration_ms > 0:
ratio = theoretical_duration / real_duration_ms
if ratio > 1.2: # 理论时长比物理时长大20%以上
config.logger.warning(f"最后一条字幕理论时长({theoretical_duration}ms)比物理时长({real_duration_ms}ms)长很多,比率: {ratio:.2f}")
# 调整音频目标时长以匹配物理视频时长
sub_item['final_audio_duration_theoretical'] = real_duration_ms
config.logger.info(f"最后一条字幕音频目标时长已调整为物理视频时长: {real_duration_ms}ms")
config.logger.info(
f"字幕[{task['line']}] 视频片段处理完成。理论时长: {sub_item['final_video_duration_theoretical']}ms, 物理探测时长: {real_duration_ms}ms")
else:
config.logger.info(f"间隙片段 {Path(task['out']).name} 处理完成。物理探测时长: {real_duration_ms}ms")
self._concat_and_finalize(clip_meta_list)
return clip_meta_list
def _recalculate_timeline_based_on_physical_video(self, clip_meta_list):
"""
[修复] 基于视频片段的物理真实时长来构建音频片段列表 - 特别处理最后一条字幕
"""
audio_concat_list = []
current_timeline_ms = 0
for i, task in enumerate(clip_meta_list):
task_real_duration = int(task.get('real_duration_ms', 0))
if task_real_duration max_audio_duration:
speed_ratio = it['dubb_time'] / max_audio_duration
config.logger.warning(f"最后一条字幕音频需要加速: {speed_ratio:.2f}倍以适应视频物理时长")
# 使用FFmpeg加速音频
temp_audio_path = Path(self.audio_clips_folder, f"last_sub_temp_{i:05d}.wav").as_posix()
if self._accelerate_audio_segment(it['filename'], temp_audio_path, speed_ratio, max_audio_duration):
# 使用加速后的音频
audio_clip = AudioSegment.from_file(temp_audio_path)
# 清理临时文件
try:
os.remove(temp_audio_path)
except:
pass
else:
# 加速失败,使用原始音频但截断
audio_clip = AudioSegment.from_file(it['filename'])[:max_audio_duration]
config.logger.warning("音频加速失败,将截断音频以匹配视频时长")
else:
# 音频时长合适,直接使用
audio_clip = AudioSegment.from_file(it['filename'])
it['dubb_time'] = len(audio_clip)
it['end_time'] = it['start_time'] + it['dubb_time']
# 创建与视频等长的静音画布,叠加音频
base_segment = AudioSegment.silent(duration=task_real_duration)
segment = base_segment.overlay(audio_clip, position=0)
config.logger.info(f"最后一条字幕特殊处理: 音频时长={it['dubb_time']}ms, 视频时长={task_real_duration}ms")
else:
# 非最后一条字幕,使用原有逻辑
it['end_time'] = it['start_time'] + it['dubb_time']
# 创建与视频等长的静音画布
base_segment = AudioSegment.silent(duration=task_real_duration)
if tools.vail_file(it['filename']):
try:
audio_clip = AudioSegment.from_file(it['filename'])
# 将配音叠加到静音画布的开头
segment = base_segment.overlay(audio_clip)
except Exception as e:
config.logger.error(f"字幕[{it['line']}] 加载音频失败: {e},使用等长静音替代。")
segment = base_segment
else:
config.logger.warning(f"字幕[{it['line']}] 配音文件不存在,使用等长静音替代。")
segment = base_segment
it['startraw'], it['endraw'] = tools.ms_to_time_string(ms=it['start_time']), tools.ms_to_time_string(
ms=it['end_time'])
config.logger.info(
f"字幕[{it['line']}] 字幕时间精确化:新区间 {it['start_time']}-{it['end_time']} (配音时长 {it['dubb_time']}ms)")
current_timeline_ms += task_real_duration
config.logger.info(f"字幕[{it['line']}] 音频流重建:生成片段,时长 {task_real_duration}ms -> {clip_path}")
if segment:
# 导出前统一所有片段的参数
self._standardize_audio_segment(segment).export(clip_path, format="wav")
audio_concat_list.append(clip_path)
return audio_concat_list
def _accelerate_audio_segment(self, input_path, output_path, speed_ratio, target_duration_ms):
"""
[新增] 使用FFmpeg加速音频片段
"""
if not self.audio_speed_filter:
return False
try:
target_duration_sec = target_duration_ms / 1000.0
cmd = ['-y', '-i', input_path]
filter_str = ""
if self.audio_speed_filter == 'rubberband':
filter_str = f"rubberband=tempo={speed_ratio}"
elif self.audio_speed_filter == 'atempo':
tempo_filters = []
current_tempo = speed_ratio
while current_tempo > 4.0:
tempo_filters.append("atempo=4.0")
current_tempo /= 4.0
if current_tempo >= 0.5:
tempo_filters.append(f"atempo={current_tempo}")
filter_str = ",".join(tempo_filters)
if not filter_str:
return False
cmd.extend(['-filter:a', filter_str, '-t', f'{target_duration_sec:.4f}'])
cmd.extend(['-ar', str(self.AUDIO_SAMPLE_RATE), '-ac', str(self.AUDIO_CHANNELS), '-c:a', 'pcm_s16le', output_path])
return tools.runffmpeg(cmd, force_cpu=True)
except Exception as e:
config.logger.error(f"音频加速失败: {e}")
return False
主要修复点:
1. 视频时长探测与调整:
- 在处理前先探测原始视频的实际总时长
- 特别处理最后一条字幕,防止结束时间超出视频实际长度
- 重新计算PTS比率以适应实际视频长度
2. 最后一条字幕的特殊处理:
- 检测理论时长与物理时长的差异,如果差异过大(>20%)则进行警告和调整
- 自动调整音频目标时长以匹配物理视频时长
3. 音频适应性处理:
- 为最后一条字幕添加专门的音频加速逻辑
- 确保音频时长不超过视频物理时长
- 提供fallback机制,如果加速失败则截断音频
4. 健壮性增强:
- 添加更多的错误处理和日志记录
- 确保在各种边界情况下都能正确处理
这些修复将确保最后一条字幕的音频能够正确匹配视频物理时长,避免音频被截断或视频慢速处理失败的问题。