mapping radio

This commit is contained in:
Jacob Dubin
2026-04-20 20:55:49 -05:00
parent a9118c142f
commit 32d63584d6
8 changed files with 258 additions and 3 deletions

View File

@@ -72,6 +72,8 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
{
"word_of_the_day" => false,
"word_of_the_day_guess" => false,
"radio" => false,
"radio_genre" => false,
_ => true
};
}

View File

@@ -29,6 +29,8 @@ public sealed class JiboInteractionService(
"dance" => BuildDanceDecision(catalog),
"time" => new JiboInteractionDecision("time", $"It is {DateTime.Now:h:mm tt}."),
"date" => new JiboInteractionDecision("date", $"Today is {DateTime.Now:dddd, MMMM d}."),
"radio" => BuildRadioLaunchDecision(),
"radio_genre" => BuildRadioGenreLaunchDecision(lowered),
"hello" => new JiboInteractionDecision("hello", randomizer.Choose(catalog.GreetingReplies)),
"how_are_you" => new JiboInteractionDecision("how_are_you", randomizer.Choose(catalog.HowAreYouReplies)),
"yes" => new JiboInteractionDecision("yes", "Yes."),
@@ -151,6 +153,16 @@ public sealed class JiboInteractionService(
return "joke";
}
if (TryResolveRadioGenre(loweredTranscript) is not null)
{
return "radio_genre";
}
if (MatchesAny(loweredTranscript, "open the radio", "play the radio", "turn on the radio", "radio"))
{
return "radio";
}
if (MatchesAny(loweredTranscript, "dance", "boogie"))
{
return "dance";
@@ -235,6 +247,33 @@ public sealed class JiboInteractionService(
});
}
private static JiboInteractionDecision BuildRadioLaunchDecision()
{
return new JiboInteractionDecision(
"radio",
"Opening the radio.",
"@be/radio",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "@be/radio"
});
}
private static JiboInteractionDecision BuildRadioGenreLaunchDecision(string loweredTranscript)
{
var station = TryResolveRadioGenre(loweredTranscript) ?? "Country";
return new JiboInteractionDecision(
"radio_genre",
$"Playing {FormatRadioGenreForSpeech(station)} on the radio.",
"@be/radio",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "@be/radio",
["station"] = station
});
}
private static JiboInteractionDecision BuildWordOfTheDayGuessDecision(
IReadOnlyDictionary<string, string> clientEntities,
string transcript,
@@ -417,6 +456,66 @@ public sealed class JiboInteractionService(
{
return candidates.Any(candidate => loweredTranscript.Contains(candidate, StringComparison.Ordinal));
}
private static string? TryResolveRadioGenre(string loweredTranscript)
{
foreach (var (phrase, station) in RadioGenreAliases)
{
if (loweredTranscript.Contains(phrase, StringComparison.Ordinal))
{
return station;
}
}
return null;
}
private static string FormatRadioGenreForSpeech(string station)
{
return station switch
{
"EightiesAndNinetiesHits" => "eighties and nineties hits",
"ChristianAndGospel" => "Christian and gospel",
"ClassicRock" => "classic rock",
"CollegeRadio" => "college radio",
"HipHop" => "hip hop",
"NewsAndTalk" => "news and talk",
"ReggaeAndIsland" => "reggae and island music",
"SoftRock" => "soft rock",
_ => station
};
}
private static readonly (string Phrase, string Station)[] RadioGenreAliases =
[
("country music", "Country"),
("country radio", "Country"),
("country", "Country"),
("classic rock", "ClassicRock"),
("soft rock", "SoftRock"),
("hip hop", "HipHop"),
("hip-hop", "HipHop"),
("news and talk", "NewsAndTalk"),
("news talk", "NewsAndTalk"),
("news radio", "NewsAndTalk"),
("sports radio", "Sports"),
("christian music", "ChristianAndGospel"),
("gospel music", "ChristianAndGospel"),
("oldies", "Oldies"),
("pop music", "Pop"),
("jazz", "Jazz"),
("latin music", "Latin"),
("dance music", "Dance"),
("reggae", "ReggaeAndIsland"),
("island music", "ReggaeAndIsland"),
("alternative", "Alternative"),
("blues", "Blues"),
("classical music", "Classical"),
("classical", "Classical"),
("college radio", "CollegeRadio"),
("comedy radio", "Comedy"),
("npr", "NPR")
];
}
public sealed record JiboInteractionDecision(

View File

@@ -23,9 +23,14 @@ public sealed class ResponsePlanToSocketMessagesMapper
string.Equals(plan.IntentName, "no", StringComparison.OrdinalIgnoreCase);
var isWordOfDayLaunch = string.Equals(plan.IntentName, "word_of_the_day", StringComparison.OrdinalIgnoreCase);
var isWordOfDayGuess = string.Equals(plan.IntentName, "word_of_the_day_guess", StringComparison.OrdinalIgnoreCase);
var isRadioLaunch = string.Equals(plan.IntentName, "radio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "radio_genre", StringComparison.OrdinalIgnoreCase);
var radioStation = ReadSkillPayloadString(skill, "station");
var nluGuess = ReadClientEntity(turn, "guess");
var wordOfDayGuess = ResolveWordOfDayGuess(turn, transcript, nluGuess);
var outboundIntent = isWordOfDayLaunch
? "menu"
: isRadioLaunch
? "menu"
: isWordOfDayGuess
? "guess"
@@ -36,6 +41,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
? wordOfDayGuess
: isWordOfDayLaunch
? string.Empty
: isRadioLaunch
? transcript
: string.Equals(clientIntent, "guess", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(nluGuess)
? nluGuess
: isYesNoTurn && isYesNoIntent
@@ -45,10 +52,12 @@ public sealed class ResponsePlanToSocketMessagesMapper
: transcript;
var outboundRules = isWordOfDayLaunch
? ["word-of-the-day/menu"]
: isRadioLaunch
? Array.Empty<string>()
: isWordOfDayGuess
? ["word-of-the-day/puzzle"]
: isYesNoTurn && isYesNoIntent ? [yesNoRule!] : rules;
var entities = ReadEntities(turn, messageType, isYesNoTurn && isYesNoIntent, isWordOfDayLaunch, isWordOfDayGuess, wordOfDayGuess);
var entities = ReadEntities(turn, messageType, isYesNoTurn && isYesNoIntent, isWordOfDayLaunch, isRadioLaunch, isWordOfDayGuess, wordOfDayGuess, radioStation);
var listenMessage = new
{
type = "LISTEN",
@@ -61,7 +70,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
final = true,
text = outboundAsrText
},
nlu = BuildNluPayload(outboundIntent, outboundRules, entities, isWordOfDayLaunch ? "@be/word-of-the-day" : null),
nlu = BuildNluPayload(outboundIntent, outboundRules, entities, isWordOfDayLaunch ? "@be/word-of-the-day" : isRadioLaunch ? "@be/radio" : null),
match = new
{
intent = outboundIntent,
@@ -100,6 +109,22 @@ public sealed class ResponsePlanToSocketMessagesMapper
DelayMs: 125));
}
if (isRadioLaunch)
{
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildSkillRedirectPayload(
transId,
"@be/radio",
outboundIntent,
outboundAsrText,
outboundRules,
entities)),
DelayMs: 75));
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/radio")),
DelayMs: 125));
}
if (emitSkillActions && speak is not null)
{
messages.Add(new SocketReplyPlan(
@@ -242,8 +267,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
string? messageType,
bool yesNoCreateTurn,
bool wordOfDayLaunch,
bool radioLaunch,
bool wordOfDayGuess,
string? guess)
string? guess,
string? radioStation)
{
if (yesNoCreateTurn)
{
@@ -261,6 +288,17 @@ public sealed class ResponsePlanToSocketMessagesMapper
};
}
if (radioLaunch)
{
var entities = new Dictionary<string, object?>();
if (!string.IsNullOrWhiteSpace(radioStation))
{
entities["station"] = radioStation;
}
return entities;
}
if (wordOfDayGuess)
{
return new Dictionary<string, object?>

View File

@@ -544,6 +544,8 @@ public sealed class WebSocketTurnFinalizationService(
: DateTimeOffset.UtcNow.Add(WebSocketTurnState.DefaultLateAudioIgnoreWindow);
var emitSkillActions = !string.Equals(plan.IntentName, "word_of_the_day", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "radio", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "radio_genre", StringComparison.OrdinalIgnoreCase) &&
(messageType != "CLIENT_NLU" ||
string.Equals(plan.IntentName, "word_of_the_day_guess", StringComparison.OrdinalIgnoreCase));
var replies = ResponsePlanToSocketMessagesMapper.Map(plan, finalizedTurn, session, emitSkillActions).Select(map => new WebSocketReply