Skip to content

Commit 28b60b6

Browse files
RMANOVclaude
andcommitted
Add --auto-exit mode: converge & stop instead of running forever
Adaptive convergence algorithm: after warmup, monitors brightness and volume deltas against targets. When both are within 1% for 3 consecutive frames, exits cleanly. Typical convergence: ~23s. Also fixes pre-existing bug: audio warmup counter stopped incrementing after brightness warmup ended (frame 20), so audio warmup (needs frame 40) never completed. Counter now increments while either warmup is active. Python: --auto-exit flag, convergence tracking in run() loop Rust: --auto-exit flag, convergence tracking in tick() → returns bool, Cargo.toml version bumped 0.1.0 → 1.2.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 494d34f commit 28b60b6

File tree

4 files changed

+95
-14
lines changed

4 files changed

+95
-14
lines changed

adaptive-rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.1.0"
10+
version = "1.2.0"
1111
edition = "2021"
1212
authors = ["Adaptive Brightness Controller"]
1313
license = "MIT"

adaptive-rust/crates/bin/src/controller.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct ControllerConfig {
2929
pub volume_smoothing: f32,
3030
pub update_interval: Duration,
3131
pub warmup_frames: u32,
32+
pub auto_exit: bool,
3233
}
3334

3435
impl Default for ControllerConfig {
@@ -42,6 +43,7 @@ impl Default for ControllerConfig {
4243
volume_smoothing: 0.2,
4344
update_interval: Duration::from_millis(500),
4445
warmup_frames: 20,
46+
auto_exit: false,
4547
}
4648
}
4749
}
@@ -89,6 +91,12 @@ pub struct Controller {
8991
// Timing
9092
last_update: Instant,
9193
last_perf_report: Instant,
94+
start_time: Instant,
95+
96+
// Auto-exit convergence tracking
97+
last_target_brightness: Option<f32>,
98+
last_target_volume: Option<f32>,
99+
converge_count: u32,
92100
}
93101

94102
impl Controller {
@@ -140,17 +148,21 @@ impl Controller {
140148
volume_control,
141149
last_update: Instant::now(),
142150
last_perf_report: Instant::now(),
151+
start_time: Instant::now(),
152+
last_target_brightness: None,
153+
last_target_volume: None,
154+
converge_count: 0,
143155
})
144156
}
145157

146-
/// Process one tick of the controller
147-
pub fn tick(&mut self) -> Result<()> {
158+
/// Process one tick. Returns true if converged (auto-exit).
159+
pub fn tick(&mut self) -> Result<bool> {
148160
let now = Instant::now();
149161

150162
// Check update interval
151163
if now.duration_since(self.last_update) < self.config.update_interval {
152164
thread::sleep(Duration::from_millis(10));
153-
return Ok(());
165+
return Ok(false);
154166
}
155167
self.last_update = now;
156168

@@ -160,13 +172,32 @@ impl Controller {
160172
// Process audio data
161173
self.process_audio()?;
162174

175+
// Auto-exit convergence check (after warmup)
176+
if self.config.auto_exit && self.warmup_frame >= self.config.warmup_frames {
177+
let b_ok = self.last_target_brightness
178+
.map_or(false, |t| (self.smoothed_brightness - t).abs() < 1.0);
179+
let v_ok = self.last_target_volume
180+
.map_or(true, |t| (self.smoothed_volume - t).abs() < 1.0);
181+
if b_ok && v_ok {
182+
self.converge_count += 1;
183+
} else {
184+
self.converge_count = 0;
185+
}
186+
if self.converge_count >= 3 {
187+
let elapsed = self.start_time.elapsed().as_secs_f32();
188+
info!("Converged in {:.1}s — brightness: {:.1}%, volume: {:.1}%",
189+
elapsed, self.smoothed_brightness, self.smoothed_volume);
190+
return Ok(true);
191+
}
192+
}
193+
163194
// Periodic performance report
164195
if now.duration_since(self.last_perf_report) > Duration::from_secs(30) {
165196
self.print_performance_stats();
166197
self.last_perf_report = now;
167198
}
168199

169-
Ok(())
200+
Ok(false)
170201
}
171202

172203
fn process_brightness(&mut self) -> Result<()> {
@@ -204,6 +235,8 @@ impl Controller {
204235
self.config.max_brightness,
205236
);
206237

238+
self.last_target_brightness = Some(target);
239+
207240
// Determine smoothing factor
208241
let time_since_change = self.last_significant_change.elapsed().as_secs_f32();
209242
let smooth_factor = if self.warmup_frame < self.config.warmup_frames {
@@ -260,6 +293,8 @@ impl Controller {
260293
self.config.max_volume,
261294
);
262295

296+
self.last_target_volume = Some(target);
297+
263298
// Apply smoothing
264299
self.smoothed_volume = smooth_transition(
265300
self.smoothed_volume,

adaptive-rust/crates/bin/src/main.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,28 @@ fn main() -> Result<()> {
4040
}
4141

4242
// Create and run controller
43-
let config = controller::ControllerConfig::default();
43+
let auto_exit = std::env::args().any(|a| a == "--auto-exit");
44+
let mut config = controller::ControllerConfig::default();
45+
config.auto_exit = auto_exit;
4446
let mut controller = controller::Controller::new(config)?;
4547

4648
info!("Controller initialized, entering main loop");
47-
info!("Press Ctrl+C to stop");
49+
if auto_exit {
50+
info!("Mode: auto-exit (converge & stop)");
51+
} else {
52+
info!("Press Ctrl+C to stop");
53+
}
4854

4955
// Main loop
5056
while !SHUTDOWN.load(Ordering::SeqCst) {
51-
if let Err(e) = controller.tick() {
52-
tracing::error!("Controller error: {}", e);
57+
match controller.tick() {
58+
Ok(true) => break, // Converged
59+
Ok(false) => {}
60+
Err(e) => tracing::error!("Controller error: {}", e),
5361
}
5462
}
5563

56-
info!("Shutdown signal received, cleaning up...");
64+
info!("Cleaning up...");
5765
controller.cleanup();
5866
info!("Cleanup complete, exiting");
5967

adaptive_brightness_volume.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ class AdaptiveBrightnessVolumeController:
231231
def __init__(self, camera_index: int = 0,
232232
lock_exposure: bool = True,
233233
brightness_range: Tuple[int, int] = (5, 45),
234-
volume_range: Tuple[int, int] = (3, 35)):
234+
volume_range: Tuple[int, int] = (3, 35),
235+
auto_exit: bool = False):
235236
self.system = platform.system().lower()
236237
if self.system not in ["linux", "windows"]:
237238
print(f"Currently only Linux and Windows are supported. Detected: {self.system}")
@@ -263,6 +264,9 @@ def __init__(self, camera_index: int = 0,
263264
self.volume_tool = tool
264265
break
265266

267+
# Auto-exit: stop once brightness & volume converge
268+
self.auto_exit: bool = auto_exit
269+
266270
# Configuration
267271
self.camera_index: int = camera_index
268272
self.lock_exposure: bool = lock_exposure
@@ -1079,6 +1083,7 @@ def process_frames(self, frame_queue: Queue, brightness_queue: Queue) -> None:
10791083
time.sleep(self.inactivity_check_interval)
10801084

10811085
def run(self) -> None:
1086+
start_time = time.time()
10821087
self.frame_queue = Queue(maxsize=10)
10831088
self.brightness_queue = Queue(maxsize=10)
10841089

@@ -1092,6 +1097,13 @@ def run(self) -> None:
10921097
last_perf_report_time = time.time()
10931098
perf_report_interval = 30.0
10941099

1100+
# Auto-exit convergence tracking
1101+
_converge_count = 0
1102+
_converge_threshold = 1.0 # % delta to consider "stable"
1103+
_converge_required = 3 # consecutive stable frames to confirm
1104+
_last_target_brightness = None
1105+
_last_target_volume = None
1106+
10951107
try:
10961108
while not self.stop_event.is_set():
10971109
current_time = time.time()
@@ -1156,8 +1168,9 @@ def run(self) -> None:
11561168
if SCREEN_CAPTURE_AVAILABLE:
11571169
target_brightness = target_brightness * self.screen_brightness_factor
11581170

1159-
if self.is_in_warmup:
1171+
if self.is_in_warmup or self.is_audio_in_warmup:
11601172
self.current_warmup_frame += 1
1173+
if self.is_in_warmup:
11611174
if self.initial_brightness is None:
11621175
try:
11631176
self.initial_brightness = self.get_brightness()
@@ -1269,6 +1282,27 @@ def run(self) -> None:
12691282
except Exception as e:
12701283
print(f"Volume setting error: {e}")
12711284

1285+
# Auto-exit: check convergence after warmup
1286+
if self.auto_exit and not self.is_in_warmup:
1287+
b_stable = (_last_target_brightness is not None and
1288+
abs(self.smoothed_brightness - _last_target_brightness) < _converge_threshold)
1289+
v_stable = (not AUDIO_AVAILABLE or not self.is_audio_in_warmup) and (
1290+
not AUDIO_AVAILABLE or (
1291+
_last_target_volume is not None and
1292+
abs(self.smoothed_volume - _last_target_volume) < _converge_threshold))
1293+
if b_stable and v_stable:
1294+
_converge_count += 1
1295+
else:
1296+
_converge_count = 0
1297+
_last_target_brightness = target_brightness if camera_brightness is not None else _last_target_brightness
1298+
_last_target_volume = target_volume if AUDIO_AVAILABLE else _last_target_volume
1299+
if _converge_count >= _converge_required:
1300+
elapsed = time.time() - start_time
1301+
print(f"\nConverged in {elapsed:.1f}s — "
1302+
f"brightness: {self.smoothed_brightness:.1f}%, "
1303+
f"volume: {self.smoothed_volume:.1f}%")
1304+
break
1305+
12721306
if current_time - last_brightness_change_time > 10:
12731307
update_interval = min(update_interval * 1.2, 2.0)
12741308
else:
@@ -1322,10 +1356,13 @@ def run(self) -> None:
13221356
print(" pip install pillow --user # Alternative method")
13231357
print("\nContinuing without screen content analysis...\n")
13241358

1359+
auto_exit = "--auto-exit" in sys.argv
1360+
13251361
try:
1326-
controller = AdaptiveBrightnessVolumeController()
1362+
controller = AdaptiveBrightnessVolumeController(auto_exit=auto_exit)
13271363
print("\nStarting adaptive brightness and volume controller...")
13281364
print(f"Platform: {platform.system()}")
1365+
print(f"Mode: {'auto-exit (converge & stop)' if auto_exit else 'continuous'}")
13291366
print(f"Brightness range: {controller.min_brightness}% - {controller.max_brightness}%")
13301367
print(f"Volume range: {controller.min_volume}% - {controller.max_volume}%")
13311368
print(f"Brightness control method: {controller.brightness_method}")
@@ -1336,7 +1373,8 @@ def run(self) -> None:
13361373
print(f"Audio control: Enabled ({AUDIO_METHOD})")
13371374
else:
13381375
print("Audio control: Disabled")
1339-
print("\nPress Ctrl+C to stop")
1376+
if not auto_exit:
1377+
print("\nPress Ctrl+C to stop")
13401378
controller.run()
13411379
except KeyboardInterrupt:
13421380
print("\nStopping controller...")

0 commit comments

Comments
 (0)