Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .cursor/rules/simple.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ alwaysApply: true
# Typescript

- Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props. Just inline them.
- After some amount of TypeScript changes, run `pnpm -r typecheck`.

# Rust

- After some amount of Rust changes, run `cargo check`.

# Mutation
- Never do manual state management for form/mutation. Things like setError is anti-pattern. use useForm(from tanstack-form) and useQuery/useMutation(from tanstack-query) for 99% cases.
Expand All @@ -19,7 +24,6 @@ alwaysApply: true

# Misc
- Do not create summary docs or example code file if not requested. Plan is ok.
- After a significant amount of TypeScript changes, run `pnpm -r typecheck`.
- If there are many classNames and they have conditional logic, use `cn` (import it with `import { cn } from "@hypr/utils"`). It is similar to `clsx`. Always pass an array. Split by logical grouping.
- Use `motion/react` instead of `framer-motion`.

55 changes: 24 additions & 31 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ async-stream = "0.3.6"
futures-channel = "0.3.31"
futures-core = "0.3.31"
futures-util = "0.3.31"
ractor = "0.15"
ractor = { version = "0.14.3" }
ractor-supervisor = "0.1.9"
reqwest = "0.12"
reqwest-streams = "0.10.0"
tokio = "1"
Expand Down
9 changes: 9 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ tasks:
cmds:
- chmod +x ./apps/desktop/src-tauri/resources/stt-aarch64-apple-darwin
- chmod +x ./apps/desktop/src-tauri/resources/passthrough-aarch64-apple-darwin

db:
env:
DB: /Users/yujonglee/Library/Application Support/com.hyprnote.nightly/db.sqlite
cmds:
- |
sqlite3 -json "$DB" 'SELECT store FROM main LIMIT 1;' |
jq -r '.[0].store' |
jless
Comment on lines +76 to +83
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard-coding a user-specific DB path

DB points at /Users/yujonglee/..., which will fail for everyone else. Please parameterize this (e.g. pick it up from env or default to $HOME/Library/...) or document how to override it before landing.

🤖 Prompt for AI Agents
In Taskfile.yaml around lines 76 to 83 the DB value is hard-coded to a single
user path (/Users/yujonglee/...), which will break for other developers; change
it to read from an environment variable with a sensible default that uses $HOME
(e.g. DB from env or fallback to $HOME/Library/... ), update the task command to
reference that variable rather than a literal path, and add a short note in
README or Taskfile comments describing how to override DB via environment for
other users.

Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ export function useFinalWords(transcriptId: string): (main.Word & { id: string }
return [];
}

return Object.entries(resultTable)
const ret = Object.entries(resultTable)
.map(([wordId, row]) => ({ ...(row as unknown as main.Word), id: wordId }))
.sort((a, b) => a.start_ms - b.start_ms);

return ret;
}, [resultTable]);
}

Expand Down
7 changes: 1 addition & 6 deletions apps/desktop/src/hooks/useAutoEnhance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ export function useAutoEnhance(tab: Extract<Tab, { type: "sessions" }>) {

if (listenerJustStopped) {
startEnhance();
}
}, [listenerStatus, prevListenerStatus, startEnhance]);

useEffect(() => {
if (enhanceTask.status === "generating" && tab.state.editor !== "enhanced") {
updateSessionTabState(tab, { editor: "enhanced" });
}
Comment on lines 60 to 63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that setting editor state to "enhanced" before enhancement completes is intentional.

The editor state is updated to "enhanced" immediately when the listener stops (line 62), but startEnhance() (line 61) runs asynchronously and may not even start due to guards at line 46. This creates several potential issues:

  1. If startEnhance returns early (no model, no transcript, or already generating), the UI shows "enhanced" when no enhancement occurred
  2. The UI indicates "enhanced" before the enhancement task actually completes
  3. If the enhancement task fails, the editor remains in "enhanced" state without actual enhanced content

Consider moving updateSessionTabState into the onSuccess callback (lines 38-42) to ensure the editor state only updates when enhancement actually succeeds.


I'll examine the codebase to verify the review comment's concerns about the timing of the editor state update.


---

🏁 Script executed:

```shell
#!/bin/bash
# Retrieve the useAutoEnhance.ts file to examine the full context
cat apps/desktop/src/hooks/useAutoEnhance.ts

Length of output: 2002


Move updateSessionTabState to the onSuccess callback to ensure editor state only updates when enhancement succeeds.

The review comment's concerns are valid:

  1. Guards at lines 46-48 confirm early return risk: The condition if (!model || !hasTranscript || enhanceTask.status === "generating") can cause startEnhance() to exit without starting the task.

  2. State update happens immediately (line 62): Calling updateSessionTabState(tab, { editor: "enhanced" }) right after startEnhance() means the editor state is set to "enhanced" even if the enhancement never starts.

  3. onSuccess callback incomplete (lines 38-42): The callback only updates updateEnhancedMd(text) but doesn't update the editor state indicator. The state update should be added here to guarantee it only happens when the task succeeds.

  4. Missing failure handling: If the enhancement task fails, the editor remains stuck in "enhanced" state without actual enhanced content.

The fix requires modifying the onSuccess callback to include updateSessionTabState(tab, { editor: "enhanced" }) and removing it from the synchronous effect, ensuring the UI accurately reflects the actual enhancement status.

🤖 Prompt for AI Agents
In apps/desktop/src/hooks/useAutoEnhance.ts around lines 60 to 63, the code
updates the session tab state to "enhanced" immediately after calling
startEnhance(), which can set the UI to enhanced even if enhancement never
starts or fails; move the call updateSessionTabState(tab, { editor: "enhanced"
}) out of the synchronous branch and into the enhanceTask's onSuccess callback
(add it alongside updateEnhancedMd(text)), and remove the immediate call so the
editor state only flips when the enhancement actually succeeds; also ensure
onFailure/onError handling resets or clears the editor state to avoid leaving it
stuck on "enhanced" after a failed enhancement.

}, [enhanceTask.status, tab, updateSessionTabState]);
}, [listenerStatus, prevListenerStatus, startEnhance]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add missing dependencies to the useEffect dependency array.

The effect uses tab (line 62) and updateSessionTabState (line 62) but neither is included in the dependency array. This violates the Rules of Hooks and can lead to stale closures where the effect references outdated values.

Apply this diff:

-  }, [listenerStatus, prevListenerStatus, startEnhance]);
+  }, [listenerStatus, prevListenerStatus, startEnhance, tab, updateSessionTabState]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, [listenerStatus, prevListenerStatus, startEnhance]);
}, [listenerStatus, prevListenerStatus, startEnhance, tab, updateSessionTabState]);
🤖 Prompt for AI Agents
In apps/desktop/src/hooks/useAutoEnhance.ts around line 64, the useEffect
dependency array currently lists [listenerStatus, prevListenerStatus,
startEnhance] but the effect also reads `tab` and `updateSessionTabState`; add
`tab` and `updateSessionTabState` to the dependency array (or if
`updateSessionTabState` is recreated every render, memoize it with useCallback
where it's defined) so the effect re-runs with the correct values and avoids
stale closures.

}
70 changes: 65 additions & 5 deletions crates/audio-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::convert::TryFrom;

use bytes::{BufMut, Bytes, BytesMut};
use futures_util::{Stream, StreamExt};
use kalosm_sound::AsyncSource;
Expand All @@ -6,11 +8,19 @@ mod error;
pub use error::*;
mod vorbis;
pub use vorbis::*;
mod stream;
pub use stream::*;

pub use rodio::Source;

const I16_SCALE: f32 = 32768.0;

#[derive(Debug, Clone, Copy)]
pub struct AudioMetadata {
pub sample_rate: u32,
pub channels: u8,
}

impl<T: AsyncSource> AudioFormatExt for T {}

pub trait AudioFormatExt: AsyncSource {
Expand Down Expand Up @@ -81,6 +91,40 @@ pub fn source_from_path(
Ok(decoder)
}

fn metadata_from_source<S>(source: &S) -> Result<AudioMetadata, crate::Error>
where
S: Source,
S::Item: rodio::Sample,
{
let sample_rate = source.sample_rate();
if sample_rate == 0 {
return Err(crate::Error::InvalidSampleRate(sample_rate));
}

let channels_u16 = source.channels();
if channels_u16 == 0 {
return Err(crate::Error::UnsupportedChannelCount {
count: channels_u16,
});
}
let channels =
u8::try_from(channels_u16).map_err(|_| crate::Error::UnsupportedChannelCount {
count: channels_u16,
})?;

Ok(AudioMetadata {
sample_rate,
channels,
})
}

pub fn audio_file_metadata(
path: impl AsRef<std::path::Path>,
) -> Result<AudioMetadata, crate::Error> {
let source = source_from_path(path)?;
metadata_from_source(&source)
}

pub fn resample_audio<S, T>(source: S, to_rate: u32) -> Result<Vec<f32>, crate::Error>
where
S: rodio::Source<Item = T> + Iterator<Item = T>,
Expand Down Expand Up @@ -136,32 +180,48 @@ where
pub struct ChunkedAudio {
pub chunks: Vec<Bytes>,
pub sample_count: usize,
pub frame_count: usize,
pub metadata: AudioMetadata,
}

pub fn chunk_audio_file(
path: impl AsRef<std::path::Path>,
sample_rate: u32,
chunk_size: usize,
chunk_ms: u64,
) -> Result<ChunkedAudio, crate::Error> {
let source = source_from_path(path)?;
let samples = resample_audio(source, sample_rate)?;
let metadata = metadata_from_source(&source)?;
let samples = resample_audio(source, metadata.sample_rate)?;

if samples.is_empty() {
return Ok(ChunkedAudio {
chunks: Vec::new(),
sample_count: 0,
frame_count: 0,
metadata,
});
}

let chunk_size = chunk_size.max(1);
let channels = metadata.channels.max(1) as usize;
let frames_per_chunk = {
let frames = ((chunk_ms as u128).saturating_mul(metadata.sample_rate as u128) + 999) / 1000;
frames.max(1).min(usize::MAX as u128) as usize
};
let samples_per_chunk = frames_per_chunk
.saturating_mul(channels)
.max(1)
.min(usize::MAX);

let sample_count = samples.len();
let frame_count = sample_count / channels;
let chunks = samples
.chunks(chunk_size)
.chunks(samples_per_chunk)
.map(|chunk| f32_to_i16_bytes(chunk.iter().copied()))
.collect();

Ok(ChunkedAudio {
chunks,
sample_count,
frame_count,
metadata,
})
}
Loading