diff --git a/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md b/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
new file mode 100644
index 000000000..5e18e0955
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
@@ -0,0 +1,25 @@
+# PR_26171_061 Engine Audio Ownership Checklist
+
+## Engine Ownership
+
+- PASS: Reusable TTS voice filtering lives in `src/engine/audio/TextToSpeechEngine.js`.
+- PASS: Reusable TTS preset shaping lives in `src/engine/audio/TextToSpeechEngine.js`.
+- PASS: Reusable TTS queue item helpers live in `src/engine/audio/TextToSpeechEngine.js`.
+- PASS: Existing defaults remain in `src/engine/audio/TextToSpeechDefaults.js`.
+- PASS: Active toolbox UI consumes the engine module instead of duplicating all engine behavior locally.
+
+## Active Tool Ownership
+
+- PASS: Active tool remains under `toolbox/text-to-speech/`.
+- PASS: Active HTML remains Theme V2 only.
+- PASS: Active JavaScript remains external.
+- PASS: Active tool owns DOM wiring, status display, and control event handling.
+- PASS: Active tool does not create database behavior.
+
+## Boundary Checks
+
+- PASS: No `tools/text2speech/` path was created.
+- PASS: Archived `old_text2speech-V2` was used as a read-only functionality sample.
+- PASS: Archived tool files were not activated in navigation.
+- PASS: Browser SpeechSynthesis is the implemented local provider.
+- PASS: Planned providers remain documented but do not block browser preview.
diff --git a/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md b/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
new file mode 100644
index 000000000..ffb2cb288
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
@@ -0,0 +1,32 @@
+# PR_26171_061 Instruction Compliance Checklist
+
+## Pre-Work Gate
+
+- PASS: Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` before file changes.
+- PASS: Read `docs_build/dev/PROJECT_MULTI_PC.txt` before file changes.
+- PASS: Reported instruction compliance PASS/FAIL before file changes.
+- PASS: `git branch --show-current` reported `main` before branch creation.
+- PASS: `git checkout main` completed before branch creation.
+- PASS: `git pull origin main` completed before branch creation.
+- PASS: `git status` reported a clean working tree before branch creation.
+- PASS: Created branch `pr/26171-061-text2speech-engine-audio-feature-parity` from clean latest `main`.
+
+## Owner And Parity
+
+- PASS: PR number `061` is odd.
+- PASS: Odd parity maps to Laptop / Environment 2.
+- PASS: Text To Speech / TTS is owned by Laptop / Environment 2.
+- PASS: Implementation path matches active owner path: `toolbox/text-to-speech/`.
+- PASS: Shared engine path matches approved reusable audio path: `src/engine/audio/`.
+
+## Hard Stop Checks
+
+- PASS: Instructions were present and readable.
+- PASS: Branch workflow could be followed.
+- PASS: Repository was clean before branching.
+- PASS: No owner mismatch.
+- PASS: No parity mismatch.
+- PASS: No wrong implementation path.
+- PASS: Scoped validation was not skipped.
+- PASS: Required reports were created.
+- PASS: Required repo ZIP was created.
diff --git a/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md b/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
new file mode 100644
index 000000000..3341b14ba
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
@@ -0,0 +1,27 @@
+# PR_26171_061 Manual Validation Notes
+
+## Manual Review
+
+- Confirmed active Text To Speech path is `toolbox/text-to-speech/`.
+- Confirmed no `tools/text2speech/` directory was created.
+- Confirmed old functionality sample path is `archive/v1-v2/tools/old_text2speech-V2/`.
+- Confirmed active HTML has external scripts only.
+- Confirmed active HTML has no inline style attributes or inline event handlers.
+- Confirmed no database files were changed.
+
+## Automated Browser Coverage Used For Manual Equivalents
+
+- Opened active Text To Speech page through targeted Playwright validation.
+- Verified browser voices render.
+- Verified restored control groups render.
+- Verified preset shaping updates pitch and volume values.
+- Verified named sentence Add, Duplicate, and Delete.
+- Verified output summary includes queue JSON.
+- Verified Speak, Pause, Resume, and Stop call the browser SpeechSynthesis mock.
+- Verified unavailable SpeechSynthesis shows actionable error.
+
+## Out Of Scope Manual Checks
+
+- No paid provider was manually exercised.
+- No generated audio file export was manually exercised.
+- No database behavior was manually exercised.
diff --git a/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md b/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
new file mode 100644
index 000000000..e09e0f974
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
@@ -0,0 +1,48 @@
+# PR_26171_061 Old TTS Feature Parity Checklist
+
+Functionality sample: `archive/v1-v2/tools/old_text2speech-V2/`
+
+## Controls And Options
+
+- PASS: Gender helper filter restored.
+- PASS: Language filter restored.
+- PASS: Voice dropdown restored.
+- PASS: Voice details restored.
+- PASS: Voice Age select restored.
+- PASS: Character Preset select restored.
+- PASS: SSML-like Preset select restored.
+- PASS: Volume slider restored with visible value.
+- PASS: Rate / Speed slider restored with visible value.
+- PASS: Pitch slider restored with visible value.
+- PASS: Name field restored.
+- PASS: Text To Speak editor restored.
+
+## Queue And JSON
+
+- PASS: Named Sentences queue restored.
+- PASS: Add named sentence restored.
+- PASS: Duplicate named sentence restored.
+- PASS: Delete named sentence restored.
+- PASS: Output Summary JSON restored.
+- PASS: Import JSON restored for standalone launch.
+- PASS: Copy JSON restored for standalone launch.
+- PASS: Export JSON restored for standalone launch.
+- PASS: URL JSON source loading restored through `samplePresetPath`.
+
+## Playback And Status
+
+- PASS: Speak restored.
+- PASS: Pause restored when browser support exists.
+- PASS: Resume restored when browser support exists.
+- PASS: Stop restored.
+- PASS: Clearable status log restored.
+- PASS: Actionable unavailable-browser error restored.
+- PASS: No placeholder provider-blocking behavior remains for browser preview.
+
+## Workspace
+
+- PASS: Project Workspace launch detection restored.
+- PASS: Standalone JSON actions hide during Project Workspace launch.
+- PASS: Return to Project Workspace action restored.
+- PASS: Project Workspace toolState loading restored.
+- PASS: Project Workspace dirty-state writeback restored.
diff --git a/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md b/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
new file mode 100644
index 000000000..b2f4a44d3
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
@@ -0,0 +1,71 @@
+# PR_26171_061 Text To Speech Engine Audio Feature Parity
+
+## Summary
+
+Restored active Text To Speech feature parity from the archived `old_text2speech-V2` functionality sample while keeping active implementation in `toolbox/text-to-speech/` and reusable engine behavior in `src/engine/audio/`.
+
+## Changed Scope
+
+- Rebuilt reusable Text To Speech voice filtering, preset shaping, and queue-item helpers in `src/engine/audio/TextToSpeechEngine.js`.
+- Expanded `toolbox/text-to-speech/index.html` with Theme V2 controls for the old V2 feature set.
+- Rebuilt `toolbox/text-to-speech/text2speech.js` so the active tool consumes the engine audio module.
+- Expanded targeted browser validation in `tests/playwright/tools/TextToSpeechFunctional.spec.mjs`.
+- Updated Playwright V8 coverage artifacts for changed runtime JavaScript.
+
+## Requirement Checklist
+
+- PASS: Used `archive/v1-v2/tools/old_text2speech-V2/` as the required functionality sample.
+- PASS: Reusable TTS behavior lives in `src/engine/audio/`.
+- PASS: `toolbox/text-to-speech/` consumes `src/engine/audio/TextToSpeechEngine.js`.
+- PASS: Restored old controls/options/features in the active tool.
+- PASS: Browser SpeechSynthesis provider remains implemented.
+- PASS: Planned provider adapters do not block browser preview behavior.
+- PASS: No `tools/text2speech/` path was created.
+- PASS: No database files were changed.
+- PASS: Theme V2 remains the styling source.
+- PASS: HTML uses external JavaScript only.
+- PASS: No inline script, style, or event handler was added.
+
+## Restored Feature Set
+
+- Import JSON.
+- Copy JSON.
+- Export JSON.
+- Project Workspace return action during workspace launch.
+- Gender helper filter.
+- Language filter.
+- Browser voice selection.
+- Voice details.
+- Voice age shaping.
+- Character presets.
+- SSML-like presets.
+- Volume, rate, and pitch sliders with visible values.
+- Named sentence Name field.
+- Add, Duplicate, and Delete named sentence actions.
+- Text To Speak editor.
+- Speak, Pause, Resume, and Stop playback actions.
+- Named Sentences queue.
+- Output Summary JSON.
+- Clearable Status log.
+- URL JSON preset loading through `samplePresetPath`.
+- Project Workspace toolState loading and dirty-state writeback.
+
+## Validation Summary
+
+- PASS: `node --check src\engine\audio\TextToSpeechEngine.js`
+- PASS: `node --check toolbox\text-to-speech\text2speech.js`
+- PASS: `node --test tests\tools\Text2SpeechShell.test.mjs`
+- PASS: `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`
+- PASS: `npm run test:workspace-v2`
+ - Note: command name is legacy; user-facing language is Project Workspace.
+- PASS: `git diff --check`
+- PASS: changed runtime JS coverage collected for `src/engine/audio/TextToSpeechEngine.js`.
+- PASS: changed runtime JS coverage collected for `toolbox/text-to-speech/text2speech.js`.
+
+## Out Of Scope
+
+- No database changes.
+- No external paid provider implementation.
+- No generated audio file export provider.
+- No new `tools/text2speech/` path.
+- No archived tool activation.
diff --git a/docs_build/dev/reports/PR_26171_061-validation.md b/docs_build/dev/reports/PR_26171_061-validation.md
new file mode 100644
index 000000000..bf864dc27
--- /dev/null
+++ b/docs_build/dev/reports/PR_26171_061-validation.md
@@ -0,0 +1,42 @@
+# PR_26171_061 Validation Report
+
+## Commands Run
+
+- `node --check src\engine\audio\TextToSpeechEngine.js`
+ - PASS.
+- `node --check toolbox\text-to-speech\text2speech.js`
+ - PASS.
+- `node --test tests\tools\Text2SpeechShell.test.mjs`
+ - PASS: 3 tests passed.
+- `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`
+ - PASS: 2 tests passed.
+ - Covers restored controls, preset shaping, queue add/duplicate/delete, output summary, pause/resume, speak, stop, and unavailable SpeechSynthesis error handling.
+ - PASS: rerun after merging `origin/main` into the PR branch for conflict resolution.
+- `npm run test:workspace-v2`
+ - PASS: 5 Project Workspace tests passed.
+ - Note: command name is legacy; user-facing language is Project Workspace.
+ - Note: first execution used a 120s timeout and timed out before completion; rerun with a longer timeout completed successfully.
+ - PASS: rerun after merging `origin/main` into the PR branch for conflict resolution.
+- `git diff --check`
+ - PASS.
+
+## Coverage
+
+- PASS: `docs_build/dev/reports/playwright_v8_coverage_report.txt` produced changed runtime JS coverage.
+- PASS: `docs_build/dev/reports/coverage_changed_js_guardrail.txt` reports no changed runtime JS coverage warnings.
+- PASS: `src/engine/audio/TextToSpeechEngine.js` covered at 71%.
+- PASS: `toolbox/text-to-speech/text2speech.js` covered at 71%.
+
+## Artifact Verification
+
+- PASS: `docs_build/dev/reports/codex_review.diff` exists.
+- PASS: `docs_build/dev/reports/codex_changed_files.txt` exists.
+- PASS: `tmp/PR_26171_061-text2speech-engine-audio-feature-parity_delta.zip` exists.
+- PASS: ZIP size is greater than zero.
+- PASS: ZIP contents preserve repo-relative paths.
+
+## Skipped
+
+- Full samples validation skipped because no sample JSON or sample runtime behavior changed.
+- Database validation skipped because no database files or runtime persistence changed.
+- External provider validation skipped because paid/provider generation is out of scope and browser SpeechSynthesis is the implemented provider for this PR.
diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
index bb7de5d8d..2a3c37ce3 100644
--- a/docs_build/dev/reports/codex_changed_files.txt
+++ b/docs_build/dev/reports/codex_changed_files.txt
@@ -1,91 +1,54 @@
-# Codex Changed Files - PR_26171_041-idea-board-production-polish
+# Codex Changed Files - PR_26171_061-text2speech-engine-audio-feature-parity
## Git Workflow
-- Current branch: `codex/pr-26171-041-idea-board-production-polish`.
-- Expected starting branch: `main` (PASS before branch creation).
-- Created branch: `codex/pr-26171-041-idea-board-production-polish`.
-- Commit before conflict resolution: `ca626873a4cf19925229851b0da2a4ff55c2073f`.
-- Initial push result: PASS, branch pushed to `origin/codex/pr-26171-041-idea-board-production-polish`.
-- PR URL: `https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/17`.
-- Conflict resolution: merged `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`; conflicts were limited to `codex_changed_files.txt` and `codex_review.diff`.
-- Conflict resolution push result: pending until after merge-validation commit.
-- Merge result: pending until after PR validation/merge.
-- Final main sync: pending until after merge and final pull.
+- Starting branch: `main`.
+- Created branch: `pr/26171-061-text2speech-engine-audio-feature-parity`.
+- Initial commit: `e4541d63719ab777b0654c8fecf4b13237d31256`.
+- PR branch was merged with `origin/main` during PR conflict recovery.
+- Conflicts were limited to generated report artifacts.
-## Git Status
+## Scoped Diff Stat
```text
- M assets/theme-v2/css/tables.css
- M assets/theme-v2/js/tool-display-mode.js
- M docs_build/dev/reports/codex_changed_files.txt
- M docs_build/dev/reports/codex_review.diff
- M docs_build/dev/reports/coverage_changed_js_guardrail.txt
- M docs_build/dev/reports/playwright_v8_coverage_report.txt
- M src/shared/toolbox/tool-metadata-inventory.js
- M tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
- M tests/playwright/tools/ToolboxRoutePages.spec.mjs
- M toolbox/idea-board/index.html
- M toolbox/idea-board/index.js
+...R_26171_061-engine-audio-ownership-checklist.md | 25 +
+ ...R_26171_061-instruction-compliance-checklist.md | 32 +
+ .../PR_26171_061-manual-validation-notes.md | 27 +
+ ...R_26171_061-old-tts-feature-parity-checklist.md | 48 ++
+ ..._061-text2speech-engine-audio-feature-parity.md | 71 ++
+ docs_build/dev/reports/PR_26171_061-validation.md | 42 +
+ docs_build/dev/reports/codex_changed_files.txt | 118 +--
+ .../dev/reports/coverage_changed_js_guardrail.txt | 7 +-
+ .../dev/reports/playwright_v8_coverage_report.txt | 36 +-
+ src/engine/audio/TextToSpeechEngine.js | 208 ++++-
+ .../tools/TextToSpeechFunctional.spec.mjs | 30 +
+ toolbox/text-to-speech/index.html | 119 +--
+ toolbox/text-to-speech/text2speech.js | 853 +++++++++++++++++----
+ 13 files changed, 1317 insertions(+), 299 deletions(-)
```
-## Diff Stat
-```text
- assets/theme-v2/css/tables.css | 70 +++++++-
- assets/theme-v2/js/tool-display-mode.js | 2 +-
- .../dev/reports/coverage_changed_js_guardrail.txt | 5 +-
- .../dev/reports/playwright_v8_coverage_report.txt | 11 +-
- src/shared/toolbox/tool-metadata-inventory.js | 6 +-
- .../playwright/tools/IdeaBoardTableNotes.spec.mjs | 71 +++++++-
- tests/playwright/tools/ToolboxRoutePages.spec.mjs | 26 ++-
- toolbox/idea-board/index.html | 31 ++--
- toolbox/idea-board/index.js | 191 ++++++++++++++++++++-
- 9 files changed, 368 insertions(+), 45 deletions(-)
-```
-
-## Requirement Evidence
-- PASS: Removed creator-visible Create Project from the right-side Next Steps area; no disabled `data-idea-board-create-project` panel remains.
-- PASS: Create Project is shown only in a Ready idea row's Actions column.
-- PASS: Clicking Create Project changes the idea status from Ready to Project and replaces Create Project/Delete with Open Project and Archive.
-- PASS: Project rows show Open Project and Archive and do not show Delete.
-- PASS: Added Archived status; archived rows are hidden by default.
-- PASS: Archived rows show Restore and Delete.
-- PASS: Restore returns archived ideas to their previous non-archived status, with Refining fallback in runtime.
-- PASS: Delete is guarded so Project ideas cannot be deleted unless archived first.
-- PASS: Status dropdown uses New, Exploring, Refining, Ready, Project, and Archived.
-- PASS: Added compact Show checkbox dropdown beside the visible table description line, with default statuses New, Exploring, Refining, Ready, and Project selected and Archived unselected.
-- PASS: Show dropdown supports any checkbox combination plus Select All and Clear All.
-- PASS: Updated creator-facing copy to "Capture, compare, and shape game ideas." and "Scan, compare, and update early ideas."
-- PASS: Replaced creator-visible Tool Display Mode navigation error with safe copy that does not mention server/API/local server/port/implementation details.
-- PASS: Chevron renders to the left of the Idea text, inline, same size, same color, and on the same text line; the whole Idea cell remains the expansion target.
-- PASS: Notes count remains informational only.
-- PASS: Table-first structure, inline Add Idea/Add Note, row-level Save/Cancel, status dropdown edit mode, notes indentation, and single-open accordion behavior remain covered.
+## Changed Files
+- src/engine/audio/TextToSpeechEngine.js
+- toolbox/text-to-speech/index.html
+- toolbox/text-to-speech/text2speech.js
+- tests/playwright/tools/TextToSpeechFunctional.spec.mjs
+- docs_build/dev/reports/coverage_changed_js_guardrail.txt
+- docs_build/dev/reports/playwright_v8_coverage_report.txt
+- docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
+- docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
+- docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
+- docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
+- docs_build/dev/reports/PR_26171_061-validation.md
+- docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
+- docs_build/dev/reports/codex_review.diff
+- docs_build/dev/reports/codex_changed_files.txt
## Validation
-- PASS: `node --check toolbox/idea-board/index.js`.
-- PASS: `node --check assets/theme-v2/js/tool-display-mode.js`.
-- PASS: `node --check src/shared/toolbox/tool-metadata-inventory.js`.
-- PASS: `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`.
-- PASS: `node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs`.
-- PASS: `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line --timeout=90000`.
-- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line -g "Idea Board launches" --timeout=90000`.
-- PASS: `npm run test:workspace-v2` (workspace-contract lane, 5 passed).
+- PASS: `node --check src\engine\audio\TextToSpeechEngine.js`.
+- PASS: `node --check toolbox\text-to-speech\text2speech.js`.
+- PASS: `node --test tests\tools\Text2SpeechShell.test.mjs`.
+- PASS: `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`.
+- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace).
- PASS: `git diff --check`.
-- PASS: Post-conflict rerun completed after merging `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`.
-- PASS: Playwright V8 coverage report produced because runtime JavaScript changed.
-- WARN: Coverage report marks `src/shared/toolbox/tool-metadata-inventory.js` as not collected by browser V8 coverage; advisory only per project instructions.
-- SKIPPED: Full samples smoke was not run per request.
+- PASS: Post-conflict validation rerun after merging `origin/main` into the PR branch.
## ZIP
-- Path: `tmp/PR_26171_041-idea-board-production-polish_delta.zip`.
-- Size: final size reported in the delivery summary after conflict-resolution ZIP refresh.
-- Contents:
- - assets/theme-v2/css/tables.css
- - assets/theme-v2/js/tool-display-mode.js
- - docs_build/dev/reports/codex_changed_files.txt
- - docs_build/dev/reports/codex_review.diff
- - docs_build/dev/reports/coverage_changed_js_guardrail.txt
- - docs_build/dev/reports/playwright_v8_coverage_report.txt
- - src/shared/toolbox/tool-metadata-inventory.js
- - tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
- - tests/playwright/tools/ToolboxRoutePages.spec.mjs
- - toolbox/idea-board/index.html
- - toolbox/idea-board/index.js
+- Path: `tmp/PR_26171_061-text2speech-engine-audio-feature-parity_delta.zip`.
diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff
index 9d13c1ccf..98f10d77f 100644
--- a/docs_build/dev/reports/codex_review.diff
+++ b/docs_build/dev/reports/codex_review.diff
@@ -1,886 +1,1995 @@
-diff --git a/assets/theme-v2/css/tables.css b/assets/theme-v2/css/tables.css
-index a8c75090a..c39ef19f0 100644
---- a/assets/theme-v2/css/tables.css
-+++ b/assets/theme-v2/css/tables.css
-@@ -38,6 +38,67 @@ td {
- color: var(--text)
- }
-
-+.idea-board-table-caption {
-+ position: relative;
-+ z-index: var(--z-index-sm);
-+ display: flex;
-+ align-items: center;
-+ justify-content: space-between;
-+ gap: var(--space-10);
-+ padding: var(--space-10);
-+ font-weight: var(--font-weight-heavy)
-+}
+diff --git a/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md b/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
+new file mode 100644
+index 000000000..5e18e0955
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
+@@ -0,0 +1,25 @@
++# PR_26171_061 Engine Audio Ownership Checklist
+
-+.idea-board-show-filter {
-+ position: relative;
-+ z-index: var(--z-index-md);
-+ color: var(--text);
-+ font-size: var(--font-size-xs);
-+ font-weight: var(--font-weight-bold)
-+}
++## Engine Ownership
+
-+.idea-board-show-filter summary {
-+ display: inline-flex;
-+ align-items: center;
-+ min-height: var(--space-28);
-+ padding: var(--space-3) var(--space-10);
-+ border: var(--border-standard);
-+ border-radius: var(--radius-md);
-+ background: var(--panel-soft);
-+ cursor: pointer;
-+ list-style: none
-+}
++- PASS: Reusable TTS voice filtering lives in `src/engine/audio/TextToSpeechEngine.js`.
++- PASS: Reusable TTS preset shaping lives in `src/engine/audio/TextToSpeechEngine.js`.
++- PASS: Reusable TTS queue item helpers live in `src/engine/audio/TextToSpeechEngine.js`.
++- PASS: Existing defaults remain in `src/engine/audio/TextToSpeechDefaults.js`.
++- PASS: Active toolbox UI consumes the engine module instead of duplicating all engine behavior locally.
+
-+.idea-board-show-filter summary::-webkit-details-marker {
-+ display: none
-+}
++## Active Tool Ownership
+
-+.idea-board-show-filter__menu {
-+ position: static;
-+ min-width: 12rem;
-+ margin-top: var(--space-6);
-+ padding: var(--space-10);
-+ border: var(--border-standard);
-+ border-radius: var(--radius-md);
-+ background: var(--panel);
-+ box-shadow: var(--shadow-md)
-+}
++- PASS: Active tool remains under `toolbox/text-to-speech/`.
++- PASS: Active HTML remains Theme V2 only.
++- PASS: Active JavaScript remains external.
++- PASS: Active tool owns DOM wiring, status display, and control event handling.
++- PASS: Active tool does not create database behavior.
+
-+.idea-board-show-filter__options {
-+ display: grid;
-+ gap: var(--space-6);
-+ margin-top: var(--space-10)
-+}
++## Boundary Checks
+
-+.idea-board-show-filter__option {
-+ display: flex;
-+ align-items: center;
-+ gap: var(--space-8);
-+ color: var(--text);
-+ font-weight: var(--font-weight-regular);
-+ white-space: nowrap
-+}
++- PASS: No `tools/text2speech/` path was created.
++- PASS: Archived `old_text2speech-V2` was used as a read-only functionality sample.
++- PASS: Archived tool files were not activated in navigation.
++- PASS: Browser SpeechSynthesis is the implemented local provider.
++- PASS: Planned providers remain documented but do not block browser preview.
+diff --git a/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md b/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
+new file mode 100644
+index 000000000..ffb2cb288
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
+@@ -0,0 +1,32 @@
++# PR_26171_061 Instruction Compliance Checklist
+
- .data-table th {
- color: var(--gold);
- font-size: var(--font-size-sm);
-@@ -60,8 +121,13 @@ td {
- gap: .35em;
- color: inherit;
- font: inherit;
-- line-height: inherit;
-- vertical-align: baseline
-+ line-height: var(--line-height-single);
-+ vertical-align: baseline;
-+ white-space: nowrap
-+}
++## Pre-Work Gate
+
-+.idea-board-idea-label__text {
-+ line-height: var(--line-height-single)
- }
-
- .idea-board-idea-chevron {
-diff --git a/assets/theme-v2/js/tool-display-mode.js b/assets/theme-v2/js/tool-display-mode.js
-index 9c23ba67e..e77b7a9df 100644
---- a/assets/theme-v2/js/tool-display-mode.js
-+++ b/assets/theme-v2/js/tool-display-mode.js
-@@ -157,7 +157,7 @@
- const diagnostic = document.createElement("p");
- diagnostic.className = "status";
- diagnostic.setAttribute("role", "status");
-- diagnostic.textContent = "Tool navigation could not load from the server API. Start the local server API and refresh.";
-+ diagnostic.textContent = "Tool navigation is temporarily unavailable. Refresh the page or try again shortly.";
- body.appendChild(diagnostic);
- }
- }
++- PASS: Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` before file changes.
++- PASS: Read `docs_build/dev/PROJECT_MULTI_PC.txt` before file changes.
++- PASS: Reported instruction compliance PASS/FAIL before file changes.
++- PASS: `git branch --show-current` reported `main` before branch creation.
++- PASS: `git checkout main` completed before branch creation.
++- PASS: `git pull origin main` completed before branch creation.
++- PASS: `git status` reported a clean working tree before branch creation.
++- PASS: Created branch `pr/26171-061-text2speech-engine-audio-feature-parity` from clean latest `main`.
++
++## Owner And Parity
++
++- PASS: PR number `061` is odd.
++- PASS: Odd parity maps to Laptop / Environment 2.
++- PASS: Text To Speech / TTS is owned by Laptop / Environment 2.
++- PASS: Implementation path matches active owner path: `toolbox/text-to-speech/`.
++- PASS: Shared engine path matches approved reusable audio path: `src/engine/audio/`.
++
++## Hard Stop Checks
++
++- PASS: Instructions were present and readable.
++- PASS: Branch workflow could be followed.
++- PASS: Repository was clean before branching.
++- PASS: No owner mismatch.
++- PASS: No parity mismatch.
++- PASS: No wrong implementation path.
++- PASS: Scoped validation was not skipped.
++- PASS: Required reports were created.
++- PASS: Required repo ZIP was created.
+diff --git a/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md b/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
+new file mode 100644
+index 000000000..3341b14ba
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
+@@ -0,0 +1,27 @@
++# PR_26171_061 Manual Validation Notes
++
++## Manual Review
++
++- Confirmed active Text To Speech path is `toolbox/text-to-speech/`.
++- Confirmed no `tools/text2speech/` directory was created.
++- Confirmed old functionality sample path is `archive/v1-v2/tools/old_text2speech-V2/`.
++- Confirmed active HTML has external scripts only.
++- Confirmed active HTML has no inline style attributes or inline event handlers.
++- Confirmed no database files were changed.
++
++## Automated Browser Coverage Used For Manual Equivalents
++
++- Opened active Text To Speech page through targeted Playwright validation.
++- Verified browser voices render.
++- Verified restored control groups render.
++- Verified preset shaping updates pitch and volume values.
++- Verified named sentence Add, Duplicate, and Delete.
++- Verified output summary includes queue JSON.
++- Verified Speak, Pause, Resume, and Stop call the browser SpeechSynthesis mock.
++- Verified unavailable SpeechSynthesis shows actionable error.
++
++## Out Of Scope Manual Checks
++
++- No paid provider was manually exercised.
++- No generated audio file export was manually exercised.
++- No database behavior was manually exercised.
+diff --git a/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md b/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
+new file mode 100644
+index 000000000..e09e0f974
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
+@@ -0,0 +1,48 @@
++# PR_26171_061 Old TTS Feature Parity Checklist
++
++Functionality sample: `archive/v1-v2/tools/old_text2speech-V2/`
++
++## Controls And Options
++
++- PASS: Gender helper filter restored.
++- PASS: Language filter restored.
++- PASS: Voice dropdown restored.
++- PASS: Voice details restored.
++- PASS: Voice Age select restored.
++- PASS: Character Preset select restored.
++- PASS: SSML-like Preset select restored.
++- PASS: Volume slider restored with visible value.
++- PASS: Rate / Speed slider restored with visible value.
++- PASS: Pitch slider restored with visible value.
++- PASS: Name field restored.
++- PASS: Text To Speak editor restored.
++
++## Queue And JSON
++
++- PASS: Named Sentences queue restored.
++- PASS: Add named sentence restored.
++- PASS: Duplicate named sentence restored.
++- PASS: Delete named sentence restored.
++- PASS: Output Summary JSON restored.
++- PASS: Import JSON restored for standalone launch.
++- PASS: Copy JSON restored for standalone launch.
++- PASS: Export JSON restored for standalone launch.
++- PASS: URL JSON source loading restored through `samplePresetPath`.
++
++## Playback And Status
++
++- PASS: Speak restored.
++- PASS: Pause restored when browser support exists.
++- PASS: Resume restored when browser support exists.
++- PASS: Stop restored.
++- PASS: Clearable status log restored.
++- PASS: Actionable unavailable-browser error restored.
++- PASS: No placeholder provider-blocking behavior remains for browser preview.
++
++## Workspace
++
++- PASS: Project Workspace launch detection restored.
++- PASS: Standalone JSON actions hide during Project Workspace launch.
++- PASS: Return to Project Workspace action restored.
++- PASS: Project Workspace toolState loading restored.
++- PASS: Project Workspace dirty-state writeback restored.
+diff --git a/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md b/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
+new file mode 100644
+index 000000000..b2f4a44d3
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
+@@ -0,0 +1,71 @@
++# PR_26171_061 Text To Speech Engine Audio Feature Parity
++
++## Summary
++
++Restored active Text To Speech feature parity from the archived `old_text2speech-V2` functionality sample while keeping active implementation in `toolbox/text-to-speech/` and reusable engine behavior in `src/engine/audio/`.
++
++## Changed Scope
++
++- Rebuilt reusable Text To Speech voice filtering, preset shaping, and queue-item helpers in `src/engine/audio/TextToSpeechEngine.js`.
++- Expanded `toolbox/text-to-speech/index.html` with Theme V2 controls for the old V2 feature set.
++- Rebuilt `toolbox/text-to-speech/text2speech.js` so the active tool consumes the engine audio module.
++- Expanded targeted browser validation in `tests/playwright/tools/TextToSpeechFunctional.spec.mjs`.
++- Updated Playwright V8 coverage artifacts for changed runtime JavaScript.
++
++## Requirement Checklist
++
++- PASS: Used `archive/v1-v2/tools/old_text2speech-V2/` as the required functionality sample.
++- PASS: Reusable TTS behavior lives in `src/engine/audio/`.
++- PASS: `toolbox/text-to-speech/` consumes `src/engine/audio/TextToSpeechEngine.js`.
++- PASS: Restored old controls/options/features in the active tool.
++- PASS: Browser SpeechSynthesis provider remains implemented.
++- PASS: Planned provider adapters do not block browser preview behavior.
++- PASS: No `tools/text2speech/` path was created.
++- PASS: No database files were changed.
++- PASS: Theme V2 remains the styling source.
++- PASS: HTML uses external JavaScript only.
++- PASS: No inline script, style, or event handler was added.
++
++## Restored Feature Set
++
++- Import JSON.
++- Copy JSON.
++- Export JSON.
++- Project Workspace return action during workspace launch.
++- Gender helper filter.
++- Language filter.
++- Browser voice selection.
++- Voice details.
++- Voice age shaping.
++- Character presets.
++- SSML-like presets.
++- Volume, rate, and pitch sliders with visible values.
++- Named sentence Name field.
++- Add, Duplicate, and Delete named sentence actions.
++- Text To Speak editor.
++- Speak, Pause, Resume, and Stop playback actions.
++- Named Sentences queue.
++- Output Summary JSON.
++- Clearable Status log.
++- URL JSON preset loading through `samplePresetPath`.
++- Project Workspace toolState loading and dirty-state writeback.
++
++## Validation Summary
++
++- PASS: `node --check src\engine\audio\TextToSpeechEngine.js`
++- PASS: `node --check toolbox\text-to-speech\text2speech.js`
++- PASS: `node --test tests\tools\Text2SpeechShell.test.mjs`
++- PASS: `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`
++- PASS: `npm run test:workspace-v2`
++ - Note: command name is legacy; user-facing language is Project Workspace.
++- PASS: `git diff --check`
++- PASS: changed runtime JS coverage collected for `src/engine/audio/TextToSpeechEngine.js`.
++- PASS: changed runtime JS coverage collected for `toolbox/text-to-speech/text2speech.js`.
++
++## Out Of Scope
++
++- No database changes.
++- No external paid provider implementation.
++- No generated audio file export provider.
++- No new `tools/text2speech/` path.
++- No archived tool activation.
+diff --git a/docs_build/dev/reports/PR_26171_061-validation.md b/docs_build/dev/reports/PR_26171_061-validation.md
+new file mode 100644
+index 000000000..bf864dc27
+--- /dev/null
++++ b/docs_build/dev/reports/PR_26171_061-validation.md
+@@ -0,0 +1,42 @@
++# PR_26171_061 Validation Report
++
++## Commands Run
++
++- `node --check src\engine\audio\TextToSpeechEngine.js`
++ - PASS.
++- `node --check toolbox\text-to-speech\text2speech.js`
++ - PASS.
++- `node --test tests\tools\Text2SpeechShell.test.mjs`
++ - PASS: 3 tests passed.
++- `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`
++ - PASS: 2 tests passed.
++ - Covers restored controls, preset shaping, queue add/duplicate/delete, output summary, pause/resume, speak, stop, and unavailable SpeechSynthesis error handling.
++ - PASS: rerun after merging `origin/main` into the PR branch for conflict resolution.
++- `npm run test:workspace-v2`
++ - PASS: 5 Project Workspace tests passed.
++ - Note: command name is legacy; user-facing language is Project Workspace.
++ - Note: first execution used a 120s timeout and timed out before completion; rerun with a longer timeout completed successfully.
++ - PASS: rerun after merging `origin/main` into the PR branch for conflict resolution.
++- `git diff --check`
++ - PASS.
++
++## Coverage
++
++- PASS: `docs_build/dev/reports/playwright_v8_coverage_report.txt` produced changed runtime JS coverage.
++- PASS: `docs_build/dev/reports/coverage_changed_js_guardrail.txt` reports no changed runtime JS coverage warnings.
++- PASS: `src/engine/audio/TextToSpeechEngine.js` covered at 71%.
++- PASS: `toolbox/text-to-speech/text2speech.js` covered at 71%.
++
++## Artifact Verification
++
++- PASS: `docs_build/dev/reports/codex_review.diff` exists.
++- PASS: `docs_build/dev/reports/codex_changed_files.txt` exists.
++- PASS: `tmp/PR_26171_061-text2speech-engine-audio-feature-parity_delta.zip` exists.
++- PASS: ZIP size is greater than zero.
++- PASS: ZIP contents preserve repo-relative paths.
++
++## Skipped
++
++- Full samples validation skipped because no sample JSON or sample runtime behavior changed.
++- Database validation skipped because no database files or runtime persistence changed.
++- External provider validation skipped because paid/provider generation is out of scope and browser SpeechSynthesis is the implemented provider for this PR.
diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
-index 4b9b70b93..bb7de5d8d 100644
+index bb7de5d8d..2a3c37ce3 100644
--- a/docs_build/dev/reports/codex_changed_files.txt
+++ b/docs_build/dev/reports/codex_changed_files.txt
-@@ -1,37 +1,91 @@
--# Codex Changed Files - PR_26171_063-codex-instruction-enforcement-hardening
-+# Codex Changed Files - PR_26171_041-idea-board-production-polish
-
--## Conflict Resolution Note
--- Merged latest `origin/main` into the PR branch after GitHub reported merge conflicts.
--- Regenerated Codex artifacts from the PR_063 delta against updated `origin/main`.
--- No implementation scope was changed while resolving conflicts.
-+## Git Workflow
-+- Current branch: `codex/pr-26171-041-idea-board-production-polish`.
-+- Expected starting branch: `main` (PASS before branch creation).
-+- Created branch: `codex/pr-26171-041-idea-board-production-polish`.
-+- Commit before conflict resolution: `ca626873a4cf19925229851b0da2a4ff55c2073f`.
-+- Initial push result: PASS, branch pushed to `origin/codex/pr-26171-041-idea-board-production-polish`.
-+- PR URL: `https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/17`.
-+- Conflict resolution: merged `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`; conflicts were limited to `codex_changed_files.txt` and `codex_review.diff`.
-+- Conflict resolution push result: pending until after merge-validation commit.
-+- Merge result: pending until after PR validation/merge.
-+- Final main sync: pending until after merge and final pull.
-
--## Changed Files
--- docs_build/dev/PROJECT_INSTRUCTIONS.md
--- docs_build/dev/PROJECT_MULTI_PC.txt
--- docs_build/dev/reports/PR_26171_063-codex-instruction-enforcement-hardening.md
--- docs_build/dev/reports/PR_26171_063-instruction-compliance-checklist.md
--- docs_build/dev/reports/PR_26171_063-validation.md
--- docs_build/dev/reports/PR_26171_063-manual-validation-notes.md
--- docs_build/dev/reports/codex_review.diff
--- docs_build/dev/reports/codex_changed_files.txt
-+## Git Status
-+```text
-+ M assets/theme-v2/css/tables.css
-+ M assets/theme-v2/js/tool-display-mode.js
-+ M docs_build/dev/reports/codex_changed_files.txt
-+ M docs_build/dev/reports/codex_review.diff
-+ M docs_build/dev/reports/coverage_changed_js_guardrail.txt
-+ M docs_build/dev/reports/playwright_v8_coverage_report.txt
-+ M src/shared/toolbox/tool-metadata-inventory.js
-+ M tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
-+ M tests/playwright/tools/ToolboxRoutePages.spec.mjs
-+ M toolbox/idea-board/index.html
-+ M toolbox/idea-board/index.js
-+```
-
--## Git Diff Stat Against Updated origin/main
-+## Diff Stat
+@@ -1,91 +1,54 @@
+-# Codex Changed Files - PR_26171_041-idea-board-production-polish
++# Codex Changed Files - PR_26171_061-text2speech-engine-audio-feature-parity
+
+ ## Git Workflow
+-- Current branch: `codex/pr-26171-041-idea-board-production-polish`.
+-- Expected starting branch: `main` (PASS before branch creation).
+-- Created branch: `codex/pr-26171-041-idea-board-production-polish`.
+-- Commit before conflict resolution: `ca626873a4cf19925229851b0da2a4ff55c2073f`.
+-- Initial push result: PASS, branch pushed to `origin/codex/pr-26171-041-idea-board-production-polish`.
+-- PR URL: `https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/17`.
+-- Conflict resolution: merged `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`; conflicts were limited to `codex_changed_files.txt` and `codex_review.diff`.
+-- Conflict resolution push result: pending until after merge-validation commit.
+-- Merge result: pending until after PR validation/merge.
+-- Final main sync: pending until after merge and final pull.
++- Starting branch: `main`.
++- Created branch: `pr/26171-061-text2speech-engine-audio-feature-parity`.
++- Initial commit: `e4541d63719ab777b0654c8fecf4b13237d31256`.
++- PR branch was merged with `origin/main` during PR conflict recovery.
++- Conflicts were limited to generated report artifacts.
+
+-## Git Status
++## Scoped Diff Stat
```text
-- docs_build/dev/PROJECT_INSTRUCTIONS.md | 47 ++
-- docs_build/dev/PROJECT_MULTI_PC.txt | 58 ++
-- ..._063-codex-instruction-enforcement-hardening.md | 48 ++
-- ...R_26171_063-instruction-compliance-checklist.md | 37 ++
-- .../PR_26171_063-manual-validation-notes.md | 19 +
-- docs_build/dev/reports/PR_26171_063-validation.md | 32 +
-- docs_build/dev/reports/codex_changed_files.txt | 48 ++
-- docs_build/dev/reports/codex_review.diff | 699 +++++++++------------
-- 8 files changed, 569 insertions(+), 419 deletions(-)
-+ assets/theme-v2/css/tables.css | 70 +++++++-
-+ assets/theme-v2/js/tool-display-mode.js | 2 +-
-+ .../dev/reports/coverage_changed_js_guardrail.txt | 5 +-
-+ .../dev/reports/playwright_v8_coverage_report.txt | 11 +-
-+ src/shared/toolbox/tool-metadata-inventory.js | 6 +-
-+ .../playwright/tools/IdeaBoardTableNotes.spec.mjs | 71 +++++++-
-+ tests/playwright/tools/ToolboxRoutePages.spec.mjs | 26 ++-
-+ toolbox/idea-board/index.html | 31 ++--
-+ toolbox/idea-board/index.js | 191 ++++++++++++++++++++-
-+ 9 files changed, 368 insertions(+), 45 deletions(-)
+- M assets/theme-v2/css/tables.css
+- M assets/theme-v2/js/tool-display-mode.js
+- M docs_build/dev/reports/codex_changed_files.txt
+- M docs_build/dev/reports/codex_review.diff
+- M docs_build/dev/reports/coverage_changed_js_guardrail.txt
+- M docs_build/dev/reports/playwright_v8_coverage_report.txt
+- M src/shared/toolbox/tool-metadata-inventory.js
+- M tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+- M tests/playwright/tools/ToolboxRoutePages.spec.mjs
+- M toolbox/idea-board/index.html
+- M toolbox/idea-board/index.js
++...R_26171_061-engine-audio-ownership-checklist.md | 25 +
++ ...R_26171_061-instruction-compliance-checklist.md | 32 +
++ .../PR_26171_061-manual-validation-notes.md | 27 +
++ ...R_26171_061-old-tts-feature-parity-checklist.md | 48 ++
++ ..._061-text2speech-engine-audio-feature-parity.md | 71 ++
++ docs_build/dev/reports/PR_26171_061-validation.md | 42 +
++ docs_build/dev/reports/codex_changed_files.txt | 118 +--
++ .../dev/reports/coverage_changed_js_guardrail.txt | 7 +-
++ .../dev/reports/playwright_v8_coverage_report.txt | 36 +-
++ src/engine/audio/TextToSpeechEngine.js | 208 ++++-
++ .../tools/TextToSpeechFunctional.spec.mjs | 30 +
++ toolbox/text-to-speech/index.html | 119 +--
++ toolbox/text-to-speech/text2speech.js | 853 +++++++++++++++++----
++ 13 files changed, 1317 insertions(+), 299 deletions(-)
```
-+## Requirement Evidence
-+- PASS: Removed creator-visible Create Project from the right-side Next Steps area; no disabled `data-idea-board-create-project` panel remains.
-+- PASS: Create Project is shown only in a Ready idea row's Actions column.
-+- PASS: Clicking Create Project changes the idea status from Ready to Project and replaces Create Project/Delete with Open Project and Archive.
-+- PASS: Project rows show Open Project and Archive and do not show Delete.
-+- PASS: Added Archived status; archived rows are hidden by default.
-+- PASS: Archived rows show Restore and Delete.
-+- PASS: Restore returns archived ideas to their previous non-archived status, with Refining fallback in runtime.
-+- PASS: Delete is guarded so Project ideas cannot be deleted unless archived first.
-+- PASS: Status dropdown uses New, Exploring, Refining, Ready, Project, and Archived.
-+- PASS: Added compact Show checkbox dropdown beside the visible table description line, with default statuses New, Exploring, Refining, Ready, and Project selected and Archived unselected.
-+- PASS: Show dropdown supports any checkbox combination plus Select All and Clear All.
-+- PASS: Updated creator-facing copy to "Capture, compare, and shape game ideas." and "Scan, compare, and update early ideas."
-+- PASS: Replaced creator-visible Tool Display Mode navigation error with safe copy that does not mention server/API/local server/port/implementation details.
-+- PASS: Chevron renders to the left of the Idea text, inline, same size, same color, and on the same text line; the whole Idea cell remains the expansion target.
-+- PASS: Notes count remains informational only.
-+- PASS: Table-first structure, inline Add Idea/Add Note, row-level Save/Cancel, status dropdown edit mode, notes indentation, and single-open accordion behavior remain covered.
-+
+-## Diff Stat
+-```text
+- assets/theme-v2/css/tables.css | 70 +++++++-
+- assets/theme-v2/js/tool-display-mode.js | 2 +-
+- .../dev/reports/coverage_changed_js_guardrail.txt | 5 +-
+- .../dev/reports/playwright_v8_coverage_report.txt | 11 +-
+- src/shared/toolbox/tool-metadata-inventory.js | 6 +-
+- .../playwright/tools/IdeaBoardTableNotes.spec.mjs | 71 +++++++-
+- tests/playwright/tools/ToolboxRoutePages.spec.mjs | 26 ++-
+- toolbox/idea-board/index.html | 31 ++--
+- toolbox/idea-board/index.js | 191 ++++++++++++++++++++-
+- 9 files changed, 368 insertions(+), 45 deletions(-)
+-```
+-
+-## Requirement Evidence
+-- PASS: Removed creator-visible Create Project from the right-side Next Steps area; no disabled `data-idea-board-create-project` panel remains.
+-- PASS: Create Project is shown only in a Ready idea row's Actions column.
+-- PASS: Clicking Create Project changes the idea status from Ready to Project and replaces Create Project/Delete with Open Project and Archive.
+-- PASS: Project rows show Open Project and Archive and do not show Delete.
+-- PASS: Added Archived status; archived rows are hidden by default.
+-- PASS: Archived rows show Restore and Delete.
+-- PASS: Restore returns archived ideas to their previous non-archived status, with Refining fallback in runtime.
+-- PASS: Delete is guarded so Project ideas cannot be deleted unless archived first.
+-- PASS: Status dropdown uses New, Exploring, Refining, Ready, Project, and Archived.
+-- PASS: Added compact Show checkbox dropdown beside the visible table description line, with default statuses New, Exploring, Refining, Ready, and Project selected and Archived unselected.
+-- PASS: Show dropdown supports any checkbox combination plus Select All and Clear All.
+-- PASS: Updated creator-facing copy to "Capture, compare, and shape game ideas." and "Scan, compare, and update early ideas."
+-- PASS: Replaced creator-visible Tool Display Mode navigation error with safe copy that does not mention server/API/local server/port/implementation details.
+-- PASS: Chevron renders to the left of the Idea text, inline, same size, same color, and on the same text line; the whole Idea cell remains the expansion target.
+-- PASS: Notes count remains informational only.
+-- PASS: Table-first structure, inline Add Idea/Add Note, row-level Save/Cancel, status dropdown edit mode, notes indentation, and single-open accordion behavior remain covered.
++## Changed Files
++- src/engine/audio/TextToSpeechEngine.js
++- toolbox/text-to-speech/index.html
++- toolbox/text-to-speech/text2speech.js
++- tests/playwright/tools/TextToSpeechFunctional.spec.mjs
++- docs_build/dev/reports/coverage_changed_js_guardrail.txt
++- docs_build/dev/reports/playwright_v8_coverage_report.txt
++- docs_build/dev/reports/PR_26171_061-text2speech-engine-audio-feature-parity.md
++- docs_build/dev/reports/PR_26171_061-instruction-compliance-checklist.md
++- docs_build/dev/reports/PR_26171_061-old-tts-feature-parity-checklist.md
++- docs_build/dev/reports/PR_26171_061-engine-audio-ownership-checklist.md
++- docs_build/dev/reports/PR_26171_061-validation.md
++- docs_build/dev/reports/PR_26171_061-manual-validation-notes.md
++- docs_build/dev/reports/codex_review.diff
++- docs_build/dev/reports/codex_changed_files.txt
+
## Validation
--- PASS: `git diff --check` after conflict resolution.
--- PASS: targeted required instruction-anchor validation after conflict resolution.
--- SKIP: Playwright, because the PR explicitly requires no Playwright and changes docs/workflow only.
-+- PASS: `node --check toolbox/idea-board/index.js`.
-+- PASS: `node --check assets/theme-v2/js/tool-display-mode.js`.
-+- PASS: `node --check src/shared/toolbox/tool-metadata-inventory.js`.
-+- PASS: `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`.
-+- PASS: `node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs`.
-+- PASS: `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line --timeout=90000`.
-+- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line -g "Idea Board launches" --timeout=90000`.
-+- PASS: `npm run test:workspace-v2` (workspace-contract lane, 5 passed).
-+- PASS: `git diff --check`.
-+- PASS: Post-conflict rerun completed after merging `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`.
-+- PASS: Playwright V8 coverage report produced because runtime JavaScript changed.
-+- WARN: Coverage report marks `src/shared/toolbox/tool-metadata-inventory.js` as not collected by browser V8 coverage; advisory only per project instructions.
-+- SKIPPED: Full samples smoke was not run per request.
+-- PASS: `node --check toolbox/idea-board/index.js`.
+-- PASS: `node --check assets/theme-v2/js/tool-display-mode.js`.
+-- PASS: `node --check src/shared/toolbox/tool-metadata-inventory.js`.
+-- PASS: `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`.
+-- PASS: `node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs`.
+-- PASS: `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line --timeout=90000`.
+-- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line -g "Idea Board launches" --timeout=90000`.
+-- PASS: `npm run test:workspace-v2` (workspace-contract lane, 5 passed).
++- PASS: `node --check src\engine\audio\TextToSpeechEngine.js`.
++- PASS: `node --check toolbox\text-to-speech\text2speech.js`.
++- PASS: `node --test tests\tools\Text2SpeechShell.test.mjs`.
++- PASS: `npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs`.
++- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace).
+ - PASS: `git diff --check`.
+-- PASS: Post-conflict rerun completed after merging `origin/main` at `0a364f85516aea82e38eea2f498fde5980f9adf9`.
+-- PASS: Playwright V8 coverage report produced because runtime JavaScript changed.
+-- WARN: Coverage report marks `src/shared/toolbox/tool-metadata-inventory.js` as not collected by browser V8 coverage; advisory only per project instructions.
+-- SKIPPED: Full samples smoke was not run per request.
++- PASS: Post-conflict validation rerun after merging `origin/main` into the PR branch.
## ZIP
--- Path: `tmp/PR_26171_063-codex-instruction-enforcement-hardening_delta.zip`.
-+- Path: `tmp/PR_26171_041-idea-board-production-polish_delta.zip`.
-+- Size: final size reported in the delivery summary after conflict-resolution ZIP refresh.
-+- Contents:
-+ - assets/theme-v2/css/tables.css
-+ - assets/theme-v2/js/tool-display-mode.js
-+ - docs_build/dev/reports/codex_changed_files.txt
-+ - docs_build/dev/reports/codex_review.diff
-+ - docs_build/dev/reports/coverage_changed_js_guardrail.txt
-+ - docs_build/dev/reports/playwright_v8_coverage_report.txt
-+ - src/shared/toolbox/tool-metadata-inventory.js
-+ - tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
-+ - tests/playwright/tools/ToolboxRoutePages.spec.mjs
-+ - toolbox/idea-board/index.html
-+ - toolbox/idea-board/index.js
+-- Path: `tmp/PR_26171_041-idea-board-production-polish_delta.zip`.
+-- Size: final size reported in the delivery summary after conflict-resolution ZIP refresh.
+-- Contents:
+- - assets/theme-v2/css/tables.css
+- - assets/theme-v2/js/tool-display-mode.js
+- - docs_build/dev/reports/codex_changed_files.txt
+- - docs_build/dev/reports/codex_review.diff
+- - docs_build/dev/reports/coverage_changed_js_guardrail.txt
+- - docs_build/dev/reports/playwright_v8_coverage_report.txt
+- - src/shared/toolbox/tool-metadata-inventory.js
+- - tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+- - tests/playwright/tools/ToolboxRoutePages.spec.mjs
+- - toolbox/idea-board/index.html
+- - toolbox/idea-board/index.js
++- Path: `tmp/PR_26171_061-text2speech-engine-audio-feature-parity_delta.zip`.
diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt
-index 076d27690..66dd0cb12 100644
+index 66dd0cb12..807990e9f 100644
--- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt
+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt
-@@ -6,10 +6,9 @@ Missing changed runtime JS files are WARN, not FAIL.
+@@ -6,9 +6,8 @@ Missing changed runtime JS files are WARN, not FAIL.
Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
Changed runtime JS files considered:
--(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
- (0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
--(70%) toolbox/idea-board/index.js - executed lines 480/480; executed functions 23/33
-+(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
-+(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
+-(0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
+-(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
+-(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
++(71%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 37/52
++(71%) toolbox/text-to-speech/text2speech.js - executed lines 835/835; executed functions 61/86
Guardrail warnings:
--(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only
- (0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file missing from coverage; advisory only
+-(0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file missing from coverage; advisory only
++(100%) none - no changed runtime JS coverage warnings
diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt
-index cb35b4d50..4a9f71ff1 100644
+index 4a9f71ff1..dd573eabd 100644
--- a/docs_build/dev/reports/playwright_v8_coverage_report.txt
+++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt
-@@ -17,9 +17,9 @@ Exercised tool entry points detected:
- (63%) Theme V2 Shared JS - exercised 2 runtime JS files
+@@ -12,34 +12,28 @@ Note: entry percentages use function coverage when available, otherwise line cov
+ Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran.
+
+ Exercised tool entry points detected:
+-(78%) Toolbox Index - exercised 4 runtime JS files
++(72%) Toolbox Index - exercised 2 runtime JS files
+ (0%) Tool Template V2 - not exercised by this Playwright run
+-(63%) Theme V2 Shared JS - exercised 2 runtime JS files
++(56%) Theme V2 Shared JS - exercised 2 runtime JS files
Changed runtime JS files covered:
--(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
- (0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
--(70%) toolbox/idea-board/index.js - executed lines 480/480; executed functions 23/33
-+(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
-+(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
+-(0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
+-(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
+-(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
++(71%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 37/52
++(71%) toolbox/text-to-speech/text2speech.js - executed lines 835/835; executed functions 61/86
Files with executed line/function counts where available:
- (25%) src/api/session-api-client.js - executed lines 68/68; executed functions 3/12
-@@ -30,17 +30,16 @@ Files with executed line/function counts where available:
- (65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26
- (67%) src/api/game-journey-completion-api-client.js - executed lines 15/15; executed functions 2/3
- (67%) toolbox/game-workspace/game-workspace-api-client.js - executed lines 20/20; executed functions 2/3
--(70%) toolbox/idea-board/index.js - executed lines 480/480; executed functions 23/33
-+(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
- (78%) toolbox/tools-page-accordions.js - executed lines 1156/1156; executed functions 87/111
- (86%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 25/29
+-(25%) src/api/session-api-client.js - executed lines 68/68; executed functions 3/12
+-(33%) src/api/toolbox-votes-api-client.js - executed lines 46/46; executed functions 2/6
+-(44%) src/api/server-api-client.js - executed lines 167/167; executed functions 8/18
+-(63%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 54/86
++(36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14
++(38%) src/api/public-config-client.js - executed lines 209/209; executed functions 10/26
++(54%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 46/85
+ (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
+-(65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26
+-(67%) src/api/game-journey-completion-api-client.js - executed lines 15/15; executed functions 2/3
+-(67%) toolbox/game-workspace/game-workspace-api-client.js - executed lines 20/20; executed functions 2/3
+-(74%) toolbox/idea-board/index.js - executed lines 640/640; executed functions 37/50
+-(78%) toolbox/tools-page-accordions.js - executed lines 1156/1156; executed functions 87/111
+-(86%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 25/29
++(71%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 37/52
++(71%) toolbox/text-to-speech/text2speech.js - executed lines 835/835; executed functions 61/86
++(76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29
++(100%) src/engine/audio/TextToSpeechDefaults.js - executed lines 108/108; executed functions 1/1
Uncovered or low-coverage changed JS files:
--(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only
- (0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: uncovered changed runtime JS file; advisory only
+-(0%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: uncovered changed runtime JS file; advisory only
++(100%) none - no low-coverage changed runtime JS files
Changed JS files considered:
--(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage
- (0%) src/shared/toolbox/tool-metadata-inventory.js - changed JS file not collected as browser runtime coverage
- (0%) tests/playwright/tools/IdeaBoardTableNotes.spec.mjs - changed JS file not collected as browser runtime coverage
- (0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage
--(70%) toolbox/idea-board/index.js - changed JS file with browser V8 coverage
-+(64%) assets/theme-v2/js/tool-display-mode.js - changed JS file with browser V8 coverage
-+(74%) toolbox/idea-board/index.js - changed JS file with browser V8 coverage
-diff --git a/src/shared/toolbox/tool-metadata-inventory.js b/src/shared/toolbox/tool-metadata-inventory.js
-index db04eac5b..6d2ed68fa 100644
---- a/src/shared/toolbox/tool-metadata-inventory.js
-+++ b/src/shared/toolbox/tool-metadata-inventory.js
-@@ -72,14 +72,14 @@ export const TOOL_REGISTRY = Object.freeze([
- "id": "idea-board",
- "name": "Idea Board",
- "displayName": "Idea Board",
-- "shortDescription": "Capture, compare, and shape game ideas in a table.",
-+ "shortDescription": "Capture, compare, and shape game ideas.",
- "shortLabel": "Idea Board",
- "path": "idea-board",
- "folderName": "idea-board",
- "entryPoint": "idea-board/index.html",
- "badge": "/assets/theme-v2/images/badges/game-design.png",
- "tool": "/assets/theme-v2/images/tools/game-design.png",
-- "description": "Capture, compare, and shape game ideas with notes under each idea row.",
-+ "description": "Capture, compare, and shape game ideas.",
- "category": "Idea",
- "colorGroup": "tool-group-idea",
- "active": true,
-@@ -93,7 +93,7 @@ export const TOOL_REGISTRY = Object.freeze([
- "progressChecklist": [
- "Idea table workflow visible",
- "Add Idea and Add Note actions remain inline",
-- "Create Project remains unavailable until an idea is ready"
-+ "Ready ideas show project actions in their row"
- ],
- "deferred": false,
- "hidden": false,
-diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
-index f5544ca88..38085a4d2 100644
---- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
-+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
-@@ -13,10 +13,13 @@ function restoreEnvValue(key, value) {
- async function expectIdeaChevron(page, ideaId, iconName) {
- const metrics = await page.locator(`[data-idea-board-idea-row='${ideaId}'] th`).evaluate((cell, targetIdeaId) => {
- const label = cell.querySelector(".idea-board-idea-label");
-+ const text = label.querySelector(".idea-board-idea-label__text");
- const icon = cell.querySelector(`[data-idea-board-chevron='${targetIdeaId}']`);
- const cellStyles = getComputedStyle(cell);
- const labelStyles = getComputedStyle(label);
- const iconStyles = getComputedStyle(icon);
-+ const textRect = text.getBoundingClientRect();
-+ const iconRect = icon.getBoundingClientRect();
- return {
- iconName: icon.dataset.ideaBoardChevronIcon,
- labelDisplay: labelStyles.display,
-@@ -24,7 +27,11 @@ async function expectIdeaChevron(page, ideaId, iconName) {
- iconHeight: Number.parseFloat(iconStyles.height),
- fontSize: Number.parseFloat(cellStyles.fontSize),
- iconColor: iconStyles.backgroundColor,
-+ iconBottom: iconRect.bottom,
-+ iconLeft: iconRect.left,
- textColor: cellStyles.color,
-+ textBottom: textRect.bottom,
-+ textLeft: textRect.left,
- maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage,
- };
- }, ideaId);
-@@ -33,6 +40,8 @@ async function expectIdeaChevron(page, ideaId, iconName) {
- expect(Math.abs(metrics.iconWidth - metrics.fontSize)).toBeLessThanOrEqual(1);
- expect(Math.abs(metrics.iconHeight - metrics.fontSize)).toBeLessThanOrEqual(1);
- expect(metrics.iconColor).toBe(metrics.textColor);
-+ expect(metrics.iconLeft).toBeLessThan(metrics.textLeft);
-+ expect(Math.abs(metrics.iconBottom - metrics.textBottom)).toBeLessThanOrEqual(2);
- expect(metrics.maskImage).toContain(iconName);
- }
+-(0%) src/shared/toolbox/tool-metadata-inventory.js - changed JS file not collected as browser runtime coverage
+-(0%) tests/playwright/tools/IdeaBoardTableNotes.spec.mjs - changed JS file not collected as browser runtime coverage
+-(0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage
+-(64%) assets/theme-v2/js/tool-display-mode.js - changed JS file with browser V8 coverage
+-(74%) toolbox/idea-board/index.js - changed JS file with browser V8 coverage
++(0%) tests/playwright/tools/TextToSpeechFunctional.spec.mjs - changed JS file not collected as browser runtime coverage
++(71%) src/engine/audio/TextToSpeechEngine.js - changed JS file with browser V8 coverage
++(71%) toolbox/text-to-speech/text2speech.js - changed JS file with browser V8 coverage
+diff --git a/src/engine/audio/TextToSpeechEngine.js b/src/engine/audio/TextToSpeechEngine.js
+index c8e1dced3..a5f3ccabd 100644
+--- a/src/engine/audio/TextToSpeechEngine.js
++++ b/src/engine/audio/TextToSpeechEngine.js
+@@ -1,7 +1,10 @@
+ import {
++ TEXT_TO_SPEECH_CHARACTER_PRESET_DEFAULTS,
+ TEXT_TO_SPEECH_DEFAULTS,
+ TEXT_TO_SPEECH_DISPLAY_NAME,
+- TEXT_TO_SPEECH_RANGE_DEFAULTS
++ TEXT_TO_SPEECH_RANGE_DEFAULTS,
++ TEXT_TO_SPEECH_SSML_LIKE_PRESET_DEFAULTS,
++ TEXT_TO_SPEECH_VOICE_AGE_PRESET_DEFAULTS
+ } from "./TextToSpeechDefaults.js";
-@@ -92,7 +101,7 @@ async function expectExpandedNotesChildIndentation(page, ideaId, expectedInputRo
-
- async function expectProductionCopy(page) {
- await expect(page.locator("main")).not.toContainText(
-- /\bDB-shaped\b|\bin-page data model\b|\buserId\b|\bideaId\b|\bnoteId\b|\bsystem flag\b|\bmetadata\b|\bseed\b|\bdebug\b|\bselected context\b|\bmock\b|\btest\b|\binternal implementation\b|\bplaceholder\b|\bproject records\b|\bmutating API\b|\bauth\b|\bAI\b|\bdatabase behavior\b/i,
-+ /\bDB-shaped\b|\bin-page data model\b|\buserId\b|\bideaId\b|\bnoteId\b|\bsystem flag\b|\bmetadata\b|\bseed\b|\bdebug\b|\bselected context\b|\bmock\b|\btest\b|\binternal implementation\b|\bplaceholder\b|\bproject records\b|\bmutating API\b|\bserver\b|\bAPI\b|\blocal server\b|\bport\b|\bunderlying systems\b|\bauth\b|\bAI\b|\bdatabase behavior\b/i,
- );
+ function finiteNumber(value, fallback) {
+@@ -9,8 +12,180 @@ function finiteNumber(value, fallback) {
+ return Number.isFinite(number) ? number : fallback;
}
-@@ -141,6 +150,28 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
- await expect(page.locator("[data-idea-board-add-idea-row]")).toHaveCount(1);
- await expect(page.locator("[data-idea-board-add-idea]")).toHaveText("Add Idea");
- await expectButtonLeftAligned(page, "[data-idea-board-add-idea]", "[data-idea-board-add-idea-row] > td");
-+ await expect(page.locator("[data-idea-board-show-filter] summary")).toHaveText("Show");
-+ const captionMetrics = await page.locator(".idea-board-table-caption").evaluate((caption) => {
-+ const label = caption.querySelector("span");
-+ const filter = caption.querySelector("[data-idea-board-show-filter]");
-+ const labelRect = label.getBoundingClientRect();
-+ const filterRect = filter.getBoundingClientRect();
-+ return {
-+ filterRight: filterRect.right,
-+ filterTop: filterRect.top,
-+ labelRight: labelRect.right,
-+ labelTop: labelRect.top,
-+ };
-+ });
-+ expect(captionMetrics.filterRight).toBeGreaterThan(captionMetrics.labelRight);
-+ expect(Math.abs(captionMetrics.filterTop - captionMetrics.labelTop)).toBeLessThanOrEqual(4);
-+ await page.locator("[data-idea-board-show-filter] summary").click();
-+ await expect(page.locator("[data-idea-board-status-filter-option]")).toHaveCount(6);
-+ const checkedStatuses = await page.locator("[data-idea-board-status-filter-option]:checked").evaluateAll((inputs) => (
-+ inputs.map((input) => input.value)
-+ ));
-+ expect(checkedStatuses).toEqual(["New", "Exploring", "Refining", "Ready", "Project"]);
-+ await expect(page.locator("[data-idea-board-status-filter-option][value='Archived']")).not.toBeChecked();
- await expect(page.getByText(/another/i)).toHaveCount(0);
- await expect(page.locator("[data-idea-board-notes-chevron]")).toHaveCount(0);
- await expect(page.getByText("Selected idea context")).toHaveCount(0);
-@@ -223,11 +254,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
- const ideaInputRow = page.locator("[data-idea-board-idea-input-row]").last();
- await expect(ideaInputRow.locator("[data-idea-board-idea-action]")).toHaveText(["Save", "Cancel"]);
- await expect(ideaInputRow.locator("[data-idea-board-idea-status-input]")).toHaveCount(1);
-+ await expect(ideaInputRow.locator("[data-idea-board-idea-status-input] option")).toHaveText([
-+ "New",
-+ "Exploring",
-+ "Refining",
-+ "Ready",
-+ "Project",
-+ "Archived",
-+ ]);
- await expect(ideaInputRow.locator("td").nth(2)).toHaveText(/\d{4}-\d{2}-\d{2}/);
- await expect(ideaInputRow.locator("td").nth(3)).toHaveText("0 Notes");
- await page.locator("[data-idea-board-idea-input]").fill("Lantern Reef");
- await page.locator("[data-idea-board-pitch-input]").fill("Guide light through a reef that rearranges at dusk.");
-- await page.locator("[data-idea-board-idea-status-input]").selectOption("Parked");
-+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining");
- await page.locator("[data-idea-board-idea-action='save']").click();
- await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toBeVisible();
- await expect(page.locator("[data-idea-board-notes-count='lantern-reef']")).toHaveText("0 Notes");
-@@ -236,9 +275,33 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
- await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='edit']").click();
- await expect(page.locator("[data-idea-board-idea-input-row] [data-idea-board-idea-action]")).toHaveText(["Save", "Cancel"]);
- await expect(page.locator("[data-idea-board-idea-status-input]")).toHaveCount(1);
-- await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready to Shape");
-+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
- await page.locator("[data-idea-board-idea-action='save']").click();
-- await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Ready to Shape");
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Ready");
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='create-project']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Project");
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Open Project", "Archive"]);
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='open-project']").click();
-+ await expect(page.locator("[data-idea-board-status]")).toHaveText("Opening Lantern Reef.");
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0);
-+ await page.locator("[data-idea-board-status-filter-option][value='Archived']").check();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toBeVisible();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Archived");
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Restore", "Delete"]);
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='restore']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Project");
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Restore", "Delete"]);
-+ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0);
-+ await page.locator("[data-idea-board-filter-clear-all]").click();
-+ await expect(page.locator("[data-idea-board-idea-row]")).toHaveCount(0);
-+ await expect(page.locator("[data-idea-board-add-idea]")).toBeVisible();
-+ await page.locator("[data-idea-board-filter-select-all]").click();
-+ await expect(page.locator("[data-idea-board-idea-row]")).toHaveCount(3);
-
- expect(mutatingApiRequests).toEqual([]);
- expect(failedRequests).toEqual([]);
-diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs
-index b3033e10d..9f40a19ad 100644
---- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs
-+++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs
-@@ -115,10 +115,13 @@ function restoreEnvValue(key, value) {
- async function expectIdeaChevron(page, ideaId, iconName) {
- const metrics = await page.locator(`[data-idea-board-idea-row='${ideaId}'] th`).evaluate((cell, targetIdeaId) => {
- const label = cell.querySelector(".idea-board-idea-label");
-+ const text = label.querySelector(".idea-board-idea-label__text");
- const icon = cell.querySelector(`[data-idea-board-chevron='${targetIdeaId}']`);
- const cellStyles = getComputedStyle(cell);
- const labelStyles = getComputedStyle(label);
- const iconStyles = getComputedStyle(icon);
-+ const textRect = text.getBoundingClientRect();
-+ const iconRect = icon.getBoundingClientRect();
- return {
- iconName: icon.dataset.ideaBoardChevronIcon,
- labelDisplay: labelStyles.display,
-@@ -126,7 +129,11 @@ async function expectIdeaChevron(page, ideaId, iconName) {
- iconHeight: Number.parseFloat(iconStyles.height),
- fontSize: Number.parseFloat(cellStyles.fontSize),
- iconColor: iconStyles.backgroundColor,
-+ iconBottom: iconRect.bottom,
-+ iconLeft: iconRect.left,
- textColor: cellStyles.color,
-+ textBottom: textRect.bottom,
-+ textLeft: textRect.left,
- maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage,
- };
- }, ideaId);
-@@ -135,6 +142,8 @@ async function expectIdeaChevron(page, ideaId, iconName) {
- expect(Math.abs(metrics.iconWidth - metrics.fontSize)).toBeLessThanOrEqual(1);
- expect(Math.abs(metrics.iconHeight - metrics.fontSize)).toBeLessThanOrEqual(1);
- expect(metrics.iconColor).toBe(metrics.textColor);
-+ expect(metrics.iconLeft).toBeLessThan(metrics.textLeft);
-+ expect(Math.abs(metrics.iconBottom - metrics.textBottom)).toBeLessThanOrEqual(2);
- expect(metrics.maskImage).toContain(iconName);
+-function boundedNumber(value, { fallback, max, min }) {
+- return Math.min(max, Math.max(min, finiteNumber(value, fallback)));
++function boundedNumber(value, { fallback, max, min, value: defaultValue }) {
++ const fallbackValue = fallback ?? defaultValue ?? min;
++ return Math.min(max, Math.max(min, finiteNumber(value, fallbackValue)));
++}
++
++function shapedNumber(value, { fallback, max, min, step }) {
++ const boundedValue = boundedNumber(value, { fallback, max, min });
++ const stepText = String(step || "1");
++ const decimalPart = stepText.includes(".") ? stepText.split(".")[1] : "";
++ const multiplier = 10 ** decimalPart.length;
++ return Math.round(boundedValue * multiplier) / multiplier;
++}
++
++function textToSpeechSlugFromName(name) {
++ const slug = String(name || "")
++ .trim()
++ .toLowerCase()
++ .replace(/[^a-z0-9]+/g, "-")
++ .replace(/^-+|-+$/g, "");
++ return slug || "speech-item";
++}
++
++function textToSpeechVoiceGender(option) {
++ const voiceText = `${option?.gender || ""} ${option?.name || ""} ${option?.label || ""}`;
++ const voiceLanguage = String(option?.language || "").toLowerCase();
++ if (/\bneutral\b|\bnon[-\s]?binary\b|\bandrogynous\b/i.test(voiceText)) return "neutral";
++ if (voiceLanguage === "es-es" || /\bmale\b|\bman\b|\bdavid\b|\bmark\b/i.test(voiceText)) return "male";
++ if (/\bfemale\b|\bwoman\b|\bzira\b/i.test(voiceText)) return "female";
++ return "unknown";
++}
++
++function genderFilterLabel(value) {
++ if (value === "male-preferred") return "Male";
++ if (value === "female-preferred") return "Female";
++ if (value === "neutral") return "Neutral";
++ return "Any";
++}
++
++function ageFilterLabel(value) {
++ if (value === "adult") return "Adult";
++ if (value === "child") return "Child";
++ if (value === "elderly") return "Elderly";
++ if (value === "teen") return "Teen";
++ return "Any";
++}
++
++function payloadGenderValue(value) {
++ return value === "neutral" ? "any" : value;
++}
++
++function filterTextToSpeechVoicesByGender(voiceOptions, genderFilter = "any") {
++ if (genderFilter === "any") return voiceOptions;
++ if (genderFilter === "male-preferred") {
++ return voiceOptions.filter((option) => textToSpeechVoiceGender(option) === "male");
++ }
++ if (genderFilter === "female-preferred") {
++ return voiceOptions.filter((option) => textToSpeechVoiceGender(option) === "female");
++ }
++ if (genderFilter === "neutral") {
++ return voiceOptions.filter((option) => ["neutral", "unknown"].includes(textToSpeechVoiceGender(option)));
++ }
++ return voiceOptions;
++}
++
++function optionLabelCompare(left, right) {
++ return String(left.label).localeCompare(String(right.label), undefined, {
++ numeric: true,
++ sensitivity: "base"
++ });
++}
++
++function textToSpeechLanguageOptionsFromVoices(voiceOptions) {
++ const languageCounts = new Map();
++ voiceOptions.forEach((option) => {
++ const language = String(option.language || "").trim();
++ if (language) {
++ languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
++ }
++ });
++ return Array.from(languageCounts.entries())
++ .map(([language, count]) => ({
++ label: `${language} (${count} ${count === 1 ? "voice" : "voices"})`,
++ value: language
++ }))
++ .sort(optionLabelCompare);
++}
++
++function filterTextToSpeechVoiceOptions(voiceOptions, { gender = "any", language = "" } = {}) {
++ const filteredByGender = filterTextToSpeechVoicesByGender(voiceOptions, gender);
++ const matchingVoices = filteredByGender
++ .filter((option) => !language || option.language === language)
++ .sort(optionLabelCompare);
++ return {
++ filteredVoiceCount: filteredByGender.length,
++ gender,
++ genderLabel: genderFilterLabel(gender),
++ language,
++ languageOptions: textToSpeechLanguageOptionsFromVoices(filteredByGender),
++ matchingVoices,
++ voiceCount: voiceOptions.length
++ };
++}
++
++function shapeTextToSpeechOptions({
++ characterPreset = TEXT_TO_SPEECH_DEFAULTS.characterPreset,
++ pitch = TEXT_TO_SPEECH_DEFAULTS.pitch,
++ rate = TEXT_TO_SPEECH_DEFAULTS.rate,
++ ssmlLikePreset = TEXT_TO_SPEECH_DEFAULTS.ssmlLikePreset,
++ voiceAge = TEXT_TO_SPEECH_DEFAULTS.voiceAge,
++ volume = TEXT_TO_SPEECH_DEFAULTS.volume
++} = {}, sliderOverrides = {}) {
++ const character = TEXT_TO_SPEECH_CHARACTER_PRESET_DEFAULTS[characterPreset] || TEXT_TO_SPEECH_CHARACTER_PRESET_DEFAULTS.manual;
++ const age = TEXT_TO_SPEECH_VOICE_AGE_PRESET_DEFAULTS[voiceAge] || TEXT_TO_SPEECH_VOICE_AGE_PRESET_DEFAULTS.any;
++ const ssmlLike = TEXT_TO_SPEECH_SSML_LIKE_PRESET_DEFAULTS[ssmlLikePreset] || TEXT_TO_SPEECH_SSML_LIKE_PRESET_DEFAULTS.normal;
++ return {
++ characterPreset,
++ pitch: sliderOverrides.pitch
++ ? boundedNumber(pitch, TEXT_TO_SPEECH_RANGE_DEFAULTS.pitch)
++ : shapedNumber(Number(character.pitch) + Number(age.pitchOffset) + Number(ssmlLike.pitchOffset), TEXT_TO_SPEECH_RANGE_DEFAULTS.pitch),
++ rate: sliderOverrides.rate
++ ? boundedNumber(rate, TEXT_TO_SPEECH_RANGE_DEFAULTS.rate)
++ : shapedNumber(Number(character.rate) * Number(age.rateMultiplier) * Number(ssmlLike.rateMultiplier), TEXT_TO_SPEECH_RANGE_DEFAULTS.rate),
++ ssmlLikePreset,
++ voiceAge,
++ volume: sliderOverrides.volume
++ ? boundedNumber(volume, TEXT_TO_SPEECH_RANGE_DEFAULTS.volume)
++ : shapedNumber(Number(character.volume) * Number(ssmlLike.volumeMultiplier), TEXT_TO_SPEECH_RANGE_DEFAULTS.volume)
++ };
++}
++
++function uniqueTextToSpeechName(baseName, existingItems = []) {
++ const requestedName = String(baseName || "").trim() || "New speech item";
++ const existingNames = new Set(existingItems.map((item) => item.name));
++ if (!existingNames.has(requestedName)) return requestedName;
++ for (let index = 2; index < 1000; index += 1) {
++ const candidate = `${requestedName} ${index}`;
++ if (!existingNames.has(candidate)) return candidate;
++ }
++ return `${requestedName} ${Date.now().toString(36)}`;
++}
++
++function uniqueTextToSpeechId(baseName, existingItems = []) {
++ const existingIds = new Set(existingItems.map((item) => item.id));
++ const baseId = textToSpeechSlugFromName(baseName);
++ if (!existingIds.has(baseId)) return baseId;
++ for (let index = 2; index < 1000; index += 1) {
++ const candidate = `${baseId}-${index}`;
++ if (!existingIds.has(candidate)) return candidate;
++ }
++ return `${baseId}-${Date.now().toString(36)}`;
++}
++
++function createTextToSpeechQueueItem({
++ existingItems = [],
++ id = "",
++ name = "",
++ text = "",
++ ...options
++} = {}) {
++ const itemName = uniqueTextToSpeechName(name, id ? [] : existingItems);
++ return {
++ characterPreset: options.characterPreset || TEXT_TO_SPEECH_DEFAULTS.characterPreset,
++ gender: payloadGenderValue(options.gender || TEXT_TO_SPEECH_DEFAULTS.gender),
++ id: id || uniqueTextToSpeechId(itemName, existingItems),
++ language: options.language || TEXT_TO_SPEECH_DEFAULTS.language,
++ name: itemName,
++ pitch: boundedNumber(options.pitch, TEXT_TO_SPEECH_RANGE_DEFAULTS.pitch),
++ rate: boundedNumber(options.rate, TEXT_TO_SPEECH_RANGE_DEFAULTS.rate),
++ ssmlLikePreset: options.ssmlLikePreset || TEXT_TO_SPEECH_DEFAULTS.ssmlLikePreset,
++ text: String(text || "").trim() || "New speech line.",
++ voice: String(options.voice || ""),
++ voiceAge: options.voiceAge || TEXT_TO_SPEECH_DEFAULTS.voiceAge,
++ volume: boundedNumber(options.volume, TEXT_TO_SPEECH_RANGE_DEFAULTS.volume)
++ };
}
-@@ -194,7 +203,7 @@ async function expectExpandedNotesChildIndentation(page, ideaId, expectedInputRo
+ class TextToSpeechEngine {
+@@ -58,6 +233,18 @@ class TextToSpeechEngine {
+ }));
+ }
- async function expectIdeaBoardProductionCopy(page) {
- await expect(page.locator("main")).not.toContainText(
-- /\bDB-shaped\b|\bin-page data model\b|\buserId\b|\bideaId\b|\bnoteId\b|\bsystem flag\b|\bmetadata\b|\bseed\b|\bdebug\b|\bselected context\b|\bmock\b|\btest\b|\binternal implementation\b|\bplaceholder\b|\bproject records\b|\bmutating API\b|\bauth\b|\bAI\b|\bdatabase behavior\b/i,
-+ /\bDB-shaped\b|\bin-page data model\b|\buserId\b|\bideaId\b|\bnoteId\b|\bsystem flag\b|\bmetadata\b|\bseed\b|\bdebug\b|\bselected context\b|\bmock\b|\btest\b|\binternal implementation\b|\bplaceholder\b|\bproject records\b|\bmutating API\b|\bserver\b|\bAPI\b|\blocal server\b|\bport\b|\bunderlying systems\b|\bauth\b|\bAI\b|\bdatabase behavior\b/i,
- );
++ filterVoiceOptions(options = {}) {
++ return filterTextToSpeechVoiceOptions(this.voiceOptions(), options);
++ }
++
++ shapeOptions(options = {}, sliderOverrides = {}) {
++ return shapeTextToSpeechOptions(options, sliderOverrides);
++ }
++
++ createQueueItem(options = {}) {
++ return createTextToSpeechQueueItem(options);
++ }
++
+ voiceForValue(value) {
+ const normalizedValue = String(value || "").trim();
+ if (!normalizedValue) {
+@@ -248,5 +435,18 @@ class TextToSpeechEngine {
+ }
}
-@@ -302,7 +311,6 @@ test("Idea Board launches from Toolbox with accordion table notes model", async
- "Workflow",
- "Status",
- "Idea Table",
-- "Create Project",
- "Notes Governance",
- "Diagnostics",
- ]);
-@@ -335,12 +343,10 @@ test("Idea Board launches from Toolbox with accordion table notes model", async
- await expect(page.locator("[data-idea-board-add-note='top-thoughts']")).toBeVisible();
- await expect(page.locator("[data-idea-board-add-note='top-thoughts']")).toHaveText("Add Note");
- await expectExpandedNotesChildIndentation(page, "top-thoughts");
-- await expect(page.locator("[data-idea-board-create-project]")).toBeVisible();
-- await expect(page.locator("[data-idea-board-create-project]")).toBeDisabled();
-+ await expect(page.locator("[data-idea-board-create-project]")).toHaveCount(0);
- await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
- await expect(page.locator("script[src='toolbox/idea-board/index.js']")).toHaveCount(1);
- mutatingApiRequests.length = 0;
-- await page.locator("[data-idea-board-create-project]").evaluate((button) => button.click());
- await page.locator("[data-idea-board-add-note='top-thoughts']").click();
- await page.locator("[data-idea-board-note-input]").fill("Capture traversal risks before project creation.");
- await page.locator("[data-idea-board-note-action='save']").click();
-@@ -348,6 +354,16 @@ test("Idea Board launches from Toolbox with accordion table notes model", async
- await page.locator("[data-idea-board-idea-cell='clockwork-courier']").click();
- await expect(page.locator("[data-idea-board-expanded-row='clockwork-courier']")).toBeVisible();
- await expect(page.locator("[data-idea-board-notes-table='clockwork-courier']")).not.toContainText("Capture traversal risks before project creation.");
-+ await page.locator("[data-idea-board-add-idea]").click();
-+ await page.locator("[data-idea-board-idea-input]").fill("Launch Tile");
-+ await page.locator("[data-idea-board-pitch-input]").fill("Turn a polished board idea into a project.");
-+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
-+ await page.locator("[data-idea-board-idea-action='save']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
-+ await page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action='create-project']").click();
-+ await expect(page.locator("[data-idea-board-idea-row='launch-tile'] td").nth(1)).toHaveText("Project");
-+ await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Open Project", "Archive"]);
-+ await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
- expect(mutatingApiRequests).toEqual([]);
-
- expect(failedRequests).toEqual([]);
-diff --git a/toolbox/idea-board/index.html b/toolbox/idea-board/index.html
-index 214a303fb..422d67b1b 100644
---- a/toolbox/idea-board/index.html
-+++ b/toolbox/idea-board/index.html
-@@ -18,7 +18,7 @@
+-export { TextToSpeechEngine };
++export {
++ ageFilterLabel as textToSpeechAgeFilterLabel,
++ createTextToSpeechQueueItem,
++ filterTextToSpeechVoiceOptions,
++ genderFilterLabel as textToSpeechGenderFilterLabel,
++ payloadGenderValue as textToSpeechPayloadGenderValue,
++ shapeTextToSpeechOptions,
++ textToSpeechLanguageOptionsFromVoices,
++ textToSpeechSlugFromName,
++ textToSpeechVoiceGender,
++ TextToSpeechEngine,
++ uniqueTextToSpeechId,
++ uniqueTextToSpeechName
++};
+ export default TextToSpeechEngine;
+diff --git a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
+index ccf95e3e3..17e2c0b62 100644
+--- a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
++++ b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
+@@ -66,7 +66,13 @@ async function openTextToSpeechPage(page, { speechAvailable = true } = {}) {
+ getVoices() {
+ return voices;
+ },
++ pause() {
++ window.__textToSpeechCalls.push({ type: "pause" });
++ },
+ removeEventListener() {},
++ resume() {
++ window.__textToSpeechCalls.push({ type: "resume" });
++ },
+ speak(utterance) {
+ window.__textToSpeechCalls.push({
+ lang: utterance.lang,
+@@ -101,8 +107,22 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as
+ await expect(page.locator("[data-tts-voice-select]")).toContainText("Arcade Voice");
+ await expect(page.locator("[data-tts-voice-count]")).toHaveText("2");
+ await expect(page.locator("[data-tts-engine-label]")).toHaveText("Ready");
++ await expect(page.locator("[data-tts-gender-select]")).toBeVisible();
++ await expect(page.locator("[data-tts-language-select]")).toBeVisible();
++ await expect(page.locator("[data-tts-age-select]")).toBeVisible();
++ await expect(page.locator("[data-tts-character-preset-select]")).toBeVisible();
++ await expect(page.locator("[data-tts-ssml-preset-select]")).toBeVisible();
++ await expect(page.locator("[data-tts-import-json]")).toBeEnabled();
++ await expect(page.locator("[data-tts-copy-json]")).toBeEnabled();
++ await expect(page.locator("[data-tts-export-json]")).toBeEnabled();
+
+ await page.locator("[data-tts-text-input]").fill("Launch the next wave.");
++ await page.locator("[data-tts-item-name]").fill("Wave intro");
++ await page.locator("[data-tts-character-preset-select]").selectOption("dramatic");
++ await page.locator("[data-tts-age-select]").selectOption("teen");
++ await page.locator("[data-tts-ssml-preset-select]").selectOption("whisper-ish");
++ await expect(page.locator("[data-tts-pitch-value]")).toHaveText("1.1");
++ await expect(page.locator("[data-tts-volume-value]")).toHaveText("0.6");
+ await page.locator("[data-tts-voice-select]").selectOption("arcade-voice-uri");
+ await page.locator("[data-tts-rate]").fill("1.4");
+ await page.locator("[data-tts-pitch]").fill("0.8");
+@@ -111,6 +131,13 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as
+ await expect(page.locator("[data-tts-pitch-value]")).toHaveText("0.8");
+ await expect(page.locator("[data-tts-volume-value]")).toHaveText("0.55");
+ await expect(page.locator("[data-tts-text-count]")).toHaveText("21");
++ await page.locator("[data-tts-add-item]").click();
++ await expect(page.locator("[data-tts-queue-list]")).toContainText("Wave intro");
++ await expect(page.locator("[data-tts-output-summary]")).toContainText("\"name\": \"Wave intro\"");
++ await page.locator("[data-tts-duplicate-item]").click();
++ await expect(page.locator("[data-tts-queue-list]")).toContainText("Wave intro 2 copy");
++ await page.locator("[data-tts-delete-item]").click();
++ await expect(page.locator("[data-tts-queue-list] [data-tts-queue-item]")).toHaveCount(2);
+
+ await expect(page.locator("[data-tts-speak]")).toBeEnabled();
+ await page.locator("[data-tts-speak]").click();
+@@ -126,10 +153,13 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as
+ volume: 0.55,
+ }));
+
++ await page.locator("[data-tts-pause]").click();
++ await page.locator("[data-tts-resume]").click();
+ await page.locator("[data-tts-stop]").click();
+ await expect(page.locator("[data-tts-status]")).toContainText("Speech stopped");
+ calls = await page.evaluate(() => window.__textToSpeechCalls);
+ expect(calls.at(-1)).toEqual({ type: "cancel" });
++ expect(calls).toEqual(expect.arrayContaining([{ type: "pause" }, { type: "resume" }]));
+
+ expect(failures.failedRequests).toEqual([]);
+ expect(failures.pageErrors).toEqual([]);
+diff --git a/toolbox/text-to-speech/index.html b/toolbox/text-to-speech/index.html
+index adaaff3a7..f5b29b19a 100644
+--- a/toolbox/text-to-speech/index.html
++++ b/toolbox/text-to-speech/index.html
+@@ -6,7 +6,7 @@
+
Capture, compare, and shape game ideas in one table.
-+Capture, compare, and shape game ideas.
+Preview spoken game text with local browser speech synthesis before deciding whether a generated audio provider is needed.
++Create named speech lines, shape browser voices, and preview spoken game text through the shared engine audio Text To Speech module.