diff --git a/.gitattributes b/.gitattributes index ae050e3..ca18ae5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ *.targets text eol=crlf *.xaml text eol=crlf *.xml text eol=crlf +*.ps1 text eol=crlf *.md text eol=lf *.json text eol=lf *.yml text eol=lf diff --git a/.github/releases/v0.2.0.md b/.github/releases/v0.2.0.md new file mode 100644 index 0000000..e98f49e --- /dev/null +++ b/.github/releases/v0.2.0.md @@ -0,0 +1,34 @@ +# TextRecast v0.2.0 + +TextRecast 0.2.0 adds explicit, user-controlled local model selection. + +## Highlights + +- Shows four local model choices before any download begins. +- Displays role, English-only status, download size, hardware compatibility, and known limitations. +- Downloads only the exact model the user confirms. +- Reuses a valid installed selection on later launches. +- Keeps resumable downloads and exact size and SHA-256 verification. +- Uses model-specific prompt and sampling profiles for Qwen 2.5, Qwen 3.5, and Granite 4.1. +- Keeps the active model and its limitation note available from the launcher menu. + +## Included choices + +| Role | Model | Download | Status | +| --- | --- | ---: | --- | +| Fast/default | Qwen 2.5 1.5B Q4_K_M | 1.04 GiB | Established default | +| Balanced | Qwen 3.5 2B Q5_K_M | 1.34 GiB | Experimental | +| Best quality | Qwen 3.5 4B Q5_K_M | 2.93 GiB | Experimental | +| Alternative | Granite 4.1 3B Q5_K_M | 2.27 GiB | Experimental | + +All four choices are currently supported for English rewriting only. Local models can change meaning or details; review the replaced text in the source application after every operation. The setup screen describes the specific risks observed for each experimental model. + +## Install + +1. Download `TextRecast-v0.2.0-win-x64.zip` and its `.zip.sha256` file. +2. Verify the checksum if desired, then extract the complete ZIP. +3. Run `TextRecast.exe` and confirm one compatible model during setup. + +The application is self-contained, so .NET 10 does not need to be installed. The Microsoft Visual C++ x64 Redistributable may still be required. The release ZIP contains no GGUF model and includes the legal notices supplied with its bundled .NET runtime packs. + +Existing valid Qwen 2.5 installations are detected and can be reused after confirmation without downloading the model again. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0595bbc..7eb104d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,7 +88,10 @@ jobs: "TextRecast.exe", "LICENSE", "NOTICE", - "THIRD-PARTY-NOTICES.md" + "THIRD-PARTY-NOTICES.md", + "DOTNET-LICENSE.txt", + "DOTNET-THIRD-PARTY-NOTICES.txt", + "WPF-LICENSE.txt" ) foreach ($file in $requiredFiles) { if (-not (Test-Path -LiteralPath (Join-Path $publishDirectory $file) -PathType Leaf)) { diff --git a/Directory.Build.props b/Directory.Build.props index d7c591e..18efe00 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.1 + 0.2.0 Copyright 2026 snss10 Apache-2.0 false diff --git a/Models/README.md b/Models/README.md index ea36203..c42f251 100644 --- a/Models/README.md +++ b/Models/README.md @@ -1,32 +1,48 @@ -# Default local SLM +# Supported local models -TextRecast currently uses the following default model profile: +TextRecast runs one cataloged GGUF model at a time. During setup, the user reviews the available choices and confirms the exact model before any download starts. Release archives do not contain model binaries. -| Property | Value | -| --- | --- | -| Model | `Qwen2.5-1.5B-Instruct-GGUF` | -| File | `qwen2.5-1.5b-instruct-q4_k_m.gguf` | -| Quantization | `Q4_K_M` | -| Size | `1,117,320,736` bytes | -| SHA-256 | `6a1a2eb6d15622bf3c96857206351ba97e1af16c30d7a74ee38970e434e9407e` | -| Source | [Qwen/Qwen2.5-1.5B-Instruct-GGUF](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF) | -| License | Apache License 2.0 | +All current profiles are for English rewriting. The three newer choices are experimental, and every replacement should be reviewed in the source application. -The model binary is intentionally ignored by Git. When no packaged model is present, TextRecast downloads and verifies it automatically, then stores it at: +| Role | Model file | Size | SHA-256 | +| --- | --- | ---: | --- | +| Fast/default | `qwen2.5-1.5b-instruct-q4_k_m.gguf` | 1,117,320,736 bytes | `6a1a2eb6d15622bf3c96857206351ba97e1af16c30d7a74ee38970e434e9407e` | +| Balanced | `Qwen3.5-2B-Q5_K_M.gguf` | 1,435,238,656 bytes | `1885b3a9195f8cc09da9a7a7a75afdc1e8d5cbf9fc4a499c3961dddea37098ac` | +| Best quality | `Qwen3.5-4B-Q5_K_M.gguf` | 3,143,656,608 bytes | `8814232b85594dcd46c50e5b8b29324a7efe9e746edbe8a3d1df3d3fce7aad39` | +| Alternative | `granite-4.1-3b-Q5_K_M.gguf` | 2,437,012,064 bytes | `f7724d259f29b0edf147144ac530ca26f91c97af8274249f933073c461678a3c` | + +## Pinned sources + +| Model | Repository and revision | License | +| --- | --- | --- | +| Qwen 2.5 1.5B Q4_K_M | [`Qwen/Qwen2.5-1.5B-Instruct-GGUF@dd26da4`](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/tree/dd26da440ef0330c47919d1ecae0966d24022222) | Apache-2.0 | +| Qwen 3.5 2B Q5_K_M | [`unsloth/Qwen3.5-2B-GGUF@f6d5376`](https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/tree/f6d5376be1edb4d416d56da11e5397a961aca8ae) | Apache-2.0 | +| Qwen 3.5 4B Q5_K_M | [`unsloth/Qwen3.5-4B-GGUF@e87f176`](https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/tree/e87f176479d0855a907a41277aca2f8ee7a09523) | Apache-2.0 | +| Granite 4.1 3B Q5_K_M | [`ibm-granite/granite-4.1-3b-GGUF@ab47014`](https://huggingface.co/ibm-granite/granite-4.1-3b-GGUF/tree/ab4701481089b58a082ef63cc1cee738887293ff) | Apache-2.0 | + +The model binaries remain subject to their upstream terms and are not relicensed as part of TextRecast. Review the linked model cards and licenses before redistributing any binary. + +## Storage and verified downloads + +Downloaded models are stored for the current Windows user under: ```text -%LOCALAPPDATA%\TextRecast\Models\qwen2.5-1.5b-instruct-q4_k_m.gguf +%LOCALAPPDATA%\TextRecast\Models ``` -Interrupted downloads use stable files beside the final model: +An interrupted download uses two temporary files beside its final destination: ```text -qwen2.5-1.5b-instruct-q4_k_m.gguf.partial -qwen2.5-1.5b-instruct-q4_k_m.gguf.partial.metadata.json +.partial +.partial.metadata.json ``` -TextRecast sends HTTP Range and If-Range requests to continue a valid partial download. It restarts when the server ignores the range, reports a mismatched range, or exposes a changed validator. Temporary HTTP failures are retried with bounded backoff. The final file is moved into place only after its exact size and SHA-256 checksum are verified; untrusted partial data is removed. +TextRecast uses HTTP Range and If-Range to resume compatible partial downloads. It restarts when the server ignores the range, returns inconsistent metadata, or exposes a changed validator. The final file is installed only after its exact size and SHA-256 pass verification. + +## Offline or portable builds + +Place only exact cataloged GGUF files in this directory before publishing. The App project links `Models/*.gguf` into its output, and setup marks matching files as installed. Do not rename a model file or substitute another quantization without adding and testing a separate catalog profile. -For an offline or portable build, place the GGUF file in this repository directory before publishing. The App project links matching `Models/*.gguf` files into its output. +The normal GitHub Actions release intentionally fails if a GGUF file enters the self-contained ZIP. This keeps the public application download small and leaves the model choice with the user. -The default metadata is defined in [`SlmModelCatalog.cs`](../src/TextRecast.Infrastructure/SLM/SlmModelCatalog.cs). Add future model profiles there and review each upstream model card and license before distribution. +Catalog metadata is defined in [`SlmModelCatalog.cs`](../src/TextRecast.Infrastructure/SLM/SlmModelCatalog.cs). diff --git a/README.md b/README.md index 850d62f..eaccda1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ TextRecast is a lightweight Windows desktop utility that rewrites text inside the applications you already use. Select editable text, click the floating TextRecast button, choose an operation, and apply the result back to the original selection. -Inference runs locally through LLamaSharp's CPU backend. The default model is Qwen2.5-1.5B-Instruct, and the SLM layer is designed so additional local models can be added later. +Inference runs locally through LLamaSharp's CPU backend. On first setup, TextRecast presents four cataloged models and downloads only the option the user explicitly confirms. Qwen 2.5 1.5B remains the fast default; the other choices are clearly labeled experimental. ## Features @@ -34,6 +34,7 @@ Inference runs locally through LLamaSharp's CPU backend. The default model is Qw - Change tone to Professional, Casual, Friendly, Formal, or Direct - Work in browsers, editors, messaging apps, and other Windows applications - Run formatting locally without an account or API key +- Choose a compatible local model before any download begins - Resume an interrupted model download instead of starting over - Cancel an active generation and retry a failed replacement - Revalidate the source window and selected text before replacing anything @@ -42,16 +43,16 @@ Inference runs locally through LLamaSharp's CPU backend. The default model is Qw ### Requirements -- Windows 10 or Windows 11 on x64 hardware +- Windows 11 on x64 hardware, or a [Windows 10 edition still supported by .NET 10](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md) - The current [Microsoft Visual C++ v14 Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) for x64 -- Approximately 1.5 GB of available disk space +- Approximately 2-4 GB of available disk space, depending on the selected model - An internet connection when no valid packaged or previously installed model is available The published Windows x64 application is self-contained. Users running that build do **not** need to install .NET 10. ### Run a published build -1. Download `TextRecast-v0.1.1-win-x64.zip` from the [Releases page](https://github.com/snss10/TextRecast/releases). +1. Download the latest `TextRecast-vX.Y.Z-win-x64.zip` from the [Releases page](https://github.com/snss10/TextRecast/releases). 2. Optionally verify it with the accompanying `.zip.sha256` file. 3. Extract the entire archive to a folder. 4. Run `TextRecast.exe`. @@ -60,9 +61,18 @@ Keep every file from the archive together; the application depends on the includ ## First launch and model download -When no valid packaged or previously installed model is available, TextRecast downloads the 1.1 GB Qwen model on first launch. The setup window shows transferred size, speed, estimated time remaining, verification, and installation status. +TextRecast shows the available models before making a network request. Each choice includes its role, download size, hardware compatibility, language support, and known limitations. The exact model and size are shown again for confirmation; only that model is downloaded. -Cancelling or losing the connection keeps a validated partial download. Select **Retry** on the same computer to continue from the saved point. TextRecast restarts safely if the server no longer accepts the saved range or the remote file has changed. A model becomes active only after its exact size and SHA-256 checksum pass verification. +| Role | Model | Download | Status | +| --- | --- | ---: | --- | +| Fast/default | Qwen 2.5 1.5B Q4_K_M | 1.04 GiB | Established default; review all output | +| Balanced | Qwen 3.5 2B Q5_K_M | 1.34 GiB | Experimental; summaries need extra review | +| Best quality | Qwen 3.5 4B Q5_K_M | 2.93 GiB | Experimental; verify roles, titles, and deadlines | +| Alternative | Granite 4.1 3B Q5_K_M | 2.27 GiB | Experimental; higher semantic-drift risk | + +These profiles are currently supported for English rewriting only. None is perfect, so review the replaced text in the source application after every operation. Hardware recommendations are guidance; Qwen 2.5 remains selectable when hardware inspection is unavailable. + +Cancelling or losing the connection keeps a validated partial download. Select **Retry** on the same computer to continue from the saved point. TextRecast restarts safely if the server no longer accepts the saved range or the remote file has changed. A model becomes active only after its exact size and SHA-256 checksum pass verification. A valid installed selection is reused on later launches. The first formatting request loads the model into memory. Later requests reuse the loaded model and usually start faster. @@ -77,7 +87,7 @@ TextRecast stores the model for the current Windows user under: `%LOCALAPPDATA%` is a standard Windows environment variable. It expands automatically to the signed-in user's local application-data folder; it is not a path tied to the developer's computer. -For the exact filename, checksum, upstream source, and offline setup instructions, see [Models/README.md](Models/README.md). +For exact filenames, checksums, pinned upstream revisions, and offline setup instructions, see [Models/README.md](Models/README.md).
@@ -102,7 +112,7 @@ There are no author-specific absolute paths in this README. TextRecast returns focus to the source application, confirms that the original selection is still valid, and then replaces it. If replacement cannot be completed safely, the original text remains unchanged and the generated result remains available for **Retry replace**. -Drag the floating icon to reposition it. Right-click the icon and choose **Quit TextRecast** to close the application. +Drag the floating icon to reposition it. Right-click it to view the active model and its limitations, or choose **Quit TextRecast** to close the application. ## Privacy and safeguards @@ -116,7 +126,7 @@ Drag the floating icon to reposition it. Right-click the icon and choose **Quit Capture fallback and replacement temporarily use the Windows clipboard. Windows clipboard history, clipboard sync, or third-party clipboard managers may retain that content, so disable those features when working with sensitive text. -Network access is used to obtain the model only when a valid model is not already packaged or installed. After that download, normal formatting does not require a cloud service. +Network access begins only after the user confirms a model that is not already packaged or installed. After that download, normal formatting does not require a cloud service. ## How it works @@ -154,7 +164,7 @@ Install the latest x64 package from Microsoft's [supported Visual C++ Redistribu
The model download or integrity check failed -Check the internet connection and available disk space, then retry. TextRecast keeps a safe partial download after cancellation or a temporary network failure and resumes it automatically. Invalid partial data is discarded instead of being installed. Offline setup details and the expected checksum are in [Models/README.md](Models/README.md). +Check the internet connection and available disk space, then retry. TextRecast keeps a safe partial download after cancellation or a temporary network failure and resumes it automatically. Invalid partial data is discarded instead of being installed. Offline setup details and expected checksums are in [Models/README.md](Models/README.md).
@@ -192,9 +202,9 @@ dotnet run --project src/TextRecast.App/TextRecast.App.csproj dotnet publish src/TextRecast.App/TextRecast.App.csproj -c Release -r win-x64 --self-contained true -o artifacts/TextRecast-win-x64 ``` -Distribute the complete output folder, not only the executable. The exact cataloged GGUF file documented in [Models/README.md](Models/README.md) is packaged when it is placed in the repository's `Models` directory before publishing; otherwise TextRecast downloads the model for the current user on first launch. +Distribute the complete output folder, not only the executable. Any cataloged GGUF files documented in [Models/README.md](Models/README.md) are packaged when placed in the repository's `Models` directory before publishing. Normal release packages intentionally contain no model; the user chooses and confirms one during setup. -For tagged releases, GitHub Actions creates a model-free `TextRecast-vX.Y.Z-win-x64.zip` containing the complete runtime and legal notices, plus a matching SHA-256 file. A release tag must use `vMAJOR.MINOR.PATCH` and match the version in `Directory.Build.props`. +For tagged releases, GitHub Actions creates a model-free `TextRecast-vX.Y.Z-win-x64.zip` containing the complete runtime, TextRecast legal files, and the legal files supplied with the bundled .NET runtime packs, plus a matching SHA-256 file. A release tag must use `vMAJOR.MINOR.PATCH` and match the version in `Directory.Build.props`. ## Project layout @@ -202,7 +212,7 @@ For tagged releases, GitHub Actions creates a model-free `TextRecast-vX.Y.Z-win- - [TextRecast.Core](src/TextRecast.Core/) contains platform-independent workflows, contracts, results, and models. - [TextRecast.Infrastructure](src/TextRecast.Infrastructure/) contains local SLM inference and Windows integrations. - [tests](tests/) contains Core and Infrastructure automated tests. -- [Models](Models/) documents the default model and offline packaging. +- [Models](Models/) documents supported models, integrity metadata, and offline packaging. ```text TextRecast.App ---------> TextRecast.Core @@ -218,7 +228,7 @@ Model profiles are defined in [SlmModelCatalog.cs](src/TextRecast.Infrastructure - WPF on .NET 10 - [LLamaSharp](https://github.com/SciSharp/LLamaSharp) with CPU inference -- [Qwen2.5-1.5B-Instruct-GGUF](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF) +- [Qwen 2.5 GGUF](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF), [Qwen 3.5 GGUF](https://huggingface.co/unsloth/Qwen3.5-2B-GGUF), and [Granite 4.1 GGUF](https://huggingface.co/ibm-granite/granite-4.1-3b-GGUF) - Windows UI Automation, clipboard, and input APIs ## Security and license diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 5e2fe97..b71c1fc 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -39,6 +39,14 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## Bundled .NET runtime packs + +The self-contained Windows release includes Microsoft .NET runtime and Windows Desktop/WPF binaries. Their accompanying legal files are copied verbatim from the exact runtime packs selected during restore and are distributed beside the application as: + +- `DOTNET-LICENSE.txt` +- `DOTNET-THIRD-PARTY-NOTICES.txt` +- `WPF-LICENSE.txt` + ## Lucide icon artwork `MahApps.Metro.IconPacks.Lucide` incorporates icon data from the [Lucide project](https://github.com/lucide-icons/lucide). Lucide is licensed under the ISC License, and the Lucide icons derived from Feather retain the Feather MIT notice below. @@ -65,14 +73,15 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## Default SLM model +## Optional local SLM models -TextRecast downloads `Qwen2.5-1.5B-Instruct-GGUF` separately when a valid model is not already installed or packaged. The model repository declares the Apache License 2.0; it is not relicensed as part of TextRecast. +TextRecast does not include a model in its normal release archive. After explicit user confirmation, it may separately download one of the following GGUF files. Each repository declares the Apache License 2.0; the model binary remains subject to its upstream terms and is not relicensed as part of TextRecast. -- Model: `Qwen2.5-1.5B-Instruct-GGUF` -- Quantization used by TextRecast: `Q4_K_M` -- Source and model card: -- Upstream license: -- SPDX identifier: `Apache-2.0` +| Model and quantization | Pinned source | SPDX license | +| --- | --- | --- | +| Qwen 2.5 1.5B Instruct `Q4_K_M` | | `Apache-2.0` | +| Qwen 3.5 2B `Q5_K_M` | | `Apache-2.0` | +| Qwen 3.5 4B `Q5_K_M` | | `Apache-2.0` | +| Granite 4.1 3B `Q5_K_M` | | `Apache-2.0` | -Review the upstream model card and license before redistributing the model binary. +Review the relevant upstream model card and license before redistributing a model binary. Exact filenames, sizes, and SHA-256 checksums are documented in `Models/README.md` and enforced by the application catalog. diff --git a/src/TextRecast.App/App.xaml.cs b/src/TextRecast.App/App.xaml.cs index b033d06..8570089 100644 --- a/src/TextRecast.App/App.xaml.cs +++ b/src/TextRecast.App/App.xaml.cs @@ -5,6 +5,7 @@ using System.Windows; using TextRecast.App.Presentation; using TextRecast.Core.Application; +using TextRecast.Infrastructure.Hardware; using TextRecast.Infrastructure.SLM; using TextRecast.Infrastructure.Windows.Replacement; using TextRecast.Infrastructure.Windows.Selection; @@ -34,30 +35,102 @@ protected override void OnStartup(StartupEventArgs e) return; } - var modelProfile = SlmModelCatalog.Default; - _modelDownloadClient = CreateModelDownloadClient(); - var modelInstaller = new SlmModelInstaller( - modelProfile, - _modelDownloadClient, - Path.Combine(AppContext.BaseDirectory, "Models"), - Path.Combine( + _ = StartAsync(); + } + + private async Task StartAsync() + { + try + { + var profiles = SlmModelCatalog.All; + var packagedModelDirectory = Path.Combine(AppContext.BaseDirectory, "Models"); + var userModelDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TextRecast", - "Models")); - var modelPath = modelInstaller.FindInstalledModel(); - if (modelPath is null) - { - var downloadWindow = new ModelDownloadWindow(modelInstaller); - if (downloadWindow.ShowDialog() != true || - string.IsNullOrWhiteSpace(downloadWindow.InstalledModelPath)) + "Models"); + + _modelDownloadClient = CreateModelDownloadClient(); + var installations = new SlmModelInstallationSet( + profiles, + _modelDownloadClient, + packagedModelDirectory, + userModelDirectory); + var installedPaths = installations.FindInstalledModels(); + + var settingsStore = new ModelSelectionSettingsStore(profiles.Select(profile => profile.Id)); + var settings = await settingsStore.LoadAsync(); + var choices = CreateModelChoices(profiles, installedPaths.Keys, userModelDirectory); + var selectedChoice = settings.ActiveModelId is string activeModelId + ? choices.FirstOrDefault(choice => + choice.IsCompatible && + choice.IsInstalled && + choice.Profile.Id.Equals(activeModelId, StringComparison.Ordinal)) + : null; + + SlmModelProfile modelProfile; + string modelPath; + if (selectedChoice is not null) { - Shutdown(); - return; + modelProfile = selectedChoice.Profile; + modelPath = installedPaths[modelProfile.Id]; + } + else + { + var initialModelId = choices.Any(choice => + choice.IsCompatible && + choice.Profile.Id.Equals(settings.ActiveModelId, StringComparison.Ordinal)) + ? settings.ActiveModelId! + : SlmModelCatalog.Default.Id; + var selectionWindow = new ModelSelectionWindow(choices, initialModelId); + if (selectionWindow.ShowDialog() != true || selectionWindow.SelectedProfile is null) + { + Shutdown(); + return; + } + + modelProfile = selectionWindow.SelectedProfile; + var modelInstaller = installations.GetInstaller(modelProfile.Id); + modelPath = modelInstaller.FindInstalledModel() ?? string.Empty; + if (string.IsNullOrWhiteSpace(modelPath)) + { + var downloadWindow = new ModelDownloadWindow(modelInstaller, modelProfile); + if (downloadWindow.ShowDialog() != true || + string.IsNullOrWhiteSpace(downloadWindow.InstalledModelPath)) + { + Shutdown(); + return; + } + + modelPath = downloadWindow.InstalledModelPath; + } + + await settingsStore.SaveAsync(new ModelSelectionSettings + { + Mode = ModelSelectionMode.Manual, + ActiveModelId = modelProfile.Id + }); } - modelPath = downloadWindow.InstalledModelPath; + StartMainWindow(modelProfile, modelPath); + } + catch (Exception exception) when (exception is + HardwareInspectionException or + IOException or + UnauthorizedAccessException or + InvalidOperationException or + ArgumentException) + { + MessageBox.Show( + $"TextRecast could not complete model setup.\n\n{exception.Message}", + "TextRecast setup", + MessageBoxButton.OK, + MessageBoxImage.Error); + Shutdown(); } + } + private void StartMainWindow(SlmModelProfile modelProfile, string modelPath) + { var selectionReader = new UiAutomationSelectionReader(); var selectionCapture = new SelectionCaptureService(selectionReader); var replacement = new WindowsTextReplacementService(selectionReader); @@ -68,11 +141,40 @@ protected override void OnStartup(StartupEventArgs e) }); var workflow = new FormatTextWorkflow(selectionCapture, _formatter, replacement); - MainWindow = new MainWindow(workflow); + MainWindow = new MainWindow(workflow, modelProfile); MainWindow.Show(); ShutdownMode = ShutdownMode.OnMainWindowClose; } + private static ModelSelectionChoice[] CreateModelChoices( + IReadOnlyList profiles, + IEnumerable installedModelIds, + string userModelDirectory) + { + var installedIds = new HashSet(installedModelIds, StringComparer.Ordinal); + HardwareProfile? hardware = null; + try + { + hardware = new HardwareInspector().Inspect(userModelDirectory); + } + catch (HardwareInspectionException) + { + } + + return SlmModelSetupPlanner.CreateChoices( + profiles, + hardware, + installedIds, + SlmModelCatalog.Default.Id) + .Select(choice => new ModelSelectionChoice( + choice.Profile, + choice.IsCompatible, + choice.IsInstalled, + choice.IsRecommended, + choice.CompatibilityText)) + .ToArray(); + } + protected override void OnExit(ExitEventArgs e) { _formatter?.Dispose(); diff --git a/src/TextRecast.App/Presentation/MainWindow.xaml b/src/TextRecast.App/Presentation/MainWindow.xaml index 0192537..9defc38 100644 --- a/src/TextRecast.App/Presentation/MainWindow.xaml +++ b/src/TextRecast.App/Presentation/MainWindow.xaml @@ -26,6 +26,8 @@ + + diff --git a/src/TextRecast.App/Presentation/MainWindow.xaml.cs b/src/TextRecast.App/Presentation/MainWindow.xaml.cs index e14112f..7997746 100644 --- a/src/TextRecast.App/Presentation/MainWindow.xaml.cs +++ b/src/TextRecast.App/Presentation/MainWindow.xaml.cs @@ -3,6 +3,7 @@ using System.Windows.Interop; using TextRecast.Core.Application; using TextRecast.Core.Models; +using TextRecast.Infrastructure.SLM; using TextRecast.Infrastructure.Windows.Native; namespace TextRecast.App.Presentation; @@ -10,6 +11,7 @@ namespace TextRecast.App.Presentation; public partial class MainWindow : Window { private readonly FormatTextWorkflow _formatTextWorkflow; + private readonly SlmModelProfile _activeModelProfile; private ResultWindow? _resultWindow; private IntPtr _windowHandle; private Point? _dragStartScreen; @@ -18,10 +20,15 @@ public partial class MainWindow : Window private bool _didDrag; private int _captureInProgress; - internal MainWindow(FormatTextWorkflow formatTextWorkflow) + internal MainWindow( + FormatTextWorkflow formatTextWorkflow, + SlmModelProfile activeModelProfile) { InitializeComponent(); - _formatTextWorkflow = formatTextWorkflow; + _formatTextWorkflow = formatTextWorkflow ?? + throw new ArgumentNullException(nameof(formatTextWorkflow)); + _activeModelProfile = activeModelProfile ?? + throw new ArgumentNullException(nameof(activeModelProfile)); } protected override void OnSourceInitialized(EventArgs e) @@ -177,4 +184,22 @@ private void Quit_Click(object sender, RoutedEventArgs e) { global::System.Windows.Application.Current.Shutdown(); } + + private void ModelInformation_Click(object sender, RoutedEventArgs e) + { + MessageBox.Show( + this, + $"{_activeModelProfile.DisplayName}\n\n" + + $"Role: {_activeModelProfile.Role}\n" + + $"Language: {_activeModelProfile.LanguageSupport}\n" + + $"License: {_activeModelProfile.LicenseExpression}\n" + + $"Source: {_activeModelProfile.SourceRepository}\n\n" + + $"{_activeModelProfile.LimitationNotice}\n\n" + + "Always review the replaced text in the source application.", + "Current TextRecast model", + MessageBoxButton.OK, + _activeModelProfile.IsExperimental + ? MessageBoxImage.Warning + : MessageBoxImage.Information); + } } diff --git a/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml b/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml index 1e946db..ad29291 100644 --- a/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml +++ b/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml @@ -140,11 +140,13 @@ Source="/Assets/Brand/textrecast-icon.png" RenderOptions.BitmapScalingMode="HighQuality" /> - - @@ -162,7 +164,8 @@ - diff --git a/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml.cs b/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml.cs index 468a715..067efa8 100644 --- a/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml.cs +++ b/src/TextRecast.App/Presentation/ModelDownloadWindow.xaml.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Net.Http; using System.Windows; @@ -15,16 +16,23 @@ namespace TextRecast.App.Presentation; public partial class ModelDownloadWindow : Window { private readonly SlmModelInstaller _installer; + private readonly SlmModelProfile _profile; private CancellationTokenSource? _downloadCancellation; private bool _downloadStarted; private bool _downloadSucceeded; private bool _isDownloadInProgress; private bool _isClosing; - public ModelDownloadWindow(SlmModelInstaller installer) + public ModelDownloadWindow(SlmModelInstaller installer, SlmModelProfile profile) { - _installer = installer; + _installer = installer ?? throw new ArgumentNullException(nameof(installer)); + _profile = profile ?? throw new ArgumentNullException(nameof(profile)); InitializeComponent(); + HeaderTextBlock.Text = $"Downloading {_profile.DisplayName}"; + SetupSubtitleTextBlock.Text = $"{FormatRole(_profile.Role)} | {_profile.LanguageSupport}"; + IntroductionTextBlock.Text = + $"TextRecast will download {FormatBytes(_profile.ExpectedFileSize)} for {_profile.DisplayName}. " + + "It stays on this computer and is used privately for formatting."; } public string? InstalledModelPath { get; private set; } @@ -241,4 +249,16 @@ private static string FormatDuration(TimeSpan duration) return $"{Math.Max((int)Math.Ceiling(duration.TotalSeconds), 1)}s"; } + + private static string FormatRole(SlmModelRole role) + { + return role switch + { + SlmModelRole.Fast => "Fast / default", + SlmModelRole.Balanced => "Balanced", + SlmModelRole.Quality => "Best quality", + SlmModelRole.Alternative => "Alternative", + _ => throw new ArgumentOutOfRangeException(nameof(role)) + }; + } } diff --git a/src/TextRecast.App/Presentation/ModelSelectionWindow.xaml b/src/TextRecast.App/Presentation/ModelSelectionWindow.xaml new file mode 100644 index 0000000..6342b57 --- /dev/null +++ b/src/TextRecast.App/Presentation/ModelSelectionWindow.xaml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +