update them

This commit is contained in:
mike
2025-12-21 17:30:40 +01:00
parent b0be3937db
commit 5d1547e39f
9 changed files with 45078 additions and 124 deletions

View File

@@ -0,0 +1,225 @@
{
"date": "2025-12-21",
"theme": "algemeen",
"difficulty": 1,
"rewards": {
"coins": 50,
"stars": 2,
"hints": 1
},
"gridv2": [
"############",
"############",
"###EROTSNI##",
"##AGES##EB##",
"##KNE##ER###",
"##IIL##LEE##",
"###BULKSILO#",
"##BBC##E#ET#",
"##NEONAN#NJ#",
"######RENEE#",
"############"
],
"words": [
{
"word": "AKI",
"clue": "AKI",
"startRow": 3,
"startCol": 2,
"direction": "vertical",
"answer": "AKI",
"arrowRow": 2,
"arrowCol": 2
},
{
"word": "EROTSNI",
"clue": "EROTSNI",
"startRow": 2,
"startCol": 3,
"direction": "horizontal",
"answer": "EROTSNI",
"arrowRow": 2,
"arrowCol": 2
},
{
"word": "AGES",
"clue": "AGES",
"startRow": 3,
"startCol": 2,
"direction": "horizontal",
"answer": "AGES",
"arrowRow": 3,
"arrowCol": 1
},
{
"word": "ELSENE",
"clue": "ELSENE",
"startRow": 4,
"startCol": 7,
"direction": "vertical",
"answer": "ELSENE",
"arrowRow": 3,
"arrowCol": 7
},
{
"word": "EB",
"clue": "EB",
"startRow": 3,
"startCol": 8,
"direction": "horizontal",
"answer": "EB",
"arrowRow": 3,
"arrowCol": 7
},
{
"word": "KNE",
"clue": "KNE",
"startRow": 4,
"startCol": 2,
"direction": "horizontal",
"answer": "KNE",
"arrowRow": 4,
"arrowCol": 1
},
{
"word": "ER",
"clue": "ER",
"startRow": 4,
"startCol": 7,
"direction": "horizontal",
"answer": "ER",
"arrowRow": 4,
"arrowCol": 6
},
{
"word": "ELENE",
"clue": "ELENE",
"startRow": 5,
"startCol": 9,
"direction": "vertical",
"answer": "ELENE",
"arrowRow": 4,
"arrowCol": 9
},
{
"word": "IIL",
"clue": "IIL",
"startRow": 5,
"startCol": 2,
"direction": "horizontal",
"answer": "IIL",
"arrowRow": 5,
"arrowCol": 1
},
{
"word": "LEE",
"clue": "LEE",
"startRow": 5,
"startCol": 7,
"direction": "horizontal",
"answer": "LEE",
"arrowRow": 5,
"arrowCol": 6
},
{
"word": "OTJE",
"clue": "OTJE",
"startRow": 6,
"startCol": 10,
"direction": "vertical",
"answer": "OTJE",
"arrowRow": 5,
"arrowCol": 10
},
{
"word": "BULKSILO",
"clue": "BULKSILO",
"startRow": 6,
"startCol": 3,
"direction": "horizontal",
"answer": "BULKSILO",
"arrowRow": 6,
"arrowCol": 2
},
{
"word": "BBC",
"clue": "BBC",
"startRow": 7,
"startCol": 2,
"direction": "horizontal",
"answer": "BBC",
"arrowRow": 7,
"arrowCol": 1
},
{
"word": "AR",
"clue": "AR",
"startRow": 8,
"startCol": 6,
"direction": "vertical",
"answer": "AR",
"arrowRow": 7,
"arrowCol": 6
},
{
"word": "NEREI",
"clue": "NEREI",
"startRow": 2,
"startCol": 8,
"direction": "vertical",
"answer": "NEREI",
"arrowRow": 1,
"arrowCol": 8
},
{
"word": "NEONAN",
"clue": "NEONAN",
"startRow": 8,
"startCol": 2,
"direction": "horizontal",
"answer": "NEONAN",
"arrowRow": 8,
"arrowCol": 1
},
{
"word": "BN",
"clue": "BN",
"startRow": 7,
"startCol": 2,
"direction": "vertical",
"answer": "BN",
"arrowRow": 6,
"arrowCol": 2
},
{
"word": "EGNIBBE",
"clue": "EGNIBBE",
"startRow": 2,
"startCol": 3,
"direction": "vertical",
"answer": "EGNIBBE",
"arrowRow": 1,
"arrowCol": 3
},
{
"word": "REELUCO",
"clue": "REELUCO",
"startRow": 2,
"startCol": 4,
"direction": "vertical",
"answer": "REELUCO",
"arrowRow": 1,
"arrowCol": 4
},
{
"word": "RENEE",
"clue": "RENEE",
"startRow": 9,
"startCol": 6,
"direction": "horizontal",
"answer": "RENEE",
"arrowRow": 9,
"arrowCol": 5
}
]
}

43751
out/pool.txt

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,38 @@
Date: 2025-12-19 Date: 2025-12-21
Feeds: https://feeds.nos.nl/nosnieuwsalgemeen, https://feeds.nos.nl/nosnieuwstech Feeds: https://feeds.nos.nl/nosnieuwsalgemeen, https://feeds.nos.nl/nosnieuwstech
Model: llama3:8b Model: mistralai/mistral-nemo-instruct-2407
Master size: 91871 Master size: 91892
Theme kept (in master): 32 Theme kept (in master): 10
Bridge size: 5000 Bridge size: 42000
Shorts kept: 28 Shorts kept: 133
Pool total: 5059 Pool total: 48786
Enforced minima:
2: 4000
3: 7000
4: 9000
5: 0
6: 0
7: 0
8: 0
Counts per length (theme): Counts per length (theme):
2: 1 2: 0
3: 3 3: 0
4: 6 4: 0
5: 3 5: 2
6: 2 6: 3
7: 13 7: 2
8: 4 8: 3
Counts per length (pool): Counts per length (pool):
2: 8 2: 248
3: 19 3: 1666
4: 10 4: 4850
5: 3 5: 30
6: 2 6: 1957
7: 73 7: 12491
8: 4944 8: 27544

View File

@@ -1,8 +1,40 @@
1. OM onderzoekt mishandeling van koeien na video's activisten 1. Explosie en dreigende teksten bij Haags tuincentrum: 'Ik begrijp er niks van'
Het Openbaar Ministerie gaat strafrechtelijk onderzoek doen naar dierenmishandeling bij verzamelcentra van vee. Dat gebeurt op basis van filmpjes waarop te zien is hoe koeien en kalfjes onder meer worden geschopt. De beelden werden begin vorige week gepubliceerd door de groep Ongehoord, die onderzoek doet naar de dierindustrie. Op vijf melkveehouderijen verspreid over het land werd met een verborgen camera gefilmd. Te zien is hoe de runderen en kalfjes worden geprikt met een prikstok of stroomstootwapen of aan een achterpoot worden weggesleept. De originele beelden zijn voor het onderzoek in beslag genomen. Het OM zegt ook nadrukkelijk te kijken naar de rol van de mensen die op de beelden te zien zijn. Het onderzoek wordt uitgevoerd door de Nederlandse Voedsel- en Warenautoriteit onder het gezag van het Functioneel Parket. Ernstige bedreigingen Vorige week werden twee mannen aangehouden die een boer hadden bedreigd in de Zuid-Hollandse gemeente Molenlanden. Hij was een van de veehouders bij wie de beelden met de verborgen camera waren gemaakt. De boer zegt na de publicatie van de beelden dagelijks meerdere ernstige bedreigingen te krijgen, waarbij ook zijn gezin doelwit is. De politie sluit niet uit dat er meer mensen worden aangehouden voor de bedreigingen. Bij een tuincentrum in Den Haag is vannacht een explosie geweest. De ingang van het tuincentrum raakte beschadigd. Bij het pand zijn ook briefjes met een dreigende tekst achtergelaten. De ontploffing was rond 03.00 uur bij het pand aan de Loosduinse Hoofdstraat. Niemand raakte gewond, wel raakte een glazen deur beschadigd. Voor de ingang zijn een aantal A4'tjes gevonden met de tekst "Hollander 20 stuks tijd om te betalen!!!". Wat de achtergelaten boodschap betekent, is onderdeel van het onderzoek, aldus de politie. Eigenaar staat voor een raadsel Maarten Kester, de eigenaar van het tuincentrum, zegt tegen Omroep West voor een raadsel te staan. "Ik zou niet weten wat ermee bedoeld wordt", reageert hij. Financiële problemen of conflicten heeft het bedrijf naar eigen zeggen niet. "We hebben alles onder controle, ik begrijp er niks van." Hij heeft op camerabeelden gezien wat er vannacht is gebeurd. "Er klimt iemand over het hek heen, legt de briefjes met dat vage verhaal neer, plakt de bom op de deur, steekt het aan en maakt een foto." Het gat in de deur is vanochtend vroeg ontdekt. "Je verwacht wel wat anders op de zondag", zegt Kester. "Ik ben heel blij dat er niemand binnen is geweest." Er is nog niemand aangehouden. "De camerabeelden zijn duidelijk, maar de dader is helemaal in het zwart gekleed en draagt een capuchon", zegt Kester. Toch heeft hij er vertrouwen in dat de politie de dader snel vindt, omdat "hij zoveel sporen heeft achtergelaten". Het tuincentrum is vandaag gewoon opengegaan voor klanten. De hoofdingang was tijdens het politieonderzoek afgezet, waardoor mensen via een tijdelijke ingang naar binnen moesten. "De beschadigde deur doet het gelukkig nog. Het gat moet worden gerepareerd, maar alles draait weer", aldus Kester. Mensen die iets gehoord of gezien hebben rond het tijdstip dat de explosie gebeurde, worden gevraagd zich bij de politie te melden. Bijna evenveel explosies als vorig jaar Dit jaar zijn zijn er in Nederland al meer dan 1300 aanslagen met explosies gepleegd op woningen en andere panden, blijkt uit de meest recente cijfers van het Offensief Tegen Explosies, waarin onder andere de politie en gemeenten zijn vertegenwoordigd. Naar verwachting valt het aantal aanslagen bijna even hoog uit als in 2024 . Heel vorig jaar waren het er 1543. De politie heeft dit jaar meer dan 300 verdachten opgepakt voor explosies, onder wie een groot aantal minderjarigen.
2. 'Eyeopener voor cursisten', alcohol werkt langer door dan bestuurders denken 2. Camiel Jansen nieuwe Componist der Nederlanden
Ondanks talloze campagnes is vaak nog de gedachte dat je met twee glazen alcohol nog wel kan rijden. Dat merkt cursusleider Robert Mazier van Trafieq, dat in opdracht van het CBR alcohol- en drugscursussen geeft. Vandaag maakte het CBR bekend dat er dit jaar 26 procent meer cursisten waren dan vorig jaar. "Wat twee glazen zijn, hangt volledig af van hoe je schenkt", vertelt Mazier, terwijl hij kijkt naar een wijnglas dat voor iets meer dan de helft gevuld is met water. "Dit glas heb ik zelf ingeschonken, zo zou ik hem op een gezellige avond drinken, maar dit zijn al twee eenheden." Eyeopener Tijdens de cursussen blijkt hoe weinig mensen weten over wat alcohol en drugs daadwerkelijk met hun lichaam doen. "Een borrel, een glas wijn of een glas bier is in principe één eenheid", legt Mazier uit. "Maar in een flesje bier zit vaak al meer dan één eenheid. Dat is voor veel cursisten een eyeopener." Even verrast zijn cursisten als het gaat over de tijd die het duurt voor een eenheid alcohol is afgebroken. Ongeveer een half uur na het drinken van een glas komt het aan bij je lever. Het lichaam doet er vervolgens anderhalf uur over om één eenheid alcohol af te breken. Cursusleider Mazier rekent voor: "Drie eenheden betekent al snel vijf uur voordat het uit je systeem is." Cursus Een van de cursisten wil alleen anoniem zijn verhaal doen, hij volgde vijf jaar geleden een alcoholcursus. Toch zat daar voor hem niet de impact: "Je wordt meegenomen door de politie, moet blazen en bent je rijbewijs kwijt. En dat terwijl ik in een autogarage werkte. Dat wil ik nooit meer." Over de inhoud van de cursus is hij niet heel enthousiast. "Ik denk dat 90 procent van de mensen er ook geen zin in had. Maar je moet zo'n cursus nu eenmaal doen om je rijbewijs terug te krijgen." Toch merkt hij wel dat er in zijn vriendengroep nog steeds mensen zijn die na festivals rijden, terwijl ze hebben gedronken of drugs hebben gebruikt. Zelf is hij strenger. "Ik doe het niet meer. Je weet nooit wat er kan gebeuren, en je bent niet verzekerd als het misgaat. Voor mij is het een no-go." Combinatiegebruik Behalve voor alcohol worden automobilisten in toenemende mate op cursus gestuurd vanwege drugsgebruik. Volgens Frits Lindeman, projectleider Verkeer bij de politie, is het aantal bekeuringen rondom rijden onder invloed al jarenlang stabiel, rond de 40.000 per jaar. Vroeger ging het vooral om alcohol. "Maar nu zien we steeds meer combinatiegebruikers: alcohol én drugs." Het hogere cijfer komt deels doordat de politie anders is gaan werken. "Sinds 2018 testen we bij speekseltests standaard ook op drugs," zegt Lindeman. "Voorheen gebeurde dat lang niet altijd." Daarnaast zijn processen geautomatiseerd, waardoor meldingen sneller en consequenter bij het CBR terechtkomen. Speekseltest In de cursussen merkt Mazier dat drugsgebruik vaak wordt onderschat. "Bij drugs denken mensen al snel: ik voel me prima, dus ik kan rijden." Maar dat gevoel zegt weinig, legt hij uit. "Als je regelmatig een joint rookt, test je vrijwel altijd positief bij een speekseltest. Bij één joint duurt het zes tot acht uur voordat het uit je lichaam is. Wie 's avonds rookt, kan de volgende ochtend soms weer rijden, maar garanties zijn er niet." Bij andere drugs zijn de risico's nog groter. "Cocaïne lijkt snel uit te werken, vaak na zo'n vier uur", zegt Mazier. "Maar bij xtc moet je denken aan een dag tot zelfs twee dagen. Je merkt dat misschien niet meer, maar het blijft zichtbaar in je speeksel en bloed." Dat betekent dat iemand zich nuchter kan voelen, terwijl die persoon juridisch nog steeds onder invloed is. Geen brainwash De politie controleert tegenwoordig gerichter. Grote alcoholfuiken zijn minder gebruikelijk. "We kijken waar de risico's het grootst zijn", zegt politieman Lindeman. "Bij festivals testen we sneller op drugs. Bij ongevallen testen we altijd op alcohol en steeds vaker ook op verdovende middelen." De cursussen zijn geen straf, benadrukt Mazier. "Mensen komen vaak binnen met weerstand of schaamte. Het is geen brainwash, maar we zien wel dat mensen soms voor het eerst echt reflecteren." En dat is nodig, want de boodschap is duidelijk: "Gebruik je drugs of alcohol, dan is autorijden eigenlijk geen optie. Better safe than sorry." Camiel Jansen is benoemd tot Componist der Nederlanden. Dat is bekendgemaakt in TivoliVredenburg in Utrecht. Jansen volgt Anne-Maartje Lemereis op, die de titel de afgelopen twee jaar droeg. De selectiecommissie was unaniem in zijn keuze en roemt zijn verbeeldingskracht, muzikale diepgang en maatschappelijke betrokkenheid. Volgens de jury maakt Jansens combinatie van muzikale diepgang, maatschappelijke betrokkenheid en jeugdig enthousiasme hem een ideale vertegenwoordiger van het Nederlandse componeren. De komende twee jaar zet de 34-jarige Jansen zich als ambassadeur in voor het Nederlandse muziekleven: van componisten en musici tot docenten, leerlingen en jonge makers. Onder Jansens eigen motto 'Een klank voor de kloof' verbindt hij volgens de jury mensen met nieuwe composities, educatieve projecten en samenwerkingen. Docent en presentator "Het is een ongelofelijke eer dat ik dit mag doen", zegt Jansen in een reactie. Hij wil zijn platform gebruiken om tegenstellingen tussen groepen weg te nemen. "Ik voel de urgentie om met muziek te laten zien dat we dichter bij elkaar staan dan we denken. Daarom wil ik projecten organiseren die klank geven aan de kloof, in de concertzaal, én op straat. Ik kijk er enorm naar uit." Jansen is componist, contrabassist en presentator. Naast zijn werk als componist is hij docent aan het Conservatorium van Amsterdam en presentator bij radiozender NPO Klassiek . Componist der Nederlanden is een initiatief van Buma Cultuur en BumaStemra. Sinds 2015 wordt er elke twee jaar een componist benoemd die zich inzet voor de zichtbaarheid en diversiteit van nieuwe Nederlandse muziek.
3. Amerikaanse verkoop TikTok rond, algoritme voortaan getraind met data VS 3. Ruim 7000 kilo illegaal vuurwerk gevonden na controle bestelbus
De politie heeft eergisteren duizenden kilo's illegaal vuurwerk gevonden na een controle op de A15. Twee verdachten zijn aangehouden. Ter hoogte van het dorp Kesteren controleerden agenten vrijdag een bestelbusje. In de laadruimte van de wagen vonden zij vier pallets illegaal vuurwerk, in totaal zo'n 1600 kilo. Na de ontdekking werd ook een loods in Enschede doorzocht, waar nog eens ruim 5500 kilo illegaal vuurwerk bleek te liggen, meldt Omroep Gelderland . De politie meldt vandaag dat twee personen zijn aangehouden: de 32-jarige bestuurder van het busje, die uit Barendrecht komt, en een 44-jarige man uit Duitsland. "Het vuurwerk wordt vernietigd", laat de politie verder weten.
4. Israël keurt 19 nederzettingen goed op bezette Westelijke Jordaanoever
De Israëlische regering heeft goedkeuring gegeven voor de oprichting van negentien nieuwe nederzettingen op de bezette Westelijke Jordaanoever. De afgelopen drie jaar werden al vijftig nederzettingen goedgekeurd. Vijf van de nederzettingen bestonden al, maar hadden onder de Israëlische wetgeving nog geen formele status. In twee gevallen gaat het om nederzettingen die in 2005 nog waren ontmanteld. De Israëlische kolonies in het bezette gebied zijn volgens het internationaal recht illegaal. Het besluit werd bekendgemaakt door de ultrarechtse minister van Financiën Smotrich, die zelf als kolonist op de Westelijke Jordaanoever woont. Hij wil met de nederzettingen de oprichting van "een Palestijnse terreurstaat" voorkomen. Sterke toename In totaal hebben nu 210 nederzettingen groen licht gekregen van het Israëlische kabinet, meldt Peace Now, die de ontwikkelingen in het gebied monitort. In 2022, aan het begin van de regeertermijn van het huidige kabinet onder leiding van premier Netanyahu waren er nog 141 goedgekeurde nederzettingen. De afgelopen jaren zijn er volgens de Verenigde Naties in het gebied vele duizenden woningen voor kolonisten gebouwd. Het nieuws komt op het moment dat de VS druk uitoefent op Israël en Hamas om een begin te maken met de tweede fase van het staakt-het-vuren in Gaza, dat sinds 10 oktober van kracht is. De illegale nederzettingen worden gezien als obstakel voor een mogelijke Palestijnse staat, omdat er daardoor steeds minder grondgebied voor de Palestijnen overblijft. De Westelijke Jordaanoever Israël veroverde de Westelijke Jordaanoever, inclusief Oost-Jeruzalem, in 1967, tegelijk met de Gazastrook. Sindsdien houdt Israël het gebied bezet. In delen ervan hebben Palestijnen een vorm van zelfbestuur. Op de Westelijke Jordaanoever wonen zo'n drie miljoen Palestijnen. Daarnaast wonen er intussen zo'n 700.000 Israëlische kolonisten. Volgens internationaal recht zijn de nederzettingen waarin zij wonen illegaal. Veel Palestijnen zien de Westelijke Jordaanoever als deel van hun toekomstige staat, met Oost-Jeruzalem als hoofdstad, ook al wordt het perspectief daarop steeds kleiner. Afgelopen najaar registreerde de Verenigde Naties honderden aanvallen van Joodse kolonisten op Palestijnse bewoners van de Westelijke Jordaanoever. Gemiddeld waren het er acht per dag. Kolonisten staken auto's en huizen in brand, vernielden landbouwgrond en bedreigden Palestijnen. Die zijn voor hun veiligheid afhankelijk van het Israëlische leger, maar dat doet nauwelijks iets om geweld te voorkomen. Ook worden de daders van de aanslagen maar zelden vervolgd. Volgens premier Netanyahu is een kleine, extremistische groep kolonisten verantwoordelijk voor het geweld, maar dat wordt door mensenrechtenorganisaties weersproken . Zij maken zich grote zorgen over de terreur en spreken van "systematisch geweld" door kolonisten die structureel steun krijgen van de regering.
5. Drie mensen gewond bij instorten balkon tijdens feestje in Zwolle
In Zwolle is afgelopen nacht het balkon van een studentenwoning ingestort terwijl er mensen op stonden. Drie personen raakten gewond, bevestigt de veiligheidsregio na berichtgeving van de regionale omroep Oost . Het gaat volgens de veiligheidsregio om een balkon van hout dat niet tijdens de bouw van de woning is gemaakt, maar later is toegevoegd. Op het moment van instorten stonden er volgens de hulpdiensten tijdens een feestje ongeveer tien mensen op het balkon. De houten constructie kon het gewicht van de mensen niet aan. Het balkon stortte in en de mensen vielen zo'n drie meter naar beneden. Snijwonden Om 03.00 uur kwam er een melding van het incident binnen bij de hulpdiensten. Brandweer, politie en ambulancepersoneel kwamen helpen. Twee mensen hadden snijwonden en één persoon had pijnlijke ribben. Ze zijn behandeld bij de huisartsenpost. Mensen die aanwezig waren op het feestje waren volgens de veiligheidsregio erg geschrokken en zochten steun bij elkaar op straat.
6. 'Miljoenen liters ontlasting van festivals illegaal geloosd in Brabant'
Op het terrein van een bedrijf in het Brabantse Wintelre zouden jarenlang miljoenen liters ontlasting van grote festivals illegaal zijn gedumpt. Dat blijkt uit stukken die Omroep Brabant bij meerdere overheden heeft opgevraagd via de Wet open overheid (Woo). Volgens Omroep Brabant is het Openbaar Ministerie een strafrechtelijk onderzoek begonnen naar de ondernemer die daarachter zou zitten. Een woordvoerder van het OM kan niet bevestigen of ontkennen dat er een onderzoek loopt. De lozingen gebeurden op het terrein van mestverwerker en varkenshouderij Daas in Wintelre, schrijft de omroep. Tankwagens met ontlasting en afvalwater van grote festivals zouden hun inhoud overhevelen naar een container. Vanuit deze container zou de ontlasting via een illegale aansluiting in het riool belanden. Ook varkensmest Naast het lozen van afvalwater van evenementen zijn deze illegale aansluitingen ook gebruikt om varkensmest te dumpen. Dat wordt volgens Omroep Brabant duidelijk uit een rapport dat is opgesteld door de gemeente Eersel, waar Wintelre onder valt. Uit documenten van de gemeente Eersel blijkt dat in 2023 bijna 9 miljoen liter aan afvalwater is geloosd op deze plek, zonder dat daar toestemming voor was gegeven, schrijft Omroep Brabant. De gemeente heeft daartegen opgetreden, waarna de container is weggehaald. Ondernemer Pierre Daas ontkent tegenover de omroep dat er sprake is van illegale lozingen. "Mest wordt afgevoerd conform mestwetgeving en alle lozingen worden geregistreerd en gemeld. Conform de afspraken wordt er een vergoeding voor de lozingen betaald." Een bedrijf uit Gilze zou de ontlasting afleveren op het terrein van Daas. Dit bedrijf haalt afvalwater op van grote evenementen in Nederland en België, zoals Paaspop, Intents Festival, Concert at Sea, Zwarte Cross en Graspop. Of medewerkers van het bedrijf wisten dat het afvalwater illegaal werd geloosd, is niet duidelijk. Wel wordt het bedrijf herhaaldelijk in de opgevraagde documenten genoemd, aldus Omroep Brabant. Het bedrijf heeft niet gereageerd op vragen. Ook de genoemde festivals zijn door de regionale omroep benaderd, maar hebben nog niet gereageerd. Rioolproblemen In Wintelre waren langere tijd problemen met de riolering. Die zijn veroorzaakt door aangekoekte dierlijke mest en het afvalwater, zo staat in de documenten. Ambtenaren schrijven volgens de regionale omroep daarin dat dat "zeer waarschijnlijk als gevolg is van de activiteiten" van het bedrijf van Daas. De rioolproblemen leidden in de omgeving tot overlast, blijkt verder uit de stukken. "Wij hebben al jarenlang overlast van stank via het riool", schreef een omwonende in november 2023 aan de gemeente. "Deze overlast wordt veroorzaakt doordat enkele huizen verder bijna dagelijks vrachtwagens vol worden geloosd op het riool. Hier hebben wij al meerdere malen een melding van gemaakt, maar de heer Daas is daar niet van onder de indruk", aldus de omwonende. Mestfraude In een reactie op persvragen over de opgevraagde stukken is de gemeente Eersel minder stellig dan de eigen ambtenaren eerder in de documenten. De gemeente laat aan de omroep weten dat er meerdere verstoppingen zijn geweest, maar "daarvan is niet met zekerheid vast te stellen dat deze afkomstig zijn van Daas". Het afgelopen jaar zouden zich geen verstoppingen of andere problemen meer hebben voorgedaan. De mestverwerker kwam eerder in het nieuws voor het overtreden van de milieuregels. Zo bleek afgelopen zomer dat een stuk grond in Eindhoven vervuild is geraakt, omdat een mestzak van het bedrijf van Daas was gaan lekken. Daardoor kwam naar schatting 180.000 liter afvalwater in de grond terecht. In 2019 is de eigenaar van het bedrijf veroordeeld voor het overtreden van de meststoffenwet en valsheid in geschrifte.
7. Minuut stilte in Australië na aanslag, premier Albanese uitgejouwd
In Australië is om 18.47 uur lokale tijd een minuut stilte gehouden voor de slachtoffers van de dodelijke aanslag op Bondi Beach in Sydney. Een week geleden werden bij een viering van het joodse lichtjesfeest Chanoeka vijftien mensen doodgeschoten, ruim veertig anderen raakten gewond. De minuut stilte was exact een week nadat de eerste schoten werden gelost. De Australische premier Albanese was aanwezig bij de herdenking bij het stadsstrand. Daar kwamen volgens internationale persbureaus meer dan 10.000 mensen op af. Bij aankomst werd de premier door mensen in het publiek uitgejouwd. Op een later moment viel zijn naam tijdens een toespraak, waarop een mengeling van applaus, gefluit en boegeroep volgde. Albanese zat op de eerste rij en droeg een keppeltje. Hij hield geen toespraak. De regering van Albanese kreeg de afgelopen week veel kritiek van vooral de Joodse gemeenschap in het land, die vindt dat de regering te weinig heeft gedaan tegen antisemitisme. Het aantal antisemitische incidenten is volgens de Raad van de Australisch-Joodse Gemeenschap in een jaar tijd verdrievoudigd tot ruim 1600. Man die het wapen afpakte David Ossip, de voorzitter van de Joodse Raad van Afgevaardigden van de deelstaat New South Wales, was een van de sprekers op het podium. Hij zei dat het land zich nu op een donkere plek bevindt, maar dat Chanoeka mensen leert dat licht zelfs op de guurste plekken kan schijnen. De vader van Ahmed Al Ahmed was ook aanwezig. De 43-jarige Ahmed pakte tijdens de aanslag een wapen af van een van de schutters. Hij belandde in het ziekenhuis met schotwonden. De beelden van Ahmed die het wapen afpakte gingen de wereld rond . De vader zei op verzoek van zijn zoon dat "De Heer dicht bij mensen met een gebroken hart is". Bekijk hier de minuut stilte op Bondi Beach: Niet alleen op Bondi Beach werd stilgestaan bij de slachtoffers van de chanoekaviering. Vandaag was voor het hele land een dag van bezinning afgekondigd. Vlaggen hingen halfstok en televisie- en radiozenders namen een minuut stilte in acht. Nieuwe wetten Voorafgaand aan de herdenking kondigde Albanese een onderzoek aan naar het werk van de inlichtingendiensten. Hij wil weten of politie en inlichtingendiensten de juiste bevoegdheden hebben en of ze goed genoeg samenwerken om het land veilig te houden. Albanese beloofde eerder deze week al dat er nieuwe wetten komen waarmee mensen die haatzaaiende taal gebruiken en geweld verspreiden kunnen worden vervolgd. Verder wil hij de wapenwetten in Australië aanscherpen en kunnen mensen met een vuurwapen dat vrijwillig inleveren . Ze krijgen daar dan een vergoeding voor. Bij de aanslag schoten twee mannen, een vader en zoon, met meerdere vuurwapens op mensen die op het populaire stadsstrand waren samengekomen voor Chanoeka. De vader werd door de politie doodgeschoten. De zoon ligt gewond in het ziekenhuis en wordt daar bewaakt door de politie. In de auto van de schutters werden IS-vlaggen en explosieven gevonden.
8. Cultuur snuiven op sociale media: 'Het is ook een vorm van nationalisme'
Expats in Nederland die zich verbazen over grote fietsenstallingen, koks die Caribische recepten delen en ouders die opvoedervaringen uitwisselen: socialemediakanalen staan er vol mee. Leuke en informatieve video's over cultuur vinden een groeiend publiek. Een van de genoemde redenen: gebruikers zoeken in een globale samenleving naar authenticiteit. Het lijkt bijna een soort verzet tegen de globalisering, zegt socialemediakenner Joey Scheufler tegen de NOS. Op sociale media kan het juist van waarde zijn om iets van je eigen cultuur of persoonlijke ervaringen te delen. "Dat zie je bijvoorbeeld met de video's van expats die delen wat ze gek vinden aan het leven in Amsterdam", legt Scheufler uit. "Dat werkt goed, omdat die mensen dat echt ervaren. Als ze opgaan in de rest, is het niet meer leuk om naar te kijken." Door sociale media komen gebruikers sneller in aanraking met andere culturen, vertelt Scheufler. Uit een rapport van Ipsos uit 2023 blijkt dat Europese TikTokgebruikers de app onder meer gebruiken om online cultuur te snuiven. Van de ondervraagden gaf 59 procent aan dat ze via TikTok getuige konden zijn van bijzondere momenten die ze in het echte leven niet snel zouden meemaken. "Cultuur verbindt mensen met gedeelde interesses en normen en waarden, zowel lokaal als globaal", aldus de Ipsos-onderzoekers. Algoritme Dat die video's de belangstelling wekken van gebruikers én goed scoren op de tijdlijn heeft te maken met een relatief nieuw algoritme, zegt Scheufler. "Voorheen was op andere platformen, zoals YouTube en Instagram, voornamelijk het aantal volgers belangrijk voor het bereik van een maker", legt hij uit. In het nieuwe algoritme wordt elke video in principe getoond, en als veel gebruikers langer naar een bepaald onderwerp kijken, toont het algoritme de video vaker. "Het is een soort sneeuwbaleffect", aldus Scheufler. Volgens hem werkt het nu beter om te laten zien wie je echt bent in plaats van een gelikt beeld te tonen. "Nu gaat het ook om echtheid en authenticiteit." Video's van deze influencers gaan de hele wereld over: TikTok, dat sinds het begin van de coronapandemie snel aan populariteit heeft gewonnen, is een pionier geweest in het introduceren van dat nieuwe algoritme, schreef het Amerikaanse Time Magazine in 2023. Voortbestaan van cultuur Ook influencers uit Aruba, Bonaire en Curaçao bereiken met video's over hun cultuur mensen over de hele wereld. "Klein zijn betekent niet dat je niets te zeggen hebt en dat je niets betekent in deze wereld", zegt cultureel antropoloog Rose Mary Allen. Zelf kijkt de hoogleraar van de Universiteit van Curaçao wel eens kookvideo's over de Caribische keuken. De 74-jarige Allen is blij om te zien dat cultuur populair is op sociale media. Iets wat ze ook bespeurt bij jonge makers van de ABC-eilanden. "Het geeft je moed en kracht. Zelf behoor ik tot een wat oudere generatie, maar als jongeren het blijven doorgeven, blijft de cultuur voortbestaan." Het is volgens Allen vanuit de antropologie te verklaren dat filmpjes over cultuur een aantrekkingskracht hebben. Online verbinden socialmediagebruikers vanuit de hele wereld zich met elkaar, en zo ontstaat er een globale cultuur. "Maar daarbinnen wil iemand ook zijn eigen cultuur laten zien aan een ander", aldus Allen. "Het is een vorm van nostalgie, maar ook een vorm van nationalisme." Caribische keuken Een van de makers van zulke filmpjes is de 44-jarige Jurino Ignacio, die jaren geleden voor zijn studie vanuit Curaçao naar Nederland kwam. Hij deelt al ruim dertien jaar zijn Antilliaanse recepten. De kok begon in eerste instantie met het delen van recepten voor zijn drie kinderen op zijn eigen website. Later maakte hij de overstap van foto's bij die recepten naar kookvideo's op sociale media. Sinds 2021 is hij online bekend als de Super Dushi Chef, waar hij video's maakt met een knipoog, en daarmee trekt hij een groot publiek. Met ruim 900.000 gecombineerde volgers op zijn socialemediakanalen is zijn bereik groot. Mensen met verschillende achtergronden maken via zijn kanalen kennis met de Caribische keuken. "Mensen zijn verliefd geraakt op onze eilanden. Sommige volgers vertelden me dat ze na het zien van de video's zelfs op vakantie zijn gegaan naar Curaçao." Hij sluit al zijn video's af met de herkenbare kreet "het is super dushi", oftewel "het is superlekker". En die slogan blijft hangen. Hij wordt zelfs aangesproken als hij bijvoorbeeld even boodschappen gaat doen. "Ik krijg er nu kippenvel van, maar ik ben ook dankbaar." Volgens Ignacio brokkelt zo, mede door sociale media, een grote muur rondom verschillende landen af. En maken mensen op een respectvolle manier kennis met andere culturen.
9. Vogelgriep bij pluimveebedrijf in Venray, 23.000 kalkoenen afgemaakt
Bij een kalkoenenhouder in Ysselsteyn, in de gemeente Venray in Limburg, is vogelgriep ontdekt. Alle 23.000 kalkoenen op het bedrijf worden gedood. Op die manier wil de Nederlandse Voedsel- en Warenautoriteit (NVWA) verdere verspreiding van het virus tegengaan. In de gemeente Venray zijn veel pluimveebedrijven gevestigd. In 10 kilometer rond het bedrijf liggen 54 andere pluimveebedrijven. In die zone geldt nu een vervoersverbod. Dat betekent dat er geen vogels, broedeieren en consumptie-eieren mogen worden vervoerd zegt de NVWA . Ook mag er geen mest of gebruikt strooisel worden vervoerd. De bedrijven die binnen 3 kilometer van de uitbraak in Ysselsteyn liggen worden de komende tijd nauwlettend in de gaten gehouden. Ook doet de NVWA traceringsonderzoek. Daarbij wordt onderzocht of er in de dagen voordat de vogelgriep bij dit bedrijf werd ontdekt, eieren of materialen van en naar dit bedrijf zijn vervoerd. Op steeds meer plekken vogelgriep Vorige maand werd in Tienray, op een kleine 20 kilometer van Ysselsteyn, ook vogelgriep ontdekt. Een aantal bedrijven dat in de 10 kilometerzone van het bedrijf in Ysselsteyn ligt, had al een vervoersverbod vanwege de besmetting in Tienray. In Nederland geldt sinds 16 oktober een landelijke ophok- en afschermplicht voor alle commercieel gehouden vogels, zoals kippen, eenden en kalkoenen. Desondanks neemt het aantal gevallen van vogelgriep snel toe. Sinds eind november geldt er ook een landelijk bezoekverbod voor bedrijven waar kippen of kalkoenen worden gehouden. En sinds begin deze maand is er een landelijk tentoonstellingsverbod. Tentoonstellingen, wedstrijden of markten met vogels mogen daardoor niet doorgaan.
10. Dode Ierse man had ID op zak, toch bleef zijn identiteit ruim een jaar onbekend
Een Iers echtpaar wil weten waarom het dertien maanden heeft geduurd voordat hun overleden zoon werd geïdentificeerd, terwijl hij negen persoonlijke documenten op zak had. Ze hebben een klacht ingediend vanwege de gang van zaken rond de autopsie. De politieombudsman gaat de zaak nu onderzoeken, meldt The Irish Times . Het lichaam van de destijds 43-jarige James O'Neill werd op 17 november 2023 gevonden in Phoenix Park in het centrum van Dublin. Bij de eerste autopsie werd in een rugzak die vlak bij het lichaam lag een cv aangetroffen met zijn naam erop. Met die informatie werd echter niets gedaan, omdat de genoemde adressen en werkgevers geen verdere aanknopingspunten opleverden. O'Neills lichaam bleef vervolgens dertien maanden in het mortuarium liggen. Pas bij een nieuw forensisch onderzoek werden in een ritsvak van zijn regenjas alsnog documenten gevonden die tot zijn identificatie leidden. Het ging in totaal om negen documenten, waaronder een identiteitsbewijs en opnieuw het cv. "Deze documenten waren niet aan het licht gekomen bij eerder onderzoek van de kleding, noch door de Ierse politie, noch door de pathologen van het mortuarium in Dublin", citeert de krant uit het autopsierapport. Levensstijl De ouders van O'Neill werden in december 2024, ruim een jaar later, geïnformeerd over de vondst van het lichaam van hun zoon. Kort daarna dienden zij een klacht in bij Fiosrú, het bureau van de politieombudsman. Ze vragen zich af waarom de identificatie zo lang heeft geduurd en noemen de gang van zaken verbijsterend. De familie zei in een eerder interview geen reden te hebben gehad om alarm te slaan, omdat hun zoon bewust voor een levensstijl had gekozen waarbij hij lange tijd uit beeld kon verdwijnen. Hij reisde veel en liet dan niets van zich horen. Het mortuarium heeft inmiddels excuses aangeboden. "Deze nalatigheid van de lijkschouwersdienst van het district Dublin heeft onnodige vertraging en leed veroorzaakt bij uw cliënten, waarvoor ik mijn oprechte excuses aanbied", aldus de betrokken lijkschouwer.
11. Nederlandse ondernemer sloeg 500 miljoen af, en wil nu zelf de AI-race winnen
Het gebeurt niet elke dag dat een Nederlandse ondernemer 'nee' zegt tegen 500 miljoen dollar. Pim de Witte deed dat wel toen OpenAI, het Amerikaanse bedrijf achter ChatGPT, hem naar verluidt dit astronomische bedrag bood voor zijn bedrijf. Maar wat de Amerikanen met zijn data van plan waren, wil De Witte nu zelf gaan doen. De Witte is eigenaar van het videoplatform Medal.tv waar gamers beelden van hun spellen delen. En dat doen ze massaal. "YouTube heeft ongeveer 800 miljoen uploads per jaar. Wij verwachten daar dit jaar overheen te gaan", zegt de 30-jarige ondernemer uit Nijmegen tegen Nieuwsuur . Die beelden, plus de bijbehorende data, zijn een goudmijn voor bedrijven die werken aan de volgende generatie AI-toepassingen. Want waar AI nu vooral draait om tekst en chatbots, is de volgende stap het trainen van robots om zelfstandig taken uit te voeren. En die robots kunnen van games leren hoe ze die taken moeten uitvoeren. "Het is de grootste dataset van hoe mensen zich gedragen in een gesimuleerde wereld", zegt De Witte. Dat deze game-data zo belangrijk blijkt voor de ontwikkeling van robots is een "verrassende ontwikkeling", zegt Deborah Nas, hoogleraar innovatie aan de TU Delft. Ze legt uit waarom games geschikt zijn als trainingsmateriaal: "Het beeldmateriaal is altijd vanuit hetzelfde perspectief geschoten, dus het is heel stabiel materiaal. Daarnaast speel je zo'n game met een console. In de echte wereld worden robots met zo'n zelfde gameconsole bestuurd." OpenAI zag dit ook en meldde zich bij De Witte. Althans, zo berichten meerdere media. De Witte kan niet bevestigen dat dat bedrijf hem een bedrag van 500 miljoen dollar bood, "maar ik kan wel vertellen hoe het is om 500 miljoen af te slaan", zegt hij met een glimlach. "Je kijkt jezelf even goed in de spiegel, geeft een paar klappen in je gezicht met wat water en dan pak je de telefoon op." In dat telefoongesprek zei hij 'nee' tegen een bedrag waar menig ondernemer alleen maar van kan dromen. "Toen het gebeurd was, dacht ik meteen: mijn God, heb ik wel de goede beslissing genomen?" 'Leuker om het zelf te doen' De Witte besloot: wat de Amerikanen kunnen, kan ik zelf ook. De data waar Medal.tv over beschikt is volgens De Witte namelijk zó uniek, dat hij verwacht een fikse voorsprong te hebben bij het verder ontwikkelen van deze AI-technologie. "We zijn aan de slag gegaan en kwamen erachter dat we ze kunnen inhalen. En ja, dan kun je meer waard worden dan wat zij betalen, toch? En het is gewoon leuker om het zelf te doen." De Witte groeide op met het syndroom Gilles de la Tourette, een neuro-psychiatrische aandoening: Een half jaar na het miljoenenaanbod staat De Witte nog altijd achter zijn beslissing. Investeerders staan in de rij voor zijn nieuwe bedrijf General Intuition. Met dat bedrijf werkt De Witte nu zelf aan het ontwikkelen van AI-systemen die ruimtelijk begrip nabootsen, de zogeheten 'wereldmodellen'. In korte tijd heeft De Witte al ruim 130 miljoen dollar opgehaald, en investeerders zouden bereid zijn nog honderden miljoenen dollars te investeren. "Er wordt veel verwacht van deze modellen, maar het is nog steeds een enorme belofte", zegt hoogleraar Nas. "En of die belofte wordt waargemaakt in AI-land, moet je altijd nog maar zien." Concurrentie China en VS De ontwikkelingen op het gebied van AI zijn tot nu toe vooral een Amerikaanse en Chinese aangelegenheid. De Witte hoopt daar met zijn nieuwe bedrijf verandering in te brengen. "Ik denk dat we een hele goede kans hebben vanuit Europa, en dat we vanuit Nederland een van de beste kansen hebben." Ook hoogleraar Nas is hoopvol. "Ik denk dat deze technologie zeker een kans biedt voor Europa, want we hebben veel talent. Je hebt voor de ontwikkeling van dit soort modellen veel wiskunde en natuurkunde nodig en daar hebben wij hele goede universitaire opleidingen voor in Europa." Kijk ook deze video van Nieuwsuur over het menselijke leed achter de race naar kunstmatige intelligentie:
12. 21/12 in Nieuwsuur: Levensgevaarlijk illegaal vuurwerk • Nederlandse tech-belofte weigert half miljard
Nieuwsuur begint vanavond nog om 21.30 uur, maar vanaf 2 januari wijzigt ons aanvangstijdstip naar 22.00 uur. Nog wel op de vertrouwde zender: NPO 2. Opslag illegaal vuurwerk Oud en nieuw komt eraan en het is de laatste keer dat je in Nederland legaal vuurwerk kan kopen. Toch komt ook het illegale vuurwerk weer in grote aantallen de grens over. Dat vuurwerk ligt vaak opgeslagen in woonwijken, met grote veiligheidsrisico's tot gevolg. De Rotterdamse burgemeester Carola Schouten en Ko Minderhoud , vuurwerkcoördinator bij de politie, maken zich daar grote zorgen over. Ze zijn te gast. Tech-ondernemer Pim de Witte Het gebeurt niet elke dag dat een Nederlandse ondernemer nee zegt tegen 500 miljoen dollar. Toch is dat wat Pim de Witte deed toen het bedrijf achter ChatGPT hem dit astronomische bedrag bood voor zijn bedrijf. De Witte is eigenaar van een videoplatform waar gamers beelden van hun spellen delen. Die beelden plus bijbehorende data spelen een sleutelrol bij het trainen van AI. De Witte besloot zelf aan de slag te gaan, in de hoop als Nederlandse ondernemer een rol te spelen bij het verder ontwikkelen van kunstmatige intelligentie. Wie is deze opvallende tech-ondernemer uit Nijmegen?
13. Amerikaanse verkoop TikTok rond, algoritme voortaan getraind met data VS
TikTok heeft een oplossing gevonden om in de VS te kunnen blijven opereren. Het Amerikaanse deel van het sociale medium komt grotendeels in handen van drie grote investeerders, waardoor bezwaren van de Amerikaanse politiek worden weggenomen. Tijdens zijn eerste termijn drong president Trump aan op een verbod op TikTok, uit angst dat de persoonlijke gegevens van vele tientallen miljoen Amerikaanse gebruikers in handen van de Chinese overheid terecht zouden kunnen komen. Ook bestond de vrees dat China de Amerikaanse publieke opinie zou kunnen beïnvloeden door het algoritme van de app onwelgevallige boodschappen te laten censureren. Onder president Biden stemde het Amerikaanse Congres in met een verbod zolang de Amerikaanse tak van het bedrijf nog in Chinese handen was. De app ging ook daadwerkelijk een kleine maand op zwart, maar na zijn aantreden dit jaar gaf Trump moederbedrijf ByteDance extra tijd om tot een vergelijk te komen. De app werd zolang gedoogd, hoewel Trump daar officieel geen wettelijke bevoegdheid voor had. Amerikaanse servers In september liet Trump weten dat er in grote lijnen overeenstemming zou zijn over de verkoop, al zweeg China toen nog. Destijds werden Oracle en investeringsmaatschappij Silver Lake al genoemd, daar blijkt nu AI-bedrijf MGX uit Abu Dhabi bij te zijn gekomen. Zij krijgen gezamenlijk bijna de helft van het Amerikaanse TikTok in handen, 19,9 procent blijft eigendom van ByteDance en de rest wordt aangevuld met bestaande investeerders van ByteDance. De gegevens van Amerikaanse gebruikers worden voortaan opgeslagen op servers in de VS. Ook wordt het algoritme van TikTok opnieuw getraind met Amerikaanse data, om buitenlandse manipulatie te voorkomen. Hoeveel geld de investeerders uittrekken voor de overname is niet bekendgemaakt. De leiding van TikTok USDS Joint Venture LLC komt in handen van een zevenkoppige raad van bestuur met een meerderheid van Amerikanen. Het Witte Huis heeft nog niet gereageerd op de deal, maar verwijst naar ByteDance. Dat bedrijf zegt dat op deze manier gegarandeerd is dat "170 miljoen Amerikanen een wereld van eindeloze mogelijkheden kunnen blijven ontdekken als onderdeel van een levendige wereldwijde gemeenschap". De deal zou op 22 januari ingaan. TikTok heeft een oplossing gevonden om in de VS te kunnen blijven opereren. Het Amerikaanse deel van het sociale medium komt grotendeels in handen van drie grote investeerders, waardoor bezwaren van de Amerikaanse politiek worden weggenomen. Tijdens zijn eerste termijn drong president Trump aan op een verbod op TikTok, uit angst dat de persoonlijke gegevens van vele tientallen miljoen Amerikaanse gebruikers in handen van de Chinese overheid terecht zouden kunnen komen. Ook bestond de vrees dat China de Amerikaanse publieke opinie zou kunnen beïnvloeden door het algoritme van de app onwelgevallige boodschappen te laten censureren. Onder president Biden stemde het Amerikaanse Congres in met een verbod zolang de Amerikaanse tak van het bedrijf nog in Chinese handen was. De app ging ook daadwerkelijk een kleine maand op zwart, maar na zijn aantreden dit jaar gaf Trump moederbedrijf ByteDance extra tijd om tot een vergelijk te komen. De app werd zolang gedoogd, hoewel Trump daar officieel geen wettelijke bevoegdheid voor had. Amerikaanse servers In september liet Trump weten dat er in grote lijnen overeenstemming zou zijn over de verkoop, al zweeg China toen nog. Destijds werden Oracle en investeringsmaatschappij Silver Lake al genoemd, daar blijkt nu AI-bedrijf MGX uit Abu Dhabi bij te zijn gekomen. Zij krijgen gezamenlijk bijna de helft van het Amerikaanse TikTok in handen, 19,9 procent blijft eigendom van ByteDance en de rest wordt aangevuld met bestaande investeerders van ByteDance. De gegevens van Amerikaanse gebruikers worden voortaan opgeslagen op servers in de VS. Ook wordt het algoritme van TikTok opnieuw getraind met Amerikaanse data, om buitenlandse manipulatie te voorkomen. Hoeveel geld de investeerders uittrekken voor de overname is niet bekendgemaakt. De leiding van TikTok USDS Joint Venture LLC komt in handen van een zevenkoppige raad van bestuur met een meerderheid van Amerikanen. Het Witte Huis heeft nog niet gereageerd op de deal, maar verwijst naar ByteDance. Dat bedrijf zegt dat op deze manier gegarandeerd is dat "170 miljoen Amerikanen een wereld van eindeloze mogelijkheden kunnen blijven ontdekken als onderdeel van een levendige wereldwijde gemeenschap". De deal zou op 22 januari ingaan.
4. Hackers dreigen klantgegevens Pornhub te publiceren en eisen losgeld 14. Hackers dreigen klantgegevens Pornhub te publiceren en eisen losgeld
Hackers van de groep ShinyHunters dreigen gegevens van klanten van de pornowebsite Pornhub te publiceren. Het gaat om data van klanten die een premium abonnement hebben of hadden bij de website, waarmee ze video's in hogere resolutie en zonder advertenties kunnen bekijken. De hackersgroep zegt zo'n 200 miljoen gegevens zoals zoek-, kijk- en downloadactiviteiten van premiumklanten te hebben buitgemaakt, schrijft cybersecuritywebsite bleepingcomputer . De hackers eisen losgeld in Bitcoin, zeggen ze tegen persbureau Reuters. Als dat wordt betaald, publiceren de hackers de data niet en worden de gegevens verwijderd, zeggen ze. Gegevens 'paar jaar oud' ShinyHunters heeft een deel van de gegevens met het persbureau gedeeld. Reuters heeft deze data aan enkele gebruikers voorgelegd. Zij erkennen dat ze op een zeker moment een premiumabonnement bij de pornowebsite hadden en bevestigen dat het om hun klantgegevens gaat. Wel tekenen ze daarbij aan dat de gegevens "een paar jaar oud" zijn. Volgens de hackersgroep hebben ze de gegevens over het kijkgedrag van de gebruikers gestolen van het analyseplatform Mixpanel, waar Pornhub mee werkte. Het is niet duidelijk hoeveel gegevens er zijn buitgemaakt. Ethical Capital Partners, het bedrijf dat eigenaar is van de pornowebsite, heeft niet gereageerd op berichten over de hack. Wel maakte het bedrijf vorige week bekend dat er een incident was geweest met de beveiliging van het analyseplatform waar de pornowebsite mee werkte. De hackersgroep stelt dat de diefstal van de Pornhub-gegevens gerelateerd is aan dat incident. 36 miljard bezoeken per jaar De hackersgroep heeft vaker bedrijven afgeperst nadat gegevens waren gestolen. Zo zei ShinyHunters in 2024 dat het buitgemaakte persoonlijke gegevens zoals namen, adressen en aankoopgegevens van 560 miljoen klanten van Ticketmaster in handen had. De groep eiste toen ook losgeld. Daarnaast hadden ze de gegevens te koop aangeboden voor een half miljoen dollar. Pornhub is een van de bekendste pornowebsites ter wereld. De site wordt naar eigen zeggen 100 miljoen keer per dag en 36 miljard keer per jaar bezocht. Hackers van de groep ShinyHunters dreigen gegevens van klanten van de pornowebsite Pornhub te publiceren. Het gaat om data van klanten die een premium abonnement hebben of hadden bij de website, waarmee ze video's in hogere resolutie en zonder advertenties kunnen bekijken. De hackersgroep zegt zo'n 200 miljoen gegevens zoals zoek-, kijk- en downloadactiviteiten van premiumklanten te hebben buitgemaakt, schrijft cybersecuritywebsite bleepingcomputer . De hackers eisen losgeld in Bitcoin, zeggen ze tegen persbureau Reuters. Als dat wordt betaald, publiceren de hackers de data niet en worden de gegevens verwijderd, zeggen ze. Gegevens 'paar jaar oud' ShinyHunters heeft een deel van de gegevens met het persbureau gedeeld. Reuters heeft deze data aan enkele gebruikers voorgelegd. Zij erkennen dat ze op een zeker moment een premiumabonnement bij de pornowebsite hadden en bevestigen dat het om hun klantgegevens gaat. Wel tekenen ze daarbij aan dat de gegevens "een paar jaar oud" zijn. Volgens de hackersgroep hebben ze de gegevens over het kijkgedrag van de gebruikers gestolen van het analyseplatform Mixpanel, waar Pornhub mee werkte. Het is niet duidelijk hoeveel gegevens er zijn buitgemaakt. Ethical Capital Partners, het bedrijf dat eigenaar is van de pornowebsite, heeft niet gereageerd op berichten over de hack. Wel maakte het bedrijf vorige week bekend dat er een incident was geweest met de beveiliging van het analyseplatform waar de pornowebsite mee werkte. De hackersgroep stelt dat de diefstal van de Pornhub-gegevens gerelateerd is aan dat incident. 36 miljard bezoeken per jaar De hackersgroep heeft vaker bedrijven afgeperst nadat gegevens waren gestolen. Zo zei ShinyHunters in 2024 dat het buitgemaakte persoonlijke gegevens zoals namen, adressen en aankoopgegevens van 560 miljoen klanten van Ticketmaster in handen had. De groep eiste toen ook losgeld. Daarnaast hadden ze de gegevens te koop aangeboden voor een half miljoen dollar. Pornhub is een van de bekendste pornowebsites ter wereld. De site wordt naar eigen zeggen 100 miljoen keer per dag en 36 miljard keer per jaar bezocht.
15. Ongeveer 45 datacenters gebruiken net zo veel stroom als 1,9 miljoen woningen
Ongeveer 45 datacenters in Nederland gebruiken net zoveel stroom als bijna 1,9 miljoen woningen. Dat meldt het Centraal Bureau voor de Statistiek ( CBS ). De datacenters verbruiken steeds meer elektriciteit: in 2017 ging het om 1,2 procent van het totale elektriciteitsverbruik, vorig jaar was dat 4,2 procent. Datacenters hebben de elektriciteit nodig voor de computerservers. Daarop kunnen bestanden opgeslagen worden of computerprogramma's draaien. Omdat de servers zoveel stroom gebruiken, geven ze ook veel warmte af. Daarom gebruiken datacenters systemen die de ruimtes koelen, die ook weer extra stroom verbruiken. Vooral door kunstmatige intelligentie gebruiken datacenters wereldwijd steeds meer stroom, zegt Alex de Vries-Gao. Hij doet aan de Vrije Universiteit Amsterdam onderzoek naar energieverbruik van technologie. "Om kunstmatige intelligentie te ontwikkelen, is relatief veel rekenkracht nodig van computers in de datacenters." Grote datacenters Het CBS keek voor de cijfers naar ongeveer 45 datacenters die elk jaar meer dan 10 gigawattuur (GWh) elektriciteit verbruiken. Deze grote datacenters gebruikten vorig jaar samen 4720 GWh aan stroom. Dat is meer dan drie keer zoveel als in 2017. Het aantal grote datacenters is de afgelopen jaren ongeveer gelijk gebleven, maar het elektriciteitsverbruik van deze centers is wel toegenomen. Op het moment dat een datacenter zich in Nederland vestigt, neemt de vraag naar stroom flink toe. "Maar als het aanbod tegelijkertijd niet toeneemt, krijgen huishoudens een hogere energierekening", zegt Roel Dobbe. Hij is AI- en veiligheidsonderzoeker aan de TU Delft. Ook zijn investeringen nodig om de capaciteit van het stroomnet uit te breiden. "Dat zijn kosten die op de samenleving worden verhaald." Zonder die investeringen moeten er keuzes worden gemaakt, want er kan niet zomaar iets bij, stelt Dobbe. "Als we prioriteit zouden geven aan datacenters dan gaat dat ten koste van andere sectoren of van bijvoorbeeld woningen."
16. Clair Obscur: Expedition 33 recordwinnaar Game Awards met negen prijzen
Videogame Clair Obscur: Expedition 33 is vannacht uitgeroepen tot Game of the Year tijdens The Game Awards, de belangrijkste prijzen in de gamesindustrie. De game won in totaal negen prijzen, een nieuw record. Het werd naast de hoofdprijs bekroond in de categorieën beste verhaal, beste gameregie, beste art direction, beste muziek, beste onafhankelijke game, beste indiegamedebuut, beste acteerprestatie en beste role-playing game (rpg). Het vorige record voor meeste gewonnen prijzen bij The Game Awards stamt uit 2020, toen won The Last of Us Part Two er zeven. Role-playing game Clair Obscur: Expedition 33 is een role-playing game die zich afspeelt in een fantasiewereld die geïnspireerd is door de belle époque, de periode in Frankrijk en Europa aan het einde van de 19e eeuw tot aan de Eerste Wereldoorlog. In het duistere spel heeft het mysterieuze wezen The Paintress ervoor gezorgd dat iedereen boven een bepaalde leeftijd (die steeds lager wordt), in rook opgaat. Vrijwilligers van de zogenoemde Expedition 33 werpen zich vervolgens op om haar te vernietigen. Dat de game zo veel Game Awards heeft gewonnen is weinig verrassend: zowel door het publiek als critici werd het spel geprezen. Zo is op Metacritic te lezen dat kenners de game met een ruime 9 beloonden. Ook gameplatform Power Unlimited gaf het een "uitstekende" score (89). Ook gamejournalist Ron Vorstermans was niet verbaasd dat de game in de prijzen viel. Hij heeft de uitreiking live gekeken. "De game is gemaakt door een kleine studio, die eigenlijk uit het niets met zo'n goed spel kwam", zegt hij. Toen het verscheen "stond de wereld echt even stil", legt hij uit, "Het is echt van een heel hoog niveau." De game-industrie is het grootste onderdeel van de entertainmentindustrie, zegt de gamejournalist. "Er wordt door iedereen uitgekeken naar GTA 6 , en dat een kleine studio uit Frankrijk dan komt met een game waarmee ze de halve wereld inpakken, dat is te gek." Kleine studio De game is het debuut van de Franse gameontwikkelaar Sandfall Interactive. De oprichter daarvan werkte voorheen voor de grote game-multinational Ubisoft, maar wilde een nieuwe creatieve uitdaging omdat hij zich daar verveelde. Daarom begon hij het relatief kleine Sandfall, waar hij begon met de ontwikkeling van de rpg. Het spel werd ontwikkeld met een team van dertig mensen. Ook buiten de gamewereld heeft het spel impact, zegt Vorstermans. "De muziek is bijvoorbeeld heel goed: het staat ook hoog op Spotify." Het bijzondere daaraan is dat ze de producer van de muziek "gewoon op Soundcloud" hebben gevonden, en hem vervolgens hebben benaderd om de muziek voor het spel te ontwikkelen". "En dat diegene nu op deze awards een prijs pakt, dat is heel bijzonder."
17. Mickey en Yoda straks in videotool Sora: Disney investeert miljard in OpenAI
Zelf video's door Sora genereren met daarin figuren als Mickey Mouse, Darth Vader of Iron Man. Dat kan binnenkort: The Walt Disney Company heeft een driejarige overeenkomst gesloten met OpenAI, het bedrijf achter chatbot ChatGPT en video-generator Sora. Disney investeert een miljard dollar in het techbedrijf en staat het gebruik van zijn karakters toe. De tweede versie van de video-generator Sora werd in oktober gepresenteerd . Gebruikers kunnen zelf met 'prompts', (opdrachten in tekst) aan de AI-tool vragen om video's te laten maken. De belofte bij de tweede editie was dat video's nog realistischer werden. Meteen gingen levensechte video's viraal van Ronald McDonald die aan de politie ontvlucht of Pikachu bij de landingen van D-day. Disney had Sora toen nog verboden gebruikt te maken van zijn intellectuele eigendommen, maar stelt nu ruim 200 karakters beschikbaar van Disney en Pixar zelf en andere series waar het bedrijf de rechten van heeft, zoals Marvel en Star Wars. Bijvoorbeeld Belle, Stitch, Mufasa, Captain America, Thor en Han Solo zijn dus te gebruiken in video's die door Sora worden gegenereerd. Disney en OpenAI zijn ook overeengekomen dat een selectie van de films die gebruikers laten genereren, te zien zullen zijn via de streamingdienst Disney+. Google gesommeerd Disney wordt ook zelf een grote klant van OpenAI. De entertainmentgigant gaat tools van het techbedrijf gebruiken om nieuwe producten, diensten en "ervaringen" te bouwen, is te lezen in het persbericht . Ook zullen medewerkers van Disney gebruik gaan maken van AI-hulp ChatGPT. Hoewel Disney dus een samenwerking aangaat met het ene techbedrijf, gaat het juist de strijd aan met het andere. Afgelopen woensdag sommeerde het bedrijf Google nog te stoppen met het gebruiken van intellectueel eigendom voor het trainen van zijn AI-modellen. Disney stelt dat Google het auteursrecht daarmee misbruikt. Disney stelt met OpenAI dat verantwoord gebruik van AI belangrijk is. Ze schrijven oog te hebben voor bescherming van gebruikers en de rechten van makers. Auteursrecht Over dat laatste is sinds de opkomst van AI veel te doen. Zo leek het erop dat AI-modellen van onder meer OpenAI zijn getraind met beelden van Nederlandse makers, die vinden dat er inbreuk wordt gemaakt op het auteursrecht. En in Duitsland oordeelde een rechter dat ChatGPT het auteursrecht op songteksten schendt . AI-plaatjesmakers Midjourney werd dit jaar aangeklaagd door filmstudio Warner Bros. vanwege copyrightschending. Zo konden gebruikers makkelijk filmpjes en afbeeldingen genereren met superhelden zoals Batman, terwijl Warner Bros. het alleenrecht daarover heeft. Niet alleen beelden en stemmen, ook teksten van schrijvers worden gebruikt door AI-generatoren. Zo moest het Amerikaanse AI-bedrijf Anthropic dit jaar voor 1,5 miljard dollar schikken met een groep auteurs, omdat het bedrijf chatbots had getraind met boeken die illegaal gedownload waren. De muziektak van Warner sloot vorige maand een deal met AI-muziekgenerator Suno, nadat de twee bedrijven eerder met elkaar overhoop lagen. Warner Music, waar onder anderen Coldplay en Ed Sheeran onder contract staan, had Suno aangeklaagd vanwege schending van de auteursrechten. Door deze overeenkomst kunnen stemmen van artiesten die daarvoor kiezen gebruikt worden voor het genereren van muziek via de AI-generator. Kinderen lokken Er klinkt ook kritiek op de samenwerking tussen OpenAI en Disney. Zo zegt Fairplay, een Amerikaanse organisatie die kinderen wil beschermen tegen marketing en Big Tech, tegen persbureau AP dat OpenAI ,"kinderen met Disney-karakters probeert te lokken naar het platform". Volgens Fairplay probeert het bedrijf hen zo verslaafd te maken aan zijn producten. Wat nog niet duidelijk is, is hoe de karakters van Disney in Sora-video's zullen klinken. In de overeenkomst is afgesproken dat stemmen van de karakters die door stemacteurs zijn ingesproken, niet gebruikt mogen worden door de video-generator.
18. Meta blokkeert tientallen queer- en abortus-accounts, zonder uitleg
Het moederbedrijf van de socialemediaplatforms WhatsApp, Facebook en Instagram heeft de afgelopen weken stilletjes zo'n 50 accounts verwijderd of geblokkeerd die draaien om lhbti-, queer- of abortus-onderwerpen. Daarbij zegt het techbedrijf dat die accounts de regels van de platforms hebben overtreden, maar levert daarbij geen hard bewijs, zo meldt The Guardian op basis van een eigen inventarisatie. Onder de slachtoffers zit ten minste één Nederlands account, het Amsterdamse The Queer Agenda. Dat brengt nieuws over queer-feestjes en foto-exposities. Oprichter Jackie van Gemert zegt dat hun Instagram-account ineens geblokkeerd werd door Meta, zonder dat er uitleg gegeven werd over de reden. Niet veel later was het account zelfs helemaal verwijderd, net als het privé-account van Van Gemert en een van haar oud-collega's. De organisatie is daarmee ineens 11.000 volgers verloren. Van Gemert: "Je bent ineens je werk en de toegang tot je community kwijt." Mensenhandel? Uit een screenshot dat Van Gemert deelde met de NOS blijkt dat een geautomatiseerd systeem van Meta The Queer Agenda heeft aangemerkt als een account dat de regels overtrad, zoals het "promoten van mensenhandel voor seksuele doeleinden". Zij zelf denkt dat het systeem is aangeslagen op foto's van mensen in een club met weinig kleding aan. Maar, zo zegt ze, "ik weet niet wat daar anders aan zou zijn dan influencers in bikini op het strand." En die worden op Insta gewoon toegestaan. Meta zegt in een reactie aan The Guardian dat de regels op zijn sociale media voor iedereen gelijk zijn, en ook zo worden gehandhaafd. Maar het lijkt er toch op dat Meta het specifiek gemunt heeft op queer- en lhbti-accounts en accounts die beheerd worden door sekswerkers of die informatie bieden over toegang tot abortus. Dat zegt Repro Uncensored , een organisatie die gevallen van censuur bijhoudt op sociale media, vooral op het gebied van gender en seksualiteit. Het probleem is daarbij, zo zegt die organisatie, dat Meta een systeem van kunstmatige intelligentie gebruikt om overtredingen van de eigen regels vast te stellen, bijvoorbeeld als het gaat om naakt of mensenhandel. Maar dat systeem wordt gemanipuleerd doordat tegenstanders van abortus of genderdiversiteit campagnes aan het voeren zijn om zulke accounts massaal te rapporteren als accounts die de regels overtreden. Conservatieve koerswijziging Lotje Beek van burgerrechtenorganisatie Bits of Freedom (BOF) noemt het in een reactie "schandalig" dat er tientallen accounts zoals The Queer Agenda de afgelopen tijd zijn geblokkeerd, maar zegt dat het "in lijn ligt met wat we vaker zien bij Meta. Vlak na de verkiezing van Trump bleek al dat mensen niet meer konden zoeken op de term '#democrats', en bepaalde abortus-accounts werden eerder ook al verwijderd." Net als Van Gemert legt zij de link met het tweede presidentschap van Trump, en de conservatieve koerswijziging die Meta-topman Mark Zuckerberg begin dit jaar aankondigde. Voor de duidelijkheid, zegt Beek, "Meta mag regels stellen op de eigen platforms, zo lang dat in lijn is met de wet. Als het gaat om bijvoorbeeld kinderporno móet het bedrijf zelfs ingrijpen. Maar ook zaken die wettelijk toegestaan zijn, zoals bijvoorbeeld naaktfoto's, mogen geweerd worden als het bedrijf dat niet passend vindt op de eigen platforms." Maar, zo zegt Beek, als er accounts worden geblokkeerd moet dat wel altijd goed gemotiveerd worden. Én er moet een mogelijkheid zijn voor de eigenaars van zulke accounts om bezwaar te maken. En dat doet het bedrijf nu niet goed. 'Europese Commissie moet ingrijpen' Beek wijst erop dat de blokkade van sommige queer- en abortus-accounts weer werd opgeheven, nadat de eigenaars hadden geklaagd bij Meta. In zulke gevallen gaf het bedrijf vaak de eigen automatische controle op overtredingen de schuld van zulke onterechte verbanningen. Beek wijst erop dat "het wel erg vaak progressieve platforms zijn, die de dupe worden van zulke 'technische fouten". Hoe dan ook is het in strijd met Europese wetgeving als socialemedia-accounts of posts met een bepaalde ideologische of politieke insteek strenger beoordelen dan andere. "De Europese Commissie moet hier ingrijpen: de wet moet worden nageleefd, en anders moeten er sancties worden opgelegd aan Meta." 'Vertrouw niet op andermans platform' Dat kan nog wel even duren, erkent Beek, en haar tip aan organisaties zoals The Queer Agenda is dan ook: "Probeer niet te afhankelijk te zijn van één platform, en probeer mensen te trekken naar je eigen platform of site, zodat je je bereik zelf in de hand hebt." Dat is ook exact wat The Queer Agenda van plan is, zegt Van Gemert: die organisatie heeft inmiddels een eigen website in de lucht, en denkt nu ook aan groepen op Signal, een non-profitconcurrent van WhatsApp. "Daarnaast willen we offline beginnen, doormiddel van bijvoorbeeld een magazine."
19. Computers dreigen veel duurder te worden door prijsstijging van één onderdeel
Smartphones, laptops en desktopcomputers dreigen de komende maanden flink duurder te worden. Dat komt doordat een belangrijk onderdeel de laatste tijd enorm in prijs stijgt: het werkgeheugen. Dat computeronderdeel is belangrijk voor de rekenkracht van het apparaat: met te weinig werkgeheugen is een telefoon of laptop veel trager. Een smartphone van 300 of 400 euro is over een aantal maanden misschien wel 50 euro duurder, verwacht Tomas Hochstenbach van techsite Tweakers, die de ontwikkelingen volgt. Ook een laptop kan zomaar 100 euro duurder worden, denkt hij. Inkopers die de NOS sprak, verwachten ook prijsstijgingen in de komende maanden. Drie keer zo duur "Werkgeheugen is de afgelopen twee maanden ongeveer drie keer zo duur geworden", zegt Hochstenbach als hij kijkt naar de gegevens op Pricewatch. Dat is een platform van Tweakers waarop de prijs van allerlei elektronica wordt bijgehouden: van smartphones en laptops, maar ook specifieke onderdelen zoals werkgeheugen. "Dit werkgeheugen wordt opgekocht door andere bedrijven", legt Hochstenbach uit. "Zij hebben het nodig voor datacenters. Die groeien enorm door kunstmatige intelligentie. Daar is de vraag enorm, waardoor tekorten ontstaan voor bijvoorbeeld laptops." Wat is werkgeheugen? Elke computer heeft werkgeheugen nodig om taken uit te voeren. De hoeveelheid werkgeheugen wordt uitgedrukt in gigabytes (GB). Laptops hebben vaak een werkgeheugen van 16 GB. Op een smartphone is dat vaak 4, 6 of 8 GB - hoewel de duurste smartphones soms ook 16 GB werkgeheugen hebben. "Met een vol werkgeheugen wordt je apparaat heel langzaam", zegt Hochstenbach. "Dat merk je bijvoorbeeld als je meerdere apps of computerprogramma's tegelijk gebruikt. Of als je computer op de achtergrond een update installeert." In krachtige laptops of desktopcomputers is het werkgeheugen 32 GB. "Zoveel werkgeheugen is bijvoorbeeld interessant voor gamers", zegt Hochstenbach. "Dat komt doordat veel games er tegenwoordig erg realistisch uitzien. Het kost veel rekenkracht om de beelden van zo'n virtuele wereld te verwerken." Sommige gamers kopen de onderdelen voor rekenkracht daarom los. Daarmee kunnen zij hun eigen game-pc bouwen of uitbreiden. "We zien dat 32 GB het populairst is", zegt Hochstenbach. Daarvan is de gemiddelde prijs van vier merken dit jaar al honderden euro's gestegen, blijkt uit gegevens van Pricewatch. Op 1 januari kostte 32 GB nog gemiddeld 111 euro, vorige week was dat 352 euro. "En de afgelopen dagen is het nog verder gestegen." "Fabrikanten waarschuwden ons in de zomer al dat zij de vraag niet aankunnen", zegt Adri Broos van Megekko, een inkoper van computers en onderdelen, waaronder modules voor rekenkracht. "Dat horen we wel vaker in de zomer, dus we dachten dat het wel zou meevallen. Maar deze extreme prijsstijgingen had ik niet verwacht. Mensen die zelf een computer willen bouwen, hebben daar nu al last van." Een van hen is Sergio Meyer uit Rotterdam. Hij bouwt elk jaar zijn eigen game-pc, vertelt hij aan de NOS. "Ik vind het leuk om te doen. Het is een beetje een uit de hand gelopen hobby." Hij had zijn oude pc al verkocht en wilde aan een nieuwe beginnen. Maar toen zag hij de prijs van het werkgeheugen: 400 euro. "Ik dacht eerst dat het een fout was. Maar het is wel echt de prijs die je nu betaalt." Dat betekent nu even geen nieuwe game-pc. "Als je dit onderdeel écht nodig hebt, dan moet je dit bedrag wel betalen, maar voor mij is het wel te duur. Ik wacht liever nog even. Dus game ik nu iets minder en ik heb meer tijd voor andere dingen." 'Laptops worden straks duurder' Broos verwacht dat de prijs van laptops en desktops binnenkort ook stijgt. "De pc's die nu in de winkel liggen, zijn drie of vier maanden geleden gebouwd, met de lage prijs voor werkgeheugen." Eind november zei computermaker HP al dat het duurdere werkgeheugen zal leiden tot duurdere laptops en desktops. Broos: "Laptops en desktops die straks in de winkel liggen, zijn gebouwd met de hoge geheugenprijzen van nu. En die zullen dus een stuk duurder zijn." Pas wanneer de prijs van het werkgeheugen zakt, zullen na verloop van tijd ook de apparaten goedkoper worden. "De prijs van werkgeheugen werkt een maand of drie door in producten waar die onderdelen in gaan", zegt Broos. Hij verwacht dat de prijzen op zijn vroegst pas over een halfjaar op hun normale niveau zijn. "Eén ding is zeker: de prijs van werkgeheugen zal de komende weken echt niet dalen."
20. X krijgt boete van 120 miljoen, onder meer om misleidende blauwe vinkjes
De Europese Commissie geeft het sociale medium X (voorheen Twitter) boetes van in totaal 120 miljoen euro voor drie overtredingen van een grote Europese internetwet. X krijgt onder meer een boete omdat het EU-bestuur vindt dat het 'blauwe vinkje' bij gebruikersnamen misleidend is. Onder Twitter was een blauw vinkje een manier waaruit bleek dat de persoon achter het account echt was. Zo kregen bedrijven, film- en muzieksterren, politici, sporters en andere bekende namen het symbool naast hun gebruikersnaam. Nadat ondernemer Elon Musk (ook directeur van autobedrijf Tesla en ruimtevaartbedrijf SpaceX) Twitter in 2022 overnam en de naam veranderde in X, veranderde het bedrijf de betekenis ervan. In plaats van een teken van echtheid, kan het blauwe vinkje geplaatst worden bij iedere gebruiker met een betaald abonnement. 'Misleiding' Volgens de Europese Commissie misleidt het bedrijf zijn gebruikers met deze aanpassing. "Het laat ze denken dat er een echte, geverifieerde gebruiker achter het account met het blauwe vinkje zit", zegt een EU-ambtenaar. Dat is volgens het EU-bestuur een overtreding van de Digital Services Act (DSA), een van de grote Europese internetwetten die sinds 2022 van kracht zijn. Voor deze overtreding krijgt X een boete van 45 miljoen euro. Het onderzoek werd in december 2023 aangekondigd. Musk heeft eerder al gezegd dat hij een besluit hierover zal aanvechten in de rechtszaal. Het bedrijf moet daarvoor naar het Europees hof in Luxemburg. De Europese Commissie geeft X ook een boete omdat het bedrijf volgens het bestuur niet transparant genoeg is over reclames. Zo moet X volgens de DSA een overzicht publiceren van advertenties die op het platform zichtbaar zijn. Het doel daarvan is om inzicht te kunnen krijgen in advertenties die oplichters plaatsen, of rond politieke boodschappen. Daarnaast vindt de Commissie dat X onderzoekers en wetenschappers onvoldoende toegang geeft om berichten op het sociale netwerk te bestuderen. Dat moet ervoor zorgen dat onderzoekers kunnen bekijken hoe bepaalde onderwerpen zich online verspreiden, zoals berichten rond eetstoornissen, jodenhaat of oplichting. De Europese Commissie oordeelt dat X ook op deze twee punten de DSA overtreedt. Daarom krijgt het bedrijf twee boetes van 35 miljoen en van 40 miljoen euro. Het totaalbedrag van de drie boetes komt daarmee uit op 120 miljoen euro. Het is de eerste keer dat de Europese Commissie een boete uitdeelt voor overtreding van de Digital Services Act. Eerder kregen Amerikaanse techbedrijven al boetes van honderden miljoene euro's onder een andere grote Europese internetwet, de Digital Markets Act. EU-redacteur Roemer Ockhuijsen: "De boete zal niet goed vallen in de Verenigde Staten. De EU en de VS liggen al maanden overhoop over Europese techregels. Volgens de Amerikaanse regering is de EU veel te streng voor hun techbedrijven. Vicepresident JD Vance plaatste gisteren, nog voor de boete officieel was, een bericht op X waarin hij refereerde aan de boete en zei dat "Europa niet Amerikaanse bedrijven aan moet vallen vanwege onzin". Vorige week bracht de Amerikaanse handelsminister Lutnick een bezoek aan Brussel. Lutnick zei daar dat de techregels van tafel moeten als de EU lagere importheffingen op aluminium en staal wil. Zo zet de VS de EU constant onder druk. Vooralsnog houdt de EU voet bij stuk. De Europese Commissie benadrukt keer op keer: over de techregels valt niet te onderhandelen." Op dit moment lopen er nog twee andere DSA-onderzoeken naar X. Een daarvan gaat erover of X genoeg doet om illegale berichten tegen te gaan. Het andere gaat over de manier waarop X-gebruikers een kader kunnen plaatsen om context te geven bij berichten die mogelijk onjuist zijn. Het is onduidelijk wanneer de Europese Commissie hierover beslist.

View File

@@ -1,32 +1,10 @@
VEE HAAGS
KALFJES EXPLOSIE
PRIKSTOK DREIGEND
CBR BRIEFJES
ALCOHOL BETALEN
DRUGS KESTER
VERKEER MAARTEN
POLITIE DADER
WIJN SPOREN
BIER ZWEEDS
GLAS
EENHEID
LEVER
SYSTEEM
COCAINE
XTC
SPEEKSEL
BLOED
DATA
VS
TIKTOK
ORACLE
DEAL
JANUARI
HACKERS
VIEW
DOWNLOAD
BITCOIN
REUTERS
BEDRIJF
INCIDENT
KLANTEN

View File

@@ -1,4 +1,9 @@
package puzzle; package puzzle;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
public class Main { public class Main {
// ---------------- CLI ---------------- // ---------------- CLI ----------------
@@ -68,5 +73,72 @@ public class Main {
System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n", System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n",
w.word(), w.direction(), w.startRow(), w.startCol(), w.arrowRow(), w.arrowCol()); w.word(), w.direction(), w.startRow(), w.startCol(), w.arrowRow(), w.arrowCol());
} }
// Export to JSON file
var dateStr = LocalDate.now().toString();
var theme = "algemeen";
var filename = String.format("crossword_%s_%02d_%s.json", dateStr, 1, safeSlug(theme));
var outDir = "data";
var outputPath = Paths.get(outDir, filename);
try {
Files.createDirectories(Paths.get(outDir));
var json = toJson(out, dateStr, theme);
Files.writeString(outputPath, json, StandardCharsets.UTF_8);
System.out.println("\nSaved to: " + outputPath);
} catch (IOException e) {
System.err.println("Failed to write " + filename + ": " + e.getMessage());
}
}
private static String toJson(ExportFormat.ExportedPuzzle puzzle, String date, String theme) {
var sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"date\": \"").append(escapeJson(date)).append("\",\n");
sb.append(" \"theme\": \"").append(escapeJson(theme)).append("\",\n");
sb.append(" \"difficulty\": ").append(puzzle.difficulty()).append(",\n");
sb.append(" \"rewards\": {\n");
sb.append(" \"coins\": ").append(puzzle.rewards().coins()).append(",\n");
sb.append(" \"stars\": ").append(puzzle.rewards().stars()).append(",\n");
sb.append(" \"hints\": ").append(puzzle.rewards().hints()).append("\n");
sb.append(" },\n");
sb.append(" \"gridv2\": [\n");
for (var i = 0; i < puzzle.gridv2().size(); i++) {
sb.append(" \"").append(escapeJson(puzzle.gridv2().get(i))).append("\"");
if (i < puzzle.gridv2().size() - 1) sb.append(",");
sb.append("\n");
}
sb.append(" ],\n");
sb.append(" \"words\": [\n");
for (var i = 0; i < puzzle.words().size(); i++) {
var w = puzzle.words().get(i);
sb.append(" {\n");
sb.append(" \"word\": \"").append(escapeJson(w.word())).append("\",\n");
sb.append(" \"clue\": \"").append(escapeJson(w.clue())).append("\",\n");
sb.append(" \"startRow\": ").append(w.startRow()).append(",\n");
sb.append(" \"startCol\": ").append(w.startCol()).append(",\n");
sb.append(" \"direction\": \"").append(escapeJson(w.direction())).append("\",\n");
sb.append(" \"answer\": \"").append(escapeJson(w.answer())).append("\",\n");
sb.append(" \"arrowRow\": ").append(w.arrowRow()).append(",\n");
sb.append(" \"arrowCol\": ").append(w.arrowCol()).append("\n");
sb.append(" }");
if (i < puzzle.words().size() - 1) sb.append(",");
sb.append("\n");
}
sb.append(" ]\n");
sb.append("}\n");
return sb.toString();
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static String safeSlug(String s) {
return s.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", "");
} }
} }

View File

@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.stream.IntStream;
/** /**
* SwedishGenerator.java * SwedishGenerator.java
@@ -108,6 +109,10 @@ public class SwedishGenerator {
if (n >= a.length) a = Arrays.copyOf(a, a.length * 2); if (n >= a.length) a = Arrays.copyOf(a, a.length * 2);
a[n++] = v; a[n++] = v;
} }
void replaceAll(int[] newData) {
this.a = newData;
this.n = newData.length;
}
int size() { return n; } int size() { return n; }
int[] data() { return a; } // note: may have extra capacity int[] data() { return a; } // note: may have extra capacity
} }
@@ -124,6 +129,18 @@ public class SwedishGenerator {
} }
} }
static class WordDifficulty {
final String word;
final int difficulty;
public WordDifficulty(String word) {
this.word = word;
// Simple heuristic for difficulty: shorter words have lower difficulty
this.difficulty = -Math.min(40,word.length() * 5);
}
}
static final class Dict { static final class Dict {
final ArrayList<String> words; final ArrayList<String> words;
@@ -135,7 +152,6 @@ public class SwedishGenerator {
this.lenCounts = lenCounts; this.lenCounts = lenCounts;
} }
} }
static Dict loadWords(String wordsPath) { static Dict loadWords(String wordsPath) {
String raw; String raw;
try { try {
@@ -144,16 +160,26 @@ public class SwedishGenerator {
raw = "EU\nUUR\nAUTO\nBOOM\nHUIS\nKAT\nZEE\nRODE\nDRAAD\nKENNIS\nNETWERK\nPAKTE\n"; raw = "EU\nUUR\nAUTO\nBOOM\nHUIS\nKAT\nZEE\nRODE\nDRAAD\nKENNIS\nNETWERK\nPAKTE\n";
} }
var words = new ArrayList<String>(); var words = new ArrayList<WordDifficulty>();
for (var line : raw.split("\\R")) { for (var line : raw.split("\\R")) {
var s = line.trim().toUpperCase(Locale.ROOT); var s = line.trim().toUpperCase(Locale.ROOT);
if (s.matches("^[A-Z]{2,8}$")) words.add(s); if (s.matches("^[A-Z]{2,8}$")) {
words.add(new WordDifficulty(s));
}
}
// Sort words by difficulty in ascending order
words.sort(Comparator.comparingInt(wd -> wd.difficulty));
var dictWords = new ArrayList<String>();
for (var wd : words) {
dictWords.add(wd.word);
} }
var index = new HashMap<Integer, DictEntry>(); var index = new HashMap<Integer, DictEntry>();
var lenCounts = new HashMap<Integer, Integer>(); var lenCounts = new HashMap<Integer, Integer>();
for (var w : words) { for (var w : dictWords) {
var L = w.length(); var L = w.length();
lenCounts.put(L, lenCounts.getOrDefault(L, 0) + 1); lenCounts.put(L, lenCounts.getOrDefault(L, 0) + 1);
@@ -172,7 +198,7 @@ public class SwedishGenerator {
} }
} }
return new Dict(words, index, lenCounts); return new Dict(dictWords, index, lenCounts);
} }
static int[] intersectSorted(int[] a, int aLen, int[] b, int bLen) { static int[] intersectSorted(int[] a, int aLen, int[] b, int bLen) {
@@ -195,7 +221,6 @@ public class SwedishGenerator {
int[] indices; // null => unconstrained int[] indices; // null => unconstrained
int count; int count;
} }
static CandidateInfo candidateInfoForPattern(DictEntry entry, char[] pattern /* 0 means null */) { static CandidateInfo candidateInfoForPattern(DictEntry entry, char[] pattern /* 0 means null */) {
var lists = new ArrayList<IntList>(); var lists = new ArrayList<IntList>();
for (var i = 0; i < pattern.length; i++) { for (var i = 0; i < pattern.length; i++) {
@@ -204,6 +229,7 @@ public class SwedishGenerator {
lists.add(entry.pos[i][ch - 'A']); lists.add(entry.pos[i][ch - 'A']);
} }
} }
var ci = new CandidateInfo(); var ci = new CandidateInfo();
if (lists.isEmpty()) { if (lists.isEmpty()) {
ci.indices = null; ci.indices = null;
@@ -211,8 +237,6 @@ public class SwedishGenerator {
return ci; return ci;
} }
lists.sort(Comparator.comparingInt(IntList::size));
var first = lists.get(0); var first = lists.get(0);
var cur = Arrays.copyOf(first.data(), first.size()); var cur = Arrays.copyOf(first.data(), first.size());
var curLen = cur.length; var curLen = cur.length;
@@ -230,6 +254,11 @@ public class SwedishGenerator {
ci.count = curLen; ci.count = curLen;
return ci; return ci;
} }
static int indexToDifficulty(DictEntry entry, int index) {
var word = entry.words.get(index);
return new WordDifficulty(word).difficulty;
}
// ---------------- Slots ---------------- // ---------------- Slots ----------------
@@ -751,11 +780,14 @@ public class SwedishGenerator {
var L = idxs.length; var L = idxs.length;
var tries = Math.min(MAX_TRIES_PER_SLOT, L); var tries = Math.min(MAX_TRIES_PER_SLOT, L);
var start = (L == 1) ? 0 : rng.randint(0, L - 1); // When picking words from sorted indices, we want to favor the beginning
var step = (L <= 1) ? 1 : rng.randint(1, L - 1); // (lower difficulty) but still have some randomness.
for (var t = 0; t < tries; t++) { for (var t = 0; t < tries; t++) {
var idx = idxs[(start + t * step) % L]; // Power law or similar to favor lower indices:
// pick a random double in [0, 1), square it to bias towards 0.
double r = rng.nextFloat();
int idxInArray = (int) (r * r * L);
var idx = idxs[idxInArray];
var w = entry.words.get(idx); var w = entry.words.get(idx);
if (tryWord.apply(w)) return true; if (tryWord.apply(w)) return true;
} }
@@ -770,12 +802,10 @@ public class SwedishGenerator {
} }
var tries = Math.min(MAX_TRIES_PER_SLOT, N); var tries = Math.min(MAX_TRIES_PER_SLOT, N);
var start = (N == 1) ? 0 : rng.randint(0, N - 1);
var step = (N <= 1) ? 1 : rng.randint(1, N - 1);
for (var t = 0; t < tries; t++) { for (var t = 0; t < tries; t++) {
var idx = (start + t * step) % N; double r = rng.nextFloat();
var w = entry.words.get(idx); int idxInArray = (int) (r * r * N);
var w = entry.words.get(idxInArray);
if (tryWord.apply(w)) return true; if (tryWord.apply(w)) return true;
} }

View File

@@ -5,7 +5,6 @@ import javax.net.ssl.*;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*; import java.io.*;
import java.net.URI;
import java.net.http.*; import java.net.http.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
@@ -24,28 +23,44 @@ public class ThemePoolBuilder {
"EU", "VS", "UK", "NAVO", "NOS", "NS", "ANP", "VN", "NPO", "RTL", "EU", "VS", "UK", "NAVO", "NOS", "NS", "ANP", "VN", "NPO", "RTL",
"UUR", "MIN", "TV", "GPS", "AI", "IT", "CPU", "GPU", "UUR", "MIN", "TV", "GPS", "AI", "IT", "CPU", "GPU",
"ING", "KPN", "KVK", "RIVM", "GGD", "AIVD", "MIVD", "CEO", "CFO", "HR", "ING", "KPN", "KVK", "RIVM", "GGD", "AIVD", "MIVD", "CEO", "CFO", "HR",
"PVV", "VVD", "CDA", "FNV" "NL", "BE", "BRU", "EUR", "EURO", "WET", "ART", "BTW", "DI", "MA",
"PVV", "VVD", "CDA", "FNV","EN","IN","OP","OM","TE","ER","DE","HET","EEN","VAN","MET","NOG","OOK","MAAR","WEL","NIET",
"HOE","ALS","EEN",
"NL", "BE", "BRU", "EUR", "EURO", "WET", "ART", "BTW", "DI", "MA",
"ZO", "DO", "WO", "VR", "ZO", "MO", "WA", "WE", "TAAL",
"LAND", "GEMEENTE", "STAAT", "BUREAU", "HUIS", "SCHOOL", "STR", "BAAN",
"WERK", "KLUS",
"FONDS", "RAAD", "CONGRESS", "GROEP", "STRAAT", "BRUG", "PARK",
"BUURT",
"BOUW", "HOTEL", "CAFE", "BAR",
"BIJBAAN", "STUDENT", "DOCENT",
"WINKEL", "MARKT", "KIOSK", "AUTO", "MOBILE", "FIETS", "SCOOTER",
// afkortingen (worden toch A-Z geforceerd)
"DHR","MEVR","DR","ST","CA","IVM","MBT","TAV","TOV","DWZ","MAW","OA","TM",
"EU","VS","NAVO","NOS","NS","ANWB","KVK","BTW","BRP","CBS","NPO","RTL","RIVM",
// romeinse cijfers (28 tekens)
"II","III","IV","VI","VII","VIII","IX",
"XI","XII","XIII","XIV","XV","XVI","XVII","XVIII","XIX","XX"
); );
// Browser-like UA (no shell quoting issues because we use ProcessBuilder args)
private static final String BROWSER_UA = private static final String BROWSER_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"; "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
// ---------------- CLI ----------------
static final class Opts { static final class Opts {
String wordsPath = "/home/mike/dev/puzzle-generator/word-list.txt"; String wordsPath = "/home/mike/dev/puzzle-generator/word-list.txt";
String endpoint = "https://jarvis-lan.appmodel.nl/api/stoic/chat/completions/"; String endpoint = "https://jarvis-lan.appmodel.nl/api/stoic/";
List<String> feeds = new ArrayList<>(DEFAULT_FEEDS); List<String> feeds = new ArrayList<>(DEFAULT_FEEDS);
String outDir = "./out"; String outDir = "./out";
int bridgeN = 5000; int bridgeN = 52000;
int themeN = 300; int themeN = 800;
int relatedN = 1200; int relatedN = 2200;
int rssItemsPerFeed = 20; int rssItemsPerFeed = 10;
String model = "openai/gpt-oss-20b"; String model = "mistralai/mistral-nemo-instruct-2407";
int timeoutSeconds = 180; // LM Studio needs more time for generation int timeoutSeconds = 180; // LM Studio needs more time for generation
int retries = 2; int retries = 2;
} }
@@ -126,8 +141,6 @@ public class ThemePoolBuilder {
return o; return o;
} }
// ---------------- Normalization ----------------
static boolean isAZ(String s) { static boolean isAZ(String s) {
for (var i = 0; i < s.length(); i++) { for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i); var ch = s.charAt(i);
@@ -141,11 +154,9 @@ public class ThemePoolBuilder {
var s = raw.trim(); var s = raw.trim();
if (s.isEmpty()) return null; if (s.isEmpty()) return null;
// strip diacritics
s = Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("\\p{M}+", ""); s = Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("\\p{M}+", "");
s = s.toUpperCase(Locale.ROOT); s = s.toUpperCase(Locale.ROOT);
// keep only A-Z
s = s.replaceAll("[^A-Z]", ""); s = s.replaceAll("[^A-Z]", "");
if (s.length() < 2 || s.length() > 8) return null; if (s.length() < 2 || s.length() > 8) return null;
if (!isAZ(s)) return null; if (!isAZ(s)) return null;
@@ -160,8 +171,6 @@ public class ThemePoolBuilder {
return x; return x;
} }
// ---------------- Crossability score ----------------
static final Map<Character, Integer> LETTER_WEIGHT = Map.ofEntries( static final Map<Character, Integer> LETTER_WEIGHT = Map.ofEntries(
Map.entry('E', 10), Map.entry('N', 9), Map.entry('A', 9), Map.entry('R', 8), Map.entry('E', 10), Map.entry('N', 9), Map.entry('A', 9), Map.entry('R', 8),
Map.entry('I', 8), Map.entry('O', 7), Map.entry('S', 7), Map.entry('T', 7), Map.entry('I', 8), Map.entry('O', 7), Map.entry('S', 7), Map.entry('T', 7),
@@ -191,21 +200,14 @@ public class ThemePoolBuilder {
return score; return score;
} }
// ---------------- Lexicon ----------------
static final class Lexicon { /**
* @param words id -> word
final List<String> words; // id -> word * @param idOf word -> id
final Map<String, Integer> idOf; // word -> id * @param score id -> crossability
final int[] score; // id -> crossability * @param byLen byLen[L] for L 0..8 */
final BitSet[] byLen; // byLen[L] for L 0..8 record Lexicon(List<String> words, Map<String, Integer> idOf, int[] score, BitSet[] byLen) {
Lexicon(List<String> words, Map<String, Integer> idOf, int[] score, BitSet[] byLen) {
this.words = words;
this.idOf = idOf;
this.score = score;
this.byLen = byLen;
}
} }
static Lexicon loadLexicon(String path) throws IOException { static Lexicon loadLexicon(String path) throws IOException {
@@ -214,6 +216,7 @@ public class ThemePoolBuilder {
var out = new ArrayList<String>(lines.size()); var out = new ArrayList<String>(lines.size());
var idOf = new HashMap<String, Integer>(lines.size() * 2); var idOf = new HashMap<String, Integer>(lines.size() * 2);
// 1) master lexicon
for (var line : lines) { for (var line : lines) {
var w = normalizeDutchToken(line); var w = normalizeDutchToken(line);
if (w == null) continue; if (w == null) continue;
@@ -222,6 +225,29 @@ public class ThemePoolBuilder {
out.add(w); out.add(w);
} }
// 2) inject extra short words (24 letters mostly)
var extraShorts = List.of(
"EN","IN","OP","OM","TE","ER","DE","HET","EEN","VAN","MET",
"AL","NU","ZO","TO","NA","BIJ","TOT","ALS","DAN","WAT","DAT",
"IK","JE","WE","WIJ","JIJ","ZIJ","HIJ","HEN","ONS","JOU",
"EIS","WET","RAAD","PLAN","TEAM","MAAT"
);
for (var raw : DEFAULT_SHORTS) {
var w = normalizeDutchToken(raw);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
}
for (var wRaw : extraShorts) {
var w = normalizeDutchToken(wRaw);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
}
var n = out.size(); var n = out.size();
var score = new int[n]; var score = new int[n];
var byLen = new BitSet[9]; var byLen = new BitSet[9];
@@ -336,7 +362,7 @@ public class ThemePoolBuilder {
if (base.endsWith("/v1")) base = base.substring(0, base.length() - 3); if (base.endsWith("/v1")) base = base.substring(0, base.length() - 3);
if (!path.startsWith("/")) path = "/" + path; if (!path.startsWith("/")) path = "/" + path;
if (!path.startsWith("/v1/")) path = "/v1" + path; if (!path.startsWith("/v1/")) path = "/" + path;
return base + path; return base + path;
} }
@@ -606,16 +632,16 @@ public class ThemePoolBuilder {
Regels: Regels:
- Output MOET exact één JSON array zijn: ["WOORD", ...] - Output MOET exact één JSON array zijn: ["WOORD", ...]
- Alleen A-Z, 2-8 letters - Alleen A-Z, 2-8 letters woorden
- Geen spaties, streepjes, cijfers, accenten, apostrofs, punten - Geen spaties, streepjes, cijfers, accenten, apostrofs, punten
- Geen duplicaten - Geen duplicaten
- Focus op zelfstandige naamwoorden/termen uit het nieuws - Focus op zelfstandige naamwoorden/termen uit het nieuws en relevante Zweedse kruiswoordpuzzel koppelwoorden in het thema.
- Lever %d THEMA-woorden en daarna %d GERELATEERDE woorden (totaal %d). - Lever %d THEMA-woorden en daarna %d GERELATEERDE woorden (totaal %d).
- Voeg ook wat korte woorden/afkortingen toe (2-4 letters), maar houd het totaal gelijk. - Voeg ook wat korte woorden/afkortingen toe (2-4 letters), maar houd het totaal gelijk.
Nieuws (koppen/samenvattingen): Nieuws (koppen/samenvattingen):
%s %s
""".formatted(o.themeN, o.relatedN, (o.themeN + o.relatedN), rssText); """.formatted(o.themeN, o.relatedN, (o.themeN + o.relatedN), rssText.substring(0,8000));
var body = """ var body = """
{ {
@@ -625,7 +651,7 @@ public class ThemePoolBuilder {
{"role":"user","content": %s} {"role":"user","content": %s}
], ],
"temperature": 0.35, "temperature": 0.35,
"max_tokens": 2000 "max_tokens": 20000
} }
""".formatted(jsonQuote(modelId), jsonQuote(prompt)); """.formatted(jsonQuote(modelId), jsonQuote(prompt));

View File

@@ -0,0 +1,855 @@
package puzzle;
import org.w3c.dom.*;
import javax.net.ssl.*;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.text.Normalizer;
import java.time.LocalDate;
import java.util.*;
public class ThemePoolBuilderLength {
private static final List<String> DEFAULT_FEEDS = List.of(
"https://feeds.nos.nl/nosnieuwsalgemeen",
"https://feeds.nos.nl/nosnieuwstech"
);
// NOTE: normalizeDutchToken strips non A-Z. Keep entries 2-8 after normalization.
private static final List<String> DEFAULT_SHORTS = List.of(
"EU", "VS", "UK", "NAVO", "NOS", "NS", "ANP", "VN", "NPO", "RTL",
"UUR", "MIN", "TV", "GPS", "AI", "IT", "CPU", "GPU",
"ING", "KPN", "KVK", "RIVM", "GGD", "AIVD", "MIVD", "CEO", "CFO", "HR",
"NL", "BE", "BRU", "EUR", "EURO", "WET", "ART", "BTW", "DI", "MA",
"PVV", "VVD", "CDA", "FNV",
"EN","IN","OP","OM","TE","ER","DE","HET","EEN","VAN","MET","NOG","OOK","MAAR","WEL","NIET",
"HOE","ALS",
"ZO", "DO", "WO", "VR", "MO", "WA", "WE", "TAAL",
"LAND", "GEMEENTE", "STAAT", "BUREAU", "HUIS", "SCHOOL", "STR", "BAAN",
"WERK", "KLUS",
"FONDS", "RAAD", "CONGRESS", "GROEP", "STRAAT", "BRUG", "PARK",
"BUURT",
"BOUW", "HOTEL", "CAFE", "BAR",
"BIJBAAN", "STUDENT", "DOCENT",
"WINKEL", "MARKT", "KIOSK", "AUTO", "MOBILE", "FIETS", "SCOOTER",
// afkortingen
"DHR","MEVR","DR","ST","CA","IVM","MBT","TAV","TOV","DWZ","MAW","OA","TM",
"ANWB","BRP","CBS",
// romeinse cijfers (2-8)
"II","III","IV","VI","VII","VIII","IX",
"XI","XII","XIII","XIV","XV","XVI","XVII","XVIII","XIX","XX"
);
private static final String BROWSER_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
static final class Opts {
String wordsPath = "/home/mike/dev/puzzle-generator/word-list.txt";
String endpoint = "https://jarvis-lan.appmodel.nl/api/stoic/";
List<String> feeds = new ArrayList<>(DEFAULT_FEEDS);
String outDir = "./out";
int bridgeN = 42000;
int themeN = 800;
int relatedN = 2200;
int rssItemsPerFeed = 10;
String model = "mistralai/mistral-nemo-instruct-2407";
int timeoutSeconds = 180;
int retries = 2;
// ---- NEW: enforce minimum counts per length in the final pool ----
// Tune these to your puzzle generators appetite for short words.
int minLen2 = 4000;
int minLen3 = 7000;
int minLen4 = 9000;
int minLen5 = 0; // set if you also want to force 5-letter words, etc.
int minLen6 = 0;
int minLen7 = 0;
int minLen8 = 0;
}
static Opts parseArgs(String[] args) {
var o = new Opts();
for (var i = 0; i < args.length; i++) {
var a = args[i];
var v = (i + 1 < args.length) ? args[i + 1] : null;
switch (a) {
case "--words" -> { o.wordsPath = v; i++; }
case "--endpoint" -> { o.endpoint = v; i++; }
case "--feeds" -> { o.feeds = Arrays.asList(v.split(",")); i++; }
case "--out" -> { o.outDir = v; i++; }
case "--bridge" -> { o.bridgeN = Integer.parseInt(v); i++; }
case "--theme" -> { o.themeN = Integer.parseInt(v); i++; }
case "--related" -> { o.relatedN = Integer.parseInt(v); i++; }
case "--items" -> { o.rssItemsPerFeed = Integer.parseInt(v); i++; }
case "--model" -> { o.model = v; i++; }
case "--timeout" -> { o.timeoutSeconds = Integer.parseInt(v); i++; }
case "--retries" -> { o.retries = Integer.parseInt(v); i++; }
// ---- NEW: minima per length ----
case "--min2" -> { o.minLen2 = Integer.parseInt(v); i++; }
case "--min3" -> { o.minLen3 = Integer.parseInt(v); i++; }
case "--min4" -> { o.minLen4 = Integer.parseInt(v); i++; }
case "--min5" -> { o.minLen5 = Integer.parseInt(v); i++; }
case "--min6" -> { o.minLen6 = Integer.parseInt(v); i++; }
case "--min7" -> { o.minLen7 = Integer.parseInt(v); i++; }
case "--min8" -> { o.minLen8 = Integer.parseInt(v); i++; }
case "-h", "--help" -> {
System.out.println("""
Usage:
java puzzle.ThemePoolBuilder --words WORDS.txt [options]
Options:
--endpoint http://HOST:1234/v1 (LM Studio)
--feeds url1,url2
--out ./out
--bridge 5000
--theme 300
--related 1200
--items 20 (per feed)
--model <id> (recommended; skips /v1/models)
--timeout 60 (seconds)
--retries 4
# enforce minima per length in final pool
--min2 4000
--min3 7000
--min4 9000
--min5 0
--min6 0
--min7 0
--min8 0
""");
System.exit(0);
}
default -> throw new IllegalArgumentException("Unknown arg: " + a);
}
}
if (o.wordsPath == null) throw new IllegalArgumentException("--words is required");
return o;
}
static boolean isAZ(String s) {
for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (ch < 'A' || ch > 'Z') return false;
}
return true;
}
static String normalizeDutchToken(String raw) {
if (raw == null) return null;
var s = raw.trim();
if (s.isEmpty()) return null;
s = Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("\\p{M}+", "");
s = s.toUpperCase(Locale.ROOT);
s = s.replaceAll("[^A-Z]", "");
if (s.length() < 2 || s.length() > 8) return null;
if (!isAZ(s)) return null;
return s;
}
static String stripHtml(String s) {
if (s == null) return "";
var x = s.replaceAll("<[^>]+>", " ");
x = x.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">");
x = x.replaceAll("\\s+", " ").trim();
return x;
}
static final Map<Character, Integer> LETTER_WEIGHT = Map.ofEntries(
Map.entry('E', 10), Map.entry('N', 9), Map.entry('A', 9), Map.entry('R', 8),
Map.entry('I', 8), Map.entry('O', 7), Map.entry('S', 7), Map.entry('T', 7),
Map.entry('D', 6), Map.entry('L', 6), Map.entry('K', 5), Map.entry('M', 5),
Map.entry('U', 5), Map.entry('P', 4), Map.entry('G', 4), Map.entry('H', 4),
Map.entry('V', 4), Map.entry('B', 3), Map.entry('W', 3),
Map.entry('C', 2), Map.entry('F', 2), Map.entry('Z', 2),
Map.entry('J', 1), Map.entry('Y', 1), Map.entry('Q', 0), Map.entry('X', 0)
);
static boolean isVowel(char ch) {
return ch == 'A' || ch == 'E' || ch == 'I' || ch == 'O' || ch == 'U';
}
static int crossabilityScore(String w) {
var score = 0;
var vowels = 0;
for (var i = 0; i < w.length(); i++) {
var ch = w.charAt(i);
score += LETTER_WEIGHT.getOrDefault(ch, 2);
if (isVowel(ch)) vowels++;
}
var ratio = vowels / (double) w.length();
if (ratio >= 0.35 && ratio <= 0.65) score += 8;
if (w.indexOf('Q') >= 0 || w.indexOf('X') >= 0) score -= 6;
if (w.indexOf('Y') >= 0 || w.indexOf('J') >= 0) score -= 2;
return score;
}
/**
* @param words id -> word
* @param idOf word -> id
* @param score id -> crossability
* @param byLen byLen[L] for L 0..8
*/
record Lexicon(List<String> words, Map<String, Integer> idOf, int[] score, BitSet[] byLen) { }
static Lexicon loadLexicon(String path) throws IOException {
var lines = Files.readAllLines(Path.of(path), StandardCharsets.UTF_8);
var out = new ArrayList<String>(lines.size());
var idOf = new HashMap<String, Integer>(lines.size() * 2);
// 1) master lexicon
for (var line : lines) {
var w = normalizeDutchToken(line);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
}
// 2) ensure DEFAULT_SHORTS are present even if absent in word-list.txt
for (var raw : DEFAULT_SHORTS) {
var w = normalizeDutchToken(raw);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
}
// 3) small extra injects (optional)
var extraShorts = List.of(
"AL","NU","TO","NA","BIJ","TOT","DAN","WAT","DAT",
"IK","JE","WE","WIJ","JIJ","ZIJ","HIJ","HEN","ONS","JOU"
);
for (var wRaw : extraShorts) {
var w = normalizeDutchToken(wRaw);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
}
var n = out.size();
var score = new int[n];
var byLen = new BitSet[9];
for (var L = 0; L <= 8; L++) byLen[L] = new BitSet(n);
for (var i = 0; i < n; i++) {
var w = out.get(i);
score[i] = crossabilityScore(w);
byLen[w.length()].set(i);
}
return new Lexicon(out, idOf, score, byLen);
}
// ---------------- RSS via curl (browser-like) ----------------
static final class RssItem {
final String title;
final String desc;
RssItem(String title, String desc) {
this.title = title;
this.desc = desc;
}
}
static String textOfFirst(Element parent, String tag) {
var nl = parent.getElementsByTagName(tag);
if (nl.getLength() == 0) return null;
var n = nl.item(0);
return n.getTextContent();
}
static List<RssItem> fetchRssViaCurlBrowser(String url, int limit, int timeoutSeconds) throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("-L");
cmd.add("--compressed");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(timeoutSeconds));
cmd.add("--retry");
cmd.add("5");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add("-H");
cmd.add("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
cmd.add("-H");
cmd.add("Accept-Language: nl-NL,nl;q=0.9,en;q=0.7");
cmd.add("-H");
cmd.add("Cache-Control: no-cache");
cmd.add("-H");
cmd.add("Pragma: no-cache");
cmd.add("-H");
cmd.add("Sec-Fetch-Dest: document");
cmd.add("-H");
cmd.add("Sec-Fetch-Mode: navigate");
cmd.add("-H");
cmd.add("Sec-Fetch-Site: none");
cmd.add("-H");
cmd.add("Sec-Fetch-User: ?1");
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl RSS failed (" + code + ") url=" + url + " output=" +
new String(bytes, StandardCharsets.UTF_8));
}
try (InputStream is = new ByteArrayInputStream(bytes)) {
var dbf = DocumentBuilderFactory.newInstance();
var doc = dbf.newDocumentBuilder().parse(is);
var items = doc.getElementsByTagName("item");
var out = new ArrayList<RssItem>();
for (var i = 0; i < items.getLength() && out.size() < limit; i++) {
var item = (Element) items.item(i);
var title = textOfFirst(item, "title");
var desc = textOfFirst(item, "description");
if (title == null) title = "";
if (desc == null) desc = "";
out.add(new RssItem(stripHtml(title), stripHtml(desc)));
}
return out;
}
}
// ---------------- LM Studio (OpenAI-compatible) ----------------
static String apiUrl(String endpointArg, String path) {
var base = endpointArg.trim();
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
if (base.endsWith("/v1")) base = base.substring(0, base.length() - 3);
if (!path.startsWith("/")) path = "/" + path;
if (!path.startsWith("/v1/")) path = "/" + path;
return base + path;
}
static HttpClient buildHttpClient(int timeoutSeconds) {
try {
return HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(Math.max(10, timeoutSeconds)))
.build();
} catch (RuntimeException ignored) { }
try {
var ssl = insecureSslContext();
return HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(Math.max(10, timeoutSeconds)))
.sslContext(ssl)
.build();
} catch (Exception e) {
throw new RuntimeException("Could not initialize HttpClient. Fix Java truststore or use curl for all HTTP.", e);
}
}
static SSLContext insecureSslContext() throws Exception {
var trustAll = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
public void checkClientTrusted(X509Certificate[] chain, String authType) { }
public void checkServerTrusted(X509Certificate[] chain, String authType) { }
}
};
var ssl = SSLContext.getInstance("TLS");
ssl.init(null, trustAll, new SecureRandom());
return ssl;
}
static void sleepBackoff(int attempt) {
try {
var ms = (long) (300L * Math.pow(2, attempt - 1)); // 300, 600, 1200, ...
Thread.sleep(Math.min(ms, 3000));
} catch (InterruptedException ignored) { }
}
static String curlGetJson(Opts o, String url) throws Exception {
Exception last = null;
for (var attempt = 1; attempt <= o.retries; attempt++) {
try {
List<String> cmd = new ArrayList<>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(o.timeoutSeconds));
cmd.add("--retry");
cmd.add("3");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("Accept: application/json");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl GET failed (" + code + ") url=" + url + "\nOutput:\n" +
new String(bytes, StandardCharsets.UTF_8));
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
last = e;
if (attempt < o.retries) sleepBackoff(attempt);
}
}
throw last;
}
static String curlPostJson(Opts o, String url, String jsonBody) throws Exception {
Exception last = null;
for (var attempt = 1; attempt <= o.retries; attempt++) {
try {
System.out.println(" Attempt " + attempt + "/" + o.retries + " via curl...");
var tempFile = Files.createTempFile("lm-request-", ".json");
try {
Files.writeString(tempFile, jsonBody, StandardCharsets.UTF_8);
List<String> cmd = new ArrayList<>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(o.timeoutSeconds));
cmd.add("--retry");
cmd.add("3");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("Content-Type: application/json");
cmd.add("-H");
cmd.add("Accept: application/json");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add("-d");
cmd.add("@" + tempFile.toString());
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl POST failed (" + code + ") url=" + url + "\nOutput:\n" +
new String(bytes, StandardCharsets.UTF_8));
}
return new String(bytes, StandardCharsets.UTF_8);
} finally {
Files.deleteIfExists(tempFile);
}
} catch (Exception e) {
System.err.println(" Error: " + e.getClass().getName() + ": " + e.getMessage());
last = e;
if (attempt < o.retries) sleepBackoff(attempt);
}
}
throw last;
}
static String pickModelId(String modelsJson) {
if (modelsJson == null) return null;
var data = modelsJson.indexOf("\"data\"");
if (data < 0) return null;
var id = modelsJson.indexOf("\"id\"", data);
if (id < 0) return null;
var q1 = modelsJson.indexOf('"', modelsJson.indexOf(':', id) + 1);
if (q1 < 0) return null;
var q2 = modelsJson.indexOf('"', q1 + 1);
if (q2 < 0) return null;
return modelsJson.substring(q1 + 1, q2);
}
static String extractChatContent(String json) {
if (json == null) return null;
var choices = json.indexOf("\"choices\"");
var p = (choices >= 0) ? choices : 0;
var i = json.indexOf("\"content\"", p);
if (i < 0) return null;
var colon = json.indexOf(':', i);
if (colon < 0) return null;
var q = json.indexOf('"', colon + 1);
if (q < 0) return null;
var sb = new StringBuilder();
var esc = false;
for (var k = q + 1; k < json.length(); k++) {
var ch = json.charAt(k);
if (esc) {
if (ch == 'n') sb.append('\n');
else if (ch == 't') sb.append('\t');
else if (ch == 'r') sb.append('\r');
else sb.append(ch);
esc = false;
} else {
if (ch == '\\') esc = true;
else if (ch == '"') break;
else sb.append(ch);
}
}
return sb.toString();
}
static List<String> parseStringArray(String s) {
if (s == null) return List.of();
var a = s.indexOf('[');
var b = s.lastIndexOf(']');
if (a < 0 || b < 0 || b <= a) return List.of();
var body = s.substring(a + 1, b);
var out = new ArrayList<String>();
var cur = new StringBuilder();
boolean in = false, esc = false;
for (var i = 0; i < body.length(); i++) {
var ch = body.charAt(i);
if (!in) {
if (ch == '"') {
in = true;
cur.setLength(0);
esc = false;
}
} else {
if (esc) {
cur.append(ch);
esc = false;
} else if (ch == '\\') {
esc = true;
} else if (ch == '"') {
out.add(cur.toString());
in = false;
} else {
cur.append(ch);
}
}
}
return out;
}
static String jsonQuote(String s) {
if (s == null) return "null";
var sb = new StringBuilder();
sb.append('"');
for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (ch == '\\' || ch == '"') sb.append('\\').append(ch);
else if (ch == '\n') sb.append("\\n");
else if (ch == '\r') sb.append("\\r");
else if (ch == '\t') sb.append("\\t");
else sb.append(ch);
}
sb.append('"');
return sb.toString();
}
static List<String> llmThemeWords(Opts o, String modelId, String rssText) throws Exception {
var prompt = """
Je genereert woorden voor een Nederlandse kruiswoordpuzzel.
Regels:
- Output MOET exact één JSON array zijn: ["WOORD", ...]
- Alleen A-Z, 2-8 letters woorden
- Geen spaties, streepjes, cijfers, accenten, apostrofs, punten
- Geen duplicaten
- Focus op zelfstandige naamwoorden/termen uit het nieuws en relevante Zweedse kruiswoordpuzzel koppelwoorden in het thema.
- Lever %d THEMA-woorden en daarna %d GERELATEERDE woorden (totaal %d).
- Voeg ook wat korte woorden/afkortingen toe (2-4 letters), maar houd het totaal gelijk.
Nieuws (koppen/samenvattingen):
%s
""".formatted(o.themeN, o.relatedN, (o.themeN + o.relatedN), rssText.substring(0, 8000));
var body = """
{
"model": %s,
"messages": [
{"role":"system","content":"Je bent een strikte JSON generator. Antwoord ALLEEN met een JSON array van strings."},
{"role":"user","content": %s}
],
"temperature": 0.35,
"max_tokens": 20000
}
""".formatted(jsonQuote(modelId), jsonQuote(prompt));
var url = apiUrl(o.endpoint, "/chat/completions");
System.out.println("LM Studio POST: " + url);
System.out.println("Request body length: " + body.length() + " bytes");
var resp = curlPostJson(o, url, body);
var content = extractChatContent(resp);
if (content == null) {
throw new IOException("Could not extract chat content from LM Studio response.\n--- response ---\n" + resp);
}
return parseStringArray(content);
}
// ---------------- Pool building ----------------
static BitSet buildBridgeBitmap(Lexicon lex, int bridgeN) {
var n = lex.words.size();
var ids = new Integer[n];
for (var i = 0; i < n; i++) ids[i] = i;
Arrays.sort(ids, (a, b) -> Integer.compare(lex.score[b], lex.score[a]));
var bs = new BitSet(n);
var take = Math.min(bridgeN, n);
for (var i = 0; i < take; i++) bs.set(ids[i]);
return bs;
}
static BitSet bitmapFromWords(Lexicon lex, Collection<String> words) {
var bs = new BitSet(lex.words.size());
for (var raw : words) {
var w = normalizeDutchToken(raw);
if (w == null) continue;
var id = lex.idOf.get(w);
if (id != null) bs.set(id);
}
return bs;
}
static Map<Integer, Integer> countsPerLen(Lexicon lex, BitSet bs) {
var out = new HashMap<Integer, Integer>();
for (var L = 2; L <= 8; L++) {
var tmp = (BitSet) bs.clone();
tmp.and(lex.byLen[L]);
out.put(L, tmp.cardinality());
}
return out;
}
static void writeWordList(Path path, Lexicon lex, BitSet bs) throws IOException {
var out = new ArrayList<String>(bs.cardinality());
for (var i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
out.add(lex.words.get(i));
}
out.sort(String::compareTo);
Files.write(path, out, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
static String mapToLines(Map<Integer, Integer> m) {
var sb = new StringBuilder();
for (var L = 2; L <= 8; L++) {
sb.append(" ").append(L).append(": ").append(m.getOrDefault(L, 0)).append("\n");
}
return sb.toString();
}
// ---------------- NEW: enforce minima per length ----------------
static int countLen(Lexicon lex, BitSet bs, int L) {
var tmp = (BitSet) bs.clone();
tmp.and(lex.byLen[L]);
return tmp.cardinality();
}
static void ensureMinLen(Lexicon lex, BitSet pool, int L, int minWanted) {
if (minWanted <= 0) return;
var current = countLen(lex, pool, L);
if (current >= minWanted) return;
var need = minWanted - current;
// Collect candidate ids of exactly length L that are not already in pool.
var candidates = new ArrayList<Integer>(Math.max(need * 2, 1024));
for (var id = lex.byLen[L].nextSetBit(0); id >= 0; id = lex.byLen[L].nextSetBit(id + 1)) {
if (!pool.get(id)) candidates.add(id);
}
if (candidates.isEmpty()) return;
// Sort by crossability score (desc)
candidates.sort((a, b) -> Integer.compare(lex.score[b], lex.score[a]));
var added = 0;
for (var id : candidates) {
pool.set(id);
added++;
if (added >= need) break;
}
}
static void enforceMinima(Opts o, Lexicon lex, BitSet pool) {
ensureMinLen(lex, pool, 2, o.minLen2);
ensureMinLen(lex, pool, 3, o.minLen3);
ensureMinLen(lex, pool, 4, o.minLen4);
ensureMinLen(lex, pool, 5, o.minLen5);
ensureMinLen(lex, pool, 6, o.minLen6);
ensureMinLen(lex, pool, 7, o.minLen7);
ensureMinLen(lex, pool, 8, o.minLen8);
}
// ---------------- Main ----------------
public static void main(String[] args) throws Exception {
var o = parseArgs(args);
var outDir = Path.of(o.outDir);
Files.createDirectories(outDir);
System.out.println("Loading lexicon...");
var lex = loadLexicon(o.wordsPath);
System.out.println("Master words (2-8, A-Z): " + lex.words.size());
// RSS via curl (browser-like)
var all = new ArrayList<RssItem>();
for (var feed : o.feeds) {
var f = feed.trim();
if (f.isEmpty()) continue;
System.out.println("Fetching RSS: " + f);
all.addAll(fetchRssViaCurlBrowser(f, o.rssItemsPerFeed, o.timeoutSeconds));
}
var rssText = new StringBuilder();
var k = 0;
for (var it : all) {
k++;
rssText.append(k).append(". ").append(it.title).append("\n");
if (!it.desc.isBlank()) rssText.append(" ").append(it.desc).append("\n");
}
Files.writeString(outDir.resolve("rss.txt"), rssText.toString(), StandardCharsets.UTF_8);
// LM Studio via curl
var modelId = o.model;
if (modelId == null) {
var modelsUrl = apiUrl(o.endpoint, "/models");
System.out.println("LM Studio GET: " + modelsUrl);
var modelsJson = curlGetJson(o, modelsUrl);
modelId = pickModelId(modelsJson);
if (modelId == null) {
throw new IOException("Could not auto-pick model id from /v1/models. Use --model <id>.\n--- /models ---\n" + modelsJson);
}
}
System.out.println("Using model: " + modelId);
System.out.println("Generating theme words via LM Studio...");
var llmWords = llmThemeWords(o, modelId, rssText.toString());
// Normalize + keep only those present in master lexicon
var themeKept = new LinkedHashSet<String>();
for (var wRaw : llmWords) {
var w = normalizeDutchToken(wRaw);
if (w == null) continue;
if (lex.idOf.containsKey(w)) themeKept.add(w);
}
Files.write(outDir.resolve("theme.txt"), themeKept, StandardCharsets.UTF_8);
// BitSets
var themeBs = bitmapFromWords(lex, themeKept);
var bridgeBs = buildBridgeBitmap(lex, o.bridgeN);
var shortBs = bitmapFromWords(lex, DEFAULT_SHORTS);
var pool = new BitSet(lex.words.size());
pool.or(themeBs);
pool.or(bridgeBs);
pool.or(shortBs);
// ---- NEW: enforce minimum counts per length ----
enforceMinima(o, lex, pool);
// Report
var themeCounts = countsPerLen(lex, themeBs);
var poolCounts = countsPerLen(lex, pool);
var report = """
Date: %s
Feeds: %s
Model: %s
Master size: %d
Theme kept (in master): %d
Bridge size: %d
Shorts kept: %d
Pool total: %d
Enforced minima:
2: %d
3: %d
4: %d
5: %d
6: %d
7: %d
8: %d
Counts per length (theme):
%s
Counts per length (pool):
%s
""".formatted(
LocalDate.now(),
String.join(", ", o.feeds),
modelId,
lex.words.size(),
themeBs.cardinality(),
bridgeBs.cardinality(),
shortBs.cardinality(),
pool.cardinality(),
o.minLen2, o.minLen3, o.minLen4, o.minLen5, o.minLen6, o.minLen7, o.minLen8,
mapToLines(themeCounts),
mapToLines(poolCounts)
);
Files.writeString(outDir.resolve("report.txt"), report, StandardCharsets.UTF_8);
System.out.println(report);
// Output pool list
var poolFile = outDir.resolve("pool.txt");
writeWordList(poolFile, lex, pool);
System.out.println("Wrote: " + poolFile.toAbsolutePath());
}
}