Klarity / src /gui.py
HAKORADev's picture
Upload folder using huggingface_hub
e0c50e8 verified
import os
import sys
import subprocess
import threading
import time
import json
from pathlib import Path
from datetime import datetime, timedelta
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QFrame, QLabel, QPushButton, QComboBox, QSlider, QCheckBox,
QFileDialog, QProgressBar, QSplitter, QScrollArea, QSizePolicy,
QMessageBox, QGroupBox, QSpacerItem, QStyle, QMenu, QAction,
QStackedWidget, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
)
from PyQt5.QtCore import (
Qt, QTimer, QThread, pyqtSignal, QSize, QPoint, QRectF,
QPropertyAnimation, QEasingCurve, QUrl, QRect
)
from PyQt5.QtGui import (
QPixmap, QImage, QPainter, QColor, QPen, QBrush, QFont,
QLinearGradient, QRadialGradient, QPalette, QTransform,
QMovie, QWheelEvent, QIcon
)
THEME = {
'background': '#0A0A0A',
'surface': '#1a1a1a',
'surface_hover': '#2a2a2a',
'surface_active': '#3a3a3a',
'text': '#E5E5E5',
'text_secondary': '#A0A0A0',
'accent': '#4CAF50',
'accent_hover': '#45a049',
'accent_pressed': '#3d8b40',
'accent_disabled': '#2d5a30',
'border': '#404040',
'border_light': '#E5E5E5',
'border_disabled': '#555555',
'error': '#f44336',
'warning': '#ff9800',
'success': '#4CAF50',
'panel_background': '#121212',
'panel_border': '#E5E5E5',
}
def get_main_button_style():
return """
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #121212, stop:0.3 #121212, stop:0.7 #1a1a1a, stop:1 #121212);
border: 2px solid #E5E5E5;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
color: white;
padding: 10px 20px;
}
QPushButton:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #121212, stop:0.3 #161616, stop:0.7 #1e1e1e, stop:1 #121212);
border: 2px solid #4CAF50;
color: #4CAF50;
}
QPushButton:pressed {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #0e0e0e, stop:0.3 #121212, stop:0.7 #161616, stop:1 #0e0e0e);
border: 2px solid #4CAF50;
}
QPushButton:disabled {
background-color: #2a2a2a;
border: 2px solid #555555;
color: #666666;
}
"""
def get_secondary_button_style():
return """
QPushButton {
background-color: #1a1a1a;
color: #E5E5E5;
border: 1px solid #404040;
border-radius: 6px;
font-size: 12px;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #2a2a2a;
border: 1px solid #E5E5E5;
}
QPushButton:pressed {
background-color: #3a3a3a;
}
QPushButton:disabled {
background-color: #1a1a1a;
border: 1px solid #404040;
color: #666666;
}
"""
def get_accent_button_style():
return """
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #2d5a30, stop:0.5 #4CAF50, stop:1 #2d5a30);
border: 2px solid #4CAF50;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
color: white;
padding: 10px 20px;
}
QPushButton:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #3d8b40, stop:0.5 #5CBF60, stop:1 #3d8b40);
border: 2px solid #5CBF60;
}
QPushButton:pressed {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #1d4a20, stop:0.5 #3CAF40, stop:1 #1d4a20);
}
QPushButton:disabled {
background-color: #2a2a2a;
border: 2px solid #555555;
color: #666666;
}
"""
def get_accent_button_disabled_style():
return """
QPushButton {
background-color: #2a2a2a;
border: 2px solid #555555;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
color: #666666;
padding: 10px 20px;
}
"""
def get_surface_button_style():
return """
QPushButton {
background-color: #2a2a2a;
color: white;
border: 1px solid #3a3a3a;
border-radius: 5px;
font-size: 12px;
padding: 6px 12px;
}
QPushButton:hover {
background-color: #3a3a3a;
border: 1px solid #E5E5E5;
}
QPushButton:pressed {
background-color: #4a4a4a;
border: 1px solid #E5E5E5;
}
QPushButton:disabled {
background-color: #2a2a2a;
border: 1px solid #404040;
color: #666666;
}
"""
def get_panel_style():
return f"""
QFrame {{
background-color: {THEME['panel_background']};
border: 2px solid {THEME['panel_border']};
border-radius: 8px;
}}
"""
def get_combo_box_style():
return f"""
QComboBox {{
background-color: {THEME['surface']};
color: {THEME['text']};
border: 2px solid {THEME['border_light']};
border-radius: 6px;
padding: 6px 12px;
min-width: 100px;
font-size: 12px;
}}
QComboBox::drop-down {{
border: none;
subcontrol-origin: padding;
subcontrol-position: right center;
width: 24px;
}}
QComboBox::down-arrow {{
image: none();
width: 0px;
height: 0px;
}}
QComboBox:hover {{
border: 2px solid #4CAF50;
}}
QComboBox:disabled {{
background-color: #2a2a2a;
border: 2px solid #555555;
color: #666666;
}}
QComboBox QAbstractItemView {{
background-color: {THEME['surface']};
color: {THEME['text']};
border: 1px solid {THEME['border_light']};
selection-background-color: {THEME['surface_hover']};
selection-color: {THEME['text']};
}}
"""
def get_progress_bar_style():
return f"""
QProgressBar {{
border: 1px solid {THEME['border']};
background-color: {THEME['surface']};
height: 20px;
border-radius: 10px;
text-align: center;
color: {THEME['text']};
font-weight: bold;
}}
QProgressBar::chunk {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #2d5a30, stop:0.5 #4CAF50, stop:1 #2d5a30);
border-radius: 9px;
}}
"""
def get_slider_style():
return f"""
QSlider::groove:horizontal {{
border: 1px solid {THEME['border']};
height: 8px;
background: {THEME['surface']};
border-radius: 4px;
}}
QSlider::handle:horizontal {{
background: {THEME['accent']};
border: 2px solid {THEME['text']};
width: 18px;
margin: -6px 0;
border-radius: 9px;
}}
QSlider::handle:horizontal:hover {{
background: #5CBF60;
}}
QSlider::sub-page:horizontal {{
background: {THEME['accent']};
border-radius: 4px;
}}
"""
def get_group_box_style():
return f"""
QGroupBox {{
font-weight: bold;
font-size: 13px;
color: {THEME['text']};
border: 1px solid {THEME['border']};
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 8px;
color: {THEME['text']};
}}
"""
def get_checkbox_style():
return f"""
QCheckBox {{
color: {THEME['text']};
font-size: 12px;
spacing: 8px;
}}
QCheckBox::indicator {{
width: 18px;
height: 18px;
border: 2px solid {THEME['border_light']};
border-radius: 4px;
background-color: {THEME['surface']};
}}
QCheckBox::indicator:checked {{
background-color: {THEME['accent']};
border: 2px solid {THEME['accent']};
}}
QCheckBox::indicator:hover {{
border: 2px solid {THEME['accent']};
}}
QCheckBox:disabled {{
color: #666666;
}}
QCheckBox::indicator:disabled {{
background-color: #2a2a2a;
border: 2px solid #555555;
}}
"""
class ProcessingThread(QThread):
progress_update = pyqtSignal(int, str)
processing_complete = pyqtSignal(str, bool)
download_status = pyqtSignal(str, str)
def __init__(self, command, input_path, output_path, mode):
super().__init__()
self.command = command
self.input_path = input_path
self.output_path = output_path
self.mode = mode
self.cancelled = False
def run(self):
try:
self.download_status.emit("Checking models...", "checking")
process = subprocess.Popen(
self.command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
cwd=os.path.dirname(os.path.abspath(__file__))
)
output_file = None
while True:
if self.cancelled:
process.terminate()
self.processing_complete.emit("Cancelled", False)
return
line = process.stdout.readline()
if not line and process.poll() is not None:
break
line = line.strip()
if not line:
continue
if line.startswith('{') and line.endswith('}'):
try:
data = json.loads(line)
percent = data.get('percent', 0)
step = data.get('step', '')
self.progress_update.emit(percent, step)
if 'output' in data:
output_file = data['output']
except json.JSONDecodeError:
pass
elif 'downloading' in line.lower():
self.download_status.emit(line, "downloading")
elif 'loaded' in line.lower() or 'downloaded' in line.lower():
self.download_status.emit(line, "done")
elif '%' in line:
try:
import re
match = re.search(r'(\d+)%', line)
if match:
percent = int(match.group(1))
self.progress_update.emit(percent, line)
except:
pass
if process.returncode == 0:
final_output = self.output_path
self.processing_complete.emit(final_output, True)
else:
self.processing_complete.emit(f"Error: Process failed with code {process.returncode}", False)
except Exception as e:
self.processing_complete.emit(f"Error: {str(e)}", False)
def cancel(self):
self.cancelled = True
class ScrollableImageViewer(QScrollArea):
def __init__(self, parent=None):
super().__init__(parent)
self.zoom_level = 1.0
self.min_zoom = 0.1
self.max_zoom = 10.0
self.pixmap = None
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet(f"background-color: {THEME['surface']};")
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setWidget(self.image_label)
self.setWidgetResizable(True)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet(f"""
QScrollArea {{
background-color: {THEME['surface']};
border: none;
}}
QScrollBar:vertical {{
background-color: {THEME['surface']};
width: 12px;
border-radius: 6px;
}}
QScrollBar::handle:vertical {{
background-color: {THEME['border']};
border-radius: 6px;
min-height: 30px;
}}
QScrollBar::handle:vertical:hover {{
background-color: {THEME['accent']};
}}
QScrollBar:add-line:vertical, QScrollBar:sub-line:vertical {{
height: 0px;
}}
QScrollBar:horizontal {{
background-color: {THEME['surface']};
height: 12px;
border-radius: 6px;
}}
QScrollBar::handle:horizontal {{
background-color: {THEME['border']};
border-radius: 6px;
min-width: 30px;
}}
QScrollBar::handle:horizontal:hover {{
background-color: {THEME['accent']};
}}
QScrollBar:add-line:horizontal, QScrollBar:sub-line:horizontal {{
width: 0px;
}}
""")
self.setMouseTracking(True)
def setPixmap(self, pixmap):
self.pixmap = pixmap
if pixmap:
self.updateImage()
else:
self.image_label.clear()
self.image_label.setText("No media loaded")
self.image_label.setStyleSheet(f"""
background-color: {THEME['surface']};
color: {THEME['text_secondary']};
font-size: 14px;
""")
def updateImage(self):
if self.pixmap:
new_width = int(self.pixmap.width() * self.zoom_level)
new_height = int(self.pixmap.height() * self.zoom_level)
scaled = self.pixmap.scaled(new_width, new_height,
Qt.KeepAspectRatio,
Qt.SmoothTransformation)
self.image_label.setPixmap(scaled)
self.image_label.resize(scaled.size())
def setZoomLevel(self, level):
self.zoom_level = max(self.min_zoom, min(self.max_zoom, level))
self.updateImage()
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(self.min_zoom, min(self.max_zoom, self.zoom_level))
self.updateImage()
parent = self.parent()
while parent:
if hasattr(parent, 'onZoomChanged'):
parent.onZoomChanged(self.zoom_level)
break
parent = parent.parent()
else:
super().wheelEvent(event)
class ImageComparisonSlider(QWidget):
zoomChanged = pyqtSignal(float)
def __init__(self, parent=None):
super().__init__(parent)
self.before_pixmap = None
self.after_pixmap = None
self.slider_pos = 0.5
self.zoom_level = 1.0
self.dragging_slider = False
self.dragging_pan = False
self.showing_result = True
self.pan_x = 0
self.pan_y = 0
self.last_mouse_pos = None
self.setMinimumSize(400, 300)
self.setMouseTracking(True)
self.setCursor(Qt.CrossCursor)
def setBeforeImage(self, pixmap):
self.before_pixmap = pixmap
self.resetView()
def setAfterImage(self, pixmap):
self.after_pixmap = pixmap
self.update()
def setZoomLevel(self, level):
self.zoom_level = max(0.1, min(10.0, level))
self.update()
def setSliderPosition(self, pos):
self.slider_pos = max(0.0, min(1.0, pos))
self.update()
def resetView(self):
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self.update()
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
cursor_pos = event.pos()
old_rect = self.getScaledRect(self.before_pixmap or self.after_pixmap)
if not old_rect:
return
rel_x = (cursor_pos.x() - old_rect.x()) / old_rect.width()
rel_y = (cursor_pos.y() - old_rect.y()) / old_rect.height()
if rel_x < 0 or rel_x > 1 or rel_y < 0 or rel_y > 1:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
self.update()
self.zoomChanged.emit(self.zoom_level)
return
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
new_rect = self.getScaledRectNoPan(self.before_pixmap or self.after_pixmap)
if new_rect:
new_cursor_x = new_rect.x() + rel_x * new_rect.width()
new_cursor_y = new_rect.y() + rel_y * new_rect.height()
self.pan_x = cursor_pos.x() - new_cursor_x
self.pan_y = cursor_pos.y() - new_cursor_y
self.update()
self.zoomChanged.emit(self.zoom_level)
else:
super().wheelEvent(event)
def getScaledRectNoPan(self, pixmap):
if not pixmap:
return None
base_size = self.rect().size()
scaled = pixmap.scaled(base_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
w = int(scaled.width() * self.zoom_level)
h = int(scaled.height() * self.zoom_level)
x = (self.width() - w) // 2
y = (self.height() - h) // 2
return QRect(x, y, w, h)
def getScaledRect(self, pixmap):
base_rect = self.getScaledRectNoPan(pixmap)
if base_rect:
return QRect(int(base_rect.x() + self.pan_x), int(base_rect.y() + self.pan_y),
base_rect.width(), base_rect.height())
return None
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor(THEME['surface']))
if not self.before_pixmap and not self.after_pixmap:
painter.setPen(QColor(THEME['text_secondary']))
painter.setFont(QFont('Arial', 14))
painter.drawText(self.rect(), Qt.AlignCenter, "No media loaded\n\nDrop an image/video or click Browse")
return
if self.before_pixmap and not self.after_pixmap:
rect = self.getScaledRect(self.before_pixmap)
if rect:
scaled = self.before_pixmap.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(rect.topLeft(), scaled)
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
label_rect = QRectF(10, 10, 70, 25)
painter.fillRect(label_rect, QColor(0, 0, 0, 180))
painter.drawText(label_rect, Qt.AlignCenter, "ORIGINAL")
return
base_rect = self.getScaledRect(self.before_pixmap)
if not base_rect:
return
if self.after_pixmap:
scaled_after = self.after_pixmap.scaled(base_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(base_rect.topLeft(), scaled_after)
if self.before_pixmap:
slider_x = int(self.width() * self.slider_pos)
painter.save()
painter.setClipRect(0, 0, slider_x, self.height())
scaled_before = self.before_pixmap.scaled(base_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(base_rect.topLeft(), scaled_before)
painter.restore()
pen = QPen(QColor(THEME['accent']), 3)
painter.setPen(pen)
painter.drawLine(slider_x, 0, slider_x, self.height())
handle_h = 50
handle_w = 24
handle_rect = QRectF(slider_x - handle_w//2, self.height()//2 - handle_h//2, handle_w, handle_h)
painter.setBrush(QBrush(QColor(THEME['accent'])))
painter.setPen(QPen(QColor(THEME['text']), 2))
painter.drawRoundedRect(handle_rect, 5, 5)
painter.setPen(QPen(QColor(THEME['text']), 2))
arrow_y = self.height() // 2
painter.drawLine(slider_x - 6, arrow_y, slider_x - 2, arrow_y - 5)
painter.drawLine(slider_x - 6, arrow_y, slider_x - 2, arrow_y + 5)
painter.drawLine(slider_x + 6, arrow_y, slider_x + 2, arrow_y - 5)
painter.drawLine(slider_x + 6, arrow_y, slider_x + 2, arrow_y + 5)
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
before_label = QRectF(10, 10, 70, 25)
painter.fillRect(before_label, QColor(0, 0, 0, 180))
painter.drawText(before_label, Qt.AlignCenter, "BEFORE")
after_label = QRectF(self.width() - 80, 10, 70, 25)
painter.fillRect(after_label, QColor(0, 0, 0, 180))
painter.drawText(after_label, Qt.AlignCenter, "AFTER")
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
slider_x = int(self.width() * self.slider_pos)
handle_rect = QRect(slider_x - 20, self.height()//2 - 30, 40, 60)
if handle_rect.contains(event.pos()):
self.dragging_slider = True
self.slider_pos = event.pos().x() / self.width()
else:
self.dragging_pan = True
self.last_mouse_pos = event.pos()
self.setCursor(Qt.ClosedHandCursor)
self.update()
def mouseMoveEvent(self, event):
if self.dragging_slider:
self.slider_pos = max(0.0, min(1.0, event.pos().x() / self.width()))
self.update()
elif self.dragging_pan and self.last_mouse_pos:
delta = event.pos() - self.last_mouse_pos
self.pan_x += delta.x()
self.pan_y += delta.y()
self.last_mouse_pos = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging_slider = False
self.dragging_pan = False
self.last_mouse_pos = None
self.setCursor(Qt.CrossCursor)
class SideBySideView(QWidget):
zoomChanged = pyqtSignal(float)
def __init__(self, parent=None):
super().__init__(parent)
self.before_pixmap = None
self.after_pixmap = None
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self.dragging_pan = False
self.last_mouse_pos = None
self.setMinimumSize(400, 300)
self.setMouseTracking(True)
def setBeforeImage(self, pixmap):
self.before_pixmap = pixmap
self.resetView()
def setAfterImage(self, pixmap):
self.after_pixmap = pixmap
self.update()
def setZoomLevel(self, level):
self.zoom_level = max(0.1, min(10.0, level))
self.update()
def resetView(self):
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self.update()
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
cursor_pos = event.pos()
half_width = self.width() // 2
on_left = cursor_pos.x() < half_width
target_rect = QRect(0, 0, half_width, self.height()) if on_left else QRect(half_width, 0, half_width, self.height())
pixmap = self.before_pixmap if on_left else self.after_pixmap
old_rect = self.getScaledRect(pixmap, target_rect)
if not old_rect:
return
rel_x = (cursor_pos.x() - old_rect.x()) / old_rect.width()
rel_y = (cursor_pos.y() - old_rect.y()) / old_rect.height()
if rel_x < 0 or rel_x > 1 or rel_y < 0 or rel_y > 1:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
self.update()
self.zoomChanged.emit(self.zoom_level)
return
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
new_rect = self.getScaledRectNoPan(pixmap, target_rect)
if new_rect:
new_cursor_x = new_rect.x() + rel_x * new_rect.width()
new_cursor_y = new_rect.y() + rel_y * new_rect.height()
self.pan_x = cursor_pos.x() - new_cursor_x
self.pan_y = cursor_pos.y() - new_cursor_y
self.update()
self.zoomChanged.emit(self.zoom_level)
else:
super().wheelEvent(event)
def getScaledRectNoPan(self, pixmap, target_rect):
if not pixmap:
return None
scaled = pixmap.scaled(target_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
w = int(scaled.width() * self.zoom_level)
h = int(scaled.height() * self.zoom_level)
x = target_rect.x() + (target_rect.width() - w) // 2
y = target_rect.y() + (target_rect.height() - h) // 2
return QRect(x, y, w, h)
def getScaledRect(self, pixmap, target_rect):
base_rect = self.getScaledRectNoPan(pixmap, target_rect)
if base_rect:
return QRect(int(base_rect.x() + self.pan_x), int(base_rect.y() + self.pan_y),
base_rect.width(), base_rect.height())
return None
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor(THEME['surface']))
half_width = self.width() // 2
left_rect = QRect(0, 0, half_width, self.height())
right_rect = QRect(half_width, 0, half_width, self.height())
painter.setPen(QPen(QColor(THEME['accent']), 3))
painter.drawLine(half_width, 0, half_width, self.height())
if not self.before_pixmap and not self.after_pixmap:
painter.setPen(QColor(THEME['text_secondary']))
painter.setFont(QFont('Arial', 14))
painter.drawText(self.rect(), Qt.AlignCenter, "No media loaded\n\nDrop an image/video or click Browse")
return
if self.before_pixmap:
rect = self.getScaledRect(self.before_pixmap, left_rect)
if rect:
painter.save()
painter.setClipRect(left_rect)
scaled = self.before_pixmap.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(rect.topLeft(), scaled)
painter.restore()
if self.after_pixmap:
rect = self.getScaledRect(self.after_pixmap, right_rect)
if rect:
painter.save()
painter.setClipRect(right_rect)
scaled = self.after_pixmap.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(rect.topLeft(), scaled)
painter.restore()
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
before_label = QRectF(10, 10, 70, 25)
painter.fillRect(before_label, QColor(0, 0, 0, 180))
painter.drawText(before_label, Qt.AlignCenter, "BEFORE")
after_label = QRectF(self.width() - 80, 10, 70, 25)
painter.fillRect(after_label, QColor(0, 0, 0, 180))
painter.drawText(after_label, Qt.AlignCenter, "AFTER")
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging_pan = True
self.last_mouse_pos = event.pos()
self.setCursor(Qt.ClosedHandCursor)
def mouseMoveEvent(self, event):
if self.dragging_pan and self.last_mouse_pos:
delta = event.pos() - self.last_mouse_pos
self.pan_x += delta.x()
self.pan_y += delta.y()
self.last_mouse_pos = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging_pan = False
self.last_mouse_pos = None
self.setCursor(Qt.ArrowCursor)
class SingleImageView(QWidget):
zoomChanged = pyqtSignal(float)
def __init__(self, parent=None):
super().__init__(parent)
self.before_pixmap = None
self.after_pixmap = None
self.pixmap = None
self.zoom_level = 1.0
self.showing_result = True
self.pan_x = 0
self.pan_y = 0
self.dragging_pan = False
self.last_mouse_pos = None
self.setMinimumSize(400, 300)
self.setMouseTracking(True)
def setBeforeImage(self, pixmap):
self.before_pixmap = pixmap
self._updateCurrentPixmap()
def setAfterImage(self, pixmap):
self.after_pixmap = pixmap
self._updateCurrentPixmap()
def _updateCurrentPixmap(self):
if self.showing_result:
if self.after_pixmap:
self.pixmap = self.after_pixmap
elif self.before_pixmap:
self.pixmap = self.before_pixmap
else:
self.pixmap = None
else:
if self.before_pixmap:
self.pixmap = self.before_pixmap
elif self.after_pixmap:
self.pixmap = self.after_pixmap
else:
self.pixmap = None
self.resetView()
def setZoomLevel(self, level):
self.zoom_level = max(0.1, min(10.0, level))
self.update()
def setShowingResult(self, show_result):
self.showing_result = show_result
self._updateCurrentPixmap()
def resetView(self):
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self.update()
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
cursor_pos = event.pos()
old_rect = self.getScaledRect(self.pixmap)
if not old_rect:
return
rel_x = (cursor_pos.x() - old_rect.x()) / old_rect.width()
rel_y = (cursor_pos.y() - old_rect.y()) / old_rect.height()
if rel_x < 0 or rel_x > 1 or rel_y < 0 or rel_y > 1:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
self.update()
self.zoomChanged.emit(self.zoom_level)
return
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
new_rect = self.getScaledRectNoPan(self.pixmap)
if new_rect:
new_cursor_x = new_rect.x() + rel_x * new_rect.width()
new_cursor_y = new_rect.y() + rel_y * new_rect.height()
self.pan_x = cursor_pos.x() - new_cursor_x
self.pan_y = cursor_pos.y() - new_cursor_y
self.update()
self.zoomChanged.emit(self.zoom_level)
else:
super().wheelEvent(event)
def getScaledRectNoPan(self, pixmap):
if not pixmap:
return None
base_size = self.rect().size()
scaled = pixmap.scaled(base_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
w = int(scaled.width() * self.zoom_level)
h = int(scaled.height() * self.zoom_level)
x = (self.width() - w) // 2
y = (self.height() - h) // 2
return QRect(x, y, w, h)
def getScaledRect(self, pixmap):
base_rect = self.getScaledRectNoPan(pixmap)
if base_rect:
return QRect(int(base_rect.x() + self.pan_x), int(base_rect.y() + self.pan_y),
base_rect.width(), base_rect.height())
return None
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor(THEME['surface']))
if self.pixmap:
rect = self.getScaledRect(self.pixmap)
if rect:
scaled = self.pixmap.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter.drawPixmap(rect.topLeft(), scaled)
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
label = "RESULT" if self.showing_result else "ORIGINAL"
label_rect = QRectF(10, 10, 80, 25)
painter.fillRect(label_rect, QColor(0, 0, 0, 180))
painter.drawText(label_rect, Qt.AlignCenter, label)
else:
painter.setPen(QColor(THEME['text_secondary']))
painter.setFont(QFont('Arial', 14))
painter.drawText(self.rect(), Qt.AlignCenter, "No media loaded")
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging_pan = True
self.last_mouse_pos = event.pos()
self.setCursor(Qt.ClosedHandCursor)
def mouseMoveEvent(self, event):
if self.dragging_pan and self.last_mouse_pos:
delta = event.pos() - self.last_mouse_pos
self.pan_x += delta.x()
self.pan_y += delta.y()
self.last_mouse_pos = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging_pan = False
self.last_mouse_pos = None
self.setCursor(Qt.ArrowCursor)
class VideoComparisonWidget(QWidget):
zoomChanged = pyqtSignal(float)
def __init__(self, parent=None):
super().__init__(parent)
self.video_original_path = None
self.video_result_path = None
self.cap_original = None
self.cap_result = None
self.fps_original = 30
self.fps_result = 30
self.frame_count_original = 0
self.frame_count_result = 0
self.duration_original = 0
self.duration_result = 0
self.duration = 0
self.current_time = 0.0
self.playback_speed = 1.0
self.is_playing = False
self.timer = QTimer(self)
self.timer.timeout.connect(self.advanceTime)
self.view_mode = 'slider'
self.slider_pos = 0.5
self.showing_result = True
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self.dragging_slider = False
self.dragging_pan = False
self.last_mouse_pos = None
self.frame_original = None
self.frame_result = None
self.frame_original_idx = -1
self.frame_result_idx = -1
self.setMinimumSize(400, 300)
self.setMouseTracking(True)
self.setCursor(Qt.CrossCursor)
self._setup_ui()
def _setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(5)
self.display_widget = QWidget()
self.display_widget.setMinimumHeight(200)
self.display_widget.setStyleSheet(f"background-color: {THEME['surface']};")
self.display_widget.paintEvent = self._paint_display
self.display_widget.wheelEvent = self._display_wheel
self.display_widget.mousePressEvent = self._display_mouse_press
self.display_widget.mouseMoveEvent = self._display_mouse_move
self.display_widget.mouseReleaseEvent = self._display_mouse_release
self.display_widget.setMouseTracking(True)
main_layout.addWidget(self.display_widget, 1)
self.timeline = QSlider(Qt.Horizontal)
self.timeline.setStyleSheet(get_slider_style())
self.timeline.setRange(0, 1000)
self.timeline.setValue(0)
self.timeline.sliderMoved.connect(self._on_timeline_moved)
self.timeline.sliderPressed.connect(self._on_timeline_pressed)
self.timeline.sliderReleased.connect(self._on_timeline_released)
main_layout.addWidget(self.timeline)
controls = QHBoxLayout()
self.play_btn = QPushButton("▶")
self.play_btn.setFixedSize(40, 40)
self.play_btn.setStyleSheet(get_surface_button_style())
self.play_btn.clicked.connect(self.togglePlay)
controls.addWidget(self.play_btn)
self.time_label = QLabel("00:00 / 00:00")
self.time_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 12px;")
controls.addWidget(self.time_label)
controls.addStretch()
controls.addWidget(QLabel("Speed:"))
self.speed_combo = QComboBox()
self.speed_combo.addItems(["25%", "50%", "75%", "100%", "150%", "200%"])
self.speed_combo.setCurrentIndex(3)
self.speed_combo.setStyleSheet(get_combo_box_style())
self.speed_combo.currentIndexChanged.connect(self._on_speed_changed)
controls.addWidget(self.speed_combo)
main_layout.addLayout(controls)
self._display = self.display_widget
def setVideo(self, video_path):
import cv2
self.video_original_path = video_path
if self.cap_original:
self.cap_original.release()
self.cap_original = cv2.VideoCapture(video_path)
self.fps_original = self.cap_original.get(cv2.CAP_PROP_FPS) or 30
self.frame_count_original = int(self.cap_original.get(cv2.CAP_PROP_FRAME_COUNT))
self.duration_original = self.frame_count_original / self.fps_original if self.fps_original > 0 else 0
self._update_duration()
self.current_time = 0
self._update_frame_at_time()
self._update_ui()
def setResultVideo(self, video_path):
import cv2
self.video_result_path = video_path
if self.cap_result:
self.cap_result.release()
self.cap_result = cv2.VideoCapture(video_path)
self.fps_result = self.cap_result.get(cv2.CAP_PROP_FPS) or 30
self.frame_count_result = int(self.cap_result.get(cv2.CAP_PROP_FRAME_COUNT))
self.duration_result = self.frame_count_result / self.fps_result if self.fps_result > 0 else 0
self._update_duration()
self._update_frame_at_time()
self._update_ui()
def _update_duration(self):
if self.duration_original > 0 and self.duration_result > 0:
self.duration = min(self.duration_original, self.duration_result)
elif self.duration_original > 0:
self.duration = self.duration_original
elif self.duration_result > 0:
self.duration = self.duration_result
else:
self.duration = 0
def setViewMode(self, mode):
self.view_mode = mode
self._display.update()
def setShowingResult(self, show_result):
self.showing_result = show_result
self._display.update()
def setSliderPosition(self, pos):
self.slider_pos = max(0.0, min(1.0, pos))
self._display.update()
def resetView(self):
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self._display.update()
self.zoomChanged.emit(self.zoom_level)
def setZoomLevel(self, level):
self.zoom_level = max(0.1, min(10.0, level))
self._display.update()
def _get_frame(self, cap, frame_idx, cache_attr, cache_idx_attr):
import cv2
if not cap:
return None
cached_idx = getattr(self, cache_idx_attr)
if cached_idx == frame_idx:
return getattr(self, cache_attr)
cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, frame_idx))
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
setattr(self, cache_attr, frame)
setattr(self, cache_idx_attr, frame_idx)
return frame
return None
def _update_frame_at_time(self):
import cv2
if self.current_time < 0:
self.current_time = 0
frame_orig = int(self.current_time * self.fps_original)
frame_result = int(self.current_time * self.fps_result)
frame_orig = min(frame_orig, max(0, self.frame_count_original - 1))
frame_result = min(frame_result, max(0, self.frame_count_result - 1))
self._get_frame(self.cap_original, frame_orig, 'frame_original', 'frame_original_idx')
self._get_frame(self.cap_result, frame_result, 'frame_result', 'frame_result_idx')
def _update_ui(self):
if self.duration > 0:
progress = int((self.current_time / self.duration) * 1000)
self.timeline.blockSignals(True)
self.timeline.setValue(min(1000, max(0, progress)))
self.timeline.blockSignals(False)
current_min = int(self.current_time // 60)
current_sec = int(self.current_time % 60)
total_min = int(self.duration // 60)
total_sec = int(self.duration % 60)
self.time_label.setText(f"{current_min:02d}:{current_sec:02d} / {total_min:02d}:{total_sec:02d}")
self._display.update()
def advanceTime(self):
base_fps = 60.0
dt = (1.0 / base_fps) * self.playback_speed
self.current_time += dt
if self.current_time >= self.duration:
self.current_time = 0
self._update_frame_at_time()
self._update_ui()
def togglePlay(self):
if self.is_playing:
self.timer.stop()
self.play_btn.setText("▶")
else:
interval = int(1000 / 60)
self.timer.start(max(1, interval))
self.play_btn.setText("⏸")
self.is_playing = not self.is_playing
def _on_timeline_moved(self, value):
if self.duration > 0:
self.current_time = (value / 1000.0) * self.duration
self._update_frame_at_time()
self._update_ui()
def _on_timeline_pressed(self):
self._was_playing = self.is_playing
if self.is_playing:
self.timer.stop()
def _on_timeline_released(self):
if hasattr(self, '_was_playing') and self._was_playing:
interval = int(1000 / 60)
self.timer.start(max(1, interval))
def _on_speed_changed(self, index):
speeds = [0.25, 0.5, 0.75, 1.0, 1.5, 2.0]
self.playback_speed = speeds[index]
def _get_scaled_rect(self, pixmap_size, target_rect):
base_w = target_rect.width()
base_h = target_rect.height()
if pixmap_size.width() > 0 and pixmap_size.height() > 0:
scale = min(base_w / pixmap_size.width(), base_h / pixmap_size.height())
else:
scale = 1.0
w = int(pixmap_size.width() * scale * self.zoom_level)
h = int(pixmap_size.height() * scale * self.zoom_level)
x = target_rect.x() + (target_rect.width() - w) // 2 + int(self.pan_x)
y = target_rect.y() + (target_rect.height() - h) // 2 + int(self.pan_y)
return QRect(x, y, w, h)
def _paint_display(self, event):
painter = QPainter(self.display_widget)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
painter.setRenderHint(QPainter.Antialiasing)
rect = self.display_widget.rect()
painter.fillRect(rect, QColor(THEME['surface']))
has_original = self.frame_original is not None
has_result = self.frame_result is not None
if not has_original and not has_result:
painter.setPen(QColor(THEME['text_secondary']))
painter.setFont(QFont('Arial', 14))
painter.drawText(rect, Qt.AlignCenter, "No media loaded\n\nDrop a video or click Browse")
return
pixmap_orig = None
pixmap_result = None
pixmap_size = None
if has_original:
h, w, ch = self.frame_original.shape
bytes_per_line = ch * w
qimg = QImage(self.frame_original.data.tobytes(), w, h, bytes_per_line, QImage.Format_RGB888)
pixmap_orig = QPixmap.fromImage(qimg)
pixmap_size = pixmap_orig.size()
if has_result:
h, w, ch = self.frame_result.shape
bytes_per_line = ch * w
qimg = QImage(self.frame_result.data.tobytes(), w, h, bytes_per_line, QImage.Format_RGB888)
pixmap_result = QPixmap.fromImage(qimg)
if pixmap_size is None:
pixmap_size = pixmap_result.size()
if self.view_mode == 'sidebyside':
self._paint_sidebyside(painter, rect, pixmap_orig, pixmap_result, pixmap_size)
elif self.view_mode == 'slider':
self._paint_slider(painter, rect, pixmap_orig, pixmap_result, pixmap_size)
else:
self._paint_single(painter, rect, pixmap_orig, pixmap_result, pixmap_size)
def _paint_slider(self, painter, rect, pixmap_orig, pixmap_result, pixmap_size):
target_rect = rect
if not pixmap_size:
return
img_rect = self._get_scaled_rect(pixmap_size, target_rect)
if pixmap_result:
painter.drawPixmap(img_rect.topLeft(), pixmap_result.scaled(img_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
if pixmap_orig:
slider_x = int(rect.width() * self.slider_pos)
painter.save()
painter.setClipRect(0, 0, slider_x, rect.height())
painter.drawPixmap(img_rect.topLeft(), pixmap_orig.scaled(img_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
painter.restore()
pen = QPen(QColor(THEME['accent']), 3)
painter.setPen(pen)
painter.drawLine(slider_x, 0, slider_x, rect.height())
handle_h = 50
handle_w = 24
handle_rect = QRectF(slider_x - handle_w//2, rect.height()//2 - handle_h//2, handle_w, handle_h)
painter.setBrush(QBrush(QColor(THEME['accent'])))
painter.setPen(QPen(QColor(THEME['text']), 2))
painter.drawRoundedRect(handle_rect, 5, 5)
painter.setPen(QPen(QColor(THEME['text']), 2))
arrow_y = rect.height() // 2
painter.drawLine(slider_x - 6, arrow_y, slider_x - 2, arrow_y - 5)
painter.drawLine(slider_x - 6, arrow_y, slider_x - 2, arrow_y + 5)
painter.drawLine(slider_x + 6, arrow_y, slider_x + 2, arrow_y - 5)
painter.drawLine(slider_x + 6, arrow_y, slider_x + 2, arrow_y + 5)
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
before_label = QRectF(10, 10, 70, 25)
painter.fillRect(before_label, QColor(0, 0, 0, 180))
painter.drawText(before_label, Qt.AlignCenter, "BEFORE")
after_label = QRectF(rect.width() - 80, 10, 70, 25)
painter.fillRect(after_label, QColor(0, 0, 0, 180))
painter.drawText(after_label, Qt.AlignCenter, "AFTER")
def _paint_sidebyside(self, painter, rect, pixmap_orig, pixmap_result, pixmap_size):
half_width = rect.width() // 2
left_rect = QRect(0, 0, half_width, rect.height())
right_rect = QRect(half_width, 0, half_width, rect.height())
painter.setPen(QPen(QColor(THEME['accent']), 3))
painter.drawLine(half_width, 0, half_width, rect.height())
if pixmap_orig and pixmap_size:
img_rect = self._get_scaled_rect(pixmap_size, left_rect)
painter.save()
painter.setClipRect(left_rect)
painter.drawPixmap(img_rect.topLeft(), pixmap_orig.scaled(img_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
painter.restore()
if pixmap_result and pixmap_size:
img_rect = self._get_scaled_rect(pixmap_size, right_rect)
painter.save()
painter.setClipRect(right_rect)
painter.drawPixmap(img_rect.topLeft(), pixmap_result.scaled(img_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
painter.restore()
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
before_label = QRectF(10, 10, 70, 25)
painter.fillRect(before_label, QColor(0, 0, 0, 180))
painter.drawText(before_label, Qt.AlignCenter, "BEFORE")
after_label = QRectF(rect.width() - 80, 10, 70, 25)
painter.fillRect(after_label, QColor(0, 0, 0, 180))
painter.drawText(after_label, Qt.AlignCenter, "AFTER")
def _paint_single(self, painter, rect, pixmap_orig, pixmap_result, pixmap_size):
pixmap = pixmap_result if (self.showing_result and pixmap_result) else pixmap_orig
if pixmap and pixmap_size:
img_rect = self._get_scaled_rect(pixmap_size, rect)
painter.drawPixmap(img_rect.topLeft(), pixmap.scaled(img_rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
painter.setPen(QColor(THEME['text']))
painter.setFont(QFont('Arial', 11, QFont.Bold))
label = "RESULT" if (self.showing_result and pixmap_result) else "ORIGINAL"
label_rect = QRectF(10, 10, 80, 25)
painter.fillRect(label_rect, QColor(0, 0, 0, 180))
painter.drawText(label_rect, Qt.AlignCenter, label)
def _display_wheel(self, event):
if event.modifiers() & Qt.ControlModifier:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_level *= 1.1
else:
self.zoom_level /= 1.1
self.zoom_level = max(0.1, min(10.0, self.zoom_level))
self._display.update()
self.zoomChanged.emit(self.zoom_level)
else:
pass
def _display_mouse_press(self, event):
if event.button() == Qt.LeftButton:
if self.view_mode == 'slider':
rect = self._display.rect()
slider_x = int(rect.width() * self.slider_pos)
handle_rect = QRect(slider_x - 20, rect.height()//2 - 30, 40, 60)
if handle_rect.contains(event.pos()):
self.dragging_slider = True
else:
self.dragging_pan = True
self.last_mouse_pos = event.pos()
self._display.setCursor(Qt.ClosedHandCursor)
else:
self.dragging_pan = True
self.last_mouse_pos = event.pos()
self._display.setCursor(Qt.ClosedHandCursor)
def _display_mouse_move(self, event):
if self.dragging_slider:
rect = self._display.rect()
self.slider_pos = max(0.0, min(1.0, event.pos().x() / rect.width()))
self._display.update()
elif self.dragging_pan and self.last_mouse_pos:
delta = event.pos() - self.last_mouse_pos
self.pan_x += delta.x()
self.pan_y += delta.y()
self.last_mouse_pos = event.pos()
self._display.update()
def _display_mouse_release(self, event):
if event.button() == Qt.LeftButton:
self.dragging_slider = False
self.dragging_pan = False
self.last_mouse_pos = None
self._display.setCursor(Qt.CrossCursor if self.view_mode == 'slider' else Qt.ArrowCursor)
def clear(self):
if self.cap_original:
self.cap_original.release()
self.cap_original = None
if self.cap_result:
self.cap_result.release()
self.cap_result = None
self.video_original_path = None
self.video_result_path = None
self.frame_original = None
self.frame_result = None
self.frame_original_idx = -1
self.frame_result_idx = -1
self.fps_original = 30
self.fps_result = 30
self.frame_count_original = 0
self.frame_count_result = 0
self.duration_original = 0
self.duration_result = 0
self.duration = 0
self.current_time = 0
self.timeline.setValue(0)
self.time_label.setText("00:00 / 00:00")
self.zoom_level = 1.0
self.pan_x = 0
self.pan_y = 0
self._display.update()
class KlarityGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Klarity - Image/Video Restoration")
self.setMinimumSize(800, 600)
self.resize(1000, 700)
logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logo.png')
if os.path.exists(logo_path):
window_icon = QIcon(logo_path)
for size in [16, 22, 32, 48, 64, 128, 256]:
window_icon.addPixmap(QPixmap(logo_path).scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.setWindowIcon(window_icon)
self.setStyleSheet(f"background-color: {THEME['background']}; color: {THEME['text']};")
self.input_path = None
self.output_path = None
self.result_path = None
self.processing_thread = None
self.start_time = None
self.timer = QTimer(self)
self.timer.timeout.connect(self.updateTimer)
self.is_video = False
self.side_by_side_mode = False
self.current_zoom = 1.0
self.setupUI()
self.checkModels()
self.updateProcessButton()
def setupUI(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
left_panel = QFrame()
left_panel.setFixedWidth(280)
left_panel.setStyleSheet(get_panel_style())
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(10, 10, 10, 10)
left_layout.setSpacing(10)
info_layout = QHBoxLayout()
self.info_btn = QPushButton("?")
self.info_btn.setFixedSize(30, 30)
self.info_btn.setStyleSheet(get_surface_button_style())
self.info_btn.clicked.connect(self.showInfo)
info_layout.addWidget(self.info_btn)
info_layout.addStretch()
title = QLabel("KLARITY")
title.setStyleSheet(f"""
font-size: 20px;
font-weight: bold;
color: {THEME['text']};
letter-spacing: 3px;
""")
info_layout.addWidget(title)
info_layout.addStretch()
left_layout.addLayout(info_layout)
mode_group = QGroupBox("Model Mode")
mode_group.setStyleSheet(get_group_box_style())
mode_layout = QVBoxLayout(mode_group)
self.mode_combo = QComboBox()
self.mode_combo.addItems(["Heavy (Best Quality)", "Lite (Faster)"])
self.mode_combo.setStyleSheet(get_combo_box_style())
self.mode_combo.currentIndexChanged.connect(self.onModeChanged)
mode_layout.addWidget(self.mode_combo)
self.model_status = QLabel("Checking models...")
self.model_status.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
mode_layout.addWidget(self.model_status)
self.download_btn = QPushButton("Download Models")
self.download_btn.setStyleSheet(get_secondary_button_style())
self.download_btn.clicked.connect(self.downloadModels)
mode_layout.addWidget(self.download_btn)
left_layout.addWidget(mode_group)
input_group = QGroupBox("Input")
input_group.setStyleSheet(get_group_box_style())
input_layout = QVBoxLayout(input_group)
self.input_label = QLabel("No file selected")
self.input_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
self.input_label.setWordWrap(True)
input_layout.addWidget(self.input_label)
browse_btn = QPushButton("Browse File")
browse_btn.setStyleSheet(get_secondary_button_style())
browse_btn.clicked.connect(self.browseInput)
input_layout.addWidget(browse_btn)
left_layout.addWidget(input_group)
proc_group = QGroupBox("Processing Mode")
proc_group.setStyleSheet(get_group_box_style())
proc_layout = QVBoxLayout(proc_group)
self.proc_mode_combo = QComboBox()
self.proc_mode_combo.addItems([
"Denoise", "Deblur", "Upscale",
"Clean (Denoise+Deblur)", "Full (All)",
"Frame Generation", "Clean + Frame Gen", "Full + Frame Gen"
])
self.proc_mode_combo.setStyleSheet(get_combo_box_style())
self.proc_mode_combo.currentIndexChanged.connect(self.onProcModeChanged)
proc_layout.addWidget(self.proc_mode_combo)
upscale_layout = QHBoxLayout()
upscale_layout.addWidget(QLabel("Upscale:"))
self.upscale_combo = QComboBox()
self.upscale_combo.addItems(["2x", "4x"])
self.upscale_combo.setStyleSheet(get_combo_box_style())
upscale_layout.addWidget(self.upscale_combo)
proc_layout.addLayout(upscale_layout)
frame_layout = QHBoxLayout()
frame_layout.addWidget(QLabel("Frame Mult:"))
self.frame_combo = QComboBox()
self.frame_combo.addItems(["2x", "4x"])
self.frame_combo.setStyleSheet(get_combo_box_style())
frame_layout.addWidget(self.frame_combo)
proc_layout.addLayout(frame_layout)
device_layout = QHBoxLayout()
device_layout.addWidget(QLabel("Device:"))
self.device_combo = QComboBox()
self.device_combo.addItems(["Auto", "CPU", "GPU"])
self.device_combo.setStyleSheet(get_combo_box_style())
device_layout.addWidget(self.device_combo)
proc_layout.addLayout(device_layout)
left_layout.addWidget(proc_group)
output_group = QGroupBox("Output")
output_group.setStyleSheet(get_group_box_style())
output_layout = QVBoxLayout(output_group)
output_btn = QPushButton("Choose Output Folder")
output_btn.setStyleSheet(get_secondary_button_style())
output_btn.clicked.connect(self.browseOutput)
output_layout.addWidget(output_btn)
self.output_label = QLabel("Same as input")
self.output_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
self.output_label.setWordWrap(True)
output_layout.addWidget(self.output_label)
left_layout.addWidget(output_group)
left_layout.addStretch()
progress_group = QGroupBox("Progress")
progress_group.setStyleSheet(get_group_box_style())
progress_layout = QVBoxLayout(progress_group)
self.progress_bar = QProgressBar()
self.progress_bar.setStyleSheet(get_progress_bar_style())
self.progress_bar.setTextVisible(True)
self.progress_bar.setValue(0)
progress_layout.addWidget(self.progress_bar)
self.status_label = QLabel("Ready")
self.status_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
progress_layout.addWidget(self.status_label)
self.timer_label = QLabel("")
self.timer_label.setStyleSheet(f"color: {THEME['text']}; font-size: 14px; font-weight: bold;")
progress_layout.addWidget(self.timer_label)
left_layout.addWidget(progress_group)
btn_layout = QHBoxLayout()
self.clear_btn = QPushButton("Clear")
self.clear_btn.setStyleSheet(get_secondary_button_style())
self.clear_btn.clicked.connect(self.clearAll)
btn_layout.addWidget(self.clear_btn)
self.process_btn = QPushButton("PROCESS")
self.process_btn.setStyleSheet(get_accent_button_disabled_style())
self.process_btn.clicked.connect(self.startProcessing)
self.process_btn.setEnabled(False)
btn_layout.addWidget(self.process_btn)
left_layout.addLayout(btn_layout)
main_layout.addWidget(left_panel)
right_panel = QFrame()
right_panel.setStyleSheet(get_panel_style())
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(10, 10, 10, 10)
right_layout.setSpacing(10)
view_controls = QHBoxLayout()
self.compare_check = QCheckBox("Before/After")
self.compare_check.setStyleSheet(get_checkbox_style())
self.compare_check.setChecked(True)
self.compare_check.stateChanged.connect(self.toggleComparison)
view_controls.addWidget(self.compare_check)
self.sidebyside_check = QCheckBox("Side-by-Side")
self.sidebyside_check.setStyleSheet(get_checkbox_style())
self.sidebyside_check.stateChanged.connect(self.toggleSideBySide)
view_controls.addWidget(self.sidebyside_check)
self.view_switcher = QPushButton("Show Original")
self.view_switcher.setStyleSheet(get_surface_button_style())
self.view_switcher.clicked.connect(self.toggleView)
self.view_switcher.setVisible(False)
view_controls.addWidget(self.view_switcher)
view_controls.addStretch()
view_controls.addWidget(QLabel("Zoom:"))
self.zoom_slider = QSlider(Qt.Horizontal)
self.zoom_slider.setFixedWidth(120)
self.zoom_slider.setRange(10, 1000)
self.zoom_slider.setValue(100)
self.zoom_slider.setStyleSheet(get_slider_style())
self.zoom_slider.valueChanged.connect(self.onZoomSliderChanged)
view_controls.addWidget(self.zoom_slider)
self.zoom_label = QLabel("100%")
self.zoom_label.setStyleSheet(f"color: {THEME['text_secondary']}; min-width: 50px;")
view_controls.addWidget(self.zoom_label)
self.reset_view_btn = QPushButton("Reset View")
self.reset_view_btn.setStyleSheet(get_surface_button_style())
self.reset_view_btn.clicked.connect(self.resetAllViews)
view_controls.addWidget(self.reset_view_btn)
right_layout.addLayout(view_controls)
self.view_stack = QStackedWidget()
self.slider_view = ImageComparisonSlider(self)
self.slider_view.zoomChanged.connect(self.onZoomChanged)
self.view_stack.addWidget(self.slider_view)
self.sidebyside_view = SideBySideView(self)
self.sidebyside_view.zoomChanged.connect(self.onZoomChanged)
self.view_stack.addWidget(self.sidebyside_view)
self.single_view = SingleImageView(self)
self.single_view.zoomChanged.connect(self.onZoomChanged)
self.view_stack.addWidget(self.single_view)
self.video_view = VideoComparisonWidget(self)
self.video_view.zoomChanged.connect(self.onZoomChanged)
self.view_stack.addWidget(self.video_view)
right_layout.addWidget(self.view_stack, 1)
self.overlay_widget = QWidget(right_panel)
self.overlay_widget.setStyleSheet(f"""
background-color: rgba(10, 10, 10, 220);
border-radius: 8px;
""")
overlay_layout = QVBoxLayout(self.overlay_widget)
overlay_layout.setAlignment(Qt.AlignCenter)
self.overlay_label = QLabel("Processing...")
self.overlay_label.setStyleSheet(f"""
color: {THEME['text']};
font-size: 24px;
font-weight: bold;
""")
self.overlay_label.setAlignment(Qt.AlignCenter)
overlay_layout.addWidget(self.overlay_label)
self.overlay_timer = QLabel("00:00")
self.overlay_timer.setStyleSheet(f"""
color: {THEME['text_secondary']};
font-size: 18px;
""")
self.overlay_timer.setAlignment(Qt.AlignCenter)
overlay_layout.addWidget(self.overlay_timer)
self.overlay_widget.hide()
main_layout.addWidget(right_panel, 1)
self.right_panel = right_panel
def resizeEvent(self, event):
super().resizeEvent(event)
if hasattr(self, 'overlay_widget') and hasattr(self, 'right_panel'):
self.overlay_widget.setGeometry(self.right_panel.rect())
def updateProcessButton(self):
if self.input_path and os.path.exists(self.input_path):
self.process_btn.setEnabled(True)
self.process_btn.setStyleSheet(get_accent_button_style())
else:
self.process_btn.setEnabled(False)
self.process_btn.setStyleSheet(get_accent_button_disabled_style())
def showInfo(self):
import torch
import shutil
info_text = f"""<h2>Klarity - Image/Video Restoration Tool</h2>
<p><b>Version:</b> 1.0</p>
<hr>
<p><b>Python:</b> {sys.version.split()[0]}</p>
<p><b>PyTorch:</b> {torch.__version__}</p>
<p><b>CUDA:</b> {'Available - ' + torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'Not Available'}</p>
<p><b>ffmpeg:</b> {'Available' if shutil.which('ffmpeg') else 'NOT FOUND'}</p>
<hr>
<p><b>Supported Image Formats:</b><br>
.jpg, .jpeg, .png, .bmp, .tiff, .tif, .webp</p>
<p><b>Supported Video Formats:</b><br>
.mp4, .avi, .mov, .mkv, .webm, .flv, .wmv, .m4v</p>
<hr>
<p><b>Credits:</b></p>
<ul>
<li>Real-ESRGAN - Upscaling</li>
<li>NAFNet - Denoising/Deblurring</li>
<li>RIFE - Frame Interpolation</li>
</ul>
<hr>
<p><b>Tip:</b> Use Ctrl + Mouse Wheel to zoom in/out</p>
"""
msg = QMessageBox(self)
msg.setWindowTitle("About Klarity")
msg.setTextFormat(Qt.RichText)
msg.setText(info_text)
msg.setStyleSheet(f"""
QMessageBox {{
background-color: {THEME['background']};
}}
QLabel {{
color: {THEME['text']};
min-width: 400px;
}}
QPushButton {{
{get_secondary_button_style()}
min-width: 80px;
}}
""")
msg.exec_()
def checkModels(self):
script_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join(script_dir, "models")
mode = "heavy" if self.mode_combo.currentIndex() == 0 else "lite"
model_files = {
'deblur': f'deblur-{mode}.pth',
'denoise': f'denoise-{mode}.pth',
'upscale': f'upscale-{mode}.pth',
'rife': f'framegen-{mode}.pkl'
}
all_exist = True
for name, filename in model_files.items():
path = os.path.join(models_dir, filename)
if not os.path.exists(path) or os.path.getsize(path) < 1000:
all_exist = False
break
if all_exist:
self.model_status.setText(f"✓ All {mode} models ready")
self.model_status.setStyleSheet(f"color: {THEME['success']}; font-size: 11px;")
self.download_btn.setVisible(False)
else:
self.model_status.setText(f"⚠ Some {mode} models missing")
self.model_status.setStyleSheet(f"color: {THEME['warning']}; font-size: 11px;")
self.download_btn.setVisible(True)
def onModeChanged(self, index):
self.checkModels()
def onProcModeChanged(self, index):
is_frame_mode = index >= 5
self.frame_combo.setEnabled(is_frame_mode)
is_upscale_mode = index in [2, 4, 7]
self.upscale_combo.setEnabled(is_upscale_mode)
def downloadModels(self):
mode = "heavy" if self.mode_combo.currentIndex() == 0 else "lite"
self.model_status.setText("Downloading models...")
self.model_status.setStyleSheet(f"color: {THEME['warning']}; font-size: 11px;")
self.download_btn.setEnabled(False)
def download():
script_dir = os.path.dirname(os.path.abspath(__file__))
cmd = [sys.executable, os.path.join(script_dir, "klarity.py"), "download-models"]
if mode == "lite":
cmd.insert(3, "-lite")
subprocess.run(cmd, cwd=script_dir)
thread = threading.Thread(target=download)
thread.start()
def check_done():
if thread.is_alive():
QTimer.singleShot(500, check_done)
else:
self.checkModels()
self.download_btn.setEnabled(True)
check_done()
def browseInput(self):
file_filter = "Media Files (*.jpg *.jpeg *.png *.bmp *.tiff *.tif *.webp *.mp4 *.avi *.mov *.mkv *.webm *.flv *.wmv *.m4v);;All Files (*)"
path, _ = QFileDialog.getOpenFileName(self, "Select Input File", "", file_filter)
if path:
self.input_path = path
self.result_path = None
self.input_label.setText(os.path.basename(path))
self.input_label.setStyleSheet(f"color: {THEME['text']}; font-size: 11px;")
video_exts = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
self.is_video = Path(path).suffix.lower() in video_exts
if self.is_video:
self.proc_mode_combo.clear()
self.proc_mode_combo.addItems([
"Denoise", "Deblur", "Upscale",
"Clean (Denoise+Deblur)", "Full (All)",
"Frame Generation", "Clean + Frame Gen", "Full + Frame Gen"
])
else:
self.proc_mode_combo.clear()
self.proc_mode_combo.addItems([
"Denoise", "Deblur", "Upscale",
"Clean (Denoise+Deblur)", "Full (All)"
])
self.loadMedia(path)
self.updateProcessButton()
def browseOutput(self):
path = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if path:
if not path.endswith('/') and not path.endswith('\\'):
path += '/'
self.output_path = path
self.output_label.setText(path.rstrip('/\\'))
self.output_label.setStyleSheet(f"color: {THEME['text']}; font-size: 11px;")
def loadMedia(self, path):
if self.is_video:
self.view_stack.setCurrentWidget(self.video_view)
self.video_view.setVideo(path)
self.compare_check.setEnabled(True)
self.sidebyside_check.setEnabled(True)
self.updateVideoView()
else:
self.updateImageView()
def updateVideoView(self):
if not self.is_video:
return
compare_enabled = self.compare_check.isChecked()
sidebyside_enabled = self.sidebyside_check.isChecked()
if sidebyside_enabled and compare_enabled:
self.video_view.setViewMode('sidebyside')
elif compare_enabled:
self.video_view.setViewMode('slider')
else:
self.video_view.setViewMode('single')
self.view_switcher.setVisible(not compare_enabled)
def updateImageView(self):
if self.is_video:
return
compare_enabled = self.compare_check.isChecked()
sidebyside_enabled = self.sidebyside_check.isChecked()
before_pixmap = None
after_pixmap = None
if self.input_path and os.path.exists(self.input_path):
before_pixmap = QPixmap(self.input_path)
if self.result_path and os.path.exists(self.result_path):
after_pixmap = QPixmap(self.result_path)
if sidebyside_enabled and compare_enabled:
self.view_stack.setCurrentWidget(self.sidebyside_view)
self.sidebyside_view.setBeforeImage(before_pixmap)
self.sidebyside_view.setAfterImage(after_pixmap)
elif compare_enabled:
self.view_stack.setCurrentWidget(self.slider_view)
self.slider_view.setBeforeImage(before_pixmap)
self.slider_view.setAfterImage(after_pixmap)
else:
self.view_stack.setCurrentWidget(self.single_view)
self.single_view.setBeforeImage(before_pixmap)
self.single_view.setAfterImage(after_pixmap)
self.view_switcher.setVisible(not compare_enabled)
def toggleComparison(self, state):
if self.is_video:
self.updateVideoView()
else:
self.updateImageView()
def toggleSideBySide(self, state):
self.side_by_side_mode = state == Qt.Checked
if self.is_video:
self.updateVideoView()
else:
self.updateImageView()
def toggleView(self):
if self.is_video:
self.video_view.setShowingResult(not self.video_view.showing_result)
is_result = self.video_view.showing_result
else:
self.single_view.setShowingResult(not self.single_view.showing_result)
is_result = self.single_view.showing_result
self.view_switcher.setText("Show Original" if is_result else "Show Result")
def onZoomSliderChanged(self, value):
self.current_zoom = value / 100.0
self.slider_view.setZoomLevel(self.current_zoom)
self.sidebyside_view.setZoomLevel(self.current_zoom)
self.single_view.setZoomLevel(self.current_zoom)
self.video_view.setZoomLevel(self.current_zoom)
self.zoom_label.setText(f"{value}%")
def onZoomChanged(self, zoom_level):
self.current_zoom = zoom_level
zoom_percent = int(zoom_level * 100)
self.zoom_slider.blockSignals(True)
self.zoom_slider.setValue(zoom_percent)
self.zoom_slider.blockSignals(False)
self.zoom_label.setText(f"{zoom_percent}%")
def resetAllViews(self):
self.current_zoom = 1.0
self.zoom_slider.blockSignals(True)
self.zoom_slider.setValue(100)
self.zoom_slider.blockSignals(False)
self.zoom_label.setText("100%")
self.slider_view.resetView()
self.sidebyside_view.resetView()
self.single_view.resetView()
self.video_view.resetView()
def startProcessing(self):
if not self.input_path:
QMessageBox.warning(self, "No Input", "Please select an input file first.")
return
script_dir = os.path.dirname(os.path.abspath(__file__))
klarity_path = os.path.join(script_dir, "klarity.py")
mode = "heavy" if self.mode_combo.currentIndex() == 0 else "lite"
proc_modes = [
"denoise", "deblur", "upscale",
"clean", "full",
"frame-gen", "clean-frame-gen", "full-frame-gen"
]
proc_mode = proc_modes[self.proc_mode_combo.currentIndex()]
upscale = "2" if self.upscale_combo.currentIndex() == 0 else "4"
frame_mult = "2" if self.frame_combo.currentIndex() == 0 else "4"
device = self.device_combo.currentText().lower()
cmd = [sys.executable, klarity_path, f"-{mode}", proc_mode, self.input_path, "--json-progress"]
if proc_mode in ["upscale", "full", "full-frame-gen"]:
cmd.extend(["--upscale", upscale])
if proc_mode in ["frame-gen", "clean-frame-gen", "full-frame-gen"]:
cmd.extend(["--multi", frame_mult])
if device != "auto":
cmd.extend(["--device", device])
if self.output_path:
cmd.extend(["-o", self.output_path])
input_p = Path(self.input_path)
suffix_map = {
'denoise': '_denoised',
'deblur': '_deblurred',
'upscale': '_upscaled',
'clean': '_cleaned',
'full': '_enhanced',
'frame-gen': '_generated',
'clean-frame-gen': '_clean_generated',
'full-frame-gen': '_full_enhanced',
}
suffix = suffix_map.get(proc_mode, '_processed')
output_dir = self.output_path if self.output_path else str(input_p.parent)
expected_output = os.path.join(output_dir, f"{input_p.stem}{suffix}{input_p.suffix}")
self.processing_thread = ProcessingThread(cmd, self.input_path, expected_output, proc_mode)
self.processing_thread.progress_update.connect(self.onProgressUpdate)
self.processing_thread.processing_complete.connect(self.onProcessingComplete)
self.processing_thread.download_status.connect(self.onDownloadStatus)
self.process_btn.setEnabled(False)
self.clear_btn.setEnabled(False)
self.progress_bar.setValue(0)
self.start_time = time.time()
self.timer.start(1000)
self.overlay_label.setText("Processing...")
self.overlay_widget.show()
self.overlay_widget.setGeometry(self.right_panel.rect())
self.processing_thread.start()
def onProgressUpdate(self, percent, step):
self.progress_bar.setValue(percent)
self.status_label.setText(step)
def onDownloadStatus(self, msg, status):
if status == "downloading":
self.model_status.setText(f"⬇ {msg[:30]}...")
self.model_status.setStyleSheet(f"color: {THEME['warning']}; font-size: 11px;")
elif status == "done":
self.model_status.setText(f"✓ {msg[:30]}")
self.model_status.setStyleSheet(f"color: {THEME['success']}; font-size: 11px;")
def onProcessingComplete(self, result, success):
self.timer.stop()
self.overlay_widget.hide()
self.clear_btn.setEnabled(True)
self.updateProcessButton()
if success:
self.progress_bar.setValue(100)
self.status_label.setText("Complete!")
self.status_label.setStyleSheet(f"color: {THEME['success']}; font-size: 11px;")
actual_result = result
if not os.path.exists(result):
input_p = Path(self.input_path)
search_dir = self.output_path if self.output_path else str(input_p.parent)
if os.path.exists(search_dir):
for f in os.listdir(search_dir):
if input_p.stem in f and f != input_p.name:
potential = os.path.join(search_dir, f)
if os.path.getmtime(potential) > os.path.getmtime(self.input_path):
actual_result = potential
break
if os.path.exists(actual_result):
self.result_path = actual_result
if self.is_video:
self.video_view.setResultVideo(actual_result)
self.video_view.current_time = 0
self.video_view._update_frame_at_time()
self.video_view._update_ui()
self.updateVideoView()
else:
self.updateImageView()
self.view_switcher.setText("Show Original")
self.overlay_label.setText("Complete!")
self.overlay_timer.setText(f"Saved: {os.path.basename(actual_result)}")
self.overlay_widget.show()
QTimer.singleShot(2000, self.overlay_widget.hide)
else:
QMessageBox.warning(self, "Output Not Found",
f"Could not find output file.\nExpected location: {result}")
else:
self.status_label.setText(result)
self.status_label.setStyleSheet(f"color: {THEME['error']}; font-size: 11px;")
QMessageBox.critical(self, "Error", result)
def updateTimer(self):
if self.start_time:
elapsed = time.time() - self.start_time
mins = int(elapsed // 60)
secs = int(elapsed % 60)
self.timer_label.setText(f"Elapsed: {mins:02d}:{secs:02d}")
self.overlay_timer.setText(f"{mins:02d}:{secs:02d}")
def clearAll(self):
self.input_path = None
self.output_path = None
self.result_path = None
self.input_label.setText("No file selected")
self.input_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
self.output_label.setText("Same as input")
self.output_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
self.progress_bar.setValue(0)
self.status_label.setText("Ready")
self.status_label.setStyleSheet(f"color: {THEME['text_secondary']}; font-size: 11px;")
self.timer_label.setText("")
self.slider_view.setBeforeImage(None)
self.slider_view.setAfterImage(None)
self.sidebyside_view.setBeforeImage(None)
self.sidebyside_view.setAfterImage(None)
self.single_view.setBeforeImage(None)
self.single_view.setAfterImage(None)
self.video_view.clear()
self.overlay_widget.hide()
self.updateProcessButton()
self.view_switcher.setText("Show Original")
self.resetAllViews()
def closeEvent(self, event):
if self.processing_thread and self.processing_thread.isRunning():
reply = QMessageBox.question(
self, 'Processing in Progress',
'A process is running. Cancel and exit?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.processing_thread.cancel()
self.processing_thread.wait()
event.accept()
else:
event.ignore()
else:
self.video_view.clear()
event.accept()
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
app.setApplicationName('Klarity')
app.setApplicationDisplayName('Klarity')
app.setDesktopFileName('klarity')
logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logo.png')
if os.path.exists(logo_path):
app_icon = QIcon(logo_path)
app.setWindowIcon(app_icon)
for size in [16, 22, 32, 48, 64, 128, 256]:
app_icon.addPixmap(QPixmap(logo_path).scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation))
palette = QPalette()
palette.setColor(QPalette.Window, QColor(THEME['background']))
palette.setColor(QPalette.WindowText, QColor(THEME['text']))
palette.setColor(QPalette.Base, QColor(THEME['surface']))
palette.setColor(QPalette.AlternateBase, QColor(THEME['surface']))
palette.setColor(QPalette.ToolTipBase, QColor(THEME['surface']))
palette.setColor(QPalette.ToolTipText, QColor(THEME['text']))
palette.setColor(QPalette.Text, QColor(THEME['text']))
palette.setColor(QPalette.Button, QColor(THEME['surface']))
palette.setColor(QPalette.ButtonText, QColor(THEME['text']))
palette.setColor(QPalette.BrightText, QColor(THEME['error']))
palette.setColor(QPalette.Highlight, QColor(THEME['accent']))
palette.setColor(QPalette.HighlightedText, QColor(THEME['text']))
app.setPalette(palette)
window = KlarityGUI()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()