///|
fn decode_options(value : Json?) -> ScoreOptions raise ArpaError {
let mut options = ScoreOptions::default()
match value {
None => options
Some(json) => {
let fields = decode_object(json)
ensure_fields(fields, ["bos", "eos", "oov"])
let boolean = fn(value : Json) -> Bool raise ArpaError {
match value {
True => true
False => false
_ => raise Invalid("expected boolean option")
}
}
let bos = match fields.get("bos") {
Some(v) => boolean(v)
None => options.bos
}
let eos = match fields.get("eos") {
Some(v) => boolean(v)
None => options.eos
}
let oov = match fields.get("oov") {
None => options.oov
Some(String("map")) => MapToUnknown
Some(String("reject")) => Reject
_ => raise Invalid("oov must be map or reject")
}
options = { bos, eos, oov, }
options
}
}
}
///|
fn optional_double(
fields : Map[String, Json],
key : String,
fallback : Double,
) -> Double raise ArpaError {
match fields.get(key) {
Some(value) => decode_double(value)
None => fallback
}
}
///|
fn decode_hypotheses(
json : Json,
limits : Limits,
) -> Array[Hypothesis] raise ArpaError {
match json {
Array(items) => {
if items.length() > limits.max_records {
raise Budget("JSON hypothesis limit")
}
let result = []
for item in items {
let fields = decode_object(item)
ensure_fields(fields, ["id", "tokens", "acoustic_score"])
result.push({
id: decode_string(required(fields, "id")),
tokens: decode_tokens(required(fields, "tokens"), limits),
acoustic_score: decode_double(required(fields, "acoustic_score")),
})
}
result
}
_ => raise Invalid("expected hypotheses array")
}
}
///|
/// Portable, bounded JSON task engine. There is no filesystem or network access.
/// Unknown fields fail instead of silently changing a user's scoring options.
pub fn execute(
request : String,
limits~ : Limits = Limits::default(),
) -> Json raise ArpaError {
limits.validate()
if request.length() > limits.max_text {
raise Budget("request text limit")
}
check_json_depth(request)
let json = @json.parse(request) catch {
_ => raise Invalid("invalid JSON request")
}
let fields = decode_object(json)
let operation = decode_string(required(fields, "op"))
if operation == "train" {
ensure_fields(fields, [
"op", "sentences", "order", "method", "discount", "unigram_prior",
])
let sentences = decode_sentences(required(fields, "sentences"), limits)
let order = decode_int(required(fields, "order"))
let training_method = match fields.get("method") {
Some(v) => decode_string(v)
None => "discount"
}
let prior = optional_double(fields, "unigram_prior", 0.5)
let counts = count_corpus(sentences, order, limits~)
let model = match training_method {
"discount" =>
counts.train_discount(
discount=optional_double(fields, "discount", 0.75),
unigram_prior=prior,
)
"mle" => {
if fields.contains("discount") {
raise Invalid("discount option requires discount method")
}
counts.train_mle(unigram_prior=prior)
}
_ => raise Invalid("unknown training method")
}
return json_object([
("arpa", Json::string(model.to_arpa())),
("summary", model.summary().to_json()),
])
}
let model = parse(decode_string(required(fields, "arpa")), limits~)
match operation {
"inspect" => {
ensure_fields(fields, ["op", "arpa"])
model.summary().to_json()
}
"canonicalize" => {
ensure_fields(fields, ["op", "arpa"])
json_object([("arpa", Json::string(model.to_arpa()))])
}
"score" => {
ensure_fields(fields, ["op", "arpa", "tokens", "options"])
model
.sentence(
decode_tokens(required(fields, "tokens"), limits),
options=decode_options(fields.get("options")),
)
.to_json()
}
"corpus" => {
ensure_fields(fields, ["op", "arpa", "sentences", "options"])
model
.corpus(
decode_sentences(required(fields, "sentences"), limits),
options=decode_options(fields.get("options")),
)
.to_json()
}
"predict" => {
ensure_fields(fields, ["op", "arpa", "history", "k"])
let ranked = model.predict(
decode_tokens(required(fields, "history"), limits),
decode_int(required(fields, "k")),
)
Json::array(ranked.map(p => p.to_json()))
}
"rescore" => {
ensure_fields(fields, [
"op", "arpa", "hypotheses", "lm_weight", "acoustic_weight", "length_weight",
"options",
])
let hypotheses = decode_hypotheses(required(fields, "hypotheses"), limits)
let options : RescoreOptions = {
lm_weight: optional_double(fields, "lm_weight", 1.0),
acoustic_weight: optional_double(fields, "acoustic_weight", 1.0),
length_weight: optional_double(fields, "length_weight", 0.0),
scoring: decode_options(fields.get("options")),
}
Json::array(model.rescore(hypotheses, options~).map(r => r.to_json()))
}
"audit" => {
ensure_fields(fields, ["op", "arpa", "history", "tolerance"])
model
.audit_mass(
decode_tokens(required(fields, "history"), limits),
tolerance=optional_double(fields, "tolerance", 1.0e-8),
)
.to_json()
}
_ => raise Invalid("unknown operation")
}
}
///|
/// A stable envelope for CLI hosts. Errors contain no partial successful result.
pub fn handle(request : String, limits~ : Limits = Limits::default()) -> String {
let result = json_object([
("ok", Json::boolean(true)),
("result", execute(request, limits~)),
]) catch {
Invalid(message) =>
json_object([
("ok", Json::boolean(false)),
("error", Json::string("invalid: " + message)),
])
Budget(message) =>
json_object([
("ok", Json::boolean(false)),
("error", Json::string("budget: " + message)),
])
Missing(message) =>
json_object([
("ok", Json::boolean(false)),
("error", Json::string("missing: " + message)),
])
}
result.stringify()
}
///|
fn check_json_depth(text : String) -> Unit raise ArpaError {
let mut depth = 0
let mut quoted = false
let mut escaped = false
for ch in text {
if quoted {
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == '"' {
quoted = false
}
} else if ch == '"' {
quoted = true
} else if ch == '[' || ch == '{' {
depth += 1
if depth > 16 {
raise Budget("JSON nesting limit")
}
} else if ch == ']' || ch == '}' {
depth -= 1
if depth < 0 {
raise Invalid("unbalanced JSON delimiters")
}
}
}
}