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

View File

@@ -0,0 +1,36 @@
# TJA 난이도 산정 핵심 요소 (Technical Factors)
'상수' 산출을 위해 채보에서 추출해야 하는 5개 핵심 차원과 세부 지표입니다.
## 1. 물리적 밀도 (Physical Density)
물리적인 타격 속도와 관련된 지표입니다.
- **Global NPS**: `총 노트 수 / 총 연주 시간`. 곡의 전반적인 속도 체급.
- **Peak NPS (Sliding Window)**: 1초~2초 단위 윈도우에서 추출한 최대 NPS. 순간 폭타의 한계치.
- **Density Variance**: NPS의 표준 편차. 곡이 얼마나 균일한지 또는 급격한지 측정.
## 2. 지구력 요구량 (Stamina Requirement)
지치지 않고 집중력을 유지해야 하는 정도입니다.
- **Longest Stream Count**: 쉼표(예: 8분음표 이상의 간격) 없이 이어지는 최대 노트 개수.
- **Stream Density Ratio**: 전체 곡 시간 대비 스트림(연타) 구간이 차지하는 비중.
- **Rest Interval Analysis**: 회복 가능한 구간의 배치와 빈도.
## 3. 배치 복잡도 (Pattern Complexity)
인지적 부하와 손 배치(Hand-switching)의 어려움입니다.
- **Color Transition Ratio**: `색상 전환(d↔k) 횟수 / 총 노트 수`. 전환이 많을수록 인지 부하 증가.
- **Hand-Switching Index**: 홀수(3, 5, 7) 및 짝수(2, 4) 연타의 혼합도. 기준 손이 강제로 바뀌는 빈도.
- **Complex Pattern Detection**: 비정형 패턴(예: ddkdk, kkkdk 등)의 출현 빈도.
## 4. 리듬 복잡도 (Rhythmic Complexity)
정확도(98%) 달성을 방해하는 타이밍 요소입니다.
- **Quantization Diversity**: 사용된 음표 단위(1/16, 1/12, 1/24, 1/48 등)의 종류와 혼합 빈도.
- **Off-beat Ratio**: 정박(1/4, 1/8)을 벗어난 엇박 노지의 비율.
- **Rhythmic Entropy**: 노트 간 시간 간격의 불규칙성 정도.
## 5. 가독성 및 기믹 (Reading & Gimmicks)
시각적인 반응 속도와 암기 요소를 측정합니다.
- **Scroll Velocity (SV) Variance**: `#SCROLL` 변화의 진폭과 빈도.
- **BPM Fluctuation**: `#BPMCHANGE`를 통한 급격한 속도 변화 및 정지(#DELAY).
- **Visual Overlap**: 저속 구간에서의 노트 겹침이나 고속 구간의 반응 한계.
## 상수 추정 가중치 (Proposed)
`Constant ∝ (Peak_NPS * 0.4) + (Complexity * 0.3) + (Stamina * 0.2) + (Reading * 0.1)`

16
.gemini/dnn-plan.md Normal file
View File

@@ -0,0 +1,16 @@
# DNN 학습 로드맵 (.gemini/dnn-plan.md)
### Phase 1: 데이터 정제
- `measure.csv`를 기준으로 분석된 Factor 데이터셋 확보.
- Null 값 제거 및 이상치(Outlier) 필터링.
### Phase 2: 환경 구축
- Python 기반 (TensorFlow/PyTorch) 또는 Node.js 기반 (TensorFlow.js) 선택.
- 데이터 학습용 훈련 세트(Train)와 검증 세트(Validation) 분리.
### Phase 3: 학습 및 검증
- 모델 훈련 및 오차(MSE) 점검.
- 실제 상수와의 상관계수 분석.
### Phase 4: 통합
- 학습된 가중치(Weight)를 `factorize.ts`에 로드하여 실제 상수 예측 수행.

View File

@@ -0,0 +1,32 @@
# TJA Factorization 방법론
이 문서는 Raw TJA 데이터를 5대 난이도 요소(Factor)로 변환하는 표준 공정을 정의합니다.
## 1. 전처리: 절대 타임라인 생성 (Timeline Mapping)
TJA의 상대적 마디/박자 구조를 초 단위의 절대 시간축으로 선형화합니다.
- **상태 추적**: `#BPMCHANGE`, `#MEASURE`, `#DELAY`를 실시간 반영하여 각 노트의 발생 시간($T_n$)을 계산합니다.
- **이벤트 객체화**: 각 노트를 위치($T_n$), 타입(Don/Ka), 현재 속도(BPM/SV)를 포함한 객체로 변환합니다.
## 2. 요소별 특징 추출 (Feature Extraction)
### A. 물리적 밀도 (Physical)
- **Peak NPS**: 2초 단위의 슬라이딩 윈도우를 사용하여 가장 밀도가 높은 구간의 초당 노트 수를 측정합니다.
- **지수화**: `Window_Note_Count / Window_Size`.
### B. 지구력 (Stamina)
- **Stream 정의**: 노트 간 간격이 1/16박자(약 150-200ms) 이하로 지속되는 구간을 하나의 스트림으로 봅니다.
- **지표**: 가장 긴 스트림의 노트 개수($S_{max}$)와 곡 전체 대비 스트림 비중($R_{stream}$)을 결합합니다.
### C. 배치 복잡도 (Technical)
- **Color Transition**: 인접한 두 노트의 색상이 다를 때(d↔k)를 전환으로 카운트합니다.
- **지표**: `Total_Transitions / Total_Notes`. 0.5에 가까울수록 복잡도가 높습니다.
### D. 리듬 복잡도 (Accuracy)
- **Subdivision Analysis**: 각 마디가 몇 등분 되었는지 분석하여 12, 24, 48분 음표 등 비정형 박자의 사용 빈도를 측정합니다.
- **지표**: `Non_Standard_Notes / Total_Notes`.
### E. 가독성 및 기믹 (Reading)
- **Velocity Flux**: `#BPMCHANGE` 횟수와 `#SCROLL` 변화량의 절대값 합산으로 시각적 혼란도를 측정합니다.
## 3. 정규화 및 통합 (Normalization)
각 지표의 원시 수치를 0.0 ~ 1.0 범위로 정규화한 뒤, '상수' 가중치를 적용하여 최종 난이도를 도출합니다.

View File

@@ -5,27 +5,13 @@
- **Language**: TypeScript
- **Library**: [tja](https://www.npmjs.com/package/tja) (TJA parser)
## Key Documents
- `.gemini/tja-spec.md`: Rigorous TJA format specification.
- `tja-format.mediawiki`: Original source document.
- `measure.csv`: Dataset with columns `상수`, `songno`, `diff`.
## Models (model/)
- `constant_predictor.py`: DNN 기반 상수 예측 모델.
## Library Usage (tja)
- **Import**: `import { TJAParser } from "tja";`
- **Parsing**: `const parsed = TJAParser.parse(content);`
- **Structure**:
- `parsed.title`, `parsed.bpm`, `parsed.offset`
- `parsed.courses` (Array of `Course` objects)
- `course.difficulty` (e.g., "Oni")
- `course.stars` (Level/Stars)
- `course.activeCourse.getCommands()` returns an array of commands and note sequences.
## Purpose
Analyzing or processing TJA (Taiko Jiro) file formats.
## Key References
- `tja-format.mediawiki`: Detailed specification of the TJA format.
## TJA Format Overview
- **Encoding**: UTF-8 with BOM or Shift-JIS.
- **Extension**: `.tja`.
- **Comments**: Start with `//`.
- **Metadata**: Key-value pairs (e.g., `TITLE:`, `BPM:`, `OFFSET:`).
- **Course Metadata**: Specific to difficulties (e.g., `COURSE:`, `LEVEL:`, `BALLOON:`).
- **Notation**: Commands prefixed with `#` (e.g., `#START`, `#END`, `#MEASURE`, `#BPMCHANGE`).
- **Notes**: `0-9`, `A`, `B`, `F`.
...

View File

@@ -0,0 +1,13 @@
# 파이프라인 실행 규칙
모든 경로는 실행 시점에 인자로 지정하여 관리합니다.
## 1. 학습 파이프라인 (`run_train.sh`)
- **인자 1 (TJA_DIR)**: 학습용 TJA 채보가 저장된 폴더 (예: `sample/training`)
- **인자 2 (MODEL_PATH)**: 모델이 저장될 경로 (예: `output/model/v2_constant`)
- **인자 3 (DATASET_DIR)**: 데이터셋이 저장될 폴더 (예: `output/dataset`)
## 2. 예측 파이프라인 (`run_predict.sh`)
- **인자 1 (MODEL_PATH)**: 추론에 사용할 모델 경로
- **인자 2 (TJA_DIR)**: 예측할 TJA 채보가 모여있는 폴더
- **인자 3 (OUTPUT_DIR)**: 결과가 저장될 폴더

68
.gemini/tja-spec.md Normal file
View File

@@ -0,0 +1,68 @@
# TJA Format Specification (Rigorous)
## 1. File Structure
- **Extension**: `.tja`
- **Encoding**: UTF-8 (with BOM) or Shift-JIS.
- **Comments**: `//` (inline, until end of line).
- **Sections**:
1. **Global Metadata**: Before any `#START`.
2. **Course Data**: Metadata specific to a difficulty (can be mixed with global).
3. **Song Notation**: Between `#START` and `#END`.
## 2. Metadata (Key: Value)
| Key | Type | Description |
| :--- | :--- | :--- |
| `TITLE` | string | Song title. |
| `BPM` | float | Beats Per Minute. Default: 120. |
| `WAVE` | string | Path to audio file. |
| `OFFSET` | float | Seconds. Negative delays notes, positive advances them. |
| `DEMOSTART`| float | Preview start time in seconds. |
| `GENRE` | string | Category (e.g., アニメ, ゲームミュージック). |
| `SCOREMODE`| 0, 1, 2| Scoring method. Default: 1. |
| `COURSE` | enum | 0:Easy, 1:Normal, 2:Hard, 3:Oni, 4:Edit/Ura, 5:Tower, 6:Dan. |
| `LEVEL` | int | 1-10 (Stars). |
| `BALLOON` | int[] | Comma-separated hit counts for balloon/kusudama notes. |
| `SCOREINIT`| int | Initial score per note. |
| `SCOREDIFF`| int | Added score for combo milestones. |
## 3. Song Notation: Notes
| Code | Name | Description |
| :--- | :--- | :--- |
| `0` | Blank | No note. |
| `1` | Don | Small Red. |
| `2` | Ka | Small Blue. |
| `3` | DON | Large Red. |
| `4` | KA | Large Blue. |
| `5` | Drumroll| Start of small drumroll. Ends with `8`. |
| `6` | DRUMROLL| Start of large drumroll. Ends with `8`. |
| `7` | Balloon | Start of balloon. Ends with `8`. |
| `8` | End | Ends 5, 6, 7, or 9. |
| `9` | Kusudama| Large balloon/Kusudama. Ends with `8` or `9`. |
| `A` | DON(H) | Large Red (Hands/Multiplayer). |
| `B` | KA(H) | Large Blue (Hands/Multiplayer). |
## 4. Commands (#COMMAND [value])
| Command | Value | Description |
| :--- | :--- | :--- |
| `#START` | (P1/P2) | Begin notation. |
| `#END` | - | End notation. |
| `#MEASURE` | n/d | Set time signature (e.g., 4/4). |
| `#BPMCHANGE`| float | Change BPM. |
| `#DELAY` | float | Delay in seconds (can be negative). |
| `#SCROLL` | float | Multiplier for note scroll speed. |
| `#GOGOSTART`| - | Start Go-Go Time (1.2x score). |
| `#GOGOEND` | - | End Go-Go Time. |
| `#SECTION` | - | Reset branch accuracy counters. |
| `#BRANCHSTART`| type,v1,v2| Branching: `p` (accuracy) or `r` (drumrolls). |
| `#N`, `#E`, `#M`| - | Branch paths: Normal, Advanced/Expert, Master. |
| `#BRANCHEND`| - | End branching section. |
| `#BARLINEOFF`| - | Hide measure lines. |
| `#BARLINEON` | - | Show measure lines. |
| `#LYRIC` | string | Display lyrics (`\n` for line break). |
## 5. Logic & Formulas
- **Measure Duration (ms)**: `60000 * (numerator / denominator) * 4 / BPM`
- **Note Timing**: Equally spaced within a measure.
- **Branching**:
- `p` (Percent): `(GOOD + OK*0.5) / TotalNotes * 100`.
- Calculated one measure before `#BRANCHSTART`.

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
tja
sample

View File

@@ -1,15 +0,0 @@
# fumen-analyze
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

245
bun.lock
View File

@@ -4,6 +4,7 @@
"": {
"name": "fumen-analyze",
"dependencies": {
"@tensorflow/tfjs-node": "^4.22.0",
"tja": "^0.1.3",
},
"devDependencies": {
@@ -15,16 +16,260 @@
},
},
"packages": {
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.9", "", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw=="],
"@tensorflow/tfjs": ["@tensorflow/tfjs@4.22.0", "", { "dependencies": { "@tensorflow/tfjs-backend-cpu": "4.22.0", "@tensorflow/tfjs-backend-webgl": "4.22.0", "@tensorflow/tfjs-converter": "4.22.0", "@tensorflow/tfjs-core": "4.22.0", "@tensorflow/tfjs-data": "4.22.0", "@tensorflow/tfjs-layers": "4.22.0", "argparse": "^1.0.10", "chalk": "^4.1.0", "core-js": "3.29.1", "regenerator-runtime": "^0.13.5", "yargs": "^16.0.3" }, "bin": { "tfjs-custom-module": "dist/tools/custom_module/cli.js" } }, "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg=="],
"@tensorflow/tfjs-backend-cpu": ["@tensorflow/tfjs-backend-cpu@4.22.0", "", { "dependencies": { "@types/seedrandom": "^2.4.28", "seedrandom": "^3.0.5" }, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } }, "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw=="],
"@tensorflow/tfjs-backend-webgl": ["@tensorflow/tfjs-backend-webgl@4.22.0", "", { "dependencies": { "@tensorflow/tfjs-backend-cpu": "4.22.0", "@types/offscreencanvas": "~2019.3.0", "@types/seedrandom": "^2.4.28", "seedrandom": "^3.0.5" }, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } }, "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg=="],
"@tensorflow/tfjs-converter": ["@tensorflow/tfjs-converter@4.22.0", "", { "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } }, "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ=="],
"@tensorflow/tfjs-core": ["@tensorflow/tfjs-core@4.22.0", "", { "dependencies": { "@types/long": "^4.0.1", "@types/offscreencanvas": "~2019.7.0", "@types/seedrandom": "^2.4.28", "@webgpu/types": "0.1.38", "long": "4.0.0", "node-fetch": "~2.6.1", "seedrandom": "^3.0.5" } }, "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A=="],
"@tensorflow/tfjs-data": ["@tensorflow/tfjs-data@4.22.0", "", { "dependencies": { "@types/node-fetch": "^2.1.2", "node-fetch": "~2.6.1", "string_decoder": "^1.3.0" }, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0", "seedrandom": "^3.0.5" } }, "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w=="],
"@tensorflow/tfjs-layers": ["@tensorflow/tfjs-layers@4.22.0", "", { "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } }, "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA=="],
"@tensorflow/tfjs-node": ["@tensorflow/tfjs-node@4.22.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "1.0.9", "@tensorflow/tfjs": "4.22.0", "adm-zip": "^0.5.2", "google-protobuf": "^3.9.2", "https-proxy-agent": "^2.2.1", "progress": "^2.0.0", "rimraf": "^2.6.2", "tar": "^6.2.1" } }, "sha512-uHrXeUlfgkMxTZqHkESSV7zSdKdV0LlsBeblqkuKU9nnfxB1pC6DtoyYVaLxznzZy7WQSegjcohxxCjAf6Dc7w=="],
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
"@types/long": ["@types/long@4.0.2", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"@types/offscreencanvas": ["@types/offscreencanvas@2019.3.0", "", {}, "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="],
"@types/seedrandom": ["@types/seedrandom@2.4.34", "", {}, "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A=="],
"@webgpu/types": ["@webgpu/types@0.1.38", "", {}, "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="],
"agent-base": ["agent-base@4.3.0", "", { "dependencies": { "es6-promisify": "^5.0.0" } }, "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="],
"are-we-there-yet": ["are-we-there-yet@2.0.0", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw=="],
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"core-js": ["core-js@3.29.1", "", {}, "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw=="],
"debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="],
"es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gauge": ["gauge@3.0.2", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", "object-assign": "^4.1.1", "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" } }, "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"google-protobuf": ["google-protobuf@3.21.4", "", {}, "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"https-proxy-agent": ["https-proxy-agent@2.2.4", "", { "dependencies": { "agent-base": "^4.3.0", "debug": "^3.1.0" } }, "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="],
"make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
"npmlog": ["npmlog@5.0.1", "", { "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", "gauge": "^3.0.0", "set-blocking": "^2.0.0" } }, "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"tja": ["tja@0.1.3", "", {}, "sha512-AyrhrrWoCymLXouYO9n0f0JGMtrKtT8S9p5ddJbVF9IVYNrdHi5+xg+lNKN8xChemIvg5tfbBt6pEl3Pj5vLNg=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
"yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"@mapbox/node-pre-gyp/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"@mapbox/node-pre-gyp/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"@tensorflow/tfjs-core/@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
"@tensorflow/tfjs-core/node-fetch": ["node-fetch@2.6.13", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA=="],
"@tensorflow/tfjs-data/node-fetch": ["node-fetch@2.6.13", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"@mapbox/node-pre-gyp/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"@mapbox/node-pre-gyp/https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
}
}

View File

@@ -0,0 +1,33 @@
# 태고 보면 난이도 평가 5대 요소
태고의 달인 보면 분석기(`fumen-analyze`)는 단순한 별 개수(Level)를 넘어, **98% 정확도와 풀콤보**를 기준으로 한 실질적 난이도인 **'상수'**를 산출합니다. 상수를 결정하는 5가지 핵심 요소는 다음과 같습니다.
---
### 1. 물리적 속도 (물리)
단순히 얼마나 빨리 손을 움직여야 하는가입니다.
- **순간 폭타**: 곡 중 가장 빠른 구간의 속도입니다.
- **체급**: 곡 전체의 평균적인 빠르기입니다.
### 2. 지구력 (체력)
쉬지 않고 얼마나 오래 몰아치는가입니다.
- **기차 길이**: 쉼표 없이 길게 이어지는 연타의 길이입니다.
- **피로도**: 곡 전체에서 연타 구간이 차지하는 비중이 높을수록 상수가 상승합니다.
### 3. 배치 복잡도 (지능)
손 배치가 얼마나 꼬여 있는가입니다.
- **색상 변화**: 빨강(동)과 파랑(딱)이 복잡하게 섞일수록 뇌의 처리 속도가 느려집니다.
- **손 교차**: 기준 손을 강제로 바꿔야 하는 연타 패턴은 매우 높은 실력을 요구합니다.
### 4. 리듬의 난해함 (정확도)
박자가 얼마나 까다로운가입니다.
- **박자 쪼개기**: 16분음표, 12분음표, 24분음표 등이 수시로 뒤섞이면 정확한 판정을 내기 어렵습니다.
- **엇박자**: 정박에서 벗어난 노트들은 풀콤보를 방해하는 주요 요소입니다.
### 5. 시각적 트릭 (독해)
눈으로 노트를 읽기가 얼마나 힘든가입니다.
- **변속(소플란)**: 갑자기 빨라지거나 느려지는 노트 속도는 암기력과 반응 속도를 시험합니다.
- **가독성**: 노트가 너무 뭉쳐 있거나 기믹이 들어간 경우 실제 난이도보다 훨씬 어렵게 느껴집니다.
---
*Generated by fumen-analyze*

View File

@@ -0,0 +1,43 @@
# 채보 요소 분해(Factorization) 가이드
이 문서는 태고(TJA) 채보의 원시 데이터를 분석하여, 난이도를 결정하는 5가지 핵심 요소로 분해하는 과정과 그 의미를 설명합니다.
---
## 1. 요소 분해(Factorization)란?
태고의 채보는 텍스트 형태의 데이터로 이루어져 있습니다. 시스템은 이 데이터를 읽어 플레이어가 실제로 느끼는 **물리적 압박, 기술적 난해함, 시각적 스트레스** 등을 수치로 추출합니다. 이 과정을 '요소 분해'라고 부릅니다.
## 2. 분석 공정 (Analysis Process)
### 1단계: 타임라인 생성 (Timeline Mapping)
채보에 적힌 마디와 박자 정보를 바탕으로, 모든 노트가 곡이 시작된 후 **정확히 몇 초**에 연주되어야 하는지 계산하여 절대적인 시간표(Timeline)를 만듭니다. 이 과정에서 BPM 변화와 딜레이가 모두 계산에 반영됩니다.
### 2단계: 핵심 요소 추출 (Feature Extraction)
#### ① 물리적 속도 (Physical) - "얼마나 빠른가?"
- **분석 방법**: 전체 곡을 2초 단위로 잘라가며 가장 노트가 많이 몰린 구간을 찾습니다.
- **의미**: 플레이어의 순발력과 물리적인 손 속도의 한계를 측정합니다.
#### ② 지구력 (Stamina) - "얼마나 오래 버티는가?"
- **분석 방법**: 노트 사이의 간격이 0.2초 이내로 유지되는 구간을 '기차(Stream)'로 정의하고, 그 최대 길이를 측정합니다.
- **의미**: 중간에 쉬지 않고 계속 쳐야 하는 구간의 길이를 통해 체력적인 소모량을 측정합니다.
#### ③ 패턴 기술 (Technical) - "얼마나 뇌가 복잡한가?"
- **분석 방법**: 빨강(동)과 파랑(딱)이 얼마나 자주 교차되는지(색상 전환율)를 계산합니다.
- **의미**: 손 배치의 복잡도를 의미하며, 0.5에 가까울수록 머리를 많이 써야 하는 기술적인 채보임을 뜻합니다.
#### ④ 리듬 정확도 (Accuracy) - "박자가 얼마나 까다로운가?"
- **분석 방법**: 마디 내에서 정박(1/4, 1/8)을 벗어난 변칙적인 박자(12, 24분음표 등)의 비중을 측정합니다.
- **의미**: 정확도 98%를 달성하기 위해 필요한 리듬 감각의 수준을 측정합니다.
#### ⑤ 가독성 (Reading) - "눈이 얼마나 어지러운가?"
- **분석 방법**: 곡 중간에 속도가 변하는 명령(BPM 변화, 스크롤 속도 변화)이 얼마나 자주 나오는지 합산합니다.
- **의미**: 시각적인 혼란과 암기 요소를 수치화합니다.
## 3. 분석 결과의 활용
이렇게 분해된 요소들은 최종적으로 **'상수(Constant)'**를 계산하는 밑바탕이 됩니다.
- **물리/체력 수치가 높은 곡**: 주로 체력을 기르는 연습에 적합합니다.
- **기술/정확도 수치가 높은 곡**: 판정을 다듬고 손 배치를 익히는 연습에 적합합니다.
---
*Generated by fumen-analyze*

View File

@@ -0,0 +1,12 @@
# DNN 기반 채보 상수 예측 모델 설계 (docs/model-architecture.md)
### 개요
단순 가중치 합산(Weighted Sum) 방식의 한계를 극복하고, 복잡한 비선형 난이도 지표들을 학습하여 실제 상수와 유사한 예측값을 생성하는 심층 신경망(DNN)을 구축합니다.
### 아키텍처
1. **입력 계층 (Input)**: 5가지 정량화된 Factor (Physical, Stamina, Technical, Accuracy, Reading).
2. **은닉 계층 (Hidden Layers)**:
- Layer 1: 16 units, ReLU activation.
- Layer 2: 8 units, ReLU activation.
3. **출력 계층 (Output)**: 1 unit, Linear activation (상수값 예측).
4. **학습 전략**: `measure.csv`의 실제 상수를 정답 데이터로 사용. MSE Loss 및 Adam 옵티마이저 활용.

1236
file_list.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +0,0 @@
import {
TJAParser,
NoteSequence,
BPMChangeCommand,
MeasureCommand,
DelayCommand
} from "tja";
import { readFile } from "node:fs/promises";
async function main() {
const filePath = "tja/1.tja";
console.log(`Analyzing ${filePath}...`);
try {
const content = await readFile(filePath, "utf-8");
const parsed = TJAParser.parse(content);
console.log("\n--- Metadata ---");
console.log(`Title: ${parsed.title}`);
console.log(`BPM: ${parsed.bpm}`);
console.log(`Offset: ${parsed.offset}`);
console.log("\n--- Courses ---");
parsed.courses.forEach((course, index) => {
console.log(`Course ${index + 1}: ${course.difficulty} (Stars: ${course.stars})`);
const commands = course.activeCourse.getCommands();
let totalNotes = 0;
let currentTime = 0; // in seconds
let currentBPM = parsed.bpm || 120;
let currentMeasure = { numerator: 4, denominator: 4 };
commands.forEach(cmd => {
if (cmd instanceof BPMChangeCommand) {
currentBPM = cmd.bpm;
} else if (cmd instanceof MeasureCommand) {
currentMeasure = {
numerator: cmd.numerator,
denominator: cmd.denominator
};
} else if (cmd instanceof DelayCommand) {
currentTime += cmd.delay;
} else if (cmd instanceof NoteSequence) {
// Duration of one measure in seconds:
// (60 / BPM) * 4 * (numerator / denominator)
const measureDuration = (60 / currentBPM) * 4 * (currentMeasure.numerator / currentMeasure.denominator);
const notesInSequence = cmd.notes;
const noteCount = notesInSequence.length;
notesInSequence.forEach((note) => {
if (!note.isBlank && !note.isMeasureEnd) {
totalNotes++;
}
});
currentTime += measureDuration;
}
});
const nps = currentTime > 0 ? (totalNotes / currentTime).toFixed(2) : "0.00";
console.log(` Total Notes: ${totalNotes}`);
console.log(` Estimated Duration: ${currentTime.toFixed(2)}s`);
console.log(` Average NPS: ${nps}`);
});
} catch (error) {
console.error("Error analyzing TJA file:", error);
}
}
main();

1472
measure.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
import tensorflow as tf
import numpy as np
def build_model():
model = tf.keras.Sequential([
tf.keras.layers.Dense(16, activation='relu', input_shape=(5,)),
tf.keras.layers.Dense(8, activation='relu'),
tf.keras.layers.Dense(1, activation='linear')
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
return model
def train_model(X_train, y_train):
model = build_model()
model.fit(X_train, y_train, epochs=100, batch_size=8, verbose=0)
return model
# 사용 예시:
# input: [physical, stamina, tech, accuracy, reading]
# X_train = np.array([[12.5, 26, 0.41, 0.1, 0.0], ...])
# y_train = np.array([11.0, ...])

1
model/dataset.json Normal file
View File

@@ -0,0 +1 @@
[]

19
model/train.py Normal file
View File

@@ -0,0 +1,19 @@
import json
import numpy as np
import tensorflow as tf
with open('model/dataset.json', 'r') as f:
data = json.load(f)
X = np.array([d['x'] for d in data])
y = np.array([d['y'] for d in data])
model = tf.keras.Sequential([
tf.keras.layers.Dense(32, activation='relu', input_shape=(4,)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X, y, epochs=200, verbose=0)
model.save('model/constant_model.keras')
print("Model saved to model/constant_model.keras")

View File

@@ -10,6 +10,7 @@
"typescript": "^5"
},
"dependencies": {
"@tensorflow/tfjs-node": "^4.22.0",
"tja": "^0.1.3"
}
}

36
pipeline/orchestrator.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
SAMPLE_DIR=${1:-"sample"}
TRAIN_DIR="$SAMPLE_DIR/training"
VAL_DIR="$SAMPLE_DIR/validation"
MODEL_DIR="$SAMPLE_DIR/model"
DATA_DIR="$SAMPLE_DIR/dataset"
EPOCHS=50
while true; do
echo "--- [준비] 폴더 초기화 ---"
rm -rf "$TRAIN_DIR" "$VAL_DIR" "$DATA_DIR" "sample/output/factorize"
mkdir -p "$TRAIN_DIR" "$VAL_DIR" "$MODEL_DIR" "$DATA_DIR" "sample/output/factorize" "sample/output"
echo "--- [채보 선별] ---"
find tja -name "*.tja" | sort -R > file_list.txt
head -n 200 file_list.txt | xargs -I {} cp {} "$TRAIN_DIR/"
tail -n 50 file_list.txt | xargs -I {} cp {} "$VAL_DIR/"
echo "--- [학습 진행] Epoch: $EPOCHS ---"
./scripts/run_train.sh "$TRAIN_DIR" "$MODEL_DIR" "$DATA_DIR" "$EPOCHS"
echo "--- [검증 및 상수 추론] ---"
bun run scripts/predict_batch.ts "$MODEL_DIR" "$SAMPLE_DIR"
bun run scripts/compare_results.ts "$SAMPLE_DIR"
# 오차 검사
high_error=$(jq 'map(select(.diff != null and (.diff | tonumber | fabs > 0.1))) | length' "$SAMPLE_DIR/comparison.json")
if [ "$high_error" -eq 0 ]; then
echo "목표 달성: 모든 예측 오차가 0.1 이내입니다."
break
else
echo "오차 발생: $high_error 건이 0.1 초과. 학습 강도(Epoch)를 2배로 늘려 재학습합니다."
EPOCHS=$((EPOCHS * 2))
fi
done

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);

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",