vod frog, frog with the vods
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

local whisper captions: background audio capture, cc_on/cc_off icons, pre-compute on video attach, settings download button

+1651 -196
.DS_Store

This is a binary file and will not be displayed.

+1019
package-lock.json
··· 10 10 "dependencies": { 11 11 "@atproto/api": "^0.19.5", 12 12 "@atproto/oauth-client-browser": "^0.3.41", 13 + "@huggingface/transformers": "^4.0.0", 13 14 "hls.js": "^1.6.15" 14 15 }, 15 16 "devDependencies": { ··· 259 260 "dependencies": { 260 261 "@atproto/lexicon": "^0.6.0", 261 262 "zod": "^3.23.8" 263 + } 264 + }, 265 + "node_modules/@emnapi/runtime": { 266 + "version": "1.9.1", 267 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", 268 + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", 269 + "license": "MIT", 270 + "optional": true, 271 + "dependencies": { 272 + "tslib": "^2.4.0" 262 273 } 263 274 }, 264 275 "node_modules/@esbuild/aix-ppc64": { ··· 701 712 "node": ">=18" 702 713 } 703 714 }, 715 + "node_modules/@huggingface/jinja": { 716 + "version": "0.5.6", 717 + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", 718 + "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", 719 + "license": "MIT", 720 + "engines": { 721 + "node": ">=18" 722 + } 723 + }, 724 + "node_modules/@huggingface/tokenizers": { 725 + "version": "0.1.3", 726 + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", 727 + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", 728 + "license": "Apache-2.0" 729 + }, 730 + "node_modules/@huggingface/transformers": { 731 + "version": "4.0.0", 732 + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.0.0.tgz", 733 + "integrity": "sha512-7shdBRvtjIDP7UUhdi0b4CMRCD+QXvQIsEsxECWS9VEDEsxSQF9Ci+51mwy2OWI8vbsoic8e4dbR4eI8aZWxaw==", 734 + "license": "Apache-2.0", 735 + "dependencies": { 736 + "@huggingface/jinja": "^0.5.6", 737 + "@huggingface/tokenizers": "^0.1.3", 738 + "onnxruntime-node": "1.24.3", 739 + "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", 740 + "sharp": "^0.34.5" 741 + } 742 + }, 743 + "node_modules/@img/colour": { 744 + "version": "1.1.0", 745 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", 746 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", 747 + "license": "MIT", 748 + "engines": { 749 + "node": ">=18" 750 + } 751 + }, 752 + "node_modules/@img/sharp-darwin-arm64": { 753 + "version": "0.34.5", 754 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", 755 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", 756 + "cpu": [ 757 + "arm64" 758 + ], 759 + "license": "Apache-2.0", 760 + "optional": true, 761 + "os": [ 762 + "darwin" 763 + ], 764 + "engines": { 765 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 766 + }, 767 + "funding": { 768 + "url": "https://opencollective.com/libvips" 769 + }, 770 + "optionalDependencies": { 771 + "@img/sharp-libvips-darwin-arm64": "1.2.4" 772 + } 773 + }, 774 + "node_modules/@img/sharp-darwin-x64": { 775 + "version": "0.34.5", 776 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", 777 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", 778 + "cpu": [ 779 + "x64" 780 + ], 781 + "license": "Apache-2.0", 782 + "optional": true, 783 + "os": [ 784 + "darwin" 785 + ], 786 + "engines": { 787 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 788 + }, 789 + "funding": { 790 + "url": "https://opencollective.com/libvips" 791 + }, 792 + "optionalDependencies": { 793 + "@img/sharp-libvips-darwin-x64": "1.2.4" 794 + } 795 + }, 796 + "node_modules/@img/sharp-libvips-darwin-arm64": { 797 + "version": "1.2.4", 798 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", 799 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", 800 + "cpu": [ 801 + "arm64" 802 + ], 803 + "license": "LGPL-3.0-or-later", 804 + "optional": true, 805 + "os": [ 806 + "darwin" 807 + ], 808 + "funding": { 809 + "url": "https://opencollective.com/libvips" 810 + } 811 + }, 812 + "node_modules/@img/sharp-libvips-darwin-x64": { 813 + "version": "1.2.4", 814 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", 815 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", 816 + "cpu": [ 817 + "x64" 818 + ], 819 + "license": "LGPL-3.0-or-later", 820 + "optional": true, 821 + "os": [ 822 + "darwin" 823 + ], 824 + "funding": { 825 + "url": "https://opencollective.com/libvips" 826 + } 827 + }, 828 + "node_modules/@img/sharp-libvips-linux-arm": { 829 + "version": "1.2.4", 830 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", 831 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", 832 + "cpu": [ 833 + "arm" 834 + ], 835 + "libc": [ 836 + "glibc" 837 + ], 838 + "license": "LGPL-3.0-or-later", 839 + "optional": true, 840 + "os": [ 841 + "linux" 842 + ], 843 + "funding": { 844 + "url": "https://opencollective.com/libvips" 845 + } 846 + }, 847 + "node_modules/@img/sharp-libvips-linux-arm64": { 848 + "version": "1.2.4", 849 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", 850 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", 851 + "cpu": [ 852 + "arm64" 853 + ], 854 + "libc": [ 855 + "glibc" 856 + ], 857 + "license": "LGPL-3.0-or-later", 858 + "optional": true, 859 + "os": [ 860 + "linux" 861 + ], 862 + "funding": { 863 + "url": "https://opencollective.com/libvips" 864 + } 865 + }, 866 + "node_modules/@img/sharp-libvips-linux-ppc64": { 867 + "version": "1.2.4", 868 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", 869 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", 870 + "cpu": [ 871 + "ppc64" 872 + ], 873 + "libc": [ 874 + "glibc" 875 + ], 876 + "license": "LGPL-3.0-or-later", 877 + "optional": true, 878 + "os": [ 879 + "linux" 880 + ], 881 + "funding": { 882 + "url": "https://opencollective.com/libvips" 883 + } 884 + }, 885 + "node_modules/@img/sharp-libvips-linux-riscv64": { 886 + "version": "1.2.4", 887 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", 888 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", 889 + "cpu": [ 890 + "riscv64" 891 + ], 892 + "libc": [ 893 + "glibc" 894 + ], 895 + "license": "LGPL-3.0-or-later", 896 + "optional": true, 897 + "os": [ 898 + "linux" 899 + ], 900 + "funding": { 901 + "url": "https://opencollective.com/libvips" 902 + } 903 + }, 904 + "node_modules/@img/sharp-libvips-linux-s390x": { 905 + "version": "1.2.4", 906 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", 907 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", 908 + "cpu": [ 909 + "s390x" 910 + ], 911 + "libc": [ 912 + "glibc" 913 + ], 914 + "license": "LGPL-3.0-or-later", 915 + "optional": true, 916 + "os": [ 917 + "linux" 918 + ], 919 + "funding": { 920 + "url": "https://opencollective.com/libvips" 921 + } 922 + }, 923 + "node_modules/@img/sharp-libvips-linux-x64": { 924 + "version": "1.2.4", 925 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", 926 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", 927 + "cpu": [ 928 + "x64" 929 + ], 930 + "libc": [ 931 + "glibc" 932 + ], 933 + "license": "LGPL-3.0-or-later", 934 + "optional": true, 935 + "os": [ 936 + "linux" 937 + ], 938 + "funding": { 939 + "url": "https://opencollective.com/libvips" 940 + } 941 + }, 942 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 943 + "version": "1.2.4", 944 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", 945 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", 946 + "cpu": [ 947 + "arm64" 948 + ], 949 + "libc": [ 950 + "musl" 951 + ], 952 + "license": "LGPL-3.0-or-later", 953 + "optional": true, 954 + "os": [ 955 + "linux" 956 + ], 957 + "funding": { 958 + "url": "https://opencollective.com/libvips" 959 + } 960 + }, 961 + "node_modules/@img/sharp-libvips-linuxmusl-x64": { 962 + "version": "1.2.4", 963 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", 964 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", 965 + "cpu": [ 966 + "x64" 967 + ], 968 + "libc": [ 969 + "musl" 970 + ], 971 + "license": "LGPL-3.0-or-later", 972 + "optional": true, 973 + "os": [ 974 + "linux" 975 + ], 976 + "funding": { 977 + "url": "https://opencollective.com/libvips" 978 + } 979 + }, 980 + "node_modules/@img/sharp-linux-arm": { 981 + "version": "0.34.5", 982 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", 983 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", 984 + "cpu": [ 985 + "arm" 986 + ], 987 + "libc": [ 988 + "glibc" 989 + ], 990 + "license": "Apache-2.0", 991 + "optional": true, 992 + "os": [ 993 + "linux" 994 + ], 995 + "engines": { 996 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 997 + }, 998 + "funding": { 999 + "url": "https://opencollective.com/libvips" 1000 + }, 1001 + "optionalDependencies": { 1002 + "@img/sharp-libvips-linux-arm": "1.2.4" 1003 + } 1004 + }, 1005 + "node_modules/@img/sharp-linux-arm64": { 1006 + "version": "0.34.5", 1007 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", 1008 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", 1009 + "cpu": [ 1010 + "arm64" 1011 + ], 1012 + "libc": [ 1013 + "glibc" 1014 + ], 1015 + "license": "Apache-2.0", 1016 + "optional": true, 1017 + "os": [ 1018 + "linux" 1019 + ], 1020 + "engines": { 1021 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1022 + }, 1023 + "funding": { 1024 + "url": "https://opencollective.com/libvips" 1025 + }, 1026 + "optionalDependencies": { 1027 + "@img/sharp-libvips-linux-arm64": "1.2.4" 1028 + } 1029 + }, 1030 + "node_modules/@img/sharp-linux-ppc64": { 1031 + "version": "0.34.5", 1032 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", 1033 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", 1034 + "cpu": [ 1035 + "ppc64" 1036 + ], 1037 + "libc": [ 1038 + "glibc" 1039 + ], 1040 + "license": "Apache-2.0", 1041 + "optional": true, 1042 + "os": [ 1043 + "linux" 1044 + ], 1045 + "engines": { 1046 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1047 + }, 1048 + "funding": { 1049 + "url": "https://opencollective.com/libvips" 1050 + }, 1051 + "optionalDependencies": { 1052 + "@img/sharp-libvips-linux-ppc64": "1.2.4" 1053 + } 1054 + }, 1055 + "node_modules/@img/sharp-linux-riscv64": { 1056 + "version": "0.34.5", 1057 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", 1058 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", 1059 + "cpu": [ 1060 + "riscv64" 1061 + ], 1062 + "libc": [ 1063 + "glibc" 1064 + ], 1065 + "license": "Apache-2.0", 1066 + "optional": true, 1067 + "os": [ 1068 + "linux" 1069 + ], 1070 + "engines": { 1071 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1072 + }, 1073 + "funding": { 1074 + "url": "https://opencollective.com/libvips" 1075 + }, 1076 + "optionalDependencies": { 1077 + "@img/sharp-libvips-linux-riscv64": "1.2.4" 1078 + } 1079 + }, 1080 + "node_modules/@img/sharp-linux-s390x": { 1081 + "version": "0.34.5", 1082 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", 1083 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", 1084 + "cpu": [ 1085 + "s390x" 1086 + ], 1087 + "libc": [ 1088 + "glibc" 1089 + ], 1090 + "license": "Apache-2.0", 1091 + "optional": true, 1092 + "os": [ 1093 + "linux" 1094 + ], 1095 + "engines": { 1096 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1097 + }, 1098 + "funding": { 1099 + "url": "https://opencollective.com/libvips" 1100 + }, 1101 + "optionalDependencies": { 1102 + "@img/sharp-libvips-linux-s390x": "1.2.4" 1103 + } 1104 + }, 1105 + "node_modules/@img/sharp-linux-x64": { 1106 + "version": "0.34.5", 1107 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", 1108 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", 1109 + "cpu": [ 1110 + "x64" 1111 + ], 1112 + "libc": [ 1113 + "glibc" 1114 + ], 1115 + "license": "Apache-2.0", 1116 + "optional": true, 1117 + "os": [ 1118 + "linux" 1119 + ], 1120 + "engines": { 1121 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1122 + }, 1123 + "funding": { 1124 + "url": "https://opencollective.com/libvips" 1125 + }, 1126 + "optionalDependencies": { 1127 + "@img/sharp-libvips-linux-x64": "1.2.4" 1128 + } 1129 + }, 1130 + "node_modules/@img/sharp-linuxmusl-arm64": { 1131 + "version": "0.34.5", 1132 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", 1133 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", 1134 + "cpu": [ 1135 + "arm64" 1136 + ], 1137 + "libc": [ 1138 + "musl" 1139 + ], 1140 + "license": "Apache-2.0", 1141 + "optional": true, 1142 + "os": [ 1143 + "linux" 1144 + ], 1145 + "engines": { 1146 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1147 + }, 1148 + "funding": { 1149 + "url": "https://opencollective.com/libvips" 1150 + }, 1151 + "optionalDependencies": { 1152 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" 1153 + } 1154 + }, 1155 + "node_modules/@img/sharp-linuxmusl-x64": { 1156 + "version": "0.34.5", 1157 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", 1158 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", 1159 + "cpu": [ 1160 + "x64" 1161 + ], 1162 + "libc": [ 1163 + "musl" 1164 + ], 1165 + "license": "Apache-2.0", 1166 + "optional": true, 1167 + "os": [ 1168 + "linux" 1169 + ], 1170 + "engines": { 1171 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1172 + }, 1173 + "funding": { 1174 + "url": "https://opencollective.com/libvips" 1175 + }, 1176 + "optionalDependencies": { 1177 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" 1178 + } 1179 + }, 1180 + "node_modules/@img/sharp-wasm32": { 1181 + "version": "0.34.5", 1182 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", 1183 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", 1184 + "cpu": [ 1185 + "wasm32" 1186 + ], 1187 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 1188 + "optional": true, 1189 + "dependencies": { 1190 + "@emnapi/runtime": "^1.7.0" 1191 + }, 1192 + "engines": { 1193 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1194 + }, 1195 + "funding": { 1196 + "url": "https://opencollective.com/libvips" 1197 + } 1198 + }, 1199 + "node_modules/@img/sharp-win32-arm64": { 1200 + "version": "0.34.5", 1201 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", 1202 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", 1203 + "cpu": [ 1204 + "arm64" 1205 + ], 1206 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1207 + "optional": true, 1208 + "os": [ 1209 + "win32" 1210 + ], 1211 + "engines": { 1212 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1213 + }, 1214 + "funding": { 1215 + "url": "https://opencollective.com/libvips" 1216 + } 1217 + }, 1218 + "node_modules/@img/sharp-win32-ia32": { 1219 + "version": "0.34.5", 1220 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", 1221 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", 1222 + "cpu": [ 1223 + "ia32" 1224 + ], 1225 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1226 + "optional": true, 1227 + "os": [ 1228 + "win32" 1229 + ], 1230 + "engines": { 1231 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1232 + }, 1233 + "funding": { 1234 + "url": "https://opencollective.com/libvips" 1235 + } 1236 + }, 1237 + "node_modules/@img/sharp-win32-x64": { 1238 + "version": "0.34.5", 1239 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", 1240 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", 1241 + "cpu": [ 1242 + "x64" 1243 + ], 1244 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1245 + "optional": true, 1246 + "os": [ 1247 + "win32" 1248 + ], 1249 + "engines": { 1250 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1251 + }, 1252 + "funding": { 1253 + "url": "https://opencollective.com/libvips" 1254 + } 1255 + }, 704 1256 "node_modules/@jridgewell/gen-mapping": { 705 1257 "version": "0.3.13", 706 1258 "dev": true, ··· 745 1297 "version": "1.0.0-next.29", 746 1298 "dev": true, 747 1299 "license": "MIT" 1300 + }, 1301 + "node_modules/@protobufjs/aspromise": { 1302 + "version": "1.1.2", 1303 + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 1304 + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", 1305 + "license": "BSD-3-Clause" 1306 + }, 1307 + "node_modules/@protobufjs/base64": { 1308 + "version": "1.1.2", 1309 + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 1310 + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 1311 + "license": "BSD-3-Clause" 1312 + }, 1313 + "node_modules/@protobufjs/codegen": { 1314 + "version": "2.0.4", 1315 + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 1316 + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", 1317 + "license": "BSD-3-Clause" 1318 + }, 1319 + "node_modules/@protobufjs/eventemitter": { 1320 + "version": "1.1.0", 1321 + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 1322 + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", 1323 + "license": "BSD-3-Clause" 1324 + }, 1325 + "node_modules/@protobufjs/fetch": { 1326 + "version": "1.1.0", 1327 + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 1328 + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 1329 + "license": "BSD-3-Clause", 1330 + "dependencies": { 1331 + "@protobufjs/aspromise": "^1.1.1", 1332 + "@protobufjs/inquire": "^1.1.0" 1333 + } 1334 + }, 1335 + "node_modules/@protobufjs/float": { 1336 + "version": "1.0.2", 1337 + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 1338 + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", 1339 + "license": "BSD-3-Clause" 1340 + }, 1341 + "node_modules/@protobufjs/inquire": { 1342 + "version": "1.1.0", 1343 + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 1344 + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", 1345 + "license": "BSD-3-Clause" 1346 + }, 1347 + "node_modules/@protobufjs/path": { 1348 + "version": "1.1.2", 1349 + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 1350 + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", 1351 + "license": "BSD-3-Clause" 1352 + }, 1353 + "node_modules/@protobufjs/pool": { 1354 + "version": "1.1.0", 1355 + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 1356 + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", 1357 + "license": "BSD-3-Clause" 1358 + }, 1359 + "node_modules/@protobufjs/utf8": { 1360 + "version": "1.1.0", 1361 + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 1362 + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", 1363 + "license": "BSD-3-Clause" 748 1364 }, 749 1365 "node_modules/@rollup/rollup-android-arm-eabi": { 750 1366 "version": "4.60.1", ··· 1249 1865 "dev": true, 1250 1866 "license": "MIT" 1251 1867 }, 1868 + "node_modules/@types/node": { 1869 + "version": "25.5.0", 1870 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", 1871 + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", 1872 + "license": "MIT", 1873 + "dependencies": { 1874 + "undici-types": "~7.18.0" 1875 + } 1876 + }, 1252 1877 "node_modules/@types/trusted-types": { 1253 1878 "version": "2.0.7", 1254 1879 "dev": true, ··· 1277 1902 "node": ">=0.4.0" 1278 1903 } 1279 1904 }, 1905 + "node_modules/adm-zip": { 1906 + "version": "0.5.17", 1907 + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", 1908 + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", 1909 + "license": "MIT", 1910 + "engines": { 1911 + "node": ">=12.0" 1912 + } 1913 + }, 1280 1914 "node_modules/aria-query": { 1281 1915 "version": "5.3.1", 1282 1916 "dev": true, ··· 1299 1933 "node": ">= 0.4" 1300 1934 } 1301 1935 }, 1936 + "node_modules/boolean": { 1937 + "version": "3.2.0", 1938 + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", 1939 + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", 1940 + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", 1941 + "license": "MIT" 1942 + }, 1302 1943 "node_modules/chokidar": { 1303 1944 "version": "4.0.3", 1304 1945 "dev": true, ··· 1348 1989 "node": ">=0.10.0" 1349 1990 } 1350 1991 }, 1992 + "node_modules/define-data-property": { 1993 + "version": "1.1.4", 1994 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 1995 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 1996 + "license": "MIT", 1997 + "dependencies": { 1998 + "es-define-property": "^1.0.0", 1999 + "es-errors": "^1.3.0", 2000 + "gopd": "^1.0.1" 2001 + }, 2002 + "engines": { 2003 + "node": ">= 0.4" 2004 + }, 2005 + "funding": { 2006 + "url": "https://github.com/sponsors/ljharb" 2007 + } 2008 + }, 2009 + "node_modules/define-properties": { 2010 + "version": "1.2.1", 2011 + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", 2012 + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", 2013 + "license": "MIT", 2014 + "dependencies": { 2015 + "define-data-property": "^1.0.1", 2016 + "has-property-descriptors": "^1.0.0", 2017 + "object-keys": "^1.1.1" 2018 + }, 2019 + "engines": { 2020 + "node": ">= 0.4" 2021 + }, 2022 + "funding": { 2023 + "url": "https://github.com/sponsors/ljharb" 2024 + } 2025 + }, 2026 + "node_modules/detect-libc": { 2027 + "version": "2.1.2", 2028 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 2029 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 2030 + "license": "Apache-2.0", 2031 + "engines": { 2032 + "node": ">=8" 2033 + } 2034 + }, 2035 + "node_modules/detect-node": { 2036 + "version": "2.1.0", 2037 + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", 2038 + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", 2039 + "license": "MIT" 2040 + }, 1351 2041 "node_modules/devalue": { 1352 2042 "version": "5.6.4", 1353 2043 "dev": true, 1354 2044 "license": "MIT" 1355 2045 }, 2046 + "node_modules/es-define-property": { 2047 + "version": "1.0.1", 2048 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 2049 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 2050 + "license": "MIT", 2051 + "engines": { 2052 + "node": ">= 0.4" 2053 + } 2054 + }, 2055 + "node_modules/es-errors": { 2056 + "version": "1.3.0", 2057 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 2058 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 2059 + "license": "MIT", 2060 + "engines": { 2061 + "node": ">= 0.4" 2062 + } 2063 + }, 2064 + "node_modules/es6-error": { 2065 + "version": "4.1.1", 2066 + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", 2067 + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", 2068 + "license": "MIT" 2069 + }, 1356 2070 "node_modules/esbuild": { 1357 2071 "version": "0.27.4", 1358 2072 "dev": true, ··· 1393 2107 "@esbuild/win32-x64": "0.27.4" 1394 2108 } 1395 2109 }, 2110 + "node_modules/escape-string-regexp": { 2111 + "version": "4.0.0", 2112 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2113 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2114 + "license": "MIT", 2115 + "engines": { 2116 + "node": ">=10" 2117 + }, 2118 + "funding": { 2119 + "url": "https://github.com/sponsors/sindresorhus" 2120 + } 2121 + }, 1396 2122 "node_modules/esm-env": { 1397 2123 "version": "1.2.2", 1398 2124 "dev": true, ··· 1423 2149 } 1424 2150 } 1425 2151 }, 2152 + "node_modules/flatbuffers": { 2153 + "version": "25.9.23", 2154 + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", 2155 + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", 2156 + "license": "Apache-2.0" 2157 + }, 1426 2158 "node_modules/fsevents": { 1427 2159 "version": "2.3.3", 1428 2160 "dev": true, ··· 1435 2167 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1436 2168 } 1437 2169 }, 2170 + "node_modules/global-agent": { 2171 + "version": "3.0.0", 2172 + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", 2173 + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", 2174 + "license": "BSD-3-Clause", 2175 + "dependencies": { 2176 + "boolean": "^3.0.1", 2177 + "es6-error": "^4.1.1", 2178 + "matcher": "^3.0.0", 2179 + "roarr": "^2.15.3", 2180 + "semver": "^7.3.2", 2181 + "serialize-error": "^7.0.1" 2182 + }, 2183 + "engines": { 2184 + "node": ">=10.0" 2185 + } 2186 + }, 2187 + "node_modules/globalthis": { 2188 + "version": "1.0.4", 2189 + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", 2190 + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", 2191 + "license": "MIT", 2192 + "dependencies": { 2193 + "define-properties": "^1.2.1", 2194 + "gopd": "^1.0.1" 2195 + }, 2196 + "engines": { 2197 + "node": ">= 0.4" 2198 + }, 2199 + "funding": { 2200 + "url": "https://github.com/sponsors/ljharb" 2201 + } 2202 + }, 2203 + "node_modules/gopd": { 2204 + "version": "1.2.0", 2205 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 2206 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 2207 + "license": "MIT", 2208 + "engines": { 2209 + "node": ">= 0.4" 2210 + }, 2211 + "funding": { 2212 + "url": "https://github.com/sponsors/ljharb" 2213 + } 2214 + }, 2215 + "node_modules/guid-typescript": { 2216 + "version": "1.0.9", 2217 + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", 2218 + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", 2219 + "license": "ISC" 2220 + }, 2221 + "node_modules/has-property-descriptors": { 2222 + "version": "1.0.2", 2223 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 2224 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 2225 + "license": "MIT", 2226 + "dependencies": { 2227 + "es-define-property": "^1.0.0" 2228 + }, 2229 + "funding": { 2230 + "url": "https://github.com/sponsors/ljharb" 2231 + } 2232 + }, 1438 2233 "node_modules/hls.js": { 1439 2234 "version": "1.6.15", 1440 2235 "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", ··· 1464 2259 "url": "https://github.com/sponsors/panva" 1465 2260 } 1466 2261 }, 2262 + "node_modules/json-stringify-safe": { 2263 + "version": "5.0.1", 2264 + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 2265 + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", 2266 + "license": "ISC" 2267 + }, 1467 2268 "node_modules/kleur": { 1468 2269 "version": "4.1.5", 1469 2270 "dev": true, ··· 1477 2278 "dev": true, 1478 2279 "license": "MIT" 1479 2280 }, 2281 + "node_modules/long": { 2282 + "version": "5.3.2", 2283 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", 2284 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", 2285 + "license": "Apache-2.0" 2286 + }, 1480 2287 "node_modules/lru-cache": { 1481 2288 "version": "10.4.3", 1482 2289 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", ··· 1491 2298 "@jridgewell/sourcemap-codec": "^1.5.5" 1492 2299 } 1493 2300 }, 2301 + "node_modules/matcher": { 2302 + "version": "3.0.0", 2303 + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", 2304 + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", 2305 + "license": "MIT", 2306 + "dependencies": { 2307 + "escape-string-regexp": "^4.0.0" 2308 + }, 2309 + "engines": { 2310 + "node": ">=10" 2311 + } 2312 + }, 1494 2313 "node_modules/mri": { 1495 2314 "version": "1.2.0", 1496 2315 "dev": true, ··· 1530 2349 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1531 2350 } 1532 2351 }, 2352 + "node_modules/object-keys": { 2353 + "version": "1.1.1", 2354 + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 2355 + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 2356 + "license": "MIT", 2357 + "engines": { 2358 + "node": ">= 0.4" 2359 + } 2360 + }, 1533 2361 "node_modules/obug": { 1534 2362 "version": "2.1.1", 1535 2363 "dev": true, ··· 1539 2367 ], 1540 2368 "license": "MIT" 1541 2369 }, 2370 + "node_modules/onnxruntime-common": { 2371 + "version": "1.24.3", 2372 + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", 2373 + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", 2374 + "license": "MIT" 2375 + }, 2376 + "node_modules/onnxruntime-node": { 2377 + "version": "1.24.3", 2378 + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", 2379 + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", 2380 + "hasInstallScript": true, 2381 + "license": "MIT", 2382 + "os": [ 2383 + "win32", 2384 + "darwin", 2385 + "linux" 2386 + ], 2387 + "dependencies": { 2388 + "adm-zip": "^0.5.16", 2389 + "global-agent": "^3.0.0", 2390 + "onnxruntime-common": "1.24.3" 2391 + } 2392 + }, 2393 + "node_modules/onnxruntime-web": { 2394 + "version": "1.25.0-dev.20260327-722743c0e2", 2395 + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.25.0-dev.20260327-722743c0e2.tgz", 2396 + "integrity": "sha512-8PXdZy4Ekhg10CLg+cFFt39b4tFDGMRJB6lGjnQL6eA+2boUQYDymZ0gtxiS+H6oIWoCjQp/ziyirvFbaFKfiw==", 2397 + "license": "MIT", 2398 + "dependencies": { 2399 + "flatbuffers": "^25.1.24", 2400 + "guid-typescript": "^1.0.9", 2401 + "long": "^5.2.3", 2402 + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", 2403 + "platform": "^1.3.6", 2404 + "protobufjs": "^7.2.4" 2405 + } 2406 + }, 2407 + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { 2408 + "version": "1.24.0-dev.20251116-b39e144322", 2409 + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", 2410 + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", 2411 + "license": "MIT" 2412 + }, 1542 2413 "node_modules/picocolors": { 1543 2414 "version": "1.1.1", 1544 2415 "dev": true, ··· 1554 2425 "funding": { 1555 2426 "url": "https://github.com/sponsors/jonschlinkert" 1556 2427 } 2428 + }, 2429 + "node_modules/platform": { 2430 + "version": "1.3.6", 2431 + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", 2432 + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", 2433 + "license": "MIT" 1557 2434 }, 1558 2435 "node_modules/postcss": { 1559 2436 "version": "8.5.8", ··· 1582 2459 "node": "^10 || ^12 || >=14" 1583 2460 } 1584 2461 }, 2462 + "node_modules/protobufjs": { 2463 + "version": "7.5.4", 2464 + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", 2465 + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", 2466 + "hasInstallScript": true, 2467 + "license": "BSD-3-Clause", 2468 + "dependencies": { 2469 + "@protobufjs/aspromise": "^1.1.2", 2470 + "@protobufjs/base64": "^1.1.2", 2471 + "@protobufjs/codegen": "^2.0.4", 2472 + "@protobufjs/eventemitter": "^1.1.0", 2473 + "@protobufjs/fetch": "^1.1.0", 2474 + "@protobufjs/float": "^1.0.2", 2475 + "@protobufjs/inquire": "^1.1.0", 2476 + "@protobufjs/path": "^1.1.2", 2477 + "@protobufjs/pool": "^1.1.0", 2478 + "@protobufjs/utf8": "^1.1.0", 2479 + "@types/node": ">=13.7.0", 2480 + "long": "^5.0.0" 2481 + }, 2482 + "engines": { 2483 + "node": ">=12.0.0" 2484 + } 2485 + }, 1585 2486 "node_modules/readdirp": { 1586 2487 "version": "4.1.2", 1587 2488 "dev": true, ··· 1592 2493 "funding": { 1593 2494 "type": "individual", 1594 2495 "url": "https://paulmillr.com/funding/" 2496 + } 2497 + }, 2498 + "node_modules/roarr": { 2499 + "version": "2.15.4", 2500 + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", 2501 + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", 2502 + "license": "BSD-3-Clause", 2503 + "dependencies": { 2504 + "boolean": "^3.0.1", 2505 + "detect-node": "^2.0.4", 2506 + "globalthis": "^1.0.1", 2507 + "json-stringify-safe": "^5.0.1", 2508 + "semver-compare": "^1.0.0", 2509 + "sprintf-js": "^1.1.2" 2510 + }, 2511 + "engines": { 2512 + "node": ">=8.0" 1595 2513 } 1596 2514 }, 1597 2515 "node_modules/rollup": { ··· 1648 2566 "node": ">=6" 1649 2567 } 1650 2568 }, 2569 + "node_modules/semver": { 2570 + "version": "7.7.4", 2571 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 2572 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 2573 + "license": "ISC", 2574 + "bin": { 2575 + "semver": "bin/semver.js" 2576 + }, 2577 + "engines": { 2578 + "node": ">=10" 2579 + } 2580 + }, 2581 + "node_modules/semver-compare": { 2582 + "version": "1.0.0", 2583 + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", 2584 + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", 2585 + "license": "MIT" 2586 + }, 2587 + "node_modules/serialize-error": { 2588 + "version": "7.0.1", 2589 + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", 2590 + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", 2591 + "license": "MIT", 2592 + "dependencies": { 2593 + "type-fest": "^0.13.1" 2594 + }, 2595 + "engines": { 2596 + "node": ">=10" 2597 + }, 2598 + "funding": { 2599 + "url": "https://github.com/sponsors/sindresorhus" 2600 + } 2601 + }, 1651 2602 "node_modules/set-cookie-parser": { 1652 2603 "version": "3.1.0", 1653 2604 "dev": true, 1654 2605 "license": "MIT" 1655 2606 }, 2607 + "node_modules/sharp": { 2608 + "version": "0.34.5", 2609 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", 2610 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", 2611 + "hasInstallScript": true, 2612 + "license": "Apache-2.0", 2613 + "dependencies": { 2614 + "@img/colour": "^1.0.0", 2615 + "detect-libc": "^2.1.2", 2616 + "semver": "^7.7.3" 2617 + }, 2618 + "engines": { 2619 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2620 + }, 2621 + "funding": { 2622 + "url": "https://opencollective.com/libvips" 2623 + }, 2624 + "optionalDependencies": { 2625 + "@img/sharp-darwin-arm64": "0.34.5", 2626 + "@img/sharp-darwin-x64": "0.34.5", 2627 + "@img/sharp-libvips-darwin-arm64": "1.2.4", 2628 + "@img/sharp-libvips-darwin-x64": "1.2.4", 2629 + "@img/sharp-libvips-linux-arm": "1.2.4", 2630 + "@img/sharp-libvips-linux-arm64": "1.2.4", 2631 + "@img/sharp-libvips-linux-ppc64": "1.2.4", 2632 + "@img/sharp-libvips-linux-riscv64": "1.2.4", 2633 + "@img/sharp-libvips-linux-s390x": "1.2.4", 2634 + "@img/sharp-libvips-linux-x64": "1.2.4", 2635 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", 2636 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", 2637 + "@img/sharp-linux-arm": "0.34.5", 2638 + "@img/sharp-linux-arm64": "0.34.5", 2639 + "@img/sharp-linux-ppc64": "0.34.5", 2640 + "@img/sharp-linux-riscv64": "0.34.5", 2641 + "@img/sharp-linux-s390x": "0.34.5", 2642 + "@img/sharp-linux-x64": "0.34.5", 2643 + "@img/sharp-linuxmusl-arm64": "0.34.5", 2644 + "@img/sharp-linuxmusl-x64": "0.34.5", 2645 + "@img/sharp-wasm32": "0.34.5", 2646 + "@img/sharp-win32-arm64": "0.34.5", 2647 + "@img/sharp-win32-ia32": "0.34.5", 2648 + "@img/sharp-win32-x64": "0.34.5" 2649 + } 2650 + }, 1656 2651 "node_modules/sirv": { 1657 2652 "version": "3.0.2", 1658 2653 "dev": true, ··· 1673 2668 "engines": { 1674 2669 "node": ">=0.10.0" 1675 2670 } 2671 + }, 2672 + "node_modules/sprintf-js": { 2673 + "version": "1.1.3", 2674 + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", 2675 + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", 2676 + "license": "BSD-3-Clause" 1676 2677 }, 1677 2678 "node_modules/svelte": { 1678 2679 "version": "5.55.1", ··· 1760 2761 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1761 2762 "license": "0BSD" 1762 2763 }, 2764 + "node_modules/type-fest": { 2765 + "version": "0.13.1", 2766 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", 2767 + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", 2768 + "license": "(MIT OR CC0-1.0)", 2769 + "engines": { 2770 + "node": ">=10" 2771 + }, 2772 + "funding": { 2773 + "url": "https://github.com/sponsors/sindresorhus" 2774 + } 2775 + }, 1763 2776 "node_modules/typescript": { 1764 2777 "version": "5.9.3", 1765 2778 "dev": true, ··· 1780 2793 "dependencies": { 1781 2794 "multiformats": "^9.4.2" 1782 2795 } 2796 + }, 2797 + "node_modules/undici-types": { 2798 + "version": "7.18.2", 2799 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", 2800 + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", 2801 + "license": "MIT" 1783 2802 }, 1784 2803 "node_modules/unicode-segmenter": { 1785 2804 "version": "0.14.5",
+1
package.json
··· 24 24 "dependencies": { 25 25 "@atproto/api": "^0.19.5", 26 26 "@atproto/oauth-client-browser": "^0.3.41", 27 + "@huggingface/transformers": "^4.0.0", 27 28 "hls.js": "^1.6.15" 28 29 } 29 30 }
spec/cc_off.png

This is a binary file and will not be displayed.

spec/cc_on.png

This is a binary file and will not be displayed.

+139 -53
src/lib/FrogHeader.svelte
··· 7 7 <script lang="ts"> 8 8 import LoginButton from './LoginButton.svelte'; 9 9 import WavyButton from './WavyButton.svelte'; 10 + import WavyBorder from './WavyBorder.svelte'; 10 11 import { playCroak } from './croak'; 11 12 import { getSettings, setSoundEnabled, setPacifistMode } from './settings.svelte'; 13 + import { getModelStatus, getModelProgress, getModelError, loadModel } from './captions.svelte'; 12 14 13 15 let { onHomeClick }: { onHomeClick?: () => void } = $props(); 14 16 const settings = getSettings(); ··· 127 129 <!-- svelte-ignore a11y_no_static_element_interactions --> 128 130 <div class="modal-backdrop" onclick={onBackdropClick}> 129 131 <div class="modal-content"> 130 - <div class="modal-body"> 131 - <h2 class="modal-title">credits</h2> 132 - <div class="credit-item"> 133 - <p class="credit-label">Frog Croaking</p> 134 - <p class="credit-author">by DrinkingWindGames</p> 135 - <a href="https://freesound.org/s/848549/" target="_blank" class="credit-link"> 136 - freesound.org/s/848549/ 137 - </a> 138 - <p class="credit-license">License: Attribution 4.0</p> 132 + <WavyBorder seed="credits-modal" fill="#39FF44" strokeColor="#0A182B" strokeWidth={2.5} padding={48}> 133 + <div class="modal-body"> 134 + <h2 class="modal-title">credits</h2> 135 + <div class="credit-item"> 136 + <p class="credit-label">Frog Croaking</p> 137 + <p class="credit-author">by DrinkingWindGames</p> 138 + <a href="https://freesound.org/s/848549/" target="_blank" class="credit-link"> 139 + freesound.org/s/848549/ 140 + </a> 141 + <p class="credit-license">License: Attribution 4.0</p> 142 + </div> 143 + <WavyButton seed="close-credits" fill="#0A182B" textColor="#FFDEED" onclick={closeCredits}>close</WavyButton> 139 144 </div> 140 - <button class="close-btn" onclick={closeCredits}>close</button> 141 - </div> 145 + </WavyBorder> 142 146 </div> 143 147 </div> 144 148 {/if} ··· 147 151 <!-- svelte-ignore a11y_no_static_element_interactions --> 148 152 <div class="modal-backdrop" onclick={onBackdropClick}> 149 153 <div class="modal-content"> 150 - <div class="modal-body"> 151 - <h2 class="modal-title">settings</h2> 154 + <WavyBorder seed="settings-modal" fill="#39FF44" strokeColor="#0A182B" strokeWidth={2.5} padding={48}> 155 + <div class="modal-body"> 156 + <h2 class="modal-title">settings</h2> 152 157 153 - <div class="setting-row"> 154 - <span class="setting-label">sound</span> 155 - <WavyButton 156 - seed="toggle-sound" 157 - fill={settings.soundEnabled ? '#39FF44' : '#0A182B'} 158 - textColor={settings.soundEnabled ? '#0A182B' : '#FFDEED'} 159 - onclick={() => setSoundEnabled(!settings.soundEnabled)} 160 - >{settings.soundEnabled ? 'on' : 'off'}</WavyButton> 161 - </div> 158 + <div class="setting-row"> 159 + <span class="setting-label">sound</span> 160 + <WavyButton 161 + seed="toggle-sound" 162 + fill={settings.soundEnabled ? '#39FF44' : '#0A182B'} 163 + textColor={settings.soundEnabled ? '#0A182B' : '#FFDEED'} 164 + onclick={() => setSoundEnabled(!settings.soundEnabled)} 165 + >{settings.soundEnabled ? 'on' : 'off'}</WavyButton> 166 + </div> 162 167 163 - <div class="setting-row"> 164 - <span class="setting-label">pacifist mode</span> 165 - <WavyButton 166 - seed="toggle-pacifist" 167 - fill={settings.pacifistMode ? '#39FF44' : '#0A182B'} 168 - textColor={settings.pacifistMode ? '#0A182B' : '#FFDEED'} 169 - onclick={() => setPacifistMode(!settings.pacifistMode)} 170 - >{settings.pacifistMode ? 'on' : 'off'}</WavyButton> 171 - </div> 168 + <div class="setting-row"> 169 + <span class="setting-label">pacifist mode</span> 170 + <WavyButton 171 + seed="toggle-pacifist" 172 + fill={settings.pacifistMode ? '#39FF44' : '#0A182B'} 173 + textColor={settings.pacifistMode ? '#0A182B' : '#FFDEED'} 174 + onclick={() => setPacifistMode(!settings.pacifistMode)} 175 + >{settings.pacifistMode ? 'on' : 'off'}</WavyButton> 176 + </div> 177 + 178 + <p class="setting-hint">pacifist mode disables the flies</p> 179 + 180 + <hr class="setting-divider" /> 181 + 182 + <h3 class="setting-section-title">closed captions</h3> 172 183 173 - <p class="setting-hint">pacifist mode disables the flies</p> 184 + {#if getModelStatus() === 'ready'} 185 + <div class="model-status ready"> 186 + <span class="model-dot"></span> 187 + model loaded 188 + </div> 189 + {:else if getModelStatus() === 'loading'} 190 + <div class="model-loading"> 191 + <p class="model-loading-text">downloading model... {Math.round(getModelProgress())}%</p> 192 + <div class="progress-track"> 193 + <div class="progress-fill" style="width: {getModelProgress()}%;"></div> 194 + </div> 195 + </div> 196 + {:else if getModelStatus() === 'error'} 197 + <p class="model-error">{getModelError()}</p> 198 + <WavyButton seed="retry-model" fill="#FF3992" textColor="#FFDEED" onclick={loadModel}>retry</WavyButton> 199 + {:else} 200 + <WavyButton seed="download-model" fill="#0A182B" textColor="#FFDEED" onclick={loadModel}>download</WavyButton> 201 + {/if} 174 202 175 - <button class="close-btn" onclick={closeSettings}>close</button> 176 - </div> 203 + <p class="setting-hint"> 204 + downloads whisper-tiny (~40mb) to run speech-to-text locally in your browser. 205 + captions are generated on-device — no audio is sent anywhere. 206 + results may be inaccurate, especially with background noise. 207 + </p> 208 + 209 + <WavyButton seed="close-settings" fill="#0A182B" textColor="#FFDEED" onclick={closeSettings}>close</WavyButton> 210 + </div> 211 + </WavyBorder> 177 212 </div> 178 213 </div> 179 214 {/if} ··· 297 332 298 333 .modal-content { 299 334 width: min(500px, 90vw); 300 - background: #39FF44; 301 - border: 3px solid #0A182B; 302 - border-radius: 24px; 303 - padding: 32px 24px; 304 335 } 305 336 306 337 .modal-body { ··· 341 372 opacity: 0.5; 342 373 margin: 0; 343 374 font-style: italic; 375 + max-width: 300px; 376 + line-height: 1.5; 377 + } 378 + 379 + .setting-divider { 380 + border: none; 381 + border-top: 1.5px solid rgba(10, 24, 43, 0.15); 382 + width: min(280px, 70vw); 383 + margin: 8px 0; 384 + } 385 + 386 + .setting-section-title { 387 + font-family: 'PicNic', cursive, system-ui; 388 + font-size: 1.2rem; 389 + color: #0A182B; 390 + margin: 0; 391 + } 392 + 393 + .model-status { 394 + font-family: 'Fang', system-ui, sans-serif; 395 + font-size: 0.9rem; 396 + color: #0A182B; 397 + display: flex; 398 + align-items: center; 399 + gap: 8px; 400 + } 401 + 402 + .model-dot { 403 + width: 10px; 404 + height: 10px; 405 + border-radius: 50%; 406 + background: #39FF44; 407 + box-shadow: 0 0 6px #39FF44; 408 + } 409 + 410 + .model-loading { 411 + display: flex; 412 + flex-direction: column; 413 + align-items: center; 414 + gap: 6px; 415 + width: min(280px, 70vw); 416 + } 417 + 418 + .model-loading-text { 419 + font-family: 'Fang', system-ui, sans-serif; 420 + font-size: 0.85rem; 421 + color: #0A182B; 422 + margin: 0; 423 + } 424 + 425 + .progress-track { 426 + width: 100%; 427 + height: 8px; 428 + background: rgba(10, 24, 43, 0.15); 429 + border-radius: 4px; 430 + overflow: hidden; 431 + } 432 + 433 + .progress-fill { 434 + height: 100%; 435 + background: #3992FF; 436 + border-radius: 4px; 437 + transition: width 0.3s ease; 438 + } 439 + 440 + .model-error { 441 + font-family: 'Fang', system-ui, sans-serif; 442 + font-size: 0.8rem; 443 + color: #FF3992; 444 + margin: 0; 344 445 } 345 446 346 447 .credit-item { ··· 386 487 font-style: italic; 387 488 } 388 489 389 - .close-btn { 390 - all: unset; 391 - font-family: 'PicNic', cursive, system-ui; 392 - font-size: 1rem; 393 - color: #FFDEED; 394 - background: #0A182B; 395 - padding: 8px 28px; 396 - border-radius: 30px; 397 - cursor: pointer; 398 - margin-top: 8px; 399 - transition: transform 0.15s ease; 400 - } 401 490 402 - .close-btn:hover { 403 - transform: scale(1.05); 404 - } 405 491 406 492 .subtitle-lines { 407 493 margin-top: 6px;
+44 -59
src/lib/LoginButton.svelte
··· 15 15 let inputEl: HTMLInputElement | undefined = $state(); 16 16 17 17 const seed = 'login-btn'; 18 - const { svgPath, clipPolygon } = generateWavyOval(seed); 19 - const logoutShape = generateWavyOval('logout-btn'); 20 - const inputShape = generateWavyOval('login-input', 46, 38); 18 + const loginPath = generateWavyOval(seed); 19 + const logoutPath = generateWavyOval('logout-btn'); 20 + const inputPath = generateWavyOval('login-input', 46, 38); 21 21 22 - function generateWavyOval(s: string, rx = 44, ry = 36): { svgPath: string; clipPolygon: string } { 22 + function generateWavyOval(s: string, rx = 44, ry = 36): string { 23 23 const cx = 50, cy = 50; 24 24 const amp = 3; 25 25 const points = 48; ··· 57 57 d += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`; 58 58 } 59 59 d += ' Z'; 60 - 61 - const clipPts = pts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', '); 62 - return { svgPath: d, clipPolygon: `polygon(${clipPts})` }; 60 + return d; 63 61 } 64 62 65 63 function handleLoginClick() { ··· 104 102 {/if} 105 103 <span class="handle-text">@{auth.handle}</span> 106 104 <button class="logout-btn" onclick={signOut}> 107 - <div class="btn-clipped" style="clip-path: {logoutShape.clipPolygon};"> 108 - <div class="btn-fill logout-fill"></div> 109 - <span class="btn-text">logout</span> 110 - </div> 105 + <svg class="btn-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 106 + <path d={logoutPath} fill="#FF3992" stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 107 + </svg> 108 + <span class="btn-text">logout</span> 111 109 </button> 112 110 </div> 113 111 {:else if showInput} 114 112 <form class="login-form" onsubmit={handleSubmit}> 115 113 <div class="input-wavy"> 116 - <div class="input-clipped" style="clip-path: {inputShape.clipPolygon};"> 117 - <div class="input-fill"></div> 118 - <input 119 - bind:this={inputEl} 120 - bind:value={handleValue} 121 - onkeydown={handleKeydown} 122 - type="text" 123 - placeholder="handle.bsky.social" 124 - class="handle-input" 125 - disabled={submitting} 126 - /> 127 - </div> 128 - 114 + <svg class="input-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 115 + <path d={inputPath} fill="#FFDEED" stroke="#0A182B" stroke-width="1.5" stroke-linejoin="round" /> 116 + </svg> 117 + <input 118 + bind:this={inputEl} 119 + bind:value={handleValue} 120 + onkeydown={handleKeydown} 121 + type="text" 122 + placeholder="handle.bsky.social" 123 + class="handle-input" 124 + disabled={submitting} 125 + /> 129 126 </div> 130 127 <button class="login-btn" type="submit" disabled={submitting}> 131 - <div class="btn-clipped" style="clip-path: {clipPolygon};"> 132 - <div class="btn-fill"></div> 133 - <span class="btn-text">{submitting ? '...' : 'go'}</span> 134 - </div> 128 + <svg class="btn-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 129 + <path d={loginPath} fill="#0A182B" stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 130 + </svg> 131 + <span class="btn-text">{submitting ? '...' : 'go'}</span> 135 132 </button> 136 133 </form> 137 134 {#if auth.error} ··· 139 136 {/if} 140 137 {:else} 141 138 <button class="login-btn" onclick={handleLoginClick}> 142 - <div class="btn-clipped" style="clip-path: {clipPolygon};"> 143 - <div class="btn-fill"></div> 144 - <span class="btn-text">login</span> 145 - </div> 139 + <svg class="btn-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 140 + <path d={loginPath} fill="#0A182B" stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 141 + </svg> 142 + <span class="btn-text">login</span> 146 143 </button> 147 144 {/if} 148 145 ··· 150 147 .login-btn, .logout-btn { 151 148 all: unset; 152 149 position: relative; 153 - width: clamp(100px, 12vw, 140px); 154 - height: clamp(44px, 5vw, 56px); 150 + width: clamp(120px, 14vw, 160px); 151 + height: clamp(50px, 6vw, 64px); 155 152 cursor: pointer; 156 153 transition: transform 0.15s ease; 154 + display: flex; 155 + align-items: center; 156 + justify-content: center; 157 157 } 158 158 159 159 .login-btn:hover, .logout-btn:hover { ··· 164 164 opacity: 0.6; 165 165 } 166 166 167 - .btn-clipped { 168 - position: relative; 167 + .btn-svg { 168 + position: absolute; 169 + inset: 0; 169 170 width: 100%; 170 171 height: 100%; 171 - display: flex; 172 - align-items: center; 173 - justify-content: center; 174 - } 175 - 176 - .btn-fill { 177 - position: absolute; 178 - inset: 0; 179 - background: #0A182B; 180 - } 181 - 182 - .logout-fill { 183 - background: #FF3992; 172 + overflow: visible; 184 173 } 185 174 186 175 .btn-text { ··· 230 219 231 220 .input-wavy { 232 221 position: relative; 233 - width: clamp(160px, 18vw, 240px); 234 - height: clamp(44px, 5vw, 56px); 235 - } 236 - 237 - .input-clipped { 238 - position: relative; 239 - width: 100%; 240 - height: 100%; 222 + width: clamp(180px, 20vw, 260px); 223 + height: clamp(50px, 6vw, 64px); 241 224 display: flex; 242 225 align-items: center; 243 226 justify-content: center; 244 227 } 245 228 246 - .input-fill { 229 + .input-svg { 247 230 position: absolute; 248 231 inset: 0; 249 - background: #FFDEED; 232 + width: 100%; 233 + height: 100%; 234 + overflow: visible; 250 235 } 251 236 252 237 .handle-input {
+79 -1
src/lib/VideoPlayer.svelte
··· 3 3 import Hls from "hls.js"; 4 4 import WavyBorder from "./WavyBorder.svelte"; 5 5 import { playCroak } from "./croak"; 6 + import { getModelStatus, getCaptionsEnabled, getCurrentCaption, attachVideo, detachVideo, toggleCaptionsDisplay, updateCaptionForTime, destroyCaptions } from "./captions.svelte"; 6 7 7 8 // HLS video source URL (m3u8 playlist) 8 9 let { src }: { src: string } = $props(); ··· 51 52 hls.attachMedia(videoEl); 52 53 hls.on(Hls.Events.MANIFEST_PARSED, () => { 53 54 videoEl?.play().catch(() => {}); 55 + // Attach video for background caption capture 56 + if (videoEl) attachVideo(videoEl); 54 57 }); 55 58 hls.on(Hls.Events.ERROR, (_event, data) => { 56 59 console.error("HLS error:", data); ··· 109 112 } 110 113 111 114 scrubProgress = newProgress; 115 + 116 + // Update captions 117 + if (getCaptionsEnabled()) { 118 + updateCaptionForTime(currentTime); 119 + } 112 120 } 113 121 114 122 function onPlay() { ··· 118 126 playing = false; 119 127 } 120 128 129 + function toggleCaptions() { 130 + playCroak(); 131 + toggleCaptionsDisplay(); 132 + } 133 + 121 134 function togglePlay() { 122 135 if (!videoEl) return; 123 136 if (videoEl.paused) videoEl.play().catch(() => {}); ··· 261 274 <video 262 275 bind:this={videoEl} 263 276 playsinline 277 + crossorigin="anonymous" 264 278 ontimeupdate={onTimeUpdate} 265 279 onplay={onPlay} 266 280 onpause={onPause} ··· 326 340 <img src="/frogeye.png" alt="fullscreen" class="frogeye" /> 327 341 </button> 328 342 343 + <!-- CC toggle button — only shows when model is loaded --> 344 + {#if getModelStatus() === 'ready'} 345 + <button 346 + class="cc-btn" 347 + class:visible={showControls || !playing} 348 + onclick={toggleCaptions} 349 + title={getCaptionsEnabled() ? "Disable captions" : "Enable captions"} 350 + > 351 + <img src={getCaptionsEnabled() ? "/cc_on.png" : "/cc_off.png"} alt="captions" class="cc-icon" /> 352 + </button> 353 + {/if} 354 + 355 + <!-- Caption overlay --> 356 + {#if getCaptionsEnabled() && getCurrentCaption()} 357 + <div class="caption-overlay"> 358 + <span class="caption-text">{getCurrentCaption()}</span> 359 + </div> 360 + {/if} 361 + 329 362 {#if errorMsg} 330 363 <div class="error-overlay">{errorMsg}</div> 331 364 {/if} ··· 424 457 } 425 458 426 459 .lilypad-end { 427 - right: -18px; 460 + right: 40px; 428 461 } 429 462 430 463 /* Frogeye fullscreen button */ ··· 453 486 width: 44px; 454 487 height: 44px; 455 488 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); 489 + } 490 + 491 + .cc-btn { 492 + all: unset; 493 + position: absolute; 494 + bottom: 5%; 495 + right: calc(8% + 56px); 496 + cursor: pointer; 497 + opacity: 0; 498 + transition: opacity 0.25s ease, transform 0.2s ease; 499 + z-index: 5; 500 + } 501 + 502 + .cc-btn.visible { 503 + opacity: 1; 504 + } 505 + 506 + .cc-btn:hover { 507 + transform: scale(1.15); 508 + } 509 + 510 + .cc-icon { 511 + width: 40px; 512 + height: auto; 513 + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); 514 + } 515 + 516 + .caption-overlay { 517 + position: absolute; 518 + bottom: 14%; 519 + left: 5%; 520 + right: 5%; 521 + text-align: center; 522 + z-index: 4; 523 + pointer-events: none; 524 + } 525 + 526 + .caption-text { 527 + font-family: 'Fang', system-ui, sans-serif; 528 + font-size: clamp(0.9rem, 2vw, 1.2rem); 529 + color: #FFDEED; 530 + background: rgba(10, 24, 43, 0.8); 531 + padding: 4px 12px; 532 + border-radius: 4px; 533 + line-height: 1.5; 456 534 } 457 535 458 536 .error-overlay {
+22 -49
src/lib/WavyBorder.svelte
··· 1 1 <!-- 2 2 WavyBorder: A procedurally generated wobbly rectangular border. 3 3 4 - The shape is built from layered sine waves along each edge, seeded by a string 5 - so every instance is unique but deterministic. The content is clipped to the 6 - wavy shape using a CSS polygon, while the visible outline is rendered as an 7 - SVG path on top. Both are derived from the same control points to stay aligned. 4 + Uses a single SVG path for both fill and stroke to avoid misalignment. 5 + Content is clipped using a CSS polygon derived from the same control points. 8 6 --> 9 7 <script lang="ts"> 10 8 import { seededRandom } from './theme'; ··· 20 18 21 19 const paddingValue = $derived(typeof padding === 'number' ? `${padding}px` : padding); 22 20 23 - const { pts, svgPath } = $derived(generateWavyShape(seed)); 24 - 25 - // Convert the points to a CSS polygon for clipping 26 - const clipPolygon = $derived(`polygon(${pts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', ')})`); 21 + const { clipPolygon, svgPath } = $derived(generateWavyShape(seed)); 27 22 28 - /** 29 - * Generate the wavy shape for this border. 30 - * Returns both the SVG bezier path (for the stroke) and densely sampled 31 - * polygon points (for the CSS clip-path). All coordinates are in 0-100 space. 32 - */ 33 - function generateWavyShape(s: string): { pts: [number, number][]; svgPath: string } { 34 - const margin = 1; // % inset from the element edges 35 - const amp = 2.5; // % max wobble amplitude 36 - const segs = 10; // control points per edge 23 + function generateWavyShape(s: string): { clipPolygon: string; svgPath: string } { 24 + const margin = 1; 25 + const amp = 2.5; 26 + const segs = 10; 37 27 38 - // Edges 0=top, 1=right, 2=bottom, 3=left 39 - // Vertical edges (1,3) get lower frequency since they're often shorter in wide boxes 40 28 const edgeParams = [0, 1, 2, 3].map((e) => { 41 29 const isVertical = e === 1 || e === 3; 42 30 const freqScale = isVertical ? 0.5 : 1; ··· 49 37 }; 50 38 }); 51 39 52 - /** Compute the perpendicular wobble offset at position t along an edge */ 53 40 function wobble(edgeIdx: number, t: number): number { 54 41 const p = edgeParams[edgeIdx]; 55 42 const w1 = Math.sin(t * p.freq1 * Math.PI * 2 + p.phase1) * amp * 0.7; ··· 92 79 } 93 80 d += ' Z'; 94 81 95 - // Sample many points along the spline for the CSS polygon 82 + // Sample points along spline for CSS clip-path (for content clipping) 96 83 const sampledPts: [number, number][] = []; 97 84 const totalPts = points.length; 98 85 const samplesPerSegment = 4; ··· 110 97 for (let j = 0; j < samplesPerSegment; j++) { 111 98 const t = j / samplesPerSegment; 112 99 const mt = 1 - t; 113 - // Cubic bezier: P = (1-t)^3*P1 + 3(1-t)^2*t*CP1 + 3(1-t)*t^2*CP2 + t^3*P2 114 100 const x = mt*mt*mt*p1[0] + 3*mt*mt*t*cp1x + 3*mt*t*t*cp2x + t*t*t*p2[0]; 115 101 const y = mt*mt*mt*p1[1] + 3*mt*mt*t*cp1y + 3*mt*t*t*cp2y + t*t*t*p2[1]; 116 102 sampledPts.push([x, y]); 117 103 } 118 104 } 119 105 120 - return { pts: sampledPts, svgPath: d }; 106 + const clipPts = sampledPts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', '); 107 + return { clipPolygon: `polygon(${clipPts})`, svgPath: d }; 121 108 } 122 109 </script> 123 110 124 111 <div class="wavy-container" style="--padding: {paddingValue};"> 125 - <!-- Clipped container: fill + content --> 126 - <div class="wavy-clipped" style="clip-path: {clipPolygon};"> 127 - <div class="wavy-fill" style="background: {fill};"></div> 128 - <div class="wavy-content"> 129 - {@render children()} 130 - </div> 131 - </div> 132 - 133 - <!-- Stroke outline rendered via SVG, same path so it aligns --> 134 - <svg class="wavy-stroke" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 112 + <!-- Single SVG for both fill and stroke — no misalignment --> 113 + <svg class="wavy-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 135 114 <path 136 115 d={svgPath} 137 - fill="none" 116 + fill={fill} 138 117 stroke={strokeColor} 139 118 stroke-width={strokeWidth} 140 119 stroke-linejoin="round" 141 120 stroke-linecap="round" 142 121 /> 143 122 </svg> 123 + 124 + <!-- Content clipped to the wavy shape --> 125 + <div class="wavy-content" style="clip-path: {clipPolygon};"> 126 + {@render children()} 127 + </div> 144 128 </div> 145 129 146 130 <style> ··· 150 134 contain: layout style; 151 135 } 152 136 153 - .wavy-clipped { 154 - position: relative; 155 - } 156 - 157 - .wavy-fill { 137 + .wavy-svg { 158 138 position: absolute; 159 139 inset: 0; 140 + width: 100%; 141 + height: 100%; 160 142 z-index: 0; 143 + overflow: visible; 161 144 } 162 145 163 146 .wavy-content { 164 147 position: relative; 165 148 z-index: 1; 166 149 padding: var(--padding); 167 - } 168 - 169 - .wavy-stroke { 170 - position: absolute; 171 - inset: 0; 172 - width: 100%; 173 - height: 100%; 174 - z-index: 3; 175 - pointer-events: none; 176 - overflow: visible; 177 150 } 178 151 </style>
+16 -34
src/lib/WavyButton.svelte
··· 1 1 <!-- 2 2 WavyButton: A wobbly oval button using sine-wave generated borders. 3 - Reusable for any text content — used for pagination, login, etc. 3 + Uses a single SVG path with fill+stroke to avoid edge gaps. 4 4 --> 5 5 <script lang="ts"> 6 6 import { seededRandom } from './theme'; ··· 14 14 children: any; 15 15 } = $props(); 16 16 17 - const { svgPath, clipPolygon } = $derived(generateWavyOval(seed)); 17 + const svgPath = $derived(generateWavyOval(seed)); 18 18 19 - function generateWavyOval(s: string): { svgPath: string; clipPolygon: string } { 19 + function generateWavyOval(s: string): string { 20 20 const cx = 50, cy = 50; 21 21 const rx = 44, ry = 36; 22 22 const amp = 3; ··· 47 47 d += ` C ${(p1[0] + (p2[0] - p0[0]) * tension).toFixed(1)} ${(p1[1] + (p2[1] - p0[1]) * tension).toFixed(1)}, ${(p2[0] - (p3[0] - p1[0]) * tension).toFixed(1)} ${(p2[1] - (p3[1] - p1[1]) * tension).toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`; 48 48 } 49 49 d += ' Z'; 50 - 51 - const clipPts = pts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', '); 52 - return { svgPath: d, clipPolygon: `polygon(${clipPts})` }; 50 + return d; 53 51 } 54 52 </script> 55 53 56 54 <button class="wavy-btn" class:disabled {disabled} onclick={onclick}> 57 - <div class="btn-clipped" style="clip-path: {clipPolygon};"> 58 - <div class="btn-fill" style="background: {fill};"></div> 59 - <span class="btn-text" style="color: {textColor};"> 60 - {@render children()} 61 - </span> 62 - </div> 63 - <svg class="btn-stroke" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 64 - <path d={svgPath} fill="none" stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 55 + <svg class="btn-svg" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 56 + <path d={svgPath} fill={fill} stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 65 57 </svg> 58 + <span class="btn-text" style="color: {textColor};"> 59 + {@render children()} 60 + </span> 66 61 </button> 67 62 68 63 <style> ··· 73 68 height: clamp(44px, 5vw, 56px); 74 69 cursor: pointer; 75 70 transition: transform 0.15s ease; 71 + display: flex; 72 + align-items: center; 73 + justify-content: center; 76 74 } 77 75 78 76 .wavy-btn:hover:not(.disabled) { ··· 84 82 cursor: wait; 85 83 } 86 84 87 - .btn-clipped { 88 - position: relative; 85 + .btn-svg { 86 + position: absolute; 87 + inset: 0; 89 88 width: 100%; 90 89 height: 100%; 91 - display: flex; 92 - align-items: center; 93 - justify-content: center; 94 - } 95 - 96 - .btn-fill { 97 - position: absolute; 98 - inset: 0; 90 + overflow: visible; 99 91 } 100 92 101 93 .btn-text { ··· 105 97 font-size: clamp(0.9rem, 1.3vw, 1.15rem); 106 98 letter-spacing: 0.5px; 107 99 white-space: nowrap; 108 - } 109 - 110 - .btn-stroke { 111 - position: absolute; 112 - inset: 0; 113 - width: 100%; 114 - height: 100%; 115 - z-index: 2; 116 - pointer-events: none; 117 - overflow: visible; 118 100 } 119 101 </style>
+263
src/lib/captions.svelte.ts
··· 1 + /** 2 + * Captions manager — captures audio from a video element in the background, 3 + * sends chunks to a Whisper Web Worker, and provides reactive caption state. 4 + * 5 + * Audio capture starts automatically when a video is attached (if the model is loaded). 6 + * The CC toggle only controls whether captions are *displayed* — transcription 7 + * happens continuously in the background so captions are pre-computed. 8 + */ 9 + 10 + const SEND_INTERVAL = 4; // send audio every N seconds 11 + const CHUNK_DURATION = 5; // label duration for display 12 + const SAMPLE_RATE = 16000; // Whisper expects 16kHz mono 13 + 14 + type ModelStatus = 'idle' | 'loading' | 'ready' | 'error'; 15 + 16 + interface CaptionChunk { 17 + text: string; 18 + start: number; 19 + end: number; 20 + receivedAt: number; 21 + } 22 + 23 + let modelStatus = $state<ModelStatus>('idle'); 24 + let modelProgress = $state(0); 25 + let modelError = $state(''); 26 + let worker: Worker | null = null; 27 + 28 + // Display toggle — does NOT control capture, only visibility 29 + let captionsEnabled = $state(false); 30 + let currentCaption = $state(''); 31 + let captionChunks = $state<CaptionChunk[]>([]); 32 + 33 + // Audio capture state (runs in background regardless of captionsEnabled) 34 + let capturing = false; 35 + let audioContext: AudioContext | null = null; 36 + let sourceNode: MediaElementAudioSourceNode | null = null; 37 + let processorNode: ScriptProcessorNode | null = null; 38 + let audioBuffer: Float32Array[] = []; 39 + let bufferSamples = 0; 40 + let attachedVideoEl: HTMLVideoElement | null = null; 41 + let chunkTimer: ReturnType<typeof setInterval> | null = null; 42 + let chunkTimeOffset = 0; 43 + 44 + export function getModelStatus() { return modelStatus; } 45 + export function getModelProgress() { return modelProgress; } 46 + export function getModelError() { return modelError; } 47 + export function getCaptionsEnabled() { return captionsEnabled; } 48 + export function getCurrentCaption() { return currentCaption; } 49 + 50 + /** Load the Whisper model in the Web Worker */ 51 + export function loadModel() { 52 + if (modelStatus === 'loading' || modelStatus === 'ready') return; 53 + 54 + modelStatus = 'loading'; 55 + modelProgress = 0; 56 + modelError = ''; 57 + 58 + worker = new Worker( 59 + new URL('./whisper-worker.ts', import.meta.url), 60 + { type: 'module' } 61 + ); 62 + 63 + worker.onmessage = (e: MessageEvent) => { 64 + const { type } = e.data; 65 + 66 + if (type === 'status') { 67 + if (e.data.status === 'ready') { 68 + modelStatus = 'ready'; 69 + modelProgress = 100; 70 + // If a video is already attached, start capturing immediately 71 + if (attachedVideoEl && !capturing) { 72 + beginCapture(attachedVideoEl); 73 + } 74 + } else if (e.data.status === 'error') { 75 + modelStatus = 'error'; 76 + modelError = e.data.error; 77 + } else if (e.data.status === 'loading') { 78 + modelStatus = 'loading'; 79 + } 80 + } 81 + 82 + if (type === 'progress') { 83 + modelProgress = e.data.progress; 84 + } 85 + 86 + if (type === 'result') { 87 + console.log('[CC] Worker result:', e.data); 88 + if (e.data.error) { 89 + console.warn('[CC] Whisper error:', e.data.error); 90 + return; 91 + } 92 + const now = Date.now(); 93 + if (e.data.chunks?.length) { 94 + const newChunks: CaptionChunk[] = e.data.chunks.map((c: CaptionChunk) => ({ 95 + text: c.text, 96 + start: c.start + chunkTimeOffset, 97 + end: c.end + chunkTimeOffset, 98 + receivedAt: now 99 + })); 100 + captionChunks = [...captionChunks, ...newChunks]; 101 + } else if (e.data.text) { 102 + captionChunks = [...captionChunks, { 103 + text: e.data.text, 104 + start: chunkTimeOffset, 105 + end: chunkTimeOffset + CHUNK_DURATION, 106 + receivedAt: now 107 + }]; 108 + } 109 + } 110 + }; 111 + 112 + worker.postMessage({ type: 'load' }); 113 + } 114 + 115 + /** 116 + * Attach a video element — starts background audio capture immediately 117 + * if the model is loaded. Call this whenever a new video starts playing. 118 + */ 119 + export function attachVideo(videoEl: HTMLVideoElement) { 120 + if (videoEl === attachedVideoEl) return; 121 + 122 + // Stop any existing capture 123 + stopCapture(); 124 + 125 + attachedVideoEl = videoEl; 126 + captionChunks = []; 127 + currentCaption = ''; 128 + 129 + // Start capturing if model is ready 130 + if (modelStatus === 'ready' && worker) { 131 + beginCapture(videoEl); 132 + } 133 + } 134 + 135 + /** Detach from the current video and stop capture */ 136 + export function detachVideo() { 137 + stopCapture(); 138 + attachedVideoEl = null; 139 + captionChunks = []; 140 + currentCaption = ''; 141 + } 142 + 143 + /** Toggle caption display on/off (capture continues in background) */ 144 + export function toggleCaptionsDisplay() { 145 + captionsEnabled = !captionsEnabled; 146 + if (!captionsEnabled) { 147 + currentCaption = ''; 148 + } 149 + } 150 + 151 + /** Internal: start background audio capture */ 152 + function beginCapture(videoEl: HTMLVideoElement) { 153 + if (capturing || !worker) return; 154 + 155 + console.log('[CC] Starting background audio capture'); 156 + capturing = true; 157 + audioBuffer = []; 158 + bufferSamples = 0; 159 + chunkTimeOffset = videoEl.currentTime; 160 + 161 + try { 162 + if (!audioContext) { 163 + audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); 164 + } 165 + 166 + if (!sourceNode) { 167 + sourceNode = audioContext.createMediaElementSource(videoEl); 168 + sourceNode.connect(audioContext.destination); 169 + } 170 + 171 + processorNode = audioContext.createScriptProcessor(4096, 1, 1); 172 + processorNode.onaudioprocess = (e) => { 173 + if (!capturing) return; 174 + const input = e.inputBuffer.getChannelData(0); 175 + audioBuffer.push(new Float32Array(input)); 176 + bufferSamples += input.length; 177 + }; 178 + 179 + sourceNode.connect(processorNode); 180 + processorNode.connect(audioContext.destination); 181 + 182 + chunkTimer = setInterval(() => { 183 + if (!worker || bufferSamples < SAMPLE_RATE * 1.5) return; 184 + 185 + const totalSamples = audioBuffer.reduce((acc, b) => acc + b.length, 0); 186 + const merged = new Float32Array(totalSamples); 187 + let offset = 0; 188 + for (const buf of audioBuffer) { 189 + merged.set(buf, offset); 190 + offset += buf.length; 191 + } 192 + 193 + chunkTimeOffset = (attachedVideoEl?.currentTime ?? 0) - (totalSamples / SAMPLE_RATE); 194 + 195 + console.log(`[CC] Sending ${(totalSamples / SAMPLE_RATE).toFixed(1)}s audio to worker`); 196 + 197 + worker!.postMessage({ 198 + type: 'transcribe', 199 + audio: merged, 200 + sampleRate: SAMPLE_RATE 201 + }); 202 + 203 + audioBuffer = []; 204 + bufferSamples = 0; 205 + }, SEND_INTERVAL * 1000); 206 + 207 + console.log('[CC] Audio capture started, sampleRate:', audioContext.sampleRate); 208 + 209 + } catch (err: any) { 210 + console.error('[CC] Audio capture failed:', err); 211 + capturing = false; 212 + } 213 + } 214 + 215 + /** Internal: stop background audio capture */ 216 + function stopCapture() { 217 + capturing = false; 218 + 219 + if (chunkTimer) { 220 + clearInterval(chunkTimer); 221 + chunkTimer = null; 222 + } 223 + 224 + if (processorNode) { 225 + processorNode.disconnect(); 226 + processorNode = null; 227 + } 228 + 229 + audioBuffer = []; 230 + bufferSamples = 0; 231 + } 232 + 233 + /** Update the current visible caption based on video time */ 234 + export function updateCaptionForTime(time: number) { 235 + if (!captionsEnabled || captionChunks.length === 0) { 236 + currentCaption = ''; 237 + return; 238 + } 239 + 240 + // Show each chunk for 5 seconds from when it was received 241 + const now = Date.now(); 242 + for (let i = captionChunks.length - 1; i >= 0; i--) { 243 + const c = captionChunks[i]; 244 + if (c.text && c.receivedAt && (now - c.receivedAt) < 5000) { 245 + currentCaption = c.text; 246 + return; 247 + } 248 + } 249 + currentCaption = ''; 250 + } 251 + 252 + /** Clean up everything */ 253 + export function destroyCaptions() { 254 + stopCapture(); 255 + captionsEnabled = false; 256 + currentCaption = ''; 257 + attachedVideoEl = null; 258 + if (audioContext) { 259 + audioContext.close().catch(() => {}); 260 + audioContext = null; 261 + sourceNode = null; 262 + } 263 + }
+68
src/lib/whisper-worker.ts
··· 1 + /** 2 + * Web Worker for running Whisper speech-to-text inference. 3 + * Receives audio chunks as Float32Array, returns transcribed text. 4 + * Uses @huggingface/transformers with whisper-tiny (ONNX, quantized). 5 + */ 6 + 7 + import { pipeline, type AutomaticSpeechRecognitionPipeline } from '@huggingface/transformers'; 8 + 9 + let transcriber: AutomaticSpeechRecognitionPipeline | null = null; 10 + 11 + self.onmessage = async (e: MessageEvent) => { 12 + const { type, audio, sampleRate } = e.data; 13 + 14 + if (type === 'load') { 15 + try { 16 + self.postMessage({ type: 'status', status: 'loading' }); 17 + transcriber = await pipeline( 18 + 'automatic-speech-recognition', 19 + 'onnx-community/whisper-tiny', 20 + { 21 + dtype: 'q4', 22 + device: 'wasm', 23 + progress_callback: (progress: any) => { 24 + if (progress.status === 'progress' && progress.progress !== undefined) { 25 + self.postMessage({ type: 'progress', progress: progress.progress }); 26 + } 27 + } 28 + } 29 + ); 30 + self.postMessage({ type: 'status', status: 'ready' }); 31 + } catch (err: any) { 32 + self.postMessage({ type: 'status', status: 'error', error: err.message }); 33 + } 34 + return; 35 + } 36 + 37 + if (type === 'transcribe') { 38 + if (!transcriber) { 39 + self.postMessage({ type: 'result', text: '', error: 'Model not loaded' }); 40 + return; 41 + } 42 + 43 + try { 44 + const result = await transcriber(audio, { 45 + sampling_rate: sampleRate, 46 + return_timestamps: true, 47 + chunk_length_s: 5, 48 + stride_length_s: 0 49 + }); 50 + 51 + // result can be { text, chunks } with timestamps 52 + const chunks = (result as any).chunks || []; 53 + const text = (result as any).text || ''; 54 + 55 + self.postMessage({ 56 + type: 'result', 57 + text: text.trim(), 58 + chunks: chunks.map((c: any) => ({ 59 + text: c.text?.trim() || '', 60 + start: c.timestamp?.[0] ?? 0, 61 + end: c.timestamp?.[1] ?? 0 62 + })) 63 + }); 64 + } catch (err: any) { 65 + self.postMessage({ type: 'result', text: '', error: err.message }); 66 + } 67 + } 68 + };
static/cc.png

This is a binary file and will not be displayed.

static/cc_off.png

This is a binary file and will not be displayed.

static/cc_on.png

This is a binary file and will not be displayed.