File size: 10,293 Bytes
9cee884 39181ba 9cee884 39181ba 9cee884 | 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 | # Bundle and import. The bundle is one zip file with the code, the setup of
# decisions, the answers and the references of a run. It holds no input image
# of the visitor and no key. Import reads a setup of this app back, with our
# own parser: it never calls decisions::read_setup() and never unserializes.
# The text of the README of a bundle (80% ASD-STE100)
bundle_readme <- function(spec, refs, name, has_data, date) {
files <- c(
sprintf("- `%s.R`: the R code of the run.", name),
sprintf(
"- `%s-setup.json`: the setup of the run. It holds the model, the question, the references and the image settings. It holds no key.",
name
),
sprintf("- `%s-answers.csv`: the answers of the run.", name),
if (nrow(refs) > 0) "- `references/`: the reference images of the run. The code reads them.",
if (has_data) "- `data.csv`: your texts. The code reads them."
)
run <- c(
"Put the files in one folder. Make this folder the working directory of R.",
if (spec$mode == "image" && spec$source != "textiles") {
'Put your images in the folder "images", as the code lists them. This bundle holds no input image.'
},
if (spec$mode == "text" && spec$source == "upload" && !has_data) {
'Save your file as "data.csv" in the same folder.'
},
sprintf("Then run `%s.R`.", name)
)
paste(c(
sprintf("# %s", name),
"",
"This zip file holds one run of the decisions Space.",
"",
"## Files",
"",
files,
"",
"## Versions",
"",
sprintf("- App: %s", APP_VERSION),
sprintf("- decisions: commit %s", DECISIONS_SHA),
sprintf("- Model: %s", MODEL),
sprintf("- Date: %s", format(date)),
"",
"## Install",
"",
"Install the package decisions in R:",
"",
' pak::pak("fmegahed/decisions")',
"",
"## Key",
"",
"The code needs your TypeSafe key. Save the key in the environment variable `TYPESAFE_API_KEY`.",
"For example, add the line `TYPESAFE_API_KEY=` followed by your key to the file `.Renviron` in your home folder. Then restart R.",
"Never write the key in the script.",
"",
"## Run",
"",
run,
"",
"## Cite",
"",
"If you use this run, cite the package and the Space:",
"",
sprintf("- Megahed, F. M., & Chen, Y.-J. (2026). *decisions: Typed decisions from decision models* (R package version %s). https://github.com/fmegahed/decisions", as.character(utils::packageVersion("decisions"))),
sprintf(
"- Megahed, F. M., & Chen, Y.-J. (2026). *decisions Space* (Version %s) [Web application]. Hugging Face. https://huggingface.co/spaces/fmegahed/decisions",
APP_VERSION
)
), collapse = "\n")
}
# Write the bundle of a run to `zipfile` and return its path. `table` is the
# answers table of answers_table(). `rows` are the rows of Try on 5 for the
# code (NULL for all inputs).
write_bundle <- function(spec, table, zipfile, date = Sys.Date(), rows = NULL) {
name <- run_name(spec)
refs <- reference_files(spec)
dir <- tempfile("bundle")
dir.create(dir)
on.exit(unlink(dir, recursive = TRUE))
readr::write_file(paste0(run_code(spec, date, rows), "\n"), file.path(dir, paste0(name, ".R")))
readr::write_file(answers_csv(table), file.path(dir, paste0(name, "-answers.csv")))
# The setup holds the references as the prepared images; the key stays out
references <- if (nrow(refs) > 0) spec$references[unique(refs$answer)]
setup <- decisions::decision_setup(
make_decider(), stats::setNames(list(as_decisions_question(spec$question)), name),
references = references, max_size = IMAGE_SETTINGS$max_size,
upscale = IMAGE_SETTINGS$upscale
)
decisions::write_setup(setup, file.path(dir, paste0(name, "-setup.json")))
# The references of a sample stay in the package (system.file())
if (nrow(refs) > 0 && spec$source != "textiles") {
dest <- file.path(dir, "references", refs$folder, refs$file)
purrr::walk(unique(dirname(dest)), dir.create, recursive = TRUE)
file.copy(refs$path, dest)
}
# Pasted text goes in as data.csv; the file of a CSV upload stays with the visitor
has_data <- spec$mode == "text" && spec$source == "upload" &&
is.null(spec$text_column) && nrow(spec$inputs) > 0
if (has_data) {
readr::write_csv(tibble::tibble(text = spec$inputs$text), file.path(dir, "data.csv"))
}
readr::write_file(
bundle_readme(spec, refs, name, has_data, date), file.path(dir, "README.md")
)
zipfile <- normalizePath(zipfile, mustWork = FALSE)
zip::zip(zipfile, files = list.files(dir), root = dir, mode = "mirror")
zipfile
}
# The files of the bundle of a spec, as write_bundle() makes them (a folder
# ends in "/"; test-bundle.R checks that the two agree)
bundle_names <- function(spec) {
name <- run_name(spec)
has_refs <- nrow(reference_files(spec)) > 0 && spec$source != "textiles"
has_data <- spec$mode == "text" && spec$source == "upload" &&
is.null(spec$text_column) && nrow(spec$inputs) > 0
c(
paste0(name, c(".R", "-setup.json", "-answers.csv")),
if (has_refs) "references/", if (has_data) "data.csv", "README.md"
)
}
# --- Import ---------------------------------------------------------------
IMPORT_REFUSED <- "This file was not made by this app."
# One text value: a string that is not NA
is_one_string <- function(x) is.character(x) && length(x) == 1 && !is.na(x)
# The path of the setup file: the file itself, or the `*-setup.json` of a zip
# file, unpacked below `dir`. NULL when there is none.
import_setup_path <- function(path, dir = tempfile("import")) {
if (!utils::file_test("-f", path) || file.size(path) > mb_bytes(MAX_ZIP_MB)) return(NULL)
if (tolower(tools::file_ext(path)) != "zip") return(path)
entries <- tryCatch(zip::zip_list(path), error = function(e) NULL)
if (is.null(entries)) return(NULL)
found <- entries |>
dplyr::filter(
endsWith(filename, "-setup.json"), purrr::map_lgl(filename, safe_entry),
!purrr::map_lgl(filename, is_junk), uncompressed_size <= mb_bytes(MAX_ZIP_MB)
)
if (nrow(found) == 0) return(NULL)
dest <- file.path(dir, "setup")
zip::unzip(path, files = found$filename[1], exdir = dest)
file.path(dest, found$filename[1])
}
# TRUE when the parsed JSON has the layout and the settings of this app
is_app_setup <- function(json) {
question <- if (length(json$questions) == 1) json$questions[[1]]
answers <- question$answers
settings <- json$image_settings
is.list(json) &&
identical(as.numeric(json$version), 1) &&
is_one_string(json$decider$model) &&
is.null(json$calibrator) && is.null(json$calibrator_hash) &&
is_one_string(question$type) && question$type %in% c("noul", "choice", "score") &&
is_one_string(question$instructions) &&
length(answers) > 0 && !is.null(names(answers)) &&
all(purrr::map_lgl(answers, \(x) is.null(x) || is_one_string(x))) &&
identical(as.numeric(settings$max_size), as.numeric(IMAGE_SETTINGS$max_size)) &&
identical(settings$upscale, IMAGE_SETTINGS$upscale) &&
identical(settings$crop, IMAGE_SETTINGS$crop)
}
# Decode the references of the file below `dir`, as
# references/<answer>/ref_<i>.jpg. NULL when there is a bad reference.
import_references <- function(json, answers, dir = tempfile("import")) {
encoded <- json$references
if (length(encoded) == 0) return(list(paths = NULL, problems = character()))
# The references are an object keyed by answer: names that are all answers
keys <- names(encoded)
if (!is.list(encoded) || is.null(keys) || anyNA(keys) || anyDuplicated(keys) > 0 ||
!all(keys %in% names(answers))) {
return(NULL)
}
folders <- answer_folders(names(answers))
paths <- tryCatch(
purrr::imap(encoded, \(images, answer) {
if (!is.list(images) || length(images) > MAX_REFERENCES) stop("bad references")
purrr::imap_chr(unname(images), \(x, i) {
bytes <- if (is_one_string(x)) jsonlite::base64_dec(x) else raw()
jpeg <- length(bytes) > 3 && all(bytes[1:3] == as.raw(c(0xff, 0xd8, 0xff)))
if (!jpeg || length(bytes) > mb_bytes(MAX_FILE_MB)) stop("bad reference")
path <- file.path(dir, "references", folders[[answer]], sprintf("ref_%s.jpg", i))
dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)
writeBin(bytes, path)
path
})
}),
error = function(e) NULL
)
if (is.null(paths)) return(NULL)
list(paths = paths, problems = character())
}
# Read a setup of this app: a bundle zip or the setup JSON. Returns the
# question (in the format of spec.R), the references (answer to file paths) and
# the problems. `question` is NULL when the file is refused. Every file of the
# import is written below `dir` (the server gives the folder of the session).
read_import_file <- function(path, dir = tempfile("import")) {
refuse <- function(problems = IMPORT_REFUSED) {
list(question = NULL, references = NULL, problems = problems)
}
setup_path <- import_setup_path(path, dir)
if (is.null(setup_path)) return(refuse())
json <- tryCatch(jsonlite::read_json(setup_path), error = function(e) NULL)
if (is.null(json) || !isTRUE(tryCatch(is_app_setup(json), error = function(e) FALSE))) {
return(refuse())
}
raw <- json$questions[[1]]
answers <- purrr::map_chr(raw$answers, \(x) x %||% "")
if (raw$type == "noul" && !setequal(names(answers), c("true", "false"))) return(refuse())
question <- list(type = raw$type, instructions = raw$instructions, answers = answers)
problems <- check_question(question)
if (length(problems) > 0) return(refuse(problems))
references <- import_references(json, answers, dir)
if (is.null(references) || (raw$type == "score" && length(references$paths) > 0)) return(refuse())
model <- json$decider$model
if (model != MODEL) {
problems <- c(problems, sprintf("This file uses %s. The app uses %s.", model, MODEL))
}
list(
question = question, references = references$paths,
problems = c(problems, references$problems)
)
}
# Read an import file. It never stops: any error refuses the file.
read_import <- function(path, dir = tempfile("import")) {
tryCatch(
read_import_file(path, dir),
error = function(e) list(question = NULL, references = NULL, problems = IMPORT_REFUSED)
)
}
|