Skip to content

Commit 0ccf17c

Browse files
authored
Add support for Bixby and audio tag (#41)
* Add Bixby support * Update version to 1.1.0
1 parent 8da55f4 commit 0ccf17c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1794
-7
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"cSpell.words": [
33
"Aditi",
4+
"Bixby",
45
"Celine",
56
"Conchita",
67
"Giorgio",

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "speechmarkdown-js",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Speech Markdown parser and formatters in TypeScript.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/formatters/AmazonAlexaSsmlFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ export class AmazonAlexaSsmlFormatter extends SsmlFormatterBase {
297297
switch (ast.name) {
298298
case 'document': {
299299
if (this.options.includeFormatterComment) {
300-
this.addComment('Speech Markdown for Amazon Alexa', lines);
300+
this.addComment('Converted from Speech Markdown to SSML for Amazon Alexa', lines);
301301
}
302302

303303
if (this.options.includeSpeakTag) {

src/formatters/FormatterFactory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { Formatter } from '../Interfaces';
22
import { SpeechOptions } from '../SpeechOptions';
3+
import { TextFormatter } from "./TextFormatter";
34
import { AmazonAlexaSsmlFormatter } from "./AmazonAlexaSsmlFormatter";
45
import { GoogleAssistantSsmlFormatter } from "./GoogleAssistantSsmlFormatter";
5-
import { TextFormatter } from "./TextFormatter";
6+
import { SamsungBixbySsmlFormatter } from './SamsungBixbySsmlFormatter';
67

78
export function createFormatter(options: SpeechOptions): Formatter {
89
switch(options.platform) {
910
case 'amazon-alexa':
1011
return new AmazonAlexaSsmlFormatter(options);
1112
case 'google-assistant':
1213
return new GoogleAssistantSsmlFormatter(options);
14+
case 'samsung-bixby':
15+
return new SamsungBixbySsmlFormatter(options);
1316
default:
1417
return new TextFormatter(options);
1518
}

src/formatters/GoogleAssistantSsmlFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class GoogleAssistantSsmlFormatter extends SsmlFormatterBase {
135135
switch (ast.name) {
136136
case 'document': {
137137
if (this.options.includeFormatterComment) {
138-
this.addComment('Speech Markdown for Google Assistant', lines);
138+
this.addComment('Converted from Speech Markdown to SSML for Google Assistant', lines);
139139
}
140140

141141
if (this.options.includeSpeakTag) {
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { SpeechOptions } from '../SpeechOptions';
2+
import { SsmlFormatterBase } from './SsmlFormatterBase';
3+
4+
export class SamsungBixbySsmlFormatter extends SsmlFormatterBase {
5+
6+
constructor(public options: SpeechOptions) {
7+
super(options);
8+
9+
this.modifierKeyToSsmlTagMappings.emphasis = null;
10+
this.modifierKeyToSsmlTagMappings.address = null;
11+
this.modifierKeyToSsmlTagMappings.number = null;
12+
this.modifierKeyToSsmlTagMappings.characters = null;
13+
this.modifierKeyToSsmlTagMappings.expletive = null;
14+
this.modifierKeyToSsmlTagMappings.fraction = null;
15+
this.modifierKeyToSsmlTagMappings.interjection = null;
16+
this.modifierKeyToSsmlTagMappings.ordinal = null;
17+
this.modifierKeyToSsmlTagMappings.telephone = null;
18+
this.modifierKeyToSsmlTagMappings.unit = null;
19+
this.modifierKeyToSsmlTagMappings.time = null;
20+
this.modifierKeyToSsmlTagMappings.date = null;
21+
this.modifierKeyToSsmlTagMappings.sub = null;
22+
this.modifierKeyToSsmlTagMappings.ipa = null;
23+
this.modifierKeyToSsmlTagMappings.rate = null;
24+
this.modifierKeyToSsmlTagMappings.pitch = null;
25+
this.modifierKeyToSsmlTagMappings.volume = null;
26+
}
27+
28+
// tslint:disable-next-line: max-func-body-length
29+
// private getTextModifierObject(ast: any): any {
30+
// let textModifierObject = {
31+
// tags: {}
32+
// };
33+
34+
// for (let index = 0; index < ast.children.length; index++) {
35+
// const child = ast.children[index];
36+
37+
// switch (child.name) {
38+
// case 'plainText':
39+
// case 'plainTextSpecialChars':
40+
// case 'plainTextEmphasis':
41+
// case 'plainTextPhone':
42+
// case 'plainTextModifier': {
43+
// textModifierObject['text'] = child.allText;
44+
// break;
45+
// }
46+
// case 'textModifierKeyOptionalValue': {
47+
// let key = child.children[0].allText;
48+
// key = this.modifierKeyMappings[key] || key;
49+
// const value = child.children.length === 2 ? child.children[1].allText : '';
50+
// const ssmlTag = this.modifierKeyToSsmlTagMappings[key];
51+
// const sortId = this.ssmlTagSortOrder.indexOf(ssmlTag);
52+
53+
// switch (key) {
54+
// case 'emphasis': {
55+
// if (!textModifierObject.tags[ssmlTag]) {
56+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
57+
// }
58+
// textModifierObject.tags[ssmlTag].attrs = { level: value || 'moderate' };
59+
// break;
60+
// }
61+
62+
// case 'address':
63+
// case 'characters':
64+
// case 'expletive':
65+
// case 'fraction':
66+
// case 'number':
67+
// case 'ordinal':
68+
// case 'telephone':
69+
// case 'unit': {
70+
// if (!textModifierObject.tags[ssmlTag]) {
71+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
72+
// }
73+
// textModifierObject.tags[ssmlTag].attrs = { 'interpret-as': key };
74+
// break;
75+
// }
76+
77+
// case 'date': {
78+
// if (!textModifierObject.tags[ssmlTag]) {
79+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
80+
// }
81+
// textModifierObject.tags[ssmlTag].attrs = { 'interpret-as': key, format: value || 'ymd' };
82+
// break;
83+
// }
84+
85+
// case 'time': {
86+
// if (!textModifierObject.tags[ssmlTag]) {
87+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
88+
// }
89+
// textModifierObject.tags[ssmlTag].attrs = { 'interpret-as': key, format: value || 'hms12' };
90+
// break;
91+
// }
92+
93+
// case 'whisper': {
94+
// if (!textModifierObject.tags[ssmlTag]) {
95+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
96+
// }
97+
// textModifierObject.tags[ssmlTag].attrs = { volume: 'x-soft', rate: 'slow' };
98+
// break;
99+
// }
100+
101+
// case 'ipa': {
102+
// // Google Assistant does not support <phoneme> tag
103+
// if (!textModifierObject.tags[ssmlTag]) {
104+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
105+
// }
106+
// textModifierObject['textOnly'] = true;
107+
// break;
108+
// }
109+
110+
// case 'sub': {
111+
// if (!textModifierObject.tags[ssmlTag]) {
112+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
113+
// }
114+
// textModifierObject.tags[ssmlTag].attrs = { alias: value };
115+
// break;
116+
// }
117+
118+
// case 'volume':
119+
// case 'rate':
120+
// case 'pitch': {
121+
122+
// if (!textModifierObject.tags[ssmlTag]) {
123+
// textModifierObject.tags[ssmlTag] = { sortId: sortId, attrs: null };
124+
// }
125+
126+
// const attrs = {};
127+
// attrs[key] = value || 'medium';
128+
// textModifierObject.tags[ssmlTag].attrs = { ...textModifierObject.tags[ssmlTag].attrs, ...attrs };
129+
130+
// break;
131+
// }
132+
133+
// default: {
134+
135+
// }
136+
137+
// }
138+
// break;
139+
// }
140+
// }
141+
142+
// }
143+
144+
// return textModifierObject;
145+
// }
146+
147+
// tslint:disable-next-line: max-func-body-length
148+
protected formatFromAst(ast: any, lines: string[] = []): string[] {
149+
150+
switch (ast.name) {
151+
case 'document': {
152+
if (this.options.includeFormatterComment) {
153+
this.addComment('Converted from Speech Markdown to SSML for Samsung Bixby', lines);
154+
}
155+
156+
if (this.options.includeSpeakTag) {
157+
return this.addSpeakTag(ast.children, true, false, null, lines);
158+
} else {
159+
this.processAst(ast.children, lines);
160+
return lines;
161+
}
162+
}
163+
case 'paragraph': {
164+
if (this.options.includeParagraphTag) {
165+
return this.addTag('p', ast.children, true, false, null, lines);
166+
} else {
167+
this.processAst(ast.children, lines);
168+
return lines;
169+
}
170+
}
171+
// case 'shortBreak': {
172+
// const time = ast.children[0].allText;
173+
// return this.addTagWithAttrs(lines, null, 'break', { time: time });
174+
// }
175+
// case 'shortEmphasisModerate': {
176+
// const text = ast.children[0].allText;
177+
// return this.addTagWithAttrs(lines, text, 'emphasis', { level: 'moderate' });
178+
// }
179+
// case 'shortEmphasisStrong': {
180+
// const text = ast.children[0].allText;
181+
// return this.addTagWithAttrs(lines, text, 'emphasis', { level: 'strong' });
182+
// }
183+
// case 'shortEmphasisNone': {
184+
// const text = ast.children[0].allText;
185+
// return this.addTagWithAttrs(lines, text, 'emphasis', { level: 'none' });
186+
// }
187+
// case 'shortEmphasisReduced': {
188+
// const text = ast.children[0].allText;
189+
// return this.addTagWithAttrs(lines, text, 'emphasis', { level: 'reduced' });
190+
// }
191+
192+
// case 'textModifier': {
193+
// const tmo = this.getTextModifierObject(ast);
194+
195+
// if (tmo.textOnly) {
196+
// // Quick return if tag is not supported
197+
// lines.push(tmo.text)
198+
// return lines
199+
// }
200+
201+
// const tagsSortedDesc = Object.keys(tmo.tags).sort((a: any, b: any) => { return tmo.tags[b].sortId - tmo.tags[a].sortId });
202+
203+
// let inner = tmo.text;
204+
205+
// for (let index = 0; index < tagsSortedDesc.length; index++) {
206+
// const tag = tagsSortedDesc[index];
207+
// const attrs = tmo.tags[tag].attrs;
208+
209+
// inner = this.getTagWithAttrs(inner, tag, attrs);
210+
211+
// }
212+
// lines.push(inner);
213+
214+
// return lines;
215+
// }
216+
case 'audio': {
217+
const url = ast.children[0].allText;
218+
return this.addTagWithAttrs(lines, null, 'audio', { src: url });
219+
}
220+
case 'simpleLine': {
221+
this.processAst(ast.children, lines);
222+
return lines;
223+
}
224+
case 'lineEnd': {
225+
lines.push(ast.allText);
226+
return lines;
227+
}
228+
case 'emptyLine': {
229+
if (this.options.preserveEmptyLines) {
230+
lines.push(ast.allText);
231+
}
232+
233+
return lines;
234+
}
235+
case 'plainText':
236+
case 'plainTextSpecialChars':
237+
case 'plainTextEmphasis':
238+
case 'plainTextPhone':
239+
case 'plainTextModifier': {
240+
lines.push(ast.allText);
241+
return lines;
242+
}
243+
244+
default: {
245+
this.processAst(ast.children, lines);
246+
return lines;
247+
}
248+
}
249+
}
250+
}

tests/address-standard.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ describe('address-standard', () => {
4242
expect(ssml).toBe(expected);
4343
});
4444

45+
test('converts to SSML - Samsung Bixby', () => {
46+
47+
const options = {
48+
platform: 'samsung-bixby'
49+
};
50+
const ssml = speech.toSSML(markdown, options);
51+
52+
const expected = dedent`
53+
<speak>
54+
I'm at 150th CT NE, Redmond, WA.
55+
</speak>
56+
`;
57+
58+
expect(ssml).toBe(expected);
59+
});
60+
4561
test('converts to Plain Text', () => {
4662

4763
const options = {

0 commit comments

Comments
 (0)