This commit is contained in:
2026-07-26 18:11:57 +02:00
parent d408e01ab5
commit 21a3b34471
139 changed files with 32872 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
/**
* S03 Kategorisieren + Gewichten
* Liest clean-phrases.json → schreibt scored-phrases.json
* (jede Phrase: type, category, weight).
*/
import { readFileSync, writeFileSync, existsSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const cleanPath = join(__dirname, "clean-phrases.json");
const dumpPath = join(__dirname, "extracted-all.txt");
const outPath = join(__dirname, "scored-phrases.json");
const raw = JSON.parse(readFileSync(cleanPath, "utf8"));
/** @param {string} text */
function categorizeBetrieb(text) {
const t = text.toLowerCase();
if (
/rechnung|gutschrift|zahlungs|mahnung|avis|skonto|betrag|\bbar\b|ec-|kassenladenabschluss|eingangsrechnung|lieferschein/.test(
t,
)
)
return "Rechnungswesen";
if (
/termin|kunde|kundin|telefon|bewertung|mobilita|mobilitäts|angebot|autoservice|digitale kundenkarte/.test(
t,
)
)
return "Kunden";
if (
/reifen|lager|lieferung|bestell|ware|artikel|\bwm\b|retour|teile|batterien|additive|warenrückgabe|lieferscheine/.test(
t,
)
)
return "Lager";
if (/kasse|kassenbuch/.test(t)) return "Kasse";
if (
/fahrzeug|abmeld|zulassung|versicherung|unfall|schaden|getriebe|mietfahrzeug|tanken|ausgeputzt|rausgeputzt/.test(
t,
)
)
return "Fahrzeuge";
if (
/gefahren|gebracht|abgeholt|feuchtwangen|crailsheim|rothenburg|getränke|getranke|bosch|guardey|karlsruhe/.test(
t,
)
)
return "Fahrten";
if (
/ablage|spül|auftr[aä]ge?|büro|cyber|kalender|archiv|papier|wartebereich|betriebsurlaub|organisiert|einsortiert/.test(
t,
)
)
return "Organisation";
return "Betrieb";
}
/** @param {string} text */
function categorizeSchool(text) {
const t = text.toLowerCase();
if (
/bwl|kalkulation|beschaffung|lagerkenn|polypol|bruttoinlands|bestell|markt|preis|abc-analyse|abc-kunden|wettbewerbs|absatzpolit|bip\b|wohlstands|werbung|personalauswahl/.test(
t,
)
)
return "BWL";
if (
/buchhaltung|buchung|rechnung bestandteil|umsatzsteuer|sozialabgaben|inventar|bilanz|guv|konten/.test(
t,
)
)
return "Buchhaltung";
if (
/englisch|introducing|listening|dialogues|briefe im englischen|telefonate|mediation|enquiry|hörverstehen/.test(
t,
)
)
return "Englisch";
if (
/deutsch|das-\/dass|rechtschreib|stellenanzeige|leserbrief|argumentation|protokoll|ausdrucksvermögen|deutschbuch/.test(
t,
)
)
return "Deutsch";
if (
/excel|word|textverarbeitung|formatvorlage|diagramm|ergonomie|seriendruck|powerpoint|summen-funktionen|min-funktionen/.test(
t,
)
)
return "IT";
if (
/geschichte|migration|medien|politik|tarif|arbeitsschutz|demograf|bundestag|mauer|religion|demokratie|volksentscheid|integration|assimilation|homeoffice/.test(
t,
)
)
return "Gesellschaft";
if (
/projekt|präsentation|kommunikation|meeting|checkliste|e-mail|büromanagement|\bbfk\b|zwischenprüfung|zeitmanagement|zeugnis/.test(
t,
)
)
return "BFK";
return "Berufsschule";
}
/**
* Optional: Phrasen, die in beiden Ausbildungsjahren vorkommen → weight +1..+2
* Quelle: extracted-all.txt (Jahr-Marker „Erstes/Zweites Ausbildungsjahr“).
* @returns {Map<string, Set<number>>}
*/
function buildYearPresence() {
/** @type {Map<string, Set<number>>} */
const yearsByKey = new Map();
if (!existsSync(dumpPath)) return yearsByKey;
const dump = readFileSync(dumpPath, "utf8");
const blocks = dump.split(/^===== .+ =====$/m).slice(1);
for (const block of blocks) {
let year = 0;
if (/Erstes Ausbildungsjahr/i.test(block)) year = 1;
else if (/Zweites Ausbildungsjahr/i.test(block)) year = 2;
if (!year) continue;
const lines = block.split(/\r?\n/);
for (const line of lines) {
const key = line.replace(/\s+/g, " ").trim().toLowerCase();
if (key.length < 4) continue;
let set = yearsByKey.get(key);
if (!set) {
set = new Set();
yearsByKey.set(key, set);
}
set.add(year);
}
}
return yearsByKey;
}
/**
* @param {number} count
* @param {Set<number>|undefined} years
*/
function computeWeight(count, years) {
let weight = Math.max(1, count || 1);
if (years && years.size >= 2) {
// leichte Anhebung: häufige Phrasen +2, sonst +1
weight += weight >= 3 ? 2 : 1;
}
return weight;
}
/** @param {unknown} item */
function asEntry(item) {
if (typeof item === "string") return { text: item.trim(), count: 1 };
if (item && typeof item === "object" && "text" in item) {
const text = String(/** @type {{text: unknown}} */ (item).text).trim();
const count =
"count" in item && typeof /** @type {{count: unknown}} */ (item).count === "number"
? /** @type {{count: number}} */ (item).count
: 1;
return { text, count };
}
return null;
}
const yearPresence = buildYearPresence();
/** @type {{ text: string, type: 'betrieb'|'schule', category: string, weight: number, count: number, years: number[] }[]} */
const scored = [];
const seen = new Set();
/**
* @param {string} text
* @param {'betrieb'|'schule'} type
* @param {string} category
* @param {number} count
*/
function pushPhrase(text, type, category, count) {
const t = text.replace(/\s+/g, " ").trim();
if (!t) return;
const key = `${type}::${t.toLowerCase()}`;
if (seen.has(key)) return;
seen.add(key);
const years = yearPresence.get(t.toLowerCase());
const yearList = years ? [...years].sort() : [];
scored.push({
text: t,
type,
category,
weight: computeWeight(count, years),
count: Math.max(1, count || 1),
years: yearList,
});
}
const activities = raw.activities ?? [];
for (const a of activities) {
const e = asEntry(a);
if (!e) continue;
pushPhrase(e.text, "betrieb", categorizeBetrieb(e.text), e.count);
}
const schoolSource =
Array.isArray(raw.schoolWeighted) && raw.schoolWeighted.length
? raw.schoolWeighted
: (raw.school ?? []);
for (const s of schoolSource) {
const e = asEntry(s);
if (!e) continue;
pushPhrase(e.text, "schule", categorizeSchool(e.text), e.count);
}
scored.sort(
(a, b) =>
(a.type === b.type ? 0 : a.type === "betrieb" ? -1 : 1) ||
b.weight - a.weight ||
a.text.localeCompare(b.text, "de"),
);
const out = {
phrases: scored.map(({ text, type, category, weight }) => ({
text,
type,
category,
weight,
})),
meta: {
source: "clean-phrases.json",
total: scored.length,
betrieb: scored.filter((p) => p.type === "betrieb").length,
schule: scored.filter((p) => p.type === "schule").length,
multiYearBoosted: scored.filter((p) => p.years.length >= 2).length,
yearPresenceFrom: existsSync(dumpPath) ? "extracted-all.txt" : null,
generatedAt: new Date().toISOString(),
// Debug-Hilfen für Abnahme (nicht Teil des Datenmodells)
topBetrieb: scored
.filter((p) => p.type === "betrieb")
.slice(0, 10)
.map((p) => `${p.text}×${p.weight} [${p.category}]`),
topSchule: scored
.filter((p) => p.type === "schule")
.slice(0, 10)
.map((p) => `${p.text}×${p.weight} [${p.category}]`),
categoryCounts: scored.reduce((acc, p) => {
acc[p.category] = (acc[p.category] || 0) + 1;
return acc;
}, /** @type {Record<string, number>} */ ({})),
},
};
writeFileSync(outPath, JSON.stringify(out, null, 2), "utf8");
// Abnahme-Checks
const missing = out.phrases.filter((p) => !p.type || !p.category || !p.weight);
const schoolAsBetrieb = out.phrases.filter(
(p) =>
p.type === "betrieb" &&
/bwl|englisch|polypol|introducing|klassenarbeit|berufsschule|excel klassen|hörverstehen|umsatzsteuer|das-\/dass/i.test(
p.text,
),
);
console.log(
`Wrote ${out.phrases.length} scored phrases (betrieb ${out.meta.betrieb}, schule ${out.meta.schule}) → ${outPath}`,
);
console.log(`Multi-year boost: ${out.meta.multiYearBoosted}`);
console.log("Top Betrieb:", out.meta.topBetrieb.slice(0, 6).join(" · "));
console.log("Top Schule:", out.meta.topSchule.slice(0, 6).join(" · "));
if (missing.length) {
console.error(`FAIL: ${missing.length} Phrasen ohne type/category/weight`);
process.exit(1);
}
if (schoolAsBetrieb.length) {
console.warn(
`WARN Stichprobe: ${schoolAsBetrieb.length} mögliche Schule-als-Betrieb:`,
schoolAsBetrieb.slice(0, 5).map((p) => p.text),
);
} else {
console.log("OK: Schule-Stichworte nicht als betrieb gelabelt");
}
console.log("OK: jede Phrase hat type, category, weight");