feat: speed up output clips to 1.25x speed by default and scale subtitle/mute timestamps
This commit is contained in:
+27
-11
@@ -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('-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('--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('--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -1020,16 +1021,30 @@ if __name__ == '__main__':
|
|||||||
clip_temp_path = os.path.join(output_dir, f"temp_{clip_filename}")
|
clip_temp_path = os.path.join(output_dir, f"temp_{clip_filename}")
|
||||||
clip_final_path = os.path.join(output_dir, clip_filename)
|
clip_final_path = os.path.join(output_dir, clip_filename)
|
||||||
|
|
||||||
# ffmpeg cut
|
# ffmpeg cut & speed up
|
||||||
cut_command = [
|
if args.speed != 1.0:
|
||||||
'ffmpeg', '-y',
|
setpts = 1.0 / args.speed
|
||||||
'-ss', str(start),
|
cut_command = [
|
||||||
'-to', str(end),
|
'ffmpeg', '-y',
|
||||||
'-i', input_video,
|
'-ss', str(start),
|
||||||
'-c:v', 'libx264', '-crf', '18', '-preset', 'fast',
|
'-to', str(end),
|
||||||
'-c:a', 'aac',
|
'-i', input_video,
|
||||||
clip_temp_path
|
'-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)
|
subprocess.run(cut_command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
# Generate SRT for this clip and get mute ranges for profanity
|
# Generate SRT for this clip and get mute ranges for profanity
|
||||||
@@ -1038,7 +1053,8 @@ if __name__ == '__main__':
|
|||||||
success_srt, mute_ranges = generate_srt(
|
success_srt, mute_ranges = generate_srt(
|
||||||
transcript, start, end, srt_path,
|
transcript, start, end, srt_path,
|
||||||
max_chars=20, max_duration=2.0,
|
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
|
# Process vertical, passing the relative mute ranges for profanity muting
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def _merge_overlapping(ranges, pad_seconds=0.05):
|
|||||||
merged.append((s, e))
|
merged.append((s, e))
|
||||||
return merged
|
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.
|
Generates an SRT file from the transcript for a specific time range.
|
||||||
Groups words into short lines suitable for vertical video.
|
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())
|
cleaned_word = re.sub(r'[^\w]', '', word_text.lower())
|
||||||
|
|
||||||
if cleaned_word in swears:
|
if cleaned_word in swears:
|
||||||
# Record absolute mute range relative to clip start
|
# Record absolute mute range relative to clip start, scaled by speed_factor
|
||||||
rel_start = max(0.0, word_copy['start'] - clip_start)
|
rel_start = (max(0.0, word_copy['start'] - clip_start)) / speed_factor
|
||||||
rel_end = max(0.0, word_copy['end'] - clip_start)
|
rel_end = (max(0.0, word_copy['end'] - clip_start)) / speed_factor
|
||||||
raw_mute_ranges.append((rel_start, rel_end))
|
raw_mute_ranges.append((rel_start, rel_end))
|
||||||
# Censor word
|
# Censor word
|
||||||
word_copy['word'] = censor_word(word_text)
|
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
|
block_start = None
|
||||||
|
|
||||||
for i, word in enumerate(words):
|
for i, word in enumerate(words):
|
||||||
# Adjust times relative to clip
|
# Adjust times relative to clip, scaled by speed_factor
|
||||||
start = max(0, word['start'] - clip_start)
|
start = (max(0, word['start'] - clip_start)) / speed_factor
|
||||||
end = max(0, word['end'] - clip_start)
|
end = (max(0, word['end'] - clip_start)) / speed_factor
|
||||||
|
|
||||||
# Word info copy with relative times
|
# Word info copy with relative times
|
||||||
w_rel = {
|
w_rel = {
|
||||||
|
|||||||
Reference in New Issue
Block a user