phase 1
This commit is contained in:
29
scripts/clean_csv.ts
Normal file
29
scripts/clean_csv.ts
Normal 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
19
scripts/compare.ts
Normal 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();
|
||||
20
scripts/compare_results.ts
Normal file
20
scripts/compare_results.ts
Normal 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
57
scripts/factorize.ts
Normal 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
11
scripts/predict.ts
Normal 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
21
scripts/predict_batch.ts
Normal 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);
|
||||
26
scripts/prepare_dataset.ts
Normal file
26
scripts/prepare_dataset.ts
Normal 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
9
scripts/run_predict.sh
Executable 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
11
scripts/run_train.sh
Executable 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
35
scripts/train.ts
Normal 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);
|
||||
Reference in New Issue
Block a user