Skip to content
Merged
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
39 changes: 1 addition & 38 deletions apps/desktop-gpui/src/editor_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use std::{

use cap_cursor_info::CursorFamily;
use cap_editor::{EditorFrameOutput, EditorInstance, EditorState};
use cap_project::caption_words::caption_text_from_words;
use cap_project::{
ClipSpeedAudioMode, Cursors, ProjectConfiguration, RecordingMeta, RecordingMetaInner,
StudioRecordingMeta, TimelineConfiguration, XY,
Expand Down Expand Up @@ -5346,44 +5347,6 @@ pub(crate) struct EditorNotice {
/// How long a notice stays up.
const EDITOR_NOTICE_DURATION: Duration = Duration::from_millis(2500);

fn caption_text_from_words(words: &[cap_project::CaptionWord]) -> String {
let mut text = String::new();
for word in words {
let value = word.text.trim();
if value.is_empty() {
continue;
}
let attaches = value.chars().next().is_some_and(|value| {
matches!(
value,
',' | '.'
| '!'
| '?'
| ';'
| ':'
| '%'
| ')'
| ']'
| '}'
| '\''
| '’'
| '、'
| '。'
| '!'
| '?'
| ';'
| ':'
| ','
)
});
if !text.is_empty() && !attaches {
text.push(' ');
}
text.push_str(value);
}
text
}

fn fresh_caption_source_id(project: &ProjectConfiguration) -> String {
use std::hash::{BuildHasher, Hasher};

Expand Down
165 changes: 4 additions & 161 deletions apps/desktop-gpui/src/transcription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1856,167 +1856,10 @@ fn extract_audio_from_video(video_path: &Path, output_path: &Path) -> Result<(),
// Caption words & chunking
// ---------------------------------------------------------------------------

/// `MAX_CAPTION_WORD_DURATION` (`captions.rs:75`): Whisper/Parakeet sometimes
/// stretch a trailing word's end across a following silence, which leaves the
/// rendered caption stuck on screen and duplicates the word across timeline
/// cuts once projected.
pub const MAX_CAPTION_WORD_DURATION: f32 = 2.5;
/// `captions.rs:67-69`.
const TARGET_CAPTION_WORDS_PER_SEGMENT: usize = 6;
const MAX_CAPTION_WORDS_PER_SEGMENT: usize = 8;
const MIN_FINAL_CAPTION_WORDS: usize = 3;

/// `caption_char_attaches_to_previous` (`captions.rs:745-767`).
fn caption_char_attaches_to_previous(value: char) -> bool {
matches!(
value,
',' | '.'
| '!'
| '?'
| ';'
| ':'
| '%'
| ')'
| ']'
| '}'
| '\''
| '’'
| '、'
| '。'
| '!'
| '?'
| ';'
| ':'
| ','
)
}

fn caption_token_attaches_to_previous(text: &str) -> bool {
text.trim()
.chars()
.next()
.is_some_and(caption_char_attaches_to_previous)
}

/// `caption_boundary_word_is_weak` (`captions.rs:769-801`).
fn caption_boundary_word_is_weak(word: &CaptionWord) -> bool {
let normalized = word
.text
.trim()
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_lowercase();

normalized.chars().count() <= 1
|| matches!(
normalized.as_str(),
"an" | "as"
| "at"
| "be"
| "by"
| "do"
| "he"
| "if"
| "in"
| "is"
| "it"
| "me"
| "my"
| "of"
| "on"
| "or"
| "so"
| "to"
| "up"
| "we"
)
}

/// `normalize_caption_words` (`captions.rs:803-831`).
fn normalize_caption_words(words: Vec<CaptionWord>) -> Vec<CaptionWord> {
let mut normalized: Vec<CaptionWord> = Vec::with_capacity(words.len());

for word in words {
let text = word.text.trim();
if text.is_empty() {
continue;
}

if caption_token_attaches_to_previous(text)
&& let Some(previous) = normalized.last_mut()
{
previous.text.push_str(text);
previous.end = word.end;
} else {
normalized.push(CaptionWord {
text: text.to_string(),
start: word.start,
end: word.end,
});
}
}

for word in &mut normalized {
word.end = word.end.min(word.start + MAX_CAPTION_WORD_DURATION);
}

normalized
}

/// `caption_text_from_words` (`captions.rs:833-849`).
fn caption_text_from_words<'a>(words: impl IntoIterator<Item = &'a CaptionWord>) -> String {
let mut text = String::new();

for word in words {
let word_text = word.text.trim();
if word_text.is_empty() {
continue;
}

if !text.is_empty() && !caption_token_attaches_to_previous(word_text) {
text.push(' ');
}
text.push_str(word_text);
}

text
}

/// `caption_word_chunks` (`captions.rs:851-883`).
fn caption_word_chunks(words: &[CaptionWord]) -> Vec<&[CaptionWord]> {
let mut chunks = Vec::new();
let mut start = 0;

while start < words.len() {
let remaining = words.len() - start;
if remaining <= TARGET_CAPTION_WORDS_PER_SEGMENT {
chunks.push(&words[start..]);
break;
}

let mut end = (start + TARGET_CAPTION_WORDS_PER_SEGMENT).min(words.len());
while end < words.len()
&& caption_boundary_word_is_weak(&words[end - 1])
&& end - start < MAX_CAPTION_WORDS_PER_SEGMENT
{
end += 1;
}

let remaining_after = words.len() - end;
if remaining_after > 0
&& remaining_after < MIN_FINAL_CAPTION_WORDS
&& caption_boundary_word_is_weak(&words[end])
{
end = words.len();
}

chunks.push(&words[start..end]);
start = end;
}

chunks
}
use cap_project::caption_words::{
caption_text_from_words, caption_word_chunks, normalize_caption_words,
MAX_CAPTION_WORD_DURATION,
};

// ---------------------------------------------------------------------------
// Track derivation -- deriveCaptionTrackSegments, in Rust
Expand Down
Loading
Loading