BooksWriterStudio remake
Policies that keep the org coherent
name: BooksWriterStudio remake
overview: Remake books-writer as a new Avalonia app BooksWriterStudio under novolis-apps/src/, consuming only published Novolis packages plus app glue—no Python and no dotnet run --file scripts. Extend Novolis.Markup.Manuscript (catalog/diagnostics/book PDF) and Novolis.Audio.Voice.Manuscript (TTS pipeline, concat MP3 / M4B assemble, selected-text speech preview); Manuscript Studio stays as the multi-mode editor.
todos:
- id: markup-catalog-doctor-pdf
content: Extend Novolis.Markup.Manuscript with Catalog, Doctor, Book/Reference PDF + tests; publish to GPR status: completed
- id: voice-pipeline-assemble
content: Extend Novolis.Audio.Voice.Manuscript with synthesizer, VoiceMap, pipeline, ConcatMp3 + M4B assemble, verifier, and selected-text speech preview; publish to GPR status: completed
- id: app-scaffold
content: Create BooksWriterStudio Avalonia app, Directory.Packages.props + slnx wiring, workspace/editor/session glue status: completed
- id: app-publish-scm
content: Wire publish jobs (PDF/audio), selected-text audio preview, JobQueuePanel, diagnostics, git checkpoint, recovery/conflict/search dialogs status: completed
- id: verify-nuget-build
content: verify-nuget-only.ps1 + nuget.org/github restore/build of BooksWriterStudio status: completed isProject: false
Decisions locked
- App: new `novolis-apps/src/BooksWriterStudio/` (parity remake). Leave `ManuscriptStudio` alone.
- Audio: extend `Novolis.Audio.Voice.Manuscript` (not a separate Audiobook package). Assemble outputs: concatenated MP3 and/or chapterized M4B.
- No scripts: zero
dotnet run --file, zero Python/edge-ttsCLI, zero ffmpeg process. TTS viaEdgeTtsClientlibrary; Git may still spawngitthrough `Novolis.IO.Git` (system tool, not a script).
Architecture
flowchart TB
app[BooksWriterStudio glue]
studio[Avalonia.Studio Controls Markdown]
ms[Markup.Manuscript Catalog Diagnostics PDF]
voice[Voice.Manuscript plan pipeline assemble]
edge[Voice.EdgeTts]
io[IO Paths Recovery Watching Git]
app --> studio
app --> ms
app --> voice
app --> io
voice --> edgeApp owns UX orchestration only. Domain/publish logic lives in packages published to GitHub Packages, then referenced as 2026.1.*.
Reuse Avalonia lifts already in tree: ChoiceDialog, FilteredPickerDialog, MarkedListBox, JobQueuePanel / IJobQueueRow, StudioFocusMode (`generic_avalonia_lifts` plan).
Package work (publish before app wires)
1. Extend Novolis.Markup.Manuscript — Catalog
Lift from Manuscript Studio’s internal `ContentCatalog` into the package (do not add a second Catalog package).
Public API
| Type | Role |
|---|---|
ManuscriptWorkspace | TryOpen(startDir) using RootFinder markers: content/series or content/books (+ optional build.ps1) |
ManuscriptCatalog | Load(contentRoot) → series/books/chapters/refs |
SeriesInfo, BookInfo, ChapterInfo, ReferenceSetInfo | Immutable models (Id, Title, paths, ChapterKind, sort key) |
ChapterOrder | Heading/filename sort keys (from Studio) |
BookYaml / thin YAML helpers | Load series.yaml / book.yaml via YamlDotNet |
Behavior: same layout as books repo / Studio (content/series/{id}/books/{id}/chapters/*.md, appendices, references, standalone content/books/{id}).
Tests: fixture tree with 1 series × 2 chapters + 1 standalone book; assert ids, titles, order.
2. Extend Novolis.Markup.Manuscript — Diagnostics
Replace book-tool doctor --json.
Public API
| Type | Role | ||
|---|---|---|---|
ManuscriptDoctor | `Diagnose(BookInfo\ | SeriesInfo\ | contentRoot) → IReadOnlyList<DiagnosticFinding>` |
DiagnosticFinding | Severity (Error/Warning/Info), Code, Message, Path? |
Checks (v1): missing book.yaml/series.yaml; chapter file missing/empty; duplicate chapter stems; heading title mismatch; unreadable markdown; orphan refs under references/; chapter_order_from_heading vs filename order conflicts.
Tests: fixture with planted errors; assert codes.
3. Extend Novolis.Markup.Manuscript — Book PDF
Replace compile-book.cs / compile-reference.cs process shell-outs. Prefer composing `MarkdownPdfExporter` (add PackageReference from Manuscript → Markdown.Rendering).
Public API
| Type | Role |
|---|---|
ManuscriptPrintSettings | Page size (default 6×9), margins, body/heading fonts, font sizes, include cover — round-trip JSON compatible with books-writer .writer/print-settings.json shape where practical |
ManuscriptBookPdfExporter | ExportBook(BookInfo, outputPath, ManuscriptPrintSettings?) — cover + ordered chapter markdown concat, page breaks between chapters |
ExportReferenceSet(ReferenceSetInfo, outputPath, settings?) — series reference PDF |
Tests: export tiny fixture book to bytes; assert non-empty PDF header %PDF.
4. Extend Novolis.Audio.Voice.Manuscript — pipeline + assemble
Keep existing SpeechPlanner / SpeechPlan / ManuscriptSpeechOptions. Add synthesis orchestration and assemble in this package.
New dependencies (nuget.org, allowed): Novolis.Audio.Voice.EdgeTts (GPR) for TTS; for M4B: NAudio MediaFoundation AAC encode (Windows) + small in-package ISOBMFF chapter writer (no ffmpeg).
Public API
| Type | Role |
|---|---|
IManuscriptSynthesizer | Task SynthesizeMp3Async(string text, Stream/path, ManuscriptVoiceSettings, ct) — abstraction; default EdgeTtsManuscriptSynthesizer wraps EdgeTtsClient |
ManuscriptVoiceSettings / VoiceMapStore | Load/save voice-map YAML (voice, rate, pitch, volume, pauses, pronunciation) — compatible with books tools/audio/voice-map.yaml fields the writer edits |
ManuscriptSpeechPreview | Selected-text preview: normalize/pronounce selection with current voice settings → synthesize via IManuscriptSynthesizer → play through IManuscriptAudioPlayer; cancel/replace in-flight preview on re-trigger |
IManuscriptAudioPlayer | Play/stop MP3 bytes (default NaudioMp3Player using NAudio; no ffmpeg). Shared by preview so publish pipeline stays file-oriented |
ManuscriptAudiobookOptions | Output dirs, force rebuild, parallel jobs, chapter filter, assemble mode (None / ConcatMp3 / M4b / Both), chapter gap ms |
ManuscriptAudiobookPipeline | For each chapter: SpeechPlanner.Create → synthesize segments (concat spoken MP3 chunks; insert silence for pauses) → write chapters/{id}.mp3 + cache by PlanHash; write manifest.json |
AudiobookAssembler.ConcatenateMp3(...) | Ordered chapter MP3s → {bookId}.mp3 with gap silence (same bitrate as Edge TTS: 24 kHz / 48 kbps mono) |
AudiobookAssembler.WriteM4b(...) | Chapter MP3s → AAC → {bookId}.m4b with chapter titles/start times from manifest; optional cover image path |
AudiobookVerifier | Assert chapter count, non-empty files, manifest vs disk, M4B chapter count/titles when M4B present |
Speech preview behavior (library)
- Input: raw editor selection (or current line/word if selection empty — app chooses; library accepts a non-empty string).
- Apply
SpeechPlanner.ApplyPronunciation+ light strip of markdown markers from the snippet (not full chapter planning). - Use the same
ManuscriptVoiceSettingsas audiobook generation (voice/rate/pitch/volume). PreviewAsync(text, settings, ct)synthesizes then plays; a second call cancels the previous playback/synthesis.- Cap preview length (e.g. 4k chars) with a clear exception so accidental whole-chapter selects do not hammer Edge TTS.
Pipeline flow
sequenceDiagram
participant App
participant Pipe as ManuscriptAudiobookPipeline
participant Plan as SpeechPlanner
participant TTS as IManuscriptSynthesizer
participant Asm as AudiobookAssembler
App->>Pipe: GenerateAsync(book, options)
loop chapters
Pipe->>Plan: Create(markdown)
Pipe->>TTS: segments to chapter MP3
end
Pipe->>Asm: ConcatenateMp3 and/or WriteM4b
Pipe-->>App: AudiobookResult + manifestTests: planner hash stability (existing); concat of two tiny MP3 fixtures → length increases; verifier fails on missing chapter; preview cancels prior run / rejects over-limit text (fake synthesizer + null/spy player); M4B test gated [Fact(Skip=...)] or Windows-only if MediaFoundation required.
Package description update: planning + chapter TTS orchestration + MP3/M4B assemble + selection speech preview.
App: BooksWriterStudio
Path: `novolis-apps/src/BooksWriterStudio/`
Shape: Avalonia 12 WinExe, Microsoft.Extensions.Hosting DI (same pattern as Manuscript/Concept). MVVM with CommunityToolkit.Mvvm. No process runners for catalog/doctor/PDF/audio.
PackageReferences
Novolis.Avalonia.Studio,.Controls,.MarkdownNovolis.Markup.Manuscript(catalog/doctor/PDF/metadata/word count)Novolis.Audio.Voice.Manuscript,Novolis.Audio.Voice.EdgeTts(preview + publish TTS)Novolis.IO.Paths,.Recovery,.Watching,.Git- nuget.org: Avalonia stack, AvaloniaEdit, YamlDotNet, WeCantSpell.Hunspell (spell — app glue)
Add floating versions to `Directory.Packages.props`; register project in `Novolis.Apps.slnx`.
Feature map (glue only)
| books-writer | Implementation |
|---|---|
| Workspace open | ManuscriptWorkspace + folder picker; auto-open cwd if valid |
| Series/book/chapter nav | Catalog + MarkedListBox |
| Editor | Novolis.Avalonia.Markdown BookAuthoring highlighting |
| Autosave / dirty | Idle timer + EditorSession |
| Recovery | ContentRecoveryStore under .writer/recovery/; ChoiceDialog on reopen |
| External change | SingleFileWatcher + conflict ChoiceDialog |
| Metadata panel | ManuscriptMetadata.Parse / ApplyCallouts |
| Search / Goto | In-app scan of catalog chapter files; FilteredPickerDialog for Ctrl+P |
| Spellcheck | Hunspell + optional dict under Assets (same as writer) |
| Word count | ManuscriptMetadata.CountWords (chapter + book aggregate) |
| Diagnostics tab | ManuscriptDoctor |
| Source control | GitRepositoryService.GetStatus / Checkpoint; open folder / wt.exe |
| Publish PDF / ref PDF | ManuscriptBookPdfExporter on in-app job queue |
| Publish audio | ManuscriptAudiobookPipeline (jobs parallelism inside pipeline); assemble ConcatMp3 and/or M4B |
| Audio preview | Read selection aloud via ManuscriptSpeechPreview + current VoiceMapStore settings; toolbar/context action + shortcut (Ctrl+Shift+Space); Stop cancels; empty selection → status hint, no call |
| Jobs UI | JobQueuePanel + cancel via CancellationToken (in-process; not ProcessTree) |
| Print / voice settings | Bind ManuscriptPrintSettings / VoiceMapStore (preview and publish share the same configured voice) |
| Focus / theme / font | StudioFocusMode + existing Studio theme; editor zoom |
| Settings | %LocalAppData%/Novolis/BooksWriterStudio/ + workspace .writer/settings.json / workspace.json when inside a books tree |
Layout (parity)
Three-column writing chrome inspired by books-writer (not Manuscript’s preview-first rail): chapter list | AvaloniaEdit | context tabs (Metadata, Search, Diagnostics, Publish, SCM). Editor chrome includes Speak selection / Stop next to save/zoom. Dialogs: Settings, Goto, Reference picker, Conflict, Recovery via Controls package.
Out of scope for v1
- Pass start/finish / revision tags UI (APIs exist on
GitRepositoryService; wire later) - Mermaid/StarMap views (stay in Manuscript Studio)
- Generic-markdown / Concept modes
- Installer/release workflow wiring (follow-up; can add later per `release.md`)
Delivery order
- Markup.Manuscript Catalog + Diagnostics + Book PDF → unit tests → pack/publish GPR
- Voice.Manuscript synthesizer + pipeline + ConcatMp3 + M4b + verifier + VoiceMap + ManuscriptSpeechPreview → tests → publish GPR
- BooksWriterStudio app scaffold + editor/session/IO wiring
- Publish panel + audio/PDF jobs + selection audio preview
- Polish: spell, focus, SCM checkpoint, settings persistence
pwsh -File novolis-governance/scripts/verify-nuget-only.ps1;dotnet restore/buildapps with nuget.org + github only
Done criteria
- Open a books-layout workspace, edit/autosave a chapter, recover after simulated crash, see doctor findings, checkpoint git, export book PDF, generate chapter MP3 and assemble concat MP3 and M4B, and preview selected text with the configured voice — all in-process libraries, no Python/ffmpeg/
dotnet run --file.