New:Socket for Asana Is Now Available.Learn more
Get Started

typingnorm

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

typingnorm - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+123
metrics.js
/**
* 打字速度指標。每個語言有當地慣用的算法,呼叫端也可以切換成別的。
*
* 這是 Python 版 `typingnorm.metrics` 的對應實作,兩邊必須給出相同數字。
* repo 根的 vectors.json 有共用的測試案例,任一邊走鐘 CI 就會失敗。
*
* 分數永遠不該儲存,只從原始事件現算。存了分數,之後換算法就得重收資料。
* `authoritative` 為 false 的指標,介面上不得出現「合格」「專業級」這類字眼,
* 因為那個語言根本沒有可以合格的標準。
*/
/** 韓文字母。타수 是按字母鍵計數(한 = ㅎ+ㅏ+ㄴ 三打),只能從擊鍵流算。 */
const HANGUL_JAMO = /[ㄱ-ㅣ]/;
const chars = (s) => [...s];
const correct = (sample) => {
const a = chars(sample.prompt);
const b = chars(sample.typed);
return a.reduce((n, ch, i) => n + (ch === b[i] ? 1 : 0), 0);
};
const errors = (sample) => {
const a = chars(sample.prompt);
const b = chars(sample.typed);
const wrong = a.reduce(
(n, ch, i) => n + (b[i] !== undefined && ch !== b[i] ? 1 : 0),
0,
);
return wrong + Math.abs(a.length - b.length);
};
const minutes = (sample) => Math.max(sample.seconds, 1e-9) / 60;
const jamoKeys = (sample) => {
if (typeof sample.jamoKeys === "number") return sample.jamoKeys;
const ev = sample.events ?? [];
return ev.filter((e) => e[1] === "D" && e[3] && HANGUL_JAMO.test(e[3][0])).length;
};
export const METRICS = {
cpm_tqc: {
unit: "字/分", authoritative: true, note: "TQC 中文輸入,專業級 80",
// 淨字數,每錯一次扣 0.5 字;錯誤率達 10% 該次成績不予計算
fn: (s) =>
errors(s) / Math.max(chars(s.prompt).length, 1) >= 0.1
? 0
: Math.max(0, correct(s) - 0.5 * errors(s)) / minutes(s),
},
cpm_jp: {
unit: "字/分", authoritative: true, note: "全商速度部門,1 級 70",
fn: (s) => Math.max(0, correct(s) - errors(s)) / minutes(s),
},
tasu: {
unit: "타수", authoritative: true, note: "워드프로세서 1급 300",
fn: (s) => (jamoKeys(s) || s.keystrokes) / minutes(s),
},
kdph_ssc: {
unit: "KDPH", authoritative: true, note: "SSC 公職考試,10500 = 35 WPM",
fn: (s) => (s.keystrokes * 3600) / Math.max(s.seconds, 1e-9),
},
wpm_net_5: {
unit: "WPM", authoritative: false, note: "英文慣例,五字元一詞",
fn: (s) => (correct(s) / 5 - errors(s) / 5) / minutes(s),
},
wpm_4: {
unit: "คำ/นาที", authoritative: false, note: "泰文社群慣例,四字元一詞",
fn: (s) => (correct(s) / 4 - errors(s) / 4) / minutes(s),
},
kpm: {
unit: "打鍵/分", authoritative: false, note: "跨語言比較用的共同分母",
fn: (s) => s.keystrokes / minutes(s),
},
cpm_raw: {
unit: "字元/分", authoritative: false, note: "沒有當地標準時的中性預設",
fn: (s) => correct(s) / minutes(s),
},
};
export const METRIC_IDS = Object.keys(METRICS);
/** 語言的慣用指標。呼叫端可以覆寫成 METRICS 裡的任何一個。 */
export const DEFAULT_METRIC = {
"zh-TW": "cpm_tqc", "zh-HK": "cpm_tqc", "zh-CN": "cpm_raw",
ja: "cpm_jp", ko: "tasu", hi: "kdph_ssc",
en: "wpm_net_5", ar: "wpm_net_5", th: "wpm_4", vi: "kpm",
};
/**
* 算分。不指定 metric 就用該語言的慣用指標。
*
* @param {{prompt: string, typed: string, seconds: number, keystrokes: number,
* events?: Array, jamoKeys?: number}} sample
* @param {string} lang
* @param {string} [metric]
*/
export function score(sample, lang, metric) {
const id = metric ?? DEFAULT_METRIC[lang] ?? "cpm_raw";
const m = METRICS[id];
if (!m) throw new RangeError(`未知的指標 ${id}`);
return {
metric: id,
value: Math.round(m.fn(sample) * 10) / 10,
unit: m.unit,
authoritative: m.authoritative,
note: m.note,
};
}
/** 全部算一遍,給介面切換用。切換只是換一個已算好的值,不用重打。 */
export const allScores = (sample, lang) =>
METRIC_IDS.map((id) => score(sample, lang, id));
export const accuracy = (sample) =>
Math.max(0, 1 - errors(sample) / Math.max(chars(sample.prompt).length, 1));
/** TQC 的級別。只有 authoritative 的指標才給得出級別。 */
export function tqcGrade(cpm) {
if (cpm >= 80) return "專業級";
if (cpm >= 30) return "進階級";
if (cpm >= 15) return "實用級";
return null;
}
+33
-0

@@ -42,1 +42,34 @@ export type Verdict = "human-consistent" | "synthetic-signals" | "insufficient-data";

): Report;
export interface Sample {
prompt: string;
typed: string;
seconds: number;
keystrokes: number;
events?: KeyEvent[];
jamoKeys?: number;
}
export type MetricId =
| "cpm_tqc" | "cpm_jp" | "tasu" | "kdph_ssc"
| "wpm_net_5" | "wpm_4" | "kpm" | "cpm_raw";
export interface Score {
metric: MetricId;
value: number;
unit: string;
/** true 表示出自國家級認證機構,介面才可以顯示「合格」「專業級」 */
authoritative: boolean;
note: string;
}
export declare const METRICS: Record<MetricId, {
unit: string; authoritative: boolean; note: string;
fn: (s: Sample) => number;
}>;
export declare const METRIC_IDS: MetricId[];
export declare const DEFAULT_METRIC: Record<string, MetricId>;
export declare function score(sample: Sample, lang: string, metric?: MetricId): Score;
export declare function allScores(sample: Sample, lang: string): Score[];
export declare function accuracy(sample: Sample): number;
export declare function tqcGrade(cpm: number): string | null;
+4
-0

@@ -176,1 +176,5 @@ /**

}
export {
METRICS, METRIC_IDS, DEFAULT_METRIC, score, allScores, accuracy, tqcGrade,
} from "./metrics.js";
+6
-3
{
"name": "typingnorm",
"version": "0.1.0",
"description": "Tell whether keystrokes came from a human. Three rules, each with a false-positive rate measured on 2,245 real typists. Zero dependencies.",
"version": "0.2.0",
"description": "Tell whether keystrokes came from a human, and score typing speed with each locale's own standard. Zero dependencies.",
"type": "module",

@@ -17,2 +17,3 @@ "main": "index.js",

"index.js",
"metrics.js",
"index.d.ts",

@@ -38,3 +39,5 @@ "README.md",

"license": "MIT",
"engines": { "node": ">=18" }
"engines": {
"node": ">=18"
}
}