From 1bf5a7bf93c3b3bf743a34e6f431652cc3f35950 Mon Sep 17 00:00:00 2001 From: Danny Gershman Date: Fri, 23 Feb 2024 08:00:50 -0500 Subject: [PATCH] Feature/pronunciation overrides #917 (#936) --- .gitignore | 2 +- RELEASENOTES.md | 1 + app/Http/Controllers/BotController.php | 3 +- app/Http/Controllers/CallFlowController.php | 16 +- app/Services/MeetingResultsService.php | 34 ++- app/Services/SettingsService.php | 1 + docs/docs/meeting-search/pronunciations.md | 14 + tests/Feature/AddressLookupTest.php | 34 ++- tests/Feature/MeetingSearchTest.php | 309 ++++++++++++-------- 9 files changed, 267 insertions(+), 147 deletions(-) create mode 100644 docs/docs/meeting-search/pronunciations.md diff --git a/.gitignore b/.gitignore index bdf1d7863..d0b8294e5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ docs/build test-report.xml tests/reports coverage.xml -test-results +.phpunit.cache diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 08eb5d11d..49bf9cb70 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### 4.3.0 (UNRELEASED) * Added feature for SPAD playback `spad_option`. +* Added the ability to refine the spoken pronunciation of locations. [#917] * Added the ability to delete groups. [#680] * Added phone number validation for volunteers. Default is for US phone numbers, but can be configured, review the documentation for options. [#704] * Added the ability to edit a single shift for a volunteer. [#824] diff --git a/app/Http/Controllers/BotController.php b/app/Http/Controllers/BotController.php index e942a556d..4576c214d 100644 --- a/app/Http/Controllers/BotController.php +++ b/app/Http/Controllers/BotController.php @@ -20,8 +20,7 @@ public function getMeetings(Request $request) $request->has('latitude') ? $request->get('latitude') : null, $request->has('longitude') ? $request->get('longitude') : null, $request->has('results_count') ? $request->get('results_count') : 5, - $request->has('today') ? $request->get('today') : null, - $request->has('tomorrow') ? $request->get('tomorrow') : null + $request->has('today') ? $request->get('today') : null ); return response()->json($meetings)->header("Content-Type", "application/json"); } diff --git a/app/Http/Controllers/CallFlowController.php b/app/Http/Controllers/CallFlowController.php index 46dc25051..96ec07c35 100644 --- a/app/Http/Controllers/CallFlowController.php +++ b/app/Http/Controllers/CallFlowController.php @@ -408,7 +408,7 @@ public function addresslookup(Request $request) $twiml->redirect(sprintf("input-method.php?Digits=%s&Retry=1", $request->get('SearchType'))) ->setMethod('GET'); } else { - $twiml->say(sprintf("%s %s", $this->settings->word('searching_meeting_information_for'), $coordinates->location)) + $twiml->say(sprintf("%s %s", $this->settings->word('searching_meeting_information_for'), $this->pronunciationReplacement($coordinates->location))) ->setVoice($this->settings->voice()) ->setLanguage($this->settings->get("language")); $twiml->redirect(sprintf( @@ -970,8 +970,7 @@ public function meetingSearch(Request $request) $latitude, $longitude, $results_count, - null, - null + $request->get("Timestamp") ); $results_count_num = count($meeting_results->filteredList) < $results_count ? count($meeting_results->filteredList) : $results_count; } catch (Exception $e) { @@ -1064,7 +1063,7 @@ public function meetingSearch(Request $request) for ($ll = 0; $ll < count($results['location']); $ll++) { $twiml->pause()->setLength(1); - $twiml->say($results['location'][$ll]) + $twiml->say($this->pronunciationReplacement($results['location'][$ll])) ->setVoice($this->settings->voice()) ->setLanguage($this->settings->get("language")); } @@ -1276,4 +1275,13 @@ public function geocodeAddress(mixed $address, string $sms_helpline_keyword = "" } return $coordinates; } + + private function pronunciationReplacement($subject): string + { + foreach ($this->settings->get("pronunciations") as $item) { + return str_replace($item['source'], $item['target'], $subject); + } + + return $subject; + } } diff --git a/app/Services/MeetingResultsService.php b/app/Services/MeetingResultsService.php index e2ec18a64..9915e547f 100644 --- a/app/Services/MeetingResultsService.php +++ b/app/Services/MeetingResultsService.php @@ -26,23 +26,27 @@ public function __construct( $this->config = $config; } - public function getMeetings($latitude, $longitude, $results_count, $today = null, $tomorrow = null) + public function getMeetings($latitude, $longitude, $results_count, $today = null) { + $tomorrow = null; + $anchored = true; if ($latitude != null & $longitude != null) { $this->setTimeZoneForLatitudeAndLongitude($latitude, $longitude); - $graced_date_time = (new DateTime())->modify(sprintf("-%s minutes", $this->settings->get('grace_minutes'))); if ($today == null) { - $today = $graced_date_time->format("w") + 1; - } - if ($tomorrow == null) { - $tomorrow = $graced_date_time->modify("+24 hours")->format("w") + 1; + $today = (new DateTime())->modify(sprintf("-%s minutes", $this->settings->get('grace_minutes'))); + } else { + $anchored = false; + $today = new DateTime($today); } + + $tomorrow = clone $today; + $tomorrow->modify("+24 hours"); } $meeting_results = new MeetingResults(); - $meeting_results = $this->meetingSearch($meeting_results, $latitude, $longitude, $today); + $meeting_results = $this->meetingSearch($meeting_results, $latitude, $longitude, $today, $anchored); if (count($meeting_results->filteredList) < $results_count) { - $meeting_results = $this->meetingSearch($meeting_results, $latitude, $longitude, $tomorrow); + $meeting_results = $this->meetingSearch($meeting_results, $latitude, $longitude, $tomorrow, $anchored); } if ($meeting_results->originalListCount > 0) { @@ -52,11 +56,11 @@ public function getMeetings($latitude, $longitude, $results_count, $today = null $meeting_results->filteredList[0]->longitude ); - $today = (new DateTime())->format("w") + 1; + $today = new DateTime(); } $sort_day_start = $this->settings->get('meeting_result_sort') == MeetingResultSort::TODAY - ? $today : $this->settings->get('meeting_result_sort'); + ? ($today->format("w") + 1) : $this->settings->get('meeting_result_sort'); $days = array_column($meeting_results->filteredList, 'weekday_tinyint'); $today_str = strval($sort_day_start); @@ -69,8 +73,14 @@ public function getMeetings($latitude, $longitude, $results_count, $today = null return $meeting_results; } - public function meetingSearch($meeting_results, $latitude, $longitude, $day) + public function meetingSearch($meeting_results, $latitude, $longitude, $timestamp, $anchored = true) { + if ($timestamp != null) { + $day = $timestamp->format("w") + 1; + } else { + $day = null; + } + $search_results = $this->rootServer->searchForMeetings($latitude, $longitude, $day); if (is_array($search_results->meetings) || $search_results->meetings instanceof Countable) { $meeting_results->originalListCount += count($search_results->meetings); @@ -87,7 +97,7 @@ public function meetingSearch($meeting_results, $latitude, $longitude, $day) continue; } - if (strpos($this->settings->get('custom_query'), "{DAY}")) { + if ($anchored && strpos($this->settings->get('custom_query'), "{DAY}")) { if (!$this->isItPastTime( $search_results->meetings[$i]->weekday_tinyint, $search_results->meetings[$i]->start_time diff --git a/app/Services/SettingsService.php b/app/Services/SettingsService.php index 71bf7f59f..4357f26a3 100644 --- a/app/Services/SettingsService.php +++ b/app/Services/SettingsService.php @@ -55,6 +55,7 @@ class SettingsService 'postal_code_length' => ['description' => '/general/postal-code-lengths' , 'default' => 5, 'overridable' => true, 'hidden' => false], 'province_lookup' => ['description' => '/general/stateprovince-lookup' , 'default' => false, 'overridable' => true, 'hidden' => false], 'province_lookup_list' => ['description' => '/general/stateprovince-lookup' , 'default' => [], 'overridable' => true, 'hidden' => false], + 'pronunciations' => ['descriptions' => '/meeting-search/pronunciations', 'default' => [], 'overridable' => true, 'hidden' => false], 'result_count_max' => ['description' => '/meeting-search/results-counts-maximums' , 'default' => 5, 'overridable' => true, 'hidden' => false], 'say_links' => ['description' => '/meeting-search/say-links', 'default' => false, 'overridable' => true, 'hidden' => false], 'service_body_id' => ['description' => '', 'default' => null, 'overridable' => true, 'hidden' => false], diff --git a/docs/docs/meeting-search/pronunciations.md b/docs/docs/meeting-search/pronunciations.md new file mode 100644 index 000000000..0910ca4fe --- /dev/null +++ b/docs/docs/meeting-search/pronunciations.md @@ -0,0 +1,14 @@ +# Pronunciations + +--- + +Sometimes the speech result of meeting locations may not be accurate based off the voice's ability to say it. You can override this by specifying the original word (`source`) and the new word (`target`) + +Example: + +```php +static $pronunciations = [[ + "source"=>"Yakima", + "target"=>"UkEEma" +]]; +``` diff --git a/tests/Feature/AddressLookupTest.php b/tests/Feature/AddressLookupTest.php index abb4b87e1..1ddf0c4cc 100644 --- a/tests/Feature/AddressLookupTest.php +++ b/tests/Feature/AddressLookupTest.php @@ -1,4 +1,7 @@ call( $method, '/address-lookup.php', @@ -31,7 +34,7 @@ ], false); })->with(['GET', 'POST']); -test('search by zip code for someone to talk to with speech text result with google api key', function ($method) { +test('search by zip code for meeting information with speech text result with google api key', function ($method) { $response = $this->call( $method, '/address-lookup.php', @@ -51,3 +54,30 @@ '' ], false); })->with(['GET', 'POST']); + +test('search by zip code for meeting information with speech text result and pronunciation override with google api key', function ($method) { + $settingsService = new SettingsService(); + $settingsService->set('pronunciations', [[ + "source"=>"Yakima", + "target"=>"UkEEma" + ]]); + app()->instance(SettingsService::class, $settingsService); + $response = $this->call( + $method, + '/address-lookup.php', + [ + "Digits" => "98901", + "SearchType" => "2", + ] + ); + $response + ->assertStatus(200) + ->assertHeader("Content-Type", "text/xml; charset=UTF-8") + ->assertSeeInOrder([ + '', + '', + 'searching meeting information for UkEEma, WA 98901, USA', + 'meeting-search.php?Latitude=46.6140435&Longitude=-120.4454218', + '' + ], false); +})->with(['GET', 'POST']); diff --git a/tests/Feature/MeetingSearchTest.php b/tests/Feature/MeetingSearchTest.php index 4e6fcaeab..2db650f39 100644 --- a/tests/Feature/MeetingSearchTest.php +++ b/tests/Feature/MeetingSearchTest.php @@ -134,7 +134,7 @@ $response ->assertStatus(200) ->assertHeader("Content-Type", "text/xml; charset=UTF-8") - ->assertSee([ + ->assertSeeInOrder([ '', '', '5 meetings have been texted to you', @@ -179,55 +179,54 @@ 'Longitude' => $this->longitude, 'To' => $this->to, 'From' => $this->from, + 'Timestamp' => '2024-02-19 00:00:00' ]); $response ->assertStatus(200) ->assertHeader("Content-Type", "text/xml; charset=UTF-8") - ->assertDontSee("post-call-action.php") - ->assertSee([ + ->assertSeeInOrder([ '', '', - 'meeting information found, listing the top 5 results', - '', + 'meeting information found, listing the top 5 results', + '', 'number 1', - '', - '', - '', - '', - '', - '', + 'Step up and Be Free', + '', + 'starts at Monday 7:00 PM', + '', + '128 Main street, Clifton Springs, NY', + '', 'number 2', - '', - '', - '', - '', - '', - '', + 'A New Way of Life', + '', + 'starts at Monday 6:30 PM', + '', + '27 West Genesee Street, Clyde, NY', + '', 'number 3', - '', - '', - '', - '', - '', - '', + 'Ties That Bind Us Together', + '', + 'starts at Monday 6:30 PM', + '', + '99 South St, Auburn, NY', + '', 'number 4', - '', - '', - '', - '', - '', - '', + 'Courage to Change', + '', + 'starts at Monday 10:15 AM', + '', + '12 South Street, Auburn, NY', + '', 'number 5', - '', - '', - '', - '', - '', - '', + 'Eye of the Hurricane', + '', + 'starts at Monday 7:30 PM', + '', + '1008 Main St., East Rochester, NY', '', 'thank you for calling, goodbye', - '' + '', ], false); })->with(['GET', 'POST']); @@ -260,37 +259,38 @@ 'Longitude' => $this->longitude, 'To' => $this->to, 'From' => $this->from, + 'Timestamp' => '2024-02-19 00:00:00' ]); $response ->assertStatus(200) ->assertHeader("Content-Type", "text/xml; charset=UTF-8") ->assertDontSee("post-call-action.php") - ->assertSee([ + ->assertSeeInOrder([ '', '', - 'meeting information found, listing the top 3 results', - '', + 'meeting information found, listing the top 3 results', + 'Meeting search results will also be sent to you by SMS text message.', + '', 'number 1', - '', - '', - '', - '', - '', - '', + 'Step up and Be Free', + '', + 'starts at Monday 7:00 PM', + '', + '128 Main street, Clifton Springs, NY', + '', 'number 2', - '', - '', - '', - '', - '', - '', + 'A New Way of Life', + '', + 'starts at Monday 6:30 PM', + '', + '27 West Genesee Street, Clyde, NY', + '', 'number 3', - '', - '', - '', - '', - '', - '', + 'Ties That Bind Us Together', + '', + 'starts at Monday 6:30 PM', + '', + '99 South St, Auburn, NY', '', 'thank you for calling, goodbye', '' @@ -312,7 +312,8 @@ $response = $this->call($method, '/meeting-search.php', [ 'Latitude' => $this->latitude, - 'Longitude' => $this->longitude + 'Longitude' => $this->longitude, + 'Timestamp' => '2024-02-19 00:00:00' ]); $response ->assertStatus(200) @@ -321,48 +322,48 @@ ->assertSee([ '', '', - 'meeting information found, listing the top 5 results', - '', + 'meeting information found, listing the top 5 results', + '', 'number 1', - '', - '', - '', - '', - '', - '', + 'Step up and Be Free', + '', + 'starts at Monday 7:00 PM', + '', + '128 Main street, Clifton Springs, NY', + '', 'number 2', - '', - '', - '', - '', - '', - '', + 'A New Way of Life', + '', + 'starts at Monday 6:30 PM', + '', + '27 West Genesee Street, Clyde, NY', + '', 'number 3', - '', - '', - '', - '', - '', - '', + 'Ties That Bind Us Together', + '', + 'starts at Monday 6:30 PM', + '', + '99 South St, Auburn, NY', + '', 'number 4', - '', - '', - '', - '', - '', - '', + 'Courage to Change', + '', + 'starts at Monday 10:15 AM', + '', + '12 South Street, Auburn, NY', + '', 'number 5', - '', - '', - '', - '', - '', - '', + 'Eye of the Hurricane', + '', + 'starts at Monday 7:30 PM', + '', + '1008 Main St., East Rochester, NY', '', - 'press one if you would like these results to be texted to you.', + 'press one if you would like these results to be texted to you.', + '', 'thank you for calling, goodbye', - '' + '', ], false); })->with(['GET', 'POST']); @@ -394,50 +395,106 @@ 'Longitude' => $this->longitude, 'To' => $this->to, 'From' => $this->from, + 'Timestamp' => '2024-02-19 00:00:00' ]); $response ->assertStatus(200) ->assertHeader("Content-Type", "text/xml; charset=UTF-8") - ->assertSee([ + ->assertSeeInOrder([ '', '', - 'meeting information found, listing the top 5 results', - '', + 'meeting information found, listing the top 5 results', + '', 'number 1', - '', - '', - '', - '', - '', - '', + 'Step up and Be Free', + '', + 'starts at Monday 7:00 PM', + '', + '128 Main street, Clifton Springs, NY', + '', 'number 2', - '', - '', - '', - '', - '', - '', + 'A New Way of Life', + '', + 'starts at Monday 6:30 PM', + '', + '27 West Genesee Street, Clyde, NY', + '', 'number 3', - '', - '', - '', - '', - '', - '', + 'Ties That Bind Us Together', + '', + 'starts at Monday 6:30 PM', + '', + '99 South St, Auburn, NY', + '', 'number 4', - '', - '', - '', - '', - '', - '', + 'Courage to Change', + '', + 'starts at Monday 10:15 AM', + '', + '12 South Street, Auburn, NY', + '', 'number 5', - '', - '', - '', - '', - '', - '', + 'Eye of the Hurricane', + '', + 'starts at Monday 7:30 PM', + '', + '1008 Main St., East Rochester, NY', + '', + 'thank you for calling, goodbye', + '', + ], false); +})->with(['GET', 'POST']); + +test('meeting search with valid latitude and longitude with pronunciation override', function ($method) { + $settingsService = new SettingsService(); + $settingsService->set('result_count_max', 3); + $settingsService->set('pronunciations', [[ + "source"=>"Auburn", + "target"=>"Ohhh-it-burns" + ]]); + app()->instance(SettingsService::class, $settingsService); + $timezone = new Timezone('OK', 0, -18000, 'America/New_York', 'Eastern Standard Time'); + $timezoneService = mock(TimeZoneService::class)->makePartial(); + $timezoneService->shouldReceive('getTimeZoneForCoordinates') + ->withArgs([$this->latitude, $this->longitude]) + ->once() + ->andReturn($timezone); + app()->instance(TimeZoneService::class, $timezoneService); + + $response = $this->call($method, '/meeting-search.php', [ + 'Latitude' => $this->latitude, + 'Longitude' => $this->longitude, + 'Timestamp' => '2024-02-19 00:00:00' + ]); + $response + ->assertStatus(200) + ->assertHeader("Content-Type", "text/xml; charset=UTF-8") + ->assertSeeInOrder([ + '', + '', + 'meeting information found, listing the top 3 results', + 'Meeting search results will also be sent to you by SMS text message.', + '', + 'number 1', + 'Step up and Be Free', + '', + 'starts at Monday 7:00 PM', + '', + '128 Main street, Clifton Springs, NY', + '', + 'number 2', + 'A New Way of Life', + '', + 'starts at Monday 6:30 PM', + '', + '27 West Genesee Street, Clyde, NY', + '', + 'number 3', + 'Ties That Bind Us Together', + '', + 'starts at Monday 6:30 PM', + '', + '99 South St, Ohhh-it-burns, NY', '', 'thank you for calling, goodbye', ''