From 98b8d0e9d68ed575473bae35b5e75e28d6945630 Mon Sep 17 00:00:00 2001 From: DavidQ Date: Sat, 20 Jun 2026 17:10:55 -0400 Subject: [PATCH] PR_26171_069 align message tts profile contract --- .../PR_26171_069-manual-validation-notes.md | 7 + ...171_069-message-tts-ownership-checklist.md | 11 + ...-message-tts-profile-contract-alignment.md | 29 +++ ..._26171_069-parent-child-table-checklist.md | 17 ++ .../dev/reports/PR_26171_069-validation.md | 15 ++ .../dev/reports/codex_changed_files.txt | 71 ++---- docs_build/dev/reports/codex_review.diff | Bin 63581 -> 80482 bytes .../APPLY_PR.md | 18 ++ .../BUILD_PR.md | 61 ++++++ .../PLAN_PR.md | 32 +++ tests/playwright/tools/MessagesTool.spec.mjs | 10 +- .../tools/TextToSpeechFunctional.spec.mjs | 19 +- tests/tools/Text2SpeechShell.test.mjs | 6 +- toolbox/messages/index.html | 56 +---- .../messages/message-tts-service-registry.js | 4 + toolbox/messages/messages.js | 203 ++---------------- toolbox/text-to-speech/text2speech.js | 20 +- 17 files changed, 279 insertions(+), 300 deletions(-) create mode 100644 docs_build/dev/reports/PR_26171_069-manual-validation-notes.md create mode 100644 docs_build/dev/reports/PR_26171_069-message-tts-ownership-checklist.md create mode 100644 docs_build/dev/reports/PR_26171_069-message-tts-profile-contract-alignment.md create mode 100644 docs_build/dev/reports/PR_26171_069-parent-child-table-checklist.md create mode 100644 docs_build/dev/reports/PR_26171_069-validation.md create mode 100644 docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/APPLY_PR.md create mode 100644 docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/BUILD_PR.md create mode 100644 docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/PLAN_PR.md diff --git a/docs_build/dev/reports/PR_26171_069-manual-validation-notes.md b/docs_build/dev/reports/PR_26171_069-manual-validation-notes.md new file mode 100644 index 000000000..c55aa9daf --- /dev/null +++ b/docs_build/dev/reports/PR_26171_069-manual-validation-notes.md @@ -0,0 +1,7 @@ +# PR_26171_069 Manual Validation Notes + +- Confirmed Message Studio center work surface now presents the Messages parent table without detached Emotion Profile or Available TTS Profile tables. +- Confirmed Message Parts remain directly under the expanded Message row. +- Confirmed Message Studio exposes Play Message, Play Part, and Stop. +- Confirmed TTS Studio default profiles render the expected parent/child TTS Profiles to Emotion Settings model. +- Confirmed the scoped ZIP must be generated under `tmp/` and not staged. diff --git a/docs_build/dev/reports/PR_26171_069-message-tts-ownership-checklist.md b/docs_build/dev/reports/PR_26171_069-message-tts-ownership-checklist.md new file mode 100644 index 000000000..fce5a6afd --- /dev/null +++ b/docs_build/dev/reports/PR_26171_069-message-tts-ownership-checklist.md @@ -0,0 +1,11 @@ +# PR_26171_069 Message/TTS Ownership Checklist + +- PASS: Message Studio owns text entered for Messages. +- PASS: Message Studio owns ordered Message Parts. +- PASS: Message Studio does not expose reusable Emotion Profile authoring. +- PASS: Message Studio does not expose reusable TTS Profile authoring. +- PASS: TTS Studio owns reusable TTS Profiles. +- PASS: TTS Studio owns per-profile Emotion Settings. +- PASS: `src/engine/audio/TextToSpeechEngine.js` remains the playback owner. +- PASS: Message playback and Stop are routed through the Message Studio TTS service registry. +- PASS: Audio output remains generated/played by the shared audio engine path. diff --git a/docs_build/dev/reports/PR_26171_069-message-tts-profile-contract-alignment.md b/docs_build/dev/reports/PR_26171_069-message-tts-profile-contract-alignment.md new file mode 100644 index 000000000..a31357ce7 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_069-message-tts-profile-contract-alignment.md @@ -0,0 +1,29 @@ +# PR_26171_069 Message/TTS Profile Contract Alignment + +## Scope +- Align Message Studio and TTS Studio around the parent/child table model. +- Keep Message Studio as the owner of game text and ordered Message Parts. +- Keep TTS Studio as the owner of reusable TTS Profiles and per-profile Emotion Settings. +- Keep playback routed through `src/engine/audio/TextToSpeechEngine`. + +## Requirement Evidence +- PASS: Message Studio parent table remains `Messages` in `toolbox/messages/index.html`. +- PASS: Clicking a Message row opens the `Message Parts` child table in `toolbox/messages/messages.js`. +- PASS: Message Parts expose Text, Emotion, TTS Profile, and row actions. +- PASS: Message Studio exposes Play Part, Play Message, and Stop. +- PASS: Stop routes through `toolbox/messages/message-tts-service-registry.js` to `TextToSpeechEngine.stop()`. +- PASS: Message Studio no longer displays Message-owned Emotion Profile or Available TTS Profile side tables. +- PASS: TTS Studio parent table remains `TTS Profiles`. +- PASS: Clicking a TTS Profile row opens the `Emotion Settings` child table. +- PASS: `Man Profile 1` and `Woman Profile 2` seed Neutral, Happy, Angry, and Scared Emotion Settings. +- PASS: No separate Emotion Studio was created. +- PASS: No database schema changes were added. +- PASS: Theme V2 and external JavaScript structure remain in use; no inline styles, style blocks, or inline handlers were added. + +## Git Workflow +- Current branch: `codex/pr-26171-069-message-tts-profile-contract-alignment` +- Created branch: `codex/pr-26171-069-message-tts-profile-contract-alignment` +- Push result: pending at report creation +- PR URL: pending at report creation +- Merge result: pending at report creation +- Final main commit: pending at report creation diff --git a/docs_build/dev/reports/PR_26171_069-parent-child-table-checklist.md b/docs_build/dev/reports/PR_26171_069-parent-child-table-checklist.md new file mode 100644 index 000000000..52d25d2a7 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_069-parent-child-table-checklist.md @@ -0,0 +1,17 @@ +# PR_26171_069 Parent/Child Table Checklist + +## Message Studio +- PASS: Parent table is `Messages`. +- PASS: Message row click expands the child `Message Parts` table directly under the parent row. +- PASS: Message Parts include Text, Emotion, and TTS Profile controls. +- PASS: Message Parts provide row-level Play Part and inline edit behavior. +- PASS: Message rows provide Play Message and inline edit behavior. +- PASS: Stop is available from Message Studio playback controls. +- PASS: Message-owned detached Emotion Profile and Available TTS Profile tables were removed. + +## TTS Studio +- PASS: Parent table is `TTS Profiles`. +- PASS: TTS Profile row click expands the child `Emotion Settings` table. +- PASS: Emotion Settings belong to the selected TTS Profile. +- PASS: Default profile data includes Neutral, Happy, Angry, and Scared settings for the example profiles. +- PASS: No separate Emotion Studio was introduced. diff --git a/docs_build/dev/reports/PR_26171_069-validation.md b/docs_build/dev/reports/PR_26171_069-validation.md new file mode 100644 index 000000000..2fa239af9 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_069-validation.md @@ -0,0 +1,15 @@ +# PR_26171_069 Validation + +- PASS: `node --check toolbox/messages/messages.js` +- PASS: `node --check toolbox/messages/message-tts-service-registry.js` +- PASS: `node --check toolbox/text-to-speech/text2speech.js` +- PASS: `node --check tests/playwright/tools/MessagesTool.spec.mjs` +- PASS: `node --check tests/playwright/tools/TextToSpeechFunctional.spec.mjs` +- PASS: `node --check tests/tools/Text2SpeechShell.test.mjs` +- PASS: `node --test tests/tools/Text2SpeechShell.test.mjs` +- PASS: `npx playwright test tests/playwright/tools/MessagesTool.spec.mjs tests/playwright/tools/TextToSpeechFunctional.spec.mjs --project=playwright --workers=1 --reporter=list` +- PASS: `npm run test:workspace-v2` + +## Notes +- `npm run test:workspace-v2` is the legacy command name; this report treats it as Project Workspace validation. +- No full samples smoke was run. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 3fb4228d0..2f0436ffd 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,54 +1,17 @@ -PR_26171_044-idea-board-game-hub-project-flow - -Changed files: -- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md -- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md -- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md -- docs_build/dev/reports/codex_review.diff -- docs_build/dev/reports/codex_changed_files.txt -- src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js -- tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -- tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -- tests/playwright/tools/ToolboxRoutePages.spec.mjs -- toolbox/game-workspace/game-workspace.js -- toolbox/game-workspace/index.html -- toolbox/idea-board/index.html -- toolbox/idea-board/index.js - -Requirement PASS evidence: -- PASS: Create Project appears only for Ready ideas. Evidence: Idea Board actions remain Edit/Create Project/Delete for Ready rows and switch to Open in Game Hub/Archive after creation in targeted Playwright. -- PASS: Create Project uses the shared Game Hub project contract. Evidence: Idea Board calls the existing game-workspace server repository client createGame method; Playwright asserts the createGame repository request occurs. -- PASS: Create Project sets Idea status to Project. Evidence: targeted Playwright asserts the row status becomes Project. -- PASS: Project ideas and notes are read-only. Evidence: targeted Playwright asserts no Edit/Delete/Add Note/note actions remain for the Project row. -- PASS: Project row actions are Open in Game Hub and Archive. Evidence: targeted Playwright asserts exact action labels. -- PASS: Project ideas cannot be edited or deleted while Project. Evidence: Project rows do not render Edit/Delete actions and deletion guard remains active. -- PASS: Project ideas cannot add/edit/delete notes. Evidence: expanded Project notes render without Add Note or note actions. -- PASS: Archived ideas remain hidden by default and show Restore/Delete when visible. Evidence: targeted Playwright archives, shows Archived through the filter, and asserts Restore/Delete. -- PASS: Game Hub shows creator-facing Project Information. Evidence: Game Hub HTML/JS replaces internal record display with Project Information and targeted Game Hub Playwright passes. -- PASS: Game Hub shows read-only Source Idea fields. Evidence: Idea Board-to-Game Hub Playwright asserts Idea, Pitch, and Notes from the source idea render in Game Hub. -- PASS: Game Hub avoids internal IDs, DB/API/mock/debug/seed wording in creator-facing display. Evidence: touched runtime files pass creator-visible text scan and targeted Playwright checks the navigated Game Hub main content. -- PASS: Open in Game Hub navigates to the related project. Evidence: targeted Playwright waits for /toolbox/game-workspace/index.html?game=lantern-reef-* and asserts the linked project renders. -- PASS: No Game Journey expansion beyond existing link/reference. Evidence: implementation does not add Game Journey creation or new journey wiring. - -Validation evidence: -- PASS: node --check toolbox/idea-board/index.js -- PASS: node --check toolbox/game-workspace/game-workspace.js -- PASS: node --check toolbox/game-workspace/game-workspace-api-client.js -- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js -- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -- PASS: node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs -- PASS: node --check tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line -- PASS: npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Idea Board launches" -- PASS: npx playwright test tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub" -- PASS: npm run test:workspace-v2 -- PASS: git diff --check - -Git workflow: -- Current branch: codex/pr-26171-044-idea-board-game-hub-project-flow -- Created branch: codex/pr-26171-044-idea-board-game-hub-project-flow -- Commit hash: 2a38796f81aae2dceb4151095b8f89a276cd2d32 -- Push result: pushed to origin/codex/pr-26171-044-idea-board-game-hub-project-flow -- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/22 -- Merge result: to be reported after merge -- Final main commit: to be reported after returning to main and pulling latest +A docs_build/dev/reports/PR_26171_069-manual-validation-notes.md +A docs_build/dev/reports/PR_26171_069-message-tts-ownership-checklist.md +A docs_build/dev/reports/PR_26171_069-message-tts-profile-contract-alignment.md +A docs_build/dev/reports/PR_26171_069-parent-child-table-checklist.md +A docs_build/dev/reports/PR_26171_069-validation.md +A docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/APPLY_PR.md +A docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/BUILD_PR.md +A docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/PLAN_PR.md +M tests/playwright/tools/MessagesTool.spec.mjs +M tests/playwright/tools/TextToSpeechFunctional.spec.mjs +M tests/tools/Text2SpeechShell.test.mjs +M toolbox/messages/index.html +M toolbox/messages/message-tts-service-registry.js +M toolbox/messages/messages.js +M toolbox/text-to-speech/text2speech.js +M docs_build/dev/reports/codex_review.diff +M docs_build/dev/reports/codex_changed_files.txt diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index cffbc382327dcdb1739db4559445f764369cc334..0cad4fde44e2a985e35270131d4e26aa26391a4c 100644 GIT binary patch literal 80482 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 literal 63581 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 diff --git a/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/APPLY_PR.md b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/APPLY_PR.md new file mode 100644 index 000000000..033db7128 --- /dev/null +++ b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/APPLY_PR.md @@ -0,0 +1,18 @@ +# PR_26171_069-message-tts-profile-contract-alignment Apply + +## Apply Steps + +1. Start from clean latest `main`. +2. Create `codex/pr-26171-069-message-tts-profile-contract-alignment`. +3. Implement only the BUILD scope. +4. Run targeted Message Studio validation. +5. Run targeted TTS Studio validation. +6. Run `npm run test:workspace-v2`. +7. Stage only scoped files. +8. Commit. +9. Push branch. +10. Create PR. +11. Resolve merge conflicts if encountered and rerun validation. +12. Merge after validation passes. +13. Return to `main` and pull latest. +14. Produce final code file change list only, excluding MD, TXT, reports, and other non-code files. diff --git a/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/BUILD_PR.md b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/BUILD_PR.md new file mode 100644 index 000000000..de77298b6 --- /dev/null +++ b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/BUILD_PR.md @@ -0,0 +1,61 @@ +# PR_26171_069-message-tts-profile-contract-alignment Build + +## Source Of Truth + +Use this BUILD doc, the original PR request, `docs_build/dev/PROJECT_INSTRUCTIONS.md`, and `docs_build/dev/PROJECT_MULTI_PC.txt`. + +## Singular Purpose + +Align Message Studio and TTS Studio around reusable TTS Profiles, per-profile Emotion Settings, and Message Parts playback. + +## Exact Targets + +- `toolbox/messages/index.html` +- `toolbox/messages/messages.js` +- `toolbox/messages/message-tts-service-registry.js` +- `toolbox/text-to-speech/index.html` +- `toolbox/text-to-speech/text2speech.js` +- `src/engine/audio/TextToSpeechEngine.js` +- Targeted Message Studio Playwright/unit tests. +- Targeted TTS Studio Playwright/unit tests. +- `docs_build/dev/reports/codex_review.diff` +- `docs_build/dev/reports/codex_changed_files.txt` +- PR-specific report. +- Message/TTS ownership checklist. +- Parent-child table checklist. +- Validation report. +- Manual validation notes. + +## Requirements + +- Message Studio parent table is Messages. +- Clicking a Message row opens the Message Parts child table. +- Message Parts select Text, Emotion, and TTS Profile. +- Message Studio exposes Play Part, Play Message, and Stop. +- Message playback uses `src/engine/audio/TextToSpeechEngine`. +- Default TTS Profile exists until real profiles are available. +- TTS Studio parent table is TTS Profiles. +- Clicking a TTS Profile row opens Emotion Settings child table. +- Emotion settings belong to the selected TTS Profile. +- Do not create separate Emotion Studio. +- Include example profiles with Neutral, Happy, Angry, and Scared settings. +- Message Studio owns text and ordered message parts. +- TTS Studio owns reusable profiles and per-profile emotion settings. +- `src/engine/audio` owns playback. +- Audio owns generated/played output. +- Use Theme V2 and external JavaScript only. +- Do not add inline styles, style blocks, inline event handlers, page-local CSS, or tool-local CSS. +- Do not add database changes unless an existing Local API contract already supports them. +- Do not use browser-owned product data as source of truth. +- Do not add silent fallbacks. + +## Validation + +- Run targeted Message Studio validation. +- Run targeted TTS Studio validation. +- Run `npm run test:workspace-v2`. +- Do not run full samples smoke. + +## ZIP + +Produce `tmp/PR_26171_069-message-tts-profile-contract-alignment_delta.zip` with repo-structured changed files only. diff --git a/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/PLAN_PR.md b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/PLAN_PR.md new file mode 100644 index 000000000..3b9fedb0e --- /dev/null +++ b/docs_build/pr/PR_26171_069-message-tts-profile-contract-alignment/PLAN_PR.md @@ -0,0 +1,32 @@ +# PR_26171_069-message-tts-profile-contract-alignment Plan + +## Purpose + +Align Message Studio and TTS Studio around the parent/child table model and shared TTS profile contract. + +## Scope + +- Message Studio owns Messages and ordered Message Parts. +- Message Parts select Text, Emotion, and TTS Profile. +- Message Studio provides Play Part, Play Message, and Stop using `src/engine/audio/TextToSpeechEngine`. +- TTS Studio owns TTS Profiles and per-profile Emotion Settings. +- TTS Profile rows expand to Emotion Settings child rows. +- Keep Theme V2 only, external JavaScript only, no page-local or tool-local CSS. +- Add targeted validation and required PR reports. + +## Non-Goals + +- Do not add database schema or persistence changes. +- Do not create a separate Emotion Studio. +- Do not introduce browser storage as product source of truth. +- Do not change unrelated tools or shared runtime behavior beyond the audio playback contract needed by Message Studio. + +## Validation + +- Targeted Message Studio validation. +- Targeted TTS Studio validation. +- `npm run test:workspace-v2` with note that the command name is legacy and the user-facing language is Project Workspace. + +## Delivery + +- Commit, push, create PR, resolve conflicts if encountered, merge after validation passes, return to `main`, pull latest, and produce `tmp/PR_26171_069-message-tts-profile-contract-alignment_delta.zip`. diff --git a/tests/playwright/tools/MessagesTool.spec.mjs b/tests/playwright/tools/MessagesTool.spec.mjs index 451c4439f..0b7f357b4 100644 --- a/tests/playwright/tools/MessagesTool.spec.mjs +++ b/tests/playwright/tools/MessagesTool.spec.mjs @@ -162,10 +162,12 @@ test("Message Studio renders Messages with child Message Parts and plays ordered await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); await expect(page.locator("[data-messages-category-name]")).toHaveCount(0); await expect(page.locator("[data-messages-tts-add-row]")).toHaveCount(0); + await expect(page.locator("[data-messages-emotions]")).toHaveCount(0); + await expect(page.locator("[data-messages-tts-profiles]")).toHaveCount(0); await expect(page.getByRole("columnheader", { name: "Message Name" })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "Default TTS Profile" })).toBeVisible(); - await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Browser Speech Default"); - await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Owned by TTS Studio"); + await expect(page.locator("[data-messages-segment-count]")).toHaveText("0"); + await expect(page.getByRole("button", { name: "Stop" })).toBeEnabled(); await page.getByRole("button", { name: "Add Message" }).click(); await page.locator("[data-messages-commit='__new__']").click(); @@ -230,6 +232,10 @@ test("Message Studio renders Messages with child Message Parts and plays ordered type: "speak", voiceName: "Test Voice", })); + await page.getByRole("button", { name: "Stop" }).click(); + await expect(page.locator("[data-messages-log]")).toHaveText("Message Studio playback stopped. Cleared 2 queued items."); + speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); + expect(speechCalls.at(-1)).toEqual({ type: "cancel" }); await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).getByRole("button", { name: "Play Part" }).click(); await expect(page.locator("[data-messages-log]")).toHaveText("Play Part queued Part 2 using Browser Speech Default."); diff --git a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs index 8c964fecc..984835da8 100644 --- a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs +++ b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs @@ -107,7 +107,7 @@ 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-profile-count]")).toHaveText("3"); - await expect(page.locator("[data-tts-emotion-count]")).toHaveText("3"); + await expect(page.locator("[data-tts-emotion-count]")).toHaveText("12"); await expect(page.locator("[data-tts-profile-table]")).toContainText("Default Balanced Profile"); await expect(page.locator("[data-tts-profile-table]")).toContainText("Man Profile 1"); await expect(page.locator("[data-tts-profile-table]")).toContainText("Woman Profile 2"); @@ -116,7 +116,23 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as await expect(page.getByRole("heading", { name: "Emotion Settings" })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "Emotion", exact: true })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "SSML-like Preset" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]")).toHaveCount(4); await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" }).getByRole("button", { name: "Delete" })).toBeDisabled(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Happy" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Angry" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Scared" })).toBeVisible(); + await page.locator("[data-tts-profile-row]").filter({ hasText: "Man Profile 1" }).click(); + await expect(page.locator("[data-tts-emotion-row]")).toHaveCount(4); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Happy" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Angry" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Scared" })).toBeVisible(); + await page.locator("[data-tts-profile-row]").filter({ hasText: "Woman Profile 2" }).click(); + await expect(page.locator("[data-tts-emotion-row]")).toHaveCount(4); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Happy" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Angry" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Scared" })).toBeVisible(); await page.getByRole("button", { name: "Add Profile" }).click(); await expect(page.locator("[data-tts-profile-editor='__new__']")).toBeVisible(); @@ -212,6 +228,7 @@ test("Text To Speech shows actionable error when browser speech synthesis is una try { await expect(page.getByRole("heading", { level: 1, name: "Text To Speech" })).toBeVisible(); await expect(page.locator("[data-tts-profile-count]")).toHaveText("3"); + await expect(page.locator("[data-tts-emotion-count]")).toHaveText("12"); await expect(page.locator("[data-tts-engine-status]")).toContainText("SpeechSynthesis is unavailable"); await expect(page.locator("[data-tts-status]")).toContainText("Use a browser with Web Speech API support"); await expect(page.locator("[data-tts-voice-select]")).toContainText("No browser voices available"); diff --git a/tests/tools/Text2SpeechShell.test.mjs b/tests/tools/Text2SpeechShell.test.mjs index 51e4f019f..719243bff 100644 --- a/tests/tools/Text2SpeechShell.test.mjs +++ b/tests/tools/Text2SpeechShell.test.mjs @@ -88,8 +88,10 @@ test("Text2Speech profile contract exposes Message Studio compatible profile opt assert.equal(TTS_PROFILE_CONTRACT_VERSION, "tts-profile-emotion-v1"); assert.equal(defaults[0].name, "Default Balanced Profile"); assert.equal(defaults[0].messageStudioUsageCount, 1); - assert.equal(defaults[0].emotions[0].emotionLabel, "Neutral"); - assert.equal(defaults[0].emotions[0].messagePartsUsageCount, 1); + assert.deepEqual(defaults[0].emotions.map((emotion) => emotion.emotionLabel), ["Neutral", "Happy", "Angry", "Scared"]); + assert.deepEqual(defaults[1].emotions.map((emotion) => emotion.emotionLabel), ["Neutral", "Happy", "Angry", "Scared"]); + assert.deepEqual(defaults[2].emotions.map((emotion) => emotion.emotionLabel), ["Neutral", "Happy", "Angry", "Scared"]); + assert.equal(defaults[0].emotions.find((emotion) => emotion.emotion === "neutral").messagePartsUsageCount, 1); assert.deepEqual(options, [{ active: true, emotionSettings: [{ diff --git a/toolbox/messages/index.html b/toolbox/messages/index.html index 5df9a0290..74a54356d 100644 --- a/toolbox/messages/index.html +++ b/toolbox/messages/index.html @@ -51,7 +51,7 @@

Message Studio

Messages own game text and ordered parts. Audio playback is delegated to the audio engine.

0Messages
-
0Emotion Profiles
+
0Message Parts
0TTS Profiles
@@ -86,59 +86,6 @@

Messages

-
-
-
-
Delivery Configuration
-

Emotion Profiles

-
-
- - - - - - - - - - - - - - -
NameVolumePitchRateStatusActions
Loading emotion profiles.
-
-
- -
-
-
-
-
-
-
Delivery Configuration
-

Available TTS Profiles

-
-
- - - - - - - - - - - - - - -
NameProviderVoiceLanguageStatusActions
Loading TTS profiles.
-
-
-