feat: random portrait — one clip per batch gets a random crop ratio + position

Checkbox '1 random portrait' in settings row. When checked, one random
clip in each batch receives a randomly chosen portrait ratio (9:16, 4:5,
or 1:1) with a random crop center. The rest use the global portrait
setting. Disabled for single-clip overwrite exports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 23:38:56 +02:00
parent 13973dd53d
commit 439bc85505
+35 -18
View File
@@ -6,6 +6,7 @@ import sys
import os
import re
import json
import random
import shutil
import sqlite3
import subprocess
@@ -345,29 +346,25 @@ class ExportWorker(QThread):
all_done = pyqtSignal() # emitted after all jobs complete
def __init__(self, input_path: str,
jobs: list[tuple[float, str]],
jobs: list[tuple[float, str, str | None, float]],
short_side: int | None = None,
portrait_ratio: str | None = None,
crop_center: float = 0.5,
image_sequence: bool = False):
super().__init__()
self._input = input_path
self._jobs = jobs # [(start_time, output_path), ...]
self._jobs = jobs # [(start, output, portrait_ratio, crop_center), ...]
self._short_side = short_side
self._portrait_ratio = portrait_ratio
self._crop_center = crop_center
self._image_sequence = image_sequence
def run(self):
for start, output in self._jobs:
for start, output, portrait_ratio, crop_center in self._jobs:
try:
if self._image_sequence:
os.makedirs(output, exist_ok=True)
cmd = build_ffmpeg_command(
self._input, start, output,
short_side=self._short_side,
portrait_ratio=self._portrait_ratio,
crop_center=self._crop_center,
portrait_ratio=portrait_ratio,
crop_center=crop_center,
image_sequence=self._image_sequence,
)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
@@ -1270,6 +1267,17 @@ class MainWindow(QMainWindow):
lambda: self._timeline.set_clip_span(self._clip_span)
)
self._chk_rand_portrait = QCheckBox("1 random portrait")
self._chk_rand_portrait.setToolTip(
"One random clip per batch gets a random portrait crop (ratio + position)"
)
self._chk_rand_portrait.setChecked(
self._settings.value("rand_portrait", "false") == "true"
)
self._chk_rand_portrait.toggled.connect(
lambda v: self._settings.setValue("rand_portrait", "true" if v else "false")
)
self._txt_label = QComboBox()
self._txt_label.setEditable(True)
self._txt_label.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
@@ -1363,6 +1371,7 @@ class MainWindow(QMainWindow):
settings_row.addWidget(self._spn_clips)
settings_row.addWidget(QLabel("Spread:"))
settings_row.addWidget(self._spn_spread)
settings_row.addWidget(self._chk_rand_portrait)
right = QWidget()
right_layout = QVBoxLayout(right)
@@ -1655,20 +1664,33 @@ class MainWindow(QMainWindow):
os.makedirs(folder, exist_ok=True)
spread = self._spn_spread.value()
ratio_text = self._cmb_portrait.currentText()
base_ratio = None if ratio_text == "Off" else ratio_text
base_center = self._crop_center
if self._overwrite_path:
# Single-clip overwrite mode
jobs = [(self._cursor, self._overwrite_path)]
jobs = [(self._cursor, self._overwrite_path, base_ratio, base_center)]
self._overwrite_path = ""
else:
name = self._txt_name.text() or "clip"
n_clips = self._spn_clips.value()
jobs = []
for sub in range(self._spn_clips.value()):
for sub in range(n_clips):
start = self._cursor + sub * spread
if image_sequence:
out = build_sequence_dir(folder, name, self._export_counter, sub=sub)
else:
out = build_export_path(folder, name, self._export_counter, sub=sub)
jobs.append((start, out))
jobs.append((start, out, base_ratio, base_center))
# Random portrait: pick one clip, give it a random ratio + position
if self._chk_rand_portrait.isChecked() and n_clips > 1:
idx = random.randrange(n_clips)
rand_ratio = random.choice(list(_RATIOS.keys()))
rand_center = random.random()
s, o, _, _ = jobs[idx]
jobs[idx] = (s, o, rand_ratio, rand_center)
raw = self._txt_resize.text().strip()
try:
@@ -1683,18 +1705,13 @@ class MainWindow(QMainWindow):
# Show pending markers immediately.
pending = list(self._timeline._markers)
for start, out in jobs:
for start, out, _, _ in jobs:
pending.append((start, self._export_counter, out))
self._timeline.set_markers(pending)
ratio_text = self._cmb_portrait.currentText()
portrait_ratio = None if ratio_text == "Off" else ratio_text
self._export_worker = ExportWorker(
self._file_path, jobs,
short_side=short_side,
portrait_ratio=portrait_ratio,
crop_center=self._crop_center,
image_sequence=image_sequence,
)
self._export_worker.finished.connect(self._on_clip_done)