Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to Rescripto are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project uses [Semantic Versioning](https://semver.org/).

## [1.3.2] - 2026-10-27

### Changed

- Rewrite instructions now explicitly preserve the draft's language and time
format, including mixed-language passages. A clear translation or number
change is rejected before it reaches history. On-device rewrites can retry
a rejected draft by language passage; the original text remains available
if the model still cannot preserve it. Small models can still make mistakes;
review the result before using it.
- Added a hint beside the intensity controls explaining that Full rewrite can
produce stronger tone changes, especially with small models. Simple mode also
explains where to find this setting.
- Clarified the store description that preservation is an instruction to the
model, not a guarantee, and that results should be checked before use.
- Clarified that on-device rewrites can take minutes on some phones and that
model speed varies by device.
- Added an on-screen reminder to compare AI results with the original before
sending. A missed-meeting safeguard also rejects an English rewrite that
silently drops the speaker's inability to attend.
- Silence-only local dictation no longer adds a `[ Silence ]` marker to the
draft.

## [1.3.1] - 2026-09-16

### Changed
Expand Down
7 changes: 4 additions & 3 deletions docs/FDROID.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ draft build recipe, and the steps that still need a person.

## Anti-features to declare

- **NonFreeNet**: optional. In Cloud or Hybrid mode, text and recordings can
- **NonFreeNet**: local use requires downloading an AI model from Hugging Face.
In optional Cloud or Hybrid mode, text and recordings can
go to commercial AI services (OpenAI, Anthropic, Google, Groq, xAI and
others). All of it is off until the user turns it on and adds their own
API key.
Expand All @@ -41,8 +42,8 @@ to time.
```yaml
AntiFeatures:
NonFreeNet:
en-US: Can send text to commercial cloud AI services when you turn on Cloud or
Hybrid mode.
en-US: Requires downloading an AI model from Hugging Face for local use and can
send text to commercial cloud AI services in Cloud or Hybrid mode.
Categories:
- Writing
License: Apache-2.0
Expand Down
54 changes: 54 additions & 0 deletions docs/FDROID_DEVICE_RETEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# F-Droid device retest

Related review: https://gitlab.com/fdroid/fdroiddata/-/merge_requests/49009#note_4067917436

The isolated QA build was tested on a Redmi 12 (Android 15), leaving the
installed production 1.3.0 app and its data intact. See
[`testing/mobile/reports/2026-10-25_fdroid_language_retest.md`](../testing/mobile/reports/2026-10-25_fdroid_language_retest.md)
for the inputs, complete outputs, model versions, timings, and screenshots.

## Current result

- Gemma 3 1B and Qwen 2.5 1.5B kept the F-Droid reviewer's German sample in
German, including `13 uhr` and the train cancellation. A German/English
sample retained its language switches. French, Spanish, and one Chinese
sample also retained their source languages. This sample set cannot establish
reliable behavior for every language.
- The exact English meeting draft was checked at Casual / Full and
Professional / Full. Gemma preserved the observed facts and saved a modest
rewrite in about 35 seconds. Qwen omitted or changed facts, and the app
rejected both of its generated rewrites. Professional / Full also produced
two immediate empty responses until the QA app was restarted.
- Llama 3.2 1B was downloaded and verified as an English-only alternative.
Professional / Full preserved the facts but was a slight, informal edit;
Casual / Full omitted the reports and was rejected. It did not improve the
English tone and fidelity tradeoff in these runs.
- Japanese exposed a semantic failure: Gemma changed “I cannot attend the
meeting” to “the meeting cannot be held” while retaining Japanese script,
date, time, and number. The current guard cannot detect that meaning change.
Do not claim that all languages or tones are accurate.
- A silent dictation recording previously appended Whisper's `[ Silence ]`
marker. The QA build now leaves the draft untouched for a silence-only
transcript. A spoken retry captured and inserted both sentences the user
said, but misheard “Thursday” as “Lutersday.” The draft remained editable.
History, sharing, Quick Settings, custom tones and audiences, privacy
controls, and a one-step workflow were also exercised.

At `fc7dbab`, 476 Windows Flutter tests and analysis pass, and an Android
arm64 release-style APK builds. Encrypted backup export/restore, WebDAV
conflict handling, and a mock OpenAI-compatible cloud stream were exercised
on the phone against local endpoints. Real cloud and Nextcloud accounts were
not tested. Android registers Rescripto for selected-text processing, but
this Redmi's selection menu did not expose it, so insertion back into another
app remains unverified. No 1.3.2 release or F-Droid recipe update has been
published.

## Release gate

Local model output is best effort; 100% semantic or tone accuracy is not a
release requirement. Keep the English, Japanese, speech, and selected-text
limitations visible to reviewers and users, and do not claim universal
language support. Before updating the F-Droid recipe, obtain a tested
immutable release and verify reproducibility against its signed APK. Capture
actual phone screenshots for the listing if the optional screenshot request
is addressed.
4 changes: 4 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/212.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Local rewrites better preserve the draft's language and reject clear translation or number changes
- A result reminder asks you to compare AI text with the original before sending
- Full rewrite can show stronger tone changes; local model quality and speed vary by device
- Silence-only dictation no longer inserts a silence marker
4 changes: 2 additions & 2 deletions fastlane/metadata/android/en-US/full_description.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<p>Paste a rough message, pick a tone, and Rescripto rewrites it into something you'd actually send. Your facts, names, numbers and dates stay as you wrote them, and the result doesn't get padded with greetings, filler or hype.</p>
<p>Paste a rough message, pick a tone, and Rescripto rewrites it into something you'd actually send. It is instructed to preserve your language, facts, names, numbers and dates, without adding greetings, filler or hype. Models can still make mistakes, especially smaller on-device models, so check the result before using it.</p>

<p>Rewriting runs on your phone. Download a model once (Gemma 3 1B, Llama 3.2 1B or 3B, or Qwen 2.5 1.5B) and it works offline, with no account.</p>

Expand All @@ -8,7 +8,7 @@
<li>In Pro mode, set how much to change, shorter or longer, who it's for, extra instructions, and up to three versions.</li>
<li>Dictate with whisper.cpp running on the phone.</li>
<li>Chain rewrite steps into workflows.</li>
<li>Rewrite selected text from any app's text menu, share text in, or start from a Quick Settings tile.</li>
<li>Rewrite selected text from Android's text menu when available, share text in, or start from a Quick Settings tile.</li>
<li>Search and reuse past rewrites. History stays on the phone.</li>
<li>Export encrypted backups, keep automatic weekly backups, or sync an encrypted copy to your own WebDAV server.</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion lib/core/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ abstract final class AppConstants {
static const String issuesUrl =
'https://github.com/Gr33nOps/Rescripto/issues';
// Keep in step with `version:` in pubspec.yaml.
static const String versionName = '1.3.1';
static const String versionName = '1.3.2';

// Storage keys (shared_preferences).
static const String keySelectedModel = 'selected_model_id';
Expand Down
3 changes: 3 additions & 0 deletions lib/engine/engine_error_messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ String describeEngineError(EngineException error) => switch (error) {
'The model wouldn’t rewrite this text, even on a second try. Small '
'on-device models sometimes do that with ordinary writing. A '
'different model in the Models tab usually helps.',
RewriteFidelityException() =>
'The model changed the draft’s language or numbers, so its result was '
'discarded. Your original text is still here. Try another model.',
OutOfMemoryException() =>
'There isn’t enough memory for this model. Try a smaller model, or a '
'smaller context size in Settings > Performance.',
Expand Down
5 changes: 5 additions & 0 deletions lib/engine/engine_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ final class ModelRefusedException extends EngineException {
const ModelRefusedException();
}

/// A local model translated the draft or changed its numeric details.
final class RewriteFidelityException extends EngineException {
const RewriteFidelityException();
}

/// The request was cancelled before it produced a result.
final class GenerationCancelledException extends EngineException {
const GenerationCancelledException();
Expand Down
36 changes: 28 additions & 8 deletions lib/engine/workflow_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../models/rewrite_request.dart';
import '../models/workflow_definition.dart';
import '../models/workflow_step.dart';
import '../services/config_store.dart';
import '../services/language_fidelity.dart';
import '../services/prompt_builder.dart';
import '../services/refusal_detector.dart';
import '../services/storage_service.dart';
Expand Down Expand Up @@ -131,7 +132,9 @@ class WorkflowRunner extends ChangeNotifier {
await engine.prepare(step.target);

final tone = _configStore.toneById(step.toneId);
final audienceLabels = step.audience.map((id) => _configStore.audienceById(id).label).toList();
final audienceLabels = step.audience
.map((id) => _configStore.audienceById(id).label)
.toList();
final request = RewriteRequest(
sourceText: inputText,
toneId: step.toneId,
Expand All @@ -149,18 +152,32 @@ class WorkflowRunner extends ChangeNotifier {
maxOutputTokens: tone.maxOutputTokens,
stopSequences: tone.stopSequences,
);
final languageProfile = LanguageFidelity.inspect(inputText);

Future<List<String>> attempt({
bool strictRetry = false,
bool unchangedRetry = false,
}) async {
final prompt = PromptBuilder.build(
request,
tone: tone,
audienceLabels: audienceLabels,
strictRetry: strictRetry,
unchangedRetry: unchangedRetry,
);
final useCompactEnglish =
step.target.engineId == 'local.llama' &&
languageProfile.languages.length == 1 &&
languageProfile.languages.single == 'en' &&
!strictRetry &&
!unchangedRetry;
final prompt = useCompactEnglish
? PromptBuilder.buildEnglish(
request,
tone: tone,
audienceLabels: audienceLabels,
)
: PromptBuilder.build(
request,
tone: tone,
audienceLabels: audienceLabels,
sourceLanguageCues: languageProfile.promptCues,
strictRetry: strictRetry,
unchangedRetry: unchangedRetry,
);
final handle = engine.start(
EngineRequest(target: step.target, prompt: prompt, options: options),
);
Expand Down Expand Up @@ -220,6 +237,9 @@ class WorkflowRunner extends ChangeNotifier {
}
}

if (languageProfile.rejects(variants.first)) {
throw const RewriteFidelityException();
}
return variants.first;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/screens/authoring/tone_editor_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,12 @@ class _GenerationSlider extends StatelessWidget {
),
),
SizedBox(
width: 44,
width: 72,
child: Text(
display,
textAlign: TextAlign.end,
maxLines: 1,
softWrap: false,
style: TextStyle(color: color),
),
),
Expand Down
4 changes: 2 additions & 2 deletions lib/screens/models_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ class _ModelTile extends StatelessWidget {
}

String _descriptor(AiModel model) {
if (model.sizeMb <= 900) return 'Fast · works well on most phones';
if (model.sizeMb <= 900) return 'Small model · speed varies by phone';
if (model.sizeMb >= 2500) return 'Highest quality · needs more memory';
return 'Balanced quality and speed';
return 'Larger model · speed varies by phone';
}
}

Expand Down
19 changes: 19 additions & 0 deletions lib/screens/rewrite_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ class _RewriteScreenState extends State<RewriteScreen> {
selectedId: controller.toneId,
onChanged: controller.setTone,
),
if (!isPro) ...[
const SizedBox(height: 6),
Text(
'For a stronger change in tone, switch to Pro mode and try '
'Full rewrite. Small models may make subtler changes.',
style: Theme.of(context).textTheme.bodySmall,
),
],
if (isPro) ...[
const SizedBox(height: 20),
RewriteControls(
Expand Down Expand Up @@ -621,6 +629,17 @@ class _StreamingPanel extends StatelessWidget {
).textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
),
],
if (!cancelling &&
stage == EngineStage.streaming &&
capabilities.needsLocalInstall) ...[
const SizedBox(height: 8),
Text(
'On some phones, an on-device rewrite can take a few minutes.',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
),
],
if (text.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
Expand Down
Loading