Readable names: CamelCase product names and words with numbers stay readable
The production dry run of releases:readable flagged 5.37M of 19.8M rows, and its first sample held real names: "CrossStitcher - Issue 440", "SchiffsModell", "FastFilms 2.04", "BestCrypt NP v4.0", "SmithsWork", "Laura15Juli2026". Two CamelCase words with few vowels have four runs and no lone letter: a scramble now needs five runs, a lone letter or an inner capital run besides its few vowels; the consonant-run clause is gone (it counted y); a token whose letter runs are all words or acronyms with numbers between them is neutral. On a 49k-row production sample the flagged share went from 27.4% to 26.4% with the letters-only flags all scrambles; the evaluation sets are unchanged (LOG.md (129)). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
44c0c73b05
commit
3612bf3e43
3 files changed
+38
-6
No files matched your search
+14
-3
@@ -1115,7 +1115,7 @@ fn token_signals(t: &[u8]) -> TokenSignals {
|
|||||||
let mut lower = 0;
|
let mut lower = 0;
|
||||||
for i in 0..t.len() {
|
for i in 0..t.len() {
|
||||||
let c = t[i];
|
let c = t[i];
|
||||||
if c.is_ascii_alphabetic() && !is_vowel(c) {
|
if c.is_ascii_alphabetic() && !is_vowel(c) && c.to_ascii_lowercase() != b'y' {
|
||||||
consonants += 1;
|
consonants += 1;
|
||||||
s.max_consonants = s.max_consonants.max(consonants);
|
s.max_consonants = s.max_consonants.max(consonants);
|
||||||
} else {
|
} else {
|
||||||
@@ -1181,7 +1181,9 @@ pub fn token_kind(t: &[u8]) -> TokenKind {
|
|||||||
return if n >= 3 { TokenKind::Word } else { TokenKind::Neutral };
|
return if n >= 3 { TokenKind::Word } else { TokenKind::Neutral };
|
||||||
}
|
}
|
||||||
// Mixed case beyond Capitalization: CamelCase (neutral) or a scramble, which has few vowels and short runs.
|
// Mixed case beyond Capitalization: CamelCase (neutral) or a scramble, which has few vowels and short runs.
|
||||||
if n >= 6 && s.case_switches >= 2 && (s.inner_upper || (s.vowels * 4 < n && (s.runs >= 4 || s.lone >= 1))) {
|
// Two CamelCase words with few vowels (FastFilms, BestCrypt, SchiffsModell) have four runs and no lone
|
||||||
|
// letter; a scramble has more runs, a lone letter or an inner capital run.
|
||||||
|
if n >= 6 && s.case_switches >= 2 && s.vowels * 4 < n && (s.runs >= 5 || s.lone >= 1 || s.inner_upper) {
|
||||||
return TokenKind::Random;
|
return TokenKind::Random;
|
||||||
}
|
}
|
||||||
return TokenKind::Neutral;
|
return TokenKind::Neutral;
|
||||||
@@ -1193,6 +1195,14 @@ pub fn token_kind(t: &[u8]) -> TokenKind {
|
|||||||
// 51294P1CUKR2R35X; COD4MW2REMASTERED (4 switches) and BEATLES1964REMASTER (2) stay.
|
// 51294P1CUKR2R35X; COD4MW2REMASTERED (4 switches) and BEATLES1964REMASTER (2) stay.
|
||||||
return if n >= 12 && token_signals(t).switches >= 6 { TokenKind::Random } else { TokenKind::Neutral };
|
return if n >= 12 && token_signals(t).switches >= 6 { TokenKind::Random } else { TokenKind::Neutral };
|
||||||
}
|
}
|
||||||
|
// Words with numbers between them (Laura15Juli2026, Back4WinXP): every letter run a word or an acronym.
|
||||||
|
if t.split(|c: &u8| !c.is_ascii_alphabetic()).filter(|w| !w.is_empty()).all(|w| {
|
||||||
|
let vowels = w.iter().filter(|c| matches!(c.to_ascii_lowercase(), b'a' | b'e' | b'i' | b'o' | b'u' | b'y')).count();
|
||||||
|
(w.len() >= 2 && w.iter().all(u8::is_ascii_uppercase))
|
||||||
|
|| (w.len() >= 2 && w[1..].iter().all(u8::is_ascii_lowercase) && (w.len() >= 3 || w[0].is_ascii_lowercase()) && vowels * 5 >= w.len())
|
||||||
|
}) {
|
||||||
|
return TokenKind::Neutral;
|
||||||
|
}
|
||||||
let s = token_signals(&scene_case(t));
|
let s = token_signals(&scene_case(t));
|
||||||
if n >= 5 && s.switches >= 3 {
|
if n >= 5 && s.switches >= 3 {
|
||||||
return TokenKind::Random;
|
return TokenKind::Random;
|
||||||
@@ -1854,7 +1864,8 @@ mod tests {
|
|||||||
"Rocketship-Hurt_Feelings-WEB-2026-PreMaxXxing", "Drum-and-BassDJ_Samurai-Sound_With_Fire_Artemis-(FQY026)-WEB-2006-PRESCRiPTiON",
|
"Rocketship-Hurt_Feelings-WEB-2026-PreMaxXxing", "Drum-and-BassDJ_Samurai-Sound_With_Fire_Artemis-(FQY026)-WEB-2006-PRESCRiPTiON",
|
||||||
"Kleverig-H4rd-N12uk3b18-luc16037-720p-smpwr", "Eclipse_IDE_2023-12_R_win32_x86_64.zip", "PartsHPGL 1.06c", "Go4Customer Fax Machine v1.2b",
|
"Kleverig-H4rd-N12uk3b18-luc16037-720p-smpwr", "Eclipse_IDE_2023-12_R_win32_x86_64.zip", "PartsHPGL 1.06c", "Go4Customer Fax Machine v1.2b",
|
||||||
"AdBeGone 1.2", "3DMark03 320", "COD4MW2REMASTERED", "Movie.2020.1080p-GRP [a3f9e2b1]", "EPL__NZBSPLIT__f1fdb433772a1a7771d6b73c875009ff__NZBSPLIT__EPL.2026.Liverpool.vs.Brighton.1080pEN50fps",
|
"AdBeGone 1.2", "3DMark03 320", "COD4MW2REMASTERED", "Movie.2020.1080p-GRP [a3f9e2b1]", "EPL__NZBSPLIT__f1fdb433772a1a7771d6b73c875009ff__NZBSPLIT__EPL.2026.Liverpool.vs.Brighton.1080pEN50fps",
|
||||||
"iPhone", "Some.Show.S01E01.1080p.mkv",
|
"iPhone", "Some.Show.S01E01.1080p.mkv", "FastFilms 2.04", "BestCrypt NP v4.0", "EasyHTML 2.35 German", "SchiffsModell - September 2026",
|
||||||
|
"CrossStitcher - Issue 440, October 2026", "Laura15Juli2026", "IHaveAWife - Isa Bella", "DickHDDaily - Santana Red", "1904-SmithsWork-Hasluck",
|
||||||
] {
|
] {
|
||||||
assert!(!is_unreadable_name(n), "{n}");
|
assert!(!is_unreadable_name(n), "{n}");
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -43,7 +43,7 @@ function signals(t: string) {
|
|||||||
let maxConsonants = 0
|
let maxConsonants = 0
|
||||||
for (let i = 0; i < t.length; i++) {
|
for (let i = 0; i < t.length; i++) {
|
||||||
const c = t[i]
|
const c = t[i]
|
||||||
if (/[b-df-hj-np-tv-z]/i.test(c)) maxConsonants = Math.max(maxConsonants, ++consonants)
|
if (/[b-df-hj-np-tv-xz]/i.test(c)) maxConsonants = Math.max(maxConsonants, ++consonants)
|
||||||
else consonants = 0
|
else consonants = 0
|
||||||
if (i === 0) continue
|
if (i === 0) continue
|
||||||
const a = t[i - 1]
|
const a = t[i - 1]
|
||||||
@@ -90,11 +90,14 @@ export function tokenKind(t: string): 'word' | 'random' | null {
|
|||||||
if (t.length >= 10 && s.vowels * 6 < t.length) return 'random'
|
if (t.length >= 10 && s.vowels * 6 < t.length) return 'random'
|
||||||
return t.length >= 3 ? 'word' : null
|
return t.length >= 3 ? 'word' : null
|
||||||
}
|
}
|
||||||
// Mixed case beyond Capitalization: CamelCase (neutral) or a scramble, which has few vowels and short runs.
|
// Mixed case beyond Capitalization: CamelCase (neutral) or a scramble. Two CamelCase words with few vowels
|
||||||
|
// (FastFilms, BestCrypt, SchiffsModell) have four runs and no lone letter; a scramble has more runs, a lone
|
||||||
|
// letter or an inner capital run.
|
||||||
if (
|
if (
|
||||||
t.length >= 6 &&
|
t.length >= 6 &&
|
||||||
s.caseSwitches >= 2 &&
|
s.caseSwitches >= 2 &&
|
||||||
(s.innerUpper || (s.vowels * 4 < t.length && (s.runs >= 4 || s.lone >= 1)))
|
s.vowels * 4 < t.length &&
|
||||||
|
(s.runs >= 5 || s.lone >= 1 || s.innerUpper)
|
||||||
)
|
)
|
||||||
return 'random'
|
return 'random'
|
||||||
return null
|
return null
|
||||||
@@ -104,6 +107,15 @@ export function tokenKind(t: string): 'word' | 'random' | null {
|
|||||||
// 51294P1CUKR2R35X; COD4MW2REMASTERED (4 switches) and BEATLES1964REMASTER (2) stay.
|
// 51294P1CUKR2R35X; COD4MW2REMASTERED (4 switches) and BEATLES1964REMASTER (2) stay.
|
||||||
return t.length >= 12 && signals(t).switches >= 6 ? 'random' : null
|
return t.length >= 12 && signals(t).switches >= 6 ? 'random' : null
|
||||||
}
|
}
|
||||||
|
// Words with numbers between them (Laura15Juli2026, Back4WinXP): every letter run a word or an acronym.
|
||||||
|
if (
|
||||||
|
(t.match(/[A-Za-z]+/g) ?? []).every(
|
||||||
|
(w) =>
|
||||||
|
/^[A-Z]{2,}$/.test(w) ||
|
||||||
|
(/^[A-Z]?[a-z]{2,}$/.test(w) && (w.match(/[aeiouy]/g) ?? []).length * 5 >= w.length)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return null
|
||||||
const s = signals(sceneCase(t))
|
const s = signals(sceneCase(t))
|
||||||
if (t.length >= 5 && s.switches >= 3) return 'random'
|
if (t.length >= 5 && s.switches >= 3) return 'random'
|
||||||
if (t.length >= 6 && s.switches >= 2 && (s.lone >= 1 || s.innerUpper || s.maxConsonants >= 4))
|
if (t.length >= 6 && s.switches >= 2 && (s.lone >= 1 || s.innerUpper || s.maxConsonants >= 4))
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ test.group('release / isUnreadableName', () => {
|
|||||||
'EPL__NZBSPLIT__f1fdb433772a1a7771d6b73c875009ff__NZBSPLIT__EPL.2026.Liverpool.vs.Brighton.1080pEN50fps',
|
'EPL__NZBSPLIT__f1fdb433772a1a7771d6b73c875009ff__NZBSPLIT__EPL.2026.Liverpool.vs.Brighton.1080pEN50fps',
|
||||||
'iPhone',
|
'iPhone',
|
||||||
'Some.Show.S01E01.1080p.mkv',
|
'Some.Show.S01E01.1080p.mkv',
|
||||||
|
'FastFilms 2.04',
|
||||||
|
'BestCrypt NP v4.0',
|
||||||
|
'EasyHTML 2.35 German',
|
||||||
|
'SchiffsModell - September 2026',
|
||||||
|
'CrossStitcher - Issue 440, October 2026',
|
||||||
|
'Laura15Juli2026',
|
||||||
|
'IHaveAWife - Isa Bella',
|
||||||
|
'DickHDDaily - Santana Red',
|
||||||
|
'1904-SmithsWork-Hasluck',
|
||||||
]) {
|
]) {
|
||||||
assert.isFalse(isUnreadableName(n), n)
|
assert.isFalse(isUnreadableName(n), n)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user