lyric-romanizer 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +97 -31
- package/dist/detector.d.ts +74 -0
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +73 -23
- package/dist/detector.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/romanizer.d.ts +7 -2
- package/dist/romanizer.d.ts.map +1 -1
- package/dist/romanizer.js +173 -115
- package/dist/romanizer.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/README.hi.md +296 -0
- package/docs/README.ja.md +296 -0
- package/docs/README.ko.md +296 -0
- package/docs/README.ru.md +296 -0
- package/docs/README.ta.md +296 -0
- package/docs/README.th.md +296 -0
- package/docs/README.zh-CN.md +296 -0
- package/docs/README.zh-yue.md +306 -0
- package/docs/adr/0001-outputs-are-a-contract.md +21 -0
- package/docs/adr/0002-whole-array-script-pinning.md +26 -0
- package/docs/adr/0003-zero-api-default-injectable-engines.md +22 -0
- package/docs/adr/0004-no-unified-variant-seam.md +23 -0
- package/docs/adr/0005-always-latest-dependencies.md +23 -0
- package/package.json +13 -6
- package/src/detector.ts +127 -0
- package/src/index.ts +18 -0
- package/src/romanizer.ts +240 -0
- package/src/shims.d.ts +3 -0
- package/src/types.ts +75 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# lyric-romanizer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/lyric-romanizer)
|
|
4
|
+
[](https://github.com/thedavidweng/lyric-romanizer/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
> **철학: 바퀴를 다시 발명하지 마라.**
|
|
7
|
+
> 이 프로젝트는 로마자 변환 로직을 처음부터 구축하는 것을 의도적으로 피합니다. 대신 각 스크립트에 최적화된 커뮤니티 유지보수 라이브러리를 결합하고, 오케스트레이션 계층에 집중합니다: 스크립트 감지, 엔진 라우팅, 방언 처리, 통합 API. 의존성 목록의 모든 로마자 변환 엔진은 도메인 전문가들이 유지보수하는 검증된 라이브러리입니다. 이것이 이 프로젝트의 핵심입니다.
|
|
8
|
+
|
|
9
|
+
가사용 스크립트 감지 및 로컬 로마자 변환 엔진. 일본어, 중국어(보통화 및 광동어), 한국어, 키릴 문자, 인도계 문자, 타밀어, 태국어 등 12개 이상의 스크립트를 지원하며, 모두 API 호출 없이 로컬에서 실행됩니다.
|
|
10
|
+
|
|
11
|
+
[Spotify Karaoke](https://github.com/haroldalan/spotify-karaoke)에서 추출. [OpenKara](https://github.com/thedavidweng/openkara)에서 사용 중.
|
|
12
|
+
|
|
13
|
+
[English](https://github.com/thedavidweng/lyric-romanizer#readme) | [日本語](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ja.md) | [中文(简体)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-CN.md) | [中文(粵語)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-yue.md) | [Русский](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ru.md) | [हिन्दी](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.hi.md) | [தமிழ்](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ta.md) | [ไทย](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.th.md)
|
|
14
|
+
|
|
15
|
+
## 특징
|
|
16
|
+
|
|
17
|
+
- **API 호출 없음** — 모든 로마자 변환이 로컬에서 실행
|
|
18
|
+
- **자동 스크립트 감지** — 텍스트를 전달하면 사용된 스크립트를 자동 감지
|
|
19
|
+
- **12개 이상 스크립트** — 일본어, 중국어, 한국어, 키릴 문자, 6개 인도계 문자, 타밀어, 태국어
|
|
20
|
+
- **광동어 지원** — 기본 보통화 병음 외에 광동어 윈핑(Jyutping) 지원
|
|
21
|
+
- **경량 감지 서브패스** — 로마자 변환 엔진을 가져오지 않고 스크립트 감지(및 외부 스크립트 분류)만 임포트 가능
|
|
22
|
+
- **지연 로딩 엔진** — 모든 엔진이 최초 사용 시 로드되며, 로마자 변환 전까지 메인 엔트리 임포트 비용 없음
|
|
23
|
+
- **플러그 가능한 엔진** — 기본 제공 엔진을 재정의하거나 외부에서 로마자 변환되는 스크립트용 어댑터를 직접 연결
|
|
24
|
+
- **관찰 가능한 폴백** — 엔진 실패로 한 줄이 최후의 수단으로 전사된 경우를 줄별 플래그로 표시
|
|
25
|
+
- **우크라이나어 인식 키릴 문자** — 우크라이나어 고유 문자를 자동 감지하여 올바른 전사 프리셋 적용
|
|
26
|
+
- **순수 Node ESM 실행** — 번들러 불필요
|
|
27
|
+
|
|
28
|
+
## 설치
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install lyric-romanizer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add lyric-romanizer
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add lyric-romanizer
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 빠른 시작
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createRomanizer, detectScript } from 'lyric-romanizer';
|
|
46
|
+
|
|
47
|
+
const romanizer = createRomanizer();
|
|
48
|
+
|
|
49
|
+
// 스크립트 자동 감지 및 로마자 변환
|
|
50
|
+
const result = await romanizer.romanizeLines(['你好世界', '很高兴认识你']);
|
|
51
|
+
// { script: 'chinese', lines: ['nǐ hǎo shì jiè', 'hěn gāo xìng rèn shi nǐ'], fallbacks: [false, false] }
|
|
52
|
+
|
|
53
|
+
// 한 줄 로마자 변환
|
|
54
|
+
const line = await romanizer.romanizeLine('안녕하세요');
|
|
55
|
+
// 'annyeonghaseyo'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **감지 단위** — `romanizeLines`는 배열 **전체에서 한 번** 지배적 스크립트를 감지하여 모든 줄에 고정합니다(의도적: 일본어 노래 안의 한자로만 된 줄도 반드시 일본어 엔진에 도달해야 하며, 배열에 가나가 하나라도 있으면 `japanese`로 고정됩니다). 대신 `romanizeLine`을 반복 호출하면 **줄별로** 감지하여 각 줄을 서로 다른 엔진으로 라우팅할 수 있습니다. 고정된 스크립트 아래의 순수 라틴 줄은 그대로 반환됩니다. **혼합 스크립트 배열** 섹션을 참고하세요.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### 임포트
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// 메인 엔트리 — 전체 로마자 변환 엔진
|
|
66
|
+
import {
|
|
67
|
+
createRomanizer,
|
|
68
|
+
detectScript,
|
|
69
|
+
isLatinScript,
|
|
70
|
+
requiresExternalRomanization,
|
|
71
|
+
UnsupportedRomanizationError,
|
|
72
|
+
} from 'lyric-romanizer';
|
|
73
|
+
|
|
74
|
+
// 감지 전용 서브패스 — 경량, 로마자 변환 의존성 없음
|
|
75
|
+
import {
|
|
76
|
+
detectScript,
|
|
77
|
+
isLatinScript,
|
|
78
|
+
requiresExternalRomanization,
|
|
79
|
+
NON_LATIN_SCRIPT_RE,
|
|
80
|
+
} from 'lyric-romanizer/detector';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 타입
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
type ScriptType =
|
|
87
|
+
| 'japanese' | 'chinese' | 'korean' | 'cyrillic'
|
|
88
|
+
| 'devanagari' | 'gujarati' | 'gurmukhi' | 'telugu'
|
|
89
|
+
| 'kannada' | 'odia' | 'tamil' | 'malayalam'
|
|
90
|
+
| 'bengali' | 'arabic' | 'hebrew' | 'thai'
|
|
91
|
+
| 'latin' | 'other';
|
|
92
|
+
|
|
93
|
+
interface Romanizer {
|
|
94
|
+
romanizeLine(line: string, options?: RomanizeOptions): Promise<string>;
|
|
95
|
+
romanizeLines(lines: readonly string[], options?: RomanizeOptions): Promise<RomanizeResult>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// `dialect`는 'chinese'에서만 적용되며, 그 외 모든 스크립트는 무시합니다.
|
|
99
|
+
type RomanizeOptions = { script?: ScriptType; dialect?: 'mandarin' | 'cantonese' };
|
|
100
|
+
|
|
101
|
+
// `fallbacks`는 `lines`와 정렬됩니다: 엔진이 실패하여 해당 줄이 최후의 수단으로
|
|
102
|
+
// 유니버설 전사로 처리된 경우 true입니다.
|
|
103
|
+
type RomanizeResult = { script: ScriptType; lines: string[]; fallbacks?: boolean[] };
|
|
104
|
+
|
|
105
|
+
// 엔진 어댑터: 해당 스크립트의 한 줄을 로마자 변환합니다. 예외를 던지면(또는
|
|
106
|
+
// 거부하면) 유니버설 전사 폴백이 트리거됩니다.
|
|
107
|
+
type RomanizeEngine = (line: string, context: { dialect: 'mandarin' | 'cantonese' }) => string | Promise<string>;
|
|
108
|
+
|
|
109
|
+
type RomanizerOptions = {
|
|
110
|
+
japaneseDictPath?: string;
|
|
111
|
+
engines?: Partial<Record<ScriptType, RomanizeEngine>>;
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `createRomanizer(options?)`
|
|
116
|
+
|
|
117
|
+
`Romanizer` 인스턴스를 반환하는 팩토리 함수. 모든 엔진은 최초 사용 시 지연 로딩되어 캐시되며, 로드에 실패하면 다음 호출에서 다시 시도합니다.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const romanizer = createRomanizer();
|
|
121
|
+
|
|
122
|
+
// Kuromoji 사전 CDN 경로 재정의 (셀프 호스팅 등)
|
|
123
|
+
const romanizer = createRomanizer({
|
|
124
|
+
japaneseDictPath: 'https://my-cdn.com/kuromoji/dict',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### 엔진 어댑터
|
|
129
|
+
|
|
130
|
+
`options.engines`는 특정 스크립트의 기본 제공 엔진을 재정의하거나, 기본 제공 엔진이 없는 스크립트(`arabic`, `hebrew`, `malayalam`, `bengali`, `other`)에 엔진을 연결하여 `romanizeLines`가 하나의 인터페이스로 모든 스크립트를 처리할 수 있게 합니다. 라이브러리는 기본적으로 **네트워크 I/O를 전혀 수행하지 않습니다**. 원격 어댑터를 연결하는 것은 호출자의 명시적인 결정입니다.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const romanizer = createRomanizer({
|
|
134
|
+
engines: {
|
|
135
|
+
// 로컬 엔진이 없는 스크립트에 직접 외부 로마자 변환을 제공하세요.
|
|
136
|
+
arabic: async (line) => myTransliterationApi(line),
|
|
137
|
+
// 또는 기본 제공 엔진을 교체하세요 (예: 테스트에서 가짜 엔진으로).
|
|
138
|
+
korean: (line) => myKoreanRomanizer(line),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await romanizer.romanizeLines(['مرحبا']);
|
|
143
|
+
// { script: 'arabic', lines: [...], fallbacks: [false] } — 더 이상 예외를 던지지 않음
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
기본 제공이든 주입된 것이든 엔진이 없는 스크립트는 여전히 `UnsupportedRomanizationError`를 던집니다.
|
|
147
|
+
|
|
148
|
+
### `detectScript(lines)`
|
|
149
|
+
|
|
150
|
+
주어진 텍스트 라인의 지배적 스크립트를 감지합니다. 먼저 일본어 가나를 확인하고(결정적), 그다음 다른 모든 스크립트를 문자 수로 스코어링합니다.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
detectScript(['こんにちは']); // 'japanese'
|
|
154
|
+
detectScript(['你好世界']); // 'chinese'
|
|
155
|
+
detectScript(['Привет']); // 'cyrillic'
|
|
156
|
+
detectScript(['Hello world']); // 'latin'
|
|
157
|
+
detectScript(['123 ???']); // 'other'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `isLatinScript(lines)`
|
|
161
|
+
|
|
162
|
+
빠른 확인 — 텍스트에 라틴 문자만 포함된 경우 `true` 반환 (CJK, 키릴, 인도계 문자 등 없음). 로마자 변환을 완전히 건너뛸 때 유용합니다.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
isLatinScript(['Hello world']); // true
|
|
166
|
+
isLatinScript(['안녕하세요']); // false
|
|
167
|
+
isLatinScript(['♪♪♪']); // false (문자 없음)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `requiresExternalRomanization(script)`
|
|
171
|
+
|
|
172
|
+
기본 제공 엔진이 없어 외부 API가 필요한 스크립트에 대해 `true`를 반환합니다. 경량 `lyric-romanizer/detector` 서브패스에서 임포트할 수 있으므로 "외부 서비스로 분기해야 하는가?"라는 질문에 답하는 데 엔진 페이로드가 전혀 들지 않습니다.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
requiresExternalRomanization('chinese'); // false
|
|
176
|
+
requiresExternalRomanization('arabic'); // true
|
|
177
|
+
requiresExternalRomanization('malayalam'); // true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `romanizer.romanizeLine(line, options?)`
|
|
181
|
+
|
|
182
|
+
한 줄을 로마자 변환합니다. `script`를 생략하면 `detectScript`를 통해 **줄별로** 자동 감지됩니다 — `romanizeLine`을 반복 호출하면 각 줄이 서로 다른 엔진으로 라우팅될 수 있으며, 이는 배열 전체에 하나의 스크립트를 고정하는 `romanizeLines`와 다릅니다. 라틴 텍스트나 문자가 없는 콘텐츠는 원본 줄을 그대로 반환합니다.
|
|
183
|
+
|
|
184
|
+
중국어 텍스트의 경우 `dialect` 옵션으로 로마자 변환 시스템을 제어합니다: `'mandarin'` (기본값)은 병음, `'cantonese'`는 [Jyutping](https://github.com/CanCLID/to-jyutping)을 사용합니다. 다른 스크립트는 `dialect`를 무시합니다.
|
|
185
|
+
|
|
186
|
+
엔진이 없는(기본 제공이든 주입된 것이든) 스크립트의 경우 `UnsupportedRomanizationError`를 **던집니다**.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
await romanizer.romanizeLine('你好世界');
|
|
190
|
+
// 'nǐ hǎo shì jiè' (기본값: 보통화/병음)
|
|
191
|
+
|
|
192
|
+
await romanizer.romanizeLine('你好', { dialect: 'cantonese' });
|
|
193
|
+
// 'nei5 hou2' (Jyutping)
|
|
194
|
+
|
|
195
|
+
await romanizer.romanizeLine('Привет мир');
|
|
196
|
+
// 'Privet mir'
|
|
197
|
+
|
|
198
|
+
await romanizer.romanizeLine('Hello world');
|
|
199
|
+
// 'Hello world' (그대로)
|
|
200
|
+
|
|
201
|
+
await romanizer.romanizeLine('مرحبا');
|
|
202
|
+
// throws UnsupportedRomanizationError { script: 'arabic' }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `romanizer.romanizeLines(lines, options?)`
|
|
206
|
+
|
|
207
|
+
여러 줄을 병렬로 로마자 변환합니다. 배열 **전체에서 한 번** 지배적 스크립트를 감지하여 모든 줄에 고정합니다(**혼합 스크립트 배열** 참고). 스크립트, 로마자 변환된 줄, 그리고 줄별 `fallbacks` 플래그를 반환합니다 — 엔진이 실패하여 해당 줄이 최후의 수단으로 유니버설 전사로 처리된 경우 `true`입니다.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const { script, lines, fallbacks } = await romanizer.romanizeLines([
|
|
211
|
+
'สวัสดี',
|
|
212
|
+
'ชาวโลก',
|
|
213
|
+
]);
|
|
214
|
+
// { script: 'thai', lines: ['swasdi', 'chaolok'], fallbacks: [false, false] }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `UnsupportedRomanizationError`
|
|
218
|
+
|
|
219
|
+
엔진이 없는 — 기본 제공이든 `options.engines`를 통해 주입된 것이든 — 스크립트를 로마자 변환하려 할 때 던져집니다. 프로그래밍 처리를 위한 `script` 속성을 가집니다.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
try {
|
|
223
|
+
await romanizer.romanizeLine('مرحبا');
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err instanceof UnsupportedRomanizationError) {
|
|
226
|
+
console.log(err.script); // 'arabic'
|
|
227
|
+
// 외부 API로 폴백
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 지원 스크립트
|
|
233
|
+
|
|
234
|
+
### 로컬 (완전 오프라인)
|
|
235
|
+
|
|
236
|
+
| 스크립트 | 엔진 | 예시 |
|
|
237
|
+
|---------|------|------|
|
|
238
|
+
| 유니버설 *(fallback)* | [transliteration](https://github.com/nickclaw/transliteration) | `Привет` → `Privet` |
|
|
239
|
+
| 일본어 | [kuroshiro](https://github.com/sglkc/kuroshiro-ts) + [kuromoji](https://github.com/takuyaa/kuromoji.js) | `こんにちは` → `konnichiha` |
|
|
240
|
+
| 보통화 | [pinyin-pro](https://github.com/zh-lx/pinyin-pro) | `你好` → `nǐ hǎo` |
|
|
241
|
+
| 광동어 | [to-jyutping](https://github.com/CanCLID/to-jyutping) | `佢冇` → `keoi5 mou5` |
|
|
242
|
+
| 한국어 | [@romanize/korean](https://github.com/kntng/romanize) | `안녕` → `annyeong` |
|
|
243
|
+
| 키릴 문자 | [cyrillic-to-translit-js](https://github.com/greybax/CyrillicToTranslitJS) | `Привет` → `Privet` |
|
|
244
|
+
| 데바나가리 | [sanscript](https://github.com/indic-transliteration/sanscript) | `नमस्ते` → `namaste` |
|
|
245
|
+
| 구자라트어 | [sanscript](https://github.com/indic-transliteration/sanscript) | `નમસ્તે` → `namaste` |
|
|
246
|
+
| 구르무키 | [sanscript](https://github.com/indic-transliteration/sanscript) | `ਨਮਸਤੇ` → `namasate` |
|
|
247
|
+
| 텔루구어 | [sanscript](https://github.com/indic-transliteration/sanscript) | `నమస్తే` → `namaste` |
|
|
248
|
+
| 칸나다어 | [sanscript](https://github.com/indic-transliteration/sanscript) | `ನಮಸ್ತೇ` → `namaste` |
|
|
249
|
+
| 오디아어 | [sanscript](https://github.com/indic-transliteration/sanscript) | `ନମସ୍ତେ` → `namaste` |
|
|
250
|
+
| 타밀어 | [tamil-romanizer](https://github.com/haroldalan/tamil-romanizer) | `வணக்கம்` → `vanakkam` |
|
|
251
|
+
| 태국어 | [@dehoist/romanize-thai](https://github.com/Dehoist/Open-Source) | `สวัสดี` → `swasdi` |
|
|
252
|
+
|
|
253
|
+
### 외부 API 필요
|
|
254
|
+
|
|
255
|
+
| 스크립트 | 방법 |
|
|
256
|
+
|---------|------|
|
|
257
|
+
| 말라얄람어 | Google Translate `dt=rm` |
|
|
258
|
+
| 벵골어 | Google Translate `dt=rm` |
|
|
259
|
+
| 아랍어 | Google Translate `dt=rm` |
|
|
260
|
+
| 히브리어 | Google Translate `dt=rm` |
|
|
261
|
+
| 기타 | Google Translate `dt=rm` |
|
|
262
|
+
|
|
263
|
+
`requiresExternalRomanization()`로 이들을 감지하여 원하는 API로 분기하거나, API를 **엔진 어댑터**로 한 번만 연결하여 `romanizeLines`가 모든 스크립트를 처리하도록 하세요.
|
|
264
|
+
|
|
265
|
+
## 스크립트별 참고 사항
|
|
266
|
+
|
|
267
|
+
### 혼합 스크립트 배열
|
|
268
|
+
|
|
269
|
+
`romanizeLines`는 배열 전체의 **지배적** 스크립트를 모든 줄에 고정합니다. 이는 의도적입니다: 일본어 노래에 포함된 한자로만 된 줄은 그 자체만으로는 중국어와 구별되지 않으므로(일본어의 한자와 중국어의 한자는 동일한 유니코드 블록을 공유합니다), 배열 전체의 문맥만이 이를 올바른 엔진으로 라우팅할 수 있습니다. 알아두면 좋은 결과는 다음과 같습니다:
|
|
270
|
+
|
|
271
|
+
- 배열 어디에든 가나가 있으면 배열 전체가 `japanese`로 고정됩니다 — 가나는 결정적 증거입니다.
|
|
272
|
+
- 배열 안에 *다른* 비라틴 스크립트로 된 줄이 있어도 여전히 고정된 엔진으로 전달됩니다.
|
|
273
|
+
- 순수 라틴 줄(CJK 노래 안의 영어 후렴구)은 고정된 엔진으로 전달되지 않고 **그대로 반환됩니다**.
|
|
274
|
+
- 줄별 `fallbacks` 플래그는 엔진이 실패하여 해당 줄이 유니버설 전사로 격하된 경우를 알려줍니다.
|
|
275
|
+
|
|
276
|
+
진정한 줄별 엔진 라우팅이 필요하면 `romanizeLine`을 반복 호출하세요 — 줄별로 감지합니다.
|
|
277
|
+
|
|
278
|
+
### 키릴 문자 감지
|
|
279
|
+
|
|
280
|
+
키릴 문자는 우크라이나어 고유 문자(`і`, `ї`, `є`, `ґ`)를 자동 감지하여 우크라이나어 전사 프리셋을 적용합니다. 그 외 키릴 문자는 러시아어로 기본 처리됩니다.
|
|
281
|
+
|
|
282
|
+
### 광동어 지원
|
|
283
|
+
|
|
284
|
+
중국어는 기본적으로 보통화(병음)입니다. `RomanizeOptions`에서 `dialect: 'cantonese'`를 전달하면 중국어 텍스트를 [Jyutping](https://github.com/CanCLID/to-jyutping)으로 로마자 변환합니다.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const { lines } = await romanizer.romanizeLines(['你好世界', '食飯'], {
|
|
288
|
+
script: 'chinese',
|
|
289
|
+
dialect: 'cantonese',
|
|
290
|
+
});
|
|
291
|
+
// ['nei5 hou2 sai3 gaai3', 'sik6 faan6']
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## 라이선스
|
|
295
|
+
|
|
296
|
+
MIT
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# lyric-romanizer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/lyric-romanizer)
|
|
4
|
+
[](https://github.com/thedavidweng/lyric-romanizer/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
> **Философия: Не изобретайте велосипед.**
|
|
7
|
+
> Этот проект намеренно избегает создания логики романизации с нуля. Вместо этого он объединяет лучшие библиотеки, поддерживаемые сообществом — по одной на каждый скрипт — и фокусируется на слое оркестрации: определение скрипта, маршрутизация движков, обработка диалектов и единый API. Каждый движок романизации в списке зависимостей — это специализированная, проверенная библиотека, поддерживаемая экспертами в данной области. В этом суть проекта.
|
|
8
|
+
|
|
9
|
+
Движок определения скрипта и локальной романизации для текстов песен. Поддерживает 12+ скриптов: японский, китайский (мандарин и кантонский), корейский, кириллица, индийские скрипты, тамильский, тайский — всё работает локально, без вызовов API.
|
|
10
|
+
|
|
11
|
+
Извлечён из [Spotify Karaoke](https://github.com/haroldalan/spotify-karaoke). Используется в [OpenKara](https://github.com/thedavidweng/openkara).
|
|
12
|
+
|
|
13
|
+
[English](https://github.com/thedavidweng/lyric-romanizer#readme) | [日本語](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ja.md) | [中文(简体)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-CN.md) | [中文(粵語)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-yue.md) | [한국어](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ko.md) | [हिन्दी](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.hi.md) | [தமிழ்](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ta.md) | [ไทย](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.th.md)
|
|
14
|
+
|
|
15
|
+
## Возможности
|
|
16
|
+
|
|
17
|
+
- **Без вызовов API** — вся романизация выполняется локально
|
|
18
|
+
- **Автоопределение скрипта** — передайте текст, получите определённый скрипт
|
|
19
|
+
- **12+ скриптов** — японский, китайский, корейский, кириллица, 6 индийских скриптов, тамильский, тайский
|
|
20
|
+
- **Поддержка кантонского** — ютпин (Jyutping) наряду с мандаринским пиньинем
|
|
21
|
+
- **Лёгкий подпуть детектора** — импортируйте только определение скрипта (и классификацию внешних скриптов) без движков романизации
|
|
22
|
+
- **Ленивые движки** — каждый движок загружается при первом использовании; импорт основной точки входа ничего не стоит, пока вы не запустите романизацию
|
|
23
|
+
- **Подключаемые движки** — переопределите любой встроенный движок или подключите собственный адаптер для скриптов с внешней романизацией
|
|
24
|
+
- **Наблюдаемые откаты** — флаги для каждой строки сообщают, когда движок дал сбой и строка была транслитерирована в качестве крайней меры
|
|
25
|
+
- **Украиноориентированная кириллица** — автоопределение украинских символов и применение правильного пресета транслитерации
|
|
26
|
+
- **Работает в чистом Node ESM** — сборщик не требуется
|
|
27
|
+
|
|
28
|
+
## Установка
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install lyric-romanizer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add lyric-romanizer
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add lyric-romanizer
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Быстрый старт
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createRomanizer, detectScript } from 'lyric-romanizer';
|
|
46
|
+
|
|
47
|
+
const romanizer = createRomanizer();
|
|
48
|
+
|
|
49
|
+
// Автоопределение скрипта и романизация
|
|
50
|
+
const result = await romanizer.romanizeLines(['你好世界', '很高兴认识你']);
|
|
51
|
+
// { script: 'chinese', lines: ['nǐ hǎo shì jiè', 'hěn gāo xìng rèn shi nǐ'], fallbacks: [false, false] }
|
|
52
|
+
|
|
53
|
+
// Романизация одной строки
|
|
54
|
+
const line = await romanizer.romanizeLine('안녕하세요');
|
|
55
|
+
// 'annyeonghaseyo'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Гранулярность определения** — `romanizeLines` определяет доминирующий скрипт **один раз по всем строкам** и закрепляет его за каждой строкой (это сделано намеренно: строки только с кандзи внутри японской песни должны попасть в японский движок, а любая кана в массиве закрепляет `japanese`). Вызов `romanizeLine` в цикле, наоборот, определяет скрипт **для каждой строки** и может направлять каждую строку в разные движки. Строки на чистой латинице при закреплённом скрипте возвращаются без изменений. См. **Массивы со смешанными скриптами**.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### Импорт
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// Основная точка входа — полный движок романизации
|
|
66
|
+
import {
|
|
67
|
+
createRomanizer,
|
|
68
|
+
detectScript,
|
|
69
|
+
isLatinScript,
|
|
70
|
+
requiresExternalRomanization,
|
|
71
|
+
UnsupportedRomanizationError,
|
|
72
|
+
} from 'lyric-romanizer';
|
|
73
|
+
|
|
74
|
+
// Подпуть только детектора — лёгкий, без зависимостей романизации
|
|
75
|
+
import {
|
|
76
|
+
detectScript,
|
|
77
|
+
isLatinScript,
|
|
78
|
+
requiresExternalRomanization,
|
|
79
|
+
NON_LATIN_SCRIPT_RE,
|
|
80
|
+
} from 'lyric-romanizer/detector';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Типы
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
type ScriptType =
|
|
87
|
+
| 'japanese' | 'chinese' | 'korean' | 'cyrillic'
|
|
88
|
+
| 'devanagari' | 'gujarati' | 'gurmukhi' | 'telugu'
|
|
89
|
+
| 'kannada' | 'odia' | 'tamil' | 'malayalam'
|
|
90
|
+
| 'bengali' | 'arabic' | 'hebrew' | 'thai'
|
|
91
|
+
| 'latin' | 'other';
|
|
92
|
+
|
|
93
|
+
interface Romanizer {
|
|
94
|
+
romanizeLine(line: string, options?: RomanizeOptions): Promise<string>;
|
|
95
|
+
romanizeLines(lines: readonly string[], options?: RomanizeOptions): Promise<RomanizeResult>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// `dialect` учитывается только для 'chinese'; все остальные скрипты его игнорируют.
|
|
99
|
+
type RomanizeOptions = { script?: ScriptType; dialect?: 'mandarin' | 'cantonese' };
|
|
100
|
+
|
|
101
|
+
// `fallbacks` выровнен с `lines`: true там, где движок дал сбой и строка
|
|
102
|
+
// была транслитерирована универсальным способом в качестве крайней меры.
|
|
103
|
+
type RomanizeResult = { script: ScriptType; lines: string[]; fallbacks?: boolean[] };
|
|
104
|
+
|
|
105
|
+
// Адаптер движка: романизирует одну строку своего скрипта. Выброс исключения
|
|
106
|
+
// (или отклонение промиса) запускает откат к универсальной транслитерации.
|
|
107
|
+
type RomanizeEngine = (line: string, context: { dialect: 'mandarin' | 'cantonese' }) => string | Promise<string>;
|
|
108
|
+
|
|
109
|
+
type RomanizerOptions = {
|
|
110
|
+
japaneseDictPath?: string;
|
|
111
|
+
engines?: Partial<Record<ScriptType, RomanizeEngine>>;
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `createRomanizer(options?)`
|
|
116
|
+
|
|
117
|
+
Фабрика, возвращающая экземпляр `Romanizer`. Каждый движок лениво загружается при первом использовании и кэшируется — при неудачной загрузке повторная попытка выполняется при следующем вызове.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const romanizer = createRomanizer();
|
|
121
|
+
|
|
122
|
+
// Переопределение пути CDN словаря Kuromoji (например, для самохостинга)
|
|
123
|
+
const romanizer = createRomanizer({
|
|
124
|
+
japaneseDictPath: 'https://my-cdn.com/kuromoji/dict',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Адаптеры движков
|
|
129
|
+
|
|
130
|
+
`options.engines` переопределяет встроенный движок для скрипта — или подключает движок к скрипту, для которого встроенного нет (`arabic`, `hebrew`, `malayalam`, `bengali`, `other`), чтобы `romanizeLines` мог обрабатывать любой скрипт через единый интерфейс. По умолчанию библиотека не выполняет **никакого сетевого ввода-вывода**; подключение удалённого адаптера — это явное решение вызывающей стороны.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const romanizer = createRomanizer({
|
|
134
|
+
engines: {
|
|
135
|
+
// Подключите свою внешнюю романизацию для скриптов без локального движка.
|
|
136
|
+
arabic: async (line) => myTransliterationApi(line),
|
|
137
|
+
// Или замените встроенный движок (например, подделкой в тестах).
|
|
138
|
+
korean: (line) => myKoreanRomanizer(line),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await romanizer.romanizeLines(['مرحبا']);
|
|
143
|
+
// { script: 'arabic', lines: [...], fallbacks: [false] } — больше не выбрасывает исключение
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Скрипты без движка — встроенного или внедрённого — по-прежнему выбрасывают `UnsupportedRomanizationError`.
|
|
147
|
+
|
|
148
|
+
### `detectScript(lines)`
|
|
149
|
+
|
|
150
|
+
Определяет доминирующий скрипт в данных строках. Сначала проверяет японские каны (определённо), затем оценивает все остальные скрипты по количеству символов.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
detectScript(['こんにちは']); // 'japanese'
|
|
154
|
+
detectScript(['你好世界']); // 'chinese'
|
|
155
|
+
detectScript(['Привет']); // 'cyrillic'
|
|
156
|
+
detectScript(['Hello world']); // 'latin'
|
|
157
|
+
detectScript(['123 ???']); // 'other'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `isLatinScript(lines)`
|
|
161
|
+
|
|
162
|
+
Быстрая проверка — возвращает `true`, если текст содержит только латинские буквы (без CJK, кириллицы, индийских и т.д.). Полезно для полного пропуска романизации.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
isLatinScript(['Hello world']); // true
|
|
166
|
+
isLatinScript(['안녕하세요']); // false
|
|
167
|
+
isLatinScript(['♪♪♪']); // false (нет букв)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `requiresExternalRomanization(script)`
|
|
171
|
+
|
|
172
|
+
Возвращает `true` для скриптов, у которых нет встроенного движка и которые требуют внешнего API. Импортируется из лёгкого подпути `lyric-romanizer/detector`, поэтому ответ на вопрос «стоит ли переключаться на внешний сервис?» не требует загрузки движков.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
requiresExternalRomanization('chinese'); // false
|
|
176
|
+
requiresExternalRomanization('arabic'); // true
|
|
177
|
+
requiresExternalRomanization('malayalam'); // true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `romanizer.romanizeLine(line, options?)`
|
|
181
|
+
|
|
182
|
+
Романизирует одну строку. Если `script` опущен, он автоопределяется **для каждой строки** через `detectScript` — цикл по `romanizeLine` может направлять каждую строку в разные движки, в отличие от `romanizeLines`, который закрепляет один скрипт за всем массивом. Латинский текст или контент без букв возвращается без изменений.
|
|
183
|
+
|
|
184
|
+
Для китайского текста опция `dialect` управляет системой романизации: `'mandarin'` (по умолчанию) использует пиньинь, `'cantonese'` использует [Jyutping](https://github.com/CanCLID/to-jyutping). Другие скрипты игнорируют `dialect`.
|
|
185
|
+
|
|
186
|
+
**Выбрасывает** `UnsupportedRomanizationError` для скриптов без движка (встроенного или внедрённого).
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
await romanizer.romanizeLine('你好世界');
|
|
190
|
+
// 'nǐ hǎo shì jiè' (по умолчанию: мандарин/пиньинь)
|
|
191
|
+
|
|
192
|
+
await romanizer.romanizeLine('你好', { dialect: 'cantonese' });
|
|
193
|
+
// 'nei5 hou2' (Jyutping)
|
|
194
|
+
|
|
195
|
+
await romanizer.romanizeLine('Привет мир');
|
|
196
|
+
// 'Privet mir'
|
|
197
|
+
|
|
198
|
+
await romanizer.romanizeLine('Hello world');
|
|
199
|
+
// 'Hello world' (без изменений)
|
|
200
|
+
|
|
201
|
+
await romanizer.romanizeLine('مرحبا');
|
|
202
|
+
// throws UnsupportedRomanizationError { script: 'arabic' }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `romanizer.romanizeLines(lines, options?)`
|
|
206
|
+
|
|
207
|
+
Параллельно романизирует несколько строк. Определяет доминирующий скрипт **один раз по всем строкам** и закрепляет его за каждой строкой (см. **Массивы со смешанными скриптами**). Возвращает скрипт, романизированные строки и флаги `fallbacks` для каждой строки — `true` там, где движок дал сбой и строка была транслитерирована универсальным способом в качестве крайней меры.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const { script, lines, fallbacks } = await romanizer.romanizeLines([
|
|
211
|
+
'สวัสดี',
|
|
212
|
+
'ชาวโลก',
|
|
213
|
+
]);
|
|
214
|
+
// { script: 'thai', lines: ['swasdi', 'chaolok'], fallbacks: [false, false] }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `UnsupportedRomanizationError`
|
|
218
|
+
|
|
219
|
+
Выбрасывается при попытке романизовать скрипт, у которого нет движка — встроенного или внедрённого через `options.engines`. Имеет свойство `script` для программной обработки.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
try {
|
|
223
|
+
await romanizer.romanizeLine('مرحبا');
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err instanceof UnsupportedRomanizationError) {
|
|
226
|
+
console.log(err.script); // 'arabic'
|
|
227
|
+
// откат к внешнему API
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Поддерживаемые скрипты
|
|
233
|
+
|
|
234
|
+
### Локальные (полностью оффлайн)
|
|
235
|
+
|
|
236
|
+
| Скрипт | Движок | Пример |
|
|
237
|
+
|--------|--------|--------|
|
|
238
|
+
| Универсальный *(fallback)* | [transliteration](https://github.com/nickclaw/transliteration) | `Привет` → `Privet` |
|
|
239
|
+
| Японский | [kuroshiro](https://github.com/sglkc/kuroshiro-ts) + [kuromoji](https://github.com/takuyaa/kuromoji.js) | `こんにちは` → `konnichiha` |
|
|
240
|
+
| Мандарин | [pinyin-pro](https://github.com/zh-lx/pinyin-pro) | `你好` → `nǐ hǎo` |
|
|
241
|
+
| Кантонский | [to-jyutping](https://github.com/CanCLID/to-jyutping) | `佢冇` → `keoi5 mou5` |
|
|
242
|
+
| Корейский | [@romanize/korean](https://github.com/kntng/romanize) | `안녕` → `annyeong` |
|
|
243
|
+
| Кириллица | [cyrillic-to-translit-js](https://github.com/greybax/CyrillicToTranslitJS) | `Привет` → `Privet` |
|
|
244
|
+
| Деванагари | [sanscript](https://github.com/indic-transliteration/sanscript) | `नमस्ते` → `namaste` |
|
|
245
|
+
| Гуджарати | [sanscript](https://github.com/indic-transliteration/sanscript) | `નમસ્તે` → `namaste` |
|
|
246
|
+
| Гурмукхи | [sanscript](https://github.com/indic-transliteration/sanscript) | `ਨਮਸਤੇ` → `namasate` |
|
|
247
|
+
| Телугу | [sanscript](https://github.com/indic-transliteration/sanscript) | `నమస్తే` → `namaste` |
|
|
248
|
+
| Каннада | [sanscript](https://github.com/indic-transliteration/sanscript) | `ನಮಸ್ತೇ` → `namaste` |
|
|
249
|
+
| Одия | [sanscript](https://github.com/indic-transliteration/sanscript) | `ନମସ୍ତେ` → `namaste` |
|
|
250
|
+
| Тамильский | [tamil-romanizer](https://github.com/haroldalan/tamil-romanizer) | `வணக்கம்` → `vanakkam` |
|
|
251
|
+
| Тайский | [@dehoist/romanize-thai](https://github.com/Dehoist/Open-Source) | `สวัสดี` → `swasdi` |
|
|
252
|
+
|
|
253
|
+
### Требуют внешний API
|
|
254
|
+
|
|
255
|
+
| Скрипт | Метод |
|
|
256
|
+
|--------|-------|
|
|
257
|
+
| Малаялам | Google Translate `dt=rm` |
|
|
258
|
+
| Бенгальский | Google Translate `dt=rm` |
|
|
259
|
+
| Арабский | Google Translate `dt=rm` |
|
|
260
|
+
| Иврит | Google Translate `dt=rm` |
|
|
261
|
+
| Другие | Google Translate `dt=rm` |
|
|
262
|
+
|
|
263
|
+
Используйте `requiresExternalRomanization()` для определения этих скриптов и переключения на предпочитаемый API — или подключите API один раз в виде **адаптера движка**, и `romanizeLines` обработает любой скрипт.
|
|
264
|
+
|
|
265
|
+
## Примечания по скриптам
|
|
266
|
+
|
|
267
|
+
### Массивы со смешанными скриптами
|
|
268
|
+
|
|
269
|
+
`romanizeLines` закрепляет **доминирующий** скрипт всего массива за каждой строкой. Это сделано намеренно: строка только с кандзи внутри японской песни сама по себе неотличима от китайской (кандзи и ханьцзы используют один и тот же блок Unicode), поэтому только контекст всего массива направляет её в правильный движок. Полезно знать о последствиях:
|
|
270
|
+
|
|
271
|
+
- Любая кана в любом месте массива закрепляет за всем массивом `japanese` — кана является неопровержимым доказательством.
|
|
272
|
+
- Строка на *другом* нелатинском скрипте внутри массива всё равно передаётся закреплённому движку.
|
|
273
|
+
- Строки на чистой латинице (английский припев внутри CJK-песни) **возвращаются без изменений**, а не передаются закреплённому движку.
|
|
274
|
+
- Флаги `fallbacks` для каждой строки сообщают, когда движок дал сбой и строка откатилась к универсальной транслитерации.
|
|
275
|
+
|
|
276
|
+
Если вам нужна настоящая маршрутизация движков для каждой строки, вызывайте `romanizeLine` в цикле — он определяет скрипт для каждой строки.
|
|
277
|
+
|
|
278
|
+
### Определение кириллицы
|
|
279
|
+
|
|
280
|
+
Кириллица автоматически определяет украиноспецифические символы (`і`, `ї`, `є`, `ґ`) и применяет украинский пресет транслитерации. Вся остальная кириллица по умолчанию обрабатывается как русская.
|
|
281
|
+
|
|
282
|
+
### Поддержка кантонского
|
|
283
|
+
|
|
284
|
+
Китайский текст по умолчанию использует мандарин (пиньинь). Передайте `dialect: 'cantonese'` в `RomanizeOptions`, чтобы романизировать китайский текст в [Jyutping](https://github.com/CanCLID/to-jyutping).
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const { lines } = await romanizer.romanizeLines(['你好世界', '食飯'], {
|
|
288
|
+
script: 'chinese',
|
|
289
|
+
dialect: 'cantonese',
|
|
290
|
+
});
|
|
291
|
+
// ['nei5 hou2 sai3 gaai3', 'sik6 faan6']
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Лицензия
|
|
295
|
+
|
|
296
|
+
MIT
|