feat: MaskWorker, SetupWorker, SettingsDialog
This commit is contained in:
@@ -11,7 +11,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar,
|
QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar,
|
||||||
QListWidget, QListWidgetItem, QAbstractItemView, QSplitter, QToolTip,
|
QListWidget, QListWidgetItem, QAbstractItemView, QSplitter, QToolTip,
|
||||||
QComboBox,
|
QComboBox, QDialog, QPlainTextEdit,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QSettings
|
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QSettings
|
||||||
from PyQt6.QtGui import QPainter, QColor, QPen, QDragEnterEvent, QDropEvent, QCursor, QFont
|
from PyQt6.QtGui import QPainter, QColor, QPen, QDragEnterEvent, QDropEvent, QCursor, QFont
|
||||||
@@ -75,6 +75,9 @@ _RATIOS: dict[str, tuple[int, int]] = {
|
|||||||
"1:1": (1, 1),
|
"1:1": (1, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_VENV_PYTHON = str(Path.home() / ".8cut" / "venv" / "bin" / "python")
|
||||||
|
_TOOLS_DIR = str(Path(__file__).parent / "tools")
|
||||||
|
|
||||||
|
|
||||||
def _portrait_crop_filter(ratio: str, crop_center: float) -> str:
|
def _portrait_crop_filter(ratio: str, crop_center: float) -> str:
|
||||||
"""Return an ffmpeg crop= filter expression for the given portrait ratio.
|
"""Return an ffmpeg crop= filter expression for the given portrait ratio.
|
||||||
@@ -525,6 +528,133 @@ class PlaylistWidget(QListWidget):
|
|||||||
self.add_files(paths)
|
self.add_files(paths)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorker(QThread):
|
||||||
|
"""Installs the ML venv. Streams output line-by-line via `line` signal."""
|
||||||
|
line = pyqtSignal(str)
|
||||||
|
finished = pyqtSignal()
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
venv_dir = str(Path.home() / ".8cut" / "venv")
|
||||||
|
steps = [
|
||||||
|
[sys.executable, "-m", "venv", venv_dir],
|
||||||
|
[_VENV_PYTHON, "-m", "pip", "install", "--upgrade", "pip"],
|
||||||
|
[
|
||||||
|
_VENV_PYTHON, "-m", "pip", "install",
|
||||||
|
"torch", "torchvision",
|
||||||
|
"transformers",
|
||||||
|
"opencv-python",
|
||||||
|
"Pillow",
|
||||||
|
"segment-anything-2",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
for cmd in steps:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
for output_line in proc.stdout:
|
||||||
|
self.line.emit(output_line.rstrip())
|
||||||
|
proc.wait()
|
||||||
|
if proc.returncode != 0:
|
||||||
|
self.error.emit(f"Step failed: {' '.join(cmd[:3])}")
|
||||||
|
return
|
||||||
|
self.finished.emit()
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class MaskWorker(QThread):
|
||||||
|
"""Runs a mask generation script as a subprocess inside the ML venv."""
|
||||||
|
progress = pyqtSignal(str)
|
||||||
|
finished = pyqtSignal()
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, script: str, input_path: str, output_dir: str):
|
||||||
|
super().__init__()
|
||||||
|
self._script = script
|
||||||
|
self._input = input_path
|
||||||
|
self._output = output_dir
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
cmd = [_VENV_PYTHON, self._script, "--input", self._input, "--output", self._output]
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
for line in proc.stdout:
|
||||||
|
self.progress.emit(line.rstrip())
|
||||||
|
proc.wait()
|
||||||
|
if proc.returncode == 0:
|
||||||
|
self.finished.emit()
|
||||||
|
else:
|
||||||
|
self.error.emit(f"Script exited with code {proc.returncode}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.error.emit("venv not found — install ML tools via Settings")
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog(QDialog):
|
||||||
|
"""Settings dialog: shows ML venv status and Install/Reinstall button."""
|
||||||
|
|
||||||
|
venv_installed = pyqtSignal() # emitted when install completes successfully
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
self.setMinimumWidth(500)
|
||||||
|
self.setMinimumHeight(300)
|
||||||
|
|
||||||
|
self._worker: SetupWorker | None = None
|
||||||
|
|
||||||
|
status_text = "Installed" if Path(_VENV_PYTHON).exists() else "Not installed"
|
||||||
|
self._lbl_status = QLabel(f"ML Tools: {status_text}")
|
||||||
|
|
||||||
|
btn_label = "Reinstall" if Path(_VENV_PYTHON).exists() else "Install"
|
||||||
|
self._btn_install = QPushButton(btn_label)
|
||||||
|
self._btn_install.clicked.connect(self._on_install)
|
||||||
|
|
||||||
|
self._log = QPlainTextEdit()
|
||||||
|
self._log.setReadOnly(True)
|
||||||
|
self._log.setPlaceholderText("Install output will appear here…")
|
||||||
|
|
||||||
|
top = QHBoxLayout()
|
||||||
|
top.addWidget(self._lbl_status)
|
||||||
|
top.addStretch()
|
||||||
|
top.addWidget(self._btn_install)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.addLayout(top)
|
||||||
|
layout.addWidget(self._log)
|
||||||
|
|
||||||
|
def _on_install(self):
|
||||||
|
self._btn_install.setEnabled(False)
|
||||||
|
self._log.clear()
|
||||||
|
self._worker = SetupWorker()
|
||||||
|
self._worker.line.connect(self._log.appendPlainText)
|
||||||
|
self._worker.finished.connect(self._on_install_done)
|
||||||
|
self._worker.error.connect(self._on_install_error)
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
|
def _on_install_done(self):
|
||||||
|
self._lbl_status.setText("ML Tools: Installed")
|
||||||
|
self._btn_install.setText("Reinstall")
|
||||||
|
self._btn_install.setEnabled(True)
|
||||||
|
self._log.appendPlainText("✓ Installation complete.")
|
||||||
|
self.venv_installed.emit()
|
||||||
|
|
||||||
|
def _on_install_error(self, msg: str):
|
||||||
|
self._btn_install.setEnabled(True)
|
||||||
|
self._log.appendPlainText(f"ERROR: {msg}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Force X11/XCB mode so mpv can embed via wid — Wayland uses a different
|
# Force X11/XCB mode so mpv can embed via wid — Wayland uses a different
|
||||||
# surface handle that mpv's wid parameter cannot accept.
|
# surface handle that mpv's wid parameter cannot accept.
|
||||||
|
|||||||
Reference in New Issue
Block a user