-
Notifications
You must be signed in to change notification settings - Fork 0
/
emitter.go
189 lines (165 loc) · 4.72 KB
/
emitter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package alexa
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"github.com/fsm/emitable"
)
// emitter is an implementation of an FSM emitter for Amazon Alexa
//
// Because Amazon Alexa expects all outgoing messages / data to be in the form
// of a response to the inbound request (as compared to pushing messages), there
// is a speechBuffer that is generated within this struct as Emit is called
// throughout the lifecycle of a state.
//
// When Flush() is called on this struct, the SpeechBuffer is converted into the
// expected Alexa response, and written to the ResponseWriter.
//
// https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html#ssml-supported
type emitter struct {
ResponseWriter io.Writer
supportedInterfaces supportedInterfaces
hasSpeech bool
speechBuffer bytes.Buffer
shouldEndSession bool
card *card
directives []directive
}
// Emit prepares the data to be output at the end of the request.
func (e *emitter) Emit(input interface{}) error {
switch v := input.(type) {
case string:
e.speechBuffer.WriteString(copyToSSML(v))
e.hasSpeech = true
return nil
case emitable.Sleep:
e.speechBuffer.WriteString("<break time=\"")
e.speechBuffer.WriteString(strconv.Itoa(v.LengthMillis))
e.speechBuffer.WriteString("ms\"/>")
return nil
case emitable.QuickReply:
e.hasSpeech = true
// Options
optionsBuffer := new(bytes.Buffer)
for i, reply := range v.Replies {
optionsBuffer.WriteString(reply)
if i+2 < len(v.Replies) && len(v.Replies) > 2 {
optionsBuffer.WriteString(", ")
} else if i+1 < len(v.Replies) {
if len(v.Replies) > 2 {
optionsBuffer.WriteString(", or ")
} else {
optionsBuffer.WriteString(" or ")
}
}
}
// Determine format
format := "You can say %v"
if v.RepliesFormat != "" {
format = v.RepliesFormat
}
// Write out options
e.speechBuffer.WriteString(copyToSSML(fmt.Sprintf(format, optionsBuffer.String())))
// Write message
e.speechBuffer.WriteString(copyToSSML(v.Message))
return nil
case emitable.Typing:
// Intentionally do nothing
return nil
case emitable.Audio:
e.speechBuffer.WriteString("<audio src=\"")
e.speechBuffer.WriteString(v.URL)
e.speechBuffer.WriteString("\"/>")
return nil
case emitable.Video:
// TODO
return nil
case emitable.File:
// TODO
return nil
case emitable.Image:
if e.supportedInterfaces.Display != nil {
bodyTemplate := bodyTemplate7{
BackgroundImageURL: v.URL,
}
e.directives = append(e.directives, bodyTemplate.asDirective())
}
return nil
case StandardCard:
e.card = &card{
Type: "Standard",
Title: v.Title,
Text: v.Text,
Image: cardImage{
SmallImageURL: v.ImageURL,
LargeImageURL: v.ImageURL,
},
}
return nil
case EndSession:
e.shouldEndSession = true
return nil
}
return errors.New("AlexaEmitter cannot handle " + reflect.TypeOf(input).String())
}
// Converts copy to an appropriate SSML paragraph that will be read out
// by Alexa as naturally as possible.
//
// This function adds appropriate pauses to punctuation in the middle
// of a paragraph tag, as Alexa doesn't seem to do this normally for
// some strange reason.
func copyToSSML(copy string) string {
// Trim last punctuation, as we don't want to add a pause,
// as the pause will be handled by the end of the <p></p> SSML tag.
trimmed := ""
ssml := copy
if strings.HasSuffix(copy, ".") {
trimmed = "."
ssml = strings.TrimSuffix(ssml, trimmed)
} else if strings.HasSuffix(copy, "!") {
trimmed = "!"
ssml = strings.TrimSuffix(ssml, trimmed)
} else if strings.HasSuffix(copy, "?") {
trimmed = "?"
ssml = strings.TrimSuffix(ssml, trimmed)
}
// Add pauses after punctuation
ssml = strings.Replace(ssml, ".", ".<break time=\"150ms\"/>", -1)
ssml = strings.Replace(ssml, "?", "?<break time=\"150ms\"/>", -1)
ssml = strings.Replace(ssml, "!", "!<break time=\"150ms\"/>", -1)
ssml = strings.Replace(ssml, ",", ",<break time=\"50ms\"/>", -1)
// Join it all together and return
return "<s>" + ssml + trimmed + "</s>"
}
// Flush writes the expected Alexa response to the a.ResponseWriter.
func (e *emitter) Flush() error {
// Prepare response body
response := &responseBody{
Version: "1.0",
Response: &response{
ShouldEndSession: e.shouldEndSession,
Directives: &e.directives,
Card: e.card,
},
}
// Handle speech
if e.hasSpeech {
ssml := "<speak>" + e.speechBuffer.String() + "</speak>"
response.Response.OutputSpeech = &outputSpeech{
Type: "SSML",
SSML: ssml,
}
}
// Output response
b, err := json.Marshal(response)
if err != nil {
return err
}
fmt.Fprint(e.ResponseWriter, string(b))
return nil
}