Commit edfe6543d392231836d45c98040351895ae4868b

Authored by yibo
1 parent fcb9a940

Rotate the random fallback pick by play count, not pure chance

Adds playCount to each library clip, incremented every time it airs
(markPlayed). The idle-filler pick in pickNextClip now randomizes only among
the least-played clips in the eligible pool — everyone at 0 plays first, then
everyone tied at 1, and so on — instead of picking uniformly at random across
the whole library, which let some clips rack up far more airtime than others
by chance alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
Showing 2 changed files with 59 additions and 53 deletions
library.json
... ... @@ -10,7 +10,8 @@
10 10 "duration": 1,
11 11 "partition": "fl2va",
12 12 "likes": 0,
13   - "createdAt": 1788251288958
  13 + "createdAt": 1788251288958,
  14 + "lastPlayedAt": 1788254550116
14 15 },
15 16 {
16 17 "id": "8024972d6019",
... ... @@ -379,7 +380,7 @@
379 380 "likes": 0,
380 381 "createdAt": 1788181569491,
381 382 "promptEn": "a massive warship glides through the stars, the bridge suddenly shakes hard",
382   - "lastPlayedAt": 1788253093423
  383 + "lastPlayedAt": 1788254390052
383 384 },
384 385 {
385 386 "id": "44bf5b969e94",
... ... @@ -424,7 +425,7 @@
424 425 "likes": 0,
425 426 "createdAt": 1788159367556,
426 427 "promptEn": "constable talking nervously in an old room, looking scared",
427   - "lastPlayedAt": 1788252509170
  428 + "lastPlayedAt": 1788254306023
428 429 },
429 430 {
430 431 "id": "a6d767413426",
... ... @@ -439,7 +440,7 @@
439 440 "likes": 0,
440 441 "createdAt": 1788157322611,
441 442 "promptEn": "constable talking, looking scared",
442   - "lastPlayedAt": 1788250916299
  443 + "lastPlayedAt": 1788254071916
443 444 },
444 445 {
445 446 "id": "7977eeeabe8b",
... ... @@ -469,7 +470,7 @@
469 470 "likes": 0,
470 471 "createdAt": 1788156878022,
471 472 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
472   - "lastPlayedAt": 1788252225046
  473 + "lastPlayedAt": 1788254209972
473 474 },
474 475 {
475 476 "id": "a0b752e7ac0d",
... ... @@ -544,7 +545,7 @@
544 545 "likes": 0,
545 546 "createdAt": 1788120171608,
546 547 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
547   - "lastPlayedAt": 1788252411133
  548 + "lastPlayedAt": 1788254175959
548 549 },
549 550 {
550 551 "id": "b8c762a3672e",
... ... @@ -574,7 +575,7 @@
574 575 "likes": 0,
575 576 "createdAt": 1788085282201,
576 577 "promptEn": "constable stands still, quietly says he won't make a sound, woman stays silent behind the screen",
577   - "lastPlayedAt": 1788251288482
  578 + "lastPlayedAt": 1788254023891
578 579 },
579 580 {
580 581 "id": "683ab2fd4681",
... ... @@ -589,7 +590,7 @@
589 590 "likes": 0,
590 591 "createdAt": 1788071067702,
591 592 "promptEn": "constable stands still, quietly says he won't make a sound, woman stays silent behind the screen",
592   - "lastPlayedAt": 1788251524674
  593 + "lastPlayedAt": 1788253999874
593 594 },
594 595 {
595 596 "id": "2e09fcd3d42c",
... ... @@ -649,7 +650,7 @@
649 650 "likes": 0,
650 651 "createdAt": 1788027833836,
651 652 "promptEn": "wounded hunter at a ruined subway dungeon entrance, lightning flickers, pointing into the tunnel",
652   - "lastPlayedAt": 1788250480118
  653 + "lastPlayedAt": 1788254268003
653 654 },
654 655 {
655 656 "id": "cde399c27dc4",
... ... @@ -754,7 +755,7 @@
754 755 "likes": 0,
755 756 "createdAt": 1788026568242,
756 757 "promptEn": "magma commander in a white coat, raising a lava fist on the battlefield",
757   - "lastPlayedAt": 1788252487163
  758 + "lastPlayedAt": 1788254205974
758 759 },
759 760 {
760 761 "id": "1442013e94f0",
... ... @@ -784,7 +785,7 @@
784 785 "likes": 0,
785 786 "createdAt": 1788025642732,
786 787 "promptEn": "Vecna reaching out a claw in his lair in the Upside Down, red lightning",
787   - "lastPlayedAt": 1788252559192
  788 + "lastPlayedAt": 1788254043902
788 789 },
789 790 {
790 791 "id": "00862f966c3f",
... ... @@ -904,7 +905,7 @@
904 905 "likes": 0,
905 906 "createdAt": 1788025220954,
906 907 "promptEn": "pirate captain plants a cutlass on the table, making a dangerous bargain",
907   - "lastPlayedAt": 1788251456618
  908 + "lastPlayedAt": 1788254095922
908 909 },
909 910 {
910 911 "id": "3e03975ff40a",
... ... @@ -919,7 +920,7 @@
919 920 "likes": 0,
920 921 "createdAt": 1788025003469,
921 922 "promptEn": "vampire idol with red eyes, offering a blood-red contract backstage",
922   - "lastPlayedAt": 1788243983188
  923 + "lastPlayedAt": 1788254386050
923 924 },
924 925 {
925 926 "id": "2857aacf11bb",
... ... @@ -1054,7 +1055,7 @@
1054 1055 "likes": 0,
1055 1056 "createdAt": 1788015737873,
1056 1057 "promptEn": "pirate negotiation, went in head-on and Barbossa saw it coming",
1057   - "lastPlayedAt": 1788252825264
  1058 + "lastPlayedAt": 1788254215976
1058 1059 },
1059 1060 {
1060 1061 "id": "2a5ee87b8f13",
... ... @@ -1174,7 +1175,7 @@
1174 1175 "likes": 0,
1175 1176 "createdAt": 1788013870849,
1176 1177 "promptEn": "glass bridge game, exploiting a flaw to push back player 101",
1177   - "lastPlayedAt": 1788249963847
  1178 + "lastPlayedAt": 1788254201971
1178 1179 },
1179 1180 {
1180 1181 "id": "dd12bbb55060",
... ... @@ -1189,7 +1190,7 @@
1189 1190 "likes": 0,
1190 1191 "createdAt": 1788013634333,
1191 1192 "promptEn": "glass bridge game, held the goal but paid a price",
1192   - "lastPlayedAt": 1788252773238
  1193 + "lastPlayedAt": 1788254355883
1193 1194 },
1194 1195 {
1195 1196 "id": "5b8ef21616d4",
... ... @@ -1294,7 +1295,7 @@
1294 1295 "likes": 0,
1295 1296 "createdAt": 1788012441458,
1296 1297 "promptEn": "facing Vecna, held the goal but paid a price",
1297   - "lastPlayedAt": 1788251798845
  1298 + "lastPlayedAt": 1788254283882
1298 1299 },
1299 1300 {
1300 1301 "id": "691c2f679759",
... ... @@ -1429,7 +1430,7 @@
1429 1430 "likes": 0,
1430 1431 "createdAt": 1788010775613,
1431 1432 "promptEn": "facing Voldemort, stayed passive and got cornered",
1432   - "lastPlayedAt": 1788252633224
  1433 + "lastPlayedAt": 1788254061908
1433 1434 },
1434 1435 {
1435 1436 "id": "795842efa4a1",
... ... @@ -1474,7 +1475,7 @@
1474 1475 "likes": 0,
1475 1476 "createdAt": 1788010062535,
1476 1477 "promptEn": "facing Thanos, held the goal but paid a price",
1477   - "lastPlayedAt": 1788252076972
  1478 + "lastPlayedAt": 1788254091920
1478 1479 },
1479 1480 {
1480 1481 "id": "324b84ebc607",
... ... @@ -1594,7 +1595,7 @@
1594 1595 "likes": 0,
1595 1596 "createdAt": 1788008156307,
1596 1597 "promptEn": "hotel floor sealed by the Eternity Devil, an endless looping corridor",
1597   - "lastPlayedAt": 1788251948919
  1598 + "lastPlayedAt": 1788254119932
1598 1599 },
1599 1600 {
1600 1601 "id": "cd3b6dda90ad",
... ... @@ -1654,7 +1655,7 @@
1654 1655 "likes": 0,
1655 1656 "createdAt": 1788007201908,
1656 1657 "promptEn": "breaking into the palace, Neferpitou stalling to protect the king",
1657   - "lastPlayedAt": 1788252681196
  1658 + "lastPlayedAt": 1788254139946
1658 1659 },
1659 1660 {
1660 1661 "id": "628026375795",
... ... @@ -1699,7 +1700,7 @@
1699 1700 "likes": 0,
1700 1701 "createdAt": 1788006252918,
1701 1702 "promptEn": "facing L, stayed passive and the evidence pressure maxed out",
1702   - "lastPlayedAt": 1788253113430
  1703 + "lastPlayedAt": 1788254350039
1703 1704 },
1704 1705 {
1705 1706 "id": "0e28a7fecf03",
... ... @@ -1939,7 +1940,7 @@
1939 1940 "likes": 0,
1940 1941 "createdAt": 1788002929455,
1941 1942 "promptEn": "facing Reiner, exploiting a flaw to counter him",
1942   - "lastPlayedAt": 1788252195030
  1943 + "lastPlayedAt": 1788254288010
1943 1944 },
1944 1945 {
1945 1946 "id": "ea8b38014674",
... ... @@ -1954,7 +1955,7 @@
1954 1955 "likes": 0,
1955 1956 "createdAt": 1788002691822,
1956 1957 "promptEn": "facing Reiner, held the goal but paid a price",
1957   - "lastPlayedAt": 1788252655237
  1958 + "lastPlayedAt": 1788254037898
1958 1959 },
1959 1960 {
1960 1961 "id": "f65b15d67966",
... ... @@ -2059,7 +2060,7 @@
2059 2060 "likes": 0,
2060 2061 "createdAt": 1788001049233,
2061 2062 "promptEn": "facing Pain, held the goal but paid a price",
2062   - "lastPlayedAt": 1788251780839
  2063 + "lastPlayedAt": 1788254115931
2063 2064 },
2064 2065 {
2065 2066 "id": "7ecd480a3c31",
... ... @@ -2224,7 +2225,7 @@
2224 2225 "likes": 0,
2225 2226 "createdAt": 1787900324972,
2226 2227 "promptEn": "constable declines a drink, asks what this place is, she watches him through the screen",
2227   - "lastPlayedAt": 1788252709206
  2228 + "lastPlayedAt": 1788254371882
2228 2229 },
2229 2230 {
2230 2231 "id": "43ee5c728f1b",
... ... @@ -2434,7 +2435,7 @@
2434 2435 "likes": 0,
2435 2436 "createdAt": 1787708713767,
2436 2437 "promptEn": "constable stops looking around, turns to face the screen, speaks to her silhouette",
2437   - "lastPlayedAt": 1788252811258
  2438 + "lastPlayedAt": 1788254322029
2438 2439 },
2439 2440 {
2440 2441 "id": "5dc95a15cd5d",
... ... @@ -2464,7 +2465,7 @@
2464 2465 "likes": 0,
2465 2466 "createdAt": 1787708686397,
2466 2467 "promptEn": "constable accepts the invitation, moves to sit on a stool closer to the screen",
2467   - "lastPlayedAt": 1788248577063
  2468 + "lastPlayedAt": 1788254145947
2468 2469 },
2469 2470 {
2470 2471 "id": "ad611bcbd06b",
... ... @@ -2539,7 +2540,7 @@
2539 2540 "likes": 0,
2540 2541 "createdAt": 1787708648409,
2541 2542 "promptEn": "she reaches a hand through the screen gap beckoning him, he steps back and firmly refuses",
2542   - "lastPlayedAt": 1788251828861
  2543 + "lastPlayedAt": 1788254125936
2543 2544 },
2544 2545 {
2545 2546 "id": "3f0c28f5373a",
... ... @@ -2749,7 +2750,7 @@
2749 2750 "likes": 0,
2750 2751 "createdAt": 1787674688717,
2751 2752 "promptEn": "constable agrees to stay, lets his guard down, she gives a barely-there nod",
2752   - "lastPlayedAt": 1788250832261
  2753 + "lastPlayedAt": 1788254161950
2753 2754 },
2754 2755 {
2755 2756 "id": "4874fe3fdbc9",
... ... @@ -2779,7 +2780,7 @@
2779 2780 "likes": 0,
2780 2781 "createdAt": 1787674642139,
2781 2782 "promptEn": "constable forms hand seals and summons a giant toad out of smoke",
2782   - "lastPlayedAt": 1788250076972
  2783 + "lastPlayedAt": 1788254251995
2783 2784 },
2784 2785 {
2785 2786 "id": "e9a6320e18a9",
... ... @@ -2854,7 +2855,7 @@
2854 2855 "likes": 0,
2855 2856 "createdAt": 1787674446841,
2856 2857 "promptEn": "player panics into the room, glances back, out of breath, says something's chasing him outside",
2857   - "lastPlayedAt": 1788250856275
  2858 + "lastPlayedAt": 1788254185961
2858 2859 },
2859 2860 {
2860 2861 "id": "c25d53a5e086",
... ... @@ -2869,7 +2870,7 @@
2869 2870 "likes": 0,
2870 2871 "createdAt": 1787674395006,
2871 2872 "promptEn": "constable and the woman hold a tense silent stare through the screen",
2872   - "lastPlayedAt": 1788252897296
  2873 + "lastPlayedAt": 1788254099923
2873 2874 },
2874 2875 {
2875 2876 "id": "f264bce22903",
... ... @@ -2974,7 +2975,7 @@
2974 2975 "likes": 0,
2975 2976 "createdAt": 1787640567001,
2976 2977 "promptEn": "summoned toad Wentai, beat the painted-skin female ghost",
2977   - "lastPlayedAt": 1788252685196
  2978 + "lastPlayedAt": 1788254552120
2978 2979 },
2979 2980 {
2980 2981 "id": "1cc42a8669a1",
... ... @@ -3004,7 +3005,7 @@
3004 3005 "likes": 0,
3005 3006 "createdAt": 1787637292741,
3006 3007 "promptEn": "constable bursts in panicked, spots the woman behind the screen",
3007   - "lastPlayedAt": 1788251872881
  3008 + "lastPlayedAt": 1788254566123
3008 3009 },
3009 3010 {
3010 3011 "id": "93c2b84a2a10",
... ... @@ -3139,7 +3140,7 @@
3139 3140 "likes": 0,
3140 3141 "createdAt": 1787588545801,
3141 3142 "promptEn": "game menu UI, character swaps out a mechanical arm",
3142   - "lastPlayedAt": 1788249159434
  3143 + "lastPlayedAt": 1788254406058
3143 3144 },
3144 3145 {
3145 3146 "id": "fe749aa533a0",
... ... @@ -3184,7 +3185,7 @@
3184 3185 "likes": 0,
3185 3186 "createdAt": 1787588544109,
3186 3187 "promptEn": "vampire romance drama trailer, girl wanders into the castle",
3187   - "lastPlayedAt": 1788250187972
  3188 + "lastPlayedAt": 1788254272005
3188 3189 },
3189 3190 {
3190 3191 "id": "7ded7db676d3",
... ... @@ -3199,7 +3200,7 @@
3199 3200 "likes": 0,
3200 3201 "createdAt": 1787588543551,
3201 3202 "promptEn": "MiniMax H3 tech brand film, blue waveform logo",
3202   - "lastPlayedAt": 1788250954321
  3203 + "lastPlayedAt": 1788254047898
3203 3204 },
3204 3205 {
3205 3206 "id": "81711242178d",
... ... @@ -3244,7 +3245,7 @@
3244 3245 "likes": 0,
3245 3246 "createdAt": 1787588541726,
3246 3247 "promptEn": "woman opens the trunk on a desert road, takes out a bag",
3247   - "lastPlayedAt": 1788251984934
  3248 + "lastPlayedAt": 1788254335882
3248 3249 },
3249 3250 {
3250 3251 "id": "97846542ab09",
... ... @@ -3514,7 +3515,7 @@
3514 3515 "likes": 0,
3515 3516 "createdAt": 1787573698485,
3516 3517 "promptEn": "underground rap duo freestyling at the camera",
3517   - "lastPlayedAt": 1788252777241
  3518 + "lastPlayedAt": 1788254075916
3518 3519 },
3519 3520 {
3520 3521 "id": "e5246eae814e",
... ... @@ -3543,7 +3544,7 @@
3543 3544 "partition": "text",
3544 3545 "likes": 0,
3545 3546 "createdAt": 1787568512602,
3546   - "lastPlayedAt": 1788252082974,
  3547 + "lastPlayedAt": 1788254065909,
3547 3548 "promptEn": "man apologizes, says something outside was chasing him"
3548 3549 },
3549 3550 {
... ... @@ -3664,7 +3665,7 @@
3664 3665 "likes": 0,
3665 3666 "createdAt": 1787470101411,
3666 3667 "promptEn": "vertical family-fight drama, arguing with an older woman",
3667   - "lastPlayedAt": 1788251276478
  3668 + "lastPlayedAt": 1788254011880
3668 3669 },
3669 3670 {
3670 3671 "id": "57169e6751d0",
... ... @@ -3754,7 +3755,7 @@
3754 3755 "likes": 0,
3755 3756 "createdAt": 1787470099306,
3756 3757 "promptEn": "late-night laundromat footage mixed with glowing hand-drawn animation",
3757   - "lastPlayedAt": 1788251502656
  3758 + "lastPlayedAt": 1788254294016
3758 3759 },
3759 3760 {
3760 3761 "id": "c0e23beb4132",
... ... @@ -3814,7 +3815,7 @@
3814 3815 "likes": 0,
3815 3816 "createdAt": 1787455895150,
3816 3817 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3817   - "lastPlayedAt": 1788252253059
  3818 + "lastPlayedAt": 1788254418063
3818 3819 },
3819 3820 {
3820 3821 "id": "2a4e45820135",
... ... @@ -4054,7 +4055,7 @@
4054 4055 "likes": 0,
4055 4056 "createdAt": 1787413511959,
4056 4057 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4057   - "lastPlayedAt": 1788251161159
  4058 + "lastPlayedAt": 1788254235982
4058 4059 },
4059 4060 {
4060 4061 "id": "5d4f40bf0889",
... ... @@ -4099,7 +4100,7 @@
4099 4100 "likes": 0,
4100 4101 "createdAt": 1787412150062,
4101 4102 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4102   - "lastPlayedAt": 1788252809259
  4103 + "lastPlayedAt": 1788254542110
4103 4104 },
4104 4105 {
4105 4106 "id": "9d2486d6f969",
... ... @@ -4129,7 +4130,7 @@
4129 4130 "likes": 0,
4130 4131 "createdAt": 1787388090646,
4131 4132 "promptEn": "a cat sitting on a windowsill",
4132   - "lastPlayedAt": 1788252393130
  4133 + "lastPlayedAt": 1788254205967
4133 4134 },
4134 4135 {
4135 4136 "id": "c551457bc55e",
... ... @@ -4159,7 +4160,7 @@
4159 4160 "likes": 0,
4160 4161 "createdAt": 1787384983124,
4161 4162 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4162   - "lastPlayedAt": 1788251892892
  4163 + "lastPlayedAt": 1788254354037
4163 4164 },
4164 4165 {
4165 4166 "id": "7753a046ba79",
... ... @@ -4294,7 +4295,7 @@
4294 4295 "likes": 0,
4295 4296 "createdAt": 1787367453839,
4296 4297 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4297   - "lastPlayedAt": 1788252006941
  4298 + "lastPlayedAt": 1788254041895
4298 4299 },
4299 4300 {
4300 4301 "id": "8402adae59dc",
... ... @@ -4309,7 +4310,7 @@
4309 4310 "likes": 0,
4310 4311 "createdAt": 1787363956909,
4311 4312 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4312   - "lastPlayedAt": 1788250706222
  4313 + "lastPlayedAt": 1788254362044
4313 4314 },
4314 4315 {
4315 4316 "id": "5692965402f5",
... ... @@ -4399,7 +4400,7 @@
4399 4400 "likes": 0,
4400 4401 "createdAt": 1787309864037,
4401 4402 "promptEn": "transparent gaming mouse product film, blue and orange lighting",
4402   - "lastPlayedAt": 1788252567195
  4403 + "lastPlayedAt": 1788254219979
4403 4404 },
4404 4405 {
4405 4406 "id": "073f5df6427a",
... ... @@ -4549,7 +4550,7 @@
4549 4550 "likes": 0,
4550 4551 "createdAt": 1787225897650,
4551 4552 "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own",
4552   - "lastPlayedAt": 1788253107430
  4553 + "lastPlayedAt": 1788254544113
4553 4554 },
4554 4555 {
4555 4556 "id": "ed3663dcda7f",
... ...
server.js
... ... @@ -175,6 +175,7 @@ function markPlayed(id) {
175 175 const clip = lib.find((c) => c.id === id);
176 176 if (!clip) return;
177 177 clip.lastPlayedAt = Date.now();
  178 + clip.playCount = (clip.playCount || 0) + 1;
178 179 saveLibrary(lib);
179 180 }
180 181  
... ... @@ -225,7 +226,11 @@ function pickNextClip() {
225 226 // random among everything that isn't on cooldown, no bias toward "fresh".
226 227 const offCooldown = notPrev.filter((c) => !c.lastPlayedAt || now - c.lastPlayedAt > REPEAT_COOLDOWN_MS);
227 228 const pool = offCooldown.length ? offCooldown : notPrev;
228   - return { clip: pool[Math.floor(Math.random() * pool.length)], requestedBy: null, chatId: null };
  229 + // Within that pool, only randomize among the least-played clips — keeps
  230 + // rotation even instead of letting a few clips dominate airtime by chance.
  231 + const minPlays = Math.min(...pool.map((c) => c.playCount || 0));
  232 + const leastPlayed = pool.filter((c) => (c.playCount || 0) === minPlays);
  233 + return { clip: leastPlayed[Math.floor(Math.random() * leastPlayed.length)], requestedBy: null, chatId: null };
229 234 }
230 235  
231 236 const FAILED_ENTRY_TTL_MS = 20000; // how long a "generation failed" entry stays visible before cleanup
... ...