Skip to content

harmonics demo — the voice showcase (play / file / stream)#5

Merged
OriNachum merged 11 commits into
mainfrom
feat/harmonics-demo
Jul 8, 2026
Merged

harmonics demo — the voice showcase (play / file / stream)#5
OriNachum merged 11 commits into
mainfrom
feat/harmonics-demo

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

harmonics demo — the voice showcase (play / file / stream)

Closes #4. One command tours the whole agent voice across the design spine, in
three output modes — built end-to-end through the devague pipeline
(/think/spec-to-plan/assign-to-workforce), so the spec and plan are
committed alongside the code.

What ships

harmonics demo renders a curated 28-clip tour and can:

  • Play / hear it--play (live) or --html FILE (a self-contained,
    browser-playable gallery: WAVs embedded as data-URIs, per-clip axes + note
    summary, both light/dark themes, deterministic so it snapshot-tests).
  • Write files--wav DIR (one WAV per clip), --out FILE (one
    concatenated tour WAV).
  • Stream / import — a public harmonics.demo.showcase() yields one
    (label, axes, note_sequence, wav_bytes) per clip; --json emits the
    note-sequence-per-clip payload on stdout.
  • Dry-run by default — no flags lists the clips + axes; producing sound or
    files needs an explicit flag (the producing-verb contract).
  • --articulation {discrete,speechy,smooth,alien} re-renders the whole tour
    in one voice; the default renders each clip in its own articulation, so the
    dedicated articulations section still demonstrates all four styles.

The tour matrix (harmonics/demo/matrix.py) walks six groups: intents,
identity, confidence/urgency shading, say-tracks-words, stress, and
articulations. This is packaging over the existing v0.5.0 pipeline — no new
synthesis
; the pure text→notes path stays offline and dependency-free (only
--play needs an audio backend).

How it was built (7 tasks / 5 waves, TDD-gated)

Wave Task File(s) Tests
0 matrix as data demo/matrix.py 14
1 showcase() core demo/core.py, __init__.py 9
2 HTML gallery demo/gallery.py 11
2 file modes demo/files.py 14
2 live playback demo/play.py 9
3 CLI verb + register cli/_commands/demo.py, cli/__init__.py 11
4 explain catalog explain/catalog.py (auto-covered)

Each task was built test-first in isolation and merged only with the suite green
before and after. A small refactor extracts say.render_notes() as a public
function so demo's spoken clips are byte-for-byte the real harmonics say
voice (cmd_say behaviour unchanged).

Gates

  • 2336 tests pass (68 new); full suite green.
  • black / isort / flake8 / bandit (0 issues) / markdownlint clean.
  • afi rubricteken cli doctor . --strict green; harmonics explain demo
    resolves; root page lists demo.
  • Version bumped 0.5.0 → 0.6.0; CHANGELOG + README updated (the README Status
    section is also de-staled: play/say/demo now shipped, not "coming").

Spec & plan (committed)

  • docs/specs/2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.md
  • docs/plans/2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj

  • harmonics-cli (Claude)

OriNachum and others added 10 commits July 8, 2026 13:56
…(devague /think)

Converged Announcement Frame for issue #4: a `harmonics demo` verb that tours
the agent voice across every design axis in three output modes — play live,
write a file (--html gallery / --wav dir / --out concatenated), or stream via a
public showcase() + --json. Dry-run by default; offline/deterministic pure path;
articulation flag + dedicated 'articulations' row (per user decision).

12 claims + 15 honesty conditions, all confirmed; gate passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
Buildable plan seeded from the converged demo-command frame. 26 coverage
targets, all covered; acyclic graph. Waves: matrix -> showcase() core ->
{gallery, files, play} (wide, file-disjoint) -> CLI verb+register -> explain.
Each task is TDD-gated with its own test_demo_*.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
Curated 28-clip tour across 6 groups (intents, identity, shading, say, stress,
articulations) as a pure ClipSpec data table — no rendering, no audio import.
14 tests. Covers c3,h10,c6,c10,h5,c14,h8.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
showcase() yields (label, axes, notes, wav) per matrix clip — deterministic,
offline (no audio backend imported), reusing the existing pipeline. Extracts a
public say.render_notes() (cmd_say behavior unchanged) so 'say' clips sound
exactly like harmonics say. --articulation override re-renders the whole tour.
9 tests. Covers c9,h4,c5,h12,h13,c11,h2.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
render_gallery(clips) -> deterministic self-contained HTML: base64 WAV
data-URIs, per-clip axes chips + note summary, both themes, no external hosts.
11 tests. Covers h6,c11.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
write_wav_dir (per-clip), write_concat_wav (one tour WAV with gaps), and
json_payload (note-sequence-per-clip) — all offline, valid RIFF/WAVE,
JSON-serializable. 14 tests. Covers c8,h3.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
play_clips(clips) plays each clip's wav via a lazy simpleaudio/sounddevice
import; friendly CliError (not ImportError) when no backend is installed.
9 tests. Covers c8,h3.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
cmd_demo dispatches: dry-run lists clips+axes, --json streams note sequences,
--html writes the gallery, --wav writes per-clip WAVs, --out writes one tour
WAV, --play plays live. --articulation re-renders the whole tour (default None
= each clip's own, so the articulations section still shows all four). 11 tests.
Covers c1,h1,c4,h11,c7,h2,c8,h3,c12,c14,h8.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
Adds the ('demo',) explain entry documenting the three output modes mapped to
the three audiences (humans->--play/--html, developers->showcase(),
embedding->--json/showcase()), every flag, and --articulation; lists demo on
the root page. teken cli doctor . --strict stays green. Covers c2,h9,c12,h7.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
New harmonics demo command (issue #4). Also refreshes the stale README Status
section (play/say/demo now shipped, not 'coming') and lists the three audio
verbs in the CLI table.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 13 rules
✅ Skills: version-bump

Grey Divider


Remediation recommended

1. Gallery headers misgroup clips 🐞 Bug ≡ Correctness
Description
In harmonics.demo.gallery.render_gallery(), section headers are derived from the clip label prefix
before “:”, but the demo matrix includes labels like “say (spark): …”/“say (daria): …” and
“articulation: …”, causing unintended extra headers within the same curated group and misnaming the
“articulations” group as singular “articulation”. This breaks the intended six-group tour
organization in the generated HTML gallery.
Code

harmonics/demo/gallery.py[R197-239]

+def _clip_group(label: str) -> str:
+    return label.split(":", 1)[0]
+
+
+def _clip_html(index: int, clip: Clip) -> str:
+    anchor = f"clip-{index}"
+    label_html = html.escape(clip.label)
+    return "\n".join(
+        [
+            f'<section class="clip" id="{anchor}">',
+            f"<h3>{label_html}</h3>",
+            _chips_html(clip),
+            _notes_summary_html(clip),
+            _audio_html(clip),
+            "</section>",
+        ]
+    )
+
+
+def render_gallery(clips: list[Clip]) -> str:
+    """Return a self-contained HTML document showing every clip: its label,
+    axes, note summary, and a playable embedded WAV.
+
+    Fully self-contained — all CSS is inlined in a ``<style>`` block and every
+    clip's audio is a base64 ``data:audio/wav;base64,...`` URI, so the
+    document never references an external host (no CDN, no webfont, no
+    remote image). Pure function of ``clips``: calling it twice on the same
+    (or an equal) list returns an identical string — no timestamps, no
+    random ids, and any per-clip anchor is derived only from the clip's
+    index in the list.
+
+    One card is emitted per clip, in the given order; a section header is
+    emitted whenever the label's prefix before ``":"`` changes from the
+    previous clip's, grouping the tour without requiring any extra input.
+    """
+    body_sections: list[str] = []
+    last_group: object = object()  # sentinel: never equals a real group name
+    for index, clip in enumerate(clips):
+        group = _clip_group(clip.label)
+        if group != last_group:
+            body_sections.append(f'<h2 class="group-header">{html.escape(group)}</h2>')
+            last_group = group
+        body_sections.append(_clip_html(index, clip))
Evidence
The gallery groups by label.split(':', 1)[0], while the matrix contains multiple distinct label
prefixes inside the same curated group (group="say"), so the gallery will emit multiple headers
for what is intended to be one section and will also render a singular “articulation” header for the
articulations group.

harmonics/demo/gallery.py[197-239]
harmonics/demo/matrix.py[124-168]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`render_gallery()` groups clips by parsing `clip.label` (`label.split(":", 1)[0]`), but labels are not a stable or consistent source of group identity (e.g., `say (spark): ...` vs `say: ...`). This causes the HTML gallery to emit incorrect/extra group headers and mislabel the `articulations` section.

### Issue Context
The curated matrix already has an explicit `group` field per clip (`ClipSpec.group`), but that information is dropped when rendering to `Clip`, so the gallery is forced to guess grouping from labels.

### Fix Focus Areas
- harmonics/demo/gallery.py[197-239]
- harmonics/demo/core.py[49-83]
- harmonics/demo/matrix.py[124-169]

### Suggested fix
1. Extend `harmonics.demo.core.Clip` to carry the clip’s curated group (e.g., add `group: str`).
2. In `_render_clip()`, set `group=spec.group` on the returned `Clip`.
3. Update `render_gallery()` to use `clip.group` (and optionally a display-name mapping) rather than parsing `clip.label`.
4. Update any affected call sites/tests that destructure `Clip` as a 4-tuple.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Gallery title uses harmonics ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The HTML gallery uses harmonics as a page/title label, but compliance requires using
harmonics-cli for labels/metadata fields (reserving harmonics for runnable CLI examples/prog).
This can create inconsistent product identity across docs/UI surfaces.
Code

harmonics/demo/gallery.py[R27-35]

+_TITLE = "harmonics — voice showcase"
+
+_INTRO = (
+    "A self-contained tour of the harmonics voice: every intent, several agent "
+    "identities, confidence/urgency shading, sentence-to-tune inference, emphasis "
+    "stress, and the four wav articulation styles — each rendered here as a "
+    "playable clip. No server, no network, no external assets: this page is the "
+    "whole tour."
+)
Evidence
Rule 1794552 requires harmonics only for runnable CLI contexts and harmonics-cli for
labels/metadata. The gallery’s _TITLE and intro text are user-facing labels and currently use
harmonics.

Rule 1794552: Use distinct names: harmonics for runnable examples/argparse prog, harmonics-cli for labels and metadata fields
harmonics/demo/gallery.py[27-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTML gallery uses `harmonics` as a UI label/title, but our naming policy requires using `harmonics-cli` for labels/metadata fields (while `harmonics` is reserved for runnable command/prog contexts).

## Issue Context
This affects the HTML gallery output shown to users, which is a product identity location (a label), not a command invocation.

## Fix Focus Areas
- harmonics/demo/gallery.py[27-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add harmonics demo voice showcase with play/html/wav/json modes

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add harmonics demo CLI verb to tour the voice across a curated 28-clip matrix.
• Support live playback, deterministic HTML gallery, WAV exports, and JSON/streaming output.
• Extract say.render_notes() for shared sentence→notes rendering; bump to 0.6.0.
Diagram

graph TD
  CLI["harmonics demo CLI"] --> Showcase["demo.showcase() core"] --> Matrix["demo/matrix.py (28 clips)"]
  Showcase --> Files["demo/files.py (wav/json)"]
  Showcase --> Gallery["demo/gallery.py (HTML)"]
  Showcase --> Play["demo/play.py (--play)"] --> Backend["simpleaudio/sounddevice"]
  Showcase --> Pipeline["play/say note pipeline"] --> Audio["audio.render_wav()"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse `harmonics.audio.play()` instead of demo-specific playback
  • ➕ Eliminates duplicated backend-selection logic
  • ➕ Centralizes playback behavior and error messaging
  • ➖ Would require re-rendering from notes (extra CPU) or API changes to play raw WAV bytes
  • ➖ Harder to guarantee demo plays the exact bytes produced by showcase() (current approach preserves them)
2. Use a templating engine (e.g., Jinja2) for the HTML gallery
  • ➕ Cleaner separation of HTML/CSS from Python
  • ➕ Easier future styling/structure changes
  • ➖ Adds a dependency to an intentionally stdlib-only/offline path
  • ➖ Can make determinism/snapshot testing trickier if templates inject unstable formatting

Recommendation: Keep the PR’s current approach: a deterministic showcase() core with stdlib-only HTML/file renderers and lazy backend import for --play. It best matches the project’s constraints (offline by default, dependency-free core paths, reproducible output) while still providing multiple consumption modes (CLI, files, and an importable API).

Files changed (25) +2893 / -19

Enhancement (8) +1055 / -0
__init__.pyRegister the new 'demo' verb in the CLI parser +2/-0

Register the new 'demo' verb in the CLI parser

• Imports and registers the new demo command module so 'harmonics demo' appears in the CLI surface.

harmonics/cli/init.py

demo.pyImplement 'harmonics demo' CLI dispatch (play/html/wav/out/json) +191/-0

Implement 'harmonics demo' CLI dispatch (play/html/wav/out/json)

• Adds the 'demo' verb with dry-run default, '--json', file outputs ('--html/--wav/--out'), live playback ('--play'), and '--articulation' override. Wraps file-write 'OSError' into structured 'CliError' while letting backend errors propagate cleanly.

harmonics/cli/_commands/demo.py

__init__.pyAdd public 'harmonics.demo' package surface +23/-0

Add public 'harmonics.demo' package surface

• Exports 'showcase', 'Clip', and matrix helpers while explicitly keeping playback/gallery/file modules out of the import graph to remain backend-free on import.

harmonics/demo/init.py

core.pyAdd deterministic offline 'showcase()' renderer producing clips +101/-0

Add deterministic offline 'showcase()' renderer producing clips

• Implements the core 'showcase()' function that renders each ClipSpec via existing 'play'/'say' pipelines and synthesizes WAV bytes via 'audio.render_wav'. Supports a global articulation override while keeping note sequences invariant.

harmonics/demo/core.py

files.pyAdd WAV/JSON output helpers for demo clips +140/-0

Add WAV/JSON output helpers for demo clips

• Implements per-clip WAV export with deterministic filenames, concatenated WAV export with silence gaps (and format validation), and a JSON-serializable payload excluding raw WAV bytes.

harmonics/demo/files.py

gallery.pyAdd deterministic self-contained HTML gallery renderer +262/-0

Add deterministic self-contained HTML gallery renderer

• Generates a single HTML document with embedded base64 WAV data URIs, per-clip axes chips, note summaries, and light/dark theme styling with no external references.

harmonics/demo/gallery.py

matrix.pyDefine the curated 28-clip tour matrix as pure data +216/-0

Define the curated 28-clip tour matrix as pure data

• Adds 'ClipSpec', ordered group names, and the curated matrix covering intents, identity, shading, say, stress, and articulations with no rendering or audio imports.

harmonics/demo/matrix.py

play.pyAdd live playback of rendered demo clips with lazy backend import +120/-0

Add live playback of rendered demo clips with lazy backend import

• Implements sequential playback of already-rendered WAV bytes with a configurable gap and backend selection ('simpleaudio' then 'sounddevice') done lazily; raises structured 'CliError' if absent.

harmonics/demo/play.py

Refactor (1) +31 / -12
say.pyExtract 'render_notes()' as public sentence→notes core +31/-12

Extract 'render_notes()' as public sentence→notes core

• Refactors 'cmd_say' to call a new 'render_notes(sentence, agent, seq)' helper, enabling 'demo' to reuse the exact say pipeline without behavior drift.

harmonics/cli/_commands/say.py

Tests (6) +1042 / -0
test_demo_cli.pyAdd CLI integration tests for 'harmonics demo' +174/-0

Add CLI integration tests for 'harmonics demo'

• Covers verb registration, help output, dry-run listing, '--json', file outputs, '--play' friendly failure when no backend is installed, and articulation invariants (notes stable, WAV bytes differ).

tests/test_demo_cli.py

test_demo_files.pyAdd unit tests for demo file helpers +196/-0

Add unit tests for demo file helpers

• Tests deterministic per-clip WAV writing, concatenated WAV validity and format checks, JSON payload shape/serializability, and ensures no playback backend imports occur.

tests/test_demo_files.py

test_demo_gallery.pyAdd tests for deterministic, self-contained HTML gallery output +135/-0

Add tests for deterministic, self-contained HTML gallery output

• Verifies embedded audio tags/data URIs per clip, absence of external HTTP references, repeat-call determinism, proper HTML escaping, and backend-free rendering.

tests/test_demo_gallery.py

test_demo_matrix.pyAdd tests enforcing matrix structure and offline purity +157/-0

Add tests enforcing matrix structure and offline purity

• Validates group coverage, clip kinds/fields, intent coverage, identity diversity, stress and articulation rows, iter_clips behavior, and absence of audio imports in matrix source.

tests/test_demo_matrix.py

test_demo_play.pyAdd tests for live playback backend resolution and sequencing +222/-0

Add tests for live playback backend resolution and sequencing

• Asserts lazy backend imports, friendly 'CliError' when no backend is available, correct backend preference order, per-clip sequencing, and gap sleeping behavior without requiring a real audio device.

tests/test_demo_play.py

test_demo_showcase.pyAdd tests for showcase determinism and pipeline reuse +158/-0

Add tests for showcase determinism and pipeline reuse

• Checks output shape/length vs. matrix, byte determinism, no backend imports, equivalence with direct pipeline note generation for representative play/say clips, and articulation override semantics.

tests/test_demo_showcase.py

Documentation (7) +762 / -4
harmonics-ships-a-demo-command-one-command-tours-t.jsonAdd exported devague frame JSON for 'harmonics demo' +229/-0

Add exported devague frame JSON for 'harmonics demo'

• Introduces the announcement frame capturing requirements, honesty conditions, and decisions for the demo command.

.devague/frames/harmonics-ships-a-demo-command-one-command-tours-t.json

harmonics-ships-a-demo-command-one-command-tours-t.jsonAdd exported devague plan JSON (7 tasks / 5 waves) +288/-0

Add exported devague plan JSON (7 tasks / 5 waves)

• Adds the structured build plan used to implement the demo command, including tasks, dependencies, and acceptance criteria.

.devague/plans/harmonics-ships-a-demo-command-one-command-tours-t.json

CHANGELOG.mdDocument 0.6.0 release with new 'demo' command and render_notes extraction +33/-0

Document 0.6.0 release with new 'demo' command and render_notes extraction

• Adds a 0.6.0 entry describing 'harmonics demo', its modes/flags, and the new shared 'render_notes()' API for sentence→notes rendering.

CHANGELOG.md

README.mdUpdate Status and command table to include 'harmonics demo' +10/-4

Update Status and command table to include 'harmonics demo'

• Refreshes the Status section to reflect shipped voice verbs and documents 'demo' in the primary CLI surface table and examples.

README.md

2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.mdAdd markdown build plan for the demo command +66/-0

Add markdown build plan for the demo command

• Commits the human-readable plan mirroring the exported JSON plan, including tasks and acceptance criteria.

docs/plans/2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.md

2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.mdAdd markdown spec for 'harmonics demo' +53/-0

Add markdown spec for 'harmonics demo'

• Commits the spec describing audience, before/after, requirements, honesty conditions, and scope boundaries for the demo command.

docs/specs/2026-07-08-harmonics-ships-a-demo-command-one-command-tours-t.md

catalog.pyAdd 'harmonics explain demo' entry and list demo on root page +83/-0

Add 'harmonics explain demo' entry and list demo on root page

• Extends the explain catalog with a full 'demo' page documenting audiences, modes, flags, dry-run behavior, and '--articulation' semantics; includes it in the catalog mapping.

harmonics/explain/catalog.py

Other (3) +3 / -3
currentPoint devague current frame at the demo-command spec +1/-1

Point devague current frame at the demo-command spec

• Updates the devague pointer to the new exported frame slug for the demo command work.

.devague/current

current_planPoint devague current plan at the demo-command plan +1/-1

Point devague current plan at the demo-command plan

• Updates the devague pointer to the new exported plan slug for the demo command work.

.devague/current_plan

pyproject.tomlBump project version to 0.6.0 +1/-1

Bump project version to 0.6.0

• Updates the package version from 0.5.0 to 0.6.0 to reflect the new shipped demo command.

pyproject.toml

Comment thread harmonics/demo/gallery.py Outdated
Comment thread harmonics/demo/gallery.py
Address qodo review on PR #5:
- Group header derivation now strips a trailing ' (agent)' qualifier, so
  'say (spark): ...' / 'say (daria): ...' group under one 'say' header instead
  of spawning a separate header per agent (was a visual misgrouping bug).
- Gallery page title uses 'harmonics-cli' (the product/identity name) rather
  than the 'harmonics' command name, per the repo naming convention.
Adds two regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMUvDkSHhthqU3c5HrhQuj
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

@OriNachum OriNachum merged commit 98256a4 into main Jul 8, 2026
8 checks passed
@OriNachum OriNachum deleted the feat/harmonics-demo branch July 8, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a harmonics demo command — the voice showcase (play / file / stream)

1 participant