Skip to content

Commit 8d71e99

Browse files
DylanBricarclaude
andcommitted
feat: symmetric audio bars + post-transcription hook
- PR cjpais#552: Symmetric mirrored audio bars visualization (25 buckets, 150-2500Hz range, 13 bars with center-mirror effect) - PR cjpais#930: Post-transcription hook support - runs hooks/transcription script from app data dir, passing text via stdin - Overlay height adjusted for taller bars (34→40px CSS, 38→44px Rust) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 76f4326 commit 8d71e99

File tree

6 files changed

+95
-16
lines changed

6 files changed

+95
-16
lines changed

src-tauri/src/actions.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,14 @@ impl ShortcutAction for TranscribeAction {
740740
change_tray_icon(&ah, TrayIconState::Idle);
741741
}
742742

743+
// Run post-transcription hook if configured
744+
if !transcription.is_empty() {
745+
crate::managers::transcription::run_transcription_hook(
746+
&ah,
747+
&transcription,
748+
);
749+
}
750+
743751
// Always save to history for non-empty results or meaningful audio duration
744752
if !transcription.is_empty() || duration_seconds > 1.0 {
745753
let hm_clone = Arc::clone(&hm);

src-tauri/src/audio_toolkit/audio/recorder.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,14 @@ fn run_consumer(
268268
let mut recording = false;
269269

270270
// ---------- spectrum visualisation setup ---------------------------- //
271-
const BUCKETS: usize = 16;
271+
const BUCKETS: usize = 25;
272272
const WINDOW_SIZE: usize = 512;
273273
let mut visualizer = AudioVisualiser::new(
274274
in_sample_rate,
275275
WINDOW_SIZE,
276276
BUCKETS,
277-
400.0, // vocal_min_hz
278-
4000.0, // vocal_max_hz
277+
150.0, // vocal_min_hz
278+
2500.0, // vocal_max_hz
279279
);
280280

281281
fn handle_frame(

src-tauri/src/managers/transcription.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,72 @@ impl TranscriptionManager {
700700
}
701701
}
702702

703+
/// Run a post-transcription hook script if it exists.
704+
/// Looks for `hooks/transcription` (or `.exe`/`.bat`/`.cmd` on Windows)
705+
/// in the app data directory and runs it, passing the transcription text via stdin.
706+
pub fn run_transcription_hook(app_handle: &AppHandle, text: &str) {
707+
use tauri::Manager;
708+
709+
let data_dir = match app_handle.path().app_data_dir() {
710+
Ok(d) => d,
711+
Err(_) => return,
712+
};
713+
714+
let hook_path = data_dir.join("hooks").join("transcription");
715+
716+
// On Windows, also check for .exe, .bat, .cmd
717+
#[cfg(target_os = "windows")]
718+
let hook_path = {
719+
if hook_path.exists() {
720+
hook_path
721+
} else {
722+
let extensions = ["exe", "bat", "cmd"];
723+
extensions
724+
.iter()
725+
.map(|ext| hook_path.with_extension(ext))
726+
.find(|p| p.exists())
727+
.unwrap_or(hook_path)
728+
}
729+
};
730+
731+
if !hook_path.exists() {
732+
return;
733+
}
734+
735+
let text = text.to_string();
736+
let hook = hook_path.clone();
737+
std::thread::spawn(move || {
738+
use std::io::Write;
739+
use std::process::{Command, Stdio};
740+
741+
let mut child = match Command::new(&hook)
742+
.stdin(Stdio::piped())
743+
.stdout(Stdio::null())
744+
.stderr(Stdio::null())
745+
.spawn()
746+
{
747+
Ok(c) => c,
748+
Err(e) => {
749+
warn!("Failed to run transcription hook {:?}: {}", hook, e);
750+
return;
751+
}
752+
};
753+
754+
if let Some(mut stdin) = child.stdin.take() {
755+
let _ = stdin.write_all(text.as_bytes());
756+
}
757+
758+
match child.wait() {
759+
Ok(status) => {
760+
if !status.success() {
761+
warn!("Transcription hook exited with status: {}", status);
762+
}
763+
}
764+
Err(e) => warn!("Failed to wait for transcription hook: {}", e),
765+
}
766+
});
767+
}
768+
703769
impl Drop for TranscriptionManager {
704770
fn drop(&mut self) {
705771
debug!("Shutting down TranscriptionManager");

src-tauri/src/overlay.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ tauri_panel! {
3131
}
3232

3333
const OVERLAY_WIDTH: f64 = 210.0;
34-
const OVERLAY_HEIGHT: f64 = 38.0;
34+
const OVERLAY_HEIGHT: f64 = 44.0;
3535

3636
#[cfg(target_os = "macos")]
3737
const OVERLAY_TOP_OFFSET: f64 = 46.0;

src/overlay/RecordingOverlay.css

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.recording-overlay {
2-
height: 34px;
2+
height: 40px;
33
width: 200px;
44
display: flex;
55
align-items: center;
@@ -76,17 +76,17 @@
7676
.bars-container {
7777
display: flex;
7878
align-items: center;
79-
gap: 3px;
80-
height: 24px;
79+
gap: 1.5px;
80+
height: 28px;
8181
flex: 1;
8282
}
8383

8484
.bar {
8585
flex: 1;
8686
background: rgba(255, 255, 255, 0.6);
87-
max-height: 22px;
88-
border-radius: 1.5px;
89-
min-height: 3px;
87+
max-height: 26px;
88+
border-radius: 1px;
89+
min-height: 2px;
9090
transition:
9191
height 60ms ease-out,
9292
opacity 80ms ease-out;

src/overlay/RecordingOverlay.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ const TimerDisplay: React.FC<{ startTime: number; isPaused: boolean }> = ({
121121
return <div className="timer-text">{display}</div>;
122122
};
123123

124+
const NUM_BARS = 13;
125+
124126
const AudioBars: React.FC = () => {
125127
const barsRef = useRef<HTMLDivElement>(null);
126-
const smoothedRef = useRef<number[]>(Array(16).fill(0));
128+
const smoothedRef = useRef<number[]>(Array(25).fill(0));
127129

128130
useEffect(() => {
129131
let unlisten: (() => void) | null = null;
@@ -138,11 +140,14 @@ const AudioBars: React.FC = () => {
138140

139141
if (barsRef.current) {
140142
const bars = barsRef.current.children;
141-
for (let i = 0; i < bars.length; i++) {
142-
const v = smoothed[i] || 0;
143+
const half = Math.ceil(NUM_BARS / 2);
144+
for (let i = 0; i < NUM_BARS; i++) {
145+
// Mirror: map bar index to bucket index (center = highest bucket)
146+
const bucketIdx = i < half ? i : NUM_BARS - 1 - i;
147+
const v = smoothed[bucketIdx] || 0;
143148
const el = bars[i] as HTMLElement;
144-
el.style.height = `${Math.min(20, 3 + Math.pow(v, 0.6) * 17)}px`;
145-
el.style.opacity = `${Math.max(0.25, v * 1.4)}`;
149+
el.style.height = `${Math.min(24, 2 + Math.pow(v, 0.6) * 22)}px`;
150+
el.style.opacity = `${Math.max(0.2, v * 1.4)}`;
146151
}
147152
}
148153
});
@@ -155,7 +160,7 @@ const AudioBars: React.FC = () => {
155160

156161
return (
157162
<div className="bars-container" ref={barsRef}>
158-
{Array.from({ length: 9 }, (_, i) => (
163+
{Array.from({ length: NUM_BARS }, (_, i) => (
159164
<div key={i} className="bar" />
160165
))}
161166
</div>

0 commit comments

Comments
 (0)