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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# OratioText

[![Release](https://img.shields.io/github/v/release/kylethedeveloper/OratioText)](https://github.com/kylethedeveloper/OratioText/releases)
[![License](https://img.shields.io/github/license/kylethedeveloper/OratioText)](./LICENSE)
![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey)
![Tauri v2](https://img.shields.io/badge/Tauri-v2-green?logo=tauri)

| <img src="assets/appicon.png" alt="OratioText Icon" width="200" height="auto"> | A cross-platform desktop application for converting speech to text using [Whisper](https://github.com/ggerganov/whisper.cpp). This application only <ins>**runs on your computer**</ins> and uses the AI model <ins>locally</ins>. |
| :-------------------: | :----------: |

Expand All @@ -22,6 +27,11 @@ Built with [Tauri v2](https://tauri.app/) and [whisper.cpp](https://github.com/g
- **Backend**: Rust with [whisper-rs](https://github.com/tazz4843/whisper-rs) bindings
- **Audio conversion**: FFmpeg (must be installed separately)

## Screenshots

| ![Screenshot 1](assets/screenshot1.png) | ![Screenshot 2](assets/screenshot2.png) |
| :---: | :---: |

## Installation

You can download the latest installers and packages for your operating system from the [GitHub Releases](https://github.com/kylethedeveloper/OratioText/releases) page.
Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
- [x] Add language selection option (or detect automatically)
- [x] Add an about menu (feedback, donate, etc)
- [x] Add a settings menu
- [x] Build for Linux
- [x] Add option to manage downloaded models in Settings (delete, etc.)
- [ ] Add option to transcribe from URL
- [ ] Build for Linux
- [ ] Add history of transcriptions
- [ ] Send notifications when transcription is complete
- [ ] Add option to manage downloaded models in Settings (delete, etc.)
- [ ] Send notifications when transcription is complete
Binary file added assets/screenshot1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshot2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

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

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oratiotext"
version = "1.0.4"
version = "1.0.5"
description = "A cross-platform desktop application for converting speech to text using Whisper"
authors = ["kylethedeveloper"]
edition = "2021"
Expand Down
2,630 changes: 2,630 additions & 0 deletions src-tauri/gen/schemas/linux-schema.json

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct ModelInfo {
name: String,
size: String,
downloaded: bool,
file_size_bytes: Option<u64>,
}

#[tauri::command]
Expand Down Expand Up @@ -77,10 +78,27 @@ fn list_models(state: State<AppState>) -> Vec<ModelInfo> {
name: name.to_string(),
size: size.to_string(),
downloaded: state.model_manager.is_model_downloaded(name),
file_size_bytes: state.model_manager.get_model_file_size(name),
})
.collect()
}

#[tauri::command]
fn delete_model(model_name: String, state: State<AppState>) -> Result<(), String> {
state
.model_manager
.delete_model(&model_name)
.map_err(|e| e.to_string())
}

#[tauri::command]
fn open_models_dir(app: tauri::AppHandle, state: State<AppState>) -> Result<(), String> {
let path = state.model_manager.get_models_dir();
use tauri_plugin_shell::ShellExt;
app.shell().open(path.to_string_lossy().to_string(), None)
.map_err(|e| e.to_string())
}

#[tauri::command]
async fn download_model(
model_name: String,
Expand Down Expand Up @@ -187,10 +205,12 @@ pub fn run() {
get_system_info,
list_models,
download_model,
delete_model,
transcribe,
stop_transcription,
save_file,
get_app_version,
open_models_dir,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
43 changes: 43 additions & 0 deletions src-tauri/src/model_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use tauri::Emitter;
const HUGGINGFACE_BASE_URL: &str =
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main";

/// Allowlist of valid model identifiers accepted by ModelManager.
const VALID_MODEL_NAMES: &[&str] = &["tiny", "base", "small", "medium", "turbo", "large"];

/// Manages Whisper GGML model files: download, cache, and lookup.
pub struct ModelManager {
models_dir: PathBuf,
Expand All @@ -22,9 +25,26 @@ impl ModelManager {
// Ensure directory exists
std::fs::create_dir_all(&models_dir).ok();

// Canonicalize so all path comparisons use a resolved, absolute path.
let models_dir = models_dir.canonicalize().unwrap_or(models_dir);

Self { models_dir }
}

/// Returns the models directory path.
pub fn get_models_dir(&self) -> PathBuf {
self.models_dir.clone()
}

/// Returns an error if `model_name` is not in the allowlist of known model IDs.
fn validate_model_name(model_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if VALID_MODEL_NAMES.contains(&model_name) {
Ok(())
} else {
Err(format!("Unknown model '{}'", model_name).into())
}
}
Comment on lines +39 to +46
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

validate_model_name() is only used by delete_model, but other model_name entry points (e.g., download_model, get_model_path, is_model_downloaded, get_model_file_size) still accept arbitrary strings and build paths/URLs via model_filename(). Because model_filename() does not sanitize separators/.., a compromised frontend could potentially trigger path traversal or read/write outside models_dir. Consider enforcing the allowlist for all public methods that accept model_name (either by validating at the start of each method, or by making model_filename() return a Result and requiring validation before constructing paths).

Copilot uses AI. Check for mistakes.

/// Returns the filename for a given model name.
/// Maps display names to actual HuggingFace filenames.
fn model_filename(model_name: &str) -> String {
Expand Down Expand Up @@ -53,6 +73,29 @@ impl ModelManager {
.exists()
}

/// Deletes a downloaded model file. Returns an error if the model is not downloaded.
pub fn delete_model(&self, model_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Self::validate_model_name(model_name)?;
let path = self.models_dir.join(Self::model_filename(model_name));
if path.exists() {
// Verify the resolved path is still within models_dir to prevent path traversal.
let canonical_path = path.canonicalize()?;
if !canonical_path.starts_with(&self.models_dir) {
return Err("Path traversal attempt detected".into());
}
std::fs::remove_file(&canonical_path)?;
Ok(())
} else {
Err(format!("Model '{}' is not downloaded", model_name).into())
}
}

/// Returns the file size in bytes for a downloaded model, or None if not downloaded.
pub fn get_model_file_size(&self, model_name: &str) -> Option<u64> {
let path = self.models_dir.join(Self::model_filename(model_name));
std::fs::metadata(&path).ok().map(|m| m.len())
}

/// Downloads a model from Hugging Face, emitting progress events to the frontend.
pub async fn download_model(
&self,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "OratioText",
"version": "1.0.4",
"version": "1.0.5",
"identifier": "com.oratiotext.app",
"build": {
"frontendDist": "../src"
Expand Down
18 changes: 18 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,24 @@ <h1>OratioText</h1>
</div>
</div>
</section>

<section class="card models-manager-card">
<div class="models-manager-header">
<div style="display: flex; align-items: center; gap: 8px;">
<label class="section-label" style="margin-bottom: 0;">Downloaded Models</label>
<button id="open-models-dir-btn" class="btn btn-secondary btn-sm" style="padding: 2px 6px; display: flex; align-items: center; justify-content: center;" title="Open models directory in file manager" aria-label="Open models directory in file manager">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-folder-open">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 19l2.757 -7.351a1 1 0 0 1 .936 -.649h12.307a1 1 0 0 1 .986 1.164l-.996 5.211a2 2 0 0 1 -1.964 1.625h-14.026a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l2 2h5a2 2 0 0 1 2 2v1" />
</svg>
</button>
</div>
<span id="models-total-size" class="models-total-size"></span>
</div>
<div id="downloaded-models-list" class="downloaded-models-list">
<p class="placeholder-text" id="models-loading-text">Loading...</p>
</div>
</section>
</main>

<main class="app-main hidden" id="page-about">
Expand Down
Loading
Loading