diff --git a/openshorts/main.py b/openshorts/main.py index a41f309..aa15fe1 100644 --- a/openshorts/main.py +++ b/openshorts/main.py @@ -887,6 +887,7 @@ if __name__ == '__main__': parser.add_argument('-o', '--output', type=str, help="Output directory or file (if processing whole video).") parser.add_argument('--keep-original', action='store_true', help="Keep the downloaded YouTube video.") parser.add_argument('--skip-analysis', action='store_true', help="Skip AI analysis and convert the whole video.") + parser.add_argument('--speed', type=float, default=1.25, help="Speed up factor for the output clips (default: 1.25).") args = parser.parse_args() @@ -1020,16 +1021,30 @@ if __name__ == '__main__': clip_temp_path = os.path.join(output_dir, f"temp_{clip_filename}") clip_final_path = os.path.join(output_dir, clip_filename) - # ffmpeg cut - cut_command = [ - 'ffmpeg', '-y', - '-ss', str(start), - '-to', str(end), - '-i', input_video, - '-c:v', 'libx264', '-crf', '18', '-preset', 'fast', - '-c:a', 'aac', - clip_temp_path - ] + # ffmpeg cut & speed up + if args.speed != 1.0: + setpts = 1.0 / args.speed + cut_command = [ + 'ffmpeg', '-y', + '-ss', str(start), + '-to', str(end), + '-i', input_video, + '-filter_complex', f'[0:v]setpts={setpts:.6f}*PTS[v];[0:a]atempo={args.speed:.6f}[a]', + '-map', '[v]', '-map', '[a]', + '-c:v', 'libx264', '-crf', '18', '-preset', 'fast', + '-c:a', 'aac', + clip_temp_path + ] + else: + cut_command = [ + 'ffmpeg', '-y', + '-ss', str(start), + '-to', str(end), + '-i', input_video, + '-c:v', 'libx264', '-crf', '18', '-preset', 'fast', + '-c:a', 'aac', + clip_temp_path + ] subprocess.run(cut_command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) # Generate SRT for this clip and get mute ranges for profanity @@ -1038,7 +1053,8 @@ if __name__ == '__main__': success_srt, mute_ranges = generate_srt( transcript, start, end, srt_path, max_chars=20, max_duration=2.0, - swears_path=swears_path, return_mute_ranges=True + swears_path=swears_path, return_mute_ranges=True, + speed_factor=args.speed ) # Process vertical, passing the relative mute ranges for profanity muting diff --git a/openshorts/subtitles.py b/openshorts/subtitles.py index 145b64d..3b67703 100644 --- a/openshorts/subtitles.py +++ b/openshorts/subtitles.py @@ -90,7 +90,7 @@ def _merge_overlapping(ranges, pad_seconds=0.05): merged.append((s, e)) return merged -def generate_srt(transcript, clip_start, clip_end, output_path, max_chars=20, max_duration=2.0, swears_path=None, return_mute_ranges=False): +def generate_srt(transcript, clip_start, clip_end, output_path, max_chars=20, max_duration=2.0, swears_path=None, return_mute_ranges=False, speed_factor=1.0): """ Generates an SRT file from the transcript for a specific time range. Groups words into short lines suitable for vertical video. @@ -110,9 +110,9 @@ def generate_srt(transcript, clip_start, clip_end, output_path, max_chars=20, ma cleaned_word = re.sub(r'[^\w]', '', word_text.lower()) if cleaned_word in swears: - # Record absolute mute range relative to clip start - rel_start = max(0.0, word_copy['start'] - clip_start) - rel_end = max(0.0, word_copy['end'] - clip_start) + # Record absolute mute range relative to clip start, scaled by speed_factor + rel_start = (max(0.0, word_copy['start'] - clip_start)) / speed_factor + rel_end = (max(0.0, word_copy['end'] - clip_start)) / speed_factor raw_mute_ranges.append((rel_start, rel_end)) # Censor word word_copy['word'] = censor_word(word_text) @@ -134,9 +134,9 @@ def generate_srt(transcript, clip_start, clip_end, output_path, max_chars=20, ma block_start = None for i, word in enumerate(words): - # Adjust times relative to clip - start = max(0, word['start'] - clip_start) - end = max(0, word['end'] - clip_start) + # Adjust times relative to clip, scaled by speed_factor + start = (max(0, word['start'] - clip_start)) / speed_factor + end = (max(0, word['end'] - clip_start)) / speed_factor # Word info copy with relative times w_rel = {