Skip to content

feat(stories): Pro Studio Phase 3 — per-line tone tags + speed - #179

Merged
debpalash merged 1 commit into
mainfrom
feat/stories-studio-p3
May 30, 2026
Merged

feat(stories): Pro Studio Phase 3 — per-line tone tags + speed#179
debpalash merged 1 commit into
mainfrom
feat/stories-studio-p3

Conversation

@debpalash

@debpalash debpalash commented May 30, 2026

Copy link
Copy Markdown
Owner

Phase 3: studio depth per line, revealed only when you click the tune button (progressive disclosure).

  • Tone chips insert OmniVoice's native inline emotion/sound tags ([laughter], [sigh], [question-en], [surprise-wa], [confirmation-en], [dissatisfaction-hnn]) at the cursor. (The instruct param only whitelists gender/age/pitch/style/accent and rejects free emotion words — issues [Bug] Bad request - conflicting instruct items within the same category #114/Voice Design generates unsupported instructions on macOS Apple Silicon #115 — so tags are the correct mechanism.)
  • Per-line speed slider (0.5–2.0×) threaded into /generate for both preview and the audiobook export, with reset-to-default.
  • Refactor: pure insertToken in storyTokens (shared by pause + tones); exportStoryAudio resolves per-track {profileId, speed}.
  • i18n (en + zh-CN); 4 new unit tests. Full suite 143/143, typecheck/build/CJK ✓.

Next: Phase 4 (pro output — per-character stems, chapters, MP3 — + named projects).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Per-line tone insertion (laugh, sigh, question, surprise, confirm, dissatisfaction)
    • Per-track speed adjustment with reset control
  • Style

    • Improved story editor layout flexibility
  • Internationalization

    • Added English and Chinese translations for tone and speed controls
  • Tests

    • Added test coverage for token insertion functionality

Review Change Stack

Click the tune button on any line to reveal a drawer:
- Tone chips insert OmniVoice's native inline emotion/sound tags ([laughter],
  [sigh], [question-en], [surprise-wa], [confirmation-en], [dissatisfaction-hnn])
  at the cursor — the model-native way to direct tone (not the instruct param,
  which only whitelists gender/age/pitch/style/accent and rejects free emotion).
- Per-line speed slider (0.5–2.0x) → threaded into /generate for both preview
  and the audiobook export; reset-to-default.

- insertToken extracted to storyTokens (pure + tested); insertPauseInto + tone
  chips share it. exportStoryAudio now resolves per-track {profileId, speed}.
- i18n (en + zh-CN); 4 new unit tests. Full suite 143/143, typecheck/build/CJK ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR extends the StoriesEditor with per-track tone tag insertion and playback speed control. It introduces a reusable insertToken utility, adds UI controls (tune button and drawer with tone pills and speed slider), threads a speed parameter through the speech-generation pipeline, and provides localization strings for multiple languages.

Changes

Per-Line Tone and Speed Controls

Layer / File(s) Summary
Token insertion utility and tests
frontend/src/utils/storyTokens.js, frontend/src/utils/storyTokens.test.js
Introduces insertToken(text, caret, token) that inserts tokens at specified positions with whitespace padding; appends to end when caret is null or out of range. Tests cover caret insertion with padding, appending behavior, and edge cases.
Tone and speed UI controls and styling
frontend/src/components/StoriesEditor.jsx, frontend/src/components/StoriesEditor.css
Adds STORY_TONES data mapping tone tags to icons and translation keys; introduces expandedLine state to track the open drawer, insertTokenInto helper for token insertion, tune button, and conditional drawer UI rendering tone pills and a speed slider (0.5–2) with reset. CSS styles the drawer layout, tone pill buttons, speed input, and reset button hover state.
Speed parameter threading through speech generation
frontend/src/components/StoriesEditor.jsx, frontend/src/utils/storyExport.js
Extends fetchChunkBlob and fetchChunkAudio to accept speed; updates previewTrack to derive and pass track.speed to audio generation; changes exportStoryAudio signature from resolveProfile to resolveOpts to return both profileId and speed; augments chunk segments with speed before calling fetchChunkBlob.
Localization strings
frontend/src/i18n/locales/en.json, frontend/src/i18n/locales/zh-CN.json
Adds English and Chinese translations for tune/speed/reset labels and six tone options (laugh, sigh, question, surprise, confirm, dissatisfaction).

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • debpalash/OmniVoice-Studio#51: Main PR extends the Stories token infrastructure by adding insertToken utility to storyTokens.js rather than building in a separate area.
  • debpalash/OmniVoice-Studio#176: Both PRs thread speed into generateSpeech via updated fetchChunkAudio and preview-generation wiring for the speech-generation pipeline.
  • debpalash/OmniVoice-Studio#177: Main PR's tune drawer and speed-aware export changes extend the retrieved PR's StoriesEditor and exportStoryAudio pipeline with per-track speed support.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely captures the main feature additions: per-line tone tags and speed control in the stories editor.
Description check ✅ Passed The PR description comprehensively covers all key aspects: feature overview, technical implementation, refactoring changes, testing status, and i18n support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stories-studio-p3

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Phase 3 adds a per-line studio drawer (progressively revealed via a tune button) with curated tone-tag chips and a per-line speed slider (0.5–2.0×), both wired into the preview and audiobook export paths. The insertToken refactor cleanly unifies pause and tone insertion behind a single pure utility.

  • Tone chips insert OmniVoice-native inline tags ([laughter], [sigh], [question-en], etc.) at the textarea caret; parseStoryText intentionally passes them through unmodified so the TTS backend receives them directly.
  • Speed is threaded from per-track state through fetchChunkBlob into both previewTrack and exportStoryAudio, with resolveOpts replacing the old resolveProfile callback to carry both profileId and speed together.
  • insertToken is extracted to storyTokens.js as a pure function with four new unit tests; insertPauseInto now delegates to insertTokenInto which delegates to it.

Confidence Score: 4/5

The change is safe to merge; audio output and export are correctly wired end-to-end.

The speed and tone-tag wiring through preview and export is consistent and well-tested. The only noteworthy issue is a minor UX quirk where the Reset button appears when speed is explicitly set to 1.0 — clicking it changes stored state (1.0 → null) but produces no audible difference. The insertToken refactor is clean and the out-of-bounds-caret branch has no test.

StoriesEditor.jsx (Reset button condition) and storyTokens.test.js (missing out-of-bounds caret test) are worth a second look.

Important Files Changed

Filename Overview
frontend/src/components/StoriesEditor.jsx Adds expandedLine state, tune drawer with tone chips and speed slider; refactors insertPauseInto into a shared insertTokenInto. Reset button has a minor no-op UX issue at speed=1.0.
frontend/src/utils/storyExport.js resolveProfile renamed to resolveOpts returning {profileId, speed}; per-chunk speed correctly threaded to fetchChunkBlob. Logic is sound and backwards-compatible via the
frontend/src/utils/storyTokens.js Adds pure insertToken helper extracted from the old inline insertPauseInto. Logic is correct; tone tags intentionally pass through TOKEN_RE unmodified.
frontend/src/utils/storyTokens.test.js Four new tests cover the main insertToken paths; the out-of-bounds caret branch (caret > text.length) is untested.
frontend/src/i18n/locales/en.json Adds tune, speed, reset, and tones sub-keys. Keys align with usage in StoriesEditor.
frontend/src/i18n/locales/zh-CN.json Mirrors en.json additions with appropriate Chinese translations. No issues found.
frontend/src/components/StoriesEditor.css Adds drawer, tone chip, speed slider, and reset button styles; adds flex-wrap to .stories-track to support the full-width drawer row. Looks correct.

Sequence Diagram

sequenceDiagram
    participant User
    participant StoriesEditor
    participant storyTokens
    participant storyExport
    participant TTS as /generate (TTS API)

    User->>StoriesEditor: Click tune button on a line
    StoriesEditor->>StoriesEditor: setExpandedLine(track.id)

    User->>StoriesEditor: Click tone chip (e.g. [laughter])
    StoriesEditor->>storyTokens: insertToken(text, caret, "[laughter]")
    storyTokens-->>StoriesEditor: updated text
    StoriesEditor->>StoriesEditor: updateTrack(id, 'text', ...)

    User->>StoriesEditor: Drag speed slider (e.g. 1.5x)
    StoriesEditor->>StoriesEditor: updateTrack(id, 'speed', 1.5)

    User->>StoriesEditor: Click Preview
    StoriesEditor->>TTS: "fetchChunkAudio(text, profileId, speed=1.5)"
    TTS-->>StoriesEditor: audio blob
    StoriesEditor->>User: plays audio

    User->>StoriesEditor: Click Generate All
    StoriesEditor->>storyExport: exportStoryAudio(tracks, resolveOpts, fetchChunkBlob, onProgress)
    loop each track segment
        storyExport->>TTS: fetchChunkBlob(text, profileId, speed)
        TTS-->>storyExport: WAV blob
        storyExport->>storyExport: decodeAudioData + concat
    end
    storyExport-->>StoriesEditor: WAV Blob
    StoriesEditor->>User: download story.wav
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(stories): Phase 3 — per-line studio..." | Re-trigger Greptile

Comment on lines +535 to +537
{track.speed != null && (
<button type="button" className="stories-track__reset" onClick={() => updateTrack(track.id, 'speed', null)}>{t('stories.reset')}</button>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Reset button appears at speed=1.0 (no-op reset)

track.speed != null will show the Reset button even when the slider is sitting at exactly 1.0 — the same value null || 1 produces. Clicking Reset changes the stored state from 1.0null, but the slider display, the toFixed readout, and the audio output are all unchanged. A user who carefully drags back to 1.0 sees a Reset button that does nothing perceivable. Checking track.speed != null && track.speed !== 1 would hide the button when it's a no-op.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +98 to +101
it('handles empty text', () => {
expect(insertToken('', null, '[laughter]')).toBe('[laughter]');
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The insertToken test suite exercises the null caret and caret=0 branches but never the caret > t.length branch. Per the implementation this falls through to the append path — worth a covering case so that branch is explicitly verified.

Suggested change
it('handles empty text', () => {
expect(insertToken('', null, '[laughter]')).toBe('[laughter]');
});
});
it('handles empty text', () => {
expect(insertToken('', null, '[laughter]')).toBe('[laughter]');
});
it('appends when caret is beyond text length', () => {
expect(insertToken('hi', 99, '[sigh]')).toBe('hi [sigh]');
});
});

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/StoriesEditor.css`:
- Around line 382-396: The .stories-track is a grid container so
.stories-track__drawer won’t span the full row using flex properties; update the
drawer to explicitly span the grid by setting grid-column: 1 / -1 (or
grid-column: span <number> as appropriate) on .stories-track__drawer and
remove/replace ineffective flex-basis/flex-wrap assumptions; keep
.stories-track--dragover unchanged except for any needed visual adjustments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5718a20b-2161-401e-8b3a-3b8ab6fe46c9

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb8186 and 8b69c6b.

📒 Files selected for processing (7)
  • frontend/src/components/StoriesEditor.css
  • frontend/src/components/StoriesEditor.jsx
  • frontend/src/i18n/locales/en.json
  • frontend/src/i18n/locales/zh-CN.json
  • frontend/src/utils/storyExport.js
  • frontend/src/utils/storyTokens.js
  • frontend/src/utils/storyTokens.test.js

Comment on lines +382 to +396
.stories-track { cursor: grab; flex-wrap: wrap; }
.stories-track--dragover { box-shadow: inset 0 2px 0 0 var(--color-accent, #b8bb26); }

/* ── Per-line tone/speed drawer ───────────────────────────────────── */
.stories-track__btn--on { color: var(--color-accent); background: rgba(255, 255, 255, 0.06); }
.stories-track__drawer {
flex-basis: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--color-border);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does flex-wrapapply to CSS Grid containers, and isgrid-column: 1 / -1 the correct way to make a grid item span the full row?

💡 Result:

The flex-wrap property does not apply to CSS Grid containers [1][2]. The flex-wrap property is specific to the Flexbox layout model and controls whether flex items wrap onto multiple lines within a flex container [1][2]. CSS Grid and Flexbox are distinct layout systems; Grid handles two-dimensional layouts, while Flexbox handles one-dimensional layouts [3][1]. Regarding your second question, yes, grid-column: 1 / -1 is the standard and correct way to make a grid item span the full width of the grid container [4]. The value 1 refers to the first grid line, and -1 refers to the last grid line of the explicit grid, effectively spanning the item across all available columns [4][5].

Citations:


🌐 Web query:

Does flex-basis apply to items in CSS Grid layout (i.e., grid items), or is it only relevant for flex items?

💡 Result:

No—flex-basis is for flex layout only, not CSS Grid. flex-basis sets the “initial main size of a flex item” and “applies to flex items” [1]. The CSS Flexible Box spec defines flex-basis as part of Flexbox’s layout algorithm for flex items [2]. Therefore, if an element is a Grid item (i.e., it is laid out by a parent with display: grid), flex-basis does not participate in Grid track sizing; Grid uses grid-specific sizing properties (e.g., grid-template-columns/minmax(), fr units) rather than flex-basis. What you should use in Grid instead: put your constraints in the track definitions, e.g. grid-template-columns: repeat(auto-fill, minmax(<min-track>, <max-track>)) and use functions like min()/clamp() (or viewport-relative units) to achieve responsive minimums—rather than trying to apply flex-basis to Grid items [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="frontend/src/components/StoriesEditor.css"

echo "== Matching selectors around .stories-track =="
rg -n "\.stories-track(\s|\{)" "$file" || true
echo
echo "== Lines near the mentioned snippet (350-420) =="
nl -ba "$file" | sed -n '350,430p'

Repository: debpalash/OmniVoice-Studio

Length of output: 303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="frontend/src/components/StoriesEditor.css"

echo "== Lines around first .stories-track definition =="
sed -n '40,90p' "$file"

echo
echo "== Lines near the mentioned snippet (370-410) =="
cat -n "$file" | sed -n '370,420p'

Repository: debpalash/OmniVoice-Studio

Length of output: 3385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "stories-track__drawer" -S frontend/src || true
rg -n "\bstories-track\b" -S frontend/src/components | head -n 200 || true

Repository: debpalash/OmniVoice-Studio

Length of output: 1234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="frontend/src/components/StoriesEditor.jsx"

# Show context around the drawer render and the track row container
rg -n "stories-track__drawer" "$file"
echo "---- context ----"
sed -n '470,560p' "$file"

Repository: debpalash/OmniVoice-Studio

Length of output: 5588


Fix grid layout so the tone/speed drawer spans the full track row (frontend/src/components/StoriesEditor.css, lines ~382-396)

.stories-track is display: grid, so flex-wrap on the grid container has no effect, and .stories-track__drawer (a grid item) won’t span the row just via flex-basis: 100%—it will land in a single grid cell. Set grid spanning explicitly.

💡 Suggested fix
-.stories-track { cursor: grab; flex-wrap: wrap; }
+.stories-track { cursor: grab; }

 .stories-track__drawer {
-  flex-basis: 100%;
+  grid-column: 1 / -1;
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   gap: 12px;
   margin-top: 8px;
   padding-top: 8px;
   border-top: 1px solid var(--color-border);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StoriesEditor.css` around lines 382 - 396, The
.stories-track is a grid container so .stories-track__drawer won’t span the full
row using flex properties; update the drawer to explicitly span the grid by
setting grid-column: 1 / -1 (or grid-column: span <number> as appropriate) on
.stories-track__drawer and remove/replace ineffective flex-basis/flex-wrap
assumptions; keep .stories-track--dragover unchanged except for any needed
visual adjustments.

@debpalash
debpalash merged commit e07d7d9 into main May 30, 2026
15 checks passed
@debpalash
debpalash deleted the feat/stories-studio-p3 branch May 30, 2026 17:13
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.

1 participant