From cbb56c6d51a08e3128e05a818955e30a6b60e1d3 Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Fri, 3 Apr 2026 08:39:24 -0300 Subject: [PATCH] most solid commit --- cht/stream/lifecycle.py | 11 +++-------- cht/ui/waveform.py | 41 ++++++++++++++++++----------------------- cht/window.py | 41 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/cht/stream/lifecycle.py b/cht/stream/lifecycle.py index 2a5119b..19bc07c 100644 --- a/cht/stream/lifecycle.py +++ b/cht/stream/lifecycle.py @@ -5,7 +5,7 @@ from threading import Thread from gi.repository import GLib -from cht.config import TRANSCRIBE_MIN_CHUNK_S, SEGMENT_DURATION +from cht.config import TRANSCRIBE_MIN_CHUNK_S from cht.session import rebuild_manifest from cht.stream.manager import StreamManager from cht.stream.tracker import RecordingTracker @@ -122,6 +122,8 @@ class StreamLifecycle: GLib.idle_add(self._go_live_once) if self._stream_mgr: self._stream_mgr.capture_now(on_new_frames=self._handle_new_scene_frames) + if self._stream_mgr: + self._stream_mgr.capture_now(on_new_frames=self._handle_new_scene_frames) def _go_live_once(self): if self._stream_mgr: @@ -231,13 +233,6 @@ class StreamLifecycle: if not self._stream_mgr.recorder_alive(): log.warning("Recorder died — restarting into new segment") self._rotate_segment() - return True - # Proactive rotation: cut segment when it exceeds SEGMENT_DURATION - if SEGMENT_DURATION > 0: - dur = self._stream_mgr._estimate_safe_duration() - if dur and dur >= SEGMENT_DURATION: - log.info("Segment reached %.0fs — rotating", dur) - self._rotate_segment() return True def _rotate_segment(self): diff --git a/cht/ui/waveform.py b/cht/ui/waveform.py index 049df44..ab423db 100644 --- a/cht/ui/waveform.py +++ b/cht/ui/waveform.py @@ -67,41 +67,36 @@ class WaveformWidget(Gtk.Box): cr.line_to(width, mid_y) cr.stroke() - # Draw peaks - if self._peaks is not None and len(self._peaks) > 0 and duration > 0: + # Draw peaks — always stretch to fill full width + if self._peaks is not None and len(self._peaks) > 0: n_peaks = len(self._peaks) - # Map peaks to pixel columns - peak_duration = n_peaks * self._bucket_duration max_peak = np.max(self._peaks) if np.max(self._peaks) > 0 else 1.0 for x in range(width): - # Time at this pixel - t = (x / width) * duration - # Corresponding peak index - idx = int(t / self._bucket_duration) + idx = int((x / width) * n_peaks) if 0 <= idx < n_peaks: val = self._peaks[idx] / max_peak - bar_h = val * (height * 0.45) # 90% of half-height - # Green gradient based on amplitude + bar_h = val * (height * 0.45) cr.set_source_rgba(0.2, 0.6 + val * 0.4, 0.3, 0.85) cr.rectangle(x, mid_y - bar_h, 1, bar_h * 2) cr.fill() - # Scene markers - if duration > 0: - cr.set_source_rgba(1.0, 1.0, 0.3, 0.3) - cr.set_line_width(1) - for marker in state.scene_markers: - mx = (marker / duration) * width + # Scene markers and playhead use timeline duration + x_duration = max(duration, 0.1) + + cr.set_source_rgba(1.0, 1.0, 0.3, 0.3) + cr.set_line_width(1) + for marker in state.scene_markers: + mx = (marker / x_duration) * width + if 0 <= mx <= width: cr.move_to(mx, 0) cr.line_to(mx, height) cr.stroke() # Playhead - if duration > 0: - px = (state.cursor / duration) * width - cr.set_source_rgba(1.0, 0.3, 0.3, 0.9) - cr.set_line_width(2) - cr.move_to(px, 0) - cr.line_to(px, height) - cr.stroke() + px = (state.cursor / x_duration) * width + cr.set_source_rgba(1.0, 0.3, 0.3, 0.9) + cr.set_line_width(2) + cr.move_to(px, 0) + cr.line_to(px, height) + cr.stroke() diff --git a/cht/window.py b/cht/window.py index fbd1ebc..967a9fa 100644 --- a/cht/window.py +++ b/cht/window.py @@ -237,6 +237,28 @@ class ChtWindow(Adw.ApplicationWindow): self._update_scrub_bar_manifest() self._populate_model_dropdown() + def _reload_waveform(self, mgr): + """Recompute waveform from existing segments in background.""" + segments = mgr.recording_segments + if not segments: + return + from cht.stream import ffmpeg as ff + + def _compute(): + audio_dir = mgr.audio_dir + audio_dir.mkdir(parents=True, exist_ok=True) + full_wav = audio_dir / "full.wav" + try: + ff.extract_audio_chunk(segments[0], full_wav) + self._waveform_engine.compute_full(full_wav) + peaks = self._waveform_engine.peaks + bucket_dur = self._waveform_engine.bucket_duration + GLib.idle_add(self._waveform_widget.set_peaks, peaks.copy(), bucket_dur) + except Exception as e: + log.error("Waveform reload failed: %s", e) + + Thread(target=_compute, daemon=True, name="waveform_reload").start() + # -- Streaming -- def _start_stream(self, session_id=None): @@ -256,6 +278,7 @@ class ChtWindow(Adw.ApplicationWindow): if session_id: self._load_existing_frames() self._load_existing_transcript() + self._reload_waveform(mgr) self.set_title(f"{APP_NAME} — {mgr.session_id}") log.info("Waiting for sender...") @@ -406,6 +429,14 @@ class ChtWindow(Adw.ApplicationWindow): Thread(target=_capture, daemon=True, name="scrub_capture").start() + def _manual_segment_cut(self): + """Ctrl+R: manually cut recording into a new segment.""" + if not self._lifecycle.is_streaming: + return + log.info("Manual segment cut requested") + self._lifecycle._rotate_segment() + self._agent_output.append("Segment cut.\n") + def _stop_stream(self, reload_session=False): log.info("Stopping stream...") mgr = self._lifecycle.stream_mgr @@ -523,6 +554,14 @@ class ChtWindow(Adw.ApplicationWindow): kb.bind(KEY_DELETE, lambda **_: self._agent_output.clear()) kb.attach(self) + # Ctrl+R: manual segment cut + ctrl_r = Gtk.ShortcutController() + ctrl_r.add_shortcut(Gtk.Shortcut( + trigger=Gtk.ShortcutTrigger.parse_string("r"), + action=Gtk.CallbackAction.new(lambda *_: self._manual_segment_cut()), + )) + self.add_controller(ctrl_r) + # -- Agent actions -- def _build_selection_message(self, verb: str) -> str | None: @@ -635,7 +674,7 @@ class ChtWindow(Adw.ApplicationWindow): self._known_frames.add(fid) try: pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(entry["path"]), 256, 144, True) - auto = not self._transcript_panel.has_selection + auto = self._timeline.state.live and not self._transcript_panel.has_selection self._frames_panel.add_item(fid, pixbuf, entry["timestamp"], auto_select=auto) self._timeline_controls.scrub_bar.add_frame(entry["timestamp"], str(entry["path"])) except Exception as e: