Skip to content

Commit 1f3d3ae

Browse files
Add word number support for timer duration extraction
- Created convertWordToNumber helper function in both Kotlin and Swift - Converts word numbers (one, two, three, etc.) to digits before processing - Updated all timer duration extraction patterns to match word numbers - Patterns now support: 'three minutes', 'five hours', 'ten seconds', etc. - Applied to duration, timer-specific, in/after/within, and countdown patterns - Fixes issue where 'set a timer for three minutes' wasn't extracting value - Now properly returns 180 seconds for 'three minutes'
1 parent f889fb8 commit 1f3d3ae

File tree

2 files changed

+122
-40
lines changed

2 files changed

+122
-40
lines changed

examples/whisper.android/app/src/main/java/com/whispercppdemo/intent/SlotExtractor.kt

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,41 @@ class SlotExtractor {
661661
return numbers.firstOrNull()?.groupValues?.get(1)?.replace(",", "")?.toDoubleOrNull()?.toInt()
662662
}
663663

664+
/**
665+
* Converts word numbers to digits
666+
* Examples: "three" -> "3", "fifteen" -> "15", "twenty five" -> "25"
667+
*/
668+
private fun convertWordToNumber(text: String): String {
669+
val wordToDigit = mapOf(
670+
"zero" to "0", "one" to "1", "two" to "2", "three" to "3", "four" to "4",
671+
"five" to "5", "six" to "6", "seven" to "7", "eight" to "8", "nine" to "9",
672+
"ten" to "10", "eleven" to "11", "twelve" to "12", "thirteen" to "13",
673+
"fourteen" to "14", "fifteen" to "15", "sixteen" to "16", "seventeen" to "17",
674+
"eighteen" to "18", "nineteen" to "19", "twenty" to "20", "thirty" to "30",
675+
"forty" to "40", "fifty" to "50", "sixty" to "60", "seventy" to "70",
676+
"eighty" to "80", "ninety" to "90",
677+
"a" to "1", "an" to "1", "half" to "0.5", "quarter" to "0.25"
678+
)
679+
680+
var result = text.lowercase()
681+
682+
// Handle compound numbers like "twenty five" -> "25"
683+
val compoundPattern = "\\b(twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)[\\s-]+(one|two|three|four|five|six|seven|eight|nine)\\b".toRegex()
684+
compoundPattern.findAll(result).forEach { match ->
685+
val tens = wordToDigit[match.groupValues[1]] ?: "0"
686+
val ones = wordToDigit[match.groupValues[2]] ?: "0"
687+
val sum = tens.toInt() + ones.toInt()
688+
result = result.replace(match.value, sum.toString())
689+
}
690+
691+
// Replace simple word numbers
692+
wordToDigit.forEach { (word, digit) ->
693+
result = result.replace("\\b$word\\b".toRegex(), digit)
694+
}
695+
696+
return result
697+
}
698+
664699
/**
665700
* Normalizes various time formats to 24-hour HH:MM format
666701
* Examples:
@@ -801,7 +836,9 @@ class SlotExtractor {
801836
* - "2:30:45" -> 9045 (seconds, HH:MM:SS)
802837
*/
803838
private fun normalizeTimerDuration(text: String, matchedValue: String): Int {
804-
val cleanText = text.lowercase()
839+
// Convert word numbers to digits first
840+
val cleanText = convertWordToNumber(text.lowercase())
841+
val cleanMatchedValue = convertWordToNumber(matchedValue.lowercase())
805842

806843
// Check for combined duration patterns first
807844

@@ -834,7 +871,7 @@ class SlotExtractor {
834871

835872
// HH:MM:SS format
836873
val hhMmSsPattern = "^(\\d+):(\\d+):(\\d+)$".toRegex()
837-
hhMmSsPattern.find(matchedValue.trim())?.let { match ->
874+
hhMmSsPattern.find(cleanMatchedValue.trim())?.let { match ->
838875
val hours = match.groupValues[1].toIntOrNull() ?: 0
839876
val minutes = match.groupValues[2].toIntOrNull() ?: 0
840877
val seconds = match.groupValues[3].toIntOrNull() ?: 0
@@ -843,14 +880,14 @@ class SlotExtractor {
843880

844881
// MM:SS format (assuming minutes:seconds for timer)
845882
val mmSsPattern = "^(\\d+):(\\d+)$".toRegex()
846-
mmSsPattern.find(matchedValue.trim())?.let { match ->
883+
mmSsPattern.find(cleanMatchedValue.trim())?.let { match ->
847884
val minutes = match.groupValues[1].toIntOrNull() ?: 0
848885
val seconds = match.groupValues[2].toIntOrNull() ?: 0
849886
return minutes * 60 + seconds
850887
}
851888

852889
// Check for single unit patterns
853-
val value = matchedValue.replace(Regex("[^\\d.]"), "").toDoubleOrNull() ?: return 0
890+
val value = cleanMatchedValue.replace(Regex("[^\\d.]"), "").toDoubleOrNull() ?: return 0
854891

855892
return when {
856893
cleanText.contains(Regex("\\b(?:hours?|hrs?|hr|h)\\b")) -> (value * 3600).toInt()
@@ -882,33 +919,33 @@ class SlotExtractor {
882919
"\\b(\\d{1,2}(?::\\d{2})?\\s*[ap](?:\\.?\\s*m(?:\\.?)?)?(?!\\w))\\b",
883920
"\\b(\\d{3,4}\\s*[ap](?:\\.?\\s*m(?:\\.?)?)?(?!\\w))\\b", // For "1030 pm" format
884921

885-
// Duration patterns for timers
886-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:hours?|hrs?|hr|h)\\b",
887-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:minutes?|mins?|min|m)\\b",
888-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:seconds?|secs?|sec|s)\\b",
922+
// Duration patterns for timers (with word number support)
923+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:hours?|hrs?|hr|h)\\b",
924+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:minutes?|mins?|min|m)\\b",
925+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:seconds?|secs?|sec|s)\\b",
889926

890927
// Combined time patterns
891928
"\\b(\\d+)\\s*(?:h|hr|hours?)\\s*(?:and\\s+)?(\\d+)\\s*(?:m|min|minutes?)\\b",
892929
"\\b(\\d+)\\s*(?:m|min|minutes?)\\s*(?:and\\s+)?(\\d+)\\s*(?:s|sec|seconds?)\\b",
893930
"\\b(\\d+):(\\d+):(\\d+)\\b", // HH:MM:SS format
894931
"\\b(\\d+):(\\d+)\\b", // MM:SS or HH:MM format
895932

896-
// Duration keywords
897-
"\\b(?:for|during|lasting|takes?)\\s+(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
933+
// Duration keywords (with word number support)
934+
"\\b(?:for|during|lasting|takes?)\\s+(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
898935

899-
// Timer-specific patterns
900-
"\\b(?:set|start|begin|run|timer|stopwatch)\\s*(?:for|to|at)?\\s*(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
936+
// Timer-specific patterns (with word number support)
937+
"\\b(?:set|start|begin|run|timer|stopwatch)\\s*(?:for|to|at)?\\s*(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
901938

902939
// Alarm-specific patterns - capture full time with AM/PM (prioritize space-separated)
903940
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{1,2}[\\s.]+?\\d{1,2}[\\.\\s]*[ap](?:\\.?\\s*m(?:\\.?)?)?)\\b",
904941
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{1,2}(?::\\d{2})?[\\.\\s]*[ap](?:\\.?\\s*m(?:\\.?)?)?)\\b",
905942
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{3,4}[\\.\\s]*[ap](?:\\.?\\s*m(?:\\.?)?)?)\\b",
906943

907-
// "In X time" patterns
908-
"\\b(?:in|after|within)\\s+(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
944+
// "In X time" patterns (with word number support)
945+
"\\b(?:in|after|within)\\s+(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
909946

910-
// Countdown patterns
911-
"\\b(?:countdown|count\\s+down)\\s*(?:from|for)?\\s*(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
947+
// Countdown patterns (with word number support)
948+
"\\b(?:countdown|count\\s+down)\\s*(?:from|for)?\\s*(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
912949

913950
// Direct time patterns for alarm setting - capture full time with AM/PM (prioritize space-separated)
914951
"\\b(?:set|create|make).*?(?:alarm|wake).*?(?:for|at)\\s*(\\d{1,2}[\\s.]+?\\d{1,2}[\\.\\s]*[ap](?:\\.?\\s*m(?:\\.?)?)?)\\b",

examples/whisper.swiftui/whisper.swiftui.demo/Intent/SlotExtractor.swift

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,49 @@ class SlotExtractor {
674674

675675
// MARK: - Time Normalization Helper
676676

677+
/// Converts word numbers to digits
678+
/// Examples: "three" -> "3", "fifteen" -> "15", "twenty five" -> "25"
679+
private func convertWordToNumber(_ text: String) -> String {
680+
let wordToDigit: [String: String] = [
681+
"zero": "0", "one": "1", "two": "2", "three": "3", "four": "4",
682+
"five": "5", "six": "6", "seven": "7", "eight": "8", "nine": "9",
683+
"ten": "10", "eleven": "11", "twelve": "12", "thirteen": "13",
684+
"fourteen": "14", "fifteen": "15", "sixteen": "16", "seventeen": "17",
685+
"eighteen": "18", "nineteen": "19", "twenty": "20", "thirty": "30",
686+
"forty": "40", "fifty": "50", "sixty": "60", "seventy": "70",
687+
"eighty": "80", "ninety": "90",
688+
"a": "1", "an": "1", "half": "0.5", "quarter": "0.25"
689+
]
690+
691+
var result = text.lowercased()
692+
693+
// Handle compound numbers like "twenty five" -> "25"
694+
let compoundPattern = "\\b(twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)[\\s-]+(one|two|three|four|five|six|seven|eight|nine)\\b"
695+
if let regex = try? NSRegularExpression(pattern: compoundPattern, options: [.caseInsensitive]) {
696+
let matches = regex.matches(in: result, range: NSRange(result.startIndex..., in: result))
697+
for match in matches.reversed() {
698+
if let tensRange = Range(match.range(at: 1), in: result),
699+
let onesRange = Range(match.range(at: 2), in: result),
700+
let fullRange = Range(match.range, in: result) {
701+
let tens = Int(wordToDigit[String(result[tensRange])] ?? "0") ?? 0
702+
let ones = Int(wordToDigit[String(result[onesRange])] ?? "0") ?? 0
703+
let sum = tens + ones
704+
result.replaceSubrange(fullRange, with: String(sum))
705+
}
706+
}
707+
}
708+
709+
// Replace simple word numbers
710+
for (word, digit) in wordToDigit {
711+
let pattern = "\\b\(word)\\b"
712+
if let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) {
713+
result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: digit)
714+
}
715+
}
716+
717+
return result
718+
}
719+
677720
/// Normalizes various time formats to 24-hour HH:MM format
678721
/// Examples:
679722
/// - "730" -> "07:30"
@@ -828,7 +871,9 @@ class SlotExtractor {
828871
/// - "1:30" -> 90 (seconds, interpreted as MM:SS)
829872
/// - "2:30:45" -> 9045 (seconds, HH:MM:SS)
830873
private func normalizeTimerDuration(_ text: String, matchedValue: String) -> Int {
831-
let cleanText = text.lowercased()
874+
// Convert word numbers to digits first
875+
let cleanText = convertWordToNumber(text.lowercased())
876+
let cleanMatchedValue = convertWordToNumber(matchedValue.lowercased())
832877

833878
// Check for combined duration patterns first
834879

@@ -871,30 +916,30 @@ class SlotExtractor {
871916

872917
// HH:MM:SS format
873918
if let regex = try? NSRegularExpression(pattern: "^(\\d+):(\\d+):(\\d+)$"),
874-
let match = regex.firstMatch(in: matchedValue.trimmingCharacters(in: .whitespaces), range: NSRange(matchedValue.startIndex..., in: matchedValue)) {
875-
if let hoursRange = Range(match.range(at: 1), in: matchedValue),
876-
let minutesRange = Range(match.range(at: 2), in: matchedValue),
877-
let secondsRange = Range(match.range(at: 3), in: matchedValue) {
878-
let hours = Int(matchedValue[hoursRange]) ?? 0
879-
let minutes = Int(matchedValue[minutesRange]) ?? 0
880-
let seconds = Int(matchedValue[secondsRange]) ?? 0
919+
let match = regex.firstMatch(in: cleanMatchedValue.trimmingCharacters(in: .whitespaces), range: NSRange(cleanMatchedValue.startIndex..., in: cleanMatchedValue)) {
920+
if let hoursRange = Range(match.range(at: 1), in: cleanMatchedValue),
921+
let minutesRange = Range(match.range(at: 2), in: cleanMatchedValue),
922+
let secondsRange = Range(match.range(at: 3), in: cleanMatchedValue) {
923+
let hours = Int(cleanMatchedValue[hoursRange]) ?? 0
924+
let minutes = Int(cleanMatchedValue[minutesRange]) ?? 0
925+
let seconds = Int(cleanMatchedValue[secondsRange]) ?? 0
881926
return hours * 3600 + minutes * 60 + seconds
882927
}
883928
}
884929

885930
// MM:SS format (assuming minutes:seconds for timer)
886931
if let regex = try? NSRegularExpression(pattern: "^(\\d+):(\\d+)$"),
887-
let match = regex.firstMatch(in: matchedValue.trimmingCharacters(in: .whitespaces), range: NSRange(matchedValue.startIndex..., in: matchedValue)) {
888-
if let minutesRange = Range(match.range(at: 1), in: matchedValue),
889-
let secondsRange = Range(match.range(at: 2), in: matchedValue) {
890-
let minutes = Int(matchedValue[minutesRange]) ?? 0
891-
let seconds = Int(matchedValue[secondsRange]) ?? 0
932+
let match = regex.firstMatch(in: cleanMatchedValue.trimmingCharacters(in: .whitespaces), range: NSRange(cleanMatchedValue.startIndex..., in: cleanMatchedValue)) {
933+
if let minutesRange = Range(match.range(at: 1), in: cleanMatchedValue),
934+
let secondsRange = Range(match.range(at: 2), in: cleanMatchedValue) {
935+
let minutes = Int(cleanMatchedValue[minutesRange]) ?? 0
936+
let seconds = Int(cleanMatchedValue[secondsRange]) ?? 0
892937
return minutes * 60 + seconds
893938
}
894939
}
895940

896941
// Check for single unit patterns
897-
let cleanedValue = matchedValue.replacingOccurrences(of: "[^\\d.]", with: "", options: .regularExpression)
942+
let cleanedValue = cleanMatchedValue.replacingOccurrences(of: "[^\\d.]", with: "", options: .regularExpression)
898943
guard let value = Double(cleanedValue) else { return 0 }
899944

900945
if cleanText.range(of: "\\b(?:hours?|hrs?|hr|h)\\b", options: .regularExpression) != nil {
@@ -978,10 +1023,10 @@ class SlotExtractor {
9781023
"\\b(\\d{1,2}(?::\\d{2})?[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b",
9791024
"\\b(\\d{3,4}[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b", // For "1030 pm", "630.pm" format
9801025

981-
// Duration patterns for timers
982-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:hours?|hrs?|hr|h)\\b",
983-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:minutes?|mins?|min|m)\\b",
984-
"\\b(\\d+(?:\\.\\d+)?)\\s*(?:seconds?|secs?|sec|s)\\b",
1026+
// Duration patterns for timers (with word number support)
1027+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:hours?|hrs?|hr|h)\\b",
1028+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:minutes?|mins?|min|m)\\b",
1029+
"\\b(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:seconds?|secs?|sec|s)\\b",
9851030

9861031
// Combined time patterns
9871032
"\\b(\\d+)\\s*(?:h|hr|hours?)\\s*(?:and\\s+)?(\\d+)\\s*(?:m|min|minutes?)\\b",
@@ -992,19 +1037,19 @@ class SlotExtractor {
9921037
// Duration keywords
9931038
"\\b(?:for|during|lasting|takes?)\\s+(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
9941039

995-
// Timer-specific patterns
996-
"\\b(?:set|start|begin|run|timer|stopwatch)\\s*(?:for|to|at)?\\s*(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
1040+
// Timer-specific patterns (with word number support)
1041+
"\\b(?:set|start|begin|run|timer|stopwatch)\\s*(?:for|to|at)?\\s*(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
9971042

9981043
// Alarm-specific patterns - capture full time with AM/PM (prioritize space-separated)
9991044
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{1,2}[\\s.]+?\\d{1,2}[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b",
10001045
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{1,2}(?::\\d{2})?[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b",
10011046
"\\b(?:alarm|wake|remind|alert)\\s*(?:at|for|in)?\\s*(\\d{3,4}[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b",
10021047

1003-
// "In X time" patterns
1004-
"\\b(?:in|after|within)\\s+(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
1048+
// "In X time" patterns (with word number support)
1049+
"\\b(?:in|after|within)\\s+(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
10051050

1006-
// Countdown patterns
1007-
"\\b(?:countdown|count\\s+down)\\s*(?:from|for)?\\s*(\\d+(?:\\.\\d+)?)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
1051+
// Countdown patterns (with word number support)
1052+
"\\b(?:countdown|count\\s+down)\\s*(?:from|for)?\\s*(\\d+(?:\\.\\d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter)\\s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?)\\b",
10081053

10091054
// Direct time patterns for alarm setting - capture full time with AM/PM (prioritize space-separated)
10101055
"\\b(?:set|create|make).*?(?:alarm|wake).*?(?:for|at)\\s*(\\d{1,2}[\\s.]+?\\d{1,2}[\\.\\s]*([ap])(?:\\.?\\s*m(?:\\.?)?)?)\\b",

0 commit comments

Comments
 (0)