Skip to content

Fix RTL text rendering for Arabic, Hebrew, and other right-to-left scripts#63

Open
MHegazy89 wants to merge 27 commits into
f:mainfrom
MHegazy89:fix/rtl-arabic-text-rendering
Open

Fix RTL text rendering for Arabic, Hebrew, and other right-to-left scripts#63
MHegazy89 wants to merge 27 commits into
f:mainfrom
MHegazy89:fix/rtl-arabic-text-rendering

Conversation

@MHegazy89

Copy link
Copy Markdown

Summary

Fixes garbled and incorrect text rendering when using Arabic, Hebrew, Persian, Urdu, and other right-to-left (RTL) scripts in Textream.

Changes

MarqueeTextView.swift:

  • Added isRTLUnicodeScalar() helper to detect RTL script Unicode scalars (Hebrew, Arabic, Syriac, Thaana, NKo, etc.)
  • Added computed isRTL property that checks if any word contains RTL characters
  • Flips VStack alignment to .trailing for RTL content
  • Sets .environment(\.layoutDirection, .rightToLeft) on each line's HStack so word flow renders correctly right-to-left

HighlightingTextEditor.swift:

  • Added isRTLUnicodeScalar() function covering Hebrew, Arabic, Syriac, Thaana, NKo, Samaritan, Mandaic, Arabic Extended forms, and presentation forms
  • Added containsRTLText() convenience helper
  • Added updateWritingDirection() that sets textView.baseWritingDirection = .rightToLeft when RTL content is detected
  • Called updateWritingDirection() on initial setup and on text changes (e.g., paste)

Test Plan

  • Open Textream with Arabic text - verify words flow right-to-left in marquee view
  • Open Textream with Hebrew text - verify correct rendering
  • Type/paste Arabic in the editor - verify caret and alignment are RTL
  • Verify LTR text continues to work as before
  • Verify mixed LTR/RTL content handles correctly

f and others added 27 commits February 27, 2026 00:57
… add state-change debounce to prevent DoS (CWE-400)
fix: add session token auth and connection limit to DirectorServer
fix: enforce connection limit, offload broadcast to background queue,…
The page sidebar was only visible when multiple pages existed, but the
"Add Page" button lives inside the sidebar — making it impossible to
add pages from a single-page state.

Also, pressing play always reset to page 0 (the welcome text) instead
of reading whichever page the user was currently editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds customizable color and brightness controls for teleprompter
annotation cues (e.g. [pause], [smile], [breath]). Previously these
were hardcoded to white at fixed opacities.

- Cue Color: 6 color presets (matches highlight color options)
- Cue Brightness: 4 levels (Dim, Low, Medium, Bright)
- Settings preview includes [pause] sample annotation
- Remote viewer (BrowserServer) updated to use cue color
- DirectorServer state includes cue color for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: always show page sidebar and read current page on play
feat: add cue color and brightness settings for annotation text
In Classic and Voice-Activated modes, the overlay would immediately
switch to a "Done" screen and auto-dismiss 1 second after the
auto-scroll reached the last word. This made timed mode unusable
because speakers typically finish talking 10-20 seconds after the
scroll ends.

Now in timer-based modes (Classic/Voice-Activated), when the scroll
reaches the end on the last page:
- The prompter text stays visible instead of switching to "Done"
- The overlay does not auto-dismiss
- The speaker can close manually via the X button or Esc key

Word Tracking mode behavior is unchanged (auto-dismiss is appropriate
there since it knows when the speaker actually finishes).

Fixes f#29
Address code review findings:
- ExternalDisplayView: gate doneView and speechRecognizer.stop() on
  wordTracking mode, matching NotchOverlayView/FloatingOverlayView
- BrowserServer: suppress isDone in classic/silencePaused modes on
  last page so browser clients keep showing prompter text
- Revert accidental DEVELOPMENT_TEAM change in project.pbxproj
The timerWordProgress was incrementing unboundedly after the scroll
reached the end, unlike the SwiftUI views which guard with !isDone.
Add a scrollDone check before incrementing to stop wasting CPU on
the 100ms broadcast timer.
SFSpeechRecognizer silently returns no results when receiving
multi-channel audio buffers. USB audio interfaces like the RODECaster
Pro II send 2-channel 48kHz audio, and the previous `format: nil` tap
delivered these buffers unchanged to the recognition request.

Create a mono AVAudioFormat at the hardware sample rate when the device
has more than one channel and pass it to installTap, letting
AVAudioEngine handle the downmix automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce an overlay transparency mode that uses a blurred background and adjustable tint opacity.

- Add NotchBlurView (NSVisualEffectView wrapper) to provide behind-window blur.
- Update NotchOverlayView and preview to render either a solid black island or a blurred, clipped island with a dark tint driven by opacity.
- Add NotchSettings properties overlayTransparency and overlayTransparencyOpacity, persisted via UserDefaults (keys: "overlayTransparency", "overlayTransparencyOpacity") and initialized with sensible defaults.
- Expose a Toggle and an opacity Slider in SettingsView to enable transparency and control amount; update reset defaults to include the new settings.

This enables a see-through notch overlay option where desktop content shows through while keeping text readable via a configurable dark tint.
Add overlay transparency with blur and slider
…cognition

fix: downmix multi-channel audio to mono for SFSpeechRecognizer
…pm-scroll

Fix: Keep text visible after WPM auto-scroll reaches the end
Addresses two user-reported bugs: (1) highlight not tracking at the right
speed, jumping erratically or lagging behind speech, and (2) mic appearing
to stall out and stop picking up audio after ~60 seconds.

Root causes identified and fixed:

**Seamless recognition restart (P0)**
- Split cleanupRecognition() so AVAudioEngine stays alive across
  SFSpeechRecognitionTask restarts, eliminating audio gaps
- Add pre-emptive 55-second restart timer to beat Apple's ~60s timeout
- Update matchStartOffset to recognizedCharCount before each restart so
  new sessions match from the correct position
- Thread-safe request swapping via NSLock for audio I/O thread safety
- Add contextualStrings from remaining source text for better STT accuracy

**Fix fuzzy matching false positives (P1)**
- Remove overly permissive `contains` check from isFuzzyMatch that caused
  "and" to match "demand", "the" to match "other", etc.
- Tighten prefix matching to require minimum 3-char words
- Require exact match for 2-char words (no edit distance tolerance)
- Fix charLevelMatch skip-both fallback: no longer advances
  lastGoodOrigIndex on genuine mismatches (gibberish no longer matches)
- Fix wordLevelMatch +1 space overcount on last matched word
- Fix unicode scalar vs Character count mismatch in charLevelMatch

**Confidence gating (P2)**
- Replace blind max(charResult, wordResult) with agreement-based selection
- Add sliding window requiring 2-of-3 recent results to agree before
  committing large forward jumps (small steps always pass through)

**Retry resilience (P3)**
- Distinguish timeout errors (code 1110/216) from real errors
- No retry limit for expected timeouts; immediate soft restart
- Backoff with retry limit only for genuine errors

**Architecture cleanup (P4)**
- Merge two polling timers in observeDismiss() into one
- Fix retain cycle in dismiss() asyncAfter closure
- Add isDismissing guard to prevent double-dismiss
- Fix cancelled-task error callback race in restartTask()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ned mode

Two bugs fixed:

1. Clicking a page in the sidebar then pressing Play always started from
   page 1 instead of the selected page. Root cause: the `sidebarSelection`
   setter wrapped the `currentPageIndex` update in `DispatchQueue.main.async`,
   deferring it asynchronously. Since SwiftUI binding setters are already
   called on the main thread, this wrapper was unnecessary and caused `run()`
   to read the stale value (0) before the update applied. Fix: remove the
   async dispatch so `currentPageIndex` is updated synchronously on selection.

2. `showPinned()` was the only display mode not calling `installKeyMonitor()`,
   so the ESC key did not work to dismiss the overlay in pinned mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: start playback from selected page and install key monitor in pin…
fix: voice tracking highlight and mic stall bugs
…cripts

- MarqueeTextView: Detect RTL content via isRTLUnicodeScalar(), flip VStack
  alignment to .trailing for RTL, and set .environment(\.layoutDirection) so
  word flow lines render right-to-left.
- HighlightingTextEditor: Add isRTLUnicodeScalar() and containsRTLText()
  helpers, plus updateWritingDirection() that sets textView.baseWritingDirection
  to .rightToLeft when RTL content is detected, ensuring proper caret and text
  alignment in the editor.

Fixes garbled/truncated display when dictating or editing Arabic, Hebrew,
Persian, Urdu, and other RTL text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants