Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/adding-gui-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ Settings flow through the system in this order:
### Testing
After adding a setting:
1. Test CLI usage: `python -m clipper.yt_clipper --your-setting 50 markup.json`
2. For GUI-only tuning parameters (no CLI args) like `lgg_projection_gain` (an intensity multiplier for Lift/Gamma/Gain color wheel hue projection), ensure:
- Added to schema with empty `cli_args` list so it persists
- Added to `GeneralSettings` dataclass (`lgg_projection_gain: float = 1.5`)
- Consumed in frontend logic (e.g., passed into `buildLggFilterFromWheels`)
- UI control updates backend via `update_general_settings` API

Example schema entry (general section):
```python
"lgg_projection_gain": {
"type": "number",
"description": "Intensity multiplier for Lift/Gamma/Gain color wheel hue projection (0.5-2.0)",
"min": 0.5,
"max": 2.0,
"default": 1.5,
"cli_args": [],
},
```

Frontend usage snippet:
```ts
const projectionGain = settingsStore.generalSettings?.lgg_projection_gain ?? 1.5
buildLggFilterFromWheels(wheelsInput, globalGamma, projectionGain)
```
2. Test GUI usage: Verify setting appears in Settings Panel and persists when saved
3. Test processing: Ensure setting affects video output as expected
4. Test validation: Verify min/max ranges work in both CLI and GUI
Expand Down
10 changes: 10 additions & 0 deletions src/clipper/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,16 @@ def getSettingsSchema() -> Dict[str, Any]:
"default": 5000,
"cli_args": ["--cache-max-size-mb"],
},
# Color Grading / UI tuning (GUI only for now)
"lgg_projection_gain": {
"type": "number",
"description": "Intensity multiplier for Lift/Gamma/Gain color wheel hue projection (0.5-2.0)",
"min": 0.5,
"max": 2.0,
"default": 1.5,
# Intentionally no cli_args yet; internal GUI tuning parameter
"cli_args": [],
},
},
"video": {
"video_title": {
Expand Down
4 changes: 4 additions & 0 deletions src/clipper/gui/settings_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class GeneralSettings:
cache_folder_path: str = ""
cache_max_size_mb: int = 5000

# === COLOR GRADING / UI TUNING ===
# Multiplier applied to LGG color wheel hue projection (was hardcoded 1.5)
lgg_projection_gain: float = 1.5

# === WINDOW OPTIONS ===
window_width: int = 1000 # Default window width
window_height: int = 800 # Default window height
Expand Down
4 changes: 2 additions & 2 deletions src/gui-frontend/src/components/ColorControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<el-scrollbar height="100%">
<div class="color-controls-content">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="Color Adjustments" name="basic">
<el-tab-pane label="Filters" name="basic">
<div class="control-group">
<div class="control-label">Brightness</div>
<el-slider v-model="brightness" :min="-0.5" :max="0.5" :step="0.01" @change="emitFilter" show-input input-size="small" />
Expand All @@ -25,7 +25,7 @@
<el-slider v-model="gamma" :min="0.1" :max="3" :step="0.01" @change="emitFilter" show-input input-size="small" />
</div>
</el-tab-pane>
<el-tab-pane label="Advanced (Lift/Gamma/Gain)" name="advanced">
<el-tab-pane label="Colors (Lift/Gamma/Gain)" name="advanced">
<LiftGammaGainWheels
:wheel-state="advancedWheelState"
@wheel-state-changed="handleWheelStateChanged"
Expand Down
48 changes: 47 additions & 1 deletion src/gui-frontend/src/components/LiftGammaGainWheels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,26 @@
</div>
</div>

<div class="control-group">
<div class="control-label">Projection Gain
<el-tooltip placement="top" effect="dark">
<template #content>
Controls how strongly the selected hue pushes complementary channels toward neutrality.<br/>
Lower = subtle separation. Higher = more pronounced color isolation.
</template>
<el-icon style="margin-left:4px; cursor: help; font-size:14px; opacity:0.8"><InfoFilled /></el-icon>
</el-tooltip>
</div>
<el-slider
v-model="projectionGainLocal"
:min="0.5"
:max="2.0"
:step="0.01"
@change="onProjectionGainCommit"
show-input
input-size="small"
/>
</div>
<div class="control-group">
<div class="control-label">Global Gamma</div>
<el-slider v-model="globalGamma" :min="0.1" :max="3" :step="0.01" @change="emitChange" show-input input-size="small" />
Expand All @@ -91,9 +111,11 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { RefreshLeft } from '@element-plus/icons-vue'
import { InfoFilled } from '@element-plus/icons-vue'
import ColorWheel from './common/ColorWheel.vue'
import { buildLggFilterFromWheels } from '@/utils/lgg'
import type { LggWheelState } from '@/types/colorGrading'
import { useSettingsStore } from '@/stores/settings'

interface Emits {
(e: 'wheel-state-changed', state: LggWheelState, filter: string): void
Expand All @@ -118,6 +140,9 @@ const liftAmount = ref(props.wheelState.lift.amount)
const midAmount = ref(props.wheelState.mid.amount)
const gainAmount = ref(props.wheelState.gain.amount)
const globalGamma = ref(props.wheelState.globalGamma)
// Projection gain comes from settings (persisted). Keep a local ref for immediate UI responsiveness.
const settingsStore = useSettingsStore()
const projectionGainLocal = ref(settingsStore.generalSettings?.lgg_projection_gain ?? 1.5)
const wheelSize = 140

function buildState(): LggWheelState {
Expand All @@ -135,7 +160,8 @@ function computeFilter(): string {
mid: { hex: midHex.value, strength: midSat.value, neutral: midAmount.value },
gain: { hex: gainHex.value, strength: gainSat.value, neutral: gainAmount.value }
},
globalGamma.value
globalGamma.value,
projectionGainLocal.value
)
}
function emitChange() {
Expand Down Expand Up @@ -201,6 +227,26 @@ function onVectorMid(payload: { hex: string; sat: number }) {
function onVectorGain(payload: { hex: string; sat: number }) {
gainSat.value = payload.sat
}

async function onProjectionGainCommit() {
// Persist to backend settings then emit change to rebuild filter
try {
if (settingsStore.generalSettings) {
await settingsStore.updateGeneralSettings({ lgg_projection_gain: projectionGainLocal.value })
}
} catch (e) {
console.error('Failed updating projection gain setting', e)
}
emitChange()
}

// Reactively sync if settings store projection gain changes elsewhere (e.g., Settings panel)
watch(() => settingsStore.generalSettings?.lgg_projection_gain, (nv) => {
if (typeof nv === 'number' && Math.abs(nv - projectionGainLocal.value) > 1e-6) {
projectionGainLocal.value = nv
emitChange()
}
})
</script>

<style scoped>
Expand Down
15 changes: 15 additions & 0 deletions src/gui-frontend/src/components/SettingsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,21 @@
/>
<el-text class="setting-help" type="info">Show system notification when done</el-text>
</el-form-item>
<el-form-item label="Projection Gain">
<el-slider
:model-value="props.settings?.lgg_projection_gain ?? 1.5"
:min="0.5"
:max="2.0"
:step="0.01"
style="width: 280px;"
@change="(value:number) => handleNumberChange('lgg_projection_gain', value)"
show-input
input-size="small"
/>
<el-text class="setting-help" type="info">
Intensity multiplier for LGG color wheel hue projection. Lower = gentler isolation; higher = stronger channel separation.
</el-text>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
Expand Down
3 changes: 3 additions & 0 deletions src/gui-frontend/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export interface GeneralSettings {
// === CACHE OPTIONS ===
cache_folder_path: string
cache_max_size_mb: number // 0 = unlimited

// === COLOR GRADING / UI TUNING ===
lgg_projection_gain: number
}

export interface VideoSpecificSettings {
Expand Down
11 changes: 10 additions & 1 deletion src/gui-frontend/src/utils/colorFilterBuild.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import type { ColorGradingState } from '@/types/colorGrading'
import { buildBasicFilter } from '@/utils/colorBasics'
import { buildLggFilterFromWheels } from '@/utils/lgg'
import { useSettingsStore } from '@/stores/settings'
import { joinFilters } from '@/utils/filterString'

export function buildFilterFromState(state: ColorGradingState): string {
// Read projection gain from settings (fallback to default 1.5 if not loaded yet)
let projectionGain = 1.5
try {
const settings = useSettingsStore()
projectionGain = settings.generalSettings?.lgg_projection_gain ?? 1.5
} catch {
// store may not be initialized in some isolated usage contexts; ignore
}
const b = state.basic
const basic = buildBasicFilter({
brightness: b.brightness,
Expand All @@ -17,6 +26,6 @@ export function buildFilterFromState(state: ColorGradingState): string {
lift: { hex: w.lift.hex, strength: w.lift.sat, neutral: w.lift.amount },
mid: { hex: w.mid.hex, strength: w.mid.sat, neutral: w.mid.amount },
gain: { hex: w.gain.hex, strength: w.gain.sat, neutral: w.gain.amount },
}, w.globalGamma)
}, w.globalGamma, projectionGain)
return joinFilters(basic, adv)
}
24 changes: 12 additions & 12 deletions src/gui-frontend/src/utils/lgg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ function hexToHueDeg(hex: string): number {
// (RGB-based mapping variant intentionally removed; using angle-based projection)

// Angle-based mapping: aligned axis remains neutral (base), complementary axes reduce.
// w_c = clamp(base - GAIN * amount * ((1 - cos(delta))/2), 0, 1)
const PROJECTION_GAIN = 1.5
function wheelAngleToW(angleDeg: number, amount: number, base: number): { wr: number; wg: number; wb: number } {
// w_c = clamp(base - projectionGain * amount * ((1 - cos(delta))/2), 0, 1)
function wheelAngleToW(angleDeg: number, amount: number, base: number, projectionGain: number): { wr: number; wg: number; wb: number } {
const rad = Math.PI / 180
const a = clamp(amount, 0, 1)
const b = clamp(base, 0, 1)
const del = (ax: number) => (1 - Math.cos((angleDeg - ax) * rad)) / 2 // 0..1
const wr = clamp(b - PROJECTION_GAIN * a * del(0), 0, 1)
const wg = clamp(b - PROJECTION_GAIN * a * del(120), 0, 1)
const wb = clamp(b - PROJECTION_GAIN * a * del(240), 0, 1)
const pg = clamp(projectionGain, 0.01, 10) // safety clamp
const wr = clamp(b - pg * a * del(0), 0, 1)
const wg = clamp(b - pg * a * del(120), 0, 1)
const wb = clamp(b - pg * a * del(240), 0, 1)
return { wr, wg, wb }
}

Expand All @@ -92,7 +92,7 @@ function scaleWheelToValueV1(w: number): number {
return w * 2
}

export function lggParamsFromWheels(input: LGGFromWheelsInput): LGGParams {
export function lggParamsFromWheels(input: LGGFromWheelsInput, projectionGain = 1.5): LGGParams {
// Map color wheel + amount to per-channel parameters using Shotcut V1 semantics
const liftHue = hexToHueDeg(input.lift.hex)
const midHue = hexToHueDeg(input.mid.hex)
Expand All @@ -105,9 +105,9 @@ export function lggParamsFromWheels(input: LGGFromWheelsInput): LGGParams {
const mn = input.mid.neutral !== undefined ? clamp(input.mid.neutral, 0, 1) : 0.5
const hn = input.gain.neutral !== undefined ? clamp(input.gain.neutral, 0, 1) : 0.5

const lw = wheelAngleToW(liftHue, la, ln)
const mw = wheelAngleToW(midHue, ma, mn)
const hw = wheelAngleToW(hiHue, ha, hn)
const lw = wheelAngleToW(liftHue, la, ln, projectionGain)
const mw = wheelAngleToW(midHue, ma, mn, projectionGain)
const hw = wheelAngleToW(hiHue, ha, hn, projectionGain)

// Lift per channel in [-1, 1] -> Shotcut uses liftwheel.channelF * 2 - 1
const rlift = +(lw.wr * 2 - 1).toFixed(6)
Expand Down Expand Up @@ -151,8 +151,8 @@ export function buildLutrgbFromParams(p: LGGParams): string {
return `lutrgb=r=${r}:g=${g}:b=${b}`
}

export function buildLggFilterFromWheels(input: LGGFromWheelsInput, globalGamma?: number): string {
const params = lggParamsFromWheels(input)
export function buildLggFilterFromWheels(input: LGGFromWheelsInput, globalGamma?: number, projectionGain = 1.5): string {
const params = lggParamsFromWheels(input, projectionGain)
const isLiftNeutral = Math.abs(params.rlift) < 1e-6 && Math.abs(params.glift) < 1e-6 && Math.abs(params.blift) < 1e-6
const isGammaNeutral = Math.abs(params.rgamma - 1) < 1e-6 && Math.abs(params.ggamma - 1) < 1e-6 && Math.abs(params.bgamma - 1) < 1e-6
const isGainNeutral = Math.abs(params.rgain - 1) < 1e-6 && Math.abs(params.ggain - 1) < 1e-6 && Math.abs(params.bgain - 1) < 1e-6
Expand Down
Loading