diff --git a/.agents/skills/freeflow-release/SKILL.md b/.agents/skills/fluent-release/SKILL.md similarity index 78% rename from .agents/skills/freeflow-release/SKILL.md rename to .agents/skills/fluent-release/SKILL.md index ff196049..b5dd1618 100644 --- a/.agents/skills/freeflow-release/SKILL.md +++ b/.agents/skills/fluent-release/SKILL.md @@ -1,13 +1,13 @@ --- -name: freeflow-release -description: Prepare and publish FreeFlow app releases. Use when the user asks to release FreeFlow, prepare a new version, bump the FreeFlow version, write or update FreeFlow changelog entries, validate a FreeFlow semver release, create a vX.Y.Z tag, or publish a signed FreeFlow DMG through the repository GitHub Actions release workflow. +name: fluent-release +description: Prepare and publish Fluent app releases. Use when the user asks to release Fluent, prepare a new version, bump the Fluent version, write or update Fluent changelog entries, validate a Fluent semver release, create a vX.Y.Z tag, or publish a signed Fluent DMG through the repository GitHub Actions release workflow. --- -# FreeFlow Release +# Fluent Release ## Overview -Use this skill to prepare a FreeFlow release from the local repository. FreeFlow releases are semver-tag driven: pushing a tag like `v0.3.1` triggers `.github/workflows/release.yml`, which stamps the app bundle, extracts the matching `CHANGELOG.md` section, builds/signs/notarizes the DMG, and creates the GitHub Release. +Use this skill to prepare a Fluent release from the local repository. Fluent releases are semver-tag driven: pushing a tag like `v0.3.1` triggers `.github/workflows/release.yml`, which stamps the app bundle, extracts the matching `CHANGELOG.md` section, builds/signs/notarizes the DMG, and creates the GitHub Release. Every release prep must update `CHANGELOG.md` by comparing the previous public semver release with the current release target. The changelog section should describe the user-visible changes that shipped since the previous release, not just summarize the final release-prep commit. @@ -68,7 +68,7 @@ Every release prep must update `CHANGELOG.md` by comparing the previous public s 5. Validate locally before commit/tag: ```bash .github/scripts/changelog-section.sh - .agents/skills/freeflow-release/scripts/freeflow-release-check.sh + .agents/skills/fluent-release/scripts/fluent-release-check.sh git diff --check make clean make ARCH="$(uname -m)" CODESIGN_IDENTITY=- @@ -90,10 +90,10 @@ Every release prep must update `CHANGELOG.md` by comparing the previous public s 8. After GitHub Actions finishes, verify: - GitHub Release `v` exists and is marked latest. - - `FreeFlow.dmg` is attached. + - `Fluent.dmg` is attached. - Release body starts with the matching `CHANGELOG.md` section. - A previous app version detects the update and shows What’s New. ## Helper Script -Run `.agents/skills/freeflow-release/scripts/freeflow-release-check.sh ` from the FreeFlow repo root to check release preconditions. It validates semver shape, required files, changelog extraction, workflow trigger basics, and tag availability. +Run `.agents/skills/fluent-release/scripts/fluent-release-check.sh ` from the Fluent repo root to check release preconditions. It validates semver shape, required files, changelog extraction, workflow trigger basics, and tag availability. diff --git a/.agents/skills/fluent-release/agents/openai.yaml b/.agents/skills/fluent-release/agents/openai.yaml new file mode 100644 index 00000000..d737a2da --- /dev/null +++ b/.agents/skills/fluent-release/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fluent Release" + short_description: "Prepare and publish Fluent semver releases." + default_prompt: "Prepare a new Fluent release." diff --git a/.agents/skills/freeflow-release/scripts/freeflow-release-check.sh b/.agents/skills/fluent-release/scripts/fluent-release-check.sh similarity index 85% rename from .agents/skills/freeflow-release/scripts/freeflow-release-check.sh rename to .agents/skills/fluent-release/scripts/fluent-release-check.sh index 3c043dc0..5b874354 100755 --- a/.agents/skills/freeflow-release/scripts/freeflow-release-check.sh +++ b/.agents/skills/fluent-release/scripts/fluent-release-check.sh @@ -47,21 +47,21 @@ if ! grep -q 'tags:' .github/workflows/release.yml || ! grep -q 'v\*\.\*\.\*' .g exit 1 fi -if ! .github/scripts/changelog-section.sh "$version" >/tmp/freeflow-release-notes.md; then +if ! .github/scripts/changelog-section.sh "$version" >/tmp/fluent-release-notes.md; then echo "Could not extract CHANGELOG.md section for $version" >&2 exit 1 fi -if [[ ! -s /tmp/freeflow-release-notes.md ]]; then +if [[ ! -s /tmp/fluent-release-notes.md ]]; then echo "Extracted changelog section is empty for $version" >&2 exit 1 fi -if ! grep -q "^## \\[$version\\]" /tmp/freeflow-release-notes.md; then +if ! grep -q "^## \\[$version\\]" /tmp/fluent-release-notes.md; then echo "Extracted changelog section has an unexpected heading." >&2 exit 1 fi echo "Release checks passed for $tag" echo -cat /tmp/freeflow-release-notes.md +cat /tmp/fluent-release-notes.md diff --git a/.agents/skills/freeflow-release/agents/openai.yaml b/.agents/skills/freeflow-release/agents/openai.yaml deleted file mode 100644 index 6f3c1200..00000000 --- a/.agents/skills/freeflow-release/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "FreeFlow Release" - short_description: "Prepare and publish FreeFlow semver releases." - default_prompt: "Prepare a new FreeFlow release." diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 6667c957..a9af6182 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -36,7 +36,7 @@ jobs: run: | plutil -replace CFBundleShortVersionString -string "${{ steps.version.outputs.version }}" Info.plist plutil -replace CFBundleVersion -string "${{ github.run_number }}" Info.plist - plutil -replace FreeFlowBuildTag -string "${{ steps.version.outputs.build_tag }}" Info.plist + plutil -replace FluentBuildTag -string "${{ steps.version.outputs.build_tag }}" Info.plist - name: Install build tools run: brew install create-dmg fileicon @@ -107,18 +107,18 @@ jobs: run: make dmg ARCH=universal CODESIGN_IDENTITY="$CODESIGN_IDENTITY" - name: Sign universal DMG - run: codesign --force --sign "$CODESIGN_IDENTITY" "build/FreeFlow Dev.dmg" + run: codesign --force --sign "$CODESIGN_IDENTITY" "build/Fluent Dev.dmg" - name: Notarize universal DMG run: | - xcrun notarytool submit "build/FreeFlow Dev.dmg" \ + xcrun notarytool submit "build/Fluent Dev.dmg" \ --keychain-profile "notarytool-profile" \ --keychain "$KEYCHAIN_PATH" \ --wait - xcrun stapler staple "build/FreeFlow Dev.dmg" + xcrun stapler staple "build/Fluent Dev.dmg" - name: Save universal DMG - run: mv "build/FreeFlow Dev.dmg" FreeFlow-Dev.dmg + run: mv "build/Fluent Dev.dmg" Fluent-Dev.dmg - name: Move dev tag run: | @@ -130,16 +130,16 @@ jobs: uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: tag_name: ${{ steps.version.outputs.tag }} - name: FreeFlow Dev ${{ steps.version.outputs.short_sha }} + name: Fluent Dev ${{ steps.version.outputs.short_sha }} body: | Development build from `${{ github.sha }}`. - **[FreeFlow-Dev.dmg](https://github.com/zachlatta/freeflow/releases/download/${{ steps.version.outputs.tag }}/FreeFlow-Dev.dmg)** - Universal dev build for all Macs (Apple Silicon + Intel). + **[Fluent-Dev.dmg](https://github.com/inhaq/fluent/releases/download/${{ steps.version.outputs.tag }}/Fluent-Dev.dmg)** - Universal dev build for all Macs (Apple Silicon + Intel). generate_release_notes: false make_latest: false prerelease: true files: | - FreeFlow-Dev.dmg + Fluent-Dev.dmg # --- Cleanup --- - name: Cleanup signing artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d5c1934..2e915ecb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: run: | plutil -replace CFBundleShortVersionString -string "${{ steps.version.outputs.version }}" Info.plist plutil -replace CFBundleVersion -string "${{ github.run_number }}" Info.plist - plutil -replace FreeFlowBuildTag -string "${{ steps.version.outputs.tag }}" Info.plist + plutil -replace FluentBuildTag -string "${{ steps.version.outputs.tag }}" Info.plist - name: Build release notes from changelog id: release_notes @@ -46,7 +46,7 @@ jobs: echo echo "## Download" echo - echo "**[FreeFlow.dmg](https://github.com/zachlatta/freeflow/releases/download/${{ steps.version.outputs.tag }}/FreeFlow.dmg)** — Universal build for all Macs (Apple Silicon + Intel)." + echo "**[Fluent.dmg](https://github.com/inhaq/fluent/releases/download/${{ steps.version.outputs.tag }}/Fluent.dmg)** — Universal build for all Macs (Apple Silicon + Intel)." echo "EOF" } >> "$GITHUB_OUTPUT" @@ -113,36 +113,36 @@ jobs: # --- Build universal --- - name: Build universal - run: make ARCH=universal APP_NAME=FreeFlow BUNDLE_ID=com.zachlatta.freeflow CODESIGN_IDENTITY="$CODESIGN_IDENTITY" + run: make ARCH=universal APP_NAME=Fluent BUNDLE_ID=com.inhaq.fluent CODESIGN_IDENTITY="$CODESIGN_IDENTITY" - name: Create universal DMG - run: make dmg ARCH=universal APP_NAME=FreeFlow BUNDLE_ID=com.zachlatta.freeflow CODESIGN_IDENTITY="$CODESIGN_IDENTITY" + run: make dmg ARCH=universal APP_NAME=Fluent BUNDLE_ID=com.inhaq.fluent CODESIGN_IDENTITY="$CODESIGN_IDENTITY" - name: Sign universal DMG - run: codesign --force --sign "$CODESIGN_IDENTITY" build/FreeFlow.dmg + run: codesign --force --sign "$CODESIGN_IDENTITY" build/Fluent.dmg - name: Notarize universal DMG run: | - xcrun notarytool submit build/FreeFlow.dmg \ + xcrun notarytool submit build/Fluent.dmg \ --keychain-profile "notarytool-profile" \ --keychain "$KEYCHAIN_PATH" \ --wait - xcrun stapler staple build/FreeFlow.dmg + xcrun stapler staple build/Fluent.dmg - name: Save universal DMG - run: mv build/FreeFlow.dmg FreeFlow.dmg + run: mv build/Fluent.dmg Fluent.dmg # --- Release --- - name: Create Release uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: tag_name: ${{ steps.version.outputs.tag }} - name: FreeFlow ${{ steps.version.outputs.version }} + name: Fluent ${{ steps.version.outputs.version }} body: ${{ steps.release_notes.outputs.body }} generate_release_notes: false make_latest: true files: | - FreeFlow.dmg + Fluent.dmg # --- Cleanup --- - name: Cleanup signing artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9988bca9..88ee2eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to FreeFlow are documented here. +All notable changes to Fluent are documented here. This project uses semantic versioning for public releases. Use `MAJOR.MINOR.PATCH`, where: @@ -8,7 +8,22 @@ This project uses semantic versioning for public releases. Use `MAJOR.MINOR.PATC - `MINOR` changes add user-visible features and improvements. - `PATCH` changes fix bugs, polish existing behavior, or make small internal improvements. -## [1.1.0] - 2026-06-03 +## [1.2.0] - 2026-06-26 + +The project is now **Fluent**, a faster, more token-efficient fork of Free Flow. This release rebrands the app and trims the dictation pipeline so each dictation uses fewer tokens and completes more quickly, without changing the core experience. + +### Changed + +- Renamed the app and project from FreeFlow to Fluent, including the bundle identifier (`com.inhaq.fluent`), app menus, permission prompts, build tooling, and the project website. + +### Improved + +- Plain dictation now skips screenshot capture entirely and sends only lightweight app and window metadata to the context model, removing the most expensive step from the common dictation path and saving the image tokens it used to spend. +- Screenshots, when still needed (such as in Edit Mode), are captured at a smaller default dimension (768px), JPEG-compressed, and cropped of surrounding whitespace before upload, lowering image token usage. +- The default cleanup model runs with low reasoning effort and reasoning output disabled, reducing token overhead and latency. +- Reasoning blocks are always stripped from model output so reasoning-oriented models stay fast and never paste their internal scratch work. + + ### Added @@ -31,7 +46,7 @@ This project uses semantic versioning for public releases. Use `MAJOR.MINOR.PATC ## [1.0.0] - 2026-05-20 -FreeFlow is now considered feature-complete and stable enough for a 1.0 release. +Fluent is now considered feature-complete and stable enough for a 1.0 release. ### Added @@ -65,9 +80,9 @@ FreeFlow is now considered feature-complete and stable enough for a 1.0 release. ### Added - Output Language setting for automatically translating dictated text before it is pasted. -- Transcription Language setting for choosing the language FreeFlow listens for during dictation. -- Recording state flag file for external tools that need to know when FreeFlow is actively recording. -- Distinct FreeFlow Dev app and menu bar icons so development builds are easier to tell apart from release builds. +- Transcription Language setting for choosing the language Fluent listens for during dictation. +- Recording state flag file for external tools that need to know when Fluent is actively recording. +- Distinct Fluent Dev app and menu bar icons so development builds are easier to tell apart from release builds. ### Improved @@ -78,7 +93,7 @@ FreeFlow is now considered feature-complete and stable enough for a 1.0 release. ### Fixed - Fixed audio recording crashes caused by unexpected input formats, resampling, and upload-path conversion. -- Fixed cases where FreeFlow could silently fall back when the selected microphone was unavailable. +- Fixed cases where Fluent could silently fall back when the selected microphone was unavailable. - Fixed paste shortcuts on Colemak-DH and other non-QWERTY keyboard layouts. - Fixed output language handling when custom system prompts are enabled. @@ -99,7 +114,7 @@ FreeFlow is now considered feature-complete and stable enough for a 1.0 release. - A voice command for submitting text: say "press enter" at the end of a dictation. - Audio controls that can mute or pause other audio while you dictate, then restore it when recording stops. - Build details in Settings for easier troubleshooting. -- Direct shortcuts from FreeFlow to the right macOS permission settings. +- Direct shortcuts from Fluent to the right macOS permission settings. - A What’s New popup when an update is available. ### Improved @@ -109,9 +124,9 @@ FreeFlow is now considered feature-complete and stable enough for a 1.0 release. - Exported run logs include more useful context for reproducing issues. - Realtime transcription is more reliable when recordings are cancelled, retried, or finish with no text. - Provider settings are easier to edit without accidental whitespace or half-saved values. -- FreeFlow now warns you if alert sounds may be hard to hear because system audio is muted or very low. +- Fluent now warns you if alert sounds may be hard to hear because system audio is muted or very low. - Update prompts now show the version, release date, and release notes more clearly. -- FreeFlow now uses proper version numbers for updates instead of internal build names. +- Fluent now uses proper version numbers for updates instead of internal build names. ### Fixed diff --git a/FreeFlow.entitlements b/Fluent.entitlements similarity index 100% rename from FreeFlow.entitlements rename to Fluent.entitlements diff --git a/Info.plist b/Info.plist index 95a273d5..f1d5d558 100644 --- a/Info.plist +++ b/Info.plist @@ -3,11 +3,11 @@ CFBundleName - FreeFlow + Fluent CFBundleDisplayName - FreeFlow + Fluent CFBundleIdentifier - com.zachlatta.freeflow + com.inhaq.fluent CFBundleVersion 0.3.0 CFBundleShortVersionString @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleExecutable - FreeFlow + Fluent CFBundleIconFile AppIcon LSMinimumSystemVersion @@ -23,10 +23,10 @@ LSUIElement NSMicrophoneUsageDescription - FreeFlow needs microphone access to transcribe your speech. + Fluent needs microphone access to transcribe your speech. NSSpeechRecognitionUsageDescription - FreeFlow needs speech recognition to convert your voice to text. + Fluent needs speech recognition to convert your voice to text. NSAccessibilityUsageDescription - FreeFlow needs accessibility access to detect the text cursor position and paste transcribed text. + Fluent needs accessibility access to detect the text cursor position and paste transcribed text. diff --git a/Makefile b/Makefile index 9712a07c..540b35f6 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -APP_NAME ?= FreeFlow Dev -BUNDLE_ID ?= com.zachlatta.freeflow.dev +APP_NAME ?= Fluent Dev +BUNDLE_ID ?= com.inhaq.fluent.dev BUILD_DIR = build APP_BUNDLE = $(BUILD_DIR)/$(APP_NAME).app -CODESIGN_IDENTITY ?= FreeFlow Dev +CODESIGN_IDENTITY ?= Fluent Dev CONTENTS = $(APP_BUNDLE)/Contents MACOS_DIR = $(CONTENTS)/MacOS empty := @@ -16,8 +16,8 @@ ARCH ?= $(shell uname -m) # Pick the icon source based on which bundle we are building. Dev builds get # a distinct hammer-on-waveform icon so a developer's dock shows at a glance -# which FreeFlow they are running when both are installed side by side. -ifeq ($(APP_NAME),FreeFlow Dev) +# which Fluent they are running when both are installed side by side. +ifeq ($(APP_NAME),Fluent Dev) ICON_SOURCE = Resources/AppIcon-Dev-Source.png ICON_ICNS = Resources/AppIcon-Dev.icns else @@ -65,7 +65,7 @@ endif @plutil -replace NSMicrophoneUsageDescription -string "$(APP_NAME) needs microphone access to transcribe your speech." "$(CONTENTS)/Info.plist" @plutil -replace NSSpeechRecognitionUsageDescription -string "$(APP_NAME) needs speech recognition to convert your voice to text." "$(CONTENTS)/Info.plist" @plutil -replace NSAccessibilityUsageDescription -string "$(APP_NAME) needs accessibility access to detect the text cursor position and paste transcribed text." "$(CONTENTS)/Info.plist" - @codesign --force --options runtime --sign "$(CODESIGN_IDENTITY)" --entitlements FreeFlow.entitlements "$(APP_BUNDLE)" + @codesign --force --options runtime --sign "$(CODESIGN_IDENTITY)" --entitlements Fluent.entitlements "$(APP_BUNDLE)" @echo "Built $(APP_BUNDLE)" icon: $(ICON_ICNS) diff --git a/README.md b/README.md index f7d389d9..81d0fb6c 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,54 @@

- FreeFlow icon + Fluent icon

-

FreeFlow

+

Fluent

- Free and open source alternative to Wispr Flow, Superwhisper, and Monologue. + A faster, leaner Mac dictation app. Free and open source alternative to Wispr Flow, Superwhisper, and Monologue.

- ⬇ Download FreeFlow.dmg
+ ⬇ Download Fluent.dmg
Works on all Macs (Apple Silicon + Intel)

---

- FreeFlow demo + Fluent demo

- Thank you to @marcbodea for maintaining FreeFlow! + Fluent is a faster, more token-efficient fork of Free Flow.

## Overview -FreeFlow is a free Mac dictation app inspired by [Wispr Flow](https://wisprflow.ai/), [Superwhisper](https://superwhisper.com/), and [Monologue](https://www.monologue.to/). It gives you fast AI transcription, context-aware cleanup, and voice-driven text editing without a monthly subscription. +Fluent is a free Mac dictation app for fast AI transcription, context-aware cleanup, and voice-driven text editing, with no monthly subscription. It started as a fork of [Free Flow](https://github.com/zachlatta/freeflow) and has been reworked to be **more efficient, to use fewer tokens per dictation, and to feel noticeably faster** end to end. + +## Why Fluent Is Faster and Cheaper + +Fluent keeps Free Flow's full feature set but trims the expensive parts of the dictation pipeline so you spend fewer tokens and wait less: + +- **No screenshot on plain dictation.** Capturing and uploading a screenshot to the vision context model is the single most expensive step of context collection. For ordinary dictation, the app and window metadata are enough, so Fluent skips the screenshot entirely. Image capture only happens for Edit Mode (and an explicit opt-in). This removes thousands of image tokens from the typical dictation and cuts a slow step out of the hot path. +- **Smaller, compressed screenshots when they are used.** When a screenshot is needed, Fluent sends it at a 768px default dimension instead of 1024px (about 44% fewer pixels, and image tokens scale with pixel area), JPEG-compresses it, and crops surrounding whitespace before upload. +- **A lean default cleanup model.** Cleanup runs on `openai/gpt-oss-20b` with reasoning effort set to low and reasoning output disabled, so you pay for the cleaned text instead of a long hidden chain of thought. +- **Reasoning tokens never leak through.** Any ``-style reasoning blocks are always stripped from model output, so reasoning-heavy models stay fast and never paste their scratch work. + +The result is the same dictation quality with a smaller token bill and lower latency on every dictation. ## Quick Start -1. Download the app from above or [click here](https://github.com/zachlatta/freeflow/releases/latest/download/FreeFlow.dmg) +1. Download the app from above or [click here](https://github.com/inhaq/fluent/releases/latest/download/Fluent.dmg) 2. Get a free Groq API key from [groq.com](https://groq.com/) 3. Hold `Fn` to talk, or tap `Command-Fn` to start and stop dictation, and have whatever you say pasted into the current text field ## Features - **Custom shortcuts:** Customize both hold-to-talk and toggle dictation shortcuts. If your toggle shortcut extends your hold shortcut, you can start in hold mode and press the extra modifier keys to latch into tap mode without stopping the recording. -- **Context-aware cleanup:** FreeFlow can read nearby app context so names, terms, and phrases are spelled correctly when you dictate into email, terminals, docs, and other apps. -- **Custom vocabulary:** Add names, jargon, and project-specific words that FreeFlow should preserve during cleanup. +- **Context-aware cleanup:** Fluent can read nearby app context so names, terms, and phrases are spelled correctly when you dictate into email, terminals, docs, and other apps. +- **Custom vocabulary:** Add names, jargon, and project-specific words that Fluent should preserve during cleanup. - **OpenAI-compatible providers:** Use Groq by default, or configure a custom model and API URL in settings. ## Edit Mode @@ -46,7 +57,7 @@ Edit Mode lets you highlight existing text and transform it with a spoken instru ## Privacy -There is no FreeFlow server, so FreeFlow does not store or retain your data. The only information that leaves your computer are API calls to your configured transcription and LLM provider. +There is no Fluent server, so Fluent does not store or retain your data. The only information that leaves your computer are API calls to your configured transcription and LLM provider. ## Custom Cleanup @@ -78,19 +89,19 @@ Then your response would be ONLY the cleaned up text, so here your response is O ## Using a Local Model -FreeFlow can use OpenAI-compatible local or self-hosted providers instead of Groq. In settings, configure the API base URL and model IDs for your local LLM provider, such as Ollama, LM Studio, or another OpenAI-compatible server. If your transcription backend uses a different endpoint from your LLM backend, set the transcription API URL separately. +Fluent can use OpenAI-compatible local or self-hosted providers instead of Groq. In settings, configure the API base URL and model IDs for your local LLM provider, such as Ollama, LM Studio, or another OpenAI-compatible server. If your transcription backend uses a different endpoint from your LLM backend, set the transcription API URL separately. Local models are often slower than hosted providers, especially on cold start, long recordings, or busy hardware.
Configure longer timeouts for local models - FreeFlow keeps the default network timeout at 20 seconds, but you can extend it with macOS defaults: + Fluent keeps the default network timeout at 20 seconds, but you can extend it with macOS defaults: ```bash -defaults write com.zachlatta.freeflow transcription_timeout_seconds -float 120 -defaults write com.zachlatta.freeflow post_processing_timeout_seconds -float 120 -defaults write com.zachlatta.freeflow context_request_timeout_seconds -float 120 +defaults write com.inhaq.fluent transcription_timeout_seconds -float 120 +defaults write com.inhaq.fluent post_processing_timeout_seconds -float 120 +defaults write com.inhaq.fluent context_request_timeout_seconds -float 120 ``` The timeout keys are: @@ -102,13 +113,17 @@ The timeout keys are: Only positive values are used. Remove a custom timeout to return to the 20-second default: ```bash -defaults delete com.zachlatta.freeflow transcription_timeout_seconds -defaults delete com.zachlatta.freeflow post_processing_timeout_seconds -defaults delete com.zachlatta.freeflow context_request_timeout_seconds +defaults delete com.inhaq.fluent transcription_timeout_seconds +defaults delete com.inhaq.fluent post_processing_timeout_seconds +defaults delete com.inhaq.fluent context_request_timeout_seconds ```
+## Credits + +Fluent is built on top of [Free Flow](https://github.com/zachlatta/freeflow) by [@zachlatta](https://github.com/zachlatta) and its contributors. Thank you for the foundation. + ## License Licensed under the MIT license. diff --git a/Sources/App.swift b/Sources/App.swift index 0d372ac2..4f7982d6 100644 --- a/Sources/App.swift +++ b/Sources/App.swift @@ -2,7 +2,7 @@ import AppKit import SwiftUI @main -struct FreeFlowApp: App { +struct FluentApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @AppStorage("show_menu_bar_icon") private var showMenuBarIcon = true diff --git a/Sources/AppName.swift b/Sources/AppName.swift index 5bb2bc87..6e3e6b90 100644 --- a/Sources/AppName.swift +++ b/Sources/AppName.swift @@ -2,5 +2,5 @@ import Foundation enum AppName { static let displayName: String = - Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "FreeFlow" + Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Fluent" } diff --git a/Sources/AppState.swift b/Sources/AppState.swift index 954ebf92..f1075993 100644 --- a/Sources/AppState.swift +++ b/Sources/AppState.swift @@ -8,7 +8,7 @@ import ScreenCaptureKit import Carbon import os import os.log -private let recordingLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "Recording") +private let recordingLog = OSLog(subsystem: "com.inhaq.fluent", category: "Recording") struct VoiceMacro: Codable, Identifiable, Equatable { var id: UUID = UUID() @@ -59,7 +59,7 @@ enum SettingsTab: String, CaseIterable, Identifiable { enum AppBuild { static var isDevBundle: Bool { - (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) == "FreeFlow Dev" + (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) == "Fluent Dev" } } @@ -1175,7 +1175,7 @@ final class AppState: ObservableObject, @unchecked Sendable { return audioDir } - /// URL of the flag file written while FreeFlow is actively recording. + /// URL of the flag file written while Fluent is actively recording. /// /// External tools (voice assistants, TTS barge-in pipelines, conversation /// apps) can poll this file to know when the user is dictating. The file @@ -1183,18 +1183,18 @@ final class AppState: ObservableObject, @unchecked Sendable { /// Contents are the UNIX timestamp (seconds, float) of when recording /// started — useful for stale-flag detection after an unclean exit. /// - /// Path: `~/Library/Application Support/FreeFlow/is-recording` - /// (or `FreeFlow Dev/is-recording` when running the dev bundle). + /// Path: `~/Library/Application Support/Fluent/is-recording` + /// (or `Fluent Dev/is-recording` when running the dev bundle). static func recordingStateFlagURL() -> URL { let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "FreeFlow" + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Fluent" return appSupport.appendingPathComponent("\(appName)/is-recording") } /// Serial queue that owns every flag-file I/O so the recording /// start/stop hot path never blocks on disk. private static let recordingStateFlagQueue = DispatchQueue( - label: "com.zachlatta.freeflow.recording-state-flag" + label: "com.inhaq.fluent.recording-state-flag" ) /// Write or clear the `is-recording` flag file. Called from the @@ -2333,14 +2333,14 @@ final class AppState: ObservableObject, @unchecked Sendable { private func beginCriticalDictationActivity() { guard !automaticTerminationDisabled else { return } - ProcessInfo.processInfo.disableAutomaticTermination("FreeFlow dictation in progress") + ProcessInfo.processInfo.disableAutomaticTermination("Fluent dictation in progress") automaticTerminationDisabled = true } private func endCriticalDictationActivity() { defer { prewarmMicrophoneIfEnabled() } guard automaticTerminationDisabled else { return } - ProcessInfo.processInfo.enableAutomaticTermination("FreeFlow dictation in progress") + ProcessInfo.processInfo.enableAutomaticTermination("Fluent dictation in progress") automaticTerminationDisabled = false } diff --git a/Sources/AudioRecorder.swift b/Sources/AudioRecorder.swift index 221b29b9..3fbc2391 100644 --- a/Sources/AudioRecorder.swift +++ b/Sources/AudioRecorder.swift @@ -4,7 +4,7 @@ import CoreMedia import Foundation import os.log -private let recordingLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "Recording") +private let recordingLog = OSLog(subsystem: "com.inhaq.fluent", category: "Recording") struct AudioDevice: Identifiable { let id: String @@ -80,8 +80,8 @@ final class AudioRecorder: NSObject, ObservableObject, AVCaptureAudioDataOutputS private let _bufferCount = OSAllocatedUnfairLock(initialState: 0) private let fileWriteErrorLock = OSAllocatedUnfairLock(initialState: ()) private var watchdogTimer: DispatchSourceTimer? - private let sessionQueue = DispatchQueue(label: "com.zachlatta.freeflow.capture.session") - private let sampleBufferQueue = DispatchQueue(label: "com.zachlatta.freeflow.capture.samples") + private let sessionQueue = DispatchQueue(label: "com.inhaq.fluent.capture.session") + private let sampleBufferQueue = DispatchQueue(label: "com.inhaq.fluent.capture.samples") private var activeAudioFile: AVAudioFile? private var activeAudioFormat: AVAudioFormat? private var recordedFrameCount: AVAudioFramePosition = 0 diff --git a/Sources/GlobalShortcutBackend.swift b/Sources/GlobalShortcutBackend.swift index 0f0b97bd..908af3ee 100644 --- a/Sources/GlobalShortcutBackend.swift +++ b/Sources/GlobalShortcutBackend.swift @@ -1,7 +1,7 @@ import Cocoa import os.log -private let shortcutLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "Shortcuts") +private let shortcutLog = OSLog(subsystem: "com.inhaq.fluent", category: "Shortcuts") enum GlobalShortcutBackendError: LocalizedError { case eventTapUnavailable diff --git a/Sources/KeychainStorage.swift b/Sources/KeychainStorage.swift index 80620716..64d0389a 100644 --- a/Sources/KeychainStorage.swift +++ b/Sources/KeychainStorage.swift @@ -2,7 +2,7 @@ import Foundation import Security enum AppSettingsStorage { - private static let bundleID = Bundle.main.bundleIdentifier ?? "com.zachlatta.freeflow" + private static let bundleID = Bundle.main.bundleIdentifier ?? "com.inhaq.fluent" private static var storageDirectory: URL { let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! diff --git a/Sources/NetworkMonitor.swift b/Sources/NetworkMonitor.swift index cb6e3b47..8909e239 100644 --- a/Sources/NetworkMonitor.swift +++ b/Sources/NetworkMonitor.swift @@ -9,7 +9,7 @@ final class NetworkMonitor { static let shared = NetworkMonitor() private let monitor = NWPathMonitor() - private let queue = DispatchQueue(label: "com.zachlatta.freeflow.network-monitor") + private let queue = DispatchQueue(label: "com.inhaq.fluent.network-monitor") private let lock = NSLock() private var started = false diff --git a/Sources/PipelineSignpost.swift b/Sources/PipelineSignpost.swift index f794031e..f9e950ae 100644 --- a/Sources/PipelineSignpost.swift +++ b/Sources/PipelineSignpost.swift @@ -11,13 +11,13 @@ import os /// -> transcript received -> paste /// /// Open Instruments with the "os_signpost" / "Points of Interest" instrument -/// (subsystem `com.zachlatta.freeflow`) to see the end-to-end latency and the +/// (subsystem `com.inhaq.fluent`) to see the end-to-end latency and the /// gaps between stages, rather than inferring them from log lines. enum PipelineSignpost { /// Category `PointsOfInterest` makes the intervals/events land on the /// dedicated Points of Interest track in Instruments. static let signposter = OSSignposter( - subsystem: "com.zachlatta.freeflow", + subsystem: "com.inhaq.fluent", category: "PointsOfInterest" ) diff --git a/Sources/RealtimeTranscriptionService.swift b/Sources/RealtimeTranscriptionService.swift index 6f6e0488..ce4bafa0 100644 --- a/Sources/RealtimeTranscriptionService.swift +++ b/Sources/RealtimeTranscriptionService.swift @@ -1,7 +1,7 @@ import Foundation import os.log -private let realtimeLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "RealtimeTranscription") +private let realtimeLog = OSLog(subsystem: "com.inhaq.fluent", category: "RealtimeTranscription") enum RealtimeTranscriptionError: LocalizedError { case invalidBaseURL(String) @@ -32,7 +32,7 @@ final class RealtimeTranscriptionService { private var task: URLSessionWebSocketTask? private var receiveTask: Task? - private let stateQueue = DispatchQueue(label: "com.zachlatta.freeflow.realtime.state") + private let stateQueue = DispatchQueue(label: "com.inhaq.fluent.realtime.state") private var finalText: String = "" private var partialText: String = "" // Coalesce appended PCM into ~100ms chunks before encoding/sending. Capture diff --git a/Sources/SettingsView.swift b/Sources/SettingsView.swift index 6674e09b..de071cb5 100644 --- a/Sources/SettingsView.swift +++ b/Sources/SettingsView.swift @@ -501,7 +501,7 @@ struct GeneralSettingsView: View { } private var appBuildNumber: String { - Bundle.main.object(forInfoDictionaryKey: "FreeFlowBuildTag") as? String + Bundle.main.object(forInfoDictionaryKey: "FluentBuildTag") as? String ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown" } @@ -911,7 +911,7 @@ struct GeneralSettingsView: View { } .pickerStyle(.menu) - Text("When set, FreeFlow translates your speech into the selected language.") + Text("When set, Fluent translates your speech into the selected language.") .font(.caption) .foregroundStyle(.secondary) } @@ -1560,7 +1560,7 @@ struct PromptsSettingsView: View { ) .toggleStyle(.switch) - Text("When enabled, FreeFlow retries or falls back to the literal transcript if post-processing looks like it answered the dictated text instead of cleaning it.") + Text("When enabled, Fluent retries or falls back to the literal transcript if post-processing looks like it answered the dictated text instead of cleaning it.") .font(.caption) .foregroundStyle(.secondary) } @@ -1585,7 +1585,7 @@ struct PromptsSettingsView: View { let context = AppContext( appName: "\(AppName.displayName) Settings", - bundleIdentifier: "com.zachlatta.freeflow", + bundleIdentifier: "com.inhaq.fluent", windowTitle: "System Prompt Test", selectedText: nil, currentActivity: "User is testing the system prompt in \(AppName.displayName) settings.", diff --git a/Sources/StreamingMultipartBody.swift b/Sources/StreamingMultipartBody.swift index 9f39eb9a..0cf8695b 100644 --- a/Sources/StreamingMultipartBody.swift +++ b/Sources/StreamingMultipartBody.swift @@ -1,7 +1,7 @@ import Foundation import os.log -private let uploadLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "Upload") +private let uploadLog = OSLog(subsystem: "com.inhaq.fluent", category: "Upload") /// Streams a `multipart/form-data` request body to `URLSession` without ever /// holding the whole body in memory *or* writing a temporary copy to disk. @@ -99,7 +99,7 @@ final class StreamingMultipartBody: NSObject, StreamDelegate { RunLoop.current.run(mode: .default, before: .distantFuture) } } - thread.name = "com.zachlatta.freeflow.upload.body" + thread.name = "com.inhaq.fluent.upload.body" self.thread = thread thread.start() } diff --git a/Sources/TestCaseExporter.swift b/Sources/TestCaseExporter.swift index 2af3b39d..f0725b30 100644 --- a/Sources/TestCaseExporter.swift +++ b/Sources/TestCaseExporter.swift @@ -40,7 +40,7 @@ struct TestCaseExporter { let timestamp = isoTimestamp(from: item.timestamp) let panel = NSSavePanel() panel.allowedContentTypes = [UTType.zip] - panel.nameFieldStringValue = "freeflow-case-\(timestamp).zip" + panel.nameFieldStringValue = "fluent-case-\(timestamp).zip" panel.title = "Export Test Case" panel.message = "Choose where to save the test case ZIP." panel.begin { response in @@ -75,7 +75,7 @@ struct TestCaseExporter { ) throws { let fm = FileManager.default let tempDir = fm.temporaryDirectory - .appendingPathComponent("freeflow-case-\(UUID().uuidString)", isDirectory: true) + .appendingPathComponent("fluent-case-\(UUID().uuidString)", isDirectory: true) defer { try? fm.removeItem(at: tempDir) } do { diff --git a/Sources/TranscriptionService.swift b/Sources/TranscriptionService.swift index fd7d0fed..60624572 100644 --- a/Sources/TranscriptionService.swift +++ b/Sources/TranscriptionService.swift @@ -1,7 +1,7 @@ import Foundation import os.log -private let transcriptionLog = OSLog(subsystem: "com.zachlatta.freeflow", category: "Transcription") +private let transcriptionLog = OSLog(subsystem: "com.inhaq.fluent", category: "Transcription") class TranscriptionService { private let apiKey: String diff --git a/Sources/UpdateManager.swift b/Sources/UpdateManager.swift index 2af430f2..dfce1c5d 100644 --- a/Sources/UpdateManager.swift +++ b/Sources/UpdateManager.swift @@ -167,7 +167,7 @@ final class UpdateManager: ObservableObject { set { UserDefaults.standard.set(newValue, forKey: "updateLastPostTranscriptionReminderDate") } } - private let releasesURL = URL(string: "https://api.github.com/repos/zachlatta/freeflow/releases?per_page=100")! + private let releasesURL = URL(string: "https://api.github.com/repos/inhaq/fluent/releases?per_page=100")! private let stabilityBufferDays: TimeInterval = 3 private let checkIntervalSeconds: TimeInterval = 7 * 24 * 60 * 60 // 7 days private let postTranscriptionReminderInterval: TimeInterval = 24 * 60 * 60 // 1 day @@ -216,7 +216,7 @@ final class UpdateManager: ObservableObject { @MainActor func checkForUpdates(userInitiated: Bool) async { - let currentBuildTag = Bundle.main.infoDictionary?["FreeFlowBuildTag"] as? String + let currentBuildTag = Bundle.main.infoDictionary?["FluentBuildTag"] as? String let currentVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" // Dev builds (no embedded tag): skip auto-checks, but allow manual checks @@ -717,7 +717,7 @@ final class UpdateManager: ObservableObject { private func performUpdate(downloadURL: URL, expectedSize: Int) async { let fm = FileManager.default - let tempDir = fm.temporaryDirectory.appendingPathComponent("freeflow-update-\(UUID().uuidString)") + let tempDir = fm.temporaryDirectory.appendingPathComponent("fluent-update-\(UUID().uuidString)") do { try fm.createDirectory(at: tempDir, withIntermediateDirectories: true) @@ -726,7 +726,7 @@ final class UpdateManager: ObservableObject { return } - let dmgPath = tempDir.appendingPathComponent("FreeFlow.dmg") + let dmgPath = tempDir.appendingPathComponent("Fluent.dmg") // MARK: Download phase updateStatus = .downloading @@ -828,7 +828,7 @@ final class UpdateManager: ObservableObject { } // Copy app to staging directory - let stagingDir = fm.temporaryDirectory.appendingPathComponent("freeflow-staged-\(UUID().uuidString)") + let stagingDir = fm.temporaryDirectory.appendingPathComponent("fluent-staged-\(UUID().uuidString)") try fm.createDirectory(at: stagingDir, withIntermediateDirectories: true) let stagedApp = stagingDir.appendingPathComponent(appBundle.lastPathComponent) try fm.copyItem(at: appBundle, to: stagedApp) diff --git a/website/README.md b/website/README.md index bb525ff5..51d94a70 100644 --- a/website/README.md +++ b/website/README.md @@ -1,6 +1,6 @@ -# freeflow.zachlatta.com +# fluent.inhaq.com -Static landing page for FreeFlow. +Static landing page for Fluent. ## Files @@ -9,8 +9,8 @@ Static landing page for FreeFlow. - `assets/demo.gif` - product demo shown above the fold. - `llms.txt` - concise project summary for AI agents and answer engines. - `robots.txt` - crawler policy and sitemap pointer. -- `sitemap.xml` - sitemap for `https://freeflow.zachlatta.com/`. +- `sitemap.xml` - sitemap for `https://fluent.inhaq.com/`. ## Deploy -Serve this directory as the web root for `https://freeflow.zachlatta.com/`. +Serve this directory as the web root for `https://fluent.inhaq.com/`. diff --git a/website/index.html b/website/index.html index d0cfe22d..665be629 100644 --- a/website/index.html +++ b/website/index.html @@ -3,76 +3,76 @@ - FreeFlow - Free Open Source Mac Dictation - + Fluent - Free Open Source Mac Dictation + - - - + + + - - - + + + - - - + + + - - + + - - - + + + - - - + + +