Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions src/SpeechMarkdownGrammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,64 @@ export function speechMarkdownGrammar(myna: any): any {
this.time = m.seq(this.number, this.timeUnit).ast;
this.shortBreak = m.seq('[', this.time, ']').ast;

// Expressive audio tags: [laugh], [sigh], [cough], etc.
this.expressiveValue = m.keywords(
'laugh',
'laughter',
'sigh',
'cough',
'cheer',
'cheering',
'cry',
'crying',
'gasp',
'groan',
'groaning',
'hum',
'hmm',
'mm-hmm',
'oh',
'sniff',
'whew',
'wow',
'yawn',
'yeah',
'huh',
'tsk',
'uh-huh',
'mmm',
'mhm',
'ahem',
'applause',
'boo',
'giggle',
'hiccup',
'hurray',
'moan',
'pant',
'scream',
'shush',
'sneeze',
'throat-clear',
'wheeze',
'whimper',
'yay',
'bleh',
'eek',
'hmm',
'huh',
'meh',
'ooh',
'pfft',
'phew',
'psst',
'shh',
'tsk-tsk',
'uh-oh',
'umph',
).ast;
this.expressive = m.seq('[', this.expressiveValue, ']').ast;

// this.break = m.seq('[break:', this.time , ']').ast;

// this.string = m.doubleQuoted(this.quoteChar.zeroOrMore).ast;
Expand Down Expand Up @@ -449,6 +507,7 @@ export function speechMarkdownGrammar(myna: any): any {
this.shortSub,
this.textModifier,
this.emphasis,
this.expressive,
this.shortBreak,
this.break,
this.audio,
Expand Down
5 changes: 5 additions & 0 deletions src/formatters/ElevenLabsFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export class ElevenLabsFormatter extends SsmlFormatterBase {
const time = ast.children[0].allText;
return this.addTagWithAttrs(lines, null, 'break', { time: time });
}
case 'expressive': {
const value = ast.children[0].allText;
lines.push(`[${value}]`);
return lines;
}
case 'break': {
const val = ast.children[0].allText;
let time = val;
Expand Down
6 changes: 6 additions & 0 deletions src/formatters/TextFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export class TextFormatter extends FormatterBase {
case 'audio':
return lines;

case 'expressive': {
const value = ast.children[0].allText;
lines.push(`[${value}]`);
return lines;
}

default: {
this.processAst(ast.children, lines);
return lines;
Expand Down
5 changes: 5 additions & 0 deletions src/formatters/W3cSsmlFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ export class W3cSsmlFormatter extends SsmlFormatterBase {
const time = ast.children[0].allText;
return this.addTagWithAttrs(lines, null, 'break', { time });
}
case 'expressive': {
const value = ast.children[0].allText;
lines.push(`[${value}]`);
return lines;
}
case 'break': {
const val = ast.children[0].allText;
let attrs = {};
Expand Down
111 changes: 111 additions & 0 deletions tests/expressive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import dedent from 'ts-dedent';
import { SpeechMarkdown } from '../src/SpeechMarkdown';

describe('expressive', () => {
const speech = new SpeechMarkdown();

const markdown = dedent`
Hello [laugh] world
`;

test('converts to SSML - W3C', () => {
const ssml = speech.toSSML(markdown, { platform: 'w3c' });
const expected = dedent`
<speak>
Hello [laugh] world
</speak>
`;
expect(ssml).toBe(expected);
});

test('converts to SSML - ElevenLabs', () => {
const ssml = speech.toSSML(markdown, { platform: 'elevenlabs' });
expect(ssml).toBe('Hello [laugh] world');
});

test('converts to Plain Text', () => {
const text = speech.toText(markdown);
expect(text).toBe('Hello [laugh] world');
});

test('converts to SSML - Amazon Polly (strips)', () => {
const ssml = speech.toSSML(markdown, { platform: 'amazon-polly' });
const expected = dedent`
<speak>
Hello world
</speak>
`;
expect(ssml).toBe(expected);
});

test('converts to SSML - Google Assistant (strips)', () => {
const ssml = speech.toSSML(markdown, { platform: 'google-assistant' });
const expected = dedent`
<speak>
Hello world
</speak>
`;
expect(ssml).toBe(expected);
});

test('converts to SSML - Microsoft Azure (strips)', () => {
const ssml = speech.toSSML(markdown, { platform: 'microsoft-azure' });
const expected = dedent`
<speak>
Hello world
</speak>
`;
expect(ssml).toBe(expected);
});
});

describe('expressive multiple', () => {
const speech = new SpeechMarkdown();

const markdown = dedent`
Hello [laugh] how are you [sigh] I'm fine [cough]
`;

test('converts to SSML - W3C', () => {
const ssml = speech.toSSML(markdown, { platform: 'w3c' });
const expected = dedent`
<speak>
Hello [laugh] how are you [sigh] I'm fine [cough]
</speak>
`;
expect(ssml).toBe(expected);
});

test('converts to Plain Text', () => {
const text = speech.toText(markdown);
expect(text).toBe("Hello [laugh] how are you [sigh] I'm fine [cough]");
});
});

describe('expressive edge cases', () => {
const speech = new SpeechMarkdown();

test('expressive does not conflict with shortBreak [250ms]', () => {
const md = 'Hello [laugh] wait [250ms] world';
const ssml = speech.toSSML(md, { platform: 'w3c' });
const expected = dedent`
<speak>
Hello [laugh] wait <break time="250ms"/> world
</speak>
`;
expect(ssml).toBe(expected);
});

test('expressive does not conflict with textModifier', () => {
const md = '(hello)[emphasis:"strong"] [laugh]';
const ssml = speech.toSSML(md, { platform: 'w3c' });
expect(ssml).toContain('[laugh]');
expect(ssml).toContain('<emphasis');
});

test('unknown bracket content falls through as plain text', () => {
const md = 'Hello [unknownthing] world';
const text = speech.toText(md);
expect(text).toBe('Hello [unknownthing] world');
});
});
Loading