{"id":48723266,"url":"https://github.com/gaelic-ghost/textforspeech","last_synced_at":"2026-04-17T21:02:04.312Z","repository":{"id":349371319,"uuid":"1202060267","full_name":"gaelic-ghost/TextForSpeech","owner":"gaelic-ghost","description":"Text normalization and conditioning for speech-safe Swift workflows.","archived":false,"fork":false,"pushed_at":"2026-04-11T18:58:44.000Z","size":441,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-11T20:34:55.565Z","etag":null,"topics":["accessibility","swift","swift-package","text-normalization"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gaelic-ghost.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-05T14:44:34.000Z","updated_at":"2026-04-11T18:58:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gaelic-ghost/TextForSpeech","commit_stats":null,"previous_names":["gaelic-ghost/textforspeech"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/gaelic-ghost/TextForSpeech","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaelic-ghost%2FTextForSpeech","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaelic-ghost%2FTextForSpeech/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaelic-ghost%2FTextForSpeech/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaelic-ghost%2FTextForSpeech/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gaelic-ghost","download_url":"https://codeload.github.com/gaelic-ghost/TextForSpeech/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaelic-ghost%2FTextForSpeech/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31945987,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-17T17:29:20.459Z","status":"ssl_error","status_checked_at":"2026-04-17T17:28:47.801Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accessibility","swift","swift-package","text-normalization"],"created_at":"2026-04-11T20:30:13.556Z","updated_at":"2026-04-17T21:02:04.307Z","avatar_url":"https://github.com/gaelic-ghost.png","language":"Swift","readme":"# TextForSpeech\n\nA Swift package for turning code-heavy, path-heavy, and markdown-heavy developer text into speech-safe text before it reaches a speech model.\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Setup](#setup)\n- [Usage](#usage)\n- [Runtime profiles](#runtime-profiles)\n- [Source layout](#source-layout)\n- [Development](#development)\n- [Verification](#verification)\n- [License](#license)\n\n## Overview\n\n`TextForSpeech` owns the text-conditioning layer that `SpeakSwiftly` consumes as an external package. It ships one semantic built-in core plus a selectable built-in `style`, then layers persisted custom profiles on top so callers can tune pronunciation without reimplementing the core normalization pipeline.\n\nThe package currently has three main responsibilities:\n\n- normalize mixed text such as markdown, logs, CLI output, and prose with embedded code or identifiers\n- normalize whole-source input through an explicit source lane\n- persist and edit named custom profiles while keeping the built-in base layer always on\n\n### Motivation\n\nSpeech models do poorly with raw developer text such as file paths, identifiers, markdown links, inline code, repeated separators, and repeated-letter runs. `TextForSpeech` centralizes those cleanup rules so the same behavior can be reused across callers instead of being reimplemented in app code or worker code.\n\n## Setup\n\n`TextForSpeech` is a Swift Package Manager library product targeting iOS 17, macOS 14, and Swift 6 language mode.\n\nDuring local development, add it to another package with a local path dependency:\n\n```swift\ndependencies: [\n    .package(path: \"../TextForSpeech\"),\n],\ntargets: [\n    .executableTarget(\n        name: \"ExampleApp\",\n        dependencies: [\n            .product(name: \"TextForSpeech\", package: \"TextForSpeech\"),\n        ]\n    ),\n]\n```\n\nThen import `TextForSpeech` in the targets that need normalization or runtime-managed profile state.\n\n## Usage\n\nNormalize mixed text directly when you want the default built-in `.balanced` style and an optional path context:\n\n```swift\nimport TextForSpeech\n\nlet normalized = TextForSpeech.Normalize.text(\n    \"stderr: /Users/galew/Workspace/SpeakSwiftly/Sources/SpeakSwiftly/WorkerRuntime.swift\",\n    context: TextForSpeech.Context(\n        cwd: \"/Users/galew/Workspace/SpeakSwiftly\",\n        repoRoot: \"/Users/galew/Workspace/SpeakSwiftly\"\n    )\n)\n```\n\nIf you omit `format`, `TextForSpeech` detects a likely outer text format before running the text normalization pipeline.\n\nIf you want a different shipped listening mode, pass `style:`:\n\n```swift\nimport TextForSpeech\n\nlet normalized = TextForSpeech.Normalize.source(\n    sourceText,\n    as: .swift,\n    style: .compact\n)\n```\n\nThe shipped styles now differ in concrete coding-agent ways:\n\n- `.compact` assumes more visual context and says less. It drops the broad line-based spoken-code expansion and keeps common shapes terse, such as `foo()` -\u003e `foo`, `#123` -\u003e `123`, and `--help` -\u003e `help`.\n- `.balanced` is the default general-purpose mode. It keeps spoken-code expansion for code-like lines and speaks common references more explicitly, such as `foo()` -\u003e `foo function`, `#123` -\u003e `issue 123`, and `WorkerRuntime.swift:42:7` -\u003e `Worker Runtime dot swift line 42 column 7`.\n- `.explicit` is the audio-first mode. It keeps the same line-based spoken-code expansion as `.balanced`, but uses more narrated phrasing for common coding-agent shapes, such as `foo()` -\u003e `foo function call`, `#123` -\u003e `issue number 123`, and `--help` -\u003e `long flag help`.\n\nFor repeated file paths in the same utterance, the text lane now also compacts repeated anchors before the built-in path-speaking pass. The first path still speaks normally, but later repeated mentions can collapse to shorter phrases such as `same directory, Worker Runtime dot swift` or `same path` instead of repeating the full spoken prefix.\n\nWhen the outer document is mixed text but the embedded code language is known, pass `nestedFormat` so fenced or inline code can route through the source lane:\n\n```swift\nimport TextForSpeech\n\nlet normalized = TextForSpeech.Normalize.text(\n    \"\"\"\n    Read this first:\n\n    ```swift\n    let sampleRate = profile?.sampleRate ?? 24000\n    ```\n    \"\"\",\n    format: .markdown,\n    nestedFormat: .swift\n)\n```\n\nUse the source lane when the whole input is a source file or editor buffer and the caller already knows the language:\n\n```swift\nimport TextForSpeech\n\nlet normalized = TextForSpeech.Normalize.source(\n    \"\"\"\n    struct WorkerRuntime {\n        let sampleRate: Int\n    }\n    \"\"\",\n    as: .swift\n)\n```\n\nThe source lane is explicit today but still generic. It normalizes whole-source input more consistently than the mixed-text lane, but SwiftSyntax-backed Swift-specific structure is still future roadmap work rather than current behavior.\n\nThe package also exposes one small lexical helper when a caller needs natural-language word tokenization:\n\n```swift\nimport TextForSpeech\n\nlet words = TextForSpeech.Forensics.words(\n    in: \"Please read /tmp/Thing and NSApplication.didFinishLaunchingNotification.\"\n)\n```\n\n## Runtime Profiles\n\nUse `TextForSpeech.Runtime` when you need an observable owner for stored custom profiles, one active custom profile id, one selected built-in style, and JSON-backed persistence configured through a small enum:\n\n```swift\nimport TextForSpeech\n\nlet runtime = try TextForSpeech.Runtime(\n    builtInStyle: .balanced,\n    persistence: .default\n)\n\ntry runtime.profiles.setBuiltInStyle(.compact)\ntry runtime.profiles.create(id: \"logs\", name: \"Logs\")\ntry runtime.profiles.add(\n    TextForSpeech.Replacement(\"stderr\", with: \"standard error\", id: \"stderr-rule\"),\n    toProfileID: \"logs\"\n)\ntry runtime.profiles.activate(id: \"logs\")\n\nlet normalized = TextForSpeech.Normalize.text(\n    \"stderr and stdout\",\n    customProfile: runtime.profiles.active(),\n    style: runtime.profiles.builtInStyle\n)\n```\n\nThe runtime model is intentionally explicit:\n\n- `TextForSpeech.Profile.semanticCore` is the always-on semantic built-in layer.\n- `TextForSpeech.Profile.builtInStyle(_:)` returns one shipped style preset.\n- `TextForSpeech.Profile.builtInBase(style:)` composes `semanticCore + style preset`.\n- `TextForSpeech.Profile.base` is the default `.balanced` built-in base for convenience.\n- `TextForSpeech.Profile.default` is the empty default custom profile value.\n- `runtime.profiles.builtInStyle` is the currently selected shipped style preset.\n- `runtime.profiles.activeID` is the stored custom profile id currently selected by the runtime.\n- `runtime.profiles.active()` is the raw active custom profile.\n- `runtime.profiles.effective()` is always `builtInBase(style: builtInStyle) + active custom`.\n- `runtime.profiles.stored(id:)` reads a named stored custom profile without activating it.\n\nPersistence defaults to `.default`. `TextForSpeech.Runtime()` writes to Application Support automatically, namespaced by the host bundle identifier when one is available and falling back to `TextForSpeech` when it is not. In debug builds for bundled targets, the default store uses `TextForSpeech-Debug` instead so local debug runs do not touch the production namespace. Callers that need an explicit location can pass `.file(url)`. The selected built-in style is persisted alongside the active custom profile id and stored custom profiles.\n\n## Source Layout\n\nThe package source lives under `Sources/TextForSpeech` and is organized by responsibility:\n\n- `API/`\n  Public namespace-first entrypoints such as `Normalize` and `Forensics`.\n- `Models/`\n  Core value types such as `Profile`, `Replacement`, `Context`, and the built-in profiles.\n- `Normalization/`\n  The text lane, source lane, structural markdown parsing, replacement-rule engine, speech helpers, and format detection.\n- `Runtime/`\n  Runtime ownership, grouped profile and persistence handles, persisted state, and runtime-facing errors.\nThe current source split keeps structural normalization logic separate from durable lexical policy:\n\n- structural work such as markdown parsing, code-span extraction, and format detection stays in code\n- durable lexical policy such as built-in aliases, identifier speaking, path speaking, URL speaking, repeated-letter-run handling, and style-specific speaking policy lives in the built-in profile layers\n\nTests live under `Tests/TextForSpeechTests` and are grouped by role:\n\n- `Models/`\n- `Normalization/`\n- `Runtime/`\n- top-level lexical helper coverage\n\n## Development\n\nUse the standard Swift package workflow from the repository root:\n\n```bash\nswift build\nswift test\n```\n\n## Verification\n\nThe baseline verification path for this repository is:\n\n```bash\nswift build\nswift test\n```\n\nFor release work or architectural refactors, also review the current roadmap in [ROADMAP.md](ROADMAP.md) and the maintainer notes under [docs/maintainers](docs/maintainers).\n\n## License\n\nThis project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for the full text.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaelic-ghost%2Ftextforspeech","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgaelic-ghost%2Ftextforspeech","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaelic-ghost%2Ftextforspeech/lists"}