most solid commit
This commit is contained in:
@@ -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:
|
||||
@@ -232,13 +234,6 @@ class StreamLifecycle:
|
||||
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):
|
||||
"""Restart recorder into a new segment and update manifest."""
|
||||
|
||||
@@ -67,39 +67,34 @@ 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:
|
||||
# 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 / duration) * width
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user