#651 中级修复最否一条字幕 慢速失败办法

*81.175* Posted at: 8 hours ago 👁18

这个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. 健壮性增强

  • 添加更多的错误处理和日志记录
  • 确保在各种边界情况下都能正确处理

这些修复将确保最后一条字幕的音频能够正确匹配视频物理时长,避免音频被截断或视频慢速处理失败的问题。

1 Replies

Admin

代码有问题

1. if task_real_duration max_audio_duration, max_audio_duration 这个变量没有定义

2. 你的意思是最后一条字幕对应的声音还没有结束,但已达到了视频 时长,所以声音被强制中断了?如果是这样的话,在最后阶段直接定格视频,等待声音播放完毕,不是更简单吗

image

或者在音频组装阶段,判断最后一个配音时长是否大于此时视频物理时长,如果是,直接加速音频即可,类似下面代码

def _recalculate_timeline_based_on_physical_video(self, clip_meta_list):
....
elif task['type'] == 'sub':
                it = self.queue_tts[task['index']]
                it['start_time'] = current_timeline_ms
                it['end_time'] = it['start_time'] + it['dubb_time']
                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)")

                # 创建一个与视频片段等长的静音画布
                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)

#==== start 在此针对仅视频慢速、并且是最后一条有效字幕的、并且配音时长大于视频片段物理时长的情况,进行音频加速
                        if not self.shoud_audiorate and self.shoud_videorate:
                            #  仅视频加速
                            if i==len(clip_meta_list)-1 or (i==len(clip_meta_list)-2 and clip_meta_list[-1]['sub']=='gap'):
                                # 如果是最后一条,或者是倒数第二条但同时最后一条是gap
                                if len(audio_clip)>task_real_duration:
                                    # 如果此时音频时长大于视频物理时长,在此加速音频
                                    tools.precise_speed_up_audio(file_path=it['filename'], target_duration_ms=task_real_duration)
                                    segment=AudioSegment.from_file(it['filename'])

#======= end

同步对齐涉及音频加速、视频慢速、全不加速、单个加速,等多种方式和互相组合,过多修改可能导致其他模式下出问题,而调试这块非常繁琐和困难,你可以按照你的代码或上面我的思路本地测试

Post Your Reply
Open source and free maintenance is not easy. If this project is helpful to you, please consider making a small donation to help the project continue to maintain and update.

Donate: https://ko-fi.com/jianchang512

Trending Questions