-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake_timelapse_dynamic_batch.sh
More file actions
767 lines (695 loc) · 40.3 KB
/
Copy pathmake_timelapse_dynamic_batch.sh
File metadata and controls
767 lines (695 loc) · 40.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
#!/bin/bash
# --- Default Configuration (can be overridden by user input) ---
DEFAULT_PARENT_IMAGE_DIR="timelapse_projects" # New default for the parent dir
DEFAULT_IMAGE_DIR="input_images"
DEFAULT_FPS_INPUT=10
DEFAULT_FPS_OUTPUT=30
DEFAULT_CRF_VALUE=18
DEFAULT_SCALE_CHOICE="original"
DEFAULT_CODEC_CHOICE="libx264_mp4" # More specific default
DEFAULT_PRESET="medium"
DEFAULT_PRORES_PROFILE="hq" # 3 for prores_ks, or 'hq' for prores_aw
DEFAULT_DNXHR_PROFILE="hq" # hq, hqx, sq, lb
DEFAULT_OUTPUT_DIR_BATCH="timelapses_output" # New default for batch output
# --- Semi-Dynamic Pattern Configuration ---
DEFAULT_FILENAME_PREFIX="P"
DEFAULT_FILENAME_SUFFIX=".JPG"
# --- Static Configuration ---
PIXEL_FORMAT_DEFAULT="yuv420p" # Default, will be changed for some codecs
OUTPUT_VIDEO_BASENAME="timelapse_highres"
# --- Global Variables ---
IMAGE_DIR=""
OUTPUT_DIR=""
FPS_INPUT=""
FPS_OUTPUT=""
CRF_VALUE=""
VIDEO_CODEC_OPTION_NAME="" # User-friendly name like "H.264/MP4"
VIDEO_CODEC=""
OUTPUT_EXTENSION=""
CODEC_PRESET=""
SCALE_FILTER_STRING=""
RESOLUTION_DESC=""
IMAGE_PATTERN=""
START_NUMBER=""
NUM_FRAMES_TO_PROCESS=0
FILENAME_PREFIX=""
FILENAME_SUFFIX=""
PIXEL_FORMAT_FINAL="" # Final pixel format to use
PRORES_PROFILE_VAL="" # For prores profile
DNX_BITRATE_OR_PROFILE="" # For DNxHD/HR
VP9_CPU_USED="" # New global for VP9 -cpu-used
FINAL_VF_STRING=""
MAIN_OUTPUT_DIR="" # The single output directory for all batch videos
# --- Functions ---
# get_image_directory, get_output_directory, get_input_fps, get_output_fps,
# determine_image_sequence_parameters, count_sequential_images, get_scale_choice
# I'll keep them shortened for this example. Ensure you have the full correct versions.
# NEW Function: Get the main output directory for all batch jobs
get_main_output_directory() {
local dir_prompt="Enter the main output directory for ALL timelapse videos"
local default_dir="$DEFAULT_OUTPUT_DIR_BATCH"
local chosen_dir
# Uses similar logic to your existing get_output_directory for creation/writability
while true; do
read -e -p "$dir_prompt [$default_dir]: " -i "$default_dir" chosen_dir
chosen_dir="${chosen_dir:-$default_dir}"; chosen_dir="${chosen_dir%/}"
if [ ! -d "$chosen_dir" ]; then
read -p "Directory '$chosen_dir' does not exist. Create it? (y/N): " create_confirm
if [[ "$create_confirm" == [yY] || "$create_confirm" == [yY][eE][sS] ]]; then
mkdir -p "$chosen_dir"
if [ $? -ne 0 ]; then echo "Error: Could not create directory."; continue
else echo "Created directory: $chosen_dir"; fi
else echo "Please enter an existing directory or allow creation."; continue; fi
fi
if [ -w "$chosen_dir" ]; then MAIN_OUTPUT_DIR="$chosen_dir"; echo "Using main output directory: $MAIN_OUTPUT_DIR"; break
else echo "Error: Directory '$chosen_dir' is not writable."; fi
done
}
# NEW Function: Find suitable image subdirectories within a parent directory
get_image_directories_for_batch() {
local parent_dir_prompt="Enter the PARENT directory containing your timelapse sequence subdirectories"
local default_parent_dir="$DEFAULT_PARENT_IMAGE_DIR"
local chosen_parent_dir
local found_dirs=()
while true; do
read -e -p "$parent_dir_prompt [$default_parent_dir]: " -i "$default_parent_dir" chosen_parent_dir
chosen_parent_dir="${chosen_parent_dir:-$default_parent_dir}"; chosen_parent_dir="${chosen_parent_dir%/}"
if [ -d "$chosen_parent_dir" ]; then break
else echo "Error: Parent directory '$chosen_parent_dir' not found."; fi
done
echo "Scanning '$chosen_parent_dir' for timelapse sequence subdirectories..."
# Find subdirectories that contain at least one file matching the prefix/suffix
# This is a heuristic; might need refinement.
# We look for directories, then check if they contain matching files.
while IFS= read -r subdir; do
# Check if subdir contains files like P*.JPG
if find "$subdir" -maxdepth 1 -type f -name "${FILENAME_PREFIX}*[0-9]*${FILENAME_SUFFIX}" -print -quit | grep -q .; then
echo "Found potential sequence directory: $subdir"
found_dirs+=("$subdir")
fi
done < <(find "$chosen_parent_dir" -mindepth 1 -maxdepth 1 -type d -print)
if [ ${#found_dirs[@]} -eq 0 ]; then
echo "No suitable subdirectories found in '$chosen_parent_dir' that contain files matching '${FILENAME_PREFIX}*${FILENAME_SUFFIX}'."
echo "You can also manually enter directory paths one by one."
read -p "Do you want to manually enter directory paths? (y/N): " manual_entry
if [[ "$manual_entry" == [yY] || "$manual_entry" == [yY][eE][sS] ]]; then
echo "Enter full paths to image directories, one per line. Press Enter on an empty line to finish."
local manual_dir
while true; do
read -e -p "Image directory path (or Enter to finish): " manual_dir
if [ -z "$manual_dir" ]; then break; fi
if [ -d "$manual_dir" ]; then
if find "$manual_dir" -maxdepth 1 -type f -name "${FILENAME_PREFIX}*[0-9]*${FILENAME_SUFFIX}" -print -quit | grep -q .; then
found_dirs+=("$manual_dir")
echo "Added: $manual_dir"
else
echo "Warning: Directory '$manual_dir' added, but no files matching pattern found inside. It might be skipped."
found_dirs+=("$manual_dir") # Add anyway, let sequence detection fail later
fi
else echo "Error: '$manual_dir' is not a valid directory."; fi
done
fi
fi
if [ ${#found_dirs[@]} -eq 0 ]; then
echo "No image directories specified for batch processing. Exiting."
exit 1
fi
# Return the array of found directories (caller will iterate over this)
# Bash doesn't directly return arrays from functions easily. We'll populate a global.
IMAGE_DIRS_TO_PROCESS=("${found_dirs[@]}")
}
get_image_directory() { # Shortened
local dir_prompt="Enter the directory containing your JPEG photos"; local default_dir="$DEFAULT_IMAGE_DIR"; local chosen_dir
while true; do read -e -p "$dir_prompt [$default_dir]: " -i "$default_dir" chosen_dir; chosen_dir="${chosen_dir:-$default_dir}"; chosen_dir="${chosen_dir%/}";
if [ -d "$chosen_dir" ]; then IMAGE_DIR="$chosen_dir"; echo "Using image directory: $IMAGE_DIR"; break; else echo "Error: Directory '$chosen_dir' not found."; fi; done
}
get_output_directory() { # Shortened
local parent_of_image_dir; parent_of_image_dir=$(dirname "$IMAGE_DIR"); if [ "$parent_of_image_dir" == "." ] && [[ ! "$IMAGE_DIR" =~ / ]]; then parent_of_image_dir="."; elif [ "$parent_of_image_dir" == "/" ] && [ "$IMAGE_DIR" != "/" ]; then parent_of_image_dir="/"; fi
local dir_prompt="Enter the output directory for the video"; local default_dir="$parent_of_image_dir"; local chosen_dir
while true; do read -e -p "$dir_prompt [$default_dir]: " -i "$default_dir" chosen_dir; chosen_dir="${chosen_dir:-$default_dir}"; chosen_dir="${chosen_dir%/}";
if [ ! -d "$chosen_dir" ]; then read -p "Directory '$chosen_dir' does not exist. Create it? (y/N): " create_confirm; if [[ "$create_confirm" == [yY] || "$create_confirm" == [yY][eE][sS] ]]; then mkdir -p "$chosen_dir"; if [ $? -ne 0 ]; then echo "Error: Could not create directory."; continue; else echo "Created directory: $chosen_dir"; fi; else echo "Please enter an existing directory."; continue; fi; fi
if [ -w "$chosen_dir" ]; then OUTPUT_DIR="$chosen_dir"; echo "Using output directory: $OUTPUT_DIR"; break; else echo "Error: Directory '$chosen_dir' is not writable."; fi; done
}
get_input_fps() { # Shortened
local fps_prompt="Enter the input FPS"; local default_fps="$DEFAULT_FPS_INPUT"; local chosen_fps
while true; do read -e -p "$fps_prompt [$default_fps]: " -i "$default_fps" chosen_fps; chosen_fps="${chosen_fps:-$default_fps}";
if [[ "$chosen_fps" =~ ^[0-9]+([.,][0-9]+)?$ && $(LC_NUMERIC=C bc <<< "$(echo "$chosen_fps" | sed 's/,/./') > 0") -eq 1 ]]; then FPS_INPUT=$(echo "$chosen_fps" | sed 's/,/./'); echo "Using input FPS: $FPS_INPUT"; break; else echo "Error: Invalid FPS."; fi; done
}
get_output_fps() { # Shortened
local fps_prompt="Enter the output video FPS"; local default_fps="$DEFAULT_FPS_OUTPUT"; local chosen_fps
while true; do read -e -p "$fps_prompt [$default_fps]: " -i "$default_fps" chosen_fps; chosen_fps="${chosen_fps:-$default_fps}";
if [[ "$chosen_fps" =~ ^[0-9]+([.,][0-9]+)?$ && $(LC_NUMERIC=C bc <<< "$(echo "$chosen_fps" | sed 's/,/./') > 0") -eq 1 ]]; then FPS_OUTPUT=$(echo "$chosen_fps" | sed 's/,/./'); echo "Using output video FPS: $FPS_OUTPUT"; break; else echo "Error: Invalid FPS."; fi; done
}
# determine_image_sequence_parameters MODIFIED for batch
# Takes the current image directory as $1
determine_image_sequence_parameters() {
local current_processing_dir="$1" # Use argument
echo "Attempting to determine image sequence parameters for '$current_processing_dir'..."
local first_file_found="" min_num=-1 num_digits=0
# FILENAME_PREFIX and FILENAME_SUFFIX are global and set once
while IFS= read -r file_path; do
local filename=$(basename "$file_path")
if [[ "$filename" == "${FILENAME_PREFIX}"*"${FILENAME_SUFFIX}" ]]; then
local potential_num_part=${filename#"$FILENAME_PREFIX"}
potential_num_part=${potential_num_part%"$FILENAME_SUFFIX"}
if [[ "$potential_num_part" =~ ^[0-9]+$ ]]; then
local current_num_val=$((10#$potential_num_part))
if [ "$min_num" -eq -1 ] || [ "$current_num_val" -lt "$min_num" ]; then
min_num=$current_num_val
first_file_found="$filename"
num_digits=${#potential_num_part}
fi
fi
fi
done < <(find "$current_processing_dir" -maxdepth 1 -type f -name "${FILENAME_PREFIX}*[0-9]*${FILENAME_SUFFIX}" 2>/dev/null | sort -V)
if [ -n "$first_file_found" ]; then
START_NUMBER=$min_num # Set global for this iteration
IMAGE_PATTERN="${FILENAME_PREFIX}%0${num_digits}d${FILENAME_SUFFIX}" # Set global
echo "Detected starting file in '$current_processing_dir': '$first_file_found'"
echo "Deduced START_NUMBER: $START_NUMBER, IMAGE_PATTERN: $IMAGE_PATTERN"
return 0
else
echo "Error: Could not determine image sequence in '$current_processing_dir'."
return 1
fi
}
# count_sequential_images MODIFIED for batch
# Takes the current image directory as $1
# Uses global IMAGE_PATTERN and START_NUMBER (set by determine_image_sequence_parameters for this dir)
count_sequential_images() {
local current_processing_dir="$1" # Use argument
local count=0
local current_num=$START_NUMBER # Uses global START_NUMBER set for this dir
local file_to_check
local first_file_expected
first_file_expected=$(printf "${current_processing_dir}/${IMAGE_PATTERN}" "$START_NUMBER")
echo "Checking for sequential images in '$current_processing_dir'..."
echo "Starting search with pattern: '${current_processing_dir}/${IMAGE_PATTERN}'"
echo "Expecting first image: '$first_file_expected'"
while true; do
file_to_check=$(printf "${current_processing_dir}/${IMAGE_PATTERN}" "$current_num")
if [ -f "$file_to_check" ]; then
((count++)); ((current_num++))
else
if [ "$count" -eq 0 ]; then
echo "Warning: Expected starting image '$first_file_expected' NOT found in '$current_processing_dir'."
else
local last_found_file=$(printf "${current_processing_dir}/${IMAGE_PATTERN}" "$((current_num - 1))")
echo "Found sequence of $count images in '$current_processing_dir', ending with: '$last_found_file'."
echo "Next expected file ('$file_to_check') not found."
fi
break
fi
done
NUM_FRAMES_TO_PROCESS=$count # Set global for this iteration
if [ "$count" -gt 0 ]; then echo "Detected $NUM_FRAMES_TO_PROCESS image(s) to process in '$current_processing_dir'."
else echo "No sequential images detected in '$current_processing_dir'."; fi
}
get_scale_choice() {
echo "--- Resolution Scaling ---"
echo "Choose an output resolution:"
echo " 1. Original (no scaling)"
echo " 2. Percentage (e.g., 50%)"
echo " 3. 1080p (1920xH, preserves aspect ratio)"
echo " 4. 720p (1280xH, preserves aspect ratio)"
echo " 5. Custom WxH (e.g., 1920x1080, be careful with aspect ratio)"
local choice
local scale_desc_temp="Original" # Default description
SCALE_FILTER_STRING="" # Default filter (no scaling)
# Determine a valid default for the prompt
local prompt_default_val="1" # Corresponds to "original" case
local prompt_default_display="$DEFAULT_SCALE_CHOICE" # User-friendly default display
case "$DEFAULT_SCALE_CHOICE" in
"original") prompt_default_val="1" ;;
"percentage") prompt_default_val="2" ;;
"1080p") prompt_default_val="3" ;;
"720p") prompt_default_val="4" ;;
"custom") prompt_default_val="5" ;;
esac
while true; do
read -e -p "Enter scale choice (1-5) [$prompt_default_display, e.g., $prompt_default_val]: " -i "$prompt_default_val" choice
choice="${choice:-$prompt_default_val}" # If user just presses enter, use numeric default
case "$choice" in
1|"original")
SCALE_FILTER_STRING=""
scale_desc_temp="Original"
echo "Selected: Original resolution."
break
;;
2|"percentage")
local percentage
while true; do
read -e -p "Enter percentage (e.g., 50 for 50%): " percentage
if [[ "$percentage" =~ ^[0-9]+$ && "$percentage" -gt 0 && "$percentage" -le 200 ]]; then
SCALE_FILTER_STRING="scale=w=trunc(iw*${percentage}/100/2)*2:h=-2:flags=lanczos"
scale_desc_temp="${percentage}% of original (approx, even dimensions)"
echo "Selected: Scale to approximately ${percentage}% of original (ensuring even dimensions)."
break
else
echo "Invalid percentage. Please enter a number between 1 and 200."
fi
done
break
;;
3|"1080p")
SCALE_FILTER_STRING="scale=1920:-2:flags=lanczos"
scale_desc_temp="1080p (1920xH, aspect preserved)"
echo "Selected: 1080p."
break
;;
4|"720p")
SCALE_FILTER_STRING="scale=1280:-2:flags=lanczos"
scale_desc_temp="720p (1280xH, aspect preserved)"
echo "Selected: 720p."
break
;;
5|"custom")
local custom_w local custom_h
while true; do
read -e -p "Enter custom width (must be even, e.g., 1920): " custom_w
read -e -p "Enter custom height (must be even, or -2 to preserve aspect based on width, e.g., 1080 or -2): " custom_h
if [[ "$custom_w" =~ ^[0-9]+$ && "$custom_w" -gt 0 && $((custom_w % 2)) -eq 0 && \
( ( "$custom_h" =~ ^[0-9]+$ && "$custom_h" -gt 0 && $((custom_h % 2)) -eq 0 ) || "$custom_h" == "-2" ) ]]; then
SCALE_FILTER_STRING="scale=${custom_w}:${custom_h}:flags=lanczos"
scale_desc_temp="Custom ${custom_w}x${custom_h}"
if [ "$custom_h" == "-2" ]; then
scale_desc_temp="Custom ${custom_w}x(auto_even_H)"
fi
echo "Selected: Custom resolution ${custom_w}x${custom_h}."
break
else
echo "Invalid width/height. Width and Height must be positive EVEN numbers, or Height can be -2."
fi
done
break
;;
*)
echo "Invalid choice. Please select a number from 1 to 5."
;;
esac
done
RESOLUTION_DESC="$scale_desc_temp" # Set global variable
}
get_crf_value_for_codec() {
local crf_prompt_msg; local min_crf=0; local max_crf=51; local default_val="$DEFAULT_CRF_VALUE"
CRF_VALUE="" # Reset CRF if not applicable
case "$VIDEO_CODEC" in
"libx264") crf_prompt_msg="CRF H.264 (0-51, ~18-28 good)"; default_val=23 ;;
"libx265") crf_prompt_msg="CRF H.265 (0-51, ~22-30 good)"; default_val=28 ;;
"libvpx-vp9") crf_prompt_msg="CRF VP9 (0-63, ~15-35 good)"; max_crf=63; default_val=31 ;;
"prores_ks"|"prores_aw"|"dnxhd"|"dnxhr") # These don't use CRF, use profile/bitrate
echo "CRF not applicable for $VIDEO_CODEC. Quality set by profile/bitrate."
return ;; # Skip CRF prompt
*) crf_prompt_msg="CRF value (lower better)"; max_crf=63; default_val=25 ;;
esac
local chosen_crf
while true; do
read -e -p "$crf_prompt_msg [$default_val]: " -i "$default_val" chosen_crf
chosen_crf="${chosen_crf:-$default_val}"
if [[ "$chosen_crf" =~ ^[0-9]+$ && "$chosen_crf" -ge "$min_crf" && "$chosen_crf" -le "$max_crf" ]]; then
CRF_VALUE="$chosen_crf"; echo "Using CRF: $CRF_VALUE for $VIDEO_CODEC"; break
else echo "Invalid CRF for $VIDEO_CODEC. Range: $min_crf-$max_crf."; fi
done
}
get_video_codec_settings() {
echo "--- Video Codec & Format ---"
echo "Choose video codec and output format:"
# Define options in an array for easier handling
# Format: "User-friendly Name|ffmpeg_codec_name|output_extension|pixel_format_override|is_crf_based"
# pixel_format_override: set if codec needs specific pix_fmt, else uses PIXEL_FORMAT_DEFAULT
# is_crf_based: "true" or "false"
codec_options=(
"H.264 / .mp4 (Compat)|libx264|mp4|yuv420p|true"
"H.265 (HEVC) / .mp4|libx265|mp4|yuv420p|true"
"H.265 (HEVC) / .mkv|libx265|mkv|yuv420p|true"
"VP9 / .webm (Web)|libvpx-vp9|webm|yuv420p|true"
"VP9 / .mkv|libvpx-vp9|mkv|yuv420p|true"
"ProRes 422 HQ / .mov (Edit Friendly)|prores_ks|mov|yuv422p10le|false" # prores_ks common for FFmpeg
"ProRes 422 Standard / .mov|prores_ks|mov|yuv422p10le|false"
"DNxHR HQX (12-bit) / .mov (Edit Friendly)|dnxhd|mov|yuv422p10le|false" # Using dnxhd for dnxhr profiles
"DNxHR HQ (8-bit) / .mov|dnxhd|mov|yuv420p|false" # DNxHR HQ can be 8-bit yuv420p
)
# Note: For prores_aw, pix_fmts are different (e.g. yuv422p10le, yuv444p10le)
# prores_ks is more flexible on input but often outputs yuv422p10le for HQ.
# DNxHD/HR also has various pixel format requirements based on profile.
# For simplicity, using yuv422p10le for higher-end ProRes/DNxHR, yuv420p for others.
presets=(ultrafast superfast veryfast faster fast medium slow slower veryslow placebo)
local old_cols_main_codec=$COLUMNS
COLUMNS=1
PS3="Select codec option (or press Enter for default H.264/mp4): "
select opt_str in "${codec_options[@]}"; do
if [[ -n "$opt_str" ]]; then
# Parse the selected option string
IFS='|' read -r VIDEO_CODEC_OPTION_NAME VIDEO_CODEC OUTPUT_EXTENSION PIXEL_FORMAT_OVERRIDE IS_CRF_BASED <<< "$opt_str"
# Assign default if user just pressed enter with a valid number
break
elif [[ -z "$opt_str" && -n "$REPLY" && "$REPLY" -ge 1 && "$REPLY" -le ${#codec_options[@]} ]]; then
IFS='|' read -r VIDEO_CODEC_OPTION_NAME VIDEO_CODEC OUTPUT_EXTENSION PIXEL_FORMAT_OVERRIDE IS_CRF_BASED <<< "${codec_options[$((REPLY-1))]}"
break
elif [[ -z "$opt_str" && -z "$REPLY" ]]; then # Default on Enter
IFS='|' read -r VIDEO_CODEC_OPTION_NAME VIDEO_CODEC OUTPUT_EXTENSION PIXEL_FORMAT_OVERRIDE IS_CRF_BASED <<< "${codec_options[0]}" # Default to first option
echo "Defaulting to: $VIDEO_CODEC_OPTION_NAME"
break
else
echo "Invalid selection."
fi
done
COLUMNS=$old_cols_main_codec # Restore
OUTPUT_EXTENSION=".${OUTPUT_EXTENSION}"
PIXEL_FORMAT_FINAL="${PIXEL_FORMAT_OVERRIDE:-$PIXEL_FORMAT_DEFAULT}"
echo "Selected: $VIDEO_CODEC_OPTION_NAME (Codec: $VIDEO_CODEC, Extension: $OUTPUT_EXTENSION, Pixel Format: $PIXEL_FORMAT_FINAL)"
# Reset codec specific params
CRF_VALUE=""; CODEC_PRESET=""; PRORES_PROFILE_VAL=""; DNX_BITRATE_OR_PROFILE=""; VP9_CPU_USED=""
# --- This is the SINGLE place to call get_crf_value_for_codec if needed ---
if [[ "$IS_CRF_BASED" == "true" ]]; then
get_crf_value_for_codec # This function uses the global VIDEO_CODEC
fi
if [[ "$VIDEO_CODEC" == "libx264" || "$VIDEO_CODEC" == "libx265" ]]; then
local old_cols_preset=$COLUMNS
COLUMNS=1
PS3="Select preset for $VIDEO_CODEC (default: $DEFAULT_PRESET): "
select preset_choice in "${presets[@]}"; do
if [[ " ${presets[*]} " =~ " ${preset_choice} " ]]; then CODEC_PRESET="$preset_choice"; break
elif [[ -z "$preset_choice" && -n "$REPLY" && "$REPLY" -ge 1 && "$REPLY" -le ${#presets[@]} ]]; then CODEC_PRESET="${presets[$((REPLY-1))]}"; break
elif [[ -z "$preset_choice" && -z "$REPLY" ]]; then CODEC_PRESET="$DEFAULT_PRESET"; echo "Default preset: $CODEC_PRESET"; break
else echo "Invalid preset selection."; fi
done
COLUMNS=$old_cols_preset #Restore
echo "Using preset: $CODEC_PRESET"
elif [[ "$VIDEO_CODEC" == "prores_ks" ]]; then
# Profile mapping for prores_ks: 0=proxy, 1=lt, 2=std, 3=hq, 4=4444, 5=4444xq
# For simplicity, offer user-friendly names
local prores_profiles_map=("Proxy:0" "LT:1" "Standard:2" "HQ:3" "4444:4")
echo "Choose ProRes Profile:"
PS3="Select ProRes Profile (default: HQ): "
select profile_opt in "${prores_profiles_map[@]/#/: }" "HQ (default)"; do # Add default to the list for easy selection
if [[ "$profile_opt" == "HQ (default)" || ( -z "$profile_opt" && -z "$REPLY" ) ]]; then
PRORES_PROFILE_VAL="3" # Default to HQ (profile 3)
VIDEO_CODEC_OPTION_NAME+="/HQ" # Append to name for clarity
break
elif [[ -n "$profile_opt" ]]; then
PRORES_PROFILE_VAL="${profile_opt##*:}" # Get the number after colon
VIDEO_CODEC_OPTION_NAME+="/${profile_opt%%:*}" # Append to name
break
elif [[ -z "$profile_opt" && -n "$REPLY" && "$REPLY" -ge 1 && "$REPLY" -le ${#prores_profiles_map[@]} ]]; then
profile_opt="${prores_profiles_map[$((REPLY-1))]}"
PRORES_PROFILE_VAL="${profile_opt##*:}"
VIDEO_CODEC_OPTION_NAME+="/${profile_opt%%:*}"
break
else
echo "Invalid ProRes profile selection."
fi
done
echo "Using ProRes Profile value: $PRORES_PROFILE_VAL (${VIDEO_CODEC_OPTION_NAME##*/})"
# Specific overrides for ProRes if chosen profile needs it
if [[ "$PRORES_PROFILE_VAL" == "4" ]]; then # 4444
PIXEL_FORMAT_FINAL="yuv444p10le" # ProRes 4444 is 10-bit 4:4:4
echo "Note: ProRes 4444 selected, pixel format set to $PIXEL_FORMAT_FINAL"
fi
elif [[ "$VIDEO_CODEC" == "dnxhd" ]]; then # Covers DNxHR too for FFmpeg's dnxhd encoder
echo "Choose DNxHR Profile (for >HD resolutions) or DNxHD Bitrate (for HD):"
echo " 1. DNxHR HQX (12-bit 4:2:2, Highest Quality)"
echo " 2. DNxHR HQ (8-bit 4:2:2, High Quality)"
echo " 3. DNxHR SQ (8-bit 4:2:2, Standard Quality)"
echo " 4. DNxHR LB (8-bit 4:2:2, Low Bandwidth / Proxy)"
echo " 5. Enter DNxHD bitrate for HD (e.g., 175M, 220M for 1080p)"
local dnx_choice
while true; do
read -e -p "DNxHR/HD option (1-5) [2 for DNxHR HQ]: " -i "2" dnx_choice
dnx_choice="${dnx_choice:-2}"
case "$dnx_choice" in
1) DNX_BITRATE_OR_PROFILE="dnxhr_hqx"; PIXEL_FORMAT_FINAL="yuv422p10le"; VIDEO_CODEC_OPTION_NAME+="/HQX"; break ;;
2) DNX_BITRATE_OR_PROFILE="dnxhr_hq"; PIXEL_FORMAT_FINAL="yuv422p"; VIDEO_CODEC_OPTION_NAME+="/HQ"; break ;;
3) DNX_BITRATE_OR_PROFILE="dnxhr_sq"; PIXEL_FORMAT_FINAL="yuv422p"; VIDEO_CODEC_OPTION_NAME+="/SQ"; break ;;
4) DNX_BITRATE_OR_PROFILE="dnxhr_lb"; PIXEL_FORMAT_FINAL="yuv422p"; VIDEO_CODEC_OPTION_NAME+="/LB"; break ;;
5) # ... (bitrate logic, might also set PIXEL_FORMAT_FINAL to yuv422p for common DNxHD bitrates) ...
read -e -p "Enter DNxHD bitrate (e.g., 175M, 220M): " DNX_BITRATE_OR_PROFILE
if [[ "$DNX_BITRATE_OR_PROFILE" =~ ^[0-9]+[MmKk]$ ]]; then
VIDEO_CODEC_OPTION_NAME+="/${DNX_BITRATE_OR_PROFILE}"
PIXEL_FORMAT_FINAL="yuv422p" # Common for DNxHD profiles defined by bitrate
echo "Note: Ensure resolution and framerate are compatible with DNxHD at $DNX_BITRATE_OR_PROFILE."
break
else # ... (error handling) ...
echo "Invalid bitrate format. Use number followed by M or K (e.g., 175M)."
DNX_BITRATE_OR_PROFILE=""
fi ;;
*) echo "Invalid DNxHR/HD choice." ;;
esac
done
echo "Using DNx option: $DNX_BITRATE_OR_PROFILE, Pixel Format: $PIXEL_FORMAT_FINAL"
# ... (other codec cases like libx264, libx265, prores_ks, dnxhd) ...
# ... (other codec cases) ...
elif [[ "$VIDEO_CODEC" == "libvpx-vp9" ]]; then
echo "--- VP9 Specific Settings ---"
local vp9_deadlines_map=(
"realtime:Fastest encoding, lowest quality. Good for live streaming.|realtime"
"good:Good balance of speed and quality. Recommended default.|good"
"best:Slowest encoding, highest quality for a given CRF.|best"
)
local vp9_deadline_options=()
local vp9_deadline_values=()
for item in "${vp9_deadlines_map[@]}"; do
vp9_deadline_options+=("${item%|*}") # Text before |
vp9_deadline_values+=("${item#*|}") # Value after |
done
echo "The '-deadline' option controls the trade-off between encoding speed and quality."
# VVVVVV Force list format for select VVVVVV
local old_columns=$COLUMNS # Optional: save old value
COLUMNS=1 # Force single column layout
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PS3="Select VP9 Deadline (default: good): "
select deadline_desc_selected in "${vp9_deadline_options[@]}"; do
if [[ -n "$deadline_desc_selected" ]]; then
for i in "${!vp9_deadline_options[@]}"; do
if [[ "${vp9_deadline_options[$i]}" == "$deadline_desc_selected" ]]; then
CODEC_PRESET="${vp9_deadline_values[$i]}"
break 2
fi
done
elif [[ -z "$deadline_desc_selected" && -n "$REPLY" && "$REPLY" -ge 1 && "$REPLY" -le ${#vp9_deadline_options[@]} ]]; then
CODEC_PRESET="${vp9_deadline_values[$((REPLY-1))]}"
break
elif [[ -z "$deadline_desc_selected" && -z "$REPLY" ]]; then
CODEC_PRESET="good"
echo "Defaulting to VP9 Deadline: good"
break
else
echo "Invalid VP9 deadline selection. Please try again."
fi
done
# VVVVVV Optional: Restore old COLUMNS value VVVVVV
COLUMNS=$old_columns
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
echo "VP9 Deadline selected: $CODEC_PRESET"
# (get_crf_value_for_codec was ALREADY CALLED if IS_CRF_BASED was true)
echo
echo "--- VP9 Settings Summary ---"
echo "Deadline (-deadline): $CODEC_PRESET"
echo "CPU Used (-cpu-used): $VP9_CPU_USED"
if [[ -n "$CRF_VALUE" ]]; then
echo "CRF (-crf): $CRF_VALUE"
fi
echo "----------------------------"
fi # End of elif for libvpx-vp9
} # End of get_video_codec_settings function
process_sequences_in_directory() {
local current_processing_dir="$1"
local found_any_sequence_in_this_dir=false
echo "Scanning for all image sequences in '$current_processing_dir'..."
# 1. Get all potential image files matching the global prefix/suffix, extract numbers, and sort.
# Store original filename, numeric part, and digit count.
local all_potential_files_info=()
while IFS= read -r file_path; do
local filename=$(basename "$file_path")
if [[ "$filename" == "${FILENAME_PREFIX}"*"${FILENAME_SUFFIX}" ]]; then
local potential_num_part=${filename#"$FILENAME_PREFIX"}
potential_num_part=${potential_num_part%"$FILENAME_SUFFIX"}
if [[ "$potential_num_part" =~ ^[0-9]+$ ]]; then
# Store: full_path|numeric_string|numeric_value
all_potential_files_info+=("${file_path}|${potential_num_part}|${potential_num_part#0}") # Remove leading zeros for numeric sort if any
fi
fi
done < <(find "$current_processing_dir" -maxdepth 1 -type f -name "${FILENAME_PREFIX}*[0-9]*${FILENAME_SUFFIX}" 2>/dev/null)
# Sort by the numeric value (third field). If numeric value is same, sort by full path as tie-breaker.
# Using process substitution and sort command
IFS=$'\n' sorted_files_info=($(printf "%s\n" "${all_potential_files_info[@]}" | sort -t'|' -k3,3n -k1,1V))
unset IFS
if [ ${#sorted_files_info[@]} -eq 0 ]; then
echo "No potential image files (e.g., ${FILENAME_PREFIX}*${FILENAME_SUFFIX}) found in '$current_processing_dir'."
return # Nothing to do in this directory
fi
local processed_in_this_dir_run=() # Keep track of full paths of files processed in this call for this dir
for ((i=0; i<${#sorted_files_info[@]}; ++i)); do
IFS='|' read -r current_file_path current_num_str current_num_val <<< "${sorted_files_info[$i]}"
# Check if this file has already been part of a processed sequence in this directory run
local already_processed=false
for processed_file in "${processed_in_this_dir_run[@]}"; do
if [[ "$processed_file" == "$current_file_path" ]]; then
already_processed=true
break
fi
done
if $already_processed; then
continue # Skip to next potential starting file
fi
# This is a potential start of a new sequence
echo "Found potential new sequence starting with: $current_file_path"
found_any_sequence_in_this_dir=true
START_NUMBER=$((10#$current_num_str)) # Actual number with leading zeros for printf
local num_digits=${#current_num_str}
IMAGE_PATTERN="${FILENAME_PREFIX}%0${num_digits}d${FILENAME_SUFFIX}"
# Now, count contiguous frames from this START_NUMBER
local count_this_sequence=0
local temp_current_num=$START_NUMBER
local files_in_this_specific_sequence=() # Store paths for this specific sequence
while true; do
local file_to_check_path
file_to_check_path=$(printf "${current_processing_dir}/${IMAGE_PATTERN}" "$temp_current_num")
if [ -f "$file_to_check_path" ]; then
((count_this_sequence++))
files_in_this_specific_sequence+=("$file_to_check_path")
((temp_current_num++))
else
break # Sequence broken
fi
done
if [ "$count_this_sequence" -gt 0 ]; then
NUM_FRAMES_TO_PROCESS=$count_this_sequence
echo " Detected sequence: $START_NUMBER to $((temp_current_num - 1)) ($NUM_FRAMES_TO_PROCESS frames)."
echo " Pattern for this sequence: $IMAGE_PATTERN"
# Add all files from this successfully counted sequence to processed_in_this_dir_run
for f_in_seq in "${files_in_this_specific_sequence[@]}"; do
processed_in_this_dir_run+=("$f_in_seq")
done
# --- CALL YOUR FFMPEG PROCESSING LOGIC HERE ---
# This logic will use the global common settings (FPS, codec, scale, etc.)
# and the current START_NUMBER, IMAGE_PATTERN, NUM_FRAMES_TO_PROCESS,
# and current_processing_dir (for input) and MAIN_OUTPUT_DIR (for output).
# Construct output filename for THIS specific sequence
local current_image_dir_basename=$(basename "$current_processing_dir")
# Add sequence start number to filename to differentiate if multiple from same folder
local seq_tag="seq${START_NUMBER}"
local OUTPUT_VIDEO_FILENAME="${current_image_dir_basename}_${seq_tag}_${VIDEO_CODEC}"
# ... (rest of your detailed filename construction from previous script) ...
if [[ "$VIDEO_CODEC" == "prores_ks" ]]; then OUTPUT_VIDEO_FILENAME+="_p${PRORES_PROFILE_VAL}"; fi
# ... (etc. for all options) ...
OUTPUT_VIDEO_FILENAME+="${OUTPUT_EXTENSION}" # Ensure this is added at the end
local FINAL_OUTPUT_PATH="${MAIN_OUTPUT_DIR}/${OUTPUT_VIDEO_FILENAME}"
echo " Output for this sequence: $FINAL_OUTPUT_PATH"
# Reconstruct FINAL_VF_STRING (based on global scale choice and PIXEL_FORMAT_FINAL)
if [ -n "$SCALE_FILTER_STRING" ]; then
FINAL_VF_STRING="$SCALE_FILTER_STRING"
if [ -n "$PIXEL_FORMAT_FINAL" ]; then FINAL_VF_STRING+=",format=${PIXEL_FORMAT_FINAL}"; fi
else
if [ -n "$PIXEL_FORMAT_FINAL" ]; then FINAL_VF_STRING="format=${PIXEL_FORMAT_FINAL}"; else FINAL_VF_STRING=""; fi
fi
local ffmpeg_cmd_array=(ffmpeg -y -framerate "$FPS_INPUT" -start_number "$START_NUMBER" -i "${current_processing_dir}/${IMAGE_PATTERN}" -vframes "$NUM_FRAMES_TO_PROCESS")
if [ -n "$FINAL_VF_STRING" ]; then ffmpeg_cmd_array+=(-vf "$FINAL_VF_STRING"); fi
ffmpeg_cmd_array+=(-c:v "$VIDEO_CODEC")
# Add ALL codec-specific options (preset, profile, bitrate, CRF, etc.) based on globals
if [[ "$VIDEO_CODEC" == "libx264" || "$VIDEO_CODEC" == "libx265" ]]; then
if [ -n "$CODEC_PRESET" ]; then ffmpeg_cmd_array+=(-preset "$CODEC_PRESET"); fi
if [[ "$IS_CRF_BASED" == "true" && -n "$CRF_VALUE" ]]; then ffmpeg_cmd_array+=(-crf "$CRF_VALUE"); fi
elif [[ "$VIDEO_CODEC" == "prores_ks" ]]; then # ... and so on for other codecs
if [ -n "$PRORES_PROFILE_VAL" ]; then ffmpeg_cmd_array+=(-profile:v "$PRORES_PROFILE_VAL"); fi
elif [[ "$VIDEO_CODEC" == "dnxhd" ]]; then
if [[ "$DNX_BITRATE_OR_PROFILE" =~ [MmKk]$ ]]; then ffmpeg_cmd_array+=(-b:v "$DNX_BITRATE_OR_PROFILE");
elif [ -n "$DNX_BITRATE_OR_PROFILE" ]; then ffmpeg_cmd_array+=(-profile:v "$DNX_BITRATE_OR_PROFILE"); fi
elif [[ "$VIDEO_CODEC" == "libvpx-vp9" ]]; then
if [ -n "$CODEC_PRESET" ]; then ffmpeg_cmd_array+=(-deadline "$CODEC_PRESET"); fi
if [ -n "$VP9_CPU_USED" ]; then ffmpeg_cmd_array+=(-cpu-used "$VP9_CPU_USED"); fi
if [[ "$IS_CRF_BASED" == "true" && -n "$CRF_VALUE" ]]; then ffmpeg_cmd_array+=(-crf "$CRF_VALUE"); fi
if [[ "$IS_CRF_BASED" == "true" && -n "$CRF_VALUE" ]]; then ffmpeg_cmd_array+=(-b:v 0); fi
fi
ffmpeg_cmd_array+=(-r "$FPS_OUTPUT" -pix_fmt "$PIXEL_FORMAT_FINAL" "$FINAL_OUTPUT_PATH")
echo " Executing: ${ffmpeg_cmd_array[*]}"
if "${ffmpeg_cmd_array[@]}"; then
echo " Successfully created: $FINAL_OUTPUT_PATH"
else
echo " ERROR creating video for sequence starting $START_NUMBER in '$current_processing_dir'."
fi
echo # Blank line after processing a sequence
else
echo " Sequence starting with $current_file_path found 0 contiguous frames. This shouldn't happen if file exists."
fi
done # End loop through sorted_files_info
if ! $found_any_sequence_in_this_dir; then
echo "No processable image sequences found in '$current_processing_dir' with prefix '$FILENAME_PREFIX' and suffix '$FILENAME_SUFFIX'."
fi
}
# --- Main Script Logic ---
# ... (prerequisite checks) ...
for cmd in ffmpeg bc sed find sort basename dirname mkdir; do if ! command -v "$cmd" &> /dev/null; then echo "$cmd not found."; exit 1; fi; done
echo "===== Timelapse Batch Processor (Multi-Sequence per Folder Enabled) ====="
echo
# --- 1. Get Common Settings ONCE ---
echo "--- Step 1: Configure Common Timelapse Settings (will apply to all sequences) ---"
get_input_fps
get_output_fps
get_video_codec_settings # This sets VIDEO_CODEC, PIXEL_FORMAT_FINAL, CRF (if applicable), etc.
get_scale_choice # Sets RESOLUTION_DESC, SCALE_FILTER_STRING
# Set FILENAME_PREFIX and FILENAME_SUFFIX (could also be prompted if they vary)
FILENAME_PREFIX="$DEFAULT_FILENAME_PREFIX"
FILENAME_SUFFIX="$DEFAULT_FILENAME_SUFFIX"
echo "Using filename prefix: '$FILENAME_PREFIX', suffix: '$FILENAME_SUFFIX' for detecting sequences."
echo # Blank line for spacing
# --- 2. Get Output Directory ONCE ---
echo "--- Step 2: Configure Main Output Directory ---"
get_main_output_directory # Sets MAIN_OUTPUT_DIR
echo # Blank line for spacing
# --- 3. Get List of Input Image Directories ---
echo "--- Step 3: Specify Input Image Directories for Batch Processing ---"
IMAGE_DIRS_TO_PROCESS=() # Initialize global array
get_image_directories_for_batch # Populates IMAGE_DIRS_TO_PROCESS
echo # Blank line for spacing
# --- 4. Review and Confirm Batch Operation --- # Relabeled from 4 to common step
echo "--- Step 4: Review and Confirm Batch Operation ---"
echo "The following settings will be applied to all detected image sequences:"
echo " Input FPS: $FPS_INPUT"
echo " Output FPS: $FPS_OUTPUT"
echo " Codec Info: $VIDEO_CODEC_OPTION_NAME (Codec: $VIDEO_CODEC, Ext: $OUTPUT_EXTENSION)"
if [[ "$IS_CRF_BASED" == "true" && -n "$CRF_VALUE" ]]; then echo " CRF: $CRF_VALUE"; fi
if [ -n "$CODEC_PRESET" ]; then echo " Preset/Deadline: $CODEC_PRESET"; fi
if [ -n "$PRORES_PROFILE_VAL" ]; then echo " ProRes Profile: $PRORES_PROFILE_VAL"; fi
if [ -n "$DNX_BITRATE_OR_PROFILE" ]; then echo " DNx Option: $DNX_BITRATE_OR_PROFILE"; fi
if [[ "$VIDEO_CODEC" == "libvpx-vp9" && -n "$VP9_CPU_USED" ]]; then echo " VP9 CPU Used: $VP9_CPU_USED"; fi
echo " Resolution: $RESOLUTION_DESC"
echo " Target Pixel Fmt: $PIXEL_FORMAT_FINAL"
# Construct FINAL_VF_STRING here JUST for the summary display, it will be reconstructed per sequence later
# This is important because PIXEL_FORMAT_FINAL might be involved.
# The actual FINAL_VF_STRING for FFmpeg is built inside process_sequences_in_directory
SUMMARY_VF_STRING=""
if [ -n "$SCALE_FILTER_STRING" ]; then
SUMMARY_VF_STRING="$SCALE_FILTER_STRING"
if [ -n "$PIXEL_FORMAT_FINAL" ]; then SUMMARY_VF_STRING+=",format=${PIXEL_FORMAT_FINAL}"; fi
else
if [ -n "$PIXEL_FORMAT_FINAL" ]; then SUMMARY_VF_STRING="format=${PIXEL_FORMAT_FINAL}"; fi
fi
echo " FFmpeg Filters (approx): ${SUMMARY_VF_STRING:-None}"
echo
echo "Will process the following image directories:"
for dir_to_process in "${IMAGE_DIRS_TO_PROCESS[@]}"; do # Renamed loop var for clarity
echo " - $dir_to_process"
done
echo "Output videos will be saved in: $MAIN_OUTPUT_DIR"
echo
read -r -p "Proceed with batch processing? (y/N): " batch_confirm
batch_confirm_lower=$(echo "$batch_confirm" | tr '[:upper:]' '[:lower:]')
if ! [[ "$batch_confirm_lower" == "y" || "$batch_confirm_lower" == "yes" ]]; then
echo "Batch operation cancelled."
exit 0
fi
# --- 5. Loop Through Each PARENT Image Directory and Process --- # Relabeled from 5
echo # Blank line for spacing
echo "===== Starting Batch Processing ====="
total_parent_dirs=${#IMAGE_DIRS_TO_PROCESS[@]}
current_parent_dir_num=0
for BATCH_CURRENT_IMAGE_DIR in "${IMAGE_DIRS_TO_PROCESS[@]}"; do
((current_parent_dir_num++))
echo
echo "--- Processing Parent Directory $current_parent_dir_num of $total_parent_dirs: $BATCH_CURRENT_IMAGE_DIR ---"
# Call the function that finds and processes ALL sequences WITHIN this BATCH_CURRENT_IMAGE_DIR
process_sequences_in_directory "$BATCH_CURRENT_IMAGE_DIR"
done
echo
echo "===== Batch Processing Finished ====="