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
32 changes: 16 additions & 16 deletions 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.7.1"
version = "0.7.2"
edition = "2021"
authors = ["VoxCtrl Contributors"]
license = "MIT"
Expand Down
52 changes: 51 additions & 1 deletion crates/voxctrl-update/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,45 @@ impl CheckOutcome {
pub async fn check(current_version: &str, gpu_build: bool) -> Result<CheckOutcome> {
let client = release::client()?;
let latest = release::fetch_latest(&client).await?;
Ok(evaluate(&latest, current_version, install::detect(gpu_build)))
let mut outcome = evaluate(&latest, current_version, install::detect(gpu_build));
if let CheckOutcome::Available(pending) = &mut outcome {
// Best effort: if the list cannot be fetched, the latest release's own
// notes (already in place) are still shown.
if let Ok(all) = release::fetch_recent(&client).await {
if let Some(notes) = combined_notes(&all, current_version, &latest.tag_name) {
pending.info.notes = notes;
}
}
}
Ok(outcome)
}

/// The notes of every published release newer than `current_version` up to and
/// including `latest_tag`, newest first, each under its tag.
pub fn combined_notes(releases: &[Release], current_version: &str, latest_tag: &str) -> Option<String> {
let mut between: Vec<&Release> = releases
.iter()
.filter(|r| !r.draft && !r.prerelease)
.filter(|r| version::is_newer(&r.tag_name, current_version))
.filter(|r| !version::is_newer(&r.tag_name, latest_tag))
.collect();
if between.is_empty() {
return None;
}
between.sort_by(|a, b| Version::parse(&b.tag_name).cmp(&Version::parse(&a.tag_name)));

let sections: Vec<String> = between
.iter()
.map(|r| {
let body = release::summarize_notes(r.body.as_deref().unwrap_or_default(), NOTES_BUDGET);
if body.is_empty() {
format!("## {}", r.tag_name)
} else {
format!("## {}\n{}", r.tag_name, body)
}
})
.collect();
Some(sections.join("\n\n"))
}

/// The decision half of [`check`], with the network and the machine both passed
Expand Down Expand Up @@ -340,6 +378,18 @@ mod tests {
assert_eq!(pending.info.download_size, 98_000_000);
}

#[test]
fn notes_cover_every_release_between_current_and_latest() {
let mut older = release_with("v0.3.9", vec![]);
older.body = Some("old".into());
let mut mid = release_with("v0.3.11", vec![]);
mid.body = Some("mid".into());
let mut newest = release_with("v0.4.0", vec![]);
newest.body = Some("new".into());
let notes = combined_notes(&[newest, mid, older], "0.3.10", "v0.4.0").unwrap();
assert_eq!(notes, "## v0.4.0\nnew\n\n## v0.3.11\nmid");
}

#[test]
fn the_same_version_reports_up_to_date() {
let rel = release_with("v0.3.10", vec![]);
Expand Down
24 changes: 24 additions & 0 deletions crates/voxctrl-update/src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use serde::{Deserialize, Serialize};
pub const RELEASES_API_URL: &str =
"https://api.github.com/repos/JRufer/VoxCtrl/releases/latest";

/// The most recent releases, for assembling a change log across versions.
pub const RELEASES_LIST_URL: &str =
"https://api.github.com/repos/JRufer/VoxCtrl/releases?per_page=30";

/// Where a user is sent when VoxCtrl cannot update itself.
pub const RELEASES_PAGE_URL: &str = "https://github.com/JRufer/VoxCtrl/releases/latest";

Expand Down Expand Up @@ -116,6 +120,26 @@ pub async fn fetch_latest_from(client: &reqwest::Client, url: &str) -> Result<Re
parse_release(&body)
}

/// Recent published releases, newest first, so the notes for every version
/// between the running one and the latest can be shown together.
pub async fn fetch_recent(client: &reqwest::Client) -> Result<Vec<Release>> {
let response = client
.get(RELEASES_LIST_URL)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.send()
.await
.map_err(|e| UpdateError::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(UpdateError::Response(format!("HTTP {}", response.status())));
}
let body = response
.text()
.await
.map_err(|e| UpdateError::Network(e.to_string()))?;
serde_json::from_str::<Vec<Release>>(&body).map_err(|e| UpdateError::Response(e.to_string()))
}

/// Parse a release payload, kept separate from the transport so the shape of
/// what GitHub sends can be tested without a network.
pub fn parse_release(body: &str) -> Result<Release> {
Expand Down
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ user triggered it from Settings → General or the update window.

#### `check_for_update() → UpdateCheckPayload`
Asks GitHub for the latest published release and compares it with the running
version. Also resolves which release file matches this installation, so
version, and gathers the release notes of every release in between into `notes`. Also resolves which release file matches this installation, so
`install_update` does not have to fetch anything twice.

```typescript
interface UpdateInfo {
version: string; // "0.4.0"
tag: string; // "v0.4.0"
current_version: string; // the version running now
notes: string; // release notes, trimmed for a dialog
notes: string; // notes for each release newer than yours, up to the latest, newest first
release_url: string;
asset_name: string | null; // the file that would be installed
download_size: number; // bytes
Expand Down
8 changes: 5 additions & 3 deletions docs/privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ update check:

| Trigger | Destination | Sends |
|---|---|---|
| Pressing "Check for updates" in Settings → General | `api.github.com/repos/JRufer/VoxCtrl/releases/latest` | A `User-Agent` of `VoxCtrl/<version>`. Nothing else. |
| Pressing "Check for updates" in Settings → General | `api.github.com/repos/JRufer/VoxCtrl/releases/latest`, plus `.../releases?per_page=30` when a newer version exists (for the change log) | A `User-Agent` of `VoxCtrl/<version>`. Nothing else. |
| Installing an offered update | `github.com` release download (the file and its `.minisig` signature) | Nothing beyond the request for the files |
| Downloading a speech model | HuggingFace / the model host, on demand | Nothing beyond the request for the file |
| Downloading a TTS voice | HuggingFace / the Piper voice host, on demand | Nothing beyond the request for the file |
Expand All @@ -136,7 +136,9 @@ update check:
VoxCtrl never checks for updates on its own — there is no launch-time check
and nothing to turn off. Pressing "Check for updates" in Settings → General
sends a plain unauthenticated `GET` for the public release listing — the same
URL anyone can open in a browser. There is no request body, no cookie, no
URL anyone can open in a browser. When a newer version exists, one more `GET`
for the recent release list fetches the change log for every version between
yours and the latest. There is no request body, no cookie, no
account, no install ID, and no way for it to carry one: GitHub is told which
version of VoxCtrl is asking (because the API requires a `User-Agent`) and
nothing more. What comes back is the release's tag, notes and file list, which
Expand All @@ -155,7 +157,7 @@ a package manager and points you at it instead of overwriting them. See
Once the app and its models are on disk, VoxCtrl runs fully air-gapped.

**The code:** `crates/voxctrl-update/` is the whole of it — about 400 lines,
with no dependency on the rest of the app. `release.rs` builds the one request,
with no dependency on the rest of the app. `release.rs` builds the (at most two) requests,
`apply.rs` downloads and checks the digest, `signature.rs` checks the release
signature, `src-tauri/src/updater.rs` is the manual
check command and what it shows.
Expand Down
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 docs/screenshots/settings-general-updates.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 docs/screenshots/update-window.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 docs/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The main configuration interface. Organized into a sidebar with eleven tabs:
### General Tab
- "Open setup wizard" button — re-runs the first-run wizard
- **HuggingFace access token** — the single field for the token used by every gated model VoxCtrl can download (Pocket-TTS, Breeze-TTS-2, VoxCPM2); read-only, showing the value, when `HF_TOKEN` is exported. Writes `tts.hf_token`, the same field the setup wizard's voice step edits, so a token entered in either place shows up in the other.
- "Check for updates" button that reports the result inline (VoxCtrl never checks on its own)
- A single "Check for updates" button (VoxCtrl never checks on its own). Up to date: a green "✔ Up to date" pill appears beside the Updates heading and no window opens. A newer release: the Update window opens with the change log from the running version to the latest and a download link
- MCP server toggle, visual feedback toggle, and record timeout control
- Recording status indicator and word count
- Manual record/stop button
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "voxctrl",
"private": true,
"version": "0.7.1",
"version": "0.7.2",
"type": "module",
"scripts": {
"predev": "pkill -x voxctrl 2>/dev/null || true; fuser -k 5173/tcp 2>/dev/null || true",
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://schema.tauri.app/config/2",
"productName": "VoxCtrl",
"version": "0.7.1",
"version": "0.7.2",
"identifier": "ai.voxctrl.app",
"build": {
"frontendDist": "../dist",
Expand Down
Loading
Loading