// Stilpruefung: findet typische Muster maschinell wirkender Texte im Deutschen. // Wird von gen_wf2.py in den Code-Node "Artikel pruefen" eingebettet und ist // lokal mit node testbar. Liefert Hinweise, blockiert nichts. function stilpruefung(artikel) { const hinweise = []; const add = (art, stelle) => hinweise.push({ art, stelle: String(stelle).slice(0, 140) }); const ohneTags = (h) => String(h || '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim(); const html = String(artikel.content_html || ''); const absaetze = [...html.matchAll(/

([\s\S]*?)<\/p>/g)].map(m => ohneTags(m[1])).filter(Boolean); const volltext = [ohneTags(artikel.kurzantwort), ohneTags(html), ...(Array.isArray(artikel.faq) ? artikel.faq.map(f => ohneTags(f.antwort)) : [])].join(' '); // 1) Floskeln und Formeln, die in generierten Texten gehaeuft auftreten const formeln = [ ['genau das', 'Formel'], ['genau diese', 'Formel'], ['genau daran', 'Formel'], ['genau so', 'Formel'], ['der grund ist', 'Formel'], ['nicht nur', 'Formel "nicht nur, sondern auch"'], ['entscheidend ist', 'Formel'], ['entscheidend für', 'Formel'], ['größten hebel', 'Formel'], ['den unterschied', 'Formel'], ['am ende des tages', 'Floskel'], ['unterm strich', 'Floskel'], ['kurz gesagt', 'Floskel'], ['mit anderen worten', 'Floskel'], ['die gute nachricht', 'Floskel'], ['spielt eine rolle', 'Floskel'], ['es lohnt sich', 'Floskel'], ['egal ob', 'Formel'], ['in diesem artikel', 'Meta-Satz'], ['wir zeigen ihnen', 'Meta-Satz'], ['fazit', 'Zusammenfassung'], ['tatsächlich', 'Fuellwort'], ['letztlich', 'Fuellwort'], ['schlicht', 'Fuellwort'], ['simpel', 'Fuellwort'], ['erfahrungsgemäß', 'unbelegte Behauptung'], ['in aller regel', 'unbelegte Behauptung'], ['studien zeigen', 'unbelegte Behauptung'], ['nachweislich', 'unbelegte Behauptung'], ['bekanntlich', 'Floskel'], ]; const klein = volltext.toLowerCase(); formeln.forEach(([f, art]) => { let i = klein.indexOf(f); while (i >= 0) { add(art + ': "' + f + '"', '…' + volltext.slice(Math.max(0, i - 40), i + f.length + 50) + '…'); i = klein.indexOf(f, i + f.length); } }); // 2) Aufzaehlungen im Fliesstext if (/\berstens\b[\s\S]*\bzweitens\b/i.test(volltext)) add('Aufzaehlung "Erstens, zweitens"', 'Im Fliesstext lieber eine echte Liste oder zwei Saetze'); const dreier = volltext.match(/\b\w+(?:ig|lich|ell|bar|iv|sam)\b, \w+(?:ig|lich|ell|bar|iv|sam)\b und \w+(?:ig|lich|ell|bar|iv|sam)\b/gi) || []; dreier.forEach(d => add('Dreierreihe aus Adjektiven', d)); // 3) Rhetorische Frage im Absatz, direkt beantwortet absaetze.forEach(a => { const m = a.match(/[^.!?]{8,}\?\s+[A-ZÄÖÜ][^.!?]{3,}[.!]/); if (m) add('Rhetorische Frage mit Sofortantwort', m[0]); }); // 4) Satzlaengen: gleichfoermiger Rhythmus const saetze = ohneTags(html).split(/(?<=[.!?])\s+/).map(s => s.trim()) .filter(s => s.split(' ').length >= 3); const laengen = saetze.map(s => s.split(' ').length); if (laengen.length >= 8) { const mittel = laengen.reduce((x, y) => x + y, 0) / laengen.length; const sd = Math.sqrt(laengen.reduce((x, y) => x + (y - mittel) ** 2, 0) / laengen.length); if (sd < 4.5) add('Gleichfoermige Satzlaengen', `Durchschnitt ${mittel.toFixed(1)} Woerter, Streuung nur ${sd.toFixed(1)}. Kurze und lange Saetze mischen.`); for (let i = 0; i + 4 <= laengen.length; i++) { const fenster = laengen.slice(i, i + 4); if (Math.max(...fenster) - Math.min(...fenster) <= 3) { add('Vier Saetze gleicher Laenge hintereinander', saetze[i]); i += 3; } } } // 5) Gleiche Satzanfaenge for (let i = 0; i + 3 <= saetze.length; i++) { const w = saetze.slice(i, i + 3).map(s => s.split(' ')[0].toLowerCase()); if (w[0] === w[1] && w[1] === w[2] && w[0].length > 2) { add('Drei Saetze mit gleichem Anfang', saetze[i]); i += 2; } } const anfaenge = {}; absaetze.forEach(a => { const k = a.split(' ').slice(0, 2).join(' ').toLowerCase(); anfaenge[k] = (anfaenge[k] || 0) + 1; }); Object.entries(anfaenge).filter(([, n]) => n >= 3).forEach(([k, n]) => add('Absatzanfaenge wiederholen sich', `${n} Absaetze beginnen mit "${k}"`)); // 6) Gleich lange Absaetze const al = absaetze.map(a => a.split(' ').length).filter(n => n >= 12); if (al.length >= 5) { const m = al.reduce((x, y) => x + y, 0) / al.length; const sd = Math.sqrt(al.reduce((x, y) => x + (y - m) ** 2, 0) / al.length); if (sd / m < 0.25) add('Absaetze fast gleich lang', `Durchschnitt ${m.toFixed(0)} Woerter, kaum Abweichung. Einen Absatz bewusst kurz halten.`); } // 7) Praxiswissen: ohne eigene Stichpunkte bleibt der Text generisch if (!Array.isArray(artikel.praxis) || artikel.praxis.length === 0) { add('Kein Praxiswissen hinterlegt', 'Feld "praxis" in artikel-index.json fuellen, dann den Artikel darauf aufbauen'); } return hinweise; } if (typeof module !== 'undefined') module.exports = { stilpruefung };