From af6f462deeaae925f6399cd0342fbc2a2185bb72 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Tue, 2 Sep 2025 03:54:02 +0300 Subject: [PATCH 1/8] fix(logger): improve style mapping for log levels --- src/clipper/ytc_logger.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/clipper/ytc_logger.py b/src/clipper/ytc_logger.py index 715a0cbf..4f0ef396 100644 --- a/src/clipper/ytc_logger.py +++ b/src/clipper/ytc_logger.py @@ -2,7 +2,7 @@ import logging from pathlib import Path from types import TracebackType -from typing import IO, Dict +from typing import IO, Dict, Mapping import coloredlogs import verboselogs @@ -63,15 +63,40 @@ def log( | BaseException = None, stack_info: bool = False, stacklevel: int = 1, - extra: Dict[str, object] | None = None, + extra: Mapping[str, object] | None = None, ) -> None: if not self.no_rich_logs: - level_name = logging.getLevelName(level) - color = self.console.get_style(f"logging.level.{level_name.lower()}") - - msg = f"[{color}]{msg}" + # Map numeric levels to known Rich theme style keys to avoid invalid style names + # that can occur with custom levels (e.g., "Level 34"). + style_by_level = { + logging.DEBUG: "logging.level.debug", + verboselogs.VERBOSE: "logging.level.verbose", + logging.INFO: "logging.level.info", + # Some code paths use 'success' level via verboselogs + getattr(verboselogs, "SUCCESS", 25): "logging.level.success", + IMPORTANT: "logging.level.important", + NOTICE: "logging.level.notice", + HEADER: "logging.level.header", + REPORT: "logging.level.report", + logging.WARNING: "logging.level.warning", + logging.ERROR: "logging.level.error", + logging.CRITICAL: "logging.level.error", + } + + style_key = style_by_level.get(level) + if style_key is None: + # Fall back to a best-effort mapping based on the level name + level_name = str(logging.getLevelName(level)).lower().replace(" ", "_") + candidate = f"logging.level.{level_name}" + style_key = candidate if candidate in THEME_COLORS_LOG_LEVELS else "logging.level.info" + + # Wrap the message with the resolved style name; Rich will pick it from the theme + msg = f"[{style_key}]{msg}" if extra is None: extra = {} + else: + # copy Mapping to mutable dict, as logging expects a mutable mapping + extra = dict(extra) extra["markup"] = True return super().log( From f65252927c053cf7e67c44362829ae60b39d5a68 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Tue, 2 Sep 2025 03:55:42 +0300 Subject: [PATCH 2/8] style(clipper): lint --- src/clipper/ytc_logger.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/clipper/ytc_logger.py b/src/clipper/ytc_logger.py index 4f0ef396..025e166e 100644 --- a/src/clipper/ytc_logger.py +++ b/src/clipper/ytc_logger.py @@ -2,7 +2,7 @@ import logging from pathlib import Path from types import TracebackType -from typing import IO, Dict, Mapping +from typing import IO, Mapping import coloredlogs import verboselogs @@ -92,11 +92,7 @@ def log( # Wrap the message with the resolved style name; Rich will pick it from the theme msg = f"[{style_key}]{msg}" - if extra is None: - extra = {} - else: - # copy Mapping to mutable dict, as logging expects a mutable mapping - extra = dict(extra) + extra = {} if extra is None else dict(extra) extra["markup"] = True return super().log( From 1e1dc77c3f1327ce46e2617d188f68c4bf3585d5 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Tue, 2 Sep 2025 17:52:26 +0300 Subject: [PATCH 3/8] fix(gui): explicit paths for frozen builds for cache manager --- src/clipper/cache_manager.py | 16 ++++++++++++++++ src/clipper/ytdl.py | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/clipper/cache_manager.py b/src/clipper/cache_manager.py index e224ebec..0bc77065 100644 --- a/src/clipper/cache_manager.py +++ b/src/clipper/cache_manager.py @@ -3,6 +3,8 @@ """ import contextlib +import os +import sys import hashlib import sqlite3 import subprocess @@ -253,6 +255,20 @@ def _build_clipper_state(self, video_id: str, url: str, cache_path_template = self.cache_dir / cache_filename clipper_paths = ClipperPaths() + # In frozen (PyInstaller) builds, point to bundled binaries by default + if getattr(sys, "frozen", False): + bin_dir = "./bin" + ext = ".exe" if sys.platform == "win32" else "" + clipper_paths.ffmpegPath = f"{bin_dir}/ffmpeg{ext}" + clipper_paths.ffprobePath = f"{bin_dir}/ffprobe{ext}" + clipper_paths.ffplayPath = f"{bin_dir}/ffplay{ext}" + clipper_paths.ytdlPath = f"{bin_dir}/yt-dlp{ext}" + # Normalize slashes for subprocess readability + clipper_paths.ffmpegPath = clipper_paths.ffmpegPath.replace("\\", "/") + clipper_paths.ffprobePath = clipper_paths.ffprobePath.replace("\\", "/") + clipper_paths.ffplayPath = clipper_paths.ffplayPath.replace("\\", "/") + clipper_paths.ytdlPath = clipper_paths.ytdlPath.replace("\\", "/") + if ytdl_location: clipper_paths.ytdlPath = ytdl_location logger.info(f"Using custom yt-dlp location: {ytdl_location}") diff --git a/src/clipper/ytdl.py b/src/clipper/ytdl.py index 962d99a7..3754fc2b 100644 --- a/src/clipper/ytdl.py +++ b/src/clipper/ytdl.py @@ -1,7 +1,9 @@ import json +import os import shlex import subprocess import sys +from pathlib import Path from typing import Dict, List, Tuple from clipper.clipper_types import ClipperPaths, ClipperState @@ -44,7 +46,18 @@ def ytdl_bin_get_args_base(cs: ClipperState) -> List[str]: ytdl_args.extend(["--output", shlex.quote(f'{settings["downloadVideoPath"]}')]) if getattr(sys, "frozen", False): - ytdl_args.extend(["--ffmpeg-location", shlex.quote(cp.ffmpegPath)]) + # Only pass --ffmpeg-location if it points to an existing executable + ffmpeg_path = cp.ffmpegPath + try: + exists = os.path.isfile(ffmpeg_path) + except Exception: + exists = False + if exists: + ytdl_args.extend(["--ffmpeg-location", shlex.quote(ffmpeg_path)]) + else: + logger.debug( + f"Skipping --ffmpeg-location: path not found -> {ffmpeg_path!r}" + ) cookies = settings["cookiefile"] From c5969be4a829a3352fc6074180a9227b7206b652 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Wed, 3 Sep 2025 00:33:34 +0300 Subject: [PATCH 4/8] refactor(gui): break color grading panel into separate components --- .../src/components/ColorControls.vue | 178 ++++ .../src/components/ColorGradingPanel.vue | 946 +----------------- .../src/components/ColorPreview.vue | 283 ++++++ .../src/composables/useFileHandler.ts | 2 +- 4 files changed, 495 insertions(+), 914 deletions(-) create mode 100644 src/gui-frontend/src/components/ColorControls.vue create mode 100644 src/gui-frontend/src/components/ColorPreview.vue diff --git a/src/gui-frontend/src/components/ColorControls.vue b/src/gui-frontend/src/components/ColorControls.vue new file mode 100644 index 00000000..c98d50b6 --- /dev/null +++ b/src/gui-frontend/src/components/ColorControls.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/src/gui-frontend/src/components/ColorGradingPanel.vue b/src/gui-frontend/src/components/ColorGradingPanel.vue index bd2e8626..2da672e7 100644 --- a/src/gui-frontend/src/components/ColorGradingPanel.vue +++ b/src/gui-frontend/src/components/ColorGradingPanel.vue @@ -1,284 +1,37 @@ diff --git a/src/gui-frontend/src/composables/useFileHandler.ts b/src/gui-frontend/src/composables/useFileHandler.ts index 29336e28..08a33dc6 100644 --- a/src/gui-frontend/src/composables/useFileHandler.ts +++ b/src/gui-frontend/src/composables/useFileHandler.ts @@ -146,7 +146,7 @@ export function useFileHandler( selectionOpId++ clipperStore.setVideoFile(null) clearVideoState() - resetMarkupState() + // Do NOT reset markup state; allow color grading with markup-only } /** From c8e5067d033c65127280b8061656b8f9d1a9794c Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Wed, 3 Sep 2025 01:49:49 +0300 Subject: [PATCH 5/8] fix(gui): proper debounce for filter functions --- src/gui-frontend/src/components/ColorControls.vue | 8 ++++++-- src/gui-frontend/src/components/ColorPreview.vue | 11 ++++------- src/gui-frontend/src/utils/debounce.ts | 7 +++++++ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/gui-frontend/src/utils/debounce.ts diff --git a/src/gui-frontend/src/components/ColorControls.vue b/src/gui-frontend/src/components/ColorControls.vue index c98d50b6..bca5baa5 100644 --- a/src/gui-frontend/src/components/ColorControls.vue +++ b/src/gui-frontend/src/components/ColorControls.vue @@ -75,6 +75,8 @@ import { DocumentCopy, Document, CopyDocument, RefreshLeft } from '@element-plus import LiftGammaGainWheels from './LiftGammaGainWheels.vue' import { buildBasicFilter } from '@/utils/colorBasics' import { parseFilterString, joinFilters } from '@/utils/filterString' +import { debounce } from '@/utils/debounce' + interface Props { isMockMarkup?: boolean @@ -107,8 +109,10 @@ const basicFilter = computed(() => buildBasicFilter({ brightness: brightness.val const advancedFilter = computed(() => advancedFilterState.value) const generatedFilter = computed(() => joinFilters(basicFilter.value, advancedFilter.value)) -const emitFilter = () => { emit('filter-changed', generatedFilter.value) } -const handleAdvancedFilterChanged = (filter: string) => { advancedFilterState.value = filter; emitFilter() } +const debouncedFilterEmit = debounce(() => emit('filter-changed', generatedFilter.value), 180) +const emitFilter = () => { debouncedFilterEmit() } +const debouncedAdvancedHandler = debounce((filter: string) => { advancedFilterState.value = filter; emitFilter() }, 120) +const handleAdvancedFilterChanged = (filter: string) => { debouncedAdvancedHandler(filter) } const reset = () => { brightness.value = 0; contrast.value = 1; saturation.value = 1; hue.value = 0; gamma.value = 1; advancedFilterState.value = '' diff --git a/src/gui-frontend/src/components/ColorPreview.vue b/src/gui-frontend/src/components/ColorPreview.vue index f607e61b..91bdafb7 100644 --- a/src/gui-frontend/src/components/ColorPreview.vue +++ b/src/gui-frontend/src/components/ColorPreview.vue @@ -55,12 +55,8 @@ import { ref, computed, watch, nextTick } from 'vue' import { ElAlert, ElButton, ElEmpty, ElIcon, ElSelect, ElOption, ElSlider } from 'element-plus' import { Loading } from '@element-plus/icons-vue' import type { ClipInfo, VideoInfo } from '@/types/api' +import { debounce } from '@/utils/debounce' -// Simple debounce -function debounce any>(fn: T, wait: number): T { - let t: ReturnType - return ((...args: any[]) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait) }) as T -} interface Props { selectedClip?: ClipInfo | null @@ -259,9 +255,10 @@ watch( { immediate: true, deep: false } ) -// React to filter changes +// React to filter changes with debounce to avoid spamming during drag +const debouncedFilterPreview = debounce(() => { if (!isInitializing.value) updatePreview() }, 150) watch(() => [props.filter, props.previewEnabled, previewResolution.value], () => { - if (!isInitializing.value) updatePreview() + debouncedFilterPreview() }) diff --git a/src/gui-frontend/src/utils/debounce.ts b/src/gui-frontend/src/utils/debounce.ts new file mode 100644 index 00000000..e888cc39 --- /dev/null +++ b/src/gui-frontend/src/utils/debounce.ts @@ -0,0 +1,7 @@ +export function debounce any>(fn: T, wait = 200) { + let t: ReturnType + return ((...args: Parameters) => { + clearTimeout(t) + t = setTimeout(() => fn(...args), wait) + }) as T +} From 6b6a164aec867ee420201eddc86bec9d1f1769e9 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Wed, 3 Sep 2025 04:13:48 +0300 Subject: [PATCH 6/8] fix(gui): clean up --- src/clipper/gui/engine.py | 6 ------ src/gui-frontend/src/components/SettingsPanel.vue | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/clipper/gui/engine.py b/src/clipper/gui/engine.py index 86777635..2b2c3740 100644 --- a/src/clipper/gui/engine.py +++ b/src/clipper/gui/engine.py @@ -49,8 +49,6 @@ def process_files(self, markup_path: Optional[str] = None, video_path: Optional[ return {"status": "error", "message": "Either markup_path or markup_data must be provided"} try: - print(f"DEBUG: Starting file processing with CLI-identical logic and GUI settings...") - # Validate inputs - either markup_path OR markup_data must be provided if markup_path: markup_file = Path(markup_path) @@ -64,13 +62,11 @@ def process_files(self, markup_path: Optional[str] = None, video_path: Optional[ # Create a fresh clipper state for this processing session self.cs = clipper_types.ClipperState() - print("DEBUG: Created fresh ClipperState for processing") # Get comprehensive GUI settings from clipper.gui.settings_manager import SettingsManager settings_manager = SettingsManager() gui_settings = settings_manager.get_combined_settings() - print(f"DEBUG: Loaded GUI settings: {len(gui_settings)} settings") # Build minimal argv for required arguments only (no settings) simulated_argv = ["yt_clipper"] @@ -171,8 +167,6 @@ def process_files(self, markup_path: Optional[str] = None, video_path: Optional[ ytc_settings.getInputVideo(self.cs) ytc_settings.getGlobalSettings(self.cs) - print("DEBUG: Starting clip processing...") - # Process clips exactly like CLI if not self.cs.settings.get("preview", False): clip_maker.makeClips(self.cs) diff --git a/src/gui-frontend/src/components/SettingsPanel.vue b/src/gui-frontend/src/components/SettingsPanel.vue index cc373d43..8c625bec 100644 --- a/src/gui-frontend/src/components/SettingsPanel.vue +++ b/src/gui-frontend/src/components/SettingsPanel.vue @@ -138,7 +138,7 @@ Date: Wed, 3 Sep 2025 04:15:36 +0300 Subject: [PATCH 7/8] fix(clipper): adjust crf values for bitrates --- src/clipper/clip_maker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/clipper/clip_maker.py b/src/clipper/clip_maker.py index 328c4809..91504abe 100644 --- a/src/clipper/clip_maker.py +++ b/src/clipper/clip_maker.py @@ -1468,37 +1468,37 @@ def getDefaultEncodeSettings(videobr: int) -> DictStrAny: } elif videobr <= 4000: encodeSettings = { - "crf": 24, + "crf": 22, "autoTargetMaxBitrate": int(1.6 * videobr), "twoPass": False, } elif videobr <= 6000: encodeSettings = { - "crf": 26, + "crf": 24, "autoTargetMaxBitrate": int(1.4 * videobr), "twoPass": False, } elif videobr <= 10000: encodeSettings = { - "crf": 28, + "crf": 26, "autoTargetMaxBitrate": int(1.2 * videobr), "twoPass": False, } elif videobr <= 14000: encodeSettings = { - "crf": 30, + "crf": 26, "autoTargetMaxBitrate": int(1.1 * videobr), "twoPass": False, } elif videobr <= 18000: encodeSettings = { - "crf": 30, + "crf": 26, "autoTargetMaxBitrate": int(1.0 * videobr), "twoPass": False, } elif videobr <= 25000: encodeSettings = { - "crf": 32, + "crf": 30, "autoTargetMaxBitrate": int(0.9 * videobr), "twoPass": False, } From 403962d923f9a086925eab6ecdf68d1b5b4d04d4 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Wed, 3 Sep 2025 13:49:37 +0300 Subject: [PATCH 8/8] style(gui): lint --- src/clipper/cache_manager.py | 3 +-- src/clipper/ytdl.py | 5 ++--- src/gui-frontend/src/utils/debounce.ts | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/clipper/cache_manager.py b/src/clipper/cache_manager.py index 0bc77065..b23b4b7e 100644 --- a/src/clipper/cache_manager.py +++ b/src/clipper/cache_manager.py @@ -3,11 +3,10 @@ """ import contextlib -import os -import sys import hashlib import sqlite3 import subprocess +import sys import threading import time from datetime import datetime, timedelta diff --git a/src/clipper/ytdl.py b/src/clipper/ytdl.py index 3754fc2b..d0c35b1b 100644 --- a/src/clipper/ytdl.py +++ b/src/clipper/ytdl.py @@ -1,5 +1,4 @@ import json -import os import shlex import subprocess import sys @@ -49,14 +48,14 @@ def ytdl_bin_get_args_base(cs: ClipperState) -> List[str]: # Only pass --ffmpeg-location if it points to an existing executable ffmpeg_path = cp.ffmpegPath try: - exists = os.path.isfile(ffmpeg_path) + exists = Path(ffmpeg_path).is_file() except Exception: exists = False if exists: ytdl_args.extend(["--ffmpeg-location", shlex.quote(ffmpeg_path)]) else: logger.debug( - f"Skipping --ffmpeg-location: path not found -> {ffmpeg_path!r}" + f"Skipping --ffmpeg-location: path not found -> {ffmpeg_path!r}", ) cookies = settings["cookiefile"] diff --git a/src/gui-frontend/src/utils/debounce.ts b/src/gui-frontend/src/utils/debounce.ts index e888cc39..295d2a0d 100644 --- a/src/gui-frontend/src/utils/debounce.ts +++ b/src/gui-frontend/src/utils/debounce.ts @@ -1,7 +1,7 @@ -export function debounce any>(fn: T, wait = 200) { +export function debounce(fn: (...args: A) => void, wait = 200) { let t: ReturnType - return ((...args: Parameters) => { + return (...args: A): void => { clearTimeout(t) t = setTimeout(() => fn(...args), wait) - }) as T + } }