Skip to content

Commit 745bd33

Browse files
committed
Merge remote-tracking branch 'origin/master'
1 parent c18cf66 commit 745bd33

26 files changed

+1146
-60
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v6
1919
with:
2020
submodules: recursive
2121

.github/workflows/pages.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ jobs:
2020

2121
steps:
2222
- name: Checkout
23-
uses: actions/checkout@v4
23+
uses: actions/checkout@v6
2424
with:
2525
submodules: recursive
2626

2727
- name: Setup Bun
28-
uses: oven-sh/setup-bun@v1
28+
uses: oven-sh/setup-bun@v2
2929
with:
3030
bun-version: "latest"
3131
cache: true
@@ -39,12 +39,15 @@ jobs:
3939
- name: Generate cache
4040
run: bun run buildCache.js dist/cache.json
4141

42+
- name: Test traversion graph
43+
run: bun test test/TraversionGraph.test.ts
44+
4245
- name: Test common conversion routes
4346
run: bun test test/commonFormats.test.ts
4447

4548
- name: Upload Pages artifact
4649
if: github.event_name == 'push'
47-
uses: actions/upload-pages-artifact@v3
50+
uses: actions/upload-pages-artifact@v4
4851
with:
4952
path: dist
5053

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313
[submodule "src/handlers/image-to-txt"]
1414
path = src/handlers/image-to-txt
1515
url = https://git.sr.ht/~thezipcreator/image-to-txt
16+
[submodule "src/handlers/espeakng.js"]
17+
path = src/handlers/espeakng.js
18+
url = https://github.com/TheZipCreator/espeakng.js

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<link rel="manifest" href="/convert/manifest.json">
1414
<link rel="icon" href="/convert/favicon.png" type="image/png">
1515
<link rel="shortcut icon" href="/convert/favicon.png" type="image/png">
16+
<script defer src="https://cloud.umami.is/script.js" data-website-id="5ad19322-f6db-4c83-969a-701dd0ae6f8d"></script>
1617
<title>FCO.TOOLS</title>
1718
<style>
1819
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@types/react": "^19.2.14",
1313
"@types/react-dom": "^19.2.3",
1414
"@vitejs/plugin-react": "^5.1.4",
15+
"@types/jszip": "^3.4.0",
1516
"puppeteer": "^24.36.0",
1617
"typescript": "~5.9.3",
1718
"vite": "^7.2.4",
@@ -30,7 +31,6 @@
3031
"@types/meyda": "^5.3.0",
3132
"@types/pako": "^2.0.4",
3233
"@types/three": "^0.182.0",
33-
"buffer": "^6.0.3",
3434
"imagetracer": "^0.2.2",
3535
"jszip": "^3.10.1",
3636
"lucide-react": "^0.574.0",

src/CommonFormats.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ const CommonFormats = {
110110
"text/windows-batch",
111111
["text"]
112112
),
113+
SH: new FormatDefinition(
114+
"Shell Script",
115+
"sh",
116+
"sh",
117+
"application/x-sh",
118+
Category.TEXT
119+
),
113120
// audio
114121
MP3: new FormatDefinition(
115122
"MP3 Audio",

src/PriorityQueue.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
class PriorityQueue<T extends Object> {
1+
class PriorityQueue<T extends object> {
22
private _queue: Array<T>;
33
private _size: number = 0;
4-
private _comparator: Function | null;
4+
private _comparator: ((val: T, parent: T) => number) | null;
55

6-
constructor(initialCapacity?: number, comparator?: Function) {
6+
constructor(initialCapacity?: number, comparator?: (val: T, parent: T) => number) {
77
const cap = initialCapacity ?? 11;
88
const com = comparator ?? null;
99
if (cap < 1) {
@@ -19,7 +19,7 @@ class PriorityQueue<T extends Object> {
1919
const newCapacity =
2020
oldCapacity + (oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1);
2121
if (!Number.isSafeInteger(newCapacity)) {
22-
throw new Error('capacity out of range');
22+
throw new Error('OOM: new capacity not a safe integer');
2323
}
2424
this._queue.length = newCapacity;
2525
}
@@ -38,8 +38,8 @@ class PriorityQueue<T extends Object> {
3838
private siftupUsingComparator(k: number, item: T): void {
3939
while (k > 0) {
4040
// find the parent
41-
let parent = (k - 1) >>> 1;
42-
let e = this._queue[parent] as T;
41+
const parent = (k - 1) >>> 1;
42+
const e = this._queue[parent] as T;
4343
// compare item with it parent, if item's priority less, break siftup and insert
4444
if (this._comparator!(item, e) >= 0) {
4545
break;
@@ -54,8 +54,8 @@ class PriorityQueue<T extends Object> {
5454

5555
private siftupComparable(k: number, item: T): void {
5656
while (k > 0) {
57-
let parent = (k - 1) >>> 1;
58-
let e = this._queue[parent] as T;
57+
const parent = (k - 1) >>> 1;
58+
const e = this._queue[parent] as T;
5959
if (item.toString().localeCompare(e.toString()) >= 0) {
6060
break;
6161
}
@@ -74,19 +74,19 @@ class PriorityQueue<T extends Object> {
7474
}
7575

7676
private sinkUsingComparator(k: number, item: T): void {
77-
let half = this._size >>> 1;
77+
const half = this._size >>> 1;
7878
while (k < half) {
7979
let child = (k << 1) + 1;
8080
let object = this._queue[child];
81-
let right = child + 1;
82-
// compare left right child, assgn child the bigger one
81+
const right = child + 1;
82+
// compare left right child, assign child the bigger one
8383
if (
8484
right < this._size &&
8585
this._comparator!(object, this._queue[right]) > 0
8686
) {
8787
object = this._queue[(child = right)];
8888
}
89-
//compare item and child if bigger is item, break
89+
// compare item and child if bigger is item, break
9090
if (this._comparator!(item, object) <= 0) {
9191
break;
9292
}
@@ -97,11 +97,11 @@ class PriorityQueue<T extends Object> {
9797
}
9898

9999
private sinkComparable(k: number, item: T): void {
100-
let half = this._size >>> 1;
100+
const half = this._size >>> 1;
101101
while (k < half) {
102102
let child = (k << 1) + 1;
103103
let object = this._queue[child];
104-
let right = child + 1;
104+
const right = child + 1;
105105

106106
if (
107107
right < this._size &&
@@ -128,7 +128,7 @@ class PriorityQueue<T extends Object> {
128128
}
129129

130130
public add(item: T): boolean {
131-
let i = this._size;
131+
const i = this._size;
132132
if (i >= this._queue.length) {
133133
this.grow();
134134
}
@@ -145,9 +145,9 @@ class PriorityQueue<T extends Object> {
145145
if (this._size === 0) {
146146
return null;
147147
}
148-
let s = --this._size;
149-
let result = <T>this._queue[0];
150-
let x = <T>this._queue[s];
148+
const s = --this._size;
149+
const result = <T>this._queue[0];
150+
const x = <T>this._queue[s];
151151
this._queue.slice(s, 1);
152152
if (s !== 0) {
153153
this.sink(0, x);
@@ -164,9 +164,7 @@ class PriorityQueue<T extends Object> {
164164
}
165165

166166
public clear(): void {
167-
for (let item of this._queue) {
168-
(item as any) = null;
169-
}
167+
this._queue.fill(null as unknown as T);
170168
this._size = 0;
171169
}
172170

@@ -179,7 +177,7 @@ class PriorityQueue<T extends Object> {
179177
}
180178

181179
public toArray(): Array<T> {
182-
return this._queue.filter(item => item);
180+
return this._queue;
183181
}
184182

185183
public toString(): string {

src/TraversionGraph.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export class TraversionGraph {
6262
{from: "audio", to: "video", cost: 1}, // Might be lossy
6363
{from: "text", to: "image", cost: 0.5}, // Depends on the content and method, but can be relatively efficient for simple images
6464
{from: "image", to: "text", cost: 0.5}, // Depends on the content and method, but can be relatively efficient for simple images
65+
{from: "text", to: "audio", cost: 0.6}, // Somewhat lossy for anything that isn't speakable text
6566
];
6667
private categoryAdaptiveCosts: CategoryAdaptiveCost[] = [
68+
{ categories: ["text", "image", "audio"], cost: 15 }, // Text to audio through an image is likely not what the user wants
6769
{ categories: ["image", "video", "audio"], cost: 10000 }, // Converting from image to audio through video is especially lossy
6870
{ categories: ["audio", "video", "image"], cost: 10000 }, // Converting from audio to image through video is especially lossy
6971
];

src/handlers/als.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
2+
3+
class alsHandler implements FormatHandler {
4+
5+
public name: string = "als";
6+
7+
public supportedFormats: FileFormat[] = [
8+
{
9+
name: "Ableton Live Set",
10+
format: "als",
11+
extension: "als",
12+
mime: "application/gzip",
13+
from: true,
14+
to: false,
15+
internal: "als"
16+
},
17+
{
18+
name: "XML Document",
19+
format: "xml",
20+
extension: "xml",
21+
mime: "application/xml",
22+
from: false,
23+
to: true,
24+
internal: "xml"
25+
}
26+
];
27+
28+
public ready: boolean = false;
29+
30+
async init () {
31+
this.ready = true;
32+
}
33+
34+
async doConvert (
35+
inputFiles: FileData[],
36+
inputFormat: FileFormat,
37+
outputFormat: FileFormat
38+
): Promise<FileData[]> {
39+
if (inputFormat.internal !== "als" || outputFormat.internal !== "xml") {
40+
throw "Invalid conversion path.";
41+
}
42+
43+
const decoder = new TextDecoder("utf-8", { fatal: true });
44+
const encoder = new TextEncoder();
45+
46+
return Promise.all(inputFiles.map(async (inputFile) => {
47+
if (
48+
inputFile.bytes.length < 2
49+
|| inputFile.bytes[0] !== 0x1f
50+
|| inputFile.bytes[1] !== 0x8b
51+
) {
52+
throw "Invalid ALS file: expected gzip-compressed data.";
53+
}
54+
55+
const decompressedStream = new Blob([inputFile.bytes as BlobPart])
56+
.stream()
57+
.pipeThrough(new DecompressionStream("gzip"));
58+
const decompressedBytes = new Uint8Array(await new Response(decompressedStream).arrayBuffer());
59+
60+
let xml: string;
61+
try {
62+
xml = decoder.decode(decompressedBytes);
63+
} catch (_) {
64+
throw "Invalid ALS file: decompressed data is not UTF-8 XML.";
65+
}
66+
if (!xml.trimStart().startsWith("<")) {
67+
throw "Invalid ALS file: decompressed data is not XML.";
68+
}
69+
70+
const baseNameParts = inputFile.name.split(".");
71+
const baseName = baseNameParts.length > 1
72+
? baseNameParts.slice(0, -1).join(".")
73+
: inputFile.name;
74+
75+
return {
76+
name: `${baseName}.xml`,
77+
bytes: encoder.encode(xml)
78+
};
79+
}));
80+
}
81+
82+
}
83+
84+
export default alsHandler;

src/handlers/bsor.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
2+
import { Replay } from "./bsor/replay.ts";
3+
import { render } from "./bsor/renderer.ts";
4+
import CommonFormats from "src/CommonFormats.ts";
5+
6+
class bsorHandler implements FormatHandler {
7+
public name: string = "bsor";
8+
public supportedFormats: FileFormat[] = [
9+
{
10+
name: "Beat Saber Open Replay",
11+
format: "bsor",
12+
extension: "bsor",
13+
mime: "application/x-bsor",
14+
from: true,
15+
to: false,
16+
internal: "bsor"
17+
},
18+
CommonFormats.PNG.supported("png", false, true),
19+
CommonFormats.JPEG.supported("jpeg", false, true),
20+
CommonFormats.JSON.supported("json", false, true, true)
21+
];
22+
23+
public ready: boolean = true;
24+
25+
async init() {
26+
this.ready = true;
27+
}
28+
29+
async doConvert (
30+
inputFiles: FileData[],
31+
inputFormat: FileFormat,
32+
outputFormat: FileFormat
33+
): Promise<FileData[]> {
34+
let frameIndex = 0;
35+
return (await Promise.all(inputFiles.map(async(file) => {
36+
const replay = new Replay(file.bytes);
37+
if(outputFormat.internal == "json") {
38+
return [{
39+
name: file.name.split(".")[0] + ".json",
40+
bytes: new TextEncoder().encode(JSON.stringify(replay))
41+
}];
42+
}
43+
let outputs: FileData[] = [];
44+
await new Promise<void>(resolve => {
45+
render(replay, 640, 480,
46+
async(renderer) => {
47+
const bytes: Uint8Array = await new Promise((resolve, reject) => {
48+
renderer.domElement.toBlob((blob) => {
49+
if (!blob) return reject("Canvas output failed");
50+
blob.arrayBuffer().then(buf => resolve(new Uint8Array(buf)));
51+
}, outputFormat.mime);
52+
});
53+
outputs.push({
54+
name: file.name.split(".")[0]+"_"+(frameIndex++)+"."+outputFormat.extension,
55+
bytes: bytes
56+
});
57+
},
58+
async() => resolve()
59+
);
60+
})
61+
return outputs;
62+
}))).flat();
63+
}
64+
65+
}
66+
67+
export default bsorHandler;

0 commit comments

Comments
 (0)