This repository has been archived by the owner on Oct 18, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
speech.go
267 lines (229 loc) · 7.84 KB
/
speech.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package main
import (
"fmt"
alexa "github.com/brianglass/go-alexa/skillserver"
"github.com/brianglass/orthocal"
"regexp"
"strings"
"time"
)
const (
maxSpeechLength = 8000
)
var (
markupRe = regexp.MustCompile(`<.*?>`)
refRe = regexp.MustCompile(`(\d*)\s*([\w\s]+)\s+(\d+)`)
)
var epistles = map[string]string{
"acts": "The Acts of the Apostles",
"romans": "Saint Paul's letter to the Romans",
"corinthians": "Saint Paul's <say-as interpret-as=\"ordinal\">%s</say-as> letter to the Corinthians",
"galatians": "Saint Paul's letter to the Galatians",
"ephesians": "Saint Paul's letter to the Ephesians",
"philippians": "Saint Paul's letter to the Philippians",
"colossians": "Saint Paul's letter to the Colossians",
"thessalonians": "Saint Paul's <say-as interpret-as=\"ordinal\">%s</say-as> letter to the Thessalonians",
"timothy": "Saint Paul's <say-as interpret-as=\"ordinal\">%s</say-as> letter to Timothy",
"titus": "Saint Paul's letter to Titus",
"philemon": "Saint Paul's letter to Philemon",
"hebrews": "Saint Paul's letter to the Hebrews",
"james": "The Catholic letter of Saint James",
"peter": "The <say-as interpret-as=\"ordinal\">%s</say-as> Catholic letter of Saint Peter",
"john": "The <say-as interpret-as=\"ordinal\">%s</say-as> Catholic letter of Saint John",
"jude": "The Catholic letter of Saint Jude",
}
func DaySpeech(builder *alexa.SSMLTextBuilder, day *orthocal.Day, tz *time.Location) (card string) {
var feasts, saints string
when := WhenSpeach(day, tz)
// Commemorations
if len(day.Feasts) > 1 {
feasts = fmt.Sprintf("The feasts celebrated are: %s.", HumanJoin(day.Feasts))
} else if len(day.Feasts) == 1 {
feasts = fmt.Sprintf("The feast of %s is celebrated.", day.Feasts[0])
}
if len(day.Saints) > 1 {
saints = fmt.Sprintf("The commemorations are for %s.", HumanJoin(day.Saints))
} else if len(day.Saints) == 1 {
saints = fmt.Sprintf("The commemoration is for %s.", day.Saints[0])
}
// Create the Card text
if len(day.Titles) > 0 {
card = when + ", is the " + day.Titles[0] + ".\n\n"
}
if len(day.FastExceptionDesc) > 0 {
card += fmt.Sprintf("%s \u2013 %s\n\n", day.FastLevelDesc, day.FastExceptionDesc)
} else {
card += fmt.Sprintf("%s\n\n", day.FastLevelDesc)
}
if len(feasts) > 0 {
card += feasts + "\n\n"
}
if len(saints) > 0 {
card += saints + "\n\n"
}
for _, reading := range day.Readings {
card += reading.Display + "\n"
}
// Create the speech
if len(day.Titles) > 0 {
builder.AppendParagraph(when + ", is the " + day.Titles[0] + ".")
}
builder.AppendParagraph(FastingSpeech(day))
builder.AppendParagraph(strings.Replace(feasts, "Ven.", `<sub alias="The Venerable">Ven.</sub>`, -1))
builder.AppendParagraph(strings.Replace(saints, "Ven.", `<sub alias="The Venerable">Ven.</sub>`, -1))
return card
}
func WhenSpeach(day *orthocal.Day, tz *time.Location) (when string) {
now := time.Now().In(tz)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, tz)
date := time.Date(day.Year, time.Month(day.Month), day.Day, 0, 0, 0, 0, tz)
hours := date.Sub(today).Hours()
if 0 <= hours && hours < 24 {
when = "Today, " + date.Format("January 2")
} else if 24 <= hours && hours < 48 {
when = "Tomorrow, " + date.Format("January 2")
} else {
when = date.Format("Monday, January 2")
}
return when
}
func FastingSpeech(day *orthocal.Day) (text string) {
switch day.FastLevel {
case 0:
text = "On this day there is no fast."
case 1:
// normal weekly fast
if len(day.FastExceptionDesc) > 0 {
text = fmt.Sprintf("On this day there is a fast. %s.", day.FastExceptionDesc)
} else {
text = fmt.Sprintf("On this day there is a fast.")
}
default:
// One of the four great fasts
if len(day.FastExceptionDesc) > 0 {
text = fmt.Sprintf("This day is during the %s. %s.", day.FastLevelDesc, day.FastExceptionDesc)
} else {
text = fmt.Sprintf("This day is during the %s.", day.FastLevelDesc)
}
}
return text
}
func ReadingSpeech(builder *alexa.SSMLTextBuilder, reading orthocal.Reading, end int) {
reference := ReferenceSpeech(reading)
builder.AppendParagraph("The reading is from " + reference + ".")
builder.AppendBreak("medium", "750ms")
if len(reading.Passage) == 0 {
builder.AppendParagraph("Orthodox Daily could not find that reading.")
return
}
if end > 0 {
for i := 0; i < end && i < len(reading.Passage); i++ {
text := markupRe.ReplaceAllString(reading.Passage[i].Content, "")
builder.AppendParagraph(text)
}
} else {
for _, verse := range reading.Passage {
text := markupRe.ReplaceAllString(verse.Content, "")
builder.AppendParagraph(text)
}
}
}
func ReadingRangeSpeech(builder *alexa.SSMLTextBuilder, reading orthocal.Reading, start, end int) {
for i := start; i < end && i < len(reading.Passage); i++ {
text := markupRe.ReplaceAllString(reading.Passage[i].Content, "")
builder.AppendParagraph(text)
}
}
func ReferenceSpeech(reading orthocal.Reading) (speech string) {
groups := refRe.FindStringSubmatch(reading.Display)
if len(groups) < 4 {
// The reference is irregular so we just let Alexa do the best she can
return strings.Replace(reading.Display, ".", ":", -1)
}
// The book here is the book of the Bible whereas the book below is the
// liturgical book
number, book, chapter := groups[1], groups[2], groups[3]
switch strings.ToLower(reading.Book) {
case "matthew", "mark", "luke", "john":
speech = fmt.Sprintf("The Holy Gospel according to Saint %s, chapter %s", book, chapter)
case "apostol":
format, ok := epistles[strings.ToLower(book)]
if !ok {
speech = fmt.Sprintf(book+", chapter %s", chapter)
} else if len(number) > 0 {
speech = fmt.Sprintf(format+", chapter %s", number, chapter)
} else {
speech = fmt.Sprintf(format+", chapter %s", chapter)
}
case "ot":
if len(number) > 0 {
speech = fmt.Sprintf("<say-as interpret-as=\"ordinal\">%s</say-as> %s, chapter %s", number, book, chapter)
} else {
speech = fmt.Sprintf("%s, chapter %s", book, chapter)
}
default:
speech = strings.Replace(reading.Display, ".", ":", -1)
}
return speech
}
func HumanJoin(words []string) string {
if len(words) > 1 {
return strings.Join(words[:len(words)-1], ", ") + " and " + words[len(words)-1]
} else {
return words[0]
}
}
func GetPassageLength(passage orthocal.Passage, start, end int) (length int) {
if start < 0 {
start = 0
}
if end <= 0 {
end = len(passage)
}
for i := start; i < end && i < len(passage); i++ {
length += len(passage[i].Content) + len("<p></p>")
}
return length
}
func EstimateGroupSize(passage orthocal.Passage) (groupSize int) {
const (
prelude = len(`<p>There are 29 readings for Tuesday, January 3. The reading is from Saint Paul's <say-as interpret-as=\"ordinal\">2</say-as> letter to the Thessalonians</p>`)
postlude = len(`<p>Would you like to hear the next reading?</p>`)
groupPostlude = len(`<p>This is a long reading. Would you like me to continue?</p>`)
)
verseCount := len(passage)
passageLength := prelude + GetPassageLength(passage, 0, 0) + postlude
if passageLength <= maxSpeechLength {
// Yay! We don't have to break the passage into groups.
return -1
}
// Start with a good guess and then grow the group count until we find one
// that fits.
groupCount := passageLength/maxSpeechLength + 1
for failed := true; failed; groupCount++ {
groupSize = verseCount / groupCount
if verseCount%groupCount > 0 {
groupSize++
}
// loop over the groups and fail if one of the groups is too big
failed = false
for g := 0; g < groupCount; g++ {
start := g * groupSize
end := start + groupSize
length := GetPassageLength(passage, start, end)
if g == 0 {
length += prelude
}
if g == groupCount-1 {
length += postlude
} else {
length += groupPostlude
}
if length > maxSpeechLength {
failed = true
break
}
}
}
return groupSize
}