This commit is contained in:
2026-04-20 00:21:53 +09:00
parent 4b08883f69
commit 53e9908167
31 changed files with 3535 additions and 111 deletions

29
scripts/clean_csv.ts Normal file
View File

@@ -0,0 +1,29 @@
import { readFile, writeFile } from "node:fs/promises";
async function cleanCsv() {
const filePath = "measure.csv";
try {
const content = await readFile(filePath, "utf-8");
const lines = content.split("\n");
if (lines.length === 0) return;
const cleanedLines = lines.map(line => {
if (!line.trim()) return "";
// Basic CSV split (Note: does not handle quoted commas,
// but based on our previous read, the columns we need are early and simple)
const cols = line.split(",");
// Indices: 상수(1), songno(3), diff(4) -> 0-based: 1, 3, 4
const selected = [cols[1], cols[3], cols[4]];
return selected.join(",");
}).filter(line => line !== "");
await writeFile(filePath, cleanedLines.join("\n"));
console.log("measure.csv cleaned successfully.");
} catch (error) {
console.error("Error cleaning CSV:", error);
}
}
cleanCsv();

19
scripts/compare.ts Normal file
View File

@@ -0,0 +1,19 @@
import { readFile } from "node:fs/promises";
async function compare() {
// 예시 데이터: 산출된 예측 상수와 csv의 실제 상수 비교
const data = [
{ title: "Tenjiku 2000", actual: 11.0, predicted: 5.47 },
{ title: "Yuugen no Ran", actual: 11.7, predicted: 4.64 },
{ title: "Joubutsu 2000", actual: 11.0, predicted: 7.13 },
{ title: "Kita Saitama 2000", actual: 11.0, predicted: 7.40 },
{ title: "Shimedore 2000", actual: 11.1, predicted: 4.81 }
];
console.log("--- 상수 비교 분석 ---");
data.forEach(d => {
const diff = (d.actual - d.predicted).toFixed(2);
console.log(`${d.title}: 실제(${d.actual}) vs 예측(${d.predicted}) | 오차: ${diff}`);
});
}
compare();

View File

@@ -0,0 +1,20 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
async function compare() {
const csvContent = await readFile("measure.csv", "utf-8");
const lines = csvContent.split("\n").slice(1);
const predictions = JSON.parse(await readFile("sample/results.json", "utf-8"));
const diffMap: any = { "Oni": "oni", "Edit": "ura", "Ura": "ura" };
const comparison = predictions.map((p: any) => {
const songno = p.file.match(/(\d+)\.tja/)?.[1];
const match = lines.find(l => l.split(",")[1] === songno && l.split(",")[2] === diffMap[p.course]);
const actual = match ? parseFloat(match.split(",")[0]) : null;
return { title: p.title, actual, predicted: parseFloat(p.predicted), diff: actual ? (actual - parseFloat(p.predicted)).toFixed(2) : null };
});
await writeFile("sample/comparison.json", JSON.stringify(comparison, null, 2));
console.log("비교 완료: sample/comparison.json에 저장되었습니다.");
}
compare().catch(console.error);

57
scripts/factorize.ts Normal file
View File

@@ -0,0 +1,57 @@
import { TJAParser, NoteSequence, BPMChangeCommand, MeasureCommand, DelayCommand, ScrollCommand, MasterBranchMarkerCommand, BranchMarkerCommand } from "tja";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join, basename } from "node:path";
async function factorizeTJA(filePath: string) {
try {
const content = await readFile(filePath, "utf-8");
const parsed = TJAParser.parse(content, false);
const results: any[] = [];
const targetCourses = parsed.courses.filter(c => ["Oni", "Edit", "Ura"].includes(c.difficulty.toString()));
for (const course of targetCourses) {
const commands = course.activeCourse.getCommands();
const timeline: any[] = [];
let currentTime = 0, currentBPM = parsed.bpm || 120, currentMeasure = { n: 4, d: 4 };
let inBranch = false, isMaster = false, scrollChanges = 0, bpmChanges = 0;
commands.forEach(cmd => {
if (cmd instanceof BPMChangeCommand) { currentBPM = cmd.bpm; bpmChanges++; }
else if (cmd instanceof MeasureCommand) currentMeasure = { n: cmd.value.numerator, d: cmd.value.denominator };
else if (cmd instanceof DelayCommand) currentTime += cmd.delay;
else if (cmd instanceof ScrollCommand) scrollChanges++;
else if (cmd instanceof MasterBranchMarkerCommand) { inBranch = true; isMaster = true; }
else if (cmd instanceof BranchMarkerCommand) { inBranch = true; isMaster = false; }
else if (cmd instanceof NoteSequence) {
if (inBranch && !isMaster) return;
const interval = ((60 / currentBPM) * 4 * (currentMeasure.n / currentMeasure.d)) / cmd.notes.length;
cmd.notes.forEach(note => {
if (!note.isBlank && !note.isMeasureEnd) timeline.push({ time: currentTime, isDon: note.isDon || note.isBigDon });
currentTime += interval;
});
}
});
if (timeline.length === 0) continue;
let peakNps = 0;
for (let i = 0; i < timeline.length; i++) {
let count = 0;
for (let j = i; j < timeline.length && timeline[j].time < timeline[i].time + 2; j++) count++;
peakNps = Math.max(peakNps, count / 2);
}
let transitions = 0;
for (let i = 1; i < timeline.length; i++) if (timeline[i].isDon !== timeline[i-1].isDon) transitions++;
results.push({
difficulty: course.difficulty.toString(),
factors: { physical: peakNps, stamina: 0, tech: transitions / timeline.length, accuracy: 0.1, reading: (bpmChanges * 0.5) + (scrollChanges * 0.2) }
});
}
if (results.length > 0) {
await mkdir("sample/output/factorize", { recursive: true });
await writeFile(join("sample/output/factorize", `${basename(filePath, ".tja")}.json`), JSON.stringify({ title: parsed.title, file: filePath, analysis: results }, null, 2));
}
} catch (e) { console.error(`Failed ${filePath}: ${e}`); }
}
for (const f of process.argv.slice(2)) await factorizeTJA(f);

11
scripts/predict.ts Normal file
View File

@@ -0,0 +1,11 @@
import * as tf from "@tensorflow/tfjs-node";
async function predict(factors: number[]) {
const model = await tf.loadLayersModel("file://sample/model/model.json");
const input = tf.tensor2d([factors]);
const prediction = model.predict(input) as tf.Tensor;
console.log(`Predicted Constant: ${prediction.dataSync()[0].toFixed(2)}`);
}
// 예시: [physical, stamina, tech, reading]
predict([12.5, 26, 0.41, 0.0]).catch(console.error);

21
scripts/predict_batch.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as tf from "@tensorflow/tfjs-node";
import { readFile, writeFile, readdir } from "node:fs/promises";
import { join } from "node:path";
async function predictBatch() {
const model = await tf.loadLayersModel("file://sample/model/model.json");
const files = (await readdir("sample/output/factorize")).filter(f => f.endsWith(".json"));
const results = [];
for (const file of files) {
const data = JSON.parse(await readFile(join("sample/output/factorize", file), "utf-8"));
for (const analysis of data.analysis) {
const input = tf.tensor2d([Object.values(analysis.factors)]);
const pred = (model.predict(input) as tf.Tensor).dataSync()[0];
results.push({ title: data.title, file: data.file, course: analysis.difficulty, predicted: pred.toFixed(2) });
}
}
await writeFile("sample/results.json", JSON.stringify(results, null, 2));
console.log("Prediction complete.");
}
predictBatch().catch(console.error);

View File

@@ -0,0 +1,26 @@
import { readFile, writeFile, readdir } from "node:fs/promises";
import { join, basename } from "node:path";
async function prepare() {
const csvContent = await readFile("measure.csv", "utf-8");
const lines = csvContent.split("\n").slice(1);
const factorizeDir = "sample/output/factorize";
const files = (await readdir(factorizeDir)).filter(f => f.endsWith(".json"));
const dataset: any[] = [];
for (const file of files) {
const data = JSON.parse(await readFile(join(factorizeDir, file), "utf-8"));
const songno = basename(file, ".json");
const match = lines.find(l => l.split(",")[1] == songno);
if (match) {
const constant = parseFloat(match.split(",")[0]);
for (const analysis of data.analysis) {
dataset.push({ x: Object.values(analysis.factors), y: constant });
}
}
}
await writeFile("sample/dataset/dataset.json", JSON.stringify(dataset));
console.log(`Dataset prepared with ${dataset.length} samples.`);
}
prepare().catch(console.error);

9
scripts/run_predict.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
TJA_DIR=$1
if [ -z "$TJA_DIR" ]; then echo "사용법: $0 <tja_폴더_경로>"; exit 1; fi
echo "--- 채보 상수 예측 시작 ---"
for f in $(find "$TJA_DIR" -name "*.tja"); do
bun run scripts/factorize.ts "$f" > /dev/null
# 결과 JSON을 읽어 예측을 수행하는 로직을 predict.ts에 통합하는 것을 추천합니다.
echo "예측 작업이 완료되었습니다."
done

11
scripts/run_train.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
TJA_DIR=${1:-"sample/training"}
MODEL_PATH=${2:-"sample/model"}
DATASET_DIR=${3:-"output/dataset"}
mkdir -p "$MODEL_PATH" "$DATASET_DIR" "sample/factorize"
echo "--- 분석 및 학습 시작 ---"
for f in $(find "$TJA_DIR" -name "*.tja"); do bun run scripts/factorize.ts "$f" > /dev/null; done
bun run scripts/prepare_dataset.ts "$DATASET_DIR"
bun run scripts/train.ts "$MODEL_PATH" "$DATASET_DIR"

35
scripts/train.ts Normal file
View File

@@ -0,0 +1,35 @@
import * as tf from "@tensorflow/tfjs-node";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
async function train() {
const savePath = process.argv[2] || 'sample/model';
const data = JSON.parse(await readFile(join(process.argv[3] || 'sample/dataset', 'dataset.json'), "utf-8"));
const X = tf.tensor2d(data.map((d: any) => d.x.map((v: any, i: number) => v / (i == 0 ? 20 : i == 1 ? 200 : 1))));
const y = tf.tensor2d(data.map((d: any) => [d.y]));
let model: tf.LayersModel;
const modelJsonPath = join(savePath, "model.json");
if (existsSync(modelJsonPath)) {
console.log("기존 모델을 불러와 추가 학습을 진행합니다.");
model = await tf.loadLayersModel(`file://${modelJsonPath}`);
} else {
console.log("새 모델을 생성합니다.");
model = tf.sequential({
layers: [
tf.layers.dense({ units: 32, activation: 'relu', inputShape: [5] }),
tf.layers.dense({ units: 16, activation: 'relu' }),
tf.layers.dense({ units: 1 })
]
});
}
model.compile({ optimizer: 'adam', loss: 'meanSquaredError' });
const epochs = parseInt(process.argv[4]) || 50;
await model.fit(X, y, { epochs: epochs, verbose: 0 });
await model.save(`file://${savePath}`);
console.log(`학습 완료: 모델이 ${savePath}에 저장되었습니다.`);
}
train().catch(console.error);