site/model-registry/model-registry.mjs (1,134 lines of code) (raw):

// @ts-check import { create, exposeAsGlobal, formatBytes, getElement, isNever, jsonToYAML, parseSearchQuery, } from "../utils.mjs"; /** * @import { * Corpus, * TrainingRun, * ModelRun, * ModelReference, * ModelName * } from '../@types/training-run.d.ts' */ const BUCKET_NAME = "moz-fx-translations-data--303e-prod-translations-data"; const STORAGE_URL = `https://storage.googleapis.com/${BUCKET_NAME}`; /** * The elements for the page get selected here in a type-friendly manner. If the elements * aren't found, then there is a runtime error. */ const elements = { table: getElement("table", HTMLTableElement), tbody: getElement("table-body"), thead: getElement("table-thead"), tableContainer: getElement("table-container", HTMLDivElement), loading: getElement("loading"), error: getElement("error"), searchFilter: getElement("search-filter", HTMLInputElement), overlay: getElement("overlay"), overlayCloseButton: getElement("overlay-close-button"), overlayContent: getElement("overlay-content"), scrollContainer: getElement("scroll-container"), scores: getElement("scores"), }; /** * The URL-serialized state as inferred by: * @see {URLStateManager.prototype.getInitialState} * * @typedef {ReturnType<typeof URLStateManager.prototype.getInitialState>} State */ /** * This is a helper clss that manages the state of the view that is URL serialized * and pushed onto the history. */ class URLStateManager { /** * @type {State} */ state = this.getInitialState(); constructor() { addEventListener("popstate", (event) => { this.state = event.state.state; this.updateUI(); }); } /** * Initializes the current {@link State} from the URLParams. */ getInitialState() { const urlParams = new URLSearchParams(window.location.search); /** @type {ModelReference | null} */ let modelReference = null; { const name = urlParams.get("modelName"); const langpair = urlParams.get("modelLangpair"); const modelName = toModelName(urlParams.get("modelModelName")); if (name && langpair && modelName) { modelReference = { name, langpair, modelName, }; } } // The types for the State are inferred from this return: return { searchString: urlParams.get("searchString") ?? "", showModels: urlParams.get("showModels") == "true" ? true : false, showCorpora: urlParams.get("showCorpora") == "true" ? true : false, score: urlParams.get("score") || "vs-google", modelReference, }; } /** * Converts the {@link State} URLSearchParams. * @returns {URLSearchParams} */ stateToURLSearchParams() { const urlParams = new URLSearchParams(); urlParams.set("searchString", this.state.searchString); if (this.state.showModels) { urlParams.set("showModels", "true"); } if (this.state.showCorpora) { urlParams.set("showCorpora", "true"); } if (this.state.modelReference) { urlParams.set("modelName", this.state.modelReference.name); urlParams.set("modelLangpair", this.state.modelReference.langpair); urlParams.set("modelModelName", this.state.modelReference.modelName); } urlParams.set("score", this.state.score); return urlParams; } /** * Updates the state in place, but does not update the history or UI. * * @param {Partial<State>} partialState */ replaceState(partialState) { this.state = { ...this.state, ...partialState, }; } /** * Updates the state, URL history, and view. Use this for an atomic update that * should be serialize dto the view. * * @param {Partial<State>} partialState */ update(partialState) { this.replaceState(partialState); this.pushHistory(); this.updateUI(); } /** * Push the current state onto the history. */ pushHistory() { const urlParams = this.stateToURLSearchParams(); const url = new URL(window.location.href); const newLocation = `${url.origin}${url.pathname}?${urlParams}`; history.pushState(urlStateManager, "", newLocation); } /** * This is a reactive function that updates the UI based on state changes. It should * be quick to run. */ updateUI() { SearchFilter.onStateChange(this.state.searchString); ModelCardOverlay.onStateChange(this.state.modelReference); if (this.state.showModels) { elements.table.classList.add("show-models"); } else { elements.table.classList.remove("show-models"); } if (this.state.showCorpora) { elements.table.classList.add("show-corpora"); } else { elements.table.classList.remove("show-corpora"); } elements.searchFilter.value = this.state.searchString; const scoreRadio = elements.scores.querySelector( "#score-" + this.state.score ); if (scoreRadio) { scoreRadio.setAttribute("checked", ""); } document.body.dataset["score"] = this.state.score; } } /** * The state manager is statically initalized. */ const urlStateManager = new URLStateManager(); exposeAsGlobal("urlStateManager", urlStateManager); /** * These are also statically initialized. After they are initially set, the view * must be update. */ /** @type {TrainingRun[] | null} */ let trainingRuns = null; /** * The initialization function for the page. */ document.addEventListener("DOMContentLoaded", async () => { elements.table.querySelectorAll("th button").forEach((button, index) => { button.addEventListener("click", () => sortTable(index)); }); trainingRuns = await loadTrainingRuns(); exposeAsGlobal("trainingRuns", trainingRuns); SearchFilter.setupHandlers(); ModelCardOverlay.setupHandlers(); setupScoreHandlers(); urlStateManager.updateUI(); sortByDate(); elements.tableContainer.style.display = "block"; elements.loading.style.display = "none"; }); /** * Find the index of the data key, sort the table by it. */ function sortByDate() { const tr = elements.thead.querySelector("tr"); if (!tr) { throw new Error("Could not find the tr"); } for (let index = 0; index < tr.children.length; index++) { if (tr.children[index].getAttribute("data-key") === "date") { sortTable(index, -1); break; } } } class SearchFilter { /** * Sets up the event handlers. */ static setupHandlers() { elements.searchFilter.addEventListener("keyup", () => { SearchFilter.onStateChange(elements.searchFilter.value); }); function pushSearchFilter() { urlStateManager.replaceState({ searchString: elements.searchFilter.value, }); urlStateManager.pushHistory(); } elements.searchFilter.addEventListener("keyup", (event) => { if (event.key === "Enter") { pushSearchFilter(); } }); elements.searchFilter.addEventListener("blur", pushSearchFilter); } /** * Reactively handle state changes. This should be fast enough to be called many times * quickly. * * @param {string} search */ static onStateChange(search) { search = search.trim(); const trs = Array.from(elements.tbody.querySelectorAll("tr")); // Unhide everything. for (const tr of trs) { tr.style.display = "table-row"; } if (!search.trim()) { // Nothing to search. return; } const { filters, terms } = parseSearchQuery(search); // Filter terms for (const tr of elements.tbody.querySelectorAll("tr")) { const rowText = tr.innerText.toLowerCase(); for (const term of terms) { if (!rowText.includes(term)) { tr.style.display = "none"; break; } } } for (const filter of filters) { // Find the table header if (!filter.key.match(/^[a-z-]+$/)) { continue; } const ths = elements.thead.querySelectorAll("th"); let columnIndex = null; for (let i = 0; i < ths.length; i++) { if (ths[i].dataset.key === filter.key) { columnIndex = i; break; } } if (columnIndex === null) { continue; } for (const tr of trs) { const td = /** @type {HTMLElement} */ (tr.children[columnIndex]); const rowText = td.innerText.toLowerCase(); if (filter.negated) { if (rowText.includes(filter.value)) { tr.style.display = "none"; } } else if (!rowText.includes(filter.value)) { tr.style.display = "none"; } } } } } /** * This is the overlay for the model view. It takes a model reference, looks up the * training run and model run. Note that the training runs are expected to already be * loaded in. {@link ModelCardOverlay.onStateChange} can be called very cheaply to update * the view anytime the state changes. * * @param {ModelReference | null} modelReference */ class ModelCardOverlay { /** @type {TrainingRun} */ trainingRun; /** @type {ModelRun} */ modelRun; /** @type {ModelReference} */ modelReference; /** * @param {TrainingRun} trainingRun * @param {ModelRun} modelRun * @param {ModelReference} modelReference */ constructor(trainingRun, modelRun, modelReference) { this.trainingRun = trainingRun; this.modelRun = modelRun; this.modelReference = modelReference; } static setupHandlers() { function hideOverlay() { urlStateManager.update({ modelReference: null }); } elements.overlayCloseButton.addEventListener("click", hideOverlay); document.body.addEventListener("keyup", (event) => { if (event.key === "Escape") { hideOverlay(); } }); elements.overlay.addEventListener("click", (event) => { if (event.target === elements.overlay) { hideOverlay(); } }); } /** * Reactively handle a state change to the model reference. This function is fast * enough to be called reactively, and only initializes the code when it is needed. */ static onStateChange(modelReference) { if (!modelReference) { document.body.classList.remove("overlay-show"); elements.scrollContainer.removeAttribute("inert"); return null; } if (!trainingRuns) { // The training runs aren't available yet. return null; } if (document.body.classList.contains("overlay-show")) { // The model is already being shown. return null; } const { name, langpair, modelName } = modelReference; const trainingRun = trainingRuns.find( (trainingRun) => trainingRun.name === name && trainingRun.langpair == langpair ); if (!trainingRun) { elements.error.style.display = "block"; elements.error.innerText = `Could not find the model "${name}" (${langpair})`; return null; } const modelRun = trainingRun[modelName]; if (!modelRun) { elements.error.style.display = "block"; elements.error.innerText = `That model couldn't be found for "${name}" (${langpair})`; return null; } const overlay = new ModelCardOverlay(trainingRun, modelRun, modelReference); overlay.initialize(); } initialize() { // Clear out any old view. elements.overlayContent.innerText = ""; this.createHeaders(); const detailsUL = create.ul({ parent: elements.overlayContent, }); this.initModelDetails(detailsUL); this.initArtifacts(detailsUL); this.initTrainingContinuation(); this.initTrainingConfig(); // Show the overlay. elements.scrollContainer.setAttribute("inert", ""); document.body.classList.add("overlay-show"); } createHeaders() { const { name, langpair, modelName } = this.modelReference; create.h1({ children: `${name} (${langpair})`, parent: elements.overlayContent, }); create.h2({ children: modelNameToLabel(modelName), parent: elements.overlayContent, }); } /** * @param {HTMLElement} parent */ initModelDetails(parent) { const tbody = create.tbody(); const { task_group_id: taskGroupId, task_id: taskId } = this.modelRun; const { langpair, name } = this.trainingRun; create.li({ parent, children: [ "Model Details", create.table({ className: "details-table", children: [ create.thead({ children: [ create.tr({ children: [ create.th({ children: "Label" }), create.th({ children: "Details" }), ], }), ], }), tbody, ], }), ], }); /** * @param {string} label * @param {any} value */ const createRow = (label, value) => { create.tr({ parent: tbody, children: [ create.td({ children: label }), create.td({ children: value ? value : "-" }), ], }); }; createRow("Date", this.modelRun.date.slice(0, "2025-01-01".length)); createRow( "TaskGroup", create.a({ children: this.modelRun.task_group_id, href: `https://firefox-ci-tc.services.mozilla.com/tasks/groups/${taskGroupId}`, }) ); createRow( "Task", create.a({ children: this.modelRun.task_name, href: `https://firefox-ci-tc.services.mozilla.com/tasks/${taskId}`, }) ); // https://wandb.ai/moz-translations/cs-en/runs/teacher-1_ThgMJX?nw=nwuserepavlov // https://wandb.ai/moz-translations/cs-en/runs/teacher-1_LjL0bY const modelName = this.modelReference.modelName.replace("_", "-"); const idPart = this.modelRun.task_group_id.slice(0, 6); createRow( "W&B", create.ul({ children: [ create.li({ children: create.a({ children: "Model Run", href: `https://wandb.ai/moz-translations/${langpair}/runs/${modelName}_${idPart}`, }), }), create.li({ children: create.a({ children: `Task Group ${taskGroupId}`, href: `https://wandb.ai/moz-translations/${langpair}/groups/${name}_${taskGroupId}/workspace`, }), }), create.li({ children: [ create.a({ children: langpair, href: `https://wandb.ai/moz-translations/${langpair}/`, }), create.div({ className: "wandb-filter", children: [ "Group by ", create.span({ children: "Group" }), ", filter by ", create.span({ children: name }), ], }), create.div({ className: "wandb-filter", children: [ `Or filter by regex: `, create.span({ children: this.trainingRun.task_group_ids .map((t) => t.slice(0, 6)) .join("|"), }), ], }), ], }), ], }) ); } /** * @param {HTMLElement} parent */ initArtifacts(parent) { create.li({ parent, children: "Artifacts", }); { const artifactsUL = create.ul({ parent }); for (const url of this.modelRun.artifact_urls) { const urlParts = url.split("/"); const fileName = urlParts[urlParts.length - 1]; create.li({ parent: artifactsUL, children: create.a({ children: fileName, href: url }), }); } } } /** * @param {HTMLUListElement} detailsUL * @param {TrainingRun} trainingRun * @param {ModelRun} modelRun */ createEvaluationTable(detailsUL, trainingRun, modelRun) { const tbody = create.tbody(); create.li({ parent: detailsUL, children: [ "Flores Evaluation", create.table({ className: "details-table", children: [ create.thead({ children: [ create.tr({ children: [ create.th({ children: "Metric" }), create.th({ children: "Value" }), ], }), ], }), tbody, ], }), ], }); /** * @param {string} metric * @param {string} value */ const createMetricRow = (metric, value) => { create.tr({ parent: tbody, children: [ create.td({ children: metric }), create.td({ children: value ? value : "-" }), ], }); }; for (const metric of ["chrf", "bleu", "comet"]) { const value = modelRun.flores ? String(modelRun.flores[metric]) : "Not available"; createMetricRow(metric, value); } const googleFlores = getGoogleFloresCometScore(trainingRun, modelRun); if (googleFlores) { createMetricRow("comet (vs Google)", googleFlores.difference); createMetricRow("comet (Google)", googleFlores.score); } else { createMetricRow("Google Flores", "Not Available"); } } /** * Creates the section that allows you to copy and paste the part of the config * for training continuation. */ initTrainingContinuation() { const { name, langpair, modelName } = this.modelReference; // Only generate the header once, if it's required. let headerGenerated = false; /** * @param {string} text */ const createTrainingHeader = (text) => { if (!headerGenerated) { headerGenerated = true; create.h2({ parent: elements.overlayContent, children: "Training Continuation", }); create.p({ parent: elements.overlayContent, children: [ "Re-use this model in another training run. See the ", create.a({ children: "training continuation docs", href: "../docs/training/using-pretrained-models/", }), " for more information.", ], }); } create.h4({ parent: elements.overlayContent, children: text, }); }; switch (modelName) { case "backwards": createTrainingHeader("Back translation inference"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Use the ${langpair} model from the "${name}" training run for back translations.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-backwards:", " urls:", " - " + this.modelRun.artifact_folder, " mode: use", " type: default", "", ].join("\n"), }); break; case "teacher_1": case "teacher_2": createTrainingHeader("Teacher distillation inference"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Use the existing ${langpair} model from the "${name}" training run.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-teacher:", " urls:", " - " + this.modelRun.artifact_folder, " mode: use", " type: default", "", ].join("\n"), }); createTrainingHeader("Fine-tune the teacher"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Fine tune the ${langpair} model from the "${name}" training run.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-teacher:", " urls:", " - " + this.modelRun.artifact_folder, " mode: continue", " type: default", "", ].join("\n"), }); break; case "student": createTrainingHeader("Back translation inference"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Use the ${langpair} model from the "${name}" training run for back translations.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-backwards:", " urls:", " - " + this.modelRun.artifact_folder, " mode: use", " type: default", "", ].join("\n"), }); createTrainingHeader("Fine-tune the student"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Fine tune the ${langpair} model from the "${name}" training run.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-student:", " urls:", " - " + this.modelRun.artifact_folder, " mode: continue", " type: default", "", ].join("\n"), }); createTrainingHeader("Run evaluations and export"); create.pre({ parent: elements.overlayContent, children: [ "experiment:", " pretrained-models:", ` # Use the existing ${langpair} model from the "${name}" training run.`, " # See: https://mozilla.github.io/translations/docs/training/using-pretrained-models/", " train-student:", " urls:", " - " + this.modelRun.artifact_folder, " mode: use", " type: default", "", ].join("\n"), }); case "student_finetuned": case "student_quantized": case "student_exported": // These don't support training continuation. break; default: // Ensure every type of model is supported. isNever(modelName); } } initTrainingConfig() { create.h2({ parent: elements.overlayContent, children: "Training Config", }); create.pre({ parent: elements.overlayContent, children: jsonToYAML(this.modelRun.config), }); } } /** * Fetches JSON data from a given URL. * * @param {string} url * @returns {Promise<Object>} */ async function fetchJSON(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } return response.json(); } /** * Fetches and displays the training runs list. * @returns {Promise<TrainingRun[]>} */ async function loadTrainingRuns() { const trainingRunListing = await fetchJSON( `${STORAGE_URL}/models/listing.json` ); const promises = trainingRunListing.map(async (filename) => { /** @type {TrainingRun} */ const trainingRun = await fetchJSON(`${STORAGE_URL}/${filename}`); try { const row = new TrainingRunRow(trainingRun); row.build(); } catch (error) { elements.error.style.display = "block"; elements.error.innerText = "Error building training run row."; console.error(error); } return trainingRun; }); const results = await Promise.allSettled(promises); const rejected = results .filter(({ status }) => status == "rejected") // @ts-expect-error - Not sure why the allSettled disagrees. .map(({ reason }) => reason); const fulfilled = results .filter(({ status }) => status == "fulfilled") // @ts-expect-error - Not sure why the allSettled disagrees. .map(({ value }) => value); if (rejected.length) { console.error("Some fetches failed", rejected); } return fulfilled; } const displayName = new Intl.DisplayNames("en", { type: "language" }); /** * Everything needed to build a training run row. */ class TrainingRunRow { /** @type {TrainingRun} */ trainingRun; /** @type {HTMLTableRowElement} */ tr; /** * Construct the class with the required data. */ constructor(trainingRun) { this.trainingRun = trainingRun; this.tr = create.tr({ parent: elements.tbody }); } /** * Call all of the sub functions to build the parts of the row. These pieces are * broken out into separate methods to make the building process organized. */ build() { this.createInitialColumns(); this.createModelButtons(); this.createCorporaLinks(); } /** * Create the Name, Language, and Language Pair columns. */ createInitialColumns() { const trainingRun = this.trainingRun; const languageTag = trainingRun.source_lang === "en" ? trainingRun.target_lang : trainingRun.source_lang; this.createFilterableButton("name", trainingRun.name); this.createFilterableButton( "language", displayName.of(languageTag) ?? languageTag ); this.createFilterableButton("langpair", trainingRun.langpair); create.td({ parent: this.tr, children: (trainingRun.date_started ?? "–").slice(0, "2025-01-01".length), }); } /** * Creates a button that when clicked when apply the search filter * * @param {string} key * @param {string} value */ createFilterableButton(key, value) { create.td({ parent: this.tr, children: create.button({ className: "button-text", children: value, onClick() { elements.searchFilter.value = value.includes(" ") ? `${key}:"${value}"` : `${key}:${value}`; urlStateManager.update({ searchString: elements.searchFilter.value, }); }, }), }); } /** * Create a single link to a model, that when clicked will open * @param {ModelName} modelName */ createModelOverlayButton(modelName) { const div = document.createElement("div"); const trainingRun = this.trainingRun; const modelRun = trainingRun[modelName]; const googleFlores = getGoogleFloresCometScore(trainingRun, modelRun); const comet = modelRun?.flores?.comet; const bleu = modelRun?.flores?.bleu; /** @type {Partial<CSSStyleDeclaration>} */ const style = {}; let shippable = "Shippable"; if (googleFlores && googleFlores.percentage < -5) { // Does not meet release criteria. style.background = "#ffa537"; shippable = "Not shippable"; } const title = `${shippable} - COMET ${comet?.toFixed(2)} ` + `vs Google Comet ${googleFlores?.score} ` + `(${googleFlores?.difference})`; create.td({ parent: this.tr, className: "models-td", style, children: create.div({ children: !trainingRun[modelName] ? "–" : create.button({ parent: div, title, children: [ create.span({ className: "score-vs-google", children: googleFlores ? googleFlores.difference : "view", }), create.span({ className: "score-comet", children: comet?.toFixed(2) || "view", }), create.span({ className: "score-bleu", children: bleu?.toFixed(2) || "view", }), ], className: "button-text", onClick() { urlStateManager.update({ modelReference: { name: trainingRun.name, langpair: trainingRun.langpair, modelName, }, }); }, }), }), }); } /** * Create the show/hide button for the models, and the buttons that can open up * the model overlay. */ createModelButtons() { // Create the button to show models. create.td({ parent: this.tr, children: create.button({ onClick() { urlStateManager.update({ showModels: !urlStateManager.state.showModels, }); }, children: [ create.span({ className: "toggle-models-show", children: "Show", }), create.span({ className: "toggle-models-hide", children: "Hide", }), ], }), }); this.createModelOverlayButton("backwards"); this.createModelOverlayButton("teacher_1"); this.createModelOverlayButton("teacher_2"); this.createModelOverlayButton("student"); this.createModelOverlayButton("student_finetuned"); this.createModelOverlayButton("student_quantized"); this.createModelOverlayButton("student_exported"); } /** * Create a link to the source and target parts of a corpus. If there is no corpus * then a "-" is added instead. * * @param {Corpus} [corpus] */ createCorpusLink(corpus) { const div = document.createElement("div"); const { source_lang, target_lang } = this.trainingRun; create.td({ parent: this.tr, className: "corpus-td", children: create.div({ children: corpus ? [ create.a({ children: source_lang, href: corpus.source_url, title: formatBytes(corpus.source_bytes), parent: div, }), create.a({ children: target_lang, href: corpus.target_url, title: formatBytes(corpus.target_bytes), parent: div, }), ] : "–", }), }); } /** * Build the show/hide button that shows the corpora links. And build out all of * the links to the various corpora. */ createCorporaLinks() { const trainingRun = this.trainingRun; // Create the button to show corpora. create.td({ parent: this.tr, children: create.button({ onClick() { urlStateManager.update({ showCorpora: !urlStateManager.state.showCorpora, }); }, children: [ create.span({ className: "toggle-corpora-show", children: "Show", }), create.span({ className: "toggle-corpora-hide", children: "Hide", }), ], }), }); this.createCorpusLink(trainingRun.parallel_corpus_aligned); this.createCorpusLink(trainingRun.backtranslations_corpus_aligned); this.createCorpusLink(trainingRun.distillation_corpus_aligned); this.createCorpusLink(trainingRun.parallel_corpus); this.createCorpusLink(trainingRun.backtranslations_corpus); this.createCorpusLink(trainingRun.distillation_corpus); create.td({ parent: this.tr, children: create.button({ children: "log", title: "View this run in the console.log", onClick() { alert("View this run in the console.log"); console.log(trainingRun.name, trainingRun.langpair); console.log(trainingRun); }, }), }); } } let prevColumnIndex = -1; let prevDirection = 1; /** * Sort a table by a column. This quickly mutates the HTMLTableElement to reorder the * rows based on the TD's innerText property. * * @param {number} columnIndex */ function sortTable(columnIndex, defaultDirection = 1) { const rows = Array.from(elements.tbody.children); // Swap the direction on double clicks const direction = prevColumnIndex === columnIndex ? -prevDirection : defaultDirection; prevDirection = direction; prevColumnIndex = columnIndex; rows.sort((rowA, rowB) => { const valueA = rowA.querySelectorAll("td")[columnIndex].innerText; const valueB = rowB.querySelectorAll("td")[columnIndex].innerText; return String(valueA).localeCompare(String(valueB)) * direction; }); // Re-appending puts this row at the bottom rows.forEach((row) => elements.tbody.appendChild(row)); } /** * Refine a raw type to a proper {@link ModelName} or null. * * @param {string | null | undefined} modelName * @returns {ModelName | null} */ function toModelName(modelName) { switch (modelName) { case "backwards": case "teacher_1": case "teacher_2": case "student": case "student_finetuned": case "student_quantized": case "student_exported": return modelName; default: return null; } } /** * Get the human-readable label for a given {@link ModelName}. * * @param {ModelName} modelName */ function modelNameToLabel(modelName) { switch (modelName) { case "backwards": return "Backwards"; case "teacher_1": return "Teacher 1"; case "teacher_2": return "Teacher 2"; case "student": return "Student"; case "student_finetuned": return "Student Finetuned"; case "student_quantized": return "Student Quantized"; case "student_exported": return "Student Exported"; default: isNever(modelName); throw new Error("Could not convert model name to label: " + modelName); } } /** * The Google comparison requires a bit of computation. This is done in this helper class * to make it consistent. * * @param {TrainingRun} trainingRun * @param {ModelRun} [modelRun] */ function getGoogleFloresCometScore(trainingRun, modelRun) { const googleFlores = trainingRun.comet_flores_comparison.google; if (!googleFlores || !modelRun?.flores?.comet) { return null; } const percentage = 100 * (1 - googleFlores / (modelRun?.flores.comet / 100)); const sign = percentage >= 0 ? "+" : ""; return { percentage, difference: `${sign}${percentage.toFixed(2)}`, score: `${(googleFlores * 100).toFixed(2)}`, }; } function setupScoreHandlers() { for (const radio of elements.scores.querySelectorAll("input[type=radio]")) { radio.addEventListener("change", () => { urlStateManager.update({ score: getCheckedScore(), }); }); } } function getCheckedScore() { let id = ""; for (const input of elements.scores.querySelectorAll("input")) { if (input.checked) { id = input.id; } } return id.replace("score-", "") || "vs-google"; }