most solid commit

This commit is contained in:
2026-04-03 08:39:24 -03:00
parent 908231f98a
commit cbb56c6d51
3 changed files with 61 additions and 32 deletions

View File

@@ -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):

View File

@@ -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()

View File

@@ -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("<Ctrl>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: